Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
7228b0a
Cli: Add login
alavarello Dec 29, 2025
dd59ab3
Fix one line if and comments
alavarello Dec 30, 2025
c399fb2
Change missing key for api-key
alavarello Dec 30, 2025
140d1e1
Add user action when overriding the profile
alavarello Dec 30, 2025
3155d1e
Fix hardcoded default
alavarello Dec 30, 2025
4cc7d0d
Add missing default
alavarello Dec 30, 2025
58ee647
Add helpers for moving credentials
alavarello Dec 30, 2025
531eeb2
Fix deployment empty message
alavarello Dec 30, 2025
a9dc876
Merge branch 'main' into cli/add-login
alavarello Dec 30, 2025
eebb1a8
Fix test
alavarello Dec 30, 2025
f04cb86
Update packages/cli/tests/commands/deploy.spec.ts
alavarello Dec 30, 2025
45f372c
Merge branch 'main' into cli/add-login
alavarello Dec 30, 2025
f905a8f
Add changeset
alavarello Dec 30, 2025
2128f78
Fix login command and deploy error
alavarello Dec 30, 2025
e03d675
Update packages/cli/src/commands/login.ts
alavarello Jan 2, 2026
72deee7
Update packages/cli/src/commands/login.ts
alavarello Jan 2, 2026
82c6e1a
Add default const
alavarello Jan 2, 2026
e294ca8
Update packages/cli/tests/credentials.spec.ts
alavarello Jan 2, 2026
a34f71c
Update packages/cli/tests/commands/profiles.spec.ts
alavarello Jan 2, 2026
5d96d0d
Update packages/cli/tests/credentials.spec.ts
alavarello Jan 2, 2026
52fdeef
Update packages/cli/tests/commands/profiles.spec.ts
alavarello Jan 2, 2026
cdda09c
Update packages/cli/src/lib/CredentialsManager.ts
alavarello Jan 2, 2026
58df58a
Update packages/cli/src/lib/CredentialsManager.ts
alavarello Jan 2, 2026
9a7b71f
Update packages/cli/tests/commands/login.spec.ts
alavarello Jan 2, 2026
ea1f64c
Update packages/cli/tests/commands/login.spec.ts
alavarello Jan 2, 2026
062fba0
Update packages/cli/tests/commands/logout.spec.ts
alavarello Jan 2, 2026
7447ec0
Update packages/cli/tests/commands/logout.spec.ts
alavarello Jan 2, 2026
b32590a
chore: add braces to multi-line ifs
lgalende Jan 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/seven-boxes-chew.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@mimicprotocol/cli": patch
"@mimicprotocol/lib-ts": patch
"@mimicprotocol/test-ts": patch
---

Add login, logout and profiles commands
72 changes: 69 additions & 3 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,10 @@

---

## Content
## Content

The `mimic` CLI is a command-line interface to:

- Initialize a Mimic-compatible task project
- Generate types from your task manifest and ABIs
- Compile your AssemblyScript tasks to WebAssembly
Expand All @@ -35,7 +36,7 @@ The `mimic` CLI is a command-line interface to:

## Setup

To set up this project you'll need [git](https://git-scm.com) and [yarn](https://classic.yarnpkg.com) installed.
To set up this project you'll need [git](https://git-scm.com) and [yarn](https://classic.yarnpkg.com) installed.

Install the CLI from the root of the monorepo:

Expand All @@ -59,13 +60,79 @@ USAGE
$ mimic [COMMAND]

COMMANDS
login Authenticate with Mimic by storing your API key locally
logout Remove stored credentials for a profile
profiles List all configured authentication profiles
codegen Generates typed interfaces for declared inputs and ABIs from your manifest.yaml file
compile Compiles task
test Tests your tasks
deploy Uploads your compiled task artifacts to IPFS and registers it into the Mimic Registry
init Initializes a new Mimic-compatible project structure in the specified directory
```

### Authentication

Before deploying tasks, you need to authenticate with your Mimic API key:

```bash
# Interactive login (recommended)
$ mimic login

# Non-interactive login (for CI/CD)
$ mimic login --api-key YOUR_API_KEY

# Login with a specific profile
$ mimic login --profile staging -api-key YOUR_API_KEY
```

#### Managing Profiles

The CLI supports multiple authentication profiles. Credentials are stored in `~/.mimic/credentials`.

```bash
# List all configured profiles
$ mimic profiles

# Login with a specific profile
$ mimic login --profile production

# Deploy using a specific profile
$ mimic deploy --profile production

# Remove credentials for a profile
$ mimic logout --profile staging
```

#### Credential Storage

Credentials are stored in an INI-style format at `~/.mimic/credentials`:

```ini
[default]
api_key=YOUR_DEFAULT_KEY

