From 6ff130b5edc226c7d895121152bff6048b811d46 Mon Sep 17 00:00:00 2001 From: Agustincito Date: Wed, 4 Feb 2026 16:25:57 -0300 Subject: [PATCH 1/8] Cli: Refactor credentials commands --- packages/cli/src/commands/authenticate.ts | 37 ++++++++++++++++++++-- packages/cli/src/commands/init.ts | 20 ++++++++---- packages/cli/src/commands/login.ts | 25 +++++++++++---- packages/cli/src/commands/logout.ts | 15 +++++++-- packages/cli/src/commands/profiles.ts | 10 +++++- packages/cli/src/lib/CredentialsManager.ts | 9 ------ packages/cli/tests/credentials.spec.ts | 22 ------------- 7 files changed, 88 insertions(+), 50 deletions(-) diff --git a/packages/cli/src/commands/authenticate.ts b/packages/cli/src/commands/authenticate.ts index 1385b687..5414bbe5 100644 --- a/packages/cli/src/commands/authenticate.ts +++ b/packages/cli/src/commands/authenticate.ts @@ -1,4 +1,5 @@ import { Command, Flags } from '@oclif/core' +import * as fs from 'fs' import { CredentialsManager, ProfileCredentials } from '../lib/CredentialsManager' import log from '../log' @@ -33,13 +34,45 @@ export default class Authenticate extends Command { public static authenticate(cmd: Command, flags: AuthenticateFlags): ProfileCredentials { let apiKey = flags['api-key'] + const profileName = flags.profile || CredentialsManager.getDefaultProfileName() + + const credentialsManager = CredentialsManager.getDefault() if (!apiKey) { try { - const credentials = CredentialsManager.getDefault().getCredentials(flags.profile) + const credentialsDir = credentialsManager.getBaseDir() + const credentialsPath = credentialsManager.getCredentialsPath() + + if (!fs.existsSync(credentialsDir)) { + throw new Error(`No credentials directory found at ${credentialsDir}. Run 'mimic login' to authenticate.`) + } + + if (!fs.existsSync(credentialsPath)) { + throw new Error(`No credentials file found. Run 'mimic login' to authenticate.`) + } + + const profiles = credentialsManager.readCredentials() + + if (!profiles[profileName]) { + const availableProfiles = Object.keys(profiles) + const suggestion = + availableProfiles.length > 0 + ? `Available profiles: ${availableProfiles.join(', ')}` + : `No profiles found. Run 'mimic login' to create one.` + + throw new Error(`Profile '${profileName}' not found. ${suggestion}`) + } + + const credentials = profiles[profileName] + + if (!credentials.apiKey || credentials.apiKey.trim() === '') { + throw new Error( + `Profile '${profileName}' has no API key. Run 'mimic login --profile ${profileName}' to update credentials.` + ) + } apiKey = credentials.apiKey } catch (error) { if (error instanceof Error) { - cmd.error(error.message, { + cmd.error(`Authentication required: ${error.message}`, { code: 'AuthenticationRequired', suggestions: [ `Run ${log.highlightText('mimic login')} to authenticate`, diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts index 5daf54c4..01a7437c 100644 --- a/packages/cli/src/commands/init.ts +++ b/packages/cli/src/commands/init.ts @@ -6,6 +6,9 @@ import simpleGit from 'simple-git' import { execBinCommand, installDependencies } from '../lib/packageManager' import log from '../log' +import { FlagsType } from '../types' + +export type InitFlags = FlagsType & { directory: string } export default class Init extends Command { static override description = 'Initializes a new Mimic-compatible project structure in the specified directory' @@ -22,8 +25,11 @@ export default class Init extends Command { public async run(): Promise { const { args, flags } = await this.parse(Init) - const { directory } = args - const { force } = flags + await Init.init(this, { ...flags, ...args }) + } + + public static async init(cmd: Command, flags: InitFlags): Promise { + const { directory, force } = flags const absDir = path.resolve(directory) if (force && fs.existsSync(absDir) && fs.readdirSync(absDir).length > 0) { @@ -37,7 +43,7 @@ export default class Init extends Command { if (!shouldDelete) { console.log('You can remove the --force flag from your command') console.log('Stopping initialization...') - this.exit(0) + cmd.exit(0) } log.startAction(`Deleting contents of ${absDir}`) // Delete files individually instead of removing the entire directory to preserve @@ -51,7 +57,7 @@ export default class Init extends Command { log.startAction('Creating files') if (fs.existsSync(absDir) && fs.readdirSync(absDir).length > 0) { - this.error(`Directory ${log.highlightText(absDir)} is not empty`, { + cmd.error(`Directory ${log.highlightText(absDir)} is not empty`, { code: 'DirectoryNotEmpty', suggestions: [ 'You can specify the directory as a positional argument', @@ -70,7 +76,7 @@ export default class Init extends Command { const gitDir = path.join(absDir, '.git') if (fs.existsSync(gitDir)) fs.rmSync(gitDir, { recursive: true, force: true }) } catch (error) { - this.error(`Failed to clone template repository. Details: ${error}`) + cmd.error(`Failed to clone template repository. Details: ${error}`) } this.installDependencies(absDir) @@ -79,12 +85,12 @@ export default class Init extends Command { console.log('New project initialized!') } - installDependencies(absDir: string) { + public static installDependencies(absDir: string) { if (process.env.NODE_ENV === 'test') return installDependencies(absDir) } - runCodegen(absDir: string) { + public static runCodegen(absDir: string) { if (process.env.NODE_ENV === 'test') return execBinCommand('mimic', ['codegen'], absDir) } diff --git a/packages/cli/src/commands/login.ts b/packages/cli/src/commands/login.ts index 0033619f..b5dabdcb 100644 --- a/packages/cli/src/commands/login.ts +++ b/packages/cli/src/commands/login.ts @@ -1,12 +1,15 @@ import { confirm, input, password } from '@inquirer/prompts' -import { Flags } from '@oclif/core' +import { Command, Flags } from '@oclif/core' import { CredentialsManager } from '../lib/CredentialsManager' import log from '../log' +import { FlagsType } from '../types' import Authenticate from './authenticate' -export default class Login extends Authenticate { +export type LoginFlags = FlagsType + +export default class Login extends Command { static override description = 'Authenticate with Mimic by storing your API key locally' static override examples = [ @@ -26,6 +29,11 @@ export default class Login extends Authenticate { public async run(): Promise { const { flags } = await this.parse(Login) + + await Login.login(this, flags) + } + + public static async login(cmd: Command, flags: LoginFlags): Promise { const { profile: profileInput, 'api-key': apiKeyFlag } = flags let apiKey: string @@ -61,16 +69,21 @@ export default class Login extends Authenticate { } catch (error) { if (error instanceof Error && error.message.includes('User force closed')) { console.log('\nLogin cancelled') - this.exit(0) + cmd.exit(0) } throw error } } - this.saveAndConfirm(profileName || CredentialsManager.getDefaultProfileName(), apiKey, flags['force-login']) + Login.saveAndConfirm(cmd, profileName || CredentialsManager.getDefaultProfileName(), apiKey, flags['force-login']) } - private async saveAndConfirm(profileName: string, apiKey: string, forceLogin: boolean): Promise { + private static async saveAndConfirm( + cmd: Command, + profileName: string, + apiKey: string, + forceLogin: boolean + ): Promise { try { const credentialsManager = CredentialsManager.getDefault() @@ -98,7 +111,7 @@ export default class Login extends Authenticate { 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}`) + if (error instanceof Error) cmd.error(`Failed to save credentials: ${error.message}`) throw error } } diff --git a/packages/cli/src/commands/logout.ts b/packages/cli/src/commands/logout.ts index 01e2a26c..c62c1c1d 100644 --- a/packages/cli/src/commands/logout.ts +++ b/packages/cli/src/commands/logout.ts @@ -3,6 +3,9 @@ import { Command, Flags } from '@oclif/core' import { CredentialsManager } from '../lib/CredentialsManager' import log from '../log' +import { FlagsType } from '../types' + +export type LogoutFlags = FlagsType export default class Logout extends Command { static override description = 'Remove stored credentials for a profile' @@ -27,11 +30,17 @@ export default class Logout extends Command { public async run(): Promise { const { flags } = await this.parse(Logout) + + await Logout.logout(this, flags) + } + + public static async logout(cmd: Command, flags: LogoutFlags): Promise { const { profile: profileName, force } = flags - const profiles = CredentialsManager.getDefault().getProfiles() + const credentials = CredentialsManager.getDefault().readCredentials() + const profiles = Object.keys(credentials) if (!profiles.includes(profileName)) { - this.error(`Profile '${profileName}' does not exist`, { + cmd.error(`Profile '${profileName}' does not exist`, { code: 'ProfileNotFound', suggestions: profiles.length > 0 @@ -48,7 +57,7 @@ export default class Logout extends Command { if (!shouldRemove) { console.log('Logout cancelled') - this.exit(0) + cmd.exit(0) } } diff --git a/packages/cli/src/commands/profiles.ts b/packages/cli/src/commands/profiles.ts index 9ff24367..e4e483ae 100644 --- a/packages/cli/src/commands/profiles.ts +++ b/packages/cli/src/commands/profiles.ts @@ -2,6 +2,9 @@ import { Command } from '@oclif/core' import { CredentialsManager } from '../lib/CredentialsManager' import log from '../log' +import { FlagsType } from '../types' + +export type ProfilesFlags = FlagsType export default class Profiles extends Command { static override description = 'List all configured authentication profiles' @@ -9,7 +12,12 @@ export default class Profiles extends Command { static override examples = ['<%= config.bin %> <%= command.id %>'] public async run(): Promise { - const profiles = CredentialsManager.getDefault().getProfiles() + await Profiles.profiles() + } + + public static async profiles(): Promise { + const credentials = CredentialsManager.getDefault().readCredentials() + const profiles = Object.keys(credentials) if (profiles.length === 0) { console.log('No profiles found.') diff --git a/packages/cli/src/lib/CredentialsManager.ts b/packages/cli/src/lib/CredentialsManager.ts index 9cc8690c..639e84bb 100644 --- a/packages/cli/src/lib/CredentialsManager.ts +++ b/packages/cli/src/lib/CredentialsManager.ts @@ -156,15 +156,6 @@ export class CredentialsManager { return credentials } - getCredentials(profileName: string = DEFAULT_PROFILE): ProfileCredentials { - try { - return this.getProfile(profileName) - } catch (error) { - if (error instanceof Error) throw new Error(`Authentication required: ${error.message}`) - throw error - } - } - getProfiles(): string[] { const profiles = this.readCredentials() return Object.keys(profiles) diff --git a/packages/cli/tests/credentials.spec.ts b/packages/cli/tests/credentials.spec.ts index 3af5e151..64fbaa50 100644 --- a/packages/cli/tests/credentials.spec.ts +++ b/packages/cli/tests/credentials.spec.ts @@ -300,28 +300,6 @@ api_key=staging-key }) }) - describe('ensureLoggedIn', () => { - it('should return credentials if profile exists', () => { - credentialsManager.saveProfile(DEFAULT_PROFILE, 'test-key') - - const credentials = credentialsManager.getCredentials(DEFAULT_PROFILE) - - expect(credentials).to.deep.equal({ apiKey: 'test-key' }) - }) - - it('should throw error with user-friendly message if not logged in', () => { - expect(() => credentialsManager.getCredentials(DEFAULT_PROFILE)).to.throw(/Authentication required/) - }) - - it('should default to "default" profile', () => { - credentialsManager.saveProfile(DEFAULT_PROFILE, 'test-key') - - const credentials = credentialsManager.getCredentials() - - expect(credentials.apiKey).to.equal('test-key') - }) - }) - describe('listProfiles', () => { it('should return empty array if no profiles exist', () => { const profiles = credentialsManager.getProfiles() From b705dde7eb019c260d324e653dcdcd0ada719f09 Mon Sep 17 00:00:00 2001 From: Agustincito Date: Thu, 5 Feb 2026 10:31:06 -0300 Subject: [PATCH 2/8] Add changeset --- .changeset/swift-parks-jump.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/swift-parks-jump.md diff --git a/.changeset/swift-parks-jump.md b/.changeset/swift-parks-jump.md new file mode 100644 index 00000000..c7543707 --- /dev/null +++ b/.changeset/swift-parks-jump.md @@ -0,0 +1,7 @@ +--- +"@mimicprotocol/cli": patch +"@mimicprotocol/lib-ts": patch +"@mimicprotocol/test-ts": patch +--- + +Refactor credentials commands From 145ea5ebb8c6c20b67432e8d6a55cdb8e34824fb Mon Sep 17 00:00:00 2001 From: Agustincito Date: Thu, 5 Feb 2026 10:53:29 -0300 Subject: [PATCH 3/8] Refactor credentials manager methods --- packages/cli/src/commands/authenticate.ts | 94 +++++++++++----------- packages/cli/src/commands/logout.ts | 3 +- packages/cli/src/commands/profiles.ts | 3 +- packages/cli/src/lib/CredentialsManager.ts | 35 -------- packages/cli/tests/credentials.spec.ts | 58 ------------- 5 files changed, 49 insertions(+), 144 deletions(-) diff --git a/packages/cli/src/commands/authenticate.ts b/packages/cli/src/commands/authenticate.ts index 5414bbe5..83f88f2d 100644 --- a/packages/cli/src/commands/authenticate.ts +++ b/packages/cli/src/commands/authenticate.ts @@ -33,57 +33,57 @@ export default class Authenticate extends Command { } public static authenticate(cmd: Command, flags: AuthenticateFlags): ProfileCredentials { - let apiKey = flags['api-key'] + const apiKey = flags['api-key'] const profileName = flags.profile || CredentialsManager.getDefaultProfileName() const credentialsManager = CredentialsManager.getDefault() - if (!apiKey) { - try { - const credentialsDir = credentialsManager.getBaseDir() - const credentialsPath = credentialsManager.getCredentialsPath() - - if (!fs.existsSync(credentialsDir)) { - throw new Error(`No credentials directory found at ${credentialsDir}. Run 'mimic login' to authenticate.`) - } - - if (!fs.existsSync(credentialsPath)) { - throw new Error(`No credentials file found. Run 'mimic login' to authenticate.`) - } - - const profiles = credentialsManager.readCredentials() - - if (!profiles[profileName]) { - const availableProfiles = Object.keys(profiles) - const suggestion = - availableProfiles.length > 0 - ? `Available profiles: ${availableProfiles.join(', ')}` - : `No profiles found. Run 'mimic login' to create one.` - - throw new Error(`Profile '${profileName}' not found. ${suggestion}`) - } - - const credentials = profiles[profileName] - - if (!credentials.apiKey || credentials.apiKey.trim() === '') { - throw new Error( - `Profile '${profileName}' has no API key. Run 'mimic login --profile ${profileName}' to update credentials.` - ) - } - apiKey = credentials.apiKey - } catch (error) { - if (error instanceof Error) { - cmd.error(`Authentication required: ${error.message}`, { - code: 'AuthenticationRequired', - suggestions: [ - `Run ${log.highlightText('mimic login')} to authenticate`, - `Run ${log.highlightText(`mimic login --profile ${flags.profile ?? ''}`)} to create this profile`, - `Or use ${log.highlightText('--api-key')} flag to provide API key directly`, - ].filter(Boolean) as string[], - }) - } - throw error + + if (apiKey) return { apiKey } + + try { + const credentialsDir = credentialsManager.getBaseDir() + const credentialsPath = credentialsManager.getCredentialsPath() + + if (!fs.existsSync(credentialsDir)) { + throw new Error(`No credentials directory found at ${credentialsDir}. Run 'mimic login' to authenticate.`) + } + + if (!fs.existsSync(credentialsPath)) { + throw new Error(`No credentials file found. Run 'mimic login' to authenticate.`) + } + + const profiles = credentialsManager.readCredentials() + + if (!profiles[profileName]) { + const availableProfiles = Object.keys(profiles) + const suggestion = + availableProfiles.length > 0 + ? `Available profiles: ${availableProfiles.join(', ')}` + : `No profiles found. Run 'mimic login' to create one.` + + throw new Error(`Profile '${profileName}' not found. ${suggestion}`) + } + + const credentials = profiles[profileName] + + if (!credentials.apiKey || credentials.apiKey.trim() === '') { + throw new Error( + `Profile '${profileName}' has no API key. Run 'mimic login --profile ${profileName}' to update credentials.` + ) + } + return { apiKey: credentials.apiKey } + } catch (error) { + if (error instanceof Error) { + cmd.error(`Authentication required: ${error.message}`, { + code: 'AuthenticationRequired', + suggestions: [ + `Run ${log.highlightText('mimic login')} to authenticate`, + `Run ${log.highlightText(`mimic login --profile ${flags.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 } } } diff --git a/packages/cli/src/commands/logout.ts b/packages/cli/src/commands/logout.ts index c62c1c1d..0c6aa332 100644 --- a/packages/cli/src/commands/logout.ts +++ b/packages/cli/src/commands/logout.ts @@ -37,8 +37,7 @@ export default class Logout extends Command { public static async logout(cmd: Command, flags: LogoutFlags): Promise { const { profile: profileName, force } = flags - const credentials = CredentialsManager.getDefault().readCredentials() - const profiles = Object.keys(credentials) + const profiles = CredentialsManager.getDefault().getProfiles() if (!profiles.includes(profileName)) { cmd.error(`Profile '${profileName}' does not exist`, { code: 'ProfileNotFound', diff --git a/packages/cli/src/commands/profiles.ts b/packages/cli/src/commands/profiles.ts index e4e483ae..cc684df0 100644 --- a/packages/cli/src/commands/profiles.ts +++ b/packages/cli/src/commands/profiles.ts @@ -16,8 +16,7 @@ export default class Profiles extends Command { } public static async profiles(): Promise { - const credentials = CredentialsManager.getDefault().readCredentials() - const profiles = Object.keys(credentials) + const profiles = CredentialsManager.getDefault().getProfiles() if (profiles.length === 0) { console.log('No profiles found.') diff --git a/packages/cli/src/lib/CredentialsManager.ts b/packages/cli/src/lib/CredentialsManager.ts index 639e84bb..43e45eb2 100644 --- a/packages/cli/src/lib/CredentialsManager.ts +++ b/packages/cli/src/lib/CredentialsManager.ts @@ -121,41 +121,6 @@ export class CredentialsManager { this.writeCredentials(profiles) } - getProfile(profileName: string = DEFAULT_PROFILE): ProfileCredentials { - const credentialsDir = this.getBaseDir() - const credentialsPath = this.getCredentialsPath() - - if (!fs.existsSync(credentialsDir)) { - throw new Error(`No credentials directory found at ${credentialsDir}. Run 'mimic login' to authenticate.`) - } - - if (!fs.existsSync(credentialsPath)) { - throw new Error(`No credentials file found. Run 'mimic login' to authenticate.`) - } - - const profiles = this.readCredentials() - - if (!profiles[profileName]) { - const availableProfiles = Object.keys(profiles) - const suggestion = - availableProfiles.length > 0 - ? `Available profiles: ${availableProfiles.join(', ')}` - : `No profiles found. Run 'mimic login' to create one.` - - throw new Error(`Profile '${profileName}' not found. ${suggestion}`) - } - - const credentials = profiles[profileName] - - if (!credentials.apiKey || credentials.apiKey.trim() === '') { - throw new Error( - `Profile '${profileName}' has no API key. Run 'mimic login --profile ${profileName}' to update credentials.` - ) - } - - return credentials - } - getProfiles(): string[] { const profiles = this.readCredentials() return Object.keys(profiles) diff --git a/packages/cli/tests/credentials.spec.ts b/packages/cli/tests/credentials.spec.ts index 64fbaa50..eb66400a 100644 --- a/packages/cli/tests/credentials.spec.ts +++ b/packages/cli/tests/credentials.spec.ts @@ -242,64 +242,6 @@ api_key=staging-key }) }) - describe('getProfile', () => { - it('should throw error if credentials directory does not exist', () => { - const credDir = credentialsManager.getBaseDir() - if (fs.existsSync(credDir)) { - fs.rmSync(credDir, { recursive: true, force: true }) - } - expect(() => credentialsManager.getProfile(DEFAULT_PROFILE)).to.throw(/No credentials directory found/) - }) - - it('should throw error if credentials file does not exist', () => { - credentialsManager.createCredentialsDirIfNotExists() - expect(() => credentialsManager.getProfile(DEFAULT_PROFILE)).to.throw(/No credentials file found/) - }) - - it('should throw error if profile does not exist', () => { - credentialsManager.saveProfile(DEFAULT_PROFILE, 'test-key') - expect(() => credentialsManager.getProfile('nonexistent')).to.throw(/Profile 'nonexistent' not found/) - }) - - it('should include available profiles in error message', () => { - credentialsManager.saveProfile(DEFAULT_PROFILE, 'test-key') - credentialsManager.saveProfile('staging', 'staging-key') - - try { - credentialsManager.getProfile('nonexistent') - expect.fail('Should have thrown an error') - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } catch (error: any) { - expect(error.message).to.include(DEFAULT_PROFILE) - expect(error.message).to.include('staging') - } - }) - - it('should throw error if api_key is empty', () => { - credentialsManager.createCredentialsDirIfNotExists() - const credentialsPath = credentialsManager.getCredentialsPath() - fs.writeFileSync(credentialsPath, `[${DEFAULT_PROFILE}]\napi_key=\n`) - - expect(() => credentialsManager.getProfile(DEFAULT_PROFILE)).to.throw(/has no API key/) - }) - - it('should return profile credentials if valid', () => { - credentialsManager.saveProfile(DEFAULT_PROFILE, 'test-key-123') - - const credentials = credentialsManager.getProfile(DEFAULT_PROFILE) - - expect(credentials).to.deep.equal({ apiKey: 'test-key-123' }) - }) - - it('should default to "default" profile if no name provided', () => { - credentialsManager.saveProfile(DEFAULT_PROFILE, 'default-key') - - const credentials = credentialsManager.getProfile() - - expect(credentials.apiKey).to.equal('default-key') - }) - }) - describe('listProfiles', () => { it('should return empty array if no profiles exist', () => { const profiles = credentialsManager.getProfiles() From 3ed486f54b0932616c6e5462ac1c2129c94f4c03 Mon Sep 17 00:00:00 2001 From: Agustincito Date: Thu, 5 Feb 2026 11:47:09 -0300 Subject: [PATCH 4/8] Add authenticate tests --- packages/cli/src/commands/login.ts | 1 - packages/cli/src/commands/logout.ts | 1 - .../cli/tests/commands/authenticate.spec.ts | 107 ++++++++++++++++++ 3 files changed, 107 insertions(+), 2 deletions(-) create mode 100644 packages/cli/tests/commands/authenticate.spec.ts diff --git a/packages/cli/src/commands/login.ts b/packages/cli/src/commands/login.ts index b5dabdcb..47e372a8 100644 --- a/packages/cli/src/commands/login.ts +++ b/packages/cli/src/commands/login.ts @@ -29,7 +29,6 @@ export default class Login extends Command { public async run(): Promise { const { flags } = await this.parse(Login) - await Login.login(this, flags) } diff --git a/packages/cli/src/commands/logout.ts b/packages/cli/src/commands/logout.ts index 0c6aa332..6beab3b6 100644 --- a/packages/cli/src/commands/logout.ts +++ b/packages/cli/src/commands/logout.ts @@ -30,7 +30,6 @@ export default class Logout extends Command { public async run(): Promise { const { flags } = await this.parse(Logout) - await Logout.logout(this, flags) } diff --git a/packages/cli/tests/commands/authenticate.spec.ts b/packages/cli/tests/commands/authenticate.spec.ts new file mode 100644 index 00000000..df020d65 --- /dev/null +++ b/packages/cli/tests/commands/authenticate.spec.ts @@ -0,0 +1,107 @@ +import { Command } from '@oclif/core' +import { expect } from 'chai' +import * as fs from 'fs' + +import Authenticate from '../../src/commands/authenticate' +import { CredentialsManager } from '../../src/lib/CredentialsManager' +import { backupCredentials, restoreCredentials } from '../helpers' + +const DEFAULT_PROFILE = 'default' + +describe.only('authenticate', () => { + let credentialsManager: CredentialsManager + let backupDir: string | null = null + let mockCommand: Command + + beforeEach('backup existing credentials and setup mock command', () => { + credentialsManager = CredentialsManager.getDefault() + backupDir = backupCredentials(credentialsManager) + mockCommand = new Command([], {} as any) + }) + + afterEach('restore credentials', () => { + restoreCredentials(credentialsManager, backupDir) + backupDir = null + }) + + context('when api-key is not provided', () => { + context('when credentials exist', () => { + beforeEach('create credentials', () => { + credentialsManager.saveProfile(DEFAULT_PROFILE, 'test-key-123') + }) + + context('when no profile is specified', () => { + it('returns the default profile', () => { + const credentials = Authenticate.authenticate(mockCommand, {}) + + expect(credentials.apiKey).to.equal('test-key-123') + }) + }) + + context('when profile is specified', () => { + context("when profile doesn't exists", () => { + it('throws an error', () => { + expect(() => Authenticate.authenticate(mockCommand, { profile: 'nonexistent' })).to.throw( + "Profile 'nonexistent' not found" + ) + }) + }) + + context('when profile exists', () => { + it('returns the profile', () => { + const credentials = Authenticate.authenticate(mockCommand, { profile: DEFAULT_PROFILE }) + expect(credentials).to.deep.equal({ apiKey: 'test-key-123' }) + }) + }) + }) + }) + + context('when no credentials exist', () => { + context('when no folder', () => { + beforeEach('remove folder', () => { + const credDir = credentialsManager.getBaseDir() + if (fs.existsSync(credDir)) { + fs.rmSync(credDir, { recursive: true, force: true }) + } + }) + it('throws an error', () => { + expect(() => Authenticate.authenticate(mockCommand, { profile: DEFAULT_PROFILE })).to.throw( + /No credentials directory found/ + ) + }) + }) + + context('when folder exists', () => { + beforeEach('create folder', () => { + credentialsManager.createCredentialsDirIfNotExists() + }) + it('throws an error', () => { + expect(() => Authenticate.authenticate(mockCommand, { profile: DEFAULT_PROFILE })).to.throw( + /No credentials file found/ + ) + }) + }) + }) + }) + + context('when api-key flag is provided', () => { + it('returns the api key', () => { + const credentials = Authenticate.authenticate(mockCommand, { 'api-key': 'direct-key-123' }) + + expect(credentials).to.deep.equal({ apiKey: 'direct-key-123' }) + }) + + context('when profile flag is also provided', () => { + it('returns the api key', () => { + credentialsManager.saveProfile(DEFAULT_PROFILE, 'profile-key') + + const credentials = Authenticate.authenticate(mockCommand, { + profile: DEFAULT_PROFILE, + 'api-key': 'flag-key', + }) + + expect(credentials).to.deep.equal({ apiKey: 'flag-key' }) + }) + }) + }) +}) From d9e62de54a2bdc8d6e7252a4f8fd9394cb9f3c81 Mon Sep 17 00:00:00 2001 From: Agustincito Date: Thu, 5 Feb 2026 12:20:44 -0300 Subject: [PATCH 5/8] Fix lint --- packages/cli/tests/commands/authenticate.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cli/tests/commands/authenticate.spec.ts b/packages/cli/tests/commands/authenticate.spec.ts index df020d65..2862d85d 100644 --- a/packages/cli/tests/commands/authenticate.spec.ts +++ b/packages/cli/tests/commands/authenticate.spec.ts @@ -8,7 +8,7 @@ import { backupCredentials, restoreCredentials } from '../helpers' const DEFAULT_PROFILE = 'default' -describe.only('authenticate', () => { +describe('authenticate', () => { let credentialsManager: CredentialsManager let backupDir: string | null = null let mockCommand: Command @@ -16,7 +16,7 @@ describe.only('authenticate', () => { beforeEach('backup existing credentials and setup mock command', () => { credentialsManager = CredentialsManager.getDefault() backupDir = backupCredentials(credentialsManager) - mockCommand = new Command([], {} as any) + mockCommand = new Command([], {}) }) afterEach('restore credentials', () => { From 52f8fa2cf177acd3a3e70e64a934266445438196 Mon Sep 17 00:00:00 2001 From: Agustincito Date: Thu, 5 Feb 2026 12:21:50 -0300 Subject: [PATCH 6/8] Use this --- packages/cli/src/commands/login.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/commands/login.ts b/packages/cli/src/commands/login.ts index 47e372a8..3ddd9df2 100644 --- a/packages/cli/src/commands/login.ts +++ b/packages/cli/src/commands/login.ts @@ -74,7 +74,7 @@ export default class Login extends Command { } } - Login.saveAndConfirm(cmd, profileName || CredentialsManager.getDefaultProfileName(), apiKey, flags['force-login']) + this.saveAndConfirm(cmd, profileName || CredentialsManager.getDefaultProfileName(), apiKey, flags['force-login']) } private static async saveAndConfirm( From 9834e8ec42dbc5821792ab6eac82754bdfd413e4 Mon Sep 17 00:00:00 2001 From: Agustincito Date: Thu, 5 Feb 2026 12:34:30 -0300 Subject: [PATCH 7/8] Fix private functions --- packages/cli/src/commands/codegen.ts | 24 ++++++++-------- packages/cli/src/commands/deploy.ts | 41 ++++++++++++++-------------- packages/cli/src/commands/init.ts | 4 +-- 3 files changed, 35 insertions(+), 34 deletions(-) diff --git a/packages/cli/src/commands/codegen.ts b/packages/cli/src/commands/codegen.ts index 75bf856a..7e3594cc 100644 --- a/packages/cli/src/commands/codegen.ts +++ b/packages/cli/src/commands/codegen.ts @@ -59,21 +59,21 @@ export default class Codegen extends Command { } if (!fs.existsSync(typesDir)) fs.mkdirSync(typesDir, { recursive: true }) - generateAbisCode(manifest, typesDir, manifestDir) - generateInputsCode(manifest, typesDir) + this.generateAbisCode(manifest, typesDir, manifestDir) + this.generateInputsCode(manifest, typesDir) log.stopAction() } -} -function generateAbisCode(manifest: Manifest, typesDir: string, manifestDir: string) { - for (const [contractName, path] of Object.entries(manifest.abis)) { - const abi = JSON.parse(fs.readFileSync(join(manifestDir, '../', path), 'utf-8')) - const abiInterface = AbisInterfaceGenerator.generate(abi, contractName) - if (abiInterface.length > 0) fs.writeFileSync(`${typesDir}/${contractName}.ts`, abiInterface) + private static generateAbisCode(manifest: Manifest, typesDir: string, manifestDir: string) { + for (const [contractName, path] of Object.entries(manifest.abis)) { + const abi = JSON.parse(fs.readFileSync(join(manifestDir, '../', path), 'utf-8')) + const abiInterface = AbisInterfaceGenerator.generate(abi, contractName) + if (abiInterface.length > 0) fs.writeFileSync(`${typesDir}/${contractName}.ts`, abiInterface) + } } -} -function generateInputsCode(manifest: Manifest, typesDir: string) { - const inputsInterface = InputsInterfaceGenerator.generate(manifest.inputs) - if (inputsInterface.length > 0) fs.writeFileSync(`${typesDir}/index.ts`, inputsInterface) + private static generateInputsCode(manifest: Manifest, typesDir: string) { + const inputsInterface = InputsInterfaceGenerator.generate(manifest.inputs) + if (inputsInterface.length > 0) fs.writeFileSync(`${typesDir}/index.ts`, inputsInterface) + } } diff --git a/packages/cli/src/commands/deploy.ts b/packages/cli/src/commands/deploy.ts index 2f98c97b..d25ca9b3 100644 --- a/packages/cli/src/commands/deploy.ts +++ b/packages/cli/src/commands/deploy.ts @@ -40,10 +40,10 @@ export default class Deploy extends Command { public async run(): Promise { const { flags } = await this.parse(Deploy) - await this.deploy(this, flags) + await Deploy.deploy(this, flags) } - public async deploy(cmd: Command, flags: DeployFlags): Promise { + public static async deploy(cmd: Command, flags: DeployFlags): Promise { const { 'build-directory': buildDir, 'skip-build': skipBuild, url: registryUrl } = flags const absBuildDir = resolve(buildDir) @@ -69,7 +69,7 @@ export default class Deploy extends Command { } log.startAction('Uploading to Mimic Registry') - const CID = await this.uploadToRegistry(neededFiles, credentials, registryUrl) + const CID = await this.uploadToRegistry(cmd, neededFiles, credentials, registryUrl) console.log(`IPFS CID: ${log.highlightText(CID)}`) log.stopAction() @@ -78,13 +78,14 @@ export default class Deploy extends Command { console.log(`Function deployed!`) } - private async uploadToRegistry( + private static async uploadToRegistry( + cmd: Command, files: string[], credentials: ProfileCredentials, registryUrl: string ): Promise { try { - const form = filesToForm(files) + const form = this.filesToForm(files) const { data } = await axios.post(`${registryUrl}/functions`, form, { headers: { 'x-api-key': credentials.apiKey, @@ -93,28 +94,28 @@ export default class Deploy extends Command { }) return data.CID } catch (err) { - this.handleError(err, 'Failed to upload to registry') + this.handleError(cmd, err, 'Failed to upload to registry') } } - private handleError(err: unknown, message: string): never { - if (!(err instanceof AxiosError)) this.error(err as Error) + private static handleError(cmd: Command, err: unknown, message: string): never { + if (!(err instanceof AxiosError)) cmd.error(err as Error) const statusCode = err.response?.status if (statusCode === 400) { const errMessage = err.response?.data?.content?.message || message - this.error(errMessage, { code: 'Bad Request', suggestions: ['Review the uploaded files'] }) + cmd.error(errMessage, { code: 'Bad Request', suggestions: ['Review the uploaded files'] }) } - if (statusCode === 401) this.error(message, { code: 'Unauthorized', suggestions: ['Review your key'] }) - if (statusCode === 403) this.error(message, { code: 'Invalid api key', suggestions: ['Review your key'] }) - this.error(`${message} - ${err.message}`, { code: `${statusCode} Error`, suggestions: GENERIC_SUGGESTION }) + if (statusCode === 401) cmd.error(message, { code: 'Unauthorized', suggestions: ['Review your key'] }) + if (statusCode === 403) cmd.error(message, { code: 'Invalid api key', suggestions: ['Review your key'] }) + cmd.error(`${message} - ${err.message}`, { code: `${statusCode} Error`, suggestions: GENERIC_SUGGESTION }) } -} -const filesToForm = (files: string[]): FormData => { - return files.reduce((form, file) => { - const fileStream = fs.createReadStream(file) - const filename = file.split('/').pop() - form.append('file', fileStream, { filename }) - return form - }, new FormData()) + private static filesToForm(files: string[]): FormData { + return files.reduce((form, file) => { + const fileStream = fs.createReadStream(file) + const filename = file.split('/').pop() + form.append('file', fileStream, { filename }) + return form + }, new FormData()) + } } diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts index 01a7437c..c6b0f6ef 100644 --- a/packages/cli/src/commands/init.ts +++ b/packages/cli/src/commands/init.ts @@ -85,12 +85,12 @@ export default class Init extends Command { console.log('New project initialized!') } - public static installDependencies(absDir: string) { + private static installDependencies(absDir: string) { if (process.env.NODE_ENV === 'test') return installDependencies(absDir) } - public static runCodegen(absDir: string) { + private static runCodegen(absDir: string) { if (process.env.NODE_ENV === 'test') return execBinCommand('mimic', ['codegen'], absDir) } From f865ae5154c1c714f33e6a31bc4c094e5345453b Mon Sep 17 00:00:00 2001 From: Agustincito Date: Thu, 5 Feb 2026 12:57:16 -0300 Subject: [PATCH 8/8] Fix test command --- packages/cli/src/commands/test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/cli/src/commands/test.ts b/packages/cli/src/commands/test.ts index 8157b578..e742f8e8 100644 --- a/packages/cli/src/commands/test.ts +++ b/packages/cli/src/commands/test.ts @@ -21,15 +21,15 @@ export default class Test extends Command { public async run(): Promise { const { flags } = await this.parse(Test) - await this.test(this, flags) + await Test.test(this, flags) } - public async test(cmd: Command, flags: TestFlags): Promise { + public static async test(cmd: Command, flags: TestFlags): Promise { const { directory, 'skip-build': skipBuild } = flags const baseDir = path.resolve('./') const testPath = path.join(baseDir, directory) - if (!skipBuild) await Build.build(this, flags) + if (!skipBuild) await Build.build(cmd, flags) const result = execBinCommand('tsx', ['./node_modules/mocha/bin/mocha.js', `${testPath}/**/*.spec.ts`], baseDir) cmd.exit(result.status ?? 1)