Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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/swift-parks-jump.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
---

Refactor credentials commands
69 changes: 51 additions & 18 deletions packages/cli/src/commands/authenticate.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -32,25 +33,57 @@ export default class Authenticate extends Command {
}

public static authenticate(cmd: Command, flags: AuthenticateFlags): 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) {
cmd.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
const apiKey = flags['api-key']
const profileName = flags.profile || CredentialsManager.getDefaultProfileName()

const credentialsManager = CredentialsManager.getDefault()

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 ?? '<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 }
}
}
24 changes: 12 additions & 12 deletions packages/cli/src/commands/codegen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
41 changes: 21 additions & 20 deletions packages/cli/src/commands/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,10 @@ export default class Deploy extends Command {

public async run(): Promise<void> {
const { flags } = await this.parse(Deploy)
await this.deploy(this, flags)
await Deploy.deploy(this, flags)
}

public async deploy(cmd: Command, flags: DeployFlags): Promise<void> {
public static async deploy(cmd: Command, flags: DeployFlags): Promise<void> {
const { 'build-directory': buildDir, 'skip-build': skipBuild, url: registryUrl } = flags
const absBuildDir = resolve(buildDir)

Expand All @@ -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()

Expand All @@ -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<string> {
try {
const form = filesToForm(files)
const form = this.filesToForm(files)
const { data } = await axios.post(`${registryUrl}/functions`, form, {
headers: {
'x-api-key': credentials.apiKey,
Expand All @@ -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())
}
}
20 changes: 13 additions & 7 deletions packages/cli/src/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof Init> & { directory: string }

export default class Init extends Command {
static override description = 'Initializes a new Mimic-compatible project structure in the specified directory'
Expand All @@ -22,8 +25,11 @@ export default class Init extends Command {

public async run(): Promise<void> {
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<void> {
const { directory, force } = flags
const absDir = path.resolve(directory)

if (force && fs.existsSync(absDir) && fs.readdirSync(absDir).length > 0) {
Expand All @@ -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
Expand All @@ -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',
Expand All @@ -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)
Expand All @@ -79,12 +85,12 @@ export default class Init extends Command {
console.log('New project initialized!')
}

installDependencies(absDir: string) {
private static installDependencies(absDir: string) {
if (process.env.NODE_ENV === 'test') return
installDependencies(absDir)
}

runCodegen(absDir: string) {
private static runCodegen(absDir: string) {
if (process.env.NODE_ENV === 'test') return
execBinCommand('mimic', ['codegen'], absDir)
}
Expand Down
24 changes: 18 additions & 6 deletions packages/cli/src/commands/login.ts
Original file line number Diff line number Diff line change
@@ -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<typeof Login>

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

static override examples = [
Expand All @@ -26,6 +29,10 @@ export default class Login extends Authenticate {

public async run(): Promise<void> {
const { flags } = await this.parse(Login)
await Login.login(this, flags)
}

public static async login(cmd: Command, flags: LoginFlags): Promise<void> {
const { profile: profileInput, 'api-key': apiKeyFlag } = flags

let apiKey: string
Expand Down Expand Up @@ -61,16 +68,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'])
this.saveAndConfirm(cmd, profileName || CredentialsManager.getDefaultProfileName(), apiKey, flags['force-login'])
}

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

Expand Down Expand Up @@ -98,7 +110,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
}
}
Expand Down
11 changes: 9 additions & 2 deletions packages/cli/src/commands/logout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof Logout>

export default class Logout extends Command {
static override description = 'Remove stored credentials for a profile'
Expand All @@ -27,11 +30,15 @@ export default class Logout extends Command {

public async run(): Promise<void> {
const { flags } = await this.parse(Logout)
await Logout.logout(this, flags)
}

public static async logout(cmd: Command, flags: LogoutFlags): Promise<void> {
const { profile: profileName, force } = flags

const profiles = CredentialsManager.getDefault().getProfiles()
if (!profiles.includes(profileName)) {
this.error(`Profile '${profileName}' does not exist`, {
cmd.error(`Profile '${profileName}' does not exist`, {
code: 'ProfileNotFound',
suggestions:
profiles.length > 0
Expand All @@ -48,7 +55,7 @@ export default class Logout extends Command {

if (!shouldRemove) {
console.log('Logout cancelled')
this.exit(0)
cmd.exit(0)
}
}

Expand Down
7 changes: 7 additions & 0 deletions packages/cli/src/commands/profiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,20 @@ import { Command } from '@oclif/core'

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

export type ProfilesFlags = FlagsType<typeof Profiles>

export default class Profiles extends Command {
static override description = 'List all configured authentication profiles'

static override examples = ['<%= config.bin %> <%= command.id %>']

public async run(): Promise<void> {
await Profiles.profiles()
}

public static async profiles(): Promise<void> {
const profiles = CredentialsManager.getDefault().getProfiles()

if (profiles.length === 0) {
Expand Down
Loading