[staging]
api_key=YOUR_STAGING_KEY

[production]
api_key=YOUR_PRODUCTION_KEY
```

#### Deploy with Authentication

The `deploy` command now supports profile-based authentication:

```bash
# Deploy using default profile
$ mimic deploy

# Deploy using a specific profile
$ mimic deploy --profile staging

# Deploy with explicit API key (overrides profile)
$ mimic deploy --api-key YOUR_API_KEY
```

For full CLI documentation and examples please visit [docs.mimic.fi](https://docs.mimic.fi/)

## Security
Expand All @@ -86,7 +153,6 @@ This project includes code from [The Graph Tooling](https://github.com/graphprot
See the [LICENSE-MIT](https://github.com/graphprotocol/graph-tooling/blob/27659e56adfa3ef395ceaf39053dc4a31e6d86b7/LICENSE-MIT) file for details.
Their original license and attribution are preserved.


---

> Website [mimic.fi](https://mimic.fi)  · 
Expand Down
53 changes: 53 additions & 0 deletions packages/cli/src/commands/authenticate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { Command, Flags } from '@oclif/core'

import { CredentialsManager, ProfileCredentials } from '../lib/CredentialsManager'
import log from '../log'

export default class Authenticate extends Command {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
run(): Promise<any> {
throw new Error('Method not implemented.')
}

static override description = 'Authenticate with Mimic by storing your API key locally'

static override examples = [
'<%= config.bin %> <%= command.id %>',
'<%= config.bin %> <%= command.id %> --profile staging',
'<%= config.bin %> <%= command.id %> --profile production --api-key YOUR_API_KEY',
]

static flags = {
profile: Flags.string({
char: 'p',
description: 'Profile name to use for this credential',
}),
'api-key': Flags.string({
char: 'k',
description: 'API key (non-interactive mode)',
}),
}

protected authenticate(flags: { profile?: string; 'api-key'?: string }): ProfileCredentials {
let apiKey = flags['api-key']
if (!apiKey) {
try {
const credentials = CredentialsManager.getDefault().getCredentials(flags.profile)
apiKey = credentials.apiKey
} catch (error) {
if (error instanceof Error) {
this.error(error.message, {
code: 'AuthenticationRequired',
suggestions: [
`Run ${log.highlightText('mimic login')} to authenticate`,
`Run ${log.highlightText(`mimic login --profile ${flags.profile ?? '<profile>'}`)} to create this profile`,
`Or use ${log.highlightText('--api-key')} flag to provide API key directly`,
].filter(Boolean) as string[],
})
}
throw error
}
}
return { apiKey }
}
}
29 changes: 21 additions & 8 deletions packages/cli/src/commands/deploy.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,29 @@
import { Command, Flags } from '@oclif/core'
import { Flags } from '@oclif/core'
import axios, { AxiosError } from 'axios'
import FormData from 'form-data'
import * as fs from 'fs'
import { join, resolve } from 'path'

import { GENERIC_SUGGESTION } from '../errors'
import { ProfileCredentials } from '../lib/CredentialsManager'
import { execBinCommand } from '../lib/packageManager'
import log from '../log'

import Authenticate from './authenticate'

const MIMIC_REGISTRY_DEFAULT = 'https://api-protocol.mimic.fi'

export default class Deploy extends Command {
export default class Deploy extends Authenticate {
static override description = 'Uploads your compiled task artifacts to IPFS and registers it into the Mimic Registry'

static override examples = ['<%= config.bin %> <%= command.id %> --input ./dist --key MY_KEY --output ./dist']
static override examples = [
'<%= config.bin %> <%= command.id %> --input ./dist --output ./dist',
'<%= config.bin %> <%= command.id %> --profile staging',
'<%= config.bin %> <%= command.id %> --api-key MY_KEY --input ./dist --output ./dist',
]

static override flags = {
key: Flags.string({ char: 'k', description: 'Your account deployment key', required: true }),
...Authenticate.flags,
input: Flags.string({ char: 'i', description: 'Directory containing the compiled artifacts', default: './build' }),
output: Flags.string({ char: 'o', description: 'Output directory for deployment CID', default: './build' }),
url: Flags.string({ char: 'u', description: `Mimic Registry base URL`, default: MIMIC_REGISTRY_DEFAULT }),
Expand All @@ -25,10 +32,12 @@ export default class Deploy extends Command {

public async run(): Promise<void> {
const { flags } = await this.parse(Deploy)
const { key, input: inputDir, output: outputDir, 'skip-compile': skipCompile, url: registryUrl } = flags
const { input: inputDir, output: outputDir, 'skip-compile': skipCompile, url: registryUrl } = flags
const fullInputDir = resolve(inputDir)
const fullOutputDir = resolve(outputDir)

let credentials = this.authenticate(flags)

if (!skipCompile) {
const codegen = execBinCommand('mimic', ['codegen'], process.cwd())
if (codegen.status !== 0)
Expand Down Expand Up @@ -57,7 +66,7 @@ export default class Deploy extends Command {
}

log.startAction('Uploading to Mimic Registry')
const CID = await this.uploadToRegistry(neededFiles, key, registryUrl)
const CID = await this.uploadToRegistry(neededFiles, credentials, registryUrl)
console.log(`IPFS CID: ${log.highlightText(CID)}`)
log.stopAction()

Expand All @@ -67,12 +76,16 @@ export default class Deploy extends Command {
console.log(`Task deployed!`)
}

private async uploadToRegistry(files: string[], key: string, registryUrl: string): Promise<string> {
private async uploadToRegistry(
files: string[],
credentials: ProfileCredentials,
registryUrl: string
): Promise<string> {
try {
const form = filesToForm(files)
const { data } = await axios.post(`${registryUrl}/tasks`, form, {
headers: {
'x-api-key': key,
'x-api-key': credentials.apiKey,
'Content-Type': `multipart/form-data; boundary=${form.getBoundary()}`,
},
})
Expand Down
105 changes: 105 additions & 0 deletions packages/cli/src/commands/login.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { confirm, input, password } from '@inquirer/prompts'
import { Flags } from '@oclif/core'

import { CredentialsManager } from '../lib/CredentialsManager'
import log from '../log'

import Authenticate from './authenticate'

export default class Login extends Authenticate {
static override description = 'Authenticate with Mimic by storing your API key locally'

static override examples = [
'<%= config.bin %> <%= command.id %>',
'<%= config.bin %> <%= command.id %> --profile staging',
'<%= config.bin %> <%= command.id %> --profile production --api-key YOUR_API_KEY',
]

static override flags = {
...Authenticate.flags,
'force-login': Flags.boolean({
char: 'f',
description: 'Force login even if profile exists',
default: false,
}),
}

public async run(): Promise<void> {
const { flags } = await this.parse(Login)
const { profile: profileInput, 'api-key': apiKeyFlag } = flags

let apiKey: string
let profileName = profileInput

// Non-interactive mode
if (apiKeyFlag) apiKey = apiKeyFlag
else {
// Interactive mode
try {
apiKey = await password({
message: 'Enter your API key:',
mask: '*',
validate: (value) => {
if (!value || value.trim() === '') return 'API key cannot be empty'
return true
},
})

if (!profileName) {
profileName = await input({
message: `Enter a profile name (press Enter for "${CredentialsManager.getDefaultProfileName()}"):`,
default: CredentialsManager.getDefaultProfileName(),
validate: (value) => {
if (!value || value.trim() === '') return 'Profile name cannot be empty'
if (value.includes('[') || value.includes(']') || value.includes('=')) {
return 'Profile name cannot contain [, ], or = characters'
}
return true
},
})
}
} catch (error) {
if (error instanceof Error && error.message.includes('User force closed')) {
console.log('\nLogin cancelled')
this.exit(0)
}
throw error
}
}

this.saveAndConfirm(profileName || CredentialsManager.getDefaultProfileName(), apiKey, flags['force-login'])
}

private async saveAndConfirm(profileName: string, apiKey: string, forceLogin: boolean): Promise<void> {
try {
const credentialsManager = CredentialsManager.getDefault()

if (credentialsManager.profileExists(profileName) && !forceLogin) {
const shouldOverwrite = await confirm({
message: `Profile ${log.highlightText(profileName)} already exists. Overwrite?`,
default: false,
})

if (!shouldOverwrite) {
console.log('Login cancelled')
return
}
}

log.startAction('Saving credentials')
credentialsManager.saveProfile(profileName, apiKey)
log.stopAction()

console.log(`✓ Credentials saved for profile ${log.highlightText(profileName)}`)
console.log(` Location: ${log.highlightText('~/.mimic/credentials')}`)
console.log()
console.log(`You can now deploy tasks using: ${log.highlightText('mimic deploy')}`)
if (profileName !== CredentialsManager.getDefaultProfileName()) {
console.log(`Or with your profile: ${log.highlightText(`mimic deploy --profile ${profileName}`)}`)
}
} catch (error) {
if (error instanceof Error) this.error(`Failed to save credentials: ${error.message}`)
throw error
}
}
}
Loading