From 28ba2cb43c4a0a8f3a5633be83af18de5a404b65 Mon Sep 17 00:00:00 2001 From: ncomerci Date: Mon, 29 Dec 2025 11:10:12 -0300 Subject: [PATCH 01/27] CLI: Introduce MimicConfigHandler for multiple tasks management --- packages/cli/src/commands/build.ts | 21 ++- packages/cli/src/commands/codegen.ts | 20 ++- packages/cli/src/commands/compile.ts | 25 ++- packages/cli/src/commands/deploy.ts | 42 ++++- packages/cli/src/commands/test.ts | 34 +++- packages/cli/src/lib/MimicConfigHandler.ts | 61 +++++++ packages/cli/src/lib/index.ts | 3 +- packages/cli/src/types.ts | 6 +- packages/cli/src/validators.ts | 12 ++ packages/cli/tests/MimicConfigHandler.spec.ts | 158 ++++++++++++++++++ 10 files changed, 365 insertions(+), 17 deletions(-) create mode 100644 packages/cli/src/lib/MimicConfigHandler.ts create mode 100644 packages/cli/tests/MimicConfigHandler.spec.ts diff --git a/packages/cli/src/commands/build.ts b/packages/cli/src/commands/build.ts index fc60d857..4ed00bae 100644 --- a/packages/cli/src/commands/build.ts +++ b/packages/cli/src/commands/build.ts @@ -1,5 +1,9 @@ import { Command, Flags } from '@oclif/core' +import MimicConfigHandler from '../lib/MimicConfigHandler' +import log from '../log' +import { RequiredTaskConfig } from '../types' + import Codegen from './codegen' import Compile from './compile' @@ -26,12 +30,25 @@ export default class Build extends Command { const { flags } = await this.parse(Build) const { manifest, task, output, types, clean } = flags - const codegenArgs: string[] = ['--manifest', manifest, '--output', types] + if (MimicConfigHandler.exists()) { + const mimicConfig = MimicConfigHandler.load(this) + const tasks = MimicConfigHandler.getTasks(mimicConfig) + for (const taskConfig of tasks) { + console.log(`\n${log.highlightText(`[${taskConfig.name}]`)}`) + await this.runForTask(taskConfig, clean) + } + } else { + await this.runForTask({ manifest, entry: task, output, types }, clean) + } + } + + private async runForTask(task: Omit, clean: boolean): Promise { + const codegenArgs: string[] = ['--manifest', task.manifest, '--output', task.types] if (clean) codegenArgs.push('--clean') await Codegen.run(codegenArgs) - const compileArgs: string[] = ['--task', task, '--manifest', manifest, '--output', output] + const compileArgs: string[] = ['--task', task.entry, '--manifest', task.manifest, '--output', task.output] await Compile.run(compileArgs) } } diff --git a/packages/cli/src/commands/codegen.ts b/packages/cli/src/commands/codegen.ts index 01d1deca..4a08bb6e 100644 --- a/packages/cli/src/commands/codegen.ts +++ b/packages/cli/src/commands/codegen.ts @@ -3,9 +3,9 @@ import { Command, Flags } from '@oclif/core' import * as fs from 'fs' import { join } from 'path' -import { AbisInterfaceGenerator, InputsInterfaceGenerator, ManifestHandler } from '../lib' +import { AbisInterfaceGenerator, InputsInterfaceGenerator, ManifestHandler, MimicConfigHandler } from '../lib' import log from '../log' -import { Manifest } from '../types' +import { Manifest, RequiredTaskConfig } from '../types' export default class Codegen extends Command { static override description = 'Generates typed interfaces for declared inputs and ABIs from your manifest.yaml file' @@ -25,6 +25,22 @@ export default class Codegen extends Command { public async run(): Promise { const { flags } = await this.parse(Codegen) const { manifest: manifestDir, output: outputDir, clean } = flags + + if (MimicConfigHandler.exists()) { + const mimicConfig = MimicConfigHandler.load(this) + const tasks = MimicConfigHandler.getTasks(mimicConfig) + for (const task of tasks) { + console.log(`\n${log.highlightText(`[${task.name}]`)}`) + await this.runForTask(task, clean) + } + } else { + await this.runForTask({ manifest: manifestDir, types: outputDir }, clean) + } + } + + private async runForTask(task: Omit, clean: boolean): Promise { + const manifestDir = task.manifest + const outputDir = task.types const manifest = ManifestHandler.load(this, manifestDir) if (clean) { diff --git a/packages/cli/src/commands/compile.ts b/packages/cli/src/commands/compile.ts index c632cfdd..63c5a28b 100644 --- a/packages/cli/src/commands/compile.ts +++ b/packages/cli/src/commands/compile.ts @@ -3,8 +3,10 @@ import * as fs from 'fs' import * as path from 'path' import ManifestHandler from '../lib/ManifestHandler' +import MimicConfigHandler from '../lib/MimicConfigHandler' import { execBinCommand } from '../lib/packageManager' import log from '../log' +import { RequiredTaskConfig } from '../types' export default class Compile extends Command { static override description = 'Compiles task' @@ -21,13 +23,26 @@ export default class Compile extends Command { const { flags } = await this.parse(Compile) const { task: taskFile, output: outputDir, manifest: manifestDir } = flags - const absTaskFile = path.resolve(taskFile) - const absOutputDir = path.resolve(outputDir) + if (MimicConfigHandler.exists()) { + const mimicConfig = MimicConfigHandler.load(this) + const tasks = MimicConfigHandler.getTasks(mimicConfig) + for (const task of tasks) { + console.log(`\n${log.highlightText(`[${task.name}]`)}`) + this.runForTask(task) + } + } else { + this.runForTask({ manifest: manifestDir, entry: taskFile, output: outputDir }) + } + } + + private runForTask(task: Omit): void { + const absTaskFile = path.resolve(task.entry) + const absOutputDir = path.resolve(task.output) if (!fs.existsSync(absOutputDir)) fs.mkdirSync(absOutputDir, { recursive: true }) log.startAction('Verifying Manifest') - const manifest = ManifestHandler.load(this, manifestDir) + const manifest = ManifestHandler.load(this, task.manifest) log.startAction('Compiling') const ascArgs = [ @@ -52,8 +67,8 @@ export default class Compile extends Command { log.startAction('Saving files') - fs.writeFileSync(path.join(outputDir, 'manifest.json'), JSON.stringify(manifest, null, 2)) + fs.writeFileSync(path.join(absOutputDir, 'manifest.json'), JSON.stringify(manifest, null, 2)) log.stopAction() - console.log(`Build complete! Artifacts in ${outputDir}/`) + console.log(`Build complete! Artifacts in ${task.output}/`) } } diff --git a/packages/cli/src/commands/deploy.ts b/packages/cli/src/commands/deploy.ts index 1c54d234..8c30a1e2 100644 --- a/packages/cli/src/commands/deploy.ts +++ b/packages/cli/src/commands/deploy.ts @@ -5,8 +5,10 @@ import * as fs from 'fs' import { join, resolve } from 'path' import { GENERIC_SUGGESTION } from '../errors' +import MimicConfigHandler from '../lib/MimicConfigHandler' import { execBinCommand } from '../lib/packageManager' import log from '../log' +import { RequiredTaskConfig } from '../types' const MIMIC_REGISTRY_DEFAULT = 'https://api-protocol.mimic.fi' @@ -26,15 +28,51 @@ export default class Deploy extends Command { public async run(): Promise { const { flags } = await this.parse(Deploy) const { key, input: inputDir, output: outputDir, 'skip-compile': skipCompile, url: registryUrl } = flags + + if (MimicConfigHandler.exists()) { + const mimicConfig = MimicConfigHandler.load(this) + const tasks = MimicConfigHandler.getTasks(mimicConfig) + for (const task of tasks) { + console.log(`\n${log.highlightText(`[${task.name}]`)}`) + await this.runForTask(task, key, registryUrl, skipCompile, task.output, task.output) + } + } else { + await this.runForTask( + { manifest: 'manifest.yaml', entry: 'src/task.ts', types: './src/types' }, + key, + registryUrl, + skipCompile, + inputDir, + outputDir + ) + } + } + + private async runForTask( + task: Omit, + key: string, + registryUrl: string, + skipCompile: boolean, + inputDir: string, + outputDir: string + ): Promise { const fullInputDir = resolve(inputDir) const fullOutputDir = resolve(outputDir) if (!skipCompile) { - const codegen = execBinCommand('mimic', ['codegen'], process.cwd()) + const codegen = execBinCommand( + 'mimic', + ['codegen', '--manifest', task.manifest, '--output', task.types], + process.cwd() + ) if (codegen.status !== 0) this.error('Code generation failed', { code: 'CodegenError', suggestions: ['Fix manifest and ABI files'] }) - const compile = execBinCommand('mimic', ['compile', '--output', fullInputDir], process.cwd()) + const compile = execBinCommand( + 'mimic', + ['compile', '--task', task.entry, '--manifest', task.manifest, '--output', fullInputDir], + process.cwd() + ) if (compile.status !== 0) this.error('Compilation failed', { code: 'BuildError', suggestions: ['Check the task source code'] }) } diff --git a/packages/cli/src/commands/test.ts b/packages/cli/src/commands/test.ts index 4d0dd14e..eb8bd399 100644 --- a/packages/cli/src/commands/test.ts +++ b/packages/cli/src/commands/test.ts @@ -1,7 +1,10 @@ import { Args, Command, Flags } from '@oclif/core' import * as path from 'path' +import MimicConfigHandler from '../lib/MimicConfigHandler' import { execBinCommand } from '../lib/packageManager' +import log from '../log' +import { RequiredTaskConfig } from '../types' export default class Test extends Command { static override description = 'Runs task tests' @@ -21,16 +24,39 @@ export default class Test extends Command { const { directory } = args const { 'skip-compile': skipCompile } = flags const baseDir = path.resolve(directory) - const testPath = path.join(baseDir, 'tests') + + if (MimicConfigHandler.exists(baseDir)) { + const mimicConfig = MimicConfigHandler.load(this, baseDir) + const tasks = MimicConfigHandler.getTasks(mimicConfig) + for (const task of tasks) { + console.log(`\n${log.highlightText(`[${task.name}]`)}`) + this.runForTask(task, baseDir, skipCompile) + } + } else { + this.runForTask( + { manifest: 'manifest.yaml', entry: 'src/task.ts', types: './src/types', output: './build' }, + baseDir, + skipCompile + ) + } + } + + private runForTask(task: Omit, baseDir: string, skipCompile: boolean): void { + const taskDir = path.dirname(task.entry) + const testPath = path.join(baseDir, taskDir, '..', 'tests') if (!skipCompile) { - const cg = execBinCommand('mimic', ['codegen'], baseDir) + const cg = execBinCommand('mimic', ['codegen', '--manifest', task.manifest, '--output', task.types], baseDir) if (cg.status !== 0) this.exit(cg.status ?? 1) - const cp = execBinCommand('mimic', ['compile'], baseDir) + const cp = execBinCommand( + 'mimic', + ['compile', '--task', task.entry, '--manifest', task.manifest, '--output', task.output], + baseDir + ) if (cp.status !== 0) this.exit(cp.status ?? 1) } const result = execBinCommand('tsx', ['./node_modules/mocha/bin/mocha.js', `${testPath}/**/*.spec.ts`], baseDir) - this.exit(result.status ?? 1) + if (result.status !== 0) this.exit(result.status ?? 1) } } diff --git a/packages/cli/src/lib/MimicConfigHandler.ts b/packages/cli/src/lib/MimicConfigHandler.ts new file mode 100644 index 00000000..d7818702 --- /dev/null +++ b/packages/cli/src/lib/MimicConfigHandler.ts @@ -0,0 +1,61 @@ +import { Command } from '@oclif/core' +import * as fs from 'fs' +import { load } from 'js-yaml' +import * as path from 'path' +import { ZodError } from 'zod' + +import { MimicConfig, RequiredTaskConfig } from '../types' +import { MimicConfigValidator } from '../validators' + +const MIMIC_CONFIG_FILE = 'mimic.yaml' + +export default { + exists(baseDir: string = process.cwd()): boolean { + return fs.existsSync(path.join(baseDir, MIMIC_CONFIG_FILE)) + }, + + load(command: Command, baseDir: string = process.cwd()): MimicConfig { + const mimicConfigPath = path.join(baseDir, MIMIC_CONFIG_FILE) + let loadedMimicConfig + try { + loadedMimicConfig = load(fs.readFileSync(mimicConfigPath, 'utf-8')) + } catch { + command.error(`Could not find ${mimicConfigPath}`, { + code: 'FileNotFound', + suggestions: ['Ensure mimic.yaml exists in the project root'], + }) + } + + try { + return MimicConfigValidator.parse(loadedMimicConfig) + } catch (err) { + handleValidationError(command, err) + } + }, + + getTasks(mimicConfig: MimicConfig): RequiredTaskConfig[] { + return mimicConfig.tasks.map((task) => ({ + ...task, + output: task.output ?? `build/${task.name}`, + types: task.types ?? path.join(path.dirname(task.entry), 'types'), + })) + }, +} + +function handleValidationError(command: Command, err: unknown): never { + let message: string + let code: string + let suggestions: string[] + + if (err instanceof ZodError) { + ;[message, code] = ['Invalid mimic.yaml configuration', 'ValidationError'] + suggestions = err.errors.map((e) => `Fix Field "${e.path.join('.')}" -- ${e.message}`) + } else { + ;[message, code] = [`Unknown Error: ${err}`, 'UnknownError'] + suggestions = [ + 'Contact the Mimic team for further assistance at our website https://www.mimic.fi/ or discord https://discord.com/invite/cpcyV9EsEg', + ] + } + + command.error(message, { code, suggestions }) +} diff --git a/packages/cli/src/lib/index.ts b/packages/cli/src/lib/index.ts index f0d10209..6a752b65 100644 --- a/packages/cli/src/lib/index.ts +++ b/packages/cli/src/lib/index.ts @@ -1,5 +1,6 @@ import AbisInterfaceGenerator from './AbisInterfaceGenerator/index' import InputsInterfaceGenerator from './InputsInterfaceGenerator' import ManifestHandler from './ManifestHandler' +import MimicConfigHandler from './MimicConfigHandler' -export { AbisInterfaceGenerator, InputsInterfaceGenerator, ManifestHandler } +export { AbisInterfaceGenerator, InputsInterfaceGenerator, ManifestHandler, MimicConfigHandler } diff --git a/packages/cli/src/types.ts b/packages/cli/src/types.ts index e710ac0a..7f5d9269 100644 --- a/packages/cli/src/types.ts +++ b/packages/cli/src/types.ts @@ -1,10 +1,14 @@ import { z } from 'zod' -import { ManifestValidator } from './validators' +import { ManifestValidator, MimicConfigValidator, TaskConfigValidator } from './validators' export type Manifest = z.infer export type ManifestInputs = z.infer +export type TaskConfig = z.infer +export type MimicConfig = z.infer +export type RequiredTaskConfig = Required + export type AbiParameter = { name?: string escapedName?: string diff --git a/packages/cli/src/validators.ts b/packages/cli/src/validators.ts index bcf24259..6642c7da 100644 --- a/packages/cli/src/validators.ts +++ b/packages/cli/src/validators.ts @@ -25,3 +25,15 @@ export const ManifestValidator = z.object({ libVersion: String.regex(SEM_VER_REGEX, 'Must be a valid semver'), }), }) + +export const TaskConfigValidator = z.object({ + name: String, + manifest: String, + entry: String, + output: String.optional(), + types: String.optional(), +}) + +export const MimicConfigValidator = z.object({ + tasks: z.array(TaskConfigValidator).min(1, 'At least one task must be defined'), +}) diff --git a/packages/cli/tests/MimicConfigHandler.spec.ts b/packages/cli/tests/MimicConfigHandler.spec.ts new file mode 100644 index 00000000..0a24227b --- /dev/null +++ b/packages/cli/tests/MimicConfigHandler.spec.ts @@ -0,0 +1,158 @@ +import { expect } from 'chai' +import * as fs from 'fs' +import * as path from 'path' + +import MimicConfigHandler from '../src/lib/MimicConfigHandler' +import { MimicConfigValidator } from '../src/validators' + +describe('MimicConfigHandler', () => { + const mimicConfig = { + tasks: [ + { name: 'swap-task', manifest: './tasks/swap/manifest.yaml', entry: './tasks/swap/src/task.ts' }, + { name: 'transfer-task', manifest: './tasks/transfer/manifest.yaml', entry: './tasks/transfer/src/task.ts' }, + ], + } + + describe('exists', () => { + context('when mimic.yaml exists in the directory', () => { + it('returns true', () => { + const tempDir = path.join(__dirname, 'temp-workspace-test') + fs.mkdirSync(tempDir, { recursive: true }) + fs.writeFileSync(path.join(tempDir, 'mimic.yaml'), 'tasks: []') + + try { + expect(MimicConfigHandler.exists(tempDir)).to.be.true + } finally { + fs.rmSync(tempDir, { recursive: true, force: true }) + } + }) + }) + + context('when mimic.yaml does not exist in the directory', () => { + it('returns false', () => { + const tempDir = path.join(__dirname, 'temp-empty-dir') + fs.mkdirSync(tempDir, { recursive: true }) + + try { + expect(MimicConfigHandler.exists(tempDir)).to.be.false + } finally { + fs.rmSync(tempDir, { recursive: true, force: true }) + } + }) + }) + }) + + describe('validate', () => { + context('when the mimic config is valid', () => { + context('when everything is present', () => { + it('returns the parsed mimic config', () => { + const parsedMimicConfig = MimicConfigValidator.parse(mimicConfig) + + expect(parsedMimicConfig).to.not.be.undefined + expect(parsedMimicConfig.tasks).to.have.length(2) + }) + }) + + context('when dealing with tasks', () => { + context('when tasks have optional fields', () => { + it('returns the parsed mimic config with all fields', () => { + const mimicConfigWithOptionals = { + ...mimicConfig, + tasks: [{ ...mimicConfig.tasks[0], output: './build/swap', types: './types/swap' }], + } + const parsedMimicConfig = MimicConfigValidator.parse(mimicConfigWithOptionals) + + expect(parsedMimicConfig).to.not.be.undefined + expect(parsedMimicConfig.tasks[0].output).to.equal('./build/swap') + expect(parsedMimicConfig.tasks[0].types).to.equal('./types/swap') + }) + }) + + context('when tasks do not have optional fields', () => { + it('returns the parsed mimic config with undefined optional fields', () => { + const parsedMimicConfig = MimicConfigValidator.parse(mimicConfig) + + expect(parsedMimicConfig).to.not.be.undefined + expect(parsedMimicConfig.tasks[0].output).to.be.undefined + expect(parsedMimicConfig.tasks[0].types).to.be.undefined + }) + }) + }) + }) + + context('when the mimic config is not valid', () => { + const itReturnsAnError = (w: unknown, ...errors: string[]) => { + it('returns an error', () => { + for (const error of errors) expect(() => MimicConfigValidator.parse(w)).to.throw(error) + }) + } + + context('when the tasks array is empty', () => { + itReturnsAnError({ ...mimicConfig, tasks: [] }, 'At least one task must be defined') + }) + + context('when task name is missing', () => { + itReturnsAnError( + { ...mimicConfig, tasks: [{ manifest: './manifest.yaml', entry: './src/task.ts' }] }, + 'Required' + ) + }) + + context('when task manifest is missing', () => { + itReturnsAnError({ ...mimicConfig, tasks: [{ name: 'task', entry: './src/task.ts' }] }, 'Required') + }) + + context('when task entry is missing', () => { + itReturnsAnError({ ...mimicConfig, tasks: [{ name: 'task', manifest: './manifest.yaml' }] }, 'Required') + }) + }) + }) + + describe('getTasks', () => { + context('when dealing with optional fields', () => { + context('when optional fields are provided', () => { + it('returns tasks with the provided values', () => { + const mimicConfigWithOptionals = { + ...mimicConfig, + tasks: [{ ...mimicConfig.tasks[0], output: './custom-build', types: './custom-types' }], + } + + const tasks = MimicConfigHandler.getTasks(mimicConfigWithOptionals) + + expect(tasks).to.have.length(1) + expect(tasks[0].output).to.equal('./custom-build') + expect(tasks[0].types).to.equal('./custom-types') + }) + }) + + context('when optional fields are not provided', () => { + it('returns tasks with computed default values', () => { + const tasks = MimicConfigHandler.getTasks(mimicConfig) + + expect(tasks).to.have.length(2) + expect(tasks[0].output).to.equal('build/swap-task') + expect(tasks[0].types).to.equal('tasks/swap/src/types') + expect(tasks[1].output).to.equal('build/transfer-task') + expect(tasks[1].types).to.equal('tasks/transfer/src/types') + }) + }) + + context('when some tasks have optional fields and some do not', () => { + it('applies defaults only to tasks missing optional fields', () => { + const mixedMimicConfig = { + ...mimicConfig, + tasks: [mimicConfig.tasks[0], { ...mimicConfig.tasks[1], output: './custom-output' }], + } + + const tasks = MimicConfigHandler.getTasks(mixedMimicConfig) + + expect(tasks).to.have.length(2) + expect(tasks[0].output).to.equal('build/swap-task') + expect(tasks[0].types).to.equal('tasks/swap/src/types') + expect(tasks[1].output).to.equal('./custom-output') + expect(tasks[1].types).to.equal('tasks/transfer/src/types') + }) + }) + }) + }) +}) From 02aebba551ac28bb15caf46bad27293276a6257c Mon Sep 17 00:00:00 2001 From: ncomerci Date: Mon, 29 Dec 2025 13:00:25 -0300 Subject: [PATCH 02/27] CLI: Add task filtering functionality with include/exclude flags --- packages/cli/package.json | 1 + packages/cli/src/commands/build.ts | 7 +- packages/cli/src/commands/codegen.ts | 7 +- packages/cli/src/commands/compile.ts | 7 +- packages/cli/src/commands/deploy.ts | 15 ++- packages/cli/src/commands/test.ts | 7 +- packages/cli/src/helpers.ts | 60 ++++++++++- packages/cli/tests/helpers.spec.ts | 143 +++++++++++++++++++++++++++ yarn.lock | 12 +++ 9 files changed, 248 insertions(+), 11 deletions(-) create mode 100644 packages/cli/tests/helpers.spec.ts diff --git a/packages/cli/package.json b/packages/cli/package.json index b974aa72..6b15413a 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -40,6 +40,7 @@ "@types/lodash": "^4.17.15", "@types/mocha": "^10.0.1", "@types/node": "^22.10.5", + "@types/sinon": "^21.0.0", "axios-mock-adapter": "^2.1.0", "chai": "^4.3.7", "eslint-config-mimic": "^0.0.3", diff --git a/packages/cli/src/commands/build.ts b/packages/cli/src/commands/build.ts index 4ed00bae..1a2c42c9 100644 --- a/packages/cli/src/commands/build.ts +++ b/packages/cli/src/commands/build.ts @@ -1,5 +1,6 @@ import { Command, Flags } from '@oclif/core' +import { filterTasks, taskFilterFlags } from '../helpers' import MimicConfigHandler from '../lib/MimicConfigHandler' import log from '../log' import { RequiredTaskConfig } from '../types' @@ -24,15 +25,17 @@ export default class Build extends Command { description: 'remove existing generated types before generating new files', default: false, }), + ...taskFilterFlags, } public async run(): Promise { const { flags } = await this.parse(Build) - const { manifest, task, output, types, clean } = flags + const { manifest, task, output, types, clean, include, exclude } = flags if (MimicConfigHandler.exists()) { const mimicConfig = MimicConfigHandler.load(this) - const tasks = MimicConfigHandler.getTasks(mimicConfig) + const allTasks = MimicConfigHandler.getTasks(mimicConfig) + const tasks = filterTasks(this, allTasks, include, exclude) for (const taskConfig of tasks) { console.log(`\n${log.highlightText(`[${taskConfig.name}]`)}`) await this.runForTask(taskConfig, clean) diff --git a/packages/cli/src/commands/codegen.ts b/packages/cli/src/commands/codegen.ts index 4a08bb6e..e0a990e3 100644 --- a/packages/cli/src/commands/codegen.ts +++ b/packages/cli/src/commands/codegen.ts @@ -3,6 +3,7 @@ import { Command, Flags } from '@oclif/core' import * as fs from 'fs' import { join } from 'path' +import { filterTasks, taskFilterFlags } from '../helpers' import { AbisInterfaceGenerator, InputsInterfaceGenerator, ManifestHandler, MimicConfigHandler } from '../lib' import log from '../log' import { Manifest, RequiredTaskConfig } from '../types' @@ -20,15 +21,17 @@ export default class Codegen extends Command { description: 'Remove existing generated types before generating new files', default: false, }), + ...taskFilterFlags, } public async run(): Promise { const { flags } = await this.parse(Codegen) - const { manifest: manifestDir, output: outputDir, clean } = flags + const { manifest: manifestDir, output: outputDir, clean, include, exclude } = flags if (MimicConfigHandler.exists()) { const mimicConfig = MimicConfigHandler.load(this) - const tasks = MimicConfigHandler.getTasks(mimicConfig) + const allTasks = MimicConfigHandler.getTasks(mimicConfig) + const tasks = filterTasks(this, allTasks, include, exclude) for (const task of tasks) { console.log(`\n${log.highlightText(`[${task.name}]`)}`) await this.runForTask(task, clean) diff --git a/packages/cli/src/commands/compile.ts b/packages/cli/src/commands/compile.ts index 63c5a28b..13034b9e 100644 --- a/packages/cli/src/commands/compile.ts +++ b/packages/cli/src/commands/compile.ts @@ -2,6 +2,7 @@ import { Command, Flags } from '@oclif/core' import * as fs from 'fs' import * as path from 'path' +import { filterTasks, taskFilterFlags } from '../helpers' import ManifestHandler from '../lib/ManifestHandler' import MimicConfigHandler from '../lib/MimicConfigHandler' import { execBinCommand } from '../lib/packageManager' @@ -17,15 +18,17 @@ export default class Compile extends Command { task: Flags.string({ char: 't', description: 'task to compile', default: 'src/task.ts' }), manifest: Flags.string({ char: 'm', description: 'manifest to validate', default: 'manifest.yaml' }), output: Flags.string({ char: 'o', description: 'output directory', default: './build' }), + ...taskFilterFlags, } public async run(): Promise { const { flags } = await this.parse(Compile) - const { task: taskFile, output: outputDir, manifest: manifestDir } = flags + const { task: taskFile, output: outputDir, manifest: manifestDir, include, exclude } = flags if (MimicConfigHandler.exists()) { const mimicConfig = MimicConfigHandler.load(this) - const tasks = MimicConfigHandler.getTasks(mimicConfig) + const allTasks = MimicConfigHandler.getTasks(mimicConfig) + const tasks = filterTasks(this, allTasks, include, exclude) for (const task of tasks) { console.log(`\n${log.highlightText(`[${task.name}]`)}`) this.runForTask(task) diff --git a/packages/cli/src/commands/deploy.ts b/packages/cli/src/commands/deploy.ts index 8c30a1e2..c9cdd1fe 100644 --- a/packages/cli/src/commands/deploy.ts +++ b/packages/cli/src/commands/deploy.ts @@ -5,6 +5,7 @@ import * as fs from 'fs' import { join, resolve } from 'path' import { GENERIC_SUGGESTION } from '../errors' +import { filterTasks, taskFilterFlags } from '../helpers' import MimicConfigHandler from '../lib/MimicConfigHandler' import { execBinCommand } from '../lib/packageManager' import log from '../log' @@ -23,15 +24,25 @@ export default class Deploy extends Command { 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 }), 'skip-compile': Flags.boolean({ description: 'Skip codegen and compile steps before uploading', default: false }), + ...taskFilterFlags, } public async run(): Promise { const { flags } = await this.parse(Deploy) - const { key, input: inputDir, output: outputDir, 'skip-compile': skipCompile, url: registryUrl } = flags + const { + key, + input: inputDir, + output: outputDir, + 'skip-compile': skipCompile, + url: registryUrl, + include, + exclude, + } = flags if (MimicConfigHandler.exists()) { const mimicConfig = MimicConfigHandler.load(this) - const tasks = MimicConfigHandler.getTasks(mimicConfig) + const allTasks = MimicConfigHandler.getTasks(mimicConfig) + const tasks = filterTasks(this, allTasks, include, exclude) for (const task of tasks) { console.log(`\n${log.highlightText(`[${task.name}]`)}`) await this.runForTask(task, key, registryUrl, skipCompile, task.output, task.output) diff --git a/packages/cli/src/commands/test.ts b/packages/cli/src/commands/test.ts index eb8bd399..c00e001e 100644 --- a/packages/cli/src/commands/test.ts +++ b/packages/cli/src/commands/test.ts @@ -1,6 +1,7 @@ import { Args, Command, Flags } from '@oclif/core' import * as path from 'path' +import { filterTasks, taskFilterFlags } from '../helpers' import MimicConfigHandler from '../lib/MimicConfigHandler' import { execBinCommand } from '../lib/packageManager' import log from '../log' @@ -17,17 +18,19 @@ export default class Test extends Command { static override flags = { 'skip-compile': Flags.boolean({ description: 'skip codegen and compile steps' }), + ...taskFilterFlags, } public async run(): Promise { const { args, flags } = await this.parse(Test) const { directory } = args - const { 'skip-compile': skipCompile } = flags + const { 'skip-compile': skipCompile, include, exclude } = flags const baseDir = path.resolve(directory) if (MimicConfigHandler.exists(baseDir)) { const mimicConfig = MimicConfigHandler.load(this, baseDir) - const tasks = MimicConfigHandler.getTasks(mimicConfig) + const allTasks = MimicConfigHandler.getTasks(mimicConfig) + const tasks = filterTasks(this, allTasks, include, exclude) for (const task of tasks) { console.log(`\n${log.highlightText(`[${task.name}]`)}`) this.runForTask(task, baseDir, skipCompile) diff --git a/packages/cli/src/helpers.ts b/packages/cli/src/helpers.ts index 564e5adb..711b25c6 100644 --- a/packages/cli/src/helpers.ts +++ b/packages/cli/src/helpers.ts @@ -1,8 +1,10 @@ +import { Command, Flags } from '@oclif/core' import { Interface } from 'ethers' import camelCase from 'lodash/camelCase' import startCase from 'lodash/startCase' -import { AbiFunctionItem } from './types' +import log from './log' +import { AbiFunctionItem, RequiredTaskConfig } from './types' export function getFunctionSelector(fn: AbiFunctionItem): string { const iface = new Interface([fn]) @@ -12,3 +14,59 @@ export function getFunctionSelector(fn: AbiFunctionItem): string { export function pascalCase(str: string): string { return startCase(camelCase(str)).replace(/\s/g, '') } + +export const taskFilterFlags = { + include: Flags.string({ + description: 'When mimic.yaml exists, only run tasks with these names (space-separated)', + multiple: true, + exclusive: ['exclude'], + }), + exclude: Flags.string({ + description: 'When mimic.yaml exists, exclude tasks with these names (space-separated)', + multiple: true, + exclusive: ['include'], + }), +} + +export function filterTasks( + command: Command, + tasks: RequiredTaskConfig[], + include?: string[], + exclude?: string[] +): RequiredTaskConfig[] { + if (include && exclude) { + command.error('Cannot use both --include and --exclude flags simultaneously', { + code: 'ConflictingFlags', + suggestions: ['Use either --include or --exclude, but not both'], + }) + } + + if (!include && !exclude) return tasks + + const taskNames = new Set(tasks.map((task) => task.name)) + + if (include) { + const invalidNames = include.filter((name) => !taskNames.has(name)) + if (invalidNames.length > 0) + console.warn(`${log.warnText('Warning:')} The following task names were not found: ${invalidNames.join(', ')}`) + + const validNames = new Set(include.filter((name) => taskNames.has(name))) + if (validNames.size === 0) { + console.warn(`${log.warnText('Warning:')} No valid tasks to include. All tasks will be skipped.`) + return [] + } + + return tasks.filter((task) => validNames.has(task.name)) + } + + if (exclude) { + const invalidNames = exclude.filter((name) => !taskNames.has(name)) + if (invalidNames.length > 0) + console.warn(`${log.warnText('Warning:')} The following task names were not found: ${invalidNames.join(', ')}`) + + const excludeSet = new Set(exclude) + return tasks.filter((task) => !excludeSet.has(task.name)) + } + + return tasks +} diff --git a/packages/cli/tests/helpers.spec.ts b/packages/cli/tests/helpers.spec.ts new file mode 100644 index 00000000..6de2c975 --- /dev/null +++ b/packages/cli/tests/helpers.spec.ts @@ -0,0 +1,143 @@ +import { Command } from '@oclif/core' +import { expect } from 'chai' +import * as sinon from 'sinon' + +import { filterTasks } from '../src/helpers' +import { RequiredTaskConfig } from '../src/types' + +describe('filterTasks', () => { + let mockCommand: Command + let warnSpy: sinon.SinonSpy + let errorStub: sinon.SinonStub + + const createTask = (name: string): RequiredTaskConfig => ({ + name, + manifest: `./tasks/${name}/manifest.yaml`, + entry: `./tasks/${name}/src/task.ts`, + output: `build/${name}`, + types: `./tasks/${name}/src/types`, + }) + + const tasks: RequiredTaskConfig[] = [createTask('swap-task'), createTask('transfer-task'), createTask('call-task')] + + beforeEach('setup mocks', () => { + errorStub = sinon.stub().throws(new Error('Command error')) + mockCommand = { + error: errorStub, + } as unknown as Command + + warnSpy = sinon.spy(console, 'warn') + }) + + afterEach('restore mocks', () => { + warnSpy.restore() + }) + + context('when no filter flags are provided', () => { + it('returns all tasks', () => { + const result = filterTasks(mockCommand, tasks) + + expect(result).to.have.length(3) + expect(result).to.deep.equal(tasks) + expect(warnSpy.called).to.be.false + }) + }) + + context('when --include flag is provided', () => { + context('when all task names are valid', () => { + context('when including a single task', () => { + it('returns only the included task', () => { + const result = filterTasks(mockCommand, tasks, ['swap-task']) + + expect(result).to.have.length(1) + expect(result[0].name).to.equal('swap-task') + expect(warnSpy.called).to.be.false + }) + }) + + context('when including multiple tasks', () => { + it('returns all included tasks', () => { + const result = filterTasks(mockCommand, tasks, ['swap-task', 'transfer-task']) + + expect(result).to.have.length(2) + expect(result.map((t) => t.name)).to.include.members(['swap-task', 'transfer-task']) + expect(warnSpy.called).to.be.false + }) + }) + }) + + context('when some task names are invalid', () => { + it('logs a warning and returns valid tasks', () => { + const result = filterTasks(mockCommand, tasks, ['swap-task', 'invalid-task']) + + expect(result).to.have.length(1) + expect(result[0].name).to.equal('swap-task') + expect(warnSpy.calledOnce).to.be.true + expect(warnSpy.firstCall.args[0]).to.include('invalid-task') + }) + }) + + context('when all task names are invalid', () => { + it('logs a warning and returns empty array', () => { + const result = filterTasks(mockCommand, tasks, ['invalid-task-1', 'invalid-task-2']) + + expect(result).to.have.length(0) + expect(warnSpy.calledTwice).to.be.true + expect(warnSpy.firstCall.args[0]).to.include('invalid-task-1') + expect(warnSpy.secondCall.args[0]).to.include('No valid tasks to include') + }) + }) + }) + + context('when --exclude flag is provided', () => { + context('when all task names are valid', () => { + context('when excluding a single task', () => { + it('returns tasks except the excluded one', () => { + const result = filterTasks(mockCommand, tasks, undefined, ['swap-task']) + + expect(result).to.have.length(2) + expect(result.map((t) => t.name)).to.include.members(['transfer-task', 'call-task']) + expect(result.map((t) => t.name)).to.not.include('swap-task') + expect(warnSpy.called).to.be.false + }) + }) + + context('when excluding multiple tasks', () => { + it('returns tasks except the excluded ones', () => { + const result = filterTasks(mockCommand, tasks, undefined, ['swap-task', 'transfer-task']) + + expect(result).to.have.length(1) + expect(result[0].name).to.equal('call-task') + expect(warnSpy.called).to.be.false + }) + }) + }) + + context('when some task names are invalid', () => { + it('logs a warning and excludes valid tasks', () => { + const result = filterTasks(mockCommand, tasks, undefined, ['swap-task', 'invalid-task']) + + expect(result).to.have.length(2) + expect(result.map((t) => t.name)).to.include.members(['transfer-task', 'call-task']) + expect(result.map((t) => t.name)).to.not.include('swap-task') + expect(warnSpy.calledOnce).to.be.true + expect(warnSpy.firstCall.args[0]).to.include('invalid-task') + }) + }) + }) + + context('when both flags are provided', () => { + it('throws a ConflictingFlags error', () => { + expect(() => { + filterTasks(mockCommand, tasks, ['swap-task'], ['transfer-task']) + }).to.throw('Command error') + + expect(errorStub.calledOnce).to.be.true + expect(errorStub.firstCall.args[0]).to.equal('Cannot use both --include and --exclude flags simultaneously') + expect(errorStub.firstCall.args[1]).to.deep.equal({ + code: 'ConflictingFlags', + suggestions: ['Use either --include or --exclude, but not both'], + }) + }) + }) +}) diff --git a/yarn.lock b/yarn.lock index ab6b6d6f..4dd2e1b5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1072,6 +1072,18 @@ "@types/node" "*" "@types/send" "*" +"@types/sinon@^21.0.0": + version "21.0.0" + resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-21.0.0.tgz#3a598a29b3aec0512a21e57ae0fd4c09aa013ca9" + integrity sha512-+oHKZ0lTI+WVLxx1IbJDNmReQaIsQJjN2e7UUrJHEeByG7bFeKJYsv1E75JxTQ9QKJDp21bAa/0W2Xo4srsDnw== + dependencies: + "@types/sinonjs__fake-timers" "*" + +"@types/sinonjs__fake-timers@*": + version "15.0.1" + resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-15.0.1.tgz#49f731d9453f52d64dd79f5a5626c1cf1b81bea4" + integrity sha512-Ko2tjWJq8oozHzHV+reuvS5KYIRAokHnGbDwGh/J64LntgpbuylF74ipEL24HCyRjf9FOlBiBHWBR1RlVKsI1w== + "@types/uuid@^8.3.4": version "8.3.4" resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.4.tgz#bd86a43617df0594787d38b735f55c805becf1bc" From ce2b416d640869fbba410443614329340296d6d0 Mon Sep 17 00:00:00 2001 From: ncomerci Date: Mon, 29 Dec 2025 13:10:24 -0300 Subject: [PATCH 03/27] cli: fixes --- packages/cli/src/commands/compile.ts | 6 +++--- packages/cli/src/commands/deploy.ts | 14 ++++++-------- packages/cli/src/commands/test.ts | 10 +++++++--- packages/cli/src/lib/MimicConfigHandler.ts | 17 +++++++++++++---- 4 files changed, 29 insertions(+), 18 deletions(-) diff --git a/packages/cli/src/commands/compile.ts b/packages/cli/src/commands/compile.ts index 13034b9e..171f1bb3 100644 --- a/packages/cli/src/commands/compile.ts +++ b/packages/cli/src/commands/compile.ts @@ -31,14 +31,14 @@ export default class Compile extends Command { const tasks = filterTasks(this, allTasks, include, exclude) for (const task of tasks) { console.log(`\n${log.highlightText(`[${task.name}]`)}`) - this.runForTask(task) + await this.runForTask(task) } } else { - this.runForTask({ manifest: manifestDir, entry: taskFile, output: outputDir }) + await this.runForTask({ manifest: manifestDir, entry: taskFile, output: outputDir }) } } - private runForTask(task: Omit): void { + private async runForTask(task: Omit): Promise { const absTaskFile = path.resolve(task.entry) const absOutputDir = path.resolve(task.output) diff --git a/packages/cli/src/commands/deploy.ts b/packages/cli/src/commands/deploy.ts index c9cdd1fe..ea8babbd 100644 --- a/packages/cli/src/commands/deploy.ts +++ b/packages/cli/src/commands/deploy.ts @@ -45,30 +45,28 @@ export default class Deploy extends Command { const tasks = filterTasks(this, allTasks, include, exclude) for (const task of tasks) { console.log(`\n${log.highlightText(`[${task.name}]`)}`) - await this.runForTask(task, key, registryUrl, skipCompile, task.output, task.output) + await this.runForTask(task, key, registryUrl, skipCompile, task.output) } } else { await this.runForTask( - { manifest: 'manifest.yaml', entry: 'src/task.ts', types: './src/types' }, + { manifest: 'manifest.yaml', entry: 'src/task.ts', types: './src/types', output: outputDir }, key, registryUrl, skipCompile, - inputDir, - outputDir + inputDir ) } } private async runForTask( - task: Omit, + task: Omit, key: string, registryUrl: string, skipCompile: boolean, - inputDir: string, - outputDir: string + inputDir: string ): Promise { const fullInputDir = resolve(inputDir) - const fullOutputDir = resolve(outputDir) + const fullOutputDir = resolve(task.output) if (!skipCompile) { const codegen = execBinCommand( diff --git a/packages/cli/src/commands/test.ts b/packages/cli/src/commands/test.ts index c00e001e..808e2d9d 100644 --- a/packages/cli/src/commands/test.ts +++ b/packages/cli/src/commands/test.ts @@ -33,10 +33,10 @@ export default class Test extends Command { const tasks = filterTasks(this, allTasks, include, exclude) for (const task of tasks) { console.log(`\n${log.highlightText(`[${task.name}]`)}`) - this.runForTask(task, baseDir, skipCompile) + await this.runForTask(task, baseDir, skipCompile) } } else { - this.runForTask( + await this.runForTask( { manifest: 'manifest.yaml', entry: 'src/task.ts', types: './src/types', output: './build' }, baseDir, skipCompile @@ -44,7 +44,11 @@ export default class Test extends Command { } } - private runForTask(task: Omit, baseDir: string, skipCompile: boolean): void { + private async runForTask( + task: Omit, + baseDir: string, + skipCompile: boolean + ): Promise { const taskDir = path.dirname(task.entry) const testPath = path.join(baseDir, taskDir, '..', 'tests') diff --git a/packages/cli/src/lib/MimicConfigHandler.ts b/packages/cli/src/lib/MimicConfigHandler.ts index d7818702..70b64508 100644 --- a/packages/cli/src/lib/MimicConfigHandler.ts +++ b/packages/cli/src/lib/MimicConfigHandler.ts @@ -16,16 +16,25 @@ export default { load(command: Command, baseDir: string = process.cwd()): MimicConfig { const mimicConfigPath = path.join(baseDir, MIMIC_CONFIG_FILE) - let loadedMimicConfig - try { - loadedMimicConfig = load(fs.readFileSync(mimicConfigPath, 'utf-8')) - } catch { + + if (!fs.existsSync(mimicConfigPath)) { command.error(`Could not find ${mimicConfigPath}`, { code: 'FileNotFound', suggestions: ['Ensure mimic.yaml exists in the project root'], }) } + let loadedMimicConfig + try { + loadedMimicConfig = load(fs.readFileSync(mimicConfigPath, 'utf-8')) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (err) { + command.error(`Failed to parse ${mimicConfigPath} as YAML`, { + code: 'ParseError', + suggestions: ['Ensure mimic.yaml is valid YAML syntax'], + }) + } + try { return MimicConfigValidator.parse(loadedMimicConfig) } catch (err) { From 95672444b32f5778299fdcfb6f919c4ed3565d43 Mon Sep 17 00:00:00 2001 From: ncomerci Date: Tue, 30 Dec 2025 11:28:43 -0300 Subject: [PATCH 04/27] fix: build command --- packages/cli/src/commands/build.ts | 12 ++++++++++-- packages/cli/src/commands/codegen.ts | 9 +++++++-- packages/cli/src/commands/compile.ts | 16 ++++++++++++++-- 3 files changed, 31 insertions(+), 6 deletions(-) diff --git a/packages/cli/src/commands/build.ts b/packages/cli/src/commands/build.ts index 1a2c42c9..2ff65b14 100644 --- a/packages/cli/src/commands/build.ts +++ b/packages/cli/src/commands/build.ts @@ -46,12 +46,20 @@ export default class Build extends Command { } private async runForTask(task: Omit, clean: boolean): Promise { - const codegenArgs: string[] = ['--manifest', task.manifest, '--output', task.types] + const codegenArgs: string[] = ['--manifest', task.manifest, '--output', task.types, '--skip-config'] if (clean) codegenArgs.push('--clean') await Codegen.run(codegenArgs) - const compileArgs: string[] = ['--task', task.entry, '--manifest', task.manifest, '--output', task.output] + const compileArgs: string[] = [ + '--task', + task.entry, + '--manifest', + task.manifest, + '--output', + task.output, + '--skip-config', + ] await Compile.run(compileArgs) } } diff --git a/packages/cli/src/commands/codegen.ts b/packages/cli/src/commands/codegen.ts index e0a990e3..99c5a5cc 100644 --- a/packages/cli/src/commands/codegen.ts +++ b/packages/cli/src/commands/codegen.ts @@ -21,14 +21,19 @@ export default class Codegen extends Command { description: 'Remove existing generated types before generating new files', default: false, }), + ['skip-config']: Flags.boolean({ + hidden: true, + description: 'Skip mimic.yaml config (used internally by build command)', + default: false, + }), ...taskFilterFlags, } public async run(): Promise { const { flags } = await this.parse(Codegen) - const { manifest: manifestDir, output: outputDir, clean, include, exclude } = flags + const { manifest: manifestDir, output: outputDir, clean, include, exclude, ['skip-config']: skipConfig } = flags - if (MimicConfigHandler.exists()) { + if (!skipConfig && MimicConfigHandler.exists()) { const mimicConfig = MimicConfigHandler.load(this) const allTasks = MimicConfigHandler.getTasks(mimicConfig) const tasks = filterTasks(this, allTasks, include, exclude) diff --git a/packages/cli/src/commands/compile.ts b/packages/cli/src/commands/compile.ts index 171f1bb3..a077d079 100644 --- a/packages/cli/src/commands/compile.ts +++ b/packages/cli/src/commands/compile.ts @@ -18,14 +18,26 @@ export default class Compile extends Command { task: Flags.string({ char: 't', description: 'task to compile', default: 'src/task.ts' }), manifest: Flags.string({ char: 'm', description: 'manifest to validate', default: 'manifest.yaml' }), output: Flags.string({ char: 'o', description: 'output directory', default: './build' }), + ['skip-config']: Flags.boolean({ + hidden: true, + description: 'Skip mimic.yaml config (used internally by build command)', + default: false, + }), ...taskFilterFlags, } public async run(): Promise { const { flags } = await this.parse(Compile) - const { task: taskFile, output: outputDir, manifest: manifestDir, include, exclude } = flags + const { + task: taskFile, + output: outputDir, + manifest: manifestDir, + include, + exclude, + ['skip-config']: skipConfig, + } = flags - if (MimicConfigHandler.exists()) { + if (!skipConfig && MimicConfigHandler.exists()) { const mimicConfig = MimicConfigHandler.load(this) const allTasks = MimicConfigHandler.getTasks(mimicConfig) const tasks = filterTasks(this, allTasks, include, exclude) From 61a902939de76c9c1e22e9d8ba6d3e9e1995caba Mon Sep 17 00:00:00 2001 From: ncomerci Date: Tue, 30 Dec 2025 11:43:10 -0300 Subject: [PATCH 05/27] fix: test command --- packages/cli/src/commands/test.ts | 63 +++++++++++++++++++------------ 1 file changed, 38 insertions(+), 25 deletions(-) diff --git a/packages/cli/src/commands/test.ts b/packages/cli/src/commands/test.ts index 808e2d9d..37ca8c23 100644 --- a/packages/cli/src/commands/test.ts +++ b/packages/cli/src/commands/test.ts @@ -27,43 +27,56 @@ export default class Test extends Command { const { 'skip-compile': skipCompile, include, exclude } = flags const baseDir = path.resolve(directory) + const testPaths = new Set() + if (MimicConfigHandler.exists(baseDir)) { const mimicConfig = MimicConfigHandler.load(this, baseDir) const allTasks = MimicConfigHandler.getTasks(mimicConfig) const tasks = filterTasks(this, allTasks, include, exclude) + for (const task of tasks) { - console.log(`\n${log.highlightText(`[${task.name}]`)}`) - await this.runForTask(task, baseDir, skipCompile) + if (!skipCompile) { + console.log(`\n${log.highlightText(`[${task.name}]`)}`) + await this.compileTask(task, baseDir) + } + testPaths.add(this.getTestPath(task, baseDir)) } } else { - await this.runForTask( - { manifest: 'manifest.yaml', entry: 'src/task.ts', types: './src/types', output: './build' }, - baseDir, - skipCompile - ) + const defaultTask = { + manifest: 'manifest.yaml', + entry: 'src/task.ts', + types: './src/types', + output: './build', + } + if (!skipCompile) await this.compileTask(defaultTask, baseDir) + testPaths.add(this.getTestPath(defaultTask, baseDir)) } + + if (testPaths.size > 0) this.runTests(Array.from(testPaths), baseDir) } - private async runForTask( - task: Omit, - baseDir: string, - skipCompile: boolean - ): Promise { - const taskDir = path.dirname(task.entry) - const testPath = path.join(baseDir, taskDir, '..', 'tests') + private async compileTask(task: Omit, baseDir: string): Promise { + const cg = execBinCommand( + 'mimic', + ['codegen', '--manifest', task.manifest, '--output', task.types, '--skip-config'], + baseDir + ) + if (cg.status !== 0) this.exit(cg.status ?? 1) + const cp = execBinCommand( + 'mimic', + ['compile', '--task', task.entry, '--manifest', task.manifest, '--output', task.output, '--skip-config'], + baseDir + ) + if (cp.status !== 0) this.exit(cp.status ?? 1) + } - if (!skipCompile) { - const cg = execBinCommand('mimic', ['codegen', '--manifest', task.manifest, '--output', task.types], baseDir) - if (cg.status !== 0) this.exit(cg.status ?? 1) - const cp = execBinCommand( - 'mimic', - ['compile', '--task', task.entry, '--manifest', task.manifest, '--output', task.output], - baseDir - ) - if (cp.status !== 0) this.exit(cp.status ?? 1) - } + private getTestPath(task: Omit, baseDir: string): string { + const taskDir = path.dirname(task.entry) + return path.join(baseDir, taskDir, '..', 'tests', '**', '*.spec.ts') + } - const result = execBinCommand('tsx', ['./node_modules/mocha/bin/mocha.js', `${testPath}/**/*.spec.ts`], baseDir) + private runTests(testPaths: string[], baseDir: string): void { + const result = execBinCommand('tsx', ['./node_modules/mocha/bin/mocha.js', ...testPaths], baseDir) if (result.status !== 0) this.exit(result.status ?? 1) } } From 7fe826ac4864ed8ca036db2a723be3192ca2503a Mon Sep 17 00:00:00 2001 From: ncomerci Date: Tue, 30 Dec 2025 12:06:02 -0300 Subject: [PATCH 06/27] fix: getTestPath --- packages/cli/src/commands/test.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/cli/src/commands/test.ts b/packages/cli/src/commands/test.ts index 37ca8c23..867db00a 100644 --- a/packages/cli/src/commands/test.ts +++ b/packages/cli/src/commands/test.ts @@ -39,7 +39,7 @@ export default class Test extends Command { console.log(`\n${log.highlightText(`[${task.name}]`)}`) await this.compileTask(task, baseDir) } - testPaths.add(this.getTestPath(task, baseDir)) + testPaths.add(this.getTestPath(baseDir)) } } else { const defaultTask = { @@ -49,7 +49,7 @@ export default class Test extends Command { output: './build', } if (!skipCompile) await this.compileTask(defaultTask, baseDir) - testPaths.add(this.getTestPath(defaultTask, baseDir)) + testPaths.add(this.getTestPath(baseDir)) } if (testPaths.size > 0) this.runTests(Array.from(testPaths), baseDir) @@ -70,9 +70,8 @@ export default class Test extends Command { if (cp.status !== 0) this.exit(cp.status ?? 1) } - private getTestPath(task: Omit, baseDir: string): string { - const taskDir = path.dirname(task.entry) - return path.join(baseDir, taskDir, '..', 'tests', '**', '*.spec.ts') + private getTestPath(baseDir: string): string { + return path.join(baseDir, 'tests', '**', '*.spec.ts') } private runTests(testPaths: string[], baseDir: string): void { From 6c1cb7850864f15f6c871eb77591076221d03518 Mon Sep 17 00:00:00 2001 From: ncomerci Date: Tue, 6 Jan 2026 14:29:38 -0300 Subject: [PATCH 07/27] chore: requested changes --- packages/cli/src/commands/build.ts | 9 ++++++-- packages/cli/src/commands/deploy.ts | 34 +++++++++++++++++------------ packages/cli/src/helpers.ts | 12 +++++++--- 3 files changed, 36 insertions(+), 19 deletions(-) diff --git a/packages/cli/src/commands/build.ts b/packages/cli/src/commands/build.ts index 2ff65b14..dacc3d20 100644 --- a/packages/cli/src/commands/build.ts +++ b/packages/cli/src/commands/build.ts @@ -25,14 +25,19 @@ export default class Build extends Command { description: 'remove existing generated types before generating new files', default: false, }), + ['skip-config']: Flags.boolean({ + hidden: true, + description: 'Skip mimic.yaml config (used internally)', + default: false, + }), ...taskFilterFlags, } public async run(): Promise { const { flags } = await this.parse(Build) - const { manifest, task, output, types, clean, include, exclude } = flags + const { manifest, task, output, types, clean, include, exclude, ['skip-config']: skipConfig } = flags - if (MimicConfigHandler.exists()) { + if (!skipConfig && MimicConfigHandler.exists()) { const mimicConfig = MimicConfigHandler.load(this) const allTasks = MimicConfigHandler.getTasks(mimicConfig) const tasks = filterTasks(this, allTasks, include, exclude) diff --git a/packages/cli/src/commands/deploy.ts b/packages/cli/src/commands/deploy.ts index 62cd33a2..7c589ffb 100644 --- a/packages/cli/src/commands/deploy.ts +++ b/packages/cli/src/commands/deploy.ts @@ -81,38 +81,44 @@ export default class Deploy extends Authenticate { const credentials = this.authenticate({ profile, 'api-key': apiKey }) if (!skipCompile) { - const codegen = execBinCommand( + const build = execBinCommand( 'mimic', - ['codegen', '--manifest', task.manifest, '--output', task.types], + [ + 'build', + '--manifest', + task.manifest, + '--task', + task.entry, + '--output', + fullInputDir, + '--types', + task.types, + '--skip-config', + ], process.cwd() ) - if (codegen.status !== 0) - this.error('Code generation failed', { code: 'CodegenError', suggestions: ['Fix manifest and ABI files'] }) - - const compile = execBinCommand( - 'mimic', - ['compile', '--task', task.entry, '--manifest', task.manifest, '--output', fullInputDir], - process.cwd() - ) - if (compile.status !== 0) - this.error('Compilation failed', { code: 'BuildError', suggestions: ['Check the task source code'] }) + if (build.status !== 0) { + this.error('Build failed', { code: 'BuildError', suggestions: ['Check the task source code and manifest'] }) + } } log.startAction('Validating') - if (!fs.existsSync(fullInputDir)) + if (!fs.existsSync(fullInputDir)) { this.error(`Directory ${log.highlightText(fullInputDir)} does not exist`, { code: 'Directory Not Found', suggestions: ['Use the --input flag to specify the correct path'], }) + } const neededFiles = ['manifest.json', 'task.wasm'].map((file) => join(fullInputDir, file)) for (const file of neededFiles) { - if (!fs.existsSync(file)) + if (!fs.existsSync(file)) { this.error(`Could not find ${file}`, { code: 'File Not Found', suggestions: [`Use ${log.highlightText('mimic compile')} to generate the needed files`], }) + } } log.startAction('Uploading to Mimic Registry') diff --git a/packages/cli/src/helpers.ts b/packages/cli/src/helpers.ts index 711b25c6..f6163684 100644 --- a/packages/cli/src/helpers.ts +++ b/packages/cli/src/helpers.ts @@ -47,8 +47,9 @@ export function filterTasks( if (include) { const invalidNames = include.filter((name) => !taskNames.has(name)) - if (invalidNames.length > 0) + if (invalidNames.length > 0) { console.warn(`${log.warnText('Warning:')} The following task names were not found: ${invalidNames.join(', ')}`) + } const validNames = new Set(include.filter((name) => taskNames.has(name))) if (validNames.size === 0) { @@ -61,11 +62,16 @@ export function filterTasks( if (exclude) { const invalidNames = exclude.filter((name) => !taskNames.has(name)) - if (invalidNames.length > 0) + if (invalidNames.length > 0) { console.warn(`${log.warnText('Warning:')} The following task names were not found: ${invalidNames.join(', ')}`) + } const excludeSet = new Set(exclude) - return tasks.filter((task) => !excludeSet.has(task.name)) + const filteredTasks = tasks.filter((task) => !excludeSet.has(task.name)) + if (filteredTasks.length === 0) { + console.warn(`${log.warnText('Warning:')} All tasks are excluded.`) + } + return filteredTasks } return tasks From 4fdf927112128e19778fe2e57a724efe4d63d482 Mon Sep 17 00:00:00 2001 From: ncomerci Date: Wed, 7 Jan 2026 16:15:18 -0300 Subject: [PATCH 08/27] test: added mimic config cases --- packages/cli/tests/commands/build.spec.ts | 181 +++++++----- packages/cli/tests/commands/codegen.spec.ts | 140 ++++++---- packages/cli/tests/commands/compile.spec.ts | 176 +++++++----- packages/cli/tests/commands/deploy.spec.ts | 294 ++++++++++++-------- 4 files changed, 486 insertions(+), 305 deletions(-) diff --git a/packages/cli/tests/commands/build.spec.ts b/packages/cli/tests/commands/build.spec.ts index 26fd8737..48d82b4b 100644 --- a/packages/cli/tests/commands/build.spec.ts +++ b/packages/cli/tests/commands/build.spec.ts @@ -11,10 +11,12 @@ describe('build', () => { const manifestPath = `${basePath}/manifests/manifest.yaml` const outputDir = `${basePath}/output` const typesDir = `${basePath}/src/types` + const mimicConfigPath = path.join(process.cwd(), 'mimic.yaml') afterEach('cleanup generated files', () => { if (fs.existsSync(outputDir)) fs.rmSync(outputDir, { recursive: true }) if (fs.existsSync(typesDir)) fs.rmSync(typesDir, { recursive: true }) + if (fs.existsSync(mimicConfigPath)) fs.unlinkSync(mimicConfigPath) }) const buildCommand = (args: string[] = []) => { @@ -46,95 +48,136 @@ describe('build', () => { }) } - context('when the manifest exists', () => { - context('when the manifest is valid', () => { - context('when the task compiles successfully', () => { - context('when the manifest has simple inputs', () => { - const expectedInputs = { - firstStaticNumber: 'uint32', - secondStaticNumber: 'uint32', - isTrue: 'bool', - } - itBuildsAndGeneratesTypes(manifestPath, expectedInputs) - }) + context('when the mimic config exists', () => { + beforeEach('create mimic.yaml', () => { + fs.writeFileSync( + mimicConfigPath, + `tasks:\n - name: test-task\n manifest: ${manifestPath}\n entry: ${taskPath}\n output: ${outputDir}\n types: ${typesDir}\n` + ) + }) - context('when the manifest has inputs with descriptions', () => { - const manifestWithDescriptions = `${basePath}/manifests/manifest-with-descriptions.yaml` - const expectedInputs = { - firstStaticNumber: 'uint32', - describedNumber: { - type: 'uint32', - description: 'A number parameter with detailed description', - }, - tokenAddress: { - type: 'address', - description: 'The address of the ERC20 token contract', - }, - simpleFlag: 'bool', - } - itBuildsAndGeneratesTypes(manifestWithDescriptions, expectedInputs) - }) - }) + it('generates types and build artifacts for task from config', async () => { + const expectedInputs = { + firstStaticNumber: 'uint32', + secondStaticNumber: 'uint32', + isTrue: 'bool', + } - context('when the task fails to compile', () => { - const invalidTaskPath = `${basePath}/tasks/invalid-task.ts` - const command = buildCommand(withCommonFlags(manifestPath, invalidTaskPath, outputDir, typesDir)) + const command = buildCommand() + const { stdout, error } = await runCommand(command) - itThrowsACliError(command, 'AssemblyScript compilation failed', 'BuildError', 1) - }) + expect(error).to.be.undefined + expect(stdout).to.include('[test-task]') + expect(stdout).to.include('Build complete!') - context('when the types output directory already exists', () => { - beforeEach('pre-create types directory with a file', () => { - if (!fs.existsSync(typesDir)) fs.mkdirSync(typesDir, { recursive: true }) - fs.writeFileSync(path.join(typesDir, 'randomFile.txt'), 'a') + // build artifacts + expect(fs.existsSync(path.join(outputDir, 'task.wasm'))).to.be.true + expect(fs.existsSync(path.join(outputDir, 'manifest.json'))).to.be.true + + // generated types + expect(fs.existsSync(path.join(typesDir, 'index.ts'))).to.be.true + expect(fs.existsSync(path.join(typesDir, 'ERC20.ts'))).to.be.true + + const manifestJson = JSON.parse(fs.readFileSync(path.join(outputDir, 'manifest.json'), 'utf-8')) + expect(manifestJson.inputs).to.be.deep.equal(expectedInputs) + }) + }) + + context('when the mimic config does not exist', () => { + beforeEach('ensure mimic.yaml does not exist', () => { + if (fs.existsSync(mimicConfigPath)) fs.unlinkSync(mimicConfigPath) + }) + + context('when the manifest exists', () => { + context('when the manifest is valid', () => { + context('when the task compiles successfully', () => { + context('when the manifest has simple inputs', () => { + const expectedInputs = { + firstStaticNumber: 'uint32', + secondStaticNumber: 'uint32', + isTrue: 'bool', + } + itBuildsAndGeneratesTypes(manifestPath, expectedInputs) + }) + + context('when the manifest has inputs with descriptions', () => { + const manifestWithDescriptions = `${basePath}/manifests/manifest-with-descriptions.yaml` + const expectedInputs = { + firstStaticNumber: 'uint32', + describedNumber: { + type: 'uint32', + description: 'A number parameter with detailed description', + }, + tokenAddress: { + type: 'address', + description: 'The address of the ERC20 token contract', + }, + simpleFlag: 'bool', + } + itBuildsAndGeneratesTypes(manifestWithDescriptions, expectedInputs) + }) }) - it('generates types without requiring clean', async () => { - const command = buildCommand(withCommonFlags(manifestPath, taskPath, outputDir, typesDir)) - const { error } = await runCommand(command) + context('when the task fails to compile', () => { + const invalidTaskPath = `${basePath}/tasks/invalid-task.ts` + const command = buildCommand(withCommonFlags(manifestPath, invalidTaskPath, outputDir, typesDir)) - expect(error).to.be.undefined - expect(fs.existsSync(path.join(typesDir, 'index.ts'))).to.be.true - expect(fs.existsSync(path.join(typesDir, 'ERC20.ts'))).to.be.true + itThrowsACliError(command, 'AssemblyScript compilation failed', 'BuildError', 1) }) - }) - }) - context('when the manifest is not valid', () => { - context('when the manifest has invalid fields', () => { - const invalidManifest = `${basePath}/manifests/invalid-manifest.yaml` - const command = buildCommand(withCommonFlags(invalidManifest, taskPath, outputDir, typesDir)) + context('when the types output directory already exists', () => { + beforeEach('pre-create types directory with a file', () => { + if (!fs.existsSync(typesDir)) fs.mkdirSync(typesDir, { recursive: true }) + fs.writeFileSync(path.join(typesDir, 'randomFile.txt'), 'a') + }) + + it('generates types without requiring clean', async () => { + const command = buildCommand(withCommonFlags(manifestPath, taskPath, outputDir, typesDir)) + const { error } = await runCommand(command) - itThrowsACliError(command, 'More than one entry', 'MoreThanOneEntryError', 1) + expect(error).to.be.undefined + expect(fs.existsSync(path.join(typesDir, 'index.ts'))).to.be.true + expect(fs.existsSync(path.join(typesDir, 'ERC20.ts'))).to.be.true + }) + }) }) - context('when the manifest has repeated fields', () => { - const invalidManifest = `${basePath}/manifests/invalid-manifest-repeated.yaml` - const command = buildCommand(withCommonFlags(invalidManifest, taskPath, outputDir, typesDir)) + context('when the manifest is not valid', () => { + context('when the manifest has invalid fields', () => { + const invalidManifest = `${basePath}/manifests/invalid-manifest.yaml` + const command = buildCommand(withCommonFlags(invalidManifest, taskPath, outputDir, typesDir)) - itThrowsACliError(command, 'Duplicate Entry', 'DuplicateEntryError', 1) - }) + itThrowsACliError(command, 'More than one entry', 'MoreThanOneEntryError', 1) + }) - context('when the manifest is incomplete', () => { - const invalidManifest = `${basePath}/manifests/incomplete-manifest.yaml` - const command = buildCommand(withCommonFlags(invalidManifest, taskPath, outputDir, typesDir)) + context('when the manifest has repeated fields', () => { + const invalidManifest = `${basePath}/manifests/invalid-manifest-repeated.yaml` + const command = buildCommand(withCommonFlags(invalidManifest, taskPath, outputDir, typesDir)) - itThrowsACliError(command, 'Missing/Incorrect Fields', 'FieldsError', 3) - }) + itThrowsACliError(command, 'Duplicate Entry', 'DuplicateEntryError', 1) + }) - context('when the manifest is empty', () => { - const invalidManifest = `${basePath}/manifests/empty-manifest.yaml` - const command = buildCommand(withCommonFlags(invalidManifest, taskPath, outputDir, typesDir)) + context('when the manifest is incomplete', () => { + const invalidManifest = `${basePath}/manifests/incomplete-manifest.yaml` + const command = buildCommand(withCommonFlags(invalidManifest, taskPath, outputDir, typesDir)) + + itThrowsACliError(command, 'Missing/Incorrect Fields', 'FieldsError', 3) + }) - itThrowsACliError(command, 'Empty Manifest', 'EmptyManifestError', 1) + context('when the manifest is empty', () => { + const invalidManifest = `${basePath}/manifests/empty-manifest.yaml` + const command = buildCommand(withCommonFlags(invalidManifest, taskPath, outputDir, typesDir)) + + itThrowsACliError(command, 'Empty Manifest', 'EmptyManifestError', 1) + }) }) }) - }) - context('when the manifest does not exist', () => { - const inexistentManifest = `${manifestPath}-none` - const command = buildCommand(withCommonFlags(inexistentManifest, taskPath, outputDir, typesDir)) + context('when the manifest does not exist', () => { + const inexistentManifest = `${manifestPath}-none` + const command = buildCommand(withCommonFlags(inexistentManifest, taskPath, outputDir, typesDir)) - itThrowsACliError(command, `Could not find ${inexistentManifest}`, 'FileNotFound', 1) + itThrowsACliError(command, `Could not find ${inexistentManifest}`, 'FileNotFound', 1) + }) }) }) diff --git a/packages/cli/tests/commands/codegen.spec.ts b/packages/cli/tests/commands/codegen.spec.ts index 8d6fefe3..c4965ad9 100644 --- a/packages/cli/tests/commands/codegen.spec.ts +++ b/packages/cli/tests/commands/codegen.spec.ts @@ -2,6 +2,7 @@ import { runCommand } from '@oclif/test' import { expect } from 'chai' import { spawnSync } from 'child_process' import * as fs from 'fs' +import * as path from 'path' import { itThrowsACliError } from '../helpers' @@ -9,86 +10,113 @@ describe('codegen', () => { const basePath = `${__dirname}/../fixtures` const manifestPath = `${basePath}/manifests/manifest.yaml` const outputDir = `${basePath}/src/types` + const mimicConfigPath = path.join(process.cwd(), 'mimic.yaml') afterEach('delete generated files', () => { if (fs.existsSync(outputDir)) fs.rmSync(outputDir, { recursive: true }) + if (fs.existsSync(mimicConfigPath)) fs.unlinkSync(mimicConfigPath) }) - context('when the manifest exists', () => { - context('when clean flag is not passed', () => { - const command = ['codegen', `--manifest ${manifestPath}`, `--output ${outputDir}`] + context('when the mimic config exists', () => { + beforeEach('create mimic.yaml', () => { + fs.writeFileSync( + mimicConfigPath, + `tasks:\n - name: test-task\n manifest: ${manifestPath}\n entry: ${basePath}/tasks/task.ts\n types: ${outputDir}\n` + ) + }) + + it('generates correctly for task from config', async () => { + const command = ['codegen'] + const { stdout, error } = await runCommand(command) + + expect(error).to.be.undefined + expect(stdout).to.include('[test-task]') + expect(fs.existsSync(`${outputDir}/ERC20.ts`)).to.be.true + expect(fs.existsSync(`${outputDir}/index.ts`)).to.be.true + }) + }) - context('when there are inputs and abis', () => { - it('generates correctly', async () => { - const { error } = await runCommand(command) - expect(error).to.be.undefined - expect(fs.existsSync(`${outputDir}/ERC20.ts`)).to.be.true - expect(fs.existsSync(`${outputDir}/index.ts`)).to.be.true + context('when the mimic config does not exist', () => { + beforeEach('ensure mimic.yaml does not exist', () => { + if (fs.existsSync(mimicConfigPath)) fs.unlinkSync(mimicConfigPath) + }) + + context('when the manifest exists', () => { + context('when clean flag is not passed', () => { + const command = ['codegen', `--manifest ${manifestPath}`, `--output ${outputDir}`] + + context('when there are inputs and abis', () => { + it('generates correctly', async () => { + const { error } = await runCommand(command) + expect(error).to.be.undefined + expect(fs.existsSync(`${outputDir}/ERC20.ts`)).to.be.true + expect(fs.existsSync(`${outputDir}/index.ts`)).to.be.true + }) }) - }) - context('when there are no inputs or abis', () => { - const command = ['codegen', `--manifest ${basePath}/manifests/simple-manifest.yaml`, `--output ${outputDir}`] + context('when there are no inputs or abis', () => { + const command = ['codegen', `--manifest ${basePath}/manifests/simple-manifest.yaml`, `--output ${outputDir}`] - it('generates nothing', async () => { - const { error } = await runCommand(command) - expect(error).to.be.undefined - expect(fs.existsSync(`${outputDir}/ERC20.ts`)).to.be.false - expect(fs.existsSync(`${outputDir}/ERC20.ts`)).to.be.false - expect(fs.existsSync(`${outputDir}`)).to.be.false + it('generates nothing', async () => { + const { error } = await runCommand(command) + expect(error).to.be.undefined + expect(fs.existsSync(`${outputDir}/ERC20.ts`)).to.be.false + expect(fs.existsSync(`${outputDir}/ERC20.ts`)).to.be.false + expect(fs.existsSync(`${outputDir}`)).to.be.false + }) }) }) }) - }) - context('when the manifest does not exist', () => { - const command = ['codegen', `--manifest ${manifestPath}fake`, `--output ${outputDir}`] - - itThrowsACliError(command, `Could not find ${manifestPath}fake`, 'FileNotFound', 1) - }) + context('when the manifest does not exist', () => { + const command = ['codegen', `--manifest ${manifestPath}fake`, `--output ${outputDir}`] - context('when clean flag is passed', () => { - let userResponse - const command = ['codegen', `--manifest ${manifestPath}`, `--output ${outputDir}`, '--clean'] + itThrowsACliError(command, `Could not find ${manifestPath}fake`, 'FileNotFound', 1) + }) - context('when the user accepts the confirmation', () => { - beforeEach('stub user input', () => { - userResponse = 'Y' - }) + context('when clean flag is passed', () => { + let userResponse + const command = ['codegen', `--manifest ${manifestPath}`, `--output ${outputDir}`, '--clean'] - context('when the directory exists', () => { - beforeEach('create directory', () => { - fs.mkdirSync(outputDir, { recursive: true }) + context('when the user accepts the confirmation', () => { + beforeEach('stub user input', () => { + userResponse = 'Y' }) - it("deletes the folder and it's contents", async () => { - const { status } = runCommandWithUserInput(command, userResponse) - expect(status).to.be.equal(0) - expect(fs.existsSync(`${outputDir}/index.ts`)).to.be.true - expect(fs.existsSync(`${outputDir}/ERC20.ts`)).to.be.true + context('when the directory exists', () => { + beforeEach('create directory', () => { + fs.mkdirSync(outputDir, { recursive: true }) + }) + + it("deletes the folder and it's contents", async () => { + const { status } = runCommandWithUserInput(command, userResponse) + expect(status).to.be.equal(0) + expect(fs.existsSync(`${outputDir}/index.ts`)).to.be.true + expect(fs.existsSync(`${outputDir}/ERC20.ts`)).to.be.true + }) }) - }) - context('when the directory does not exist', () => { - it("deletes the folder and it's contents", async () => { - const { status } = runCommandWithUserInput(command, userResponse) - expect(status).to.be.equal(0) - expect(fs.existsSync(`${outputDir}/index.ts`)).to.be.true - expect(fs.existsSync(`${outputDir}/ERC20.ts`)).to.be.true + context('when the directory does not exist', () => { + it("deletes the folder and it's contents", async () => { + const { status } = runCommandWithUserInput(command, userResponse) + expect(status).to.be.equal(0) + expect(fs.existsSync(`${outputDir}/index.ts`)).to.be.true + expect(fs.existsSync(`${outputDir}/ERC20.ts`)).to.be.true + }) }) }) - }) - context('when the user rejects the confirmation', () => { - beforeEach('stub user input', () => { - userResponse = 'N' - }) + context('when the user rejects the confirmation', () => { + beforeEach('stub user input', () => { + userResponse = 'N' + }) - it('stops execution', async () => { - const { stdout, status } = runCommandWithUserInput(command, userResponse) - expect(status).to.be.equal(0) - expect(stdout).to.include('You can remove the --clean flag from your command') - expect(stdout).to.include('Stopping initialization...') + it('stops execution', async () => { + const { stdout, status } = runCommandWithUserInput(command, userResponse) + expect(status).to.be.equal(0) + expect(stdout).to.include('You can remove the --clean flag from your command') + expect(stdout).to.include('Stopping initialization...') + }) }) }) }) diff --git a/packages/cli/tests/commands/compile.spec.ts b/packages/cli/tests/commands/compile.spec.ts index 61bbdb47..ed3b9527 100644 --- a/packages/cli/tests/commands/compile.spec.ts +++ b/packages/cli/tests/commands/compile.spec.ts @@ -10,9 +10,11 @@ describe('compile', () => { const taskPath = `${basePath}/tasks/task.ts` const manifestPath = `${basePath}/manifests/manifest.yaml` const outputDir = `${basePath}/output` + const mimicConfigPath = path.join(process.cwd(), 'mimic.yaml') afterEach('delete generated files', () => { if (fs.existsSync(outputDir)) fs.rmSync(outputDir, { recursive: true }) + if (fs.existsSync(mimicConfigPath)) fs.unlinkSync(mimicConfigPath) }) const buildCommand = (manifestPath: string, taskPath: string, outputDir: string) => { @@ -36,93 +38,129 @@ describe('compile', () => { }) } - context('when the manifest exists', () => { - context('when the manifest is valid', () => { - context('when the task compiles successfully', () => { - context('when the manifest has simple inputs', () => { - const expectedInputs = { - firstStaticNumber: 'uint32', - secondStaticNumber: 'uint32', - isTrue: 'bool', - } - itCreatesFilesCorrectly(manifestPath, expectedInputs) - }) + context('when the mimic config exists', () => { + beforeEach('create mimic.yaml', () => { + fs.writeFileSync( + mimicConfigPath, + `tasks:\n - name: test-task\n manifest: ${manifestPath}\n entry: ${taskPath}\n output: ${outputDir}\n` + ) + }) - context('when the manifest has inputs with descriptions', () => { - const manifestPath = `${basePath}/manifests/manifest-with-descriptions.yaml` - const expectedInputs = { - firstStaticNumber: 'uint32', - describedNumber: { - type: 'uint32', - description: 'A number parameter with detailed description', - }, - tokenAddress: { - type: 'address', - description: 'The address of the ERC20 token contract', - }, - simpleFlag: 'bool', - } - itCreatesFilesCorrectly(manifestPath, expectedInputs) - }) - }) + it('creates the files correctly for task from config', async () => { + const expectedInputs = { + firstStaticNumber: 'uint32', + secondStaticNumber: 'uint32', + isTrue: 'bool', + } - context('when the task fails to compile', () => { - const taskPath = `${basePath}/tasks/invalid-task.ts` - const command = buildCommand(manifestPath, taskPath, outputDir) + const command = ['compile'] + const { stdout, error } = await runCommand(command) - itThrowsACliError(command, 'AssemblyScript compilation failed', 'BuildError', 1) - }) + expect(error).to.be.undefined + expect(stdout).to.include('[test-task]') + expect(stdout).to.include('Build complete!') + + expect(fs.existsSync(`${outputDir}/task.wasm`)).to.be.true + expect(fs.existsSync(`${outputDir}/manifest.json`)).to.be.true + + const manifest = JSON.parse(fs.readFileSync(`${outputDir}/manifest.json`, 'utf-8')) + expect(manifest.inputs).to.be.deep.equal(expectedInputs) }) + }) - context('when the manifest is not valid', () => { - context('when the manfiest has invalid fields', () => { - const manifestPath = `${basePath}/manifests/invalid-manifest.yaml` - const command = buildCommand(manifestPath, taskPath, outputDir) + context('when the mimic config does not exist', () => { + beforeEach('ensure mimic.yaml does not exist', () => { + if (fs.existsSync(mimicConfigPath)) fs.unlinkSync(mimicConfigPath) + }) - itThrowsACliError(command, 'More than one entry', 'MoreThanOneEntryError', 1) - }) + context('when the manifest exists', () => { + context('when the manifest is valid', () => { + context('when the task compiles successfully', () => { + context('when the manifest has simple inputs', () => { + const expectedInputs = { + firstStaticNumber: 'uint32', + secondStaticNumber: 'uint32', + isTrue: 'bool', + } + itCreatesFilesCorrectly(manifestPath, expectedInputs) + }) + + context('when the manifest has inputs with descriptions', () => { + const manifestPath = `${basePath}/manifests/manifest-with-descriptions.yaml` + const expectedInputs = { + firstStaticNumber: 'uint32', + describedNumber: { + type: 'uint32', + description: 'A number parameter with detailed description', + }, + tokenAddress: { + type: 'address', + description: 'The address of the ERC20 token contract', + }, + simpleFlag: 'bool', + } + itCreatesFilesCorrectly(manifestPath, expectedInputs) + }) + }) - context('when the manfiest has repeated fields', () => { - const manifestPath = `${basePath}/manifests/invalid-manifest-repeated.yaml` - const command = buildCommand(manifestPath, taskPath, outputDir) + context('when the task fails to compile', () => { + const taskPath = `${basePath}/tasks/invalid-task.ts` + const command = buildCommand(manifestPath, taskPath, outputDir) - itThrowsACliError(command, 'Duplicate Entry', 'DuplicateEntryError', 1) + itThrowsACliError(command, 'AssemblyScript compilation failed', 'BuildError', 1) + }) }) - context('when the manifest is incomplete', () => { - const manifestPath = `${basePath}/manifests/incomplete-manifest.yaml` - const command = buildCommand(manifestPath, taskPath, outputDir) + context('when the manifest is not valid', () => { + context('when the manfiest has invalid fields', () => { + const manifestPath = `${basePath}/manifests/invalid-manifest.yaml` + const command = buildCommand(manifestPath, taskPath, outputDir) - itThrowsACliError(command, 'Missing/Incorrect Fields', 'FieldsError', 3) - }) + itThrowsACliError(command, 'More than one entry', 'MoreThanOneEntryError', 1) + }) + + context('when the manfiest has repeated fields', () => { + const manifestPath = `${basePath}/manifests/invalid-manifest-repeated.yaml` + const command = buildCommand(manifestPath, taskPath, outputDir) - context('when the manifest is empty', () => { - const manifestPath = `${basePath}/manifests/empty-manifest.yaml` - const command = buildCommand(manifestPath, taskPath, outputDir) + itThrowsACliError(command, 'Duplicate Entry', 'DuplicateEntryError', 1) + }) + + context('when the manifest is incomplete', () => { + const manifestPath = `${basePath}/manifests/incomplete-manifest.yaml` + const command = buildCommand(manifestPath, taskPath, outputDir) + + itThrowsACliError(command, 'Missing/Incorrect Fields', 'FieldsError', 3) + }) - itThrowsACliError(command, 'Empty Manifest', 'EmptyManifestError', 1) + context('when the manifest is empty', () => { + const manifestPath = `${basePath}/manifests/empty-manifest.yaml` + const command = buildCommand(manifestPath, taskPath, outputDir) + + itThrowsACliError(command, 'Empty Manifest', 'EmptyManifestError', 1) + }) }) }) - }) - - context('when the manifest does not exist', () => { - const inexistentManifestPath = `${manifestPath}-none` - const command = buildCommand(inexistentManifestPath, taskPath, outputDir) - itThrowsACliError(command, `Could not find ${inexistentManifestPath}`, 'FileNotFound', 1) - }) + context('when the manifest does not exist', () => { + const inexistentManifestPath = `${manifestPath}-none` + const command = buildCommand(inexistentManifestPath, taskPath, outputDir) - context('when the output directory already exists', () => { - beforeEach('create outputDirectory with files', () => { - if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true }) - fs.writeFileSync(path.join(outputDir, 'randomFile.txt'), JSON.stringify({ a: 2 }, null, 2)) + itThrowsACliError(command, `Could not find ${inexistentManifestPath}`, 'FileNotFound', 1) }) - const expectedInputs = { - firstStaticNumber: 'uint32', - secondStaticNumber: 'uint32', - isTrue: 'bool', - } - itCreatesFilesCorrectly(manifestPath, expectedInputs) + context('when the output directory already exists', () => { + beforeEach('create outputDirectory with files', () => { + if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true }) + fs.writeFileSync(path.join(outputDir, 'randomFile.txt'), JSON.stringify({ a: 2 }, null, 2)) + }) + + const expectedInputs = { + firstStaticNumber: 'uint32', + secondStaticNumber: 'uint32', + isTrue: 'bool', + } + itCreatesFilesCorrectly(manifestPath, expectedInputs) + }) }) }) diff --git a/packages/cli/tests/commands/deploy.spec.ts b/packages/cli/tests/commands/deploy.spec.ts index 3bda2491..c61004e7 100644 --- a/packages/cli/tests/commands/deploy.spec.ts +++ b/packages/cli/tests/commands/deploy.spec.ts @@ -11,189 +11,261 @@ import { backupCredentials, itThrowsACliError, restoreCredentials } from '../hel describe('deploy', () => { const inputDir = join(__dirname, 'deploy-directory') let outputDir = inputDir + const mimicConfigPath = join(process.cwd(), 'mimic.yaml') + const basePath = `${__dirname}/../fixtures` + const manifestPath = `${basePath}/manifests/manifest.yaml` + const taskPath = `${basePath}/tasks/task.ts` - context('when the default profile exists', () => { + afterEach('cleanup mimic config', () => { + if (fs.existsSync(mimicConfigPath)) fs.unlinkSync(mimicConfigPath) + }) + + context('when the mimic config exists', () => { let credentialsManager: CredentialsManager let backupDir: string | null = null + let axiosMock: MockAdapter + const defaultKey = '123' + const CID = '456' + + beforeEach('create mimic.yaml', () => { + fs.writeFileSync( + mimicConfigPath, + `tasks:\n - name: test-task\n manifest: ${manifestPath}\n entry: ${taskPath}\n output: ${inputDir}\n` + ) + }) beforeEach('backup existing credentials', () => { credentialsManager = CredentialsManager.getDefault() backupDir = backupCredentials(credentialsManager) }) - afterEach('restore credentials and stubs', () => { + beforeEach('create default profile', () => { + credentialsManager.saveProfile('default', defaultKey) + }) + + beforeEach('create input directory with files', () => { + fs.mkdirSync(inputDir, { recursive: true }) + fs.writeFileSync(`${inputDir}/manifest.json`, '') + fs.writeFileSync(`${inputDir}/task.wasm`, '') + }) + + beforeEach('create axios mock', () => { + axiosMock = new MockAdapter(axios) + axiosMock.onPost(/.*\/tasks/gm).reply(200, { CID }) + }) + + afterEach('cleanup', () => { + axiosMock.restore() restoreCredentials(credentialsManager, backupDir) backupDir = null + if (fs.existsSync(inputDir)) fs.rmSync(inputDir, { recursive: true }) }) - const defaultKey = '123' - beforeEach('create default profile', () => { - credentialsManager.saveProfile('default', defaultKey) + it('deploys successfully using task from config', async () => { + const deployCommand = ['deploy', '--skip-compile'] + await runCommand(deployCommand) + + const requests = axiosMock.history.post + expect(requests).to.have.lengthOf(1) + expect(requests[0].headers?.['x-api-key']).to.equal(defaultKey) + }) + + it('saves the CID on a file using task output from config', async () => { + const deployCommand = ['deploy', '--skip-compile'] + await runCommand(deployCommand) + const json = JSON.parse(fs.readFileSync(`${inputDir}/CID.json`, 'utf-8')) + expect(json.CID).to.be.equal(CID) + }) + }) + + context('when the mimic config does not exist', () => { + beforeEach('ensure mimic.yaml does not exist', () => { + if (fs.existsSync(mimicConfigPath)) fs.unlinkSync(mimicConfigPath) }) - const command = ['deploy', `-i ${inputDir}`, `-o ${outputDir}`, '--skip-compile'] + context('when the default profile exists', () => { + let credentialsManager: CredentialsManager + let backupDir: string | null = null - context('when input directory exists', () => { - beforeEach('create input directory', () => { - fs.mkdirSync(inputDir, { recursive: true }) + beforeEach('backup existing credentials', () => { + credentialsManager = CredentialsManager.getDefault() + backupDir = backupCredentials(credentialsManager) }) - afterEach('delete generated files', () => { - if (fs.existsSync(inputDir)) fs.rmSync(inputDir, { recursive: true }) + afterEach('restore credentials and stubs', () => { + restoreCredentials(credentialsManager, backupDir) + backupDir = null }) - const createFile = (name: string) => { - fs.writeFileSync(`${inputDir}/${name}`, '') - } + const defaultKey = '123' + beforeEach('create default profile', () => { + credentialsManager.saveProfile('default', defaultKey) + }) - context('when the directory contains necessary files', () => { - let axiosMock: MockAdapter + const command = ['deploy', `-i ${inputDir}`, `-o ${outputDir}`, '--skip-compile'] - beforeEach('create files', () => { - ;['manifest.json', 'task.wasm'].map(createFile) + context('when input directory exists', () => { + beforeEach('create input directory', () => { + fs.mkdirSync(inputDir, { recursive: true }) }) - beforeEach('create axios mock', () => { - axiosMock = new MockAdapter(axios) + afterEach('delete generated files', () => { + if (fs.existsSync(inputDir)) fs.rmSync(inputDir, { recursive: true }) }) - afterEach('restore axios mock', () => { - axiosMock.restore() - }) + const createFile = (name: string) => { + fs.writeFileSync(`${inputDir}/${name}`, '') + } - context('when uploading to registry is successful', () => { - const CID = '123' + context('when the directory contains necessary files', () => { + let axiosMock: MockAdapter - beforeEach('mock registry response', () => { - axiosMock.onPost(/.*\/tasks/gm).reply(200, { CID }) + beforeEach('create files', () => { + ;['manifest.json', 'task.wasm'].map(createFile) }) - context('when output directory exists', () => { - context('when the api key is provided', () => { - const apiKey = '456' - const apiKeyCommand = [...command, '--api-key', apiKey] + beforeEach('create axios mock', () => { + axiosMock = new MockAdapter(axios) + }) - it('deploys successfully with the api key', async () => { - await runCommand(apiKeyCommand) + afterEach('restore axios mock', () => { + axiosMock.restore() + }) - const requests = axiosMock.history.post - expect(requests).to.have.lengthOf(1) - expect(requests[0].headers?.['x-api-key']).to.equal(apiKey) - }) + context('when uploading to registry is successful', () => { + const CID = '123' + + beforeEach('mock registry response', () => { + axiosMock.onPost(/.*\/tasks/gm).reply(200, { CID }) }) - context('when a profile is provided', () => { - const apiKey = '789' - const profile = 'custom-profile' - const profileCommand = [...command, '--profile', profile] + context('when output directory exists', () => { + context('when the api key is provided', () => { + const apiKey = '456' + const apiKeyCommand = [...command, '--api-key', apiKey] + + it('deploys successfully with the api key', async () => { + await runCommand(apiKeyCommand) - beforeEach('create profile', () => { - credentialsManager.saveProfile(profile, apiKey) + const requests = axiosMock.history.post + expect(requests).to.have.lengthOf(1) + expect(requests[0].headers?.['x-api-key']).to.equal(apiKey) + }) }) - it('deploys successfully with the custom profile', async () => { - await runCommand(profileCommand) + context('when a profile is provided', () => { + const apiKey = '789' + const profile = 'custom-profile' + const profileCommand = [...command, '--profile', profile] - const requests = axiosMock.history.post - expect(requests).to.have.lengthOf(1) - expect(requests[0].headers?.['x-api-key']).to.equal(apiKey) + beforeEach('create profile', () => { + credentialsManager.saveProfile(profile, apiKey) + }) + + it('deploys successfully with the custom profile', async () => { + await runCommand(profileCommand) + + const requests = axiosMock.history.post + expect(requests).to.have.lengthOf(1) + expect(requests[0].headers?.['x-api-key']).to.equal(apiKey) + }) }) - }) - it('saves the CID on a file', async () => { - await runCommand(command) - const json = JSON.parse(fs.readFileSync(`${outputDir}/CID.json`, 'utf-8')) - expect(json.CID).to.be.equal(CID) + it('saves the CID on a file', async () => { + await runCommand(command) + const json = JSON.parse(fs.readFileSync(`${outputDir}/CID.json`, 'utf-8')) + expect(json.CID).to.be.equal(CID) + }) }) - }) - context('when output directory does not exist', () => { - const noOutDir = `${outputDir}/does-not-exist` - const noOutDirCommand = ['deploy', `-i ${inputDir}`, `-o ${noOutDir}`, '--skip-compile'] + context('when output directory does not exist', () => { + const noOutDir = `${outputDir}/does-not-exist` + const noOutDirCommand = ['deploy', `-i ${inputDir}`, `-o ${noOutDir}`, '--skip-compile'] - it('saves the CID on a file', async () => { - await runCommand(noOutDirCommand) - const json = JSON.parse(fs.readFileSync(`${noOutDir}/CID.json`, 'utf-8')) - expect(json.CID).to.be.equal(CID) + it('saves the CID on a file', async () => { + await runCommand(noOutDirCommand) + const json = JSON.parse(fs.readFileSync(`${noOutDir}/CID.json`, 'utf-8')) + expect(json.CID).to.be.equal(CID) + }) }) }) - }) - context('when uploading to registry is not successful', () => { - context('when there is a bad request failure', () => { - context('when the error message is present', () => { - const message = 'Task with same name and version already exists' + context('when uploading to registry is not successful', () => { + context('when there is a bad request failure', () => { + context('when the error message is present', () => { + const message = 'Task with same name and version already exists' - beforeEach('mock response', () => { - axiosMock.onPost(/.*\/tasks/gm).reply(400, { content: { message } }) + beforeEach('mock response', () => { + axiosMock.onPost(/.*\/tasks/gm).reply(400, { content: { message } }) + }) + + itThrowsACliError(command, message, 'Bad Request', 1) }) - itThrowsACliError(command, message, 'Bad Request', 1) + context('when the error message is not present', () => { + beforeEach('mock response', () => { + axiosMock.onPost(/.*\/tasks/gm).reply(400, { content: { errors: ['some error'] } }) + }) + + itThrowsACliError(command, 'Failed to upload to registry', 'Bad Request', 1) + }) }) - context('when the error message is not present', () => { + context('when there is an authorization failure', () => { beforeEach('mock response', () => { - axiosMock.onPost(/.*\/tasks/gm).reply(400, { content: { errors: ['some error'] } }) + axiosMock.onPost(/.*/).reply(401) }) - itThrowsACliError(command, 'Failed to upload to registry', 'Bad Request', 1) + itThrowsACliError(command, 'Failed to upload to registry', 'Unauthorized', 1) }) - }) - context('when there is an authorization failure', () => { - beforeEach('mock response', () => { - axiosMock.onPost(/.*/).reply(401) + context('when there is an authentication failure', () => { + beforeEach('mock response', () => { + axiosMock.onPost(/.*/).reply(403) + }) + + itThrowsACliError(command, 'Failed to upload to registry', 'Invalid api key', 1) }) - itThrowsACliError(command, 'Failed to upload to registry', 'Unauthorized', 1) - }) + context('when there is a generic error', () => { + beforeEach('mock response', () => { + axiosMock.onPost(/.*/).reply(501) + }) - context('when there is an authentication failure', () => { - beforeEach('mock response', () => { - axiosMock.onPost(/.*/).reply(403) + itThrowsACliError( + command, + 'Failed to upload to registry - Request failed with status code 501', + '501 Error', + 1 + ) }) + }) + }) - itThrowsACliError(command, 'Failed to upload to registry', 'Invalid api key', 1) + context('when the directory does not contain the necessary files', () => { + context('when the directory contains no files', () => { + itThrowsACliError(command, `Could not find ${inputDir}/manifest.json`, 'File Not Found', 1) }) - context('when there is a generic error', () => { - beforeEach('mock response', () => { - axiosMock.onPost(/.*/).reply(501) + context('when the directory contains only one file', () => { + beforeEach('create file', () => { + createFile('manifest.json') }) - itThrowsACliError( - command, - 'Failed to upload to registry - Request failed with status code 501', - '501 Error', - 1 - ) + itThrowsACliError(command, `Could not find ${inputDir}/task.wasm`, 'File Not Found', 1) }) }) }) - context('when the directory does not contain the necessary files', () => { - context('when the directory contains no files', () => { - itThrowsACliError(command, `Could not find ${inputDir}/manifest.json`, 'File Not Found', 1) - }) - - context('when the directory contains only one file', () => { - beforeEach('create file', () => { - createFile('manifest.json') - }) - - itThrowsACliError(command, `Could not find ${inputDir}/task.wasm`, 'File Not Found', 1) - }) + context('when input directory does not exist', () => { + itThrowsACliError(command, `Directory ${inputDir} does not exist`, 'Directory Not Found', 1) }) }) - context('when input directory does not exist', () => { - itThrowsACliError(command, `Directory ${inputDir} does not exist`, 'Directory Not Found', 1) - }) - }) - - context("when the default profile doesn't exist", () => { - const command = ['deploy'] + context("when the default profile doesn't exist", () => { + const command = ['deploy'] - itThrowsACliError(command, 'Authentication required', 'AuthenticationRequired', 3) + itThrowsACliError(command, 'Authentication required', 'AuthenticationRequired', 3) + }) }) }) From 0d86d681e9d1e7abb870e492f6a6dbb2c0a62d38 Mon Sep 17 00:00:00 2001 From: ncomerci Date: Wed, 7 Jan 2026 16:34:56 -0300 Subject: [PATCH 09/27] chore: requested changes --- packages/cli/src/commands/build.ts | 21 +++++++++++++++------ packages/cli/src/commands/codegen.ts | 17 +++++++++++++---- packages/cli/src/commands/compile.ts | 11 ++++++----- packages/cli/src/commands/deploy.ts | 15 ++++++++++++--- packages/cli/src/commands/test.ts | 9 ++------- packages/cli/src/constants.ts | 11 +++++++++++ packages/cli/src/helpers.ts | 5 +++-- packages/cli/src/lib/ManifestHandler.ts | 6 ++---- packages/cli/src/lib/MimicConfigHandler.ts | 11 +++++------ 9 files changed, 69 insertions(+), 37 deletions(-) create mode 100644 packages/cli/src/constants.ts diff --git a/packages/cli/src/commands/build.ts b/packages/cli/src/commands/build.ts index dacc3d20..7b8e08c1 100644 --- a/packages/cli/src/commands/build.ts +++ b/packages/cli/src/commands/build.ts @@ -1,5 +1,6 @@ import { Command, Flags } from '@oclif/core' +import { DEFAULT_BUILD_OUTPUT, DEFAULT_MANIFEST_FILE, DEFAULT_TASK_ENTRY, DEFAULT_TYPES_OUTPUT } from '../constants' import { filterTasks, taskFilterFlags } from '../helpers' import MimicConfigHandler from '../lib/MimicConfigHandler' import log from '../log' @@ -16,16 +17,24 @@ export default class Build extends Command { ] static override flags = { - manifest: Flags.string({ char: 'm', description: 'manifest to use', default: 'manifest.yaml' }), - task: Flags.string({ char: 't', description: 'task to compile', default: 'src/task.ts' }), - output: Flags.string({ char: 'o', description: 'output directory for build artifacts', default: './build' }), - types: Flags.string({ char: 'y', description: 'output directory for generated types', default: './src/types' }), + manifest: Flags.string({ char: 'm', description: 'manifest to use', default: DEFAULT_MANIFEST_FILE }), + task: Flags.string({ char: 't', description: 'task to compile', default: DEFAULT_TASK_ENTRY }), + output: Flags.string({ + char: 'o', + description: 'output directory for build artifacts', + default: DEFAULT_BUILD_OUTPUT, + }), + types: Flags.string({ + char: 'y', + description: 'output directory for generated types', + default: DEFAULT_TYPES_OUTPUT, + }), clean: Flags.boolean({ char: 'c', description: 'remove existing generated types before generating new files', default: false, }), - ['skip-config']: Flags.boolean({ + 'skip-config': Flags.boolean({ hidden: true, description: 'Skip mimic.yaml config (used internally)', default: false, @@ -35,7 +44,7 @@ export default class Build extends Command { public async run(): Promise { const { flags } = await this.parse(Build) - const { manifest, task, output, types, clean, include, exclude, ['skip-config']: skipConfig } = flags + const { manifest, task, output, types, clean, include, exclude, 'skip-config': skipConfig } = flags if (!skipConfig && MimicConfigHandler.exists()) { const mimicConfig = MimicConfigHandler.load(this) diff --git a/packages/cli/src/commands/codegen.ts b/packages/cli/src/commands/codegen.ts index 99c5a5cc..f8623786 100644 --- a/packages/cli/src/commands/codegen.ts +++ b/packages/cli/src/commands/codegen.ts @@ -3,6 +3,7 @@ import { Command, Flags } from '@oclif/core' import * as fs from 'fs' import { join } from 'path' +import { DEFAULT_MANIFEST_FILE, DEFAULT_TYPES_OUTPUT } from '../constants' import { filterTasks, taskFilterFlags } from '../helpers' import { AbisInterfaceGenerator, InputsInterfaceGenerator, ManifestHandler, MimicConfigHandler } from '../lib' import log from '../log' @@ -14,14 +15,22 @@ export default class Codegen extends Command { static override examples = ['<%= config.bin %> <%= command.id %> --manifest ./manifest.yaml --output ./types'] static override flags = { - manifest: Flags.string({ char: 'm', description: 'Specify a custom manifest file path', default: 'manifest.yaml' }), - output: Flags.string({ char: 'o', description: 'Ouput directory for generated types', default: './src/types' }), + manifest: Flags.string({ + char: 'm', + description: 'Specify a custom manifest file path', + default: DEFAULT_MANIFEST_FILE, + }), + output: Flags.string({ + char: 'o', + description: 'Ouput directory for generated types', + default: DEFAULT_TYPES_OUTPUT, + }), clean: Flags.boolean({ char: 'c', description: 'Remove existing generated types before generating new files', default: false, }), - ['skip-config']: Flags.boolean({ + 'skip-config': Flags.boolean({ hidden: true, description: 'Skip mimic.yaml config (used internally by build command)', default: false, @@ -31,7 +40,7 @@ export default class Codegen extends Command { public async run(): Promise { const { flags } = await this.parse(Codegen) - const { manifest: manifestDir, output: outputDir, clean, include, exclude, ['skip-config']: skipConfig } = flags + const { manifest: manifestDir, output: outputDir, clean, include, exclude, 'skip-config': skipConfig } = flags if (!skipConfig && MimicConfigHandler.exists()) { const mimicConfig = MimicConfigHandler.load(this) diff --git a/packages/cli/src/commands/compile.ts b/packages/cli/src/commands/compile.ts index a077d079..b5cf2a2c 100644 --- a/packages/cli/src/commands/compile.ts +++ b/packages/cli/src/commands/compile.ts @@ -2,6 +2,7 @@ import { Command, Flags } from '@oclif/core' import * as fs from 'fs' import * as path from 'path' +import { DEFAULT_BUILD_OUTPUT, DEFAULT_MANIFEST_FILE, DEFAULT_TASK_ENTRY } from '../constants' import { filterTasks, taskFilterFlags } from '../helpers' import ManifestHandler from '../lib/ManifestHandler' import MimicConfigHandler from '../lib/MimicConfigHandler' @@ -15,10 +16,10 @@ export default class Compile extends Command { static override examples = ['<%= config.bin %> <%= command.id %> --task src/task.ts --output ./output'] static override flags = { - task: Flags.string({ char: 't', description: 'task to compile', default: 'src/task.ts' }), - manifest: Flags.string({ char: 'm', description: 'manifest to validate', default: 'manifest.yaml' }), - output: Flags.string({ char: 'o', description: 'output directory', default: './build' }), - ['skip-config']: Flags.boolean({ + task: Flags.string({ char: 't', description: 'task to compile', default: DEFAULT_TASK_ENTRY }), + manifest: Flags.string({ char: 'm', description: 'manifest to validate', default: DEFAULT_MANIFEST_FILE }), + output: Flags.string({ char: 'o', description: 'output directory', default: DEFAULT_BUILD_OUTPUT }), + 'skip-config': Flags.boolean({ hidden: true, description: 'Skip mimic.yaml config (used internally by build command)', default: false, @@ -34,7 +35,7 @@ export default class Compile extends Command { manifest: manifestDir, include, exclude, - ['skip-config']: skipConfig, + 'skip-config': skipConfig, } = flags if (!skipConfig && MimicConfigHandler.exists()) { diff --git a/packages/cli/src/commands/deploy.ts b/packages/cli/src/commands/deploy.ts index 7c589ffb..79582f22 100644 --- a/packages/cli/src/commands/deploy.ts +++ b/packages/cli/src/commands/deploy.ts @@ -4,6 +4,7 @@ import FormData from 'form-data' import * as fs from 'fs' import { join, resolve } from 'path' +import { DEFAULT_BUILD_OUTPUT, DEFAULT_MANIFEST_FILE, DEFAULT_TASK_ENTRY, DEFAULT_TYPES_OUTPUT } from '../constants' import { GENERIC_SUGGESTION } from '../errors' import { filterTasks, taskFilterFlags } from '../helpers' import { ProfileCredentials } from '../lib/CredentialsManager' @@ -27,8 +28,16 @@ export default class Deploy extends Authenticate { static override flags = { ...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' }), + input: Flags.string({ + char: 'i', + description: 'Directory containing the compiled artifacts', + default: DEFAULT_BUILD_OUTPUT, + }), + output: Flags.string({ + char: 'o', + description: 'Output directory for deployment CID', + default: DEFAULT_BUILD_OUTPUT, + }), url: Flags.string({ char: 'u', description: `Mimic Registry base URL`, default: MIMIC_REGISTRY_DEFAULT }), 'skip-compile': Flags.boolean({ description: 'Skip codegen and compile steps before uploading', default: false }), ...taskFilterFlags, @@ -57,7 +66,7 @@ export default class Deploy extends Authenticate { } } else { await this.runForTask( - { manifest: 'manifest.yaml', entry: 'src/task.ts', types: './src/types', output: outputDir }, + { manifest: DEFAULT_MANIFEST_FILE, entry: DEFAULT_TASK_ENTRY, types: DEFAULT_TYPES_OUTPUT, output: outputDir }, registryUrl, skipCompile, inputDir, diff --git a/packages/cli/src/commands/test.ts b/packages/cli/src/commands/test.ts index 806259ab..534ecaf7 100644 --- a/packages/cli/src/commands/test.ts +++ b/packages/cli/src/commands/test.ts @@ -1,6 +1,7 @@ import { Command, Flags } from '@oclif/core' import * as path from 'path' +import { DEFAULT_TASK_CONFIG } from '../constants' import { filterTasks, taskFilterFlags } from '../helpers' import MimicConfigHandler from '../lib/MimicConfigHandler' import { execBinCommand } from '../lib/packageManager' @@ -38,13 +39,7 @@ export default class Test extends Command { testPaths.add(this.getTestPath(baseDir)) } } else { - const defaultTask = { - manifest: 'manifest.yaml', - entry: 'src/task.ts', - types: './src/types', - output: './build', - } - if (!skipCompile) await this.compileTask(defaultTask, baseDir) + if (!skipCompile) await this.compileTask(DEFAULT_TASK_CONFIG, baseDir) testPaths.add(this.getTestPath(baseDir)) } diff --git a/packages/cli/src/constants.ts b/packages/cli/src/constants.ts new file mode 100644 index 00000000..2e7957d6 --- /dev/null +++ b/packages/cli/src/constants.ts @@ -0,0 +1,11 @@ +export const DEFAULT_MANIFEST_FILE = 'manifest.yaml' +export const DEFAULT_TASK_ENTRY = 'src/task.ts' +export const DEFAULT_TYPES_OUTPUT = './src/types' +export const DEFAULT_BUILD_OUTPUT = './build' + +export const DEFAULT_TASK_CONFIG = { + manifest: DEFAULT_MANIFEST_FILE, + entry: DEFAULT_TASK_ENTRY, + types: DEFAULT_TYPES_OUTPUT, + output: DEFAULT_BUILD_OUTPUT, +} diff --git a/packages/cli/src/helpers.ts b/packages/cli/src/helpers.ts index f6163684..3e5a5c15 100644 --- a/packages/cli/src/helpers.ts +++ b/packages/cli/src/helpers.ts @@ -3,6 +3,7 @@ import { Interface } from 'ethers' import camelCase from 'lodash/camelCase' import startCase from 'lodash/startCase' +import { MIMIC_CONFIG_FILE } from './lib/MimicConfigHandler' import log from './log' import { AbiFunctionItem, RequiredTaskConfig } from './types' @@ -17,12 +18,12 @@ export function pascalCase(str: string): string { export const taskFilterFlags = { include: Flags.string({ - description: 'When mimic.yaml exists, only run tasks with these names (space-separated)', + description: `When ${MIMIC_CONFIG_FILE} exists, only run tasks with these names (space-separated)`, multiple: true, exclusive: ['exclude'], }), exclude: Flags.string({ - description: 'When mimic.yaml exists, exclude tasks with these names (space-separated)', + description: `When ${MIMIC_CONFIG_FILE} exists, exclude tasks with these names (space-separated)`, multiple: true, exclusive: ['include'], }), diff --git a/packages/cli/src/lib/ManifestHandler.ts b/packages/cli/src/lib/ManifestHandler.ts index 8ebf4d93..bb43f8c8 100644 --- a/packages/cli/src/lib/ManifestHandler.ts +++ b/packages/cli/src/lib/ManifestHandler.ts @@ -4,7 +4,7 @@ import { load } from 'js-yaml' import * as path from 'path' import { ZodError } from 'zod' -import { DuplicateEntryError, EmptyManifestError, MoreThanOneEntryError } from '../errors' +import { DuplicateEntryError, EmptyManifestError, GENERIC_SUGGESTION, MoreThanOneEntryError } from '../errors' import { Manifest } from '../types' import { ManifestValidator } from '../validators' @@ -72,9 +72,7 @@ function handleValidationError(command: Command, err: unknown): never { suggestions = err.errors.map((e) => `Fix Field "${e.path.join('.')}" -- ${e.message}`) } else { ;[message, code] = [`Unkown Error: ${err}`, 'UnknownError'] - suggestions = [ - 'Contact the Mimic team for further assistance at our website https://www.mimic.fi/ or discord https://discord.com/invite/cpcyV9EsEg', - ] + suggestions = GENERIC_SUGGESTION } command.error(message, { code, suggestions }) diff --git a/packages/cli/src/lib/MimicConfigHandler.ts b/packages/cli/src/lib/MimicConfigHandler.ts index 70b64508..42ee4339 100644 --- a/packages/cli/src/lib/MimicConfigHandler.ts +++ b/packages/cli/src/lib/MimicConfigHandler.ts @@ -4,10 +4,11 @@ import { load } from 'js-yaml' import * as path from 'path' import { ZodError } from 'zod' +import { GENERIC_SUGGESTION } from '../errors' import { MimicConfig, RequiredTaskConfig } from '../types' import { MimicConfigValidator } from '../validators' -const MIMIC_CONFIG_FILE = 'mimic.yaml' +export const MIMIC_CONFIG_FILE = 'mimic.yaml' export default { exists(baseDir: string = process.cwd()): boolean { @@ -20,7 +21,7 @@ export default { if (!fs.existsSync(mimicConfigPath)) { command.error(`Could not find ${mimicConfigPath}`, { code: 'FileNotFound', - suggestions: ['Ensure mimic.yaml exists in the project root'], + suggestions: [`Ensure ${MIMIC_CONFIG_FILE} exists in the project root`], }) } @@ -31,7 +32,7 @@ export default { } catch (err) { command.error(`Failed to parse ${mimicConfigPath} as YAML`, { code: 'ParseError', - suggestions: ['Ensure mimic.yaml is valid YAML syntax'], + suggestions: [`Ensure ${MIMIC_CONFIG_FILE} is valid YAML syntax`], }) } @@ -61,9 +62,7 @@ function handleValidationError(command: Command, err: unknown): never { suggestions = err.errors.map((e) => `Fix Field "${e.path.join('.')}" -- ${e.message}`) } else { ;[message, code] = [`Unknown Error: ${err}`, 'UnknownError'] - suggestions = [ - 'Contact the Mimic team for further assistance at our website https://www.mimic.fi/ or discord https://discord.com/invite/cpcyV9EsEg', - ] + suggestions = GENERIC_SUGGESTION } command.error(message, { code, suggestions }) From 32ad5b21c72711fa1c802c3bbafa8a11bdb0aea4 Mon Sep 17 00:00:00 2001 From: lgalende Date: Thu, 8 Jan 2026 11:27:46 -0300 Subject: [PATCH 10/27] chore: rollback constants --- packages/cli/src/commands/build.ts | 17 ++++------------- packages/cli/src/commands/codegen.ts | 13 ++----------- packages/cli/src/commands/compile.ts | 7 +++---- packages/cli/src/commands/deploy.ts | 23 ++++------------------- packages/cli/src/commands/test.ts | 4 ++-- packages/cli/src/constants.ts | 15 +++++---------- 6 files changed, 20 insertions(+), 59 deletions(-) diff --git a/packages/cli/src/commands/build.ts b/packages/cli/src/commands/build.ts index 7b8e08c1..51f5a56d 100644 --- a/packages/cli/src/commands/build.ts +++ b/packages/cli/src/commands/build.ts @@ -1,6 +1,5 @@ import { Command, Flags } from '@oclif/core' -import { DEFAULT_BUILD_OUTPUT, DEFAULT_MANIFEST_FILE, DEFAULT_TASK_ENTRY, DEFAULT_TYPES_OUTPUT } from '../constants' import { filterTasks, taskFilterFlags } from '../helpers' import MimicConfigHandler from '../lib/MimicConfigHandler' import log from '../log' @@ -17,18 +16,10 @@ export default class Build extends Command { ] static override flags = { - manifest: Flags.string({ char: 'm', description: 'manifest to use', default: DEFAULT_MANIFEST_FILE }), - task: Flags.string({ char: 't', description: 'task to compile', default: DEFAULT_TASK_ENTRY }), - output: Flags.string({ - char: 'o', - description: 'output directory for build artifacts', - default: DEFAULT_BUILD_OUTPUT, - }), - types: Flags.string({ - char: 'y', - description: 'output directory for generated types', - default: DEFAULT_TYPES_OUTPUT, - }), + manifest: Flags.string({ char: 'm', description: 'manifest to use', default: 'manifest.yaml' }), + task: Flags.string({ char: 't', description: 'task to compile', default: 'src/task.ts' }), + output: Flags.string({ char: 'o', description: 'output directory for build artifacts', default: './build' }), + types: Flags.string({ char: 'y', description: 'output directory for generated types', default: './src/types' }), clean: Flags.boolean({ char: 'c', description: 'remove existing generated types before generating new files', diff --git a/packages/cli/src/commands/codegen.ts b/packages/cli/src/commands/codegen.ts index f8623786..5cb13a67 100644 --- a/packages/cli/src/commands/codegen.ts +++ b/packages/cli/src/commands/codegen.ts @@ -3,7 +3,6 @@ import { Command, Flags } from '@oclif/core' import * as fs from 'fs' import { join } from 'path' -import { DEFAULT_MANIFEST_FILE, DEFAULT_TYPES_OUTPUT } from '../constants' import { filterTasks, taskFilterFlags } from '../helpers' import { AbisInterfaceGenerator, InputsInterfaceGenerator, ManifestHandler, MimicConfigHandler } from '../lib' import log from '../log' @@ -15,16 +14,8 @@ export default class Codegen extends Command { static override examples = ['<%= config.bin %> <%= command.id %> --manifest ./manifest.yaml --output ./types'] static override flags = { - manifest: Flags.string({ - char: 'm', - description: 'Specify a custom manifest file path', - default: DEFAULT_MANIFEST_FILE, - }), - output: Flags.string({ - char: 'o', - description: 'Ouput directory for generated types', - default: DEFAULT_TYPES_OUTPUT, - }), + manifest: Flags.string({ char: 'm', description: 'Specify a custom manifest file path', default: 'manifest.yaml' }), + output: Flags.string({ char: 'o', description: 'Ouput directory for generated types', default: './src/types' }), clean: Flags.boolean({ char: 'c', description: 'Remove existing generated types before generating new files', diff --git a/packages/cli/src/commands/compile.ts b/packages/cli/src/commands/compile.ts index b5cf2a2c..32e8677d 100644 --- a/packages/cli/src/commands/compile.ts +++ b/packages/cli/src/commands/compile.ts @@ -2,7 +2,6 @@ import { Command, Flags } from '@oclif/core' import * as fs from 'fs' import * as path from 'path' -import { DEFAULT_BUILD_OUTPUT, DEFAULT_MANIFEST_FILE, DEFAULT_TASK_ENTRY } from '../constants' import { filterTasks, taskFilterFlags } from '../helpers' import ManifestHandler from '../lib/ManifestHandler' import MimicConfigHandler from '../lib/MimicConfigHandler' @@ -16,9 +15,9 @@ export default class Compile extends Command { static override examples = ['<%= config.bin %> <%= command.id %> --task src/task.ts --output ./output'] static override flags = { - task: Flags.string({ char: 't', description: 'task to compile', default: DEFAULT_TASK_ENTRY }), - manifest: Flags.string({ char: 'm', description: 'manifest to validate', default: DEFAULT_MANIFEST_FILE }), - output: Flags.string({ char: 'o', description: 'output directory', default: DEFAULT_BUILD_OUTPUT }), + task: Flags.string({ char: 't', description: 'task to compile', default: 'src/task.ts' }), + manifest: Flags.string({ char: 'm', description: 'manifest to validate', default: 'manifest.yaml' }), + output: Flags.string({ char: 'o', description: 'output directory', default: './build' }), 'skip-config': Flags.boolean({ hidden: true, description: 'Skip mimic.yaml config (used internally by build command)', diff --git a/packages/cli/src/commands/deploy.ts b/packages/cli/src/commands/deploy.ts index 79582f22..375901ac 100644 --- a/packages/cli/src/commands/deploy.ts +++ b/packages/cli/src/commands/deploy.ts @@ -4,7 +4,7 @@ import FormData from 'form-data' import * as fs from 'fs' import { join, resolve } from 'path' -import { DEFAULT_BUILD_OUTPUT, DEFAULT_MANIFEST_FILE, DEFAULT_TASK_ENTRY, DEFAULT_TYPES_OUTPUT } from '../constants' +import { DEFAULT_TASK } from '../constants' import { GENERIC_SUGGESTION } from '../errors' import { filterTasks, taskFilterFlags } from '../helpers' import { ProfileCredentials } from '../lib/CredentialsManager' @@ -28,16 +28,8 @@ export default class Deploy extends Authenticate { static override flags = { ...Authenticate.flags, - input: Flags.string({ - char: 'i', - description: 'Directory containing the compiled artifacts', - default: DEFAULT_BUILD_OUTPUT, - }), - output: Flags.string({ - char: 'o', - description: 'Output directory for deployment CID', - default: DEFAULT_BUILD_OUTPUT, - }), + 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 }), 'skip-compile': Flags.boolean({ description: 'Skip codegen and compile steps before uploading', default: false }), ...taskFilterFlags, @@ -65,14 +57,7 @@ export default class Deploy extends Authenticate { await this.runForTask(task, registryUrl, skipCompile, task.output, profile, apiKey) } } else { - await this.runForTask( - { manifest: DEFAULT_MANIFEST_FILE, entry: DEFAULT_TASK_ENTRY, types: DEFAULT_TYPES_OUTPUT, output: outputDir }, - registryUrl, - skipCompile, - inputDir, - profile, - apiKey - ) + await this.runForTask({ ...DEFAULT_TASK, output: outputDir }, registryUrl, skipCompile, inputDir, profile, apiKey) } } diff --git a/packages/cli/src/commands/test.ts b/packages/cli/src/commands/test.ts index 534ecaf7..71b4531a 100644 --- a/packages/cli/src/commands/test.ts +++ b/packages/cli/src/commands/test.ts @@ -1,7 +1,7 @@ import { Command, Flags } from '@oclif/core' import * as path from 'path' -import { DEFAULT_TASK_CONFIG } from '../constants' +import { DEFAULT_TASK } from '../constants' import { filterTasks, taskFilterFlags } from '../helpers' import MimicConfigHandler from '../lib/MimicConfigHandler' import { execBinCommand } from '../lib/packageManager' @@ -39,7 +39,7 @@ export default class Test extends Command { testPaths.add(this.getTestPath(baseDir)) } } else { - if (!skipCompile) await this.compileTask(DEFAULT_TASK_CONFIG, baseDir) + if (!skipCompile) await this.compileTask(DEFAULT_TASK, baseDir) testPaths.add(this.getTestPath(baseDir)) } diff --git a/packages/cli/src/constants.ts b/packages/cli/src/constants.ts index 2e7957d6..4632294c 100644 --- a/packages/cli/src/constants.ts +++ b/packages/cli/src/constants.ts @@ -1,11 +1,6 @@ -export const DEFAULT_MANIFEST_FILE = 'manifest.yaml' -export const DEFAULT_TASK_ENTRY = 'src/task.ts' -export const DEFAULT_TYPES_OUTPUT = './src/types' -export const DEFAULT_BUILD_OUTPUT = './build' - -export const DEFAULT_TASK_CONFIG = { - manifest: DEFAULT_MANIFEST_FILE, - entry: DEFAULT_TASK_ENTRY, - types: DEFAULT_TYPES_OUTPUT, - output: DEFAULT_BUILD_OUTPUT, +export const DEFAULT_TASK = { + manifest: 'manifest.yaml', + entry: 'src/task.ts', + types: './src/types', + output: './build', } From 91aca095d63462d02161c16cc172bc42f4e5353b Mon Sep 17 00:00:00 2001 From: ncomerci Date: Fri, 9 Jan 2026 16:20:57 -0300 Subject: [PATCH 11/27] chore: requested changes --- packages/cli/src/commands/build.ts | 4 +-- packages/cli/src/commands/codegen.ts | 15 +++++----- packages/cli/src/commands/compile.ts | 27 +++++++----------- packages/cli/src/commands/deploy.ts | 33 ++++++++-------------- packages/cli/src/lib/MimicConfigHandler.ts | 2 +- 5 files changed, 33 insertions(+), 48 deletions(-) diff --git a/packages/cli/src/commands/build.ts b/packages/cli/src/commands/build.ts index 51f5a56d..03b66bc3 100644 --- a/packages/cli/src/commands/build.ts +++ b/packages/cli/src/commands/build.ts @@ -1,7 +1,7 @@ import { Command, Flags } from '@oclif/core' import { filterTasks, taskFilterFlags } from '../helpers' -import MimicConfigHandler from '../lib/MimicConfigHandler' +import MimicConfigHandler, { MIMIC_CONFIG_FILE } from '../lib/MimicConfigHandler' import log from '../log' import { RequiredTaskConfig } from '../types' @@ -27,7 +27,7 @@ export default class Build extends Command { }), 'skip-config': Flags.boolean({ hidden: true, - description: 'Skip mimic.yaml config (used internally)', + description: `Skip ${MIMIC_CONFIG_FILE} config (used internally)`, default: false, }), ...taskFilterFlags, diff --git a/packages/cli/src/commands/codegen.ts b/packages/cli/src/commands/codegen.ts index 5cb13a67..5d938cec 100644 --- a/packages/cli/src/commands/codegen.ts +++ b/packages/cli/src/commands/codegen.ts @@ -5,6 +5,7 @@ import { join } from 'path' import { filterTasks, taskFilterFlags } from '../helpers' import { AbisInterfaceGenerator, InputsInterfaceGenerator, ManifestHandler, MimicConfigHandler } from '../lib' +import { MIMIC_CONFIG_FILE } from '../lib/MimicConfigHandler' import log from '../log' import { Manifest, RequiredTaskConfig } from '../types' @@ -15,7 +16,7 @@ export default class Codegen extends Command { static override flags = { manifest: Flags.string({ char: 'm', description: 'Specify a custom manifest file path', default: 'manifest.yaml' }), - output: Flags.string({ char: 'o', description: 'Ouput directory for generated types', default: './src/types' }), + output: Flags.string({ char: 'o', description: 'Output directory for generated types', default: './src/types' }), clean: Flags.boolean({ char: 'c', description: 'Remove existing generated types before generating new files', @@ -23,7 +24,7 @@ export default class Codegen extends Command { }), 'skip-config': Flags.boolean({ hidden: true, - description: 'Skip mimic.yaml config (used internally by build command)', + description: `Skip ${MIMIC_CONFIG_FILE} config (used internally by build command)`, default: false, }), ...taskFilterFlags, @@ -31,7 +32,7 @@ export default class Codegen extends Command { public async run(): Promise { const { flags } = await this.parse(Codegen) - const { manifest: manifestDir, output: outputDir, clean, include, exclude, 'skip-config': skipConfig } = flags + const { manifest, output, clean, include, exclude, 'skip-config': skipConfig } = flags if (!skipConfig && MimicConfigHandler.exists()) { const mimicConfig = MimicConfigHandler.load(this) @@ -42,14 +43,14 @@ export default class Codegen extends Command { await this.runForTask(task, clean) } } else { - await this.runForTask({ manifest: manifestDir, types: outputDir }, clean) + await this.runForTask({ manifest, types: output }, clean) } } private async runForTask(task: Omit, clean: boolean): Promise { - const manifestDir = task.manifest + const manifestPath = task.manifest const outputDir = task.types - const manifest = ManifestHandler.load(this, manifestDir) + const manifest = ManifestHandler.load(this, manifestPath) if (clean) { const shouldDelete = await confirm({ @@ -73,7 +74,7 @@ export default class Codegen extends Command { if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true }) - generateAbisCode(manifest, outputDir, manifestDir) + generateAbisCode(manifest, outputDir, manifestPath) generateInputsCode(manifest, outputDir) log.stopAction() } diff --git a/packages/cli/src/commands/compile.ts b/packages/cli/src/commands/compile.ts index 32e8677d..2d51dbd8 100644 --- a/packages/cli/src/commands/compile.ts +++ b/packages/cli/src/commands/compile.ts @@ -4,7 +4,7 @@ import * as path from 'path' import { filterTasks, taskFilterFlags } from '../helpers' import ManifestHandler from '../lib/ManifestHandler' -import MimicConfigHandler from '../lib/MimicConfigHandler' +import MimicConfigHandler, { MIMIC_CONFIG_FILE } from '../lib/MimicConfigHandler' import { execBinCommand } from '../lib/packageManager' import log from '../log' import { RequiredTaskConfig } from '../types' @@ -20,7 +20,7 @@ export default class Compile extends Command { output: Flags.string({ char: 'o', description: 'output directory', default: './build' }), 'skip-config': Flags.boolean({ hidden: true, - description: 'Skip mimic.yaml config (used internally by build command)', + description: `Skip ${MIMIC_CONFIG_FILE} config (used internally by build command)`, default: false, }), ...taskFilterFlags, @@ -28,14 +28,7 @@ export default class Compile extends Command { public async run(): Promise { const { flags } = await this.parse(Compile) - const { - task: taskFile, - output: outputDir, - manifest: manifestDir, - include, - exclude, - 'skip-config': skipConfig, - } = flags + const { task: taskPath, output, manifest, include, exclude, 'skip-config': skipConfig } = flags if (!skipConfig && MimicConfigHandler.exists()) { const mimicConfig = MimicConfigHandler.load(this) @@ -46,26 +39,26 @@ export default class Compile extends Command { await this.runForTask(task) } } else { - await this.runForTask({ manifest: manifestDir, entry: taskFile, output: outputDir }) + await this.runForTask({ manifest, entry: taskPath, output }) } } private async runForTask(task: Omit): Promise { - const absTaskFile = path.resolve(task.entry) - const absOutputDir = path.resolve(task.output) + const taskPath = path.resolve(task.entry) + const outputDir = path.resolve(task.output) - if (!fs.existsSync(absOutputDir)) fs.mkdirSync(absOutputDir, { recursive: true }) + if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true }) log.startAction('Verifying Manifest') const manifest = ManifestHandler.load(this, task.manifest) log.startAction('Compiling') const ascArgs = [ - absTaskFile, + taskPath, '--target', 'release', '--outFile', - path.join(absOutputDir, 'task.wasm'), + path.join(outputDir, 'task.wasm'), '--optimize', '--exportRuntime', '--transform', @@ -82,7 +75,7 @@ export default class Compile extends Command { log.startAction('Saving files') - fs.writeFileSync(path.join(absOutputDir, 'manifest.json'), JSON.stringify(manifest, null, 2)) + fs.writeFileSync(path.join(outputDir, 'manifest.json'), JSON.stringify(manifest, null, 2)) log.stopAction() console.log(`Build complete! Artifacts in ${task.output}/`) } diff --git a/packages/cli/src/commands/deploy.ts b/packages/cli/src/commands/deploy.ts index 375901ac..82738597 100644 --- a/packages/cli/src/commands/deploy.ts +++ b/packages/cli/src/commands/deploy.ts @@ -37,16 +37,7 @@ export default class Deploy extends Authenticate { public async run(): Promise { const { flags } = await this.parse(Deploy) - const { - profile, - 'api-key': apiKey, - input: inputDir, - output: outputDir, - 'skip-compile': skipCompile, - url: registryUrl, - include, - exclude, - } = flags + const { profile, 'api-key': apiKey, input, output, 'skip-compile': skipCompile, url, include, exclude } = flags if (MimicConfigHandler.exists()) { const mimicConfig = MimicConfigHandler.load(this) @@ -54,10 +45,10 @@ export default class Deploy extends Authenticate { const tasks = filterTasks(this, allTasks, include, exclude) for (const task of tasks) { console.log(`\n${log.highlightText(`[${task.name}]`)}`) - await this.runForTask(task, registryUrl, skipCompile, task.output, profile, apiKey) + await this.runForTask(task, url, skipCompile, task.output, profile, apiKey) } } else { - await this.runForTask({ ...DEFAULT_TASK, output: outputDir }, registryUrl, skipCompile, inputDir, profile, apiKey) + await this.runForTask({ ...DEFAULT_TASK, output }, url, skipCompile, input, profile, apiKey) } } @@ -69,8 +60,8 @@ export default class Deploy extends Authenticate { profile?: string, apiKey?: string ): Promise { - const fullInputDir = resolve(inputDir) - const fullOutputDir = resolve(task.output) + const inputPath = resolve(inputDir) + const outputPath = resolve(task.output) const credentials = this.authenticate({ profile, 'api-key': apiKey }) @@ -84,7 +75,7 @@ export default class Deploy extends Authenticate { '--task', task.entry, '--output', - fullInputDir, + inputPath, '--types', task.types, '--skip-config', @@ -98,14 +89,14 @@ export default class Deploy extends Authenticate { log.startAction('Validating') - if (!fs.existsSync(fullInputDir)) { - this.error(`Directory ${log.highlightText(fullInputDir)} does not exist`, { + if (!fs.existsSync(inputPath)) { + this.error(`Directory ${log.highlightText(inputPath)} does not exist`, { code: 'Directory Not Found', suggestions: ['Use the --input flag to specify the correct path'], }) } - const neededFiles = ['manifest.json', 'task.wasm'].map((file) => join(fullInputDir, file)) + const neededFiles = ['manifest.json', 'task.wasm'].map((file) => join(inputPath, file)) for (const file of neededFiles) { if (!fs.existsSync(file)) { this.error(`Could not find ${file}`, { @@ -120,9 +111,9 @@ export default class Deploy extends Authenticate { console.log(`IPFS CID: ${log.highlightText(CID)}`) log.stopAction() - if (!fs.existsSync(fullOutputDir)) fs.mkdirSync(fullOutputDir, { recursive: true }) - fs.writeFileSync(join(fullOutputDir, 'CID.json'), JSON.stringify({ CID }, null, 2)) - console.log(`CID saved at ${log.highlightText(fullOutputDir)}`) + if (!fs.existsSync(outputPath)) fs.mkdirSync(outputPath, { recursive: true }) + fs.writeFileSync(join(outputPath, 'CID.json'), JSON.stringify({ CID }, null, 2)) + console.log(`CID saved at ${log.highlightText(outputPath)}`) console.log(`Task deployed!`) } diff --git a/packages/cli/src/lib/MimicConfigHandler.ts b/packages/cli/src/lib/MimicConfigHandler.ts index 42ee4339..464c54e5 100644 --- a/packages/cli/src/lib/MimicConfigHandler.ts +++ b/packages/cli/src/lib/MimicConfigHandler.ts @@ -58,7 +58,7 @@ function handleValidationError(command: Command, err: unknown): never { let suggestions: string[] if (err instanceof ZodError) { - ;[message, code] = ['Invalid mimic.yaml configuration', 'ValidationError'] + ;[message, code] = [`Invalid ${MIMIC_CONFIG_FILE} configuration`, 'ValidationError'] suggestions = err.errors.map((e) => `Fix Field "${e.path.join('.')}" -- ${e.message}`) } else { ;[message, code] = [`Unknown Error: ${err}`, 'UnknownError'] From 6865dcc28e2afb128b0930a9a0f257531d17cad3 Mon Sep 17 00:00:00 2001 From: ncomerci Date: Mon, 12 Jan 2026 11:09:17 -0300 Subject: [PATCH 12/27] chore: requested changes --- packages/cli/src/commands/build.ts | 4 ++-- packages/cli/src/commands/codegen.ts | 2 +- packages/cli/src/commands/compile.ts | 4 ++-- packages/cli/src/commands/deploy.ts | 2 +- packages/cli/src/commands/test.ts | 2 +- packages/cli/src/constants.ts | 2 +- packages/cli/src/lib/MimicConfigHandler.ts | 2 +- packages/cli/src/validators.ts | 2 +- packages/cli/tests/MimicConfigHandler.spec.ts | 10 +++++----- packages/cli/tests/commands/build.spec.ts | 2 +- packages/cli/tests/commands/codegen.spec.ts | 2 +- packages/cli/tests/commands/compile.spec.ts | 2 +- packages/cli/tests/commands/deploy.spec.ts | 2 +- packages/cli/tests/helpers.spec.ts | 2 +- 14 files changed, 20 insertions(+), 20 deletions(-) diff --git a/packages/cli/src/commands/build.ts b/packages/cli/src/commands/build.ts index 03b66bc3..1383fb99 100644 --- a/packages/cli/src/commands/build.ts +++ b/packages/cli/src/commands/build.ts @@ -46,7 +46,7 @@ export default class Build extends Command { await this.runForTask(taskConfig, clean) } } else { - await this.runForTask({ manifest, entry: task, output, types }, clean) + await this.runForTask({ manifest, path: task, output, types }, clean) } } @@ -58,7 +58,7 @@ export default class Build extends Command { const compileArgs: string[] = [ '--task', - task.entry, + task.path, '--manifest', task.manifest, '--output', diff --git a/packages/cli/src/commands/codegen.ts b/packages/cli/src/commands/codegen.ts index 5d938cec..2f3637e3 100644 --- a/packages/cli/src/commands/codegen.ts +++ b/packages/cli/src/commands/codegen.ts @@ -47,7 +47,7 @@ export default class Codegen extends Command { } } - private async runForTask(task: Omit, clean: boolean): Promise { + private async runForTask(task: Omit, clean: boolean): Promise { const manifestPath = task.manifest const outputDir = task.types const manifest = ManifestHandler.load(this, manifestPath) diff --git a/packages/cli/src/commands/compile.ts b/packages/cli/src/commands/compile.ts index 2d51dbd8..db4fc020 100644 --- a/packages/cli/src/commands/compile.ts +++ b/packages/cli/src/commands/compile.ts @@ -39,12 +39,12 @@ export default class Compile extends Command { await this.runForTask(task) } } else { - await this.runForTask({ manifest, entry: taskPath, output }) + await this.runForTask({ manifest, path: taskPath, output }) } } private async runForTask(task: Omit): Promise { - const taskPath = path.resolve(task.entry) + const taskPath = path.resolve(task.path) const outputDir = path.resolve(task.output) if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true }) diff --git a/packages/cli/src/commands/deploy.ts b/packages/cli/src/commands/deploy.ts index 82738597..9fa3acf2 100644 --- a/packages/cli/src/commands/deploy.ts +++ b/packages/cli/src/commands/deploy.ts @@ -73,7 +73,7 @@ export default class Deploy extends Authenticate { '--manifest', task.manifest, '--task', - task.entry, + task.path, '--output', inputPath, '--types', diff --git a/packages/cli/src/commands/test.ts b/packages/cli/src/commands/test.ts index 71b4531a..a2e345fa 100644 --- a/packages/cli/src/commands/test.ts +++ b/packages/cli/src/commands/test.ts @@ -55,7 +55,7 @@ export default class Test extends Command { if (cg.status !== 0) this.exit(cg.status ?? 1) const cp = execBinCommand( 'mimic', - ['compile', '--task', task.entry, '--manifest', task.manifest, '--output', task.output, '--skip-config'], + ['compile', '--task', task.path, '--manifest', task.manifest, '--output', task.output, '--skip-config'], baseDir ) if (cp.status !== 0) this.exit(cp.status ?? 1) diff --git a/packages/cli/src/constants.ts b/packages/cli/src/constants.ts index 4632294c..d67aca2f 100644 --- a/packages/cli/src/constants.ts +++ b/packages/cli/src/constants.ts @@ -1,6 +1,6 @@ export const DEFAULT_TASK = { manifest: 'manifest.yaml', - entry: 'src/task.ts', + path: 'src/task.ts', types: './src/types', output: './build', } diff --git a/packages/cli/src/lib/MimicConfigHandler.ts b/packages/cli/src/lib/MimicConfigHandler.ts index 464c54e5..2a5cb42f 100644 --- a/packages/cli/src/lib/MimicConfigHandler.ts +++ b/packages/cli/src/lib/MimicConfigHandler.ts @@ -47,7 +47,7 @@ export default { return mimicConfig.tasks.map((task) => ({ ...task, output: task.output ?? `build/${task.name}`, - types: task.types ?? path.join(path.dirname(task.entry), 'types'), + types: task.types ?? path.join(path.dirname(task.path), 'types'), })) }, } diff --git a/packages/cli/src/validators.ts b/packages/cli/src/validators.ts index 6642c7da..82f33bee 100644 --- a/packages/cli/src/validators.ts +++ b/packages/cli/src/validators.ts @@ -29,7 +29,7 @@ export const ManifestValidator = z.object({ export const TaskConfigValidator = z.object({ name: String, manifest: String, - entry: String, + path: String, output: String.optional(), types: String.optional(), }) diff --git a/packages/cli/tests/MimicConfigHandler.spec.ts b/packages/cli/tests/MimicConfigHandler.spec.ts index 0a24227b..d4901a15 100644 --- a/packages/cli/tests/MimicConfigHandler.spec.ts +++ b/packages/cli/tests/MimicConfigHandler.spec.ts @@ -8,8 +8,8 @@ import { MimicConfigValidator } from '../src/validators' describe('MimicConfigHandler', () => { const mimicConfig = { tasks: [ - { name: 'swap-task', manifest: './tasks/swap/manifest.yaml', entry: './tasks/swap/src/task.ts' }, - { name: 'transfer-task', manifest: './tasks/transfer/manifest.yaml', entry: './tasks/transfer/src/task.ts' }, + { name: 'swap-task', manifest: './tasks/swap/manifest.yaml', path: './tasks/swap/src/task.ts' }, + { name: 'transfer-task', manifest: './tasks/transfer/manifest.yaml', path: './tasks/transfer/src/task.ts' }, ], } @@ -93,16 +93,16 @@ describe('MimicConfigHandler', () => { context('when task name is missing', () => { itReturnsAnError( - { ...mimicConfig, tasks: [{ manifest: './manifest.yaml', entry: './src/task.ts' }] }, + { ...mimicConfig, tasks: [{ manifest: './manifest.yaml', path: './src/task.ts' }] }, 'Required' ) }) context('when task manifest is missing', () => { - itReturnsAnError({ ...mimicConfig, tasks: [{ name: 'task', entry: './src/task.ts' }] }, 'Required') + itReturnsAnError({ ...mimicConfig, tasks: [{ name: 'task', path: './src/task.ts' }] }, 'Required') }) - context('when task entry is missing', () => { + context('when task path is missing', () => { itReturnsAnError({ ...mimicConfig, tasks: [{ name: 'task', manifest: './manifest.yaml' }] }, 'Required') }) }) diff --git a/packages/cli/tests/commands/build.spec.ts b/packages/cli/tests/commands/build.spec.ts index 48d82b4b..e780ad95 100644 --- a/packages/cli/tests/commands/build.spec.ts +++ b/packages/cli/tests/commands/build.spec.ts @@ -52,7 +52,7 @@ describe('build', () => { beforeEach('create mimic.yaml', () => { fs.writeFileSync( mimicConfigPath, - `tasks:\n - name: test-task\n manifest: ${manifestPath}\n entry: ${taskPath}\n output: ${outputDir}\n types: ${typesDir}\n` + `tasks:\n - name: test-task\n manifest: ${manifestPath}\n path: ${taskPath}\n output: ${outputDir}\n types: ${typesDir}\n` ) }) diff --git a/packages/cli/tests/commands/codegen.spec.ts b/packages/cli/tests/commands/codegen.spec.ts index c4965ad9..73fb2d7f 100644 --- a/packages/cli/tests/commands/codegen.spec.ts +++ b/packages/cli/tests/commands/codegen.spec.ts @@ -21,7 +21,7 @@ describe('codegen', () => { beforeEach('create mimic.yaml', () => { fs.writeFileSync( mimicConfigPath, - `tasks:\n - name: test-task\n manifest: ${manifestPath}\n entry: ${basePath}/tasks/task.ts\n types: ${outputDir}\n` + `tasks:\n - name: test-task\n manifest: ${manifestPath}\n path: ${basePath}/tasks/task.ts\n types: ${outputDir}\n` ) }) diff --git a/packages/cli/tests/commands/compile.spec.ts b/packages/cli/tests/commands/compile.spec.ts index ed3b9527..c9385284 100644 --- a/packages/cli/tests/commands/compile.spec.ts +++ b/packages/cli/tests/commands/compile.spec.ts @@ -42,7 +42,7 @@ describe('compile', () => { beforeEach('create mimic.yaml', () => { fs.writeFileSync( mimicConfigPath, - `tasks:\n - name: test-task\n manifest: ${manifestPath}\n entry: ${taskPath}\n output: ${outputDir}\n` + `tasks:\n - name: test-task\n manifest: ${manifestPath}\n path: ${taskPath}\n output: ${outputDir}\n` ) }) diff --git a/packages/cli/tests/commands/deploy.spec.ts b/packages/cli/tests/commands/deploy.spec.ts index c61004e7..39e12b66 100644 --- a/packages/cli/tests/commands/deploy.spec.ts +++ b/packages/cli/tests/commands/deploy.spec.ts @@ -30,7 +30,7 @@ describe('deploy', () => { beforeEach('create mimic.yaml', () => { fs.writeFileSync( mimicConfigPath, - `tasks:\n - name: test-task\n manifest: ${manifestPath}\n entry: ${taskPath}\n output: ${inputDir}\n` + `tasks:\n - name: test-task\n manifest: ${manifestPath}\n path: ${taskPath}\n output: ${inputDir}\n` ) }) diff --git a/packages/cli/tests/helpers.spec.ts b/packages/cli/tests/helpers.spec.ts index 6de2c975..49d8e825 100644 --- a/packages/cli/tests/helpers.spec.ts +++ b/packages/cli/tests/helpers.spec.ts @@ -13,7 +13,7 @@ describe('filterTasks', () => { const createTask = (name: string): RequiredTaskConfig => ({ name, manifest: `./tasks/${name}/manifest.yaml`, - entry: `./tasks/${name}/src/task.ts`, + path: `./tasks/${name}/src/task.ts`, output: `build/${name}`, types: `./tasks/${name}/src/types`, }) From 5fe7fc0308d1d0a5a0ed53e61cd443751fb38582 Mon Sep 17 00:00:00 2001 From: ncomerci Date: Mon, 12 Jan 2026 11:40:18 -0300 Subject: [PATCH 13/27] feat: execute tasks independently --- packages/cli/src/commands/build.ts | 8 +--- packages/cli/src/commands/codegen.ts | 9 ++--- packages/cli/src/commands/compile.ts | 10 ++--- packages/cli/src/commands/deploy.ts | 50 +++++++++++++++++-------- packages/cli/src/commands/test.ts | 24 +++++++----- packages/cli/src/errors.ts | 13 +++++++ packages/cli/src/helpers.ts | 56 +++++++++++++++++++++++++--- 7 files changed, 121 insertions(+), 49 deletions(-) diff --git a/packages/cli/src/commands/build.ts b/packages/cli/src/commands/build.ts index 1383fb99..9dbccdec 100644 --- a/packages/cli/src/commands/build.ts +++ b/packages/cli/src/commands/build.ts @@ -1,8 +1,7 @@ import { Command, Flags } from '@oclif/core' -import { filterTasks, taskFilterFlags } from '../helpers' +import { filterTasks, runTasks, taskFilterFlags } from '../helpers' import MimicConfigHandler, { MIMIC_CONFIG_FILE } from '../lib/MimicConfigHandler' -import log from '../log' import { RequiredTaskConfig } from '../types' import Codegen from './codegen' @@ -41,10 +40,7 @@ export default class Build extends Command { const mimicConfig = MimicConfigHandler.load(this) const allTasks = MimicConfigHandler.getTasks(mimicConfig) const tasks = filterTasks(this, allTasks, include, exclude) - for (const taskConfig of tasks) { - console.log(`\n${log.highlightText(`[${taskConfig.name}]`)}`) - await this.runForTask(taskConfig, clean) - } + await runTasks(this, tasks, (taskConfig) => this.runForTask(taskConfig, clean)) } else { await this.runForTask({ manifest, path: task, output, types }, clean) } diff --git a/packages/cli/src/commands/codegen.ts b/packages/cli/src/commands/codegen.ts index 2f3637e3..b585005e 100644 --- a/packages/cli/src/commands/codegen.ts +++ b/packages/cli/src/commands/codegen.ts @@ -3,7 +3,7 @@ import { Command, Flags } from '@oclif/core' import * as fs from 'fs' import { join } from 'path' -import { filterTasks, taskFilterFlags } from '../helpers' +import { filterTasks, runTasks, taskFilterFlags } from '../helpers' import { AbisInterfaceGenerator, InputsInterfaceGenerator, ManifestHandler, MimicConfigHandler } from '../lib' import { MIMIC_CONFIG_FILE } from '../lib/MimicConfigHandler' import log from '../log' @@ -38,10 +38,7 @@ export default class Codegen extends Command { const mimicConfig = MimicConfigHandler.load(this) const allTasks = MimicConfigHandler.getTasks(mimicConfig) const tasks = filterTasks(this, allTasks, include, exclude) - for (const task of tasks) { - console.log(`\n${log.highlightText(`[${task.name}]`)}`) - await this.runForTask(task, clean) - } + await runTasks(this, tasks, (task) => this.runForTask(task, clean)) } else { await this.runForTask({ manifest, types: output }, clean) } @@ -67,7 +64,7 @@ export default class Codegen extends Command { } log.startAction('Generating code') - if (Object.keys(manifest.inputs).length == 0 && Object.keys(manifest.abis).length == 0) { + if (Object.keys(manifest.inputs).length === 0 && Object.keys(manifest.abis).length === 0) { log.stopAction() return } diff --git a/packages/cli/src/commands/compile.ts b/packages/cli/src/commands/compile.ts index db4fc020..9cd66a59 100644 --- a/packages/cli/src/commands/compile.ts +++ b/packages/cli/src/commands/compile.ts @@ -2,7 +2,8 @@ import { Command, Flags } from '@oclif/core' import * as fs from 'fs' import * as path from 'path' -import { filterTasks, taskFilterFlags } from '../helpers' +import { CommandError } from '../errors' +import { filterTasks, runTasks, taskFilterFlags } from '../helpers' import ManifestHandler from '../lib/ManifestHandler' import MimicConfigHandler, { MIMIC_CONFIG_FILE } from '../lib/MimicConfigHandler' import { execBinCommand } from '../lib/packageManager' @@ -34,10 +35,7 @@ export default class Compile extends Command { const mimicConfig = MimicConfigHandler.load(this) const allTasks = MimicConfigHandler.getTasks(mimicConfig) const tasks = filterTasks(this, allTasks, include, exclude) - for (const task of tasks) { - console.log(`\n${log.highlightText(`[${task.name}]`)}`) - await this.runForTask(task) - } + await runTasks(this, tasks, (task) => this.runForTask(task)) } else { await this.runForTask({ manifest, path: taskPath, output }) } @@ -67,7 +65,7 @@ export default class Compile extends Command { const result = execBinCommand('asc', ascArgs, process.cwd()) if (result.status !== 0) { - this.error('AssemblyScript compilation failed', { + throw new CommandError('AssemblyScript compilation failed', { code: 'BuildError', suggestions: ['Check the AssemblyScript file'], }) diff --git a/packages/cli/src/commands/deploy.ts b/packages/cli/src/commands/deploy.ts index 9fa3acf2..38b60d75 100644 --- a/packages/cli/src/commands/deploy.ts +++ b/packages/cli/src/commands/deploy.ts @@ -5,8 +5,8 @@ import * as fs from 'fs' import { join, resolve } from 'path' import { DEFAULT_TASK } from '../constants' -import { GENERIC_SUGGESTION } from '../errors' -import { filterTasks, taskFilterFlags } from '../helpers' +import { CommandError, GENERIC_SUGGESTION } from '../errors' +import { filterTasks, runTasks, taskFilterFlags } from '../helpers' import { ProfileCredentials } from '../lib/CredentialsManager' import MimicConfigHandler from '../lib/MimicConfigHandler' import { execBinCommand } from '../lib/packageManager' @@ -43,10 +43,7 @@ export default class Deploy extends Authenticate { const mimicConfig = MimicConfigHandler.load(this) const allTasks = MimicConfigHandler.getTasks(mimicConfig) const tasks = filterTasks(this, allTasks, include, exclude) - for (const task of tasks) { - console.log(`\n${log.highlightText(`[${task.name}]`)}`) - await this.runForTask(task, url, skipCompile, task.output, profile, apiKey) - } + await runTasks(this, tasks, (task) => this.runForTask(task, url, skipCompile, task.output, profile, apiKey)) } else { await this.runForTask({ ...DEFAULT_TASK, output }, url, skipCompile, input, profile, apiKey) } @@ -83,14 +80,17 @@ export default class Deploy extends Authenticate { process.cwd() ) if (build.status !== 0) { - this.error('Build failed', { code: 'BuildError', suggestions: ['Check the task source code and manifest'] }) + throw new CommandError('Build failed', { + code: 'BuildError', + suggestions: ['Check the task source code and manifest'], + }) } } log.startAction('Validating') if (!fs.existsSync(inputPath)) { - this.error(`Directory ${log.highlightText(inputPath)} does not exist`, { + throw new CommandError(`Directory ${log.highlightText(inputPath)} does not exist`, { code: 'Directory Not Found', suggestions: ['Use the --input flag to specify the correct path'], }) @@ -99,7 +99,7 @@ export default class Deploy extends Authenticate { const neededFiles = ['manifest.json', 'task.wasm'].map((file) => join(inputPath, file)) for (const file of neededFiles) { if (!fs.existsSync(file)) { - this.error(`Could not find ${file}`, { + throw new CommandError(`Could not find ${file}`, { code: 'File Not Found', suggestions: [`Use ${log.highlightText('mimic compile')} to generate the needed files`], }) @@ -137,15 +137,33 @@ export default class Deploy extends Authenticate { } private handleError(err: unknown, message: string): never { - if (!(err instanceof AxiosError)) this.error(err as Error) + if (!(err instanceof AxiosError)) throw 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'] }) + + switch (statusCode) { + case 400: { + const errMessage = err.response?.data?.content?.message || message + throw new CommandError(errMessage, { + code: 'Bad Request', + suggestions: ['Review the uploaded files'], + }) + } + case 401: + throw new CommandError(message, { + code: 'Unauthorized', + suggestions: ['Review your key'], + }) + case 403: + throw new CommandError(message, { + code: 'Invalid api key', + suggestions: ['Review your key'], + }) + default: + throw new CommandError(`${message} - ${err.message}`, { + code: `${statusCode} Error`, + suggestions: GENERIC_SUGGESTION, + }) } - 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 }) } } diff --git a/packages/cli/src/commands/test.ts b/packages/cli/src/commands/test.ts index a2e345fa..f5450398 100644 --- a/packages/cli/src/commands/test.ts +++ b/packages/cli/src/commands/test.ts @@ -2,10 +2,9 @@ import { Command, Flags } from '@oclif/core' import * as path from 'path' import { DEFAULT_TASK } from '../constants' -import { filterTasks, taskFilterFlags } from '../helpers' +import { filterTasks, runTasks, taskFilterFlags } from '../helpers' import MimicConfigHandler from '../lib/MimicConfigHandler' import { execBinCommand } from '../lib/packageManager' -import log from '../log' import { RequiredTaskConfig } from '../types' export default class Test extends Command { @@ -31,12 +30,15 @@ export default class Test extends Command { const allTasks = MimicConfigHandler.getTasks(mimicConfig) const tasks = filterTasks(this, allTasks, include, exclude) - for (const task of tasks) { - if (!skipCompile) { - console.log(`\n${log.highlightText(`[${task.name}]`)}`) + if (!skipCompile) { + await runTasks(this, tasks, async (task) => { await this.compileTask(task, baseDir) - } - testPaths.add(this.getTestPath(baseDir)) + testPaths.add(this.getTestPath(baseDir)) + }) + } else { + tasks.forEach(() => { + testPaths.add(this.getTestPath(baseDir)) + }) } } else { if (!skipCompile) await this.compileTask(DEFAULT_TASK, baseDir) @@ -52,13 +54,17 @@ export default class Test extends Command { ['codegen', '--manifest', task.manifest, '--output', task.types, '--skip-config'], baseDir ) - if (cg.status !== 0) this.exit(cg.status ?? 1) + if (cg.status !== 0) { + throw new Error(`Codegen failed for task with status ${cg.status}`) + } const cp = execBinCommand( 'mimic', ['compile', '--task', task.path, '--manifest', task.manifest, '--output', task.output, '--skip-config'], baseDir ) - if (cp.status !== 0) this.exit(cp.status ?? 1) + if (cp.status !== 0) { + throw new Error(`Compile failed for task with status ${cp.status}`) + } } private getTestPath(baseDir: string): string { diff --git a/packages/cli/src/errors.ts b/packages/cli/src/errors.ts index 80a0ada9..773a6dbf 100644 --- a/packages/cli/src/errors.ts +++ b/packages/cli/src/errors.ts @@ -31,3 +31,16 @@ export class EmptyManifestError extends Error { export const GENERIC_SUGGESTION = [ 'Contact the Mimic team for further assistance at our website https://www.mimic.fi or discord https://discord.mimic.fi', ] + +export class CommandError extends Error { + public code: string + public suggestions: string[] + + constructor(message: string, options: { code: string; suggestions: string[] }) { + super(message) + this.name = this.constructor.name + this.code = options.code + this.suggestions = options.suggestions + Object.setPrototypeOf(this, new.target.prototype) + } +} diff --git a/packages/cli/src/helpers.ts b/packages/cli/src/helpers.ts index 3e5a5c15..03c6b75c 100644 --- a/packages/cli/src/helpers.ts +++ b/packages/cli/src/helpers.ts @@ -4,6 +4,7 @@ import camelCase from 'lodash/camelCase' import startCase from 'lodash/startCase' import { MIMIC_CONFIG_FILE } from './lib/MimicConfigHandler' +import { CommandError } from './errors' import log from './log' import { AbiFunctionItem, RequiredTaskConfig } from './types' @@ -46,11 +47,15 @@ export function filterTasks( const taskNames = new Set(tasks.map((task) => task.name)) + const warnInvalidTaskNames = (names: string[]): void => { + if (names.length > 0) { + console.warn(`${log.warnText('Warning:')} The following task names were not found: ${names.join(', ')}`) + } + } + if (include) { const invalidNames = include.filter((name) => !taskNames.has(name)) - if (invalidNames.length > 0) { - console.warn(`${log.warnText('Warning:')} The following task names were not found: ${invalidNames.join(', ')}`) - } + warnInvalidTaskNames(invalidNames) const validNames = new Set(include.filter((name) => taskNames.has(name))) if (validNames.size === 0) { @@ -63,9 +68,7 @@ export function filterTasks( if (exclude) { const invalidNames = exclude.filter((name) => !taskNames.has(name)) - if (invalidNames.length > 0) { - console.warn(`${log.warnText('Warning:')} The following task names were not found: ${invalidNames.join(', ')}`) - } + warnInvalidTaskNames(invalidNames) const excludeSet = new Set(exclude) const filteredTasks = tasks.filter((task) => !excludeSet.has(task.name)) @@ -77,3 +80,44 @@ export function filterTasks( return tasks } + +export async function runTasks( + command: Command, + tasks: RequiredTaskConfig[], + runTask: (task: RequiredTaskConfig) => Promise +): Promise { + const errors: Array<{ task: string; error: Error; code?: string; suggestions?: string[] }> = [] + + for (const task of tasks) { + console.log(`\n${log.highlightText(`[${task.name}]`)}`) + try { + await runTask(task) + } catch (error) { + const err = error as Error + console.error(log.warnText(`Task "${task.name}" failed: ${err.message}`)) + + if (err instanceof CommandError) { + if (err.code) console.error(` Code: ${err.code}`) + if (err.suggestions?.length) { + console.error(` Suggestions:`) + err.suggestions.forEach((s) => console.error(` - ${s}`)) + } + errors.push({ task: task.name, error: err, code: err.code, suggestions: err.suggestions }) + } else { + errors.push({ task: task.name, error: err }) + } + } + } + + if (errors.length > 0) { + console.log(`\n${log.warnText('Summary:')} ${errors.length}/${tasks.length} task(s) failed`) + errors.forEach(({ task, code }) => { + if (code) { + console.log(` - ${task} (${code})`) + } else { + console.log(` - ${task}`) + } + }) + command.exit(1) + } +} From 056fab3d5905455d4610b70f23599f0f1a9a832a Mon Sep 17 00:00:00 2001 From: ncomerci Date: Mon, 12 Jan 2026 13:33:00 -0300 Subject: [PATCH 14/27] refactor: minor tweaks --- packages/cli/src/commands/codegen.ts | 4 ++-- packages/cli/src/commands/test.ts | 4 +--- packages/cli/src/constants.ts | 4 +++- packages/cli/src/helpers.ts | 6 +----- 4 files changed, 7 insertions(+), 11 deletions(-) diff --git a/packages/cli/src/commands/codegen.ts b/packages/cli/src/commands/codegen.ts index b585005e..2a781cf6 100644 --- a/packages/cli/src/commands/codegen.ts +++ b/packages/cli/src/commands/codegen.ts @@ -4,8 +4,8 @@ import * as fs from 'fs' import { join } from 'path' import { filterTasks, runTasks, taskFilterFlags } from '../helpers' -import { AbisInterfaceGenerator, InputsInterfaceGenerator, ManifestHandler, MimicConfigHandler } from '../lib' -import { MIMIC_CONFIG_FILE } from '../lib/MimicConfigHandler' +import { AbisInterfaceGenerator, InputsInterfaceGenerator, ManifestHandler } from '../lib' +import MimicConfigHandler, { MIMIC_CONFIG_FILE } from '../lib/MimicConfigHandler' import log from '../log' import { Manifest, RequiredTaskConfig } from '../types' diff --git a/packages/cli/src/commands/test.ts b/packages/cli/src/commands/test.ts index f5450398..a24ed20f 100644 --- a/packages/cli/src/commands/test.ts +++ b/packages/cli/src/commands/test.ts @@ -36,9 +36,7 @@ export default class Test extends Command { testPaths.add(this.getTestPath(baseDir)) }) } else { - tasks.forEach(() => { - testPaths.add(this.getTestPath(baseDir)) - }) + testPaths.add(this.getTestPath(baseDir)) } } else { if (!skipCompile) await this.compileTask(DEFAULT_TASK, baseDir) diff --git a/packages/cli/src/constants.ts b/packages/cli/src/constants.ts index d67aca2f..334c1125 100644 --- a/packages/cli/src/constants.ts +++ b/packages/cli/src/constants.ts @@ -1,4 +1,6 @@ -export const DEFAULT_TASK = { +import { RequiredTaskConfig } from './types' + +export const DEFAULT_TASK: Omit = { manifest: 'manifest.yaml', path: 'src/task.ts', types: './src/types', diff --git a/packages/cli/src/helpers.ts b/packages/cli/src/helpers.ts index 03c6b75c..0629044d 100644 --- a/packages/cli/src/helpers.ts +++ b/packages/cli/src/helpers.ts @@ -112,11 +112,7 @@ export async function runTasks( if (errors.length > 0) { console.log(`\n${log.warnText('Summary:')} ${errors.length}/${tasks.length} task(s) failed`) errors.forEach(({ task, code }) => { - if (code) { - console.log(` - ${task} (${code})`) - } else { - console.log(` - ${task}`) - } + console.log(code ? ` - ${task} (${code})` : ` - ${task}`) }) command.exit(1) } From 97a4eaa7127b7f64123a644de2eedf0de0f770ec Mon Sep 17 00:00:00 2001 From: ncomerci Date: Tue, 13 Jan 2026 17:35:32 -0300 Subject: [PATCH 15/27] refactor: cli commands --- packages/cli/src/commands/build.ts | 61 ++++--- packages/cli/src/commands/codegen.ts | 88 ++++------ packages/cli/src/commands/compile.ts | 73 +++------ packages/cli/src/commands/deploy.ts | 147 ++++------------- packages/cli/src/commands/test.ts | 86 +++++----- packages/cli/src/core/build.ts | 43 +++++ packages/cli/src/core/codegen.ts | 168 ++++++++++++++++++++ packages/cli/src/core/compile.ts | 63 ++++++++ packages/cli/src/core/deploy.ts | 103 ++++++++++++ packages/cli/src/core/errors.ts | 109 +++++++++++++ packages/cli/src/core/index.ts | 7 + packages/cli/src/core/test.ts | 45 ++++++ packages/cli/src/core/types.ts | 142 +++++++++++++++++ packages/cli/src/helpers.ts | 22 +++ packages/cli/src/log.ts | 20 +++ packages/cli/tests/commands/build.spec.ts | 12 +- packages/cli/tests/commands/codegen.spec.ts | 2 +- packages/cli/tests/commands/compile.spec.ts | 12 +- packages/cli/tests/commands/deploy.spec.ts | 21 +-- 19 files changed, 911 insertions(+), 313 deletions(-) create mode 100644 packages/cli/src/core/build.ts create mode 100644 packages/cli/src/core/codegen.ts create mode 100644 packages/cli/src/core/compile.ts create mode 100644 packages/cli/src/core/deploy.ts create mode 100644 packages/cli/src/core/errors.ts create mode 100644 packages/cli/src/core/index.ts create mode 100644 packages/cli/src/core/test.ts create mode 100644 packages/cli/src/core/types.ts diff --git a/packages/cli/src/commands/build.ts b/packages/cli/src/commands/build.ts index 9dbccdec..22f7461c 100644 --- a/packages/cli/src/commands/build.ts +++ b/packages/cli/src/commands/build.ts @@ -1,12 +1,12 @@ +import { confirm } from '@inquirer/prompts' import { Command, Flags } from '@oclif/core' -import { filterTasks, runTasks, taskFilterFlags } from '../helpers' -import MimicConfigHandler, { MIMIC_CONFIG_FILE } from '../lib/MimicConfigHandler' +import { build } from '../core' +import { filterTasks, handleCoreError, runTasks, taskFilterFlags, toTaskConfig } from '../helpers' +import MimicConfigHandler from '../lib/MimicConfigHandler' +import log, { coreLogger } from '../log' import { RequiredTaskConfig } from '../types' -import Codegen from './codegen' -import Compile from './compile' - export default class Build extends Command { static override description = 'Runs code generation and then compiles the task' @@ -24,19 +24,14 @@ export default class Build extends Command { description: 'remove existing generated types before generating new files', default: false, }), - 'skip-config': Flags.boolean({ - hidden: true, - description: `Skip ${MIMIC_CONFIG_FILE} config (used internally)`, - default: false, - }), ...taskFilterFlags, } public async run(): Promise { const { flags } = await this.parse(Build) - const { manifest, task, output, types, clean, include, exclude, 'skip-config': skipConfig } = flags + const { manifest, task, output, types, clean, include, exclude } = flags - if (!skipConfig && MimicConfigHandler.exists()) { + if (MimicConfigHandler.exists()) { const mimicConfig = MimicConfigHandler.load(this) const allTasks = MimicConfigHandler.getTasks(mimicConfig) const tasks = filterTasks(this, allTasks, include, exclude) @@ -47,20 +42,36 @@ export default class Build extends Command { } private async runForTask(task: Omit, clean: boolean): Promise { - const codegenArgs: string[] = ['--manifest', task.manifest, '--output', task.types, '--skip-config'] - if (clean) codegenArgs.push('--clean') + const taskConfig = toTaskConfig(task) - await Codegen.run(codegenArgs) + try { + const result = await build( + { + manifestPath: taskConfig.manifestPath, + taskPath: taskConfig.taskPath, + outputDir: taskConfig.outputDir, + typesDir: taskConfig.typesDir, + clean, + confirmClean: async () => { + const shouldDelete = await confirm({ + message: `Are you sure you want to ${log.warnText('delete')} all the contents in ${log.highlightText(taskConfig.typesDir)}. This action is ${log.warnText('irreversible')}`, + default: false, + }) + if (!shouldDelete) { + coreLogger.info('You can remove the --clean flag from your command') + coreLogger.info('Stopping initialization...') + } + return shouldDelete + }, + }, + coreLogger + ) - const compileArgs: string[] = [ - '--task', - task.path, - '--manifest', - task.manifest, - '--output', - task.output, - '--skip-config', - ] - await Compile.run(compileArgs) + if (clean && !result.success) this.exit(0) + + coreLogger.info(`Build complete! Artifacts in ${task.output}/`) + } catch (error) { + handleCoreError(this, error) + } } } diff --git a/packages/cli/src/commands/codegen.ts b/packages/cli/src/commands/codegen.ts index 2a781cf6..81bb6da1 100644 --- a/packages/cli/src/commands/codegen.ts +++ b/packages/cli/src/commands/codegen.ts @@ -1,13 +1,11 @@ import { confirm } from '@inquirer/prompts' import { Command, Flags } from '@oclif/core' -import * as fs from 'fs' -import { join } from 'path' -import { filterTasks, runTasks, taskFilterFlags } from '../helpers' -import { AbisInterfaceGenerator, InputsInterfaceGenerator, ManifestHandler } from '../lib' -import MimicConfigHandler, { MIMIC_CONFIG_FILE } from '../lib/MimicConfigHandler' -import log from '../log' -import { Manifest, RequiredTaskConfig } from '../types' +import { codegen } from '../core' +import { filterTasks, handleCoreError, runTasks, taskFilterFlags, toTaskConfig } from '../helpers' +import MimicConfigHandler from '../lib/MimicConfigHandler' +import log, { coreLogger } from '../log' +import { RequiredTaskConfig } from '../types' export default class Codegen extends Command { static override description = 'Generates typed interfaces for declared inputs and ABIs from your manifest.yaml file' @@ -22,70 +20,50 @@ export default class Codegen extends Command { description: 'Remove existing generated types before generating new files', default: false, }), - 'skip-config': Flags.boolean({ - hidden: true, - description: `Skip ${MIMIC_CONFIG_FILE} config (used internally by build command)`, - default: false, - }), ...taskFilterFlags, } public async run(): Promise { const { flags } = await this.parse(Codegen) - const { manifest, output, clean, include, exclude, 'skip-config': skipConfig } = flags + const { manifest, output, clean, include, exclude } = flags - if (!skipConfig && MimicConfigHandler.exists()) { + if (MimicConfigHandler.exists()) { const mimicConfig = MimicConfigHandler.load(this) const allTasks = MimicConfigHandler.getTasks(mimicConfig) const tasks = filterTasks(this, allTasks, include, exclude) await runTasks(this, tasks, (task) => this.runForTask(task, clean)) } else { - await this.runForTask({ manifest, types: output }, clean) + await this.runForTask({ manifest, types: output, path: '', output: '' }, clean) } } - private async runForTask(task: Omit, clean: boolean): Promise { - const manifestPath = task.manifest - const outputDir = task.types - const manifest = ManifestHandler.load(this, manifestPath) + private async runForTask(task: Omit, clean: boolean): Promise { + const taskConfig = toTaskConfig(task) - if (clean) { - const shouldDelete = await confirm({ - message: `Are you sure you want to ${log.warnText('delete')} all the contents in ${log.highlightText(outputDir)}. This action is ${log.warnText('irreversible')}`, - default: false, - }) - if (!shouldDelete) { - console.log('You can remove the --clean flag from your command') - console.log('Stopping initialization...') - this.exit(0) - } - log.startAction(`Deleting contents of ${outputDir}`) - if (fs.existsSync(outputDir)) fs.rmSync(outputDir, { recursive: true, force: true }) - } + try { + const result = await codegen( + { + manifestPath: taskConfig.manifestPath, + outputDir: taskConfig.typesDir, + clean, + confirmClean: async () => { + const shouldDelete = await confirm({ + message: `Are you sure you want to ${log.warnText('delete')} all the contents in ${log.highlightText(taskConfig.typesDir)}. This action is ${log.warnText('irreversible')}`, + default: false, + }) + if (!shouldDelete) { + console.log('You can remove the --clean flag from your command') + console.log('Stopping initialization...') + } + return shouldDelete + }, + }, + coreLogger + ) - log.startAction('Generating code') - if (Object.keys(manifest.inputs).length === 0 && Object.keys(manifest.abis).length === 0) { - log.stopAction() - return + if (clean && !result.success) this.exit(0) + } catch (error) { + handleCoreError(this, error) } - - if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true }) - - generateAbisCode(manifest, outputDir, manifestPath) - generateInputsCode(manifest, outputDir) - log.stopAction() } } - -function generateAbisCode(manifest: Manifest, outputDir: 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(`${outputDir}/${contractName}.ts`, abiInterface) - } -} - -function generateInputsCode(manifest: Manifest, outputDir: string) { - const inputsInterface = InputsInterfaceGenerator.generate(manifest.inputs) - if (inputsInterface.length > 0) fs.writeFileSync(`${outputDir}/index.ts`, inputsInterface) -} diff --git a/packages/cli/src/commands/compile.ts b/packages/cli/src/commands/compile.ts index 9cd66a59..2becdc5b 100644 --- a/packages/cli/src/commands/compile.ts +++ b/packages/cli/src/commands/compile.ts @@ -1,13 +1,9 @@ import { Command, Flags } from '@oclif/core' -import * as fs from 'fs' -import * as path from 'path' -import { CommandError } from '../errors' -import { filterTasks, runTasks, taskFilterFlags } from '../helpers' -import ManifestHandler from '../lib/ManifestHandler' -import MimicConfigHandler, { MIMIC_CONFIG_FILE } from '../lib/MimicConfigHandler' -import { execBinCommand } from '../lib/packageManager' -import log from '../log' +import { compile } from '../core' +import { filterTasks, handleCoreError, runTasks, taskFilterFlags, toTaskConfig } from '../helpers' +import MimicConfigHandler from '../lib/MimicConfigHandler' +import { coreLogger } from '../log' import { RequiredTaskConfig } from '../types' export default class Compile extends Command { @@ -19,62 +15,39 @@ export default class Compile extends Command { task: Flags.string({ char: 't', description: 'task to compile', default: 'src/task.ts' }), manifest: Flags.string({ char: 'm', description: 'manifest to validate', default: 'manifest.yaml' }), output: Flags.string({ char: 'o', description: 'output directory', default: './build' }), - 'skip-config': Flags.boolean({ - hidden: true, - description: `Skip ${MIMIC_CONFIG_FILE} config (used internally by build command)`, - default: false, - }), ...taskFilterFlags, } public async run(): Promise { const { flags } = await this.parse(Compile) - const { task: taskPath, output, manifest, include, exclude, 'skip-config': skipConfig } = flags + const { task: taskPath, output, manifest, include, exclude } = flags - if (!skipConfig && MimicConfigHandler.exists()) { + if (MimicConfigHandler.exists()) { const mimicConfig = MimicConfigHandler.load(this) const allTasks = MimicConfigHandler.getTasks(mimicConfig) const tasks = filterTasks(this, allTasks, include, exclude) await runTasks(this, tasks, (task) => this.runForTask(task)) } else { - await this.runForTask({ manifest, path: taskPath, output }) + await this.runForTask({ manifest, path: taskPath, output, types: '' }) } } - private async runForTask(task: Omit): Promise { - const taskPath = path.resolve(task.path) - const outputDir = path.resolve(task.output) - - if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true }) - - log.startAction('Verifying Manifest') - const manifest = ManifestHandler.load(this, task.manifest) - log.startAction('Compiling') - - const ascArgs = [ - taskPath, - '--target', - 'release', - '--outFile', - path.join(outputDir, 'task.wasm'), - '--optimize', - '--exportRuntime', - '--transform', - 'json-as/transform', - ] - - const result = execBinCommand('asc', ascArgs, process.cwd()) - if (result.status !== 0) { - throw new CommandError('AssemblyScript compilation failed', { - code: 'BuildError', - suggestions: ['Check the AssemblyScript file'], - }) + private async runForTask(task: Omit): Promise { + const taskConfig = toTaskConfig(task) + + try { + await compile( + { + manifestPath: taskConfig.manifestPath, + taskPath: taskConfig.taskPath, + outputDir: taskConfig.outputDir, + }, + coreLogger + ) + + coreLogger.info(`Build complete! Artifacts in ${task.output}/`) + } catch (error) { + handleCoreError(this, error) } - - log.startAction('Saving files') - - fs.writeFileSync(path.join(outputDir, 'manifest.json'), JSON.stringify(manifest, null, 2)) - log.stopAction() - console.log(`Build complete! Artifacts in ${task.output}/`) } } diff --git a/packages/cli/src/commands/deploy.ts b/packages/cli/src/commands/deploy.ts index 38b60d75..d64c09a5 100644 --- a/packages/cli/src/commands/deploy.ts +++ b/packages/cli/src/commands/deploy.ts @@ -1,22 +1,15 @@ 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 { resolve } from 'path' import { DEFAULT_TASK } from '../constants' -import { CommandError, GENERIC_SUGGESTION } from '../errors' -import { filterTasks, runTasks, taskFilterFlags } from '../helpers' -import { ProfileCredentials } from '../lib/CredentialsManager' +import { build, deploy, MIMIC_REGISTRY_DEFAULT } from '../core' +import { filterTasks, handleCoreError, runTasks, taskFilterFlags, toTaskConfig } from '../helpers' import MimicConfigHandler from '../lib/MimicConfigHandler' -import { execBinCommand } from '../lib/packageManager' -import log from '../log' +import log, { coreLogger } from '../log' import { RequiredTaskConfig } from '../types' import Authenticate from './authenticate' -const MIMIC_REGISTRY_DEFAULT = 'https://api-protocol.mimic.fi' - export default class Deploy extends Authenticate { static override description = 'Uploads your compiled task artifacts to IPFS and registers it into the Mimic Registry' @@ -59,119 +52,39 @@ export default class Deploy extends Authenticate { ): Promise { const inputPath = resolve(inputDir) const outputPath = resolve(task.output) + const taskConfig = toTaskConfig(task) const credentials = this.authenticate({ profile, 'api-key': apiKey }) - if (!skipCompile) { - const build = execBinCommand( - 'mimic', - [ - 'build', - '--manifest', - task.manifest, - '--task', - task.path, - '--output', - inputPath, - '--types', - task.types, - '--skip-config', - ], - process.cwd() - ) - if (build.status !== 0) { - throw new CommandError('Build failed', { - code: 'BuildError', - suggestions: ['Check the task source code and manifest'], - }) - } - } - - log.startAction('Validating') - - if (!fs.existsSync(inputPath)) { - throw new CommandError(`Directory ${log.highlightText(inputPath)} does not exist`, { - code: 'Directory Not Found', - suggestions: ['Use the --input flag to specify the correct path'], - }) - } - - const neededFiles = ['manifest.json', 'task.wasm'].map((file) => join(inputPath, file)) - for (const file of neededFiles) { - if (!fs.existsSync(file)) { - throw new CommandError(`Could not find ${file}`, { - code: 'File Not Found', - suggestions: [`Use ${log.highlightText('mimic compile')} to generate the needed files`], - }) + try { + if (!skipCompile) { + await build( + { + manifestPath: taskConfig.manifestPath, + taskPath: taskConfig.taskPath, + outputDir: inputPath, + typesDir: taskConfig.typesDir, + clean: false, + }, + coreLogger + ) } - } - - log.startAction('Uploading to Mimic Registry') - const CID = await this.uploadToRegistry(neededFiles, credentials, registryUrl) - console.log(`IPFS CID: ${log.highlightText(CID)}`) - log.stopAction() - if (!fs.existsSync(outputPath)) fs.mkdirSync(outputPath, { recursive: true }) - fs.writeFileSync(join(outputPath, 'CID.json'), JSON.stringify({ CID }, null, 2)) - console.log(`CID saved at ${log.highlightText(outputPath)}`) - console.log(`Task deployed!`) - } - - private async uploadToRegistry( - files: string[], - credentials: ProfileCredentials, - registryUrl: string - ): Promise { - try { - const form = filesToForm(files) - const { data } = await axios.post(`${registryUrl}/tasks`, form, { - headers: { - 'x-api-key': credentials.apiKey, - 'Content-Type': `multipart/form-data; boundary=${form.getBoundary()}`, + const result = await deploy( + { + inputDir: inputPath, + outputDir: outputPath, + apiKey: credentials.apiKey, + registryUrl, }, - }) - return data.CID - } catch (err) { - this.handleError(err, 'Failed to upload to registry') - } - } - - private handleError(err: unknown, message: string): never { - if (!(err instanceof AxiosError)) throw err as Error - const statusCode = err.response?.status + coreLogger + ) - switch (statusCode) { - case 400: { - const errMessage = err.response?.data?.content?.message || message - throw new CommandError(errMessage, { - code: 'Bad Request', - suggestions: ['Review the uploaded files'], - }) - } - case 401: - throw new CommandError(message, { - code: 'Unauthorized', - suggestions: ['Review your key'], - }) - case 403: - throw new CommandError(message, { - code: 'Invalid api key', - suggestions: ['Review your key'], - }) - default: - throw new CommandError(`${message} - ${err.message}`, { - code: `${statusCode} Error`, - suggestions: GENERIC_SUGGESTION, - }) + coreLogger.info(`IPFS CID: ${log.highlightText(result.cid)}`) + coreLogger.info(`CID saved at ${log.highlightText(outputPath)}`) + coreLogger.info(`Task deployed!`) + } catch (error) { + handleCoreError(this, error) } } } - -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()) -} diff --git a/packages/cli/src/commands/test.ts b/packages/cli/src/commands/test.ts index a24ed20f..16889e64 100644 --- a/packages/cli/src/commands/test.ts +++ b/packages/cli/src/commands/test.ts @@ -2,9 +2,10 @@ import { Command, Flags } from '@oclif/core' import * as path from 'path' import { DEFAULT_TASK } from '../constants' -import { filterTasks, runTasks, taskFilterFlags } from '../helpers' +import { buildForTest, getTestPath, runTests, TestError } from '../core' +import { filterTasks, handleCoreError, runTasks, taskFilterFlags, toTaskConfig } from '../helpers' import MimicConfigHandler from '../lib/MimicConfigHandler' -import { execBinCommand } from '../lib/packageManager' +import { coreLogger } from '../log' import { RequiredTaskConfig } from '../types' export default class Test extends Command { @@ -25,52 +26,57 @@ export default class Test extends Command { const testPaths = new Set() - if (MimicConfigHandler.exists(baseDir)) { - const mimicConfig = MimicConfigHandler.load(this, baseDir) - const allTasks = MimicConfigHandler.getTasks(mimicConfig) - const tasks = filterTasks(this, allTasks, include, exclude) + try { + if (MimicConfigHandler.exists(baseDir)) { + const mimicConfig = MimicConfigHandler.load(this, baseDir) + const allTasks = MimicConfigHandler.getTasks(mimicConfig) + const tasks = filterTasks(this, allTasks, include, exclude) - if (!skipCompile) { - await runTasks(this, tasks, async (task) => { - await this.compileTask(task, baseDir) - testPaths.add(this.getTestPath(baseDir)) - }) + if (!skipCompile) { + await runTasks(this, tasks, async (task) => { + await this.compileTask(task, baseDir) + testPaths.add(getTestPath(baseDir)) + }) + } else { + testPaths.add(getTestPath(baseDir)) + } } else { - testPaths.add(this.getTestPath(baseDir)) + if (!skipCompile) await this.compileTask(DEFAULT_TASK, baseDir) + testPaths.add(getTestPath(baseDir)) } - } else { - if (!skipCompile) await this.compileTask(DEFAULT_TASK, baseDir) - testPaths.add(this.getTestPath(baseDir)) - } - if (testPaths.size > 0) this.runTests(Array.from(testPaths), baseDir) + if (testPaths.size > 0) { + runTests({ testPaths: Array.from(testPaths), baseDir }, coreLogger) + } + } catch (error) { + if (error instanceof TestError) { + this.exit(error.exitCode) + } + handleCoreError(this, error) + } } private async compileTask(task: Omit, baseDir: string): Promise { - const cg = execBinCommand( - 'mimic', - ['codegen', '--manifest', task.manifest, '--output', task.types, '--skip-config'], - baseDir - ) - if (cg.status !== 0) { - throw new Error(`Codegen failed for task with status ${cg.status}`) - } - const cp = execBinCommand( - 'mimic', - ['compile', '--task', task.path, '--manifest', task.manifest, '--output', task.output, '--skip-config'], - baseDir - ) - if (cp.status !== 0) { - throw new Error(`Compile failed for task with status ${cp.status}`) - } - } + const taskConfig = toTaskConfig(task) - private getTestPath(baseDir: string): string { - return path.join(baseDir, 'tests', '**', '*.spec.ts') - } + // Change to baseDir for compilation + const originalCwd = process.cwd() + try { + process.chdir(baseDir) - private runTests(testPaths: string[], baseDir: string): void { - const result = execBinCommand('tsx', ['./node_modules/mocha/bin/mocha.js', ...testPaths], baseDir) - if (result.status !== 0) this.exit(result.status ?? 1) + await buildForTest( + { + manifestPath: taskConfig.manifestPath, + taskPath: taskConfig.taskPath, + outputDir: taskConfig.outputDir, + typesDir: taskConfig.typesDir, + clean: false, + cwd: baseDir, + }, + coreLogger + ) + } finally { + process.chdir(originalCwd) + } } } diff --git a/packages/cli/src/core/build.ts b/packages/cli/src/core/build.ts new file mode 100644 index 00000000..f887fc22 --- /dev/null +++ b/packages/cli/src/core/build.ts @@ -0,0 +1,43 @@ +import { defaultLogger } from '../log' + +import { codegen } from './codegen' +import { compile } from './compile' +import { BuildOptions, BuildResult, Logger } from './types' + +export async function build(options: BuildOptions, logger: Logger = defaultLogger): Promise { + const { manifestPath, taskPath, outputDir, typesDir, clean, confirmClean, cwd } = options + + const codegenResult = await codegen( + { + manifestPath, + outputDir: typesDir, + clean, + confirmClean, + }, + logger + ) + + if (clean && !codegenResult.success) { + return { + codegen: codegenResult, + compile: { wasmPath: '', manifestJsonPath: '', success: false }, + success: false, + } + } + + const compileResult = await compile( + { + manifestPath, + taskPath, + outputDir, + cwd, + }, + logger + ) + + return { + codegen: codegenResult, + compile: compileResult, + success: codegenResult.success && compileResult.success, + } +} diff --git a/packages/cli/src/core/codegen.ts b/packages/cli/src/core/codegen.ts new file mode 100644 index 00000000..4a628c42 --- /dev/null +++ b/packages/cli/src/core/codegen.ts @@ -0,0 +1,168 @@ +import * as fs from 'fs' +import { load } from 'js-yaml' +import * as path from 'path' +import { ZodError } from 'zod' + +import { DuplicateEntryError, EmptyManifestError, MoreThanOneEntryError } from '../errors' +import { AbisInterfaceGenerator, InputsInterfaceGenerator } from '../lib' +import { defaultLogger } from '../log' +import { Manifest } from '../types' +import { ManifestValidator } from '../validators' + +import { CodegenError, FileNotFoundError, ManifestValidationError } from './errors' +import { CodegenOptions, CodegenResult, Logger } from './types' + +export function loadManifest(manifestPath: string): Manifest { + if (!fs.existsSync(manifestPath)) { + throw new FileNotFoundError(manifestPath, ['Use the -m or --manifest flag to specify the correct path']) + } + + let loadedManifest + try { + loadedManifest = load(fs.readFileSync(manifestPath, 'utf-8')) + } catch { + throw new FileNotFoundError(manifestPath, [ + 'Could not read or parse the manifest file', + 'Ensure the file is valid YAML', + ]) + } + + try { + return validateManifest(loadedManifest) + } catch (err) { + throw convertManifestError(err) + } +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function validateManifest(manifest: any): Manifest { + if (!manifest) throw new EmptyManifestError() + + const mergedManifest = { + ...manifest, + inputs: mergeIfUnique(manifest.inputs), + abis: mergeIfUnique(manifest.abis), + metadata: { libVersion: getLibVersion() }, + } + return ManifestValidator.parse(mergedManifest) +} + +function mergeIfUnique(list: Record[]) { + const merged: Record = {} + for (const obj of list || []) { + const entries = Object.entries(obj) + if (entries.length !== 1) throw new MoreThanOneEntryError(entries) + const [key, val] = entries[0] + if (key in merged) throw new DuplicateEntryError(key) + merged[key] = val + } + return merged +} + +function getLibVersion(): string { + try { + let currentDir = process.cwd() + while (currentDir !== path.dirname(currentDir)) { + const libPackagePath = path.join(currentDir, 'node_modules', '@mimicprotocol', 'lib-ts', 'package.json') + if (fs.existsSync(libPackagePath)) return JSON.parse(fs.readFileSync(libPackagePath, 'utf-8')).version + currentDir = path.dirname(currentDir) + } + throw new Error('Could not find @mimicprotocol/lib-ts package') + } catch (error) { + throw new Error(`Failed to read @mimicprotocol/lib-ts version: ${error}`) + } +} + +function convertManifestError(err: unknown): ManifestValidationError { + if (err instanceof MoreThanOneEntryError) { + return new ManifestValidationError(err.message, [ + `${err.location[1][0]}: ${err.location[1][1]} might be missing a prepended '-' on manifest`, + ]) + } + if (err instanceof DuplicateEntryError) { + return new ManifestValidationError(err.message, [`Review manifest for duplicate key: ${err.duplicateKey}`]) + } + if (err instanceof EmptyManifestError) { + return new ManifestValidationError(err.message, ['Verify if you are using the correct manifest file']) + } + if (err instanceof ZodError) { + return new ManifestValidationError( + 'Missing/Incorrect Fields', + err.errors.map((e) => `Fix Field "${e.path.join('.')}" -- ${e.message}`) + ) + } + return new ManifestValidationError(`Unknown Error: ${err}`) +} + +function generateAbisCode(manifest: Manifest, outputDir: string, manifestDir: string): string[] { + const generatedFiles: string[] = [] + + for (const [contractName, abiRelativePath] of Object.entries(manifest.abis)) { + const abiPath = path.join(manifestDir, '../', abiRelativePath) + if (!fs.existsSync(abiPath)) { + throw new CodegenError(`ABI file not found: ${abiPath}`, [ + `Ensure the ABI file exists at: ${abiRelativePath}`, + 'Check the paths in your manifest.yaml', + ]) + } + + const abi = JSON.parse(fs.readFileSync(abiPath, 'utf-8')) + const abiInterface = AbisInterfaceGenerator.generate(abi, contractName) + if (abiInterface.length > 0) { + const outputPath = `${outputDir}/${contractName}.ts` + fs.writeFileSync(outputPath, abiInterface) + generatedFiles.push(outputPath) + } + } + + return generatedFiles +} + +function generateInputsCode(manifest: Manifest, outputDir: string): string[] { + const generatedFiles: string[] = [] + const inputsInterface = InputsInterfaceGenerator.generate(manifest.inputs) + + if (inputsInterface.length > 0) { + const outputPath = `${outputDir}/index.ts` + fs.writeFileSync(outputPath, inputsInterface) + generatedFiles.push(outputPath) + } + + return generatedFiles +} + +export async function codegen(options: CodegenOptions, logger: Logger = defaultLogger): Promise { + const { manifestPath, outputDir, clean, confirmClean } = options + + const manifest = loadManifest(manifestPath) + + if (clean) { + if (confirmClean) { + const shouldDelete = await confirmClean() + if (!shouldDelete) return { generatedFiles: [], success: false } + } + + logger.startAction(`Deleting contents of ${outputDir}`) + if (fs.existsSync(outputDir)) fs.rmSync(outputDir, { recursive: true, force: true }) + } + + logger.startAction('Generating code') + + if (Object.keys(manifest.inputs).length === 0 && Object.keys(manifest.abis).length === 0) { + logger.stopAction() + return { generatedFiles: [], success: true } + } + + if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true }) + + const generatedFiles: string[] = [] + generatedFiles.push(...generateAbisCode(manifest, outputDir, manifestPath)) + generatedFiles.push(...generateInputsCode(manifest, outputDir)) + + logger.stopAction() + + return { + generatedFiles, + success: true, + } +} diff --git a/packages/cli/src/core/compile.ts b/packages/cli/src/core/compile.ts new file mode 100644 index 00000000..3b302b4a --- /dev/null +++ b/packages/cli/src/core/compile.ts @@ -0,0 +1,63 @@ +import * as fs from 'fs' +import * as path from 'path' + +import { execBinCommand } from '../lib/packageManager' +import { defaultLogger } from '../log' + +import { loadManifest } from './codegen' +import { CompilationError, FileNotFoundError } from './errors' +import { CompileOptions, CompileResult, Logger } from './types' + +export async function compile(options: CompileOptions, logger: Logger = defaultLogger): Promise { + const { manifestPath, taskPath, outputDir, cwd = process.cwd() } = options + + const resolvedTaskPath = path.resolve(taskPath) + const resolvedOutputDir = path.resolve(outputDir) + + if (!fs.existsSync(resolvedTaskPath)) { + throw new FileNotFoundError(resolvedTaskPath, [ + 'Use the -t or --task flag to specify the correct path', + `Expected task file at: ${taskPath}`, + ]) + } + + if (!fs.existsSync(resolvedOutputDir)) fs.mkdirSync(resolvedOutputDir, { recursive: true }) + + logger.startAction('Verifying Manifest') + const manifest = loadManifest(manifestPath) + + logger.startAction('Compiling') + + const wasmPath = path.join(resolvedOutputDir, 'task.wasm') + const ascArgs = [ + resolvedTaskPath, + '--target', + 'release', + '--outFile', + wasmPath, + '--optimize', + '--exportRuntime', + '--transform', + 'json-as/transform', + ] + + const result = execBinCommand('asc', ascArgs, cwd) + if (result.status !== 0) { + throw new CompilationError('AssemblyScript compilation failed', [ + 'Check the AssemblyScript file for syntax errors', + 'Ensure all dependencies are installed', + ]) + } + + logger.startAction('Saving files') + const manifestJsonPath = path.join(resolvedOutputDir, 'manifest.json') + fs.writeFileSync(manifestJsonPath, JSON.stringify(manifest, null, 2)) + + logger.stopAction() + + return { + wasmPath, + manifestJsonPath, + success: true, + } +} diff --git a/packages/cli/src/core/deploy.ts b/packages/cli/src/core/deploy.ts new file mode 100644 index 00000000..67ec3257 --- /dev/null +++ b/packages/cli/src/core/deploy.ts @@ -0,0 +1,103 @@ +import axios, { AxiosError } from 'axios' +import FormData from 'form-data' +import * as fs from 'fs' +import { join } from 'path' + +import { defaultLogger } from '../log' + +import { DeployError, DirectoryNotFoundError, FileNotFoundError } from './errors' +import { DeployOptions, DeployResult, Logger } from './types' + +export const MIMIC_REGISTRY_DEFAULT = 'https://api-protocol.mimic.fi' +const REQUIRED_FILES = ['manifest.json', 'task.wasm'] + +function validateInputDirectory(inputDir: string): string[] { + if (!fs.existsSync(inputDir)) { + throw new DirectoryNotFoundError(inputDir, ['Use the --input flag to specify the correct path']) + } + + const neededFiles = REQUIRED_FILES.map((file) => join(inputDir, file)) + + for (const file of neededFiles) { + if (!fs.existsSync(file)) throw new FileNotFoundError(file, ['Use `mimic compile` to generate the needed files']) + } + + return neededFiles +} + +function 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()) +} + +async function uploadToRegistry(files: string[], apiKey: string, registryUrl: string): Promise { + try { + const form = filesToForm(files) + const { data } = await axios.post(`${registryUrl}/tasks`, form, { + headers: { + 'x-api-key': apiKey, + 'Content-Type': `multipart/form-data; boundary=${form.getBoundary()}`, + }, + }) + return data.CID + } catch (err) { + handleUploadError(err) + } +} + +function handleUploadError(err: unknown): never { + if (!(err instanceof AxiosError)) throw new DeployError(err instanceof Error ? err.message : String(err)) + + const statusCode = err.response?.status + + switch (statusCode) { + case 400: { + const errMessage = err.response?.data?.content?.message || 'Bad request' + throw new DeployError(errMessage, { + statusCode, + suggestions: ['Review the uploaded files'], + }) + } + case 401: + throw new DeployError('Unauthorized', { + statusCode, + suggestions: ['Review your API key'], + }) + case 403: + throw new DeployError('Invalid API key', { + statusCode, + suggestions: ['Review your API key'], + }) + default: + throw new DeployError(`Upload failed: ${err.message}`, { + statusCode, + }) + } +} + +export async function deploy(options: DeployOptions, logger: Logger = defaultLogger): Promise { + const { inputDir, outputDir, apiKey, registryUrl = MIMIC_REGISTRY_DEFAULT } = options + + logger.startAction('Validating') + const files = validateInputDirectory(inputDir) + + logger.startAction('Uploading to Mimic Registry') + const cid = await uploadToRegistry(files, apiKey, registryUrl) + + logger.stopAction() + + if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true }) + + const cidJsonPath = join(outputDir, 'CID.json') + fs.writeFileSync(cidJsonPath, JSON.stringify({ CID: cid }, null, 2)) + + return { + cid, + cidJsonPath, + success: true, + } +} diff --git a/packages/cli/src/core/errors.ts b/packages/cli/src/core/errors.ts new file mode 100644 index 00000000..54229e7a --- /dev/null +++ b/packages/cli/src/core/errors.ts @@ -0,0 +1,109 @@ +export const GENERIC_SUGGESTION = [ + 'Contact the Mimic team for further assistance at our website https://www.mimic.fi or discord https://discord.mimic.fi', +] + +export class CoreError extends Error { + public code: string + public suggestions: string[] + + constructor(message: string, options: { code: string; suggestions?: string[] }) { + super(message) + this.name = this.constructor.name + this.code = options.code + this.suggestions = options.suggestions ?? GENERIC_SUGGESTION + Object.setPrototypeOf(this, new.target.prototype) + } +} + +export class FileNotFoundError extends CoreError { + public filePath: string + + constructor(filePath: string, suggestions?: string[]) { + super(`File not found: ${filePath}`, { + code: 'FileNotFound', + suggestions: suggestions ?? [`Check that the file exists at: ${filePath}`], + }) + this.filePath = filePath + } +} + +export class DirectoryNotFoundError extends CoreError { + public dirPath: string + + constructor(dirPath: string, suggestions?: string[]) { + super(`Directory not found: ${dirPath}`, { + code: 'DirectoryNotFound', + suggestions: suggestions ?? [`Check that the directory exists at: ${dirPath}`], + }) + this.dirPath = dirPath + } +} + +export class ManifestValidationError extends CoreError { + constructor(message: string, suggestions?: string[]) { + super(message, { + code: 'ManifestValidationError', + suggestions: suggestions ?? ['Check the manifest.yaml file for errors'], + }) + } +} + +export class CodegenError extends CoreError { + constructor(message: string, suggestions?: string[]) { + super(message, { + code: 'CodegenError', + suggestions: suggestions ?? ['Check the manifest.yaml file and ABI files'], + }) + } +} + +export class CompilationError extends CoreError { + constructor(message: string, suggestions?: string[]) { + super(message, { + code: 'CompilationError', + suggestions: suggestions ?? ['Check the AssemblyScript file for syntax errors'], + }) + } +} + +export class BuildError extends CoreError { + constructor(message: string, suggestions?: string[]) { + super(message, { + code: 'BuildError', + suggestions: suggestions ?? ['Check the task source code and manifest'], + }) + } +} + +export class DeployError extends CoreError { + public statusCode?: number + + constructor(message: string, options?: { statusCode?: number; suggestions?: string[] }) { + super(message, { + code: options?.statusCode ? `Deploy${options.statusCode}Error` : 'DeployError', + suggestions: options?.suggestions ?? ['Check your API key and network connection'], + }) + this.statusCode = options?.statusCode + } +} + +export class AuthenticationError extends CoreError { + constructor(message: string, suggestions?: string[]) { + super(message, { + code: 'AuthenticationError', + suggestions: suggestions ?? ['Check your API key or login credentials'], + }) + } +} + +export class TestError extends CoreError { + public exitCode: number + + constructor(message: string, exitCode: number, suggestions?: string[]) { + super(message, { + code: 'TestError', + suggestions: suggestions ?? ['Check the test output for details'], + }) + this.exitCode = exitCode + } +} diff --git a/packages/cli/src/core/index.ts b/packages/cli/src/core/index.ts new file mode 100644 index 00000000..0535e6ed --- /dev/null +++ b/packages/cli/src/core/index.ts @@ -0,0 +1,7 @@ +export * from './build' +export * from './codegen' +export * from './compile' +export * from './deploy' +export * from './errors' +export * from './test' +export * from './types' diff --git a/packages/cli/src/core/test.ts b/packages/cli/src/core/test.ts new file mode 100644 index 00000000..8d221653 --- /dev/null +++ b/packages/cli/src/core/test.ts @@ -0,0 +1,45 @@ +import * as path from 'path' + +import { execBinCommand } from '../lib/packageManager' +import { defaultLogger } from '../log' + +import { build } from './build' +import { TestError } from './errors' +import { BuildOptions, Logger, RunTestsOptions, RunTestsResult } from './types' + +export function getTestPath(baseDir: string): string { + return path.join(baseDir, 'tests', '**', '*.spec.ts') +} + +export async function buildForTest( + options: Omit, + logger: Logger = defaultLogger +): Promise { + await build( + { + ...options, + clean: false, + }, + logger + ) +} + +export function runTests(options: RunTestsOptions, logger: Logger = defaultLogger): RunTestsResult { + const { testPaths, baseDir } = options + + logger.startAction('Running tests') + + const result = execBinCommand('tsx', ['./node_modules/mocha/bin/mocha.js', ...testPaths], baseDir) + + const exitCode = result.status ?? 1 + const success = exitCode === 0 + + logger.stopAction() + + if (!success) throw new TestError('Tests failed', exitCode, ['Check the test output for details']) + + return { + exitCode, + success, + } +} diff --git a/packages/cli/src/core/types.ts b/packages/cli/src/core/types.ts new file mode 100644 index 00000000..1bdafc0b --- /dev/null +++ b/packages/cli/src/core/types.ts @@ -0,0 +1,142 @@ +// ============================================================================ +// Codegen Types +// ============================================================================ + +export interface CodegenOptions { + /** Path to the manifest.yaml file */ + manifestPath: string + /** Output directory for generated types */ + outputDir: string + /** Whether to delete existing files before generating */ + clean: boolean + /** Callback for confirming clean operation (returns true to proceed) */ + confirmClean?: () => Promise +} + +export interface CodegenResult { + /** List of generated files */ + generatedFiles: string[] + /** Whether any files were generated */ + success: boolean +} + +// ============================================================================ +// Compile Types +// ============================================================================ + +export interface CompileOptions { + /** Path to the manifest.yaml file */ + manifestPath: string + /** Path to the task TypeScript file */ + taskPath: string + /** Output directory for compiled artifacts */ + outputDir: string + /** Working directory for compilation */ + cwd?: string +} + +export interface CompileResult { + /** Path to the generated WASM file */ + wasmPath: string + /** Path to the generated manifest.json */ + manifestJsonPath: string + /** Whether compilation succeeded */ + success: boolean +} + +// ============================================================================ +// Build Types +// ============================================================================ + +export interface BuildOptions { + /** Path to the manifest.yaml file */ + manifestPath: string + /** Path to the task TypeScript file */ + taskPath: string + /** Output directory for build artifacts */ + outputDir: string + /** Output directory for generated types */ + typesDir: string + /** Whether to delete existing types before generating */ + clean: boolean + /** Callback for confirming clean operation */ + confirmClean?: () => Promise + /** Working directory for build */ + cwd?: string +} + +export interface BuildResult { + /** Result from codegen step */ + codegen: CodegenResult + /** Result from compile step */ + compile: CompileResult + /** Whether the entire build succeeded */ + success: boolean +} + +// ============================================================================ +// Deploy Types +// ============================================================================ + +export interface DeployOptions { + /** Directory containing compiled artifacts (task.wasm, manifest.json) */ + inputDir: string + /** Output directory for CID.json */ + outputDir: string + /** API key for authentication */ + apiKey: string + /** Registry URL */ + registryUrl: string +} + +export interface DeployResult { + /** IPFS CID of the deployed task */ + cid: string + /** Path to the generated CID.json file */ + cidJsonPath: string + /** Whether deployment succeeded */ + success: boolean +} + +// ============================================================================ +// Test Types +// ============================================================================ + +export interface RunTestsOptions { + /** Glob patterns for test files */ + testPaths: string[] + /** Base directory for running tests */ + baseDir: string +} + +export interface RunTestsResult { + /** Exit code from test runner */ + exitCode: number + /** Whether tests passed */ + success: boolean +} + +// ============================================================================ +// Task Configuration +// ============================================================================ + +export interface TaskConfig { + name: string + manifestPath: string + taskPath: string + outputDir: string + typesDir: string +} + +// ============================================================================ +// Logging Interface +// ============================================================================ + +export interface Logger { + startAction(message: string): void + stopAction(): void + info(message: string): void + warn(message: string): void + error(message: string): void + success(message: string): void +} diff --git a/packages/cli/src/helpers.ts b/packages/cli/src/helpers.ts index 0629044d..98fbf6ef 100644 --- a/packages/cli/src/helpers.ts +++ b/packages/cli/src/helpers.ts @@ -3,7 +3,9 @@ import { Interface } from 'ethers' import camelCase from 'lodash/camelCase' import startCase from 'lodash/startCase' +import { TaskConfig } from './core/types' import { MIMIC_CONFIG_FILE } from './lib/MimicConfigHandler' +import { CoreError } from './core' import { CommandError } from './errors' import log from './log' import { AbiFunctionItem, RequiredTaskConfig } from './types' @@ -117,3 +119,23 @@ export async function runTasks( command.exit(1) } } + +export function handleCoreError(command: Command, error: unknown): void { + if (error instanceof CoreError) { + command.error(error.message, { + code: error.code, + suggestions: error.suggestions, + }) + } + throw error +} + +export function toTaskConfig(task: RequiredTaskConfig | Omit): TaskConfig { + return { + name: 'name' in task ? task.name : 'default', + manifestPath: task.manifest, + taskPath: task.path, + outputDir: task.output, + typesDir: task.types, + } +} diff --git a/packages/cli/src/log.ts b/packages/cli/src/log.ts index 58f26351..e2d2ec76 100644 --- a/packages/cli/src/log.ts +++ b/packages/cli/src/log.ts @@ -1,6 +1,8 @@ import { ux } from '@oclif/core' import { StandardAnsi } from '@oclif/core/lib/interfaces/theme' +import { Logger } from './core/types' + const log = { startAction: (text: string, color?: StandardAnsi) => { log.stopAction() @@ -14,4 +16,22 @@ const log = { highlightText: (text: string) => ux.colorize('yellow', text), } +export const coreLogger: Logger = { + startAction: log.startAction, + stopAction: log.stopAction, + info: console.log, + warn: console.warn, + error: console.error, + success: console.log, +} + +export const defaultLogger: Logger = { + startAction: () => {}, + stopAction: () => {}, + info: () => {}, + warn: () => {}, + error: () => {}, + success: () => {}, +} + export default log diff --git a/packages/cli/tests/commands/build.spec.ts b/packages/cli/tests/commands/build.spec.ts index e780ad95..350e5117 100644 --- a/packages/cli/tests/commands/build.spec.ts +++ b/packages/cli/tests/commands/build.spec.ts @@ -122,7 +122,7 @@ describe('build', () => { const invalidTaskPath = `${basePath}/tasks/invalid-task.ts` const command = buildCommand(withCommonFlags(manifestPath, invalidTaskPath, outputDir, typesDir)) - itThrowsACliError(command, 'AssemblyScript compilation failed', 'BuildError', 1) + itThrowsACliError(command, 'AssemblyScript compilation failed', 'CompilationError', 2) }) context('when the types output directory already exists', () => { @@ -147,28 +147,28 @@ describe('build', () => { const invalidManifest = `${basePath}/manifests/invalid-manifest.yaml` const command = buildCommand(withCommonFlags(invalidManifest, taskPath, outputDir, typesDir)) - itThrowsACliError(command, 'More than one entry', 'MoreThanOneEntryError', 1) + itThrowsACliError(command, 'More than one entry', 'ManifestValidationError', 1) }) context('when the manifest has repeated fields', () => { const invalidManifest = `${basePath}/manifests/invalid-manifest-repeated.yaml` const command = buildCommand(withCommonFlags(invalidManifest, taskPath, outputDir, typesDir)) - itThrowsACliError(command, 'Duplicate Entry', 'DuplicateEntryError', 1) + itThrowsACliError(command, 'Duplicate Entry', 'ManifestValidationError', 1) }) context('when the manifest is incomplete', () => { const invalidManifest = `${basePath}/manifests/incomplete-manifest.yaml` const command = buildCommand(withCommonFlags(invalidManifest, taskPath, outputDir, typesDir)) - itThrowsACliError(command, 'Missing/Incorrect Fields', 'FieldsError', 3) + itThrowsACliError(command, 'Missing/Incorrect Fields', 'ManifestValidationError', 3) }) context('when the manifest is empty', () => { const invalidManifest = `${basePath}/manifests/empty-manifest.yaml` const command = buildCommand(withCommonFlags(invalidManifest, taskPath, outputDir, typesDir)) - itThrowsACliError(command, 'Empty Manifest', 'EmptyManifestError', 1) + itThrowsACliError(command, 'Empty Manifest', 'ManifestValidationError', 1) }) }) }) @@ -177,7 +177,7 @@ describe('build', () => { const inexistentManifest = `${manifestPath}-none` const command = buildCommand(withCommonFlags(inexistentManifest, taskPath, outputDir, typesDir)) - itThrowsACliError(command, `Could not find ${inexistentManifest}`, 'FileNotFound', 1) + itThrowsACliError(command, `File not found: ${inexistentManifest}`, 'FileNotFound', 1) }) }) }) diff --git a/packages/cli/tests/commands/codegen.spec.ts b/packages/cli/tests/commands/codegen.spec.ts index 73fb2d7f..7c71eda3 100644 --- a/packages/cli/tests/commands/codegen.spec.ts +++ b/packages/cli/tests/commands/codegen.spec.ts @@ -71,7 +71,7 @@ describe('codegen', () => { context('when the manifest does not exist', () => { const command = ['codegen', `--manifest ${manifestPath}fake`, `--output ${outputDir}`] - itThrowsACliError(command, `Could not find ${manifestPath}fake`, 'FileNotFound', 1) + itThrowsACliError(command, `File not found: ${manifestPath}fake`, 'FileNotFound', 1) }) context('when clean flag is passed', () => { diff --git a/packages/cli/tests/commands/compile.spec.ts b/packages/cli/tests/commands/compile.spec.ts index c9385284..57f08eea 100644 --- a/packages/cli/tests/commands/compile.spec.ts +++ b/packages/cli/tests/commands/compile.spec.ts @@ -107,7 +107,7 @@ describe('compile', () => { const taskPath = `${basePath}/tasks/invalid-task.ts` const command = buildCommand(manifestPath, taskPath, outputDir) - itThrowsACliError(command, 'AssemblyScript compilation failed', 'BuildError', 1) + itThrowsACliError(command, 'AssemblyScript compilation failed', 'CompilationError', 2) }) }) @@ -116,28 +116,28 @@ describe('compile', () => { const manifestPath = `${basePath}/manifests/invalid-manifest.yaml` const command = buildCommand(manifestPath, taskPath, outputDir) - itThrowsACliError(command, 'More than one entry', 'MoreThanOneEntryError', 1) + itThrowsACliError(command, 'More than one entry', 'ManifestValidationError', 1) }) context('when the manfiest has repeated fields', () => { const manifestPath = `${basePath}/manifests/invalid-manifest-repeated.yaml` const command = buildCommand(manifestPath, taskPath, outputDir) - itThrowsACliError(command, 'Duplicate Entry', 'DuplicateEntryError', 1) + itThrowsACliError(command, 'Duplicate Entry', 'ManifestValidationError', 1) }) context('when the manifest is incomplete', () => { const manifestPath = `${basePath}/manifests/incomplete-manifest.yaml` const command = buildCommand(manifestPath, taskPath, outputDir) - itThrowsACliError(command, 'Missing/Incorrect Fields', 'FieldsError', 3) + itThrowsACliError(command, 'Missing/Incorrect Fields', 'ManifestValidationError', 3) }) context('when the manifest is empty', () => { const manifestPath = `${basePath}/manifests/empty-manifest.yaml` const command = buildCommand(manifestPath, taskPath, outputDir) - itThrowsACliError(command, 'Empty Manifest', 'EmptyManifestError', 1) + itThrowsACliError(command, 'Empty Manifest', 'ManifestValidationError', 1) }) }) }) @@ -146,7 +146,7 @@ describe('compile', () => { const inexistentManifestPath = `${manifestPath}-none` const command = buildCommand(inexistentManifestPath, taskPath, outputDir) - itThrowsACliError(command, `Could not find ${inexistentManifestPath}`, 'FileNotFound', 1) + itThrowsACliError(command, `File not found: ${inexistentManifestPath}`, 'FileNotFound', 1) }) context('when the output directory already exists', () => { diff --git a/packages/cli/tests/commands/deploy.spec.ts b/packages/cli/tests/commands/deploy.spec.ts index 39e12b66..ab538c3a 100644 --- a/packages/cli/tests/commands/deploy.spec.ts +++ b/packages/cli/tests/commands/deploy.spec.ts @@ -199,7 +199,7 @@ describe('deploy', () => { axiosMock.onPost(/.*\/tasks/gm).reply(400, { content: { message } }) }) - itThrowsACliError(command, message, 'Bad Request', 1) + itThrowsACliError(command, message, 'Deploy400Error', 1) }) context('when the error message is not present', () => { @@ -207,7 +207,7 @@ describe('deploy', () => { axiosMock.onPost(/.*\/tasks/gm).reply(400, { content: { errors: ['some error'] } }) }) - itThrowsACliError(command, 'Failed to upload to registry', 'Bad Request', 1) + itThrowsACliError(command, 'Bad request', 'Deploy400Error', 1) }) }) @@ -216,7 +216,7 @@ describe('deploy', () => { axiosMock.onPost(/.*/).reply(401) }) - itThrowsACliError(command, 'Failed to upload to registry', 'Unauthorized', 1) + itThrowsACliError(command, 'Unauthorized', 'Deploy401Error', 1) }) context('when there is an authentication failure', () => { @@ -224,7 +224,7 @@ describe('deploy', () => { axiosMock.onPost(/.*/).reply(403) }) - itThrowsACliError(command, 'Failed to upload to registry', 'Invalid api key', 1) + itThrowsACliError(command, 'Invalid API key', 'Deploy403Error', 1) }) context('when there is a generic error', () => { @@ -232,19 +232,14 @@ describe('deploy', () => { axiosMock.onPost(/.*/).reply(501) }) - itThrowsACliError( - command, - 'Failed to upload to registry - Request failed with status code 501', - '501 Error', - 1 - ) + itThrowsACliError(command, 'Upload failed: Request failed with status code 501', 'Deploy501Error', 1) }) }) }) context('when the directory does not contain the necessary files', () => { context('when the directory contains no files', () => { - itThrowsACliError(command, `Could not find ${inputDir}/manifest.json`, 'File Not Found', 1) + itThrowsACliError(command, `File not found: ${inputDir}/manifest.json`, 'FileNotFound', 1) }) context('when the directory contains only one file', () => { @@ -252,13 +247,13 @@ describe('deploy', () => { createFile('manifest.json') }) - itThrowsACliError(command, `Could not find ${inputDir}/task.wasm`, 'File Not Found', 1) + itThrowsACliError(command, `File not found: ${inputDir}/task.wasm`, 'FileNotFound', 1) }) }) }) context('when input directory does not exist', () => { - itThrowsACliError(command, `Directory ${inputDir} does not exist`, 'Directory Not Found', 1) + itThrowsACliError(command, `Directory not found: ${inputDir}`, 'DirectoryNotFound', 1) }) }) From 5de9882b7648ef42c0134ad1af5b0257a7bdc92f Mon Sep 17 00:00:00 2001 From: ncomerci Date: Wed, 14 Jan 2026 12:07:33 -0300 Subject: [PATCH 16/27] feat: task handling with default task support --- packages/cli/src/commands/build.ts | 43 ++++++++-------------- packages/cli/src/commands/codegen.ts | 39 +++++++------------- packages/cli/src/commands/compile.ts | 26 ++++++------- packages/cli/src/commands/deploy.ts | 20 ++++------ packages/cli/src/commands/test.ts | 22 ++++------- packages/cli/src/constants.ts | 2 + packages/cli/src/core/test.ts | 2 +- packages/cli/src/helpers.ts | 30 ++++++++++----- packages/cli/src/lib/MimicConfigHandler.ts | 19 ++++++++++ 9 files changed, 98 insertions(+), 105 deletions(-) diff --git a/packages/cli/src/commands/build.ts b/packages/cli/src/commands/build.ts index 22f7461c..5b577e48 100644 --- a/packages/cli/src/commands/build.ts +++ b/packages/cli/src/commands/build.ts @@ -1,10 +1,9 @@ -import { confirm } from '@inquirer/prompts' import { Command, Flags } from '@oclif/core' import { build } from '../core' -import { filterTasks, handleCoreError, runTasks, taskFilterFlags, toTaskConfig } from '../helpers' +import { createConfirmClean, filterTasks, handleCoreError, runTasks, taskFilterFlags } from '../helpers' import MimicConfigHandler from '../lib/MimicConfigHandler' -import log, { coreLogger } from '../log' +import { coreLogger } from '../log' import { RequiredTaskConfig } from '../types' export default class Build extends Command { @@ -31,38 +30,26 @@ export default class Build extends Command { const { flags } = await this.parse(Build) const { manifest, task, output, types, clean, include, exclude } = flags - if (MimicConfigHandler.exists()) { - const mimicConfig = MimicConfigHandler.load(this) - const allTasks = MimicConfigHandler.getTasks(mimicConfig) - const tasks = filterTasks(this, allTasks, include, exclude) - await runTasks(this, tasks, (taskConfig) => this.runForTask(taskConfig, clean)) - } else { - await this.runForTask({ manifest, path: task, output, types }, clean) - } + const allTasks = MimicConfigHandler.loadOrDefault(this, { + manifest, + path: task, + output, + types, + }) + const tasks = filterTasks(this, allTasks, include, exclude) + await runTasks(this, tasks, (taskConfig) => this.runForTask(taskConfig, clean)) } private async runForTask(task: Omit, clean: boolean): Promise { - const taskConfig = toTaskConfig(task) - try { const result = await build( { - manifestPath: taskConfig.manifestPath, - taskPath: taskConfig.taskPath, - outputDir: taskConfig.outputDir, - typesDir: taskConfig.typesDir, + manifestPath: task.manifest, + taskPath: task.path, + outputDir: task.output, + typesDir: task.types, clean, - confirmClean: async () => { - const shouldDelete = await confirm({ - message: `Are you sure you want to ${log.warnText('delete')} all the contents in ${log.highlightText(taskConfig.typesDir)}. This action is ${log.warnText('irreversible')}`, - default: false, - }) - if (!shouldDelete) { - coreLogger.info('You can remove the --clean flag from your command') - coreLogger.info('Stopping initialization...') - } - return shouldDelete - }, + confirmClean: createConfirmClean(task.types, coreLogger), }, coreLogger ) diff --git a/packages/cli/src/commands/codegen.ts b/packages/cli/src/commands/codegen.ts index 81bb6da1..7538b9bd 100644 --- a/packages/cli/src/commands/codegen.ts +++ b/packages/cli/src/commands/codegen.ts @@ -1,10 +1,9 @@ -import { confirm } from '@inquirer/prompts' import { Command, Flags } from '@oclif/core' import { codegen } from '../core' -import { filterTasks, handleCoreError, runTasks, taskFilterFlags, toTaskConfig } from '../helpers' +import { createConfirmClean, filterTasks, handleCoreError, runTasks, taskFilterFlags } from '../helpers' import MimicConfigHandler from '../lib/MimicConfigHandler' -import log, { coreLogger } from '../log' +import { coreLogger } from '../log' import { RequiredTaskConfig } from '../types' export default class Codegen extends Command { @@ -27,36 +26,24 @@ export default class Codegen extends Command { const { flags } = await this.parse(Codegen) const { manifest, output, clean, include, exclude } = flags - if (MimicConfigHandler.exists()) { - const mimicConfig = MimicConfigHandler.load(this) - const allTasks = MimicConfigHandler.getTasks(mimicConfig) - const tasks = filterTasks(this, allTasks, include, exclude) - await runTasks(this, tasks, (task) => this.runForTask(task, clean)) - } else { - await this.runForTask({ manifest, types: output, path: '', output: '' }, clean) - } + const allTasks = MimicConfigHandler.loadOrDefault(this, { + manifest, + types: output, + path: '', + output: '', + }) + const tasks = filterTasks(this, allTasks, include, exclude) + await runTasks(this, tasks, (task) => this.runForTask(task, clean)) } private async runForTask(task: Omit, clean: boolean): Promise { - const taskConfig = toTaskConfig(task) - try { const result = await codegen( { - manifestPath: taskConfig.manifestPath, - outputDir: taskConfig.typesDir, + manifestPath: task.manifest, + outputDir: task.types, clean, - confirmClean: async () => { - const shouldDelete = await confirm({ - message: `Are you sure you want to ${log.warnText('delete')} all the contents in ${log.highlightText(taskConfig.typesDir)}. This action is ${log.warnText('irreversible')}`, - default: false, - }) - if (!shouldDelete) { - console.log('You can remove the --clean flag from your command') - console.log('Stopping initialization...') - } - return shouldDelete - }, + confirmClean: createConfirmClean(task.types, coreLogger), }, coreLogger ) diff --git a/packages/cli/src/commands/compile.ts b/packages/cli/src/commands/compile.ts index 2becdc5b..283b6379 100644 --- a/packages/cli/src/commands/compile.ts +++ b/packages/cli/src/commands/compile.ts @@ -1,7 +1,7 @@ import { Command, Flags } from '@oclif/core' import { compile } from '../core' -import { filterTasks, handleCoreError, runTasks, taskFilterFlags, toTaskConfig } from '../helpers' +import { filterTasks, handleCoreError, runTasks, taskFilterFlags } from '../helpers' import MimicConfigHandler from '../lib/MimicConfigHandler' import { coreLogger } from '../log' import { RequiredTaskConfig } from '../types' @@ -22,25 +22,23 @@ export default class Compile extends Command { const { flags } = await this.parse(Compile) const { task: taskPath, output, manifest, include, exclude } = flags - if (MimicConfigHandler.exists()) { - const mimicConfig = MimicConfigHandler.load(this) - const allTasks = MimicConfigHandler.getTasks(mimicConfig) - const tasks = filterTasks(this, allTasks, include, exclude) - await runTasks(this, tasks, (task) => this.runForTask(task)) - } else { - await this.runForTask({ manifest, path: taskPath, output, types: '' }) - } + const allTasks = MimicConfigHandler.loadOrDefault(this, { + manifest, + path: taskPath, + output, + types: '', + }) + const tasks = filterTasks(this, allTasks, include, exclude) + await runTasks(this, tasks, (task) => this.runForTask(task)) } private async runForTask(task: Omit): Promise { - const taskConfig = toTaskConfig(task) - try { await compile( { - manifestPath: taskConfig.manifestPath, - taskPath: taskConfig.taskPath, - outputDir: taskConfig.outputDir, + manifestPath: task.manifest, + taskPath: task.path, + outputDir: task.output, }, coreLogger ) diff --git a/packages/cli/src/commands/deploy.ts b/packages/cli/src/commands/deploy.ts index d64c09a5..57036b23 100644 --- a/packages/cli/src/commands/deploy.ts +++ b/packages/cli/src/commands/deploy.ts @@ -3,7 +3,7 @@ import { resolve } from 'path' import { DEFAULT_TASK } from '../constants' import { build, deploy, MIMIC_REGISTRY_DEFAULT } from '../core' -import { filterTasks, handleCoreError, runTasks, taskFilterFlags, toTaskConfig } from '../helpers' +import { filterTasks, handleCoreError, runTasks, taskFilterFlags } from '../helpers' import MimicConfigHandler from '../lib/MimicConfigHandler' import log, { coreLogger } from '../log' import { RequiredTaskConfig } from '../types' @@ -32,14 +32,9 @@ export default class Deploy extends Authenticate { const { flags } = await this.parse(Deploy) const { profile, 'api-key': apiKey, input, output, 'skip-compile': skipCompile, url, include, exclude } = flags - if (MimicConfigHandler.exists()) { - const mimicConfig = MimicConfigHandler.load(this) - const allTasks = MimicConfigHandler.getTasks(mimicConfig) - const tasks = filterTasks(this, allTasks, include, exclude) - await runTasks(this, tasks, (task) => this.runForTask(task, url, skipCompile, task.output, profile, apiKey)) - } else { - await this.runForTask({ ...DEFAULT_TASK, output }, url, skipCompile, input, profile, apiKey) - } + const allTasks = MimicConfigHandler.loadOrDefault(this, { ...DEFAULT_TASK, output }) + const tasks = filterTasks(this, allTasks, include, exclude) + await runTasks(this, tasks, (task) => this.runForTask(task, url, skipCompile, input, profile, apiKey)) } private async runForTask( @@ -52,7 +47,6 @@ export default class Deploy extends Authenticate { ): Promise { const inputPath = resolve(inputDir) const outputPath = resolve(task.output) - const taskConfig = toTaskConfig(task) const credentials = this.authenticate({ profile, 'api-key': apiKey }) @@ -60,10 +54,10 @@ export default class Deploy extends Authenticate { if (!skipCompile) { await build( { - manifestPath: taskConfig.manifestPath, - taskPath: taskConfig.taskPath, + manifestPath: task.manifest, + taskPath: task.path, outputDir: inputPath, - typesDir: taskConfig.typesDir, + typesDir: task.types, clean: false, }, coreLogger diff --git a/packages/cli/src/commands/test.ts b/packages/cli/src/commands/test.ts index 16889e64..9ffe0a7c 100644 --- a/packages/cli/src/commands/test.ts +++ b/packages/cli/src/commands/test.ts @@ -3,7 +3,7 @@ import * as path from 'path' import { DEFAULT_TASK } from '../constants' import { buildForTest, getTestPath, runTests, TestError } from '../core' -import { filterTasks, handleCoreError, runTasks, taskFilterFlags, toTaskConfig } from '../helpers' +import { filterTasks, handleCoreError, runTasks, taskFilterFlags } from '../helpers' import MimicConfigHandler from '../lib/MimicConfigHandler' import { coreLogger } from '../log' import { RequiredTaskConfig } from '../types' @@ -45,20 +45,15 @@ export default class Test extends Command { testPaths.add(getTestPath(baseDir)) } - if (testPaths.size > 0) { - runTests({ testPaths: Array.from(testPaths), baseDir }, coreLogger) - } + if (testPaths.size > 0) runTests({ testPaths: Array.from(testPaths), baseDir }, coreLogger) } catch (error) { - if (error instanceof TestError) { - this.exit(error.exitCode) - } + if (error instanceof TestError) this.exit(error.exitCode) + handleCoreError(this, error) } } private async compileTask(task: Omit, baseDir: string): Promise { - const taskConfig = toTaskConfig(task) - // Change to baseDir for compilation const originalCwd = process.cwd() try { @@ -66,11 +61,10 @@ export default class Test extends Command { await buildForTest( { - manifestPath: taskConfig.manifestPath, - taskPath: taskConfig.taskPath, - outputDir: taskConfig.outputDir, - typesDir: taskConfig.typesDir, - clean: false, + manifestPath: task.manifest, + taskPath: task.path, + outputDir: task.output, + typesDir: task.types, cwd: baseDir, }, coreLogger diff --git a/packages/cli/src/constants.ts b/packages/cli/src/constants.ts index 334c1125..39200384 100644 --- a/packages/cli/src/constants.ts +++ b/packages/cli/src/constants.ts @@ -1,5 +1,7 @@ import { RequiredTaskConfig } from './types' +export const DEFAULT_TASK_NAME = 'default' + export const DEFAULT_TASK: Omit = { manifest: 'manifest.yaml', path: 'src/task.ts', diff --git a/packages/cli/src/core/test.ts b/packages/cli/src/core/test.ts index 8d221653..04efe3ac 100644 --- a/packages/cli/src/core/test.ts +++ b/packages/cli/src/core/test.ts @@ -12,7 +12,7 @@ export function getTestPath(baseDir: string): string { } export async function buildForTest( - options: Omit, + options: Omit, logger: Logger = defaultLogger ): Promise { await build( diff --git a/packages/cli/src/helpers.ts b/packages/cli/src/helpers.ts index 98fbf6ef..6ff6ea87 100644 --- a/packages/cli/src/helpers.ts +++ b/packages/cli/src/helpers.ts @@ -1,10 +1,12 @@ +import { confirm } from '@inquirer/prompts' import { Command, Flags } from '@oclif/core' import { Interface } from 'ethers' import camelCase from 'lodash/camelCase' import startCase from 'lodash/startCase' -import { TaskConfig } from './core/types' +import { Logger } from './core/types' import { MIMIC_CONFIG_FILE } from './lib/MimicConfigHandler' +import { DEFAULT_TASK_NAME } from './constants' import { CoreError } from './core' import { CommandError } from './errors' import log from './log' @@ -90,8 +92,12 @@ export async function runTasks( ): Promise { const errors: Array<{ task: string; error: Error; code?: string; suggestions?: string[] }> = [] + const shouldLogHeader = tasks.length > 1 || tasks[0].name !== DEFAULT_TASK_NAME + for (const task of tasks) { - console.log(`\n${log.highlightText(`[${task.name}]`)}`) + if (shouldLogHeader) { + console.log(`\n${log.highlightText(`[${task.name}]`)}`) + } try { await runTask(task) } catch (error) { @@ -130,12 +136,18 @@ export function handleCoreError(command: Command, error: unknown): void { throw error } -export function toTaskConfig(task: RequiredTaskConfig | Omit): TaskConfig { - return { - name: 'name' in task ? task.name : 'default', - manifestPath: task.manifest, - taskPath: task.path, - outputDir: task.output, - typesDir: task.types, +export function createConfirmClean(directory: string, logger: Logger): () => Promise { + return async () => { + const shouldDelete = await confirm({ + message: `Are you sure you want to ${log.warnText('delete')} all the contents in ${log.highlightText( + directory + )}. This action is ${log.warnText('irreversible')}`, + default: false, + }) + if (!shouldDelete) { + logger.info('You can remove the --clean flag from your command') + logger.info('Stopping initialization...') + } + return shouldDelete } } diff --git a/packages/cli/src/lib/MimicConfigHandler.ts b/packages/cli/src/lib/MimicConfigHandler.ts index 2a5cb42f..2b48e06c 100644 --- a/packages/cli/src/lib/MimicConfigHandler.ts +++ b/packages/cli/src/lib/MimicConfigHandler.ts @@ -4,6 +4,7 @@ import { load } from 'js-yaml' import * as path from 'path' import { ZodError } from 'zod' +import { DEFAULT_TASK_NAME } from '../constants' import { GENERIC_SUGGESTION } from '../errors' import { MimicConfig, RequiredTaskConfig } from '../types' import { MimicConfigValidator } from '../validators' @@ -50,6 +51,24 @@ export default { types: task.types ?? path.join(path.dirname(task.path), 'types'), })) }, + + loadOrDefault( + command: Command, + defaultTask: Omit, + baseDir: string = process.cwd() + ): RequiredTaskConfig[] { + if (this.exists(baseDir)) { + const mimicConfig = this.load(command, baseDir) + return this.getTasks(mimicConfig) + } + + return [ + { + ...defaultTask, + name: DEFAULT_TASK_NAME, + }, + ] + }, } function handleValidationError(command: Command, err: unknown): never { From 4b0baf383b2862bfe45efd10623bf7339ce652e5 Mon Sep 17 00:00:00 2001 From: ncomerci Date: Wed, 14 Jan 2026 12:23:48 -0300 Subject: [PATCH 17/27] refactor: simplify return types --- packages/cli/src/core/build.ts | 18 +++------- packages/cli/src/core/codegen.ts | 18 ++++------ packages/cli/src/core/compile.ts | 10 ++---- packages/cli/src/core/deploy.ts | 6 +--- packages/cli/src/core/test.ts | 9 ++--- packages/cli/src/core/types.ts | 56 +++----------------------------- packages/cli/src/log.ts | 6 ---- 7 files changed, 22 insertions(+), 101 deletions(-) diff --git a/packages/cli/src/core/build.ts b/packages/cli/src/core/build.ts index f887fc22..1d6dc038 100644 --- a/packages/cli/src/core/build.ts +++ b/packages/cli/src/core/build.ts @@ -2,9 +2,9 @@ import { defaultLogger } from '../log' import { codegen } from './codegen' import { compile } from './compile' -import { BuildOptions, BuildResult, Logger } from './types' +import { BuildOptions, CommandResult, Logger } from './types' -export async function build(options: BuildOptions, logger: Logger = defaultLogger): Promise { +export async function build(options: BuildOptions, logger: Logger = defaultLogger): Promise { const { manifestPath, taskPath, outputDir, typesDir, clean, confirmClean, cwd } = options const codegenResult = await codegen( @@ -17,13 +17,7 @@ export async function build(options: BuildOptions, logger: Logger = defaultLogge logger ) - if (clean && !codegenResult.success) { - return { - codegen: codegenResult, - compile: { wasmPath: '', manifestJsonPath: '', success: false }, - success: false, - } - } + if (clean && !codegenResult.success) return { success: false } const compileResult = await compile( { @@ -35,9 +29,5 @@ export async function build(options: BuildOptions, logger: Logger = defaultLogge logger ) - return { - codegen: codegenResult, - compile: compileResult, - success: codegenResult.success && compileResult.success, - } + return { success: codegenResult.success && compileResult.success } } diff --git a/packages/cli/src/core/codegen.ts b/packages/cli/src/core/codegen.ts index 4a628c42..d5c27deb 100644 --- a/packages/cli/src/core/codegen.ts +++ b/packages/cli/src/core/codegen.ts @@ -10,7 +10,7 @@ import { Manifest } from '../types' import { ManifestValidator } from '../validators' import { CodegenError, FileNotFoundError, ManifestValidationError } from './errors' -import { CodegenOptions, CodegenResult, Logger } from './types' +import { CodegenOptions, CommandResult, Logger } from './types' export function loadManifest(manifestPath: string): Manifest { if (!fs.existsSync(manifestPath)) { @@ -131,7 +131,7 @@ function generateInputsCode(manifest: Manifest, outputDir: string): string[] { return generatedFiles } -export async function codegen(options: CodegenOptions, logger: Logger = defaultLogger): Promise { +export async function codegen(options: CodegenOptions, logger: Logger = defaultLogger): Promise { const { manifestPath, outputDir, clean, confirmClean } = options const manifest = loadManifest(manifestPath) @@ -139,7 +139,7 @@ export async function codegen(options: CodegenOptions, logger: Logger = defaultL if (clean) { if (confirmClean) { const shouldDelete = await confirmClean() - if (!shouldDelete) return { generatedFiles: [], success: false } + if (!shouldDelete) return { success: false } } logger.startAction(`Deleting contents of ${outputDir}`) @@ -150,19 +150,15 @@ export async function codegen(options: CodegenOptions, logger: Logger = defaultL if (Object.keys(manifest.inputs).length === 0 && Object.keys(manifest.abis).length === 0) { logger.stopAction() - return { generatedFiles: [], success: true } + return { success: true } } if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true }) - const generatedFiles: string[] = [] - generatedFiles.push(...generateAbisCode(manifest, outputDir, manifestPath)) - generatedFiles.push(...generateInputsCode(manifest, outputDir)) + generateAbisCode(manifest, outputDir, manifestPath) + generateInputsCode(manifest, outputDir) logger.stopAction() - return { - generatedFiles, - success: true, - } + return { success: true } } diff --git a/packages/cli/src/core/compile.ts b/packages/cli/src/core/compile.ts index 3b302b4a..81a48233 100644 --- a/packages/cli/src/core/compile.ts +++ b/packages/cli/src/core/compile.ts @@ -6,9 +6,9 @@ import { defaultLogger } from '../log' import { loadManifest } from './codegen' import { CompilationError, FileNotFoundError } from './errors' -import { CompileOptions, CompileResult, Logger } from './types' +import { CommandResult, CompileOptions, Logger } from './types' -export async function compile(options: CompileOptions, logger: Logger = defaultLogger): Promise { +export async function compile(options: CompileOptions, logger: Logger = defaultLogger): Promise { const { manifestPath, taskPath, outputDir, cwd = process.cwd() } = options const resolvedTaskPath = path.resolve(taskPath) @@ -55,9 +55,5 @@ export async function compile(options: CompileOptions, logger: Logger = defaultL logger.stopAction() - return { - wasmPath, - manifestJsonPath, - success: true, - } + return { success: true } } diff --git a/packages/cli/src/core/deploy.ts b/packages/cli/src/core/deploy.ts index 67ec3257..d730052b 100644 --- a/packages/cli/src/core/deploy.ts +++ b/packages/cli/src/core/deploy.ts @@ -95,9 +95,5 @@ export async function deploy(options: DeployOptions, logger: Logger = defaultLog const cidJsonPath = join(outputDir, 'CID.json') fs.writeFileSync(cidJsonPath, JSON.stringify({ CID: cid }, null, 2)) - return { - cid, - cidJsonPath, - success: true, - } + return { cid } } diff --git a/packages/cli/src/core/test.ts b/packages/cli/src/core/test.ts index 04efe3ac..c1233e80 100644 --- a/packages/cli/src/core/test.ts +++ b/packages/cli/src/core/test.ts @@ -5,7 +5,7 @@ import { defaultLogger } from '../log' import { build } from './build' import { TestError } from './errors' -import { BuildOptions, Logger, RunTestsOptions, RunTestsResult } from './types' +import { BuildOptions, Logger, RunTestsOptions } from './types' export function getTestPath(baseDir: string): string { return path.join(baseDir, 'tests', '**', '*.spec.ts') @@ -24,7 +24,7 @@ export async function buildForTest( ) } -export function runTests(options: RunTestsOptions, logger: Logger = defaultLogger): RunTestsResult { +export function runTests(options: RunTestsOptions, logger: Logger = defaultLogger): void { const { testPaths, baseDir } = options logger.startAction('Running tests') @@ -37,9 +37,4 @@ export function runTests(options: RunTestsOptions, logger: Logger = defaultLogge logger.stopAction() if (!success) throw new TestError('Tests failed', exitCode, ['Check the test output for details']) - - return { - exitCode, - success, - } } diff --git a/packages/cli/src/core/types.ts b/packages/cli/src/core/types.ts index 1bdafc0b..55f0fc51 100644 --- a/packages/cli/src/core/types.ts +++ b/packages/cli/src/core/types.ts @@ -1,3 +1,8 @@ +export interface CommandResult { + /** Whether the operation succeeded */ + success: boolean +} + // ============================================================================ // Codegen Types // ============================================================================ @@ -13,13 +18,6 @@ export interface CodegenOptions { confirmClean?: () => Promise } -export interface CodegenResult { - /** List of generated files */ - generatedFiles: string[] - /** Whether any files were generated */ - success: boolean -} - // ============================================================================ // Compile Types // ============================================================================ @@ -35,15 +33,6 @@ export interface CompileOptions { cwd?: string } -export interface CompileResult { - /** Path to the generated WASM file */ - wasmPath: string - /** Path to the generated manifest.json */ - manifestJsonPath: string - /** Whether compilation succeeded */ - success: boolean -} - // ============================================================================ // Build Types // ============================================================================ @@ -65,15 +54,6 @@ export interface BuildOptions { cwd?: string } -export interface BuildResult { - /** Result from codegen step */ - codegen: CodegenResult - /** Result from compile step */ - compile: CompileResult - /** Whether the entire build succeeded */ - success: boolean -} - // ============================================================================ // Deploy Types // ============================================================================ @@ -92,10 +72,6 @@ export interface DeployOptions { export interface DeployResult { /** IPFS CID of the deployed task */ cid: string - /** Path to the generated CID.json file */ - cidJsonPath: string - /** Whether deployment succeeded */ - success: boolean } // ============================================================================ @@ -109,25 +85,6 @@ export interface RunTestsOptions { baseDir: string } -export interface RunTestsResult { - /** Exit code from test runner */ - exitCode: number - /** Whether tests passed */ - success: boolean -} - -// ============================================================================ -// Task Configuration -// ============================================================================ - -export interface TaskConfig { - name: string - manifestPath: string - taskPath: string - outputDir: string - typesDir: string -} - // ============================================================================ // Logging Interface // ============================================================================ @@ -136,7 +93,4 @@ export interface Logger { startAction(message: string): void stopAction(): void info(message: string): void - warn(message: string): void - error(message: string): void - success(message: string): void } diff --git a/packages/cli/src/log.ts b/packages/cli/src/log.ts index e2d2ec76..5665be10 100644 --- a/packages/cli/src/log.ts +++ b/packages/cli/src/log.ts @@ -20,18 +20,12 @@ export const coreLogger: Logger = { startAction: log.startAction, stopAction: log.stopAction, info: console.log, - warn: console.warn, - error: console.error, - success: console.log, } export const defaultLogger: Logger = { startAction: () => {}, stopAction: () => {}, info: () => {}, - warn: () => {}, - error: () => {}, - success: () => {}, } export default log From 7a0f7c1a78972fe17a5c3ba11090416251d1282b Mon Sep 17 00:00:00 2001 From: ncomerci Date: Wed, 14 Jan 2026 12:46:16 -0300 Subject: [PATCH 18/27] fix: tests --- packages/cli/src/commands/deploy.ts | 6 +++--- packages/cli/src/helpers.ts | 5 ++++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/cli/src/commands/deploy.ts b/packages/cli/src/commands/deploy.ts index 57036b23..b1502a6b 100644 --- a/packages/cli/src/commands/deploy.ts +++ b/packages/cli/src/commands/deploy.ts @@ -21,7 +21,7 @@ export default class Deploy extends Authenticate { static override flags = { ...Authenticate.flags, - input: Flags.string({ char: 'i', description: 'Directory containing the compiled artifacts', default: './build' }), + input: Flags.string({ char: 'i', description: 'Directory containing the compiled artifacts' }), 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 }), 'skip-compile': Flags.boolean({ description: 'Skip codegen and compile steps before uploading', default: false }), @@ -41,11 +41,11 @@ export default class Deploy extends Authenticate { task: Omit, registryUrl: string, skipCompile: boolean, - inputDir: string, + inputDir?: string, profile?: string, apiKey?: string ): Promise { - const inputPath = resolve(inputDir) + const inputPath = resolve(inputDir ?? task.output) const outputPath = resolve(task.output) const credentials = this.authenticate({ profile, 'api-key': apiKey }) diff --git a/packages/cli/src/helpers.ts b/packages/cli/src/helpers.ts index 6ff6ea87..19a2eeb1 100644 --- a/packages/cli/src/helpers.ts +++ b/packages/cli/src/helpers.ts @@ -93,6 +93,7 @@ export async function runTasks( const errors: Array<{ task: string; error: Error; code?: string; suggestions?: string[] }> = [] const shouldLogHeader = tasks.length > 1 || tasks[0].name !== DEFAULT_TASK_NAME + const isSingleTask = tasks.length === 1 for (const task of tasks) { if (shouldLogHeader) { @@ -101,6 +102,8 @@ export async function runTasks( try { await runTask(task) } catch (error) { + if (isSingleTask) throw error + const err = error as Error console.error(log.warnText(`Task "${task.name}" failed: ${err.message}`)) @@ -128,7 +131,7 @@ export async function runTasks( export function handleCoreError(command: Command, error: unknown): void { if (error instanceof CoreError) { - command.error(error.message, { + throw new CommandError(error.message, { code: error.code, suggestions: error.suggestions, }) From bf9504e0babc34eb6525a41fb332a330391be1d8 Mon Sep 17 00:00:00 2001 From: ncomerci Date: Wed, 14 Jan 2026 12:54:11 -0300 Subject: [PATCH 19/27] refactor: update error handling in CLI commands --- packages/cli/src/commands/build.ts | 2 +- packages/cli/src/commands/codegen.ts | 2 +- packages/cli/src/commands/compile.ts | 2 +- packages/cli/src/commands/deploy.ts | 2 +- packages/cli/src/commands/test.ts | 2 +- packages/cli/src/helpers.ts | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/cli/src/commands/build.ts b/packages/cli/src/commands/build.ts index 5b577e48..10406abb 100644 --- a/packages/cli/src/commands/build.ts +++ b/packages/cli/src/commands/build.ts @@ -58,7 +58,7 @@ export default class Build extends Command { coreLogger.info(`Build complete! Artifacts in ${task.output}/`) } catch (error) { - handleCoreError(this, error) + handleCoreError(error) } } } diff --git a/packages/cli/src/commands/codegen.ts b/packages/cli/src/commands/codegen.ts index 7538b9bd..07af2a49 100644 --- a/packages/cli/src/commands/codegen.ts +++ b/packages/cli/src/commands/codegen.ts @@ -50,7 +50,7 @@ export default class Codegen extends Command { if (clean && !result.success) this.exit(0) } catch (error) { - handleCoreError(this, error) + handleCoreError(error) } } } diff --git a/packages/cli/src/commands/compile.ts b/packages/cli/src/commands/compile.ts index 283b6379..129f58c8 100644 --- a/packages/cli/src/commands/compile.ts +++ b/packages/cli/src/commands/compile.ts @@ -45,7 +45,7 @@ export default class Compile extends Command { coreLogger.info(`Build complete! Artifacts in ${task.output}/`) } catch (error) { - handleCoreError(this, error) + handleCoreError(error) } } } diff --git a/packages/cli/src/commands/deploy.ts b/packages/cli/src/commands/deploy.ts index b1502a6b..e5473cb4 100644 --- a/packages/cli/src/commands/deploy.ts +++ b/packages/cli/src/commands/deploy.ts @@ -78,7 +78,7 @@ export default class Deploy extends Authenticate { coreLogger.info(`CID saved at ${log.highlightText(outputPath)}`) coreLogger.info(`Task deployed!`) } catch (error) { - handleCoreError(this, error) + handleCoreError(error) } } } diff --git a/packages/cli/src/commands/test.ts b/packages/cli/src/commands/test.ts index 9ffe0a7c..de500e7a 100644 --- a/packages/cli/src/commands/test.ts +++ b/packages/cli/src/commands/test.ts @@ -49,7 +49,7 @@ export default class Test extends Command { } catch (error) { if (error instanceof TestError) this.exit(error.exitCode) - handleCoreError(this, error) + handleCoreError(error) } } diff --git a/packages/cli/src/helpers.ts b/packages/cli/src/helpers.ts index 19a2eeb1..9f466004 100644 --- a/packages/cli/src/helpers.ts +++ b/packages/cli/src/helpers.ts @@ -129,7 +129,7 @@ export async function runTasks( } } -export function handleCoreError(command: Command, error: unknown): void { +export function handleCoreError(error: unknown): void { if (error instanceof CoreError) { throw new CommandError(error.message, { code: error.code, From 34499ca8b0339a78f60573c12361008cd8683f6f Mon Sep 17 00:00:00 2001 From: ncomerci Date: Wed, 14 Jan 2026 14:38:55 -0300 Subject: [PATCH 20/27] refactor: test command --- packages/cli/src/commands/test.ts | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/packages/cli/src/commands/test.ts b/packages/cli/src/commands/test.ts index de500e7a..07747ace 100644 --- a/packages/cli/src/commands/test.ts +++ b/packages/cli/src/commands/test.ts @@ -27,21 +27,15 @@ export default class Test extends Command { const testPaths = new Set() try { - if (MimicConfigHandler.exists(baseDir)) { - const mimicConfig = MimicConfigHandler.load(this, baseDir) - const allTasks = MimicConfigHandler.getTasks(mimicConfig) - const tasks = filterTasks(this, allTasks, include, exclude) + const allTasks = MimicConfigHandler.loadOrDefault(this, DEFAULT_TASK, baseDir) + const tasks = filterTasks(this, allTasks, include, exclude) - if (!skipCompile) { - await runTasks(this, tasks, async (task) => { - await this.compileTask(task, baseDir) - testPaths.add(getTestPath(baseDir)) - }) - } else { + if (!skipCompile) { + await runTasks(this, tasks, async (task) => { + await this.compileTask(task, baseDir) testPaths.add(getTestPath(baseDir)) - } + }) } else { - if (!skipCompile) await this.compileTask(DEFAULT_TASK, baseDir) testPaths.add(getTestPath(baseDir)) } From 20694919bbaa19f1d9b8072f82258162d7c046b4 Mon Sep 17 00:00:00 2001 From: ncomerci Date: Wed, 14 Jan 2026 14:41:47 -0300 Subject: [PATCH 21/27] refactor: rename 'path' to 'task' in task configuration and related commands --- packages/cli/src/commands/build.ts | 4 ++-- packages/cli/src/commands/codegen.ts | 2 +- packages/cli/src/commands/compile.ts | 4 ++-- packages/cli/src/commands/deploy.ts | 2 +- packages/cli/src/commands/test.ts | 2 +- packages/cli/src/constants.ts | 2 +- packages/cli/src/lib/MimicConfigHandler.ts | 2 +- packages/cli/src/validators.ts | 2 +- 8 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/cli/src/commands/build.ts b/packages/cli/src/commands/build.ts index 10406abb..d2b79895 100644 --- a/packages/cli/src/commands/build.ts +++ b/packages/cli/src/commands/build.ts @@ -32,7 +32,7 @@ export default class Build extends Command { const allTasks = MimicConfigHandler.loadOrDefault(this, { manifest, - path: task, + task: task, output, types, }) @@ -45,7 +45,7 @@ export default class Build extends Command { const result = await build( { manifestPath: task.manifest, - taskPath: task.path, + taskPath: task.task, outputDir: task.output, typesDir: task.types, clean, diff --git a/packages/cli/src/commands/codegen.ts b/packages/cli/src/commands/codegen.ts index 07af2a49..883f9d12 100644 --- a/packages/cli/src/commands/codegen.ts +++ b/packages/cli/src/commands/codegen.ts @@ -29,7 +29,7 @@ export default class Codegen extends Command { const allTasks = MimicConfigHandler.loadOrDefault(this, { manifest, types: output, - path: '', + task: '', output: '', }) const tasks = filterTasks(this, allTasks, include, exclude) diff --git a/packages/cli/src/commands/compile.ts b/packages/cli/src/commands/compile.ts index 129f58c8..f9558425 100644 --- a/packages/cli/src/commands/compile.ts +++ b/packages/cli/src/commands/compile.ts @@ -24,7 +24,7 @@ export default class Compile extends Command { const allTasks = MimicConfigHandler.loadOrDefault(this, { manifest, - path: taskPath, + task: taskPath, output, types: '', }) @@ -37,7 +37,7 @@ export default class Compile extends Command { await compile( { manifestPath: task.manifest, - taskPath: task.path, + taskPath: task.task, outputDir: task.output, }, coreLogger diff --git a/packages/cli/src/commands/deploy.ts b/packages/cli/src/commands/deploy.ts index e5473cb4..528d8583 100644 --- a/packages/cli/src/commands/deploy.ts +++ b/packages/cli/src/commands/deploy.ts @@ -55,7 +55,7 @@ export default class Deploy extends Authenticate { await build( { manifestPath: task.manifest, - taskPath: task.path, + taskPath: task.task, outputDir: inputPath, typesDir: task.types, clean: false, diff --git a/packages/cli/src/commands/test.ts b/packages/cli/src/commands/test.ts index 07747ace..2fd4ab31 100644 --- a/packages/cli/src/commands/test.ts +++ b/packages/cli/src/commands/test.ts @@ -56,7 +56,7 @@ export default class Test extends Command { await buildForTest( { manifestPath: task.manifest, - taskPath: task.path, + taskPath: task.task, outputDir: task.output, typesDir: task.types, cwd: baseDir, diff --git a/packages/cli/src/constants.ts b/packages/cli/src/constants.ts index 39200384..a7166c78 100644 --- a/packages/cli/src/constants.ts +++ b/packages/cli/src/constants.ts @@ -4,7 +4,7 @@ export const DEFAULT_TASK_NAME = 'default' export const DEFAULT_TASK: Omit = { manifest: 'manifest.yaml', - path: 'src/task.ts', + task: 'src/task.ts', types: './src/types', output: './build', } diff --git a/packages/cli/src/lib/MimicConfigHandler.ts b/packages/cli/src/lib/MimicConfigHandler.ts index 2b48e06c..1acb5dab 100644 --- a/packages/cli/src/lib/MimicConfigHandler.ts +++ b/packages/cli/src/lib/MimicConfigHandler.ts @@ -48,7 +48,7 @@ export default { return mimicConfig.tasks.map((task) => ({ ...task, output: task.output ?? `build/${task.name}`, - types: task.types ?? path.join(path.dirname(task.path), 'types'), + types: task.types ?? path.join(path.dirname(task.task), 'types'), })) }, diff --git a/packages/cli/src/validators.ts b/packages/cli/src/validators.ts index 82f33bee..919514c1 100644 --- a/packages/cli/src/validators.ts +++ b/packages/cli/src/validators.ts @@ -29,7 +29,7 @@ export const ManifestValidator = z.object({ export const TaskConfigValidator = z.object({ name: String, manifest: String, - path: String, + task: String, output: String.optional(), types: String.optional(), }) From 49ffa4e698eaf703225ac3dcc352863bd18d279e Mon Sep 17 00:00:00 2001 From: ncomerci Date: Wed, 14 Jan 2026 14:50:07 -0300 Subject: [PATCH 22/27] refactor: change interfaces to types --- packages/cli/src/core/types.ts | 35 ++++++++++++---------------------- 1 file changed, 12 insertions(+), 23 deletions(-) diff --git a/packages/cli/src/core/types.ts b/packages/cli/src/core/types.ts index 55f0fc51..9531eef5 100644 --- a/packages/cli/src/core/types.ts +++ b/packages/cli/src/core/types.ts @@ -1,4 +1,4 @@ -export interface CommandResult { +export type CommandResult = { /** Whether the operation succeeded */ success: boolean } @@ -7,7 +7,7 @@ export interface CommandResult { // Codegen Types // ============================================================================ -export interface CodegenOptions { +export type CodegenOptions = { /** Path to the manifest.yaml file */ manifestPath: string /** Output directory for generated types */ @@ -22,7 +22,7 @@ export interface CodegenOptions { // Compile Types // ============================================================================ -export interface CompileOptions { +export type CompileOptions = { /** Path to the manifest.yaml file */ manifestPath: string /** Path to the task TypeScript file */ @@ -37,28 +37,17 @@ export interface CompileOptions { // Build Types // ============================================================================ -export interface BuildOptions { - /** Path to the manifest.yaml file */ - manifestPath: string - /** Path to the task TypeScript file */ - taskPath: string - /** Output directory for build artifacts */ - outputDir: string - /** Output directory for generated types */ - typesDir: string - /** Whether to delete existing types before generating */ - clean: boolean - /** Callback for confirming clean operation */ - confirmClean?: () => Promise - /** Working directory for build */ - cwd?: string -} +export type BuildOptions = Omit & + CompileOptions & { + /** Output directory for generated types (from codegen) */ + typesDir: string + } // ============================================================================ // Deploy Types // ============================================================================ -export interface DeployOptions { +export type DeployOptions = { /** Directory containing compiled artifacts (task.wasm, manifest.json) */ inputDir: string /** Output directory for CID.json */ @@ -69,7 +58,7 @@ export interface DeployOptions { registryUrl: string } -export interface DeployResult { +export type DeployResult = { /** IPFS CID of the deployed task */ cid: string } @@ -78,7 +67,7 @@ export interface DeployResult { // Test Types // ============================================================================ -export interface RunTestsOptions { +export type RunTestsOptions = { /** Glob patterns for test files */ testPaths: string[] /** Base directory for running tests */ @@ -89,7 +78,7 @@ export interface RunTestsOptions { // Logging Interface // ============================================================================ -export interface Logger { +export type Logger = { startAction(message: string): void stopAction(): void info(message: string): void From 1076c7cfc97483f081ecc4da9abdd14027a8eeaa Mon Sep 17 00:00:00 2001 From: ncomerci Date: Wed, 14 Jan 2026 14:59:02 -0300 Subject: [PATCH 23/27] fix: tests --- packages/cli/tests/MimicConfigHandler.spec.ts | 10 +++++----- packages/cli/tests/commands/build.spec.ts | 2 +- packages/cli/tests/commands/codegen.spec.ts | 2 +- packages/cli/tests/commands/compile.spec.ts | 2 +- packages/cli/tests/commands/deploy.spec.ts | 2 +- packages/cli/tests/helpers.spec.ts | 2 +- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/cli/tests/MimicConfigHandler.spec.ts b/packages/cli/tests/MimicConfigHandler.spec.ts index d4901a15..b4b1a0eb 100644 --- a/packages/cli/tests/MimicConfigHandler.spec.ts +++ b/packages/cli/tests/MimicConfigHandler.spec.ts @@ -8,8 +8,8 @@ import { MimicConfigValidator } from '../src/validators' describe('MimicConfigHandler', () => { const mimicConfig = { tasks: [ - { name: 'swap-task', manifest: './tasks/swap/manifest.yaml', path: './tasks/swap/src/task.ts' }, - { name: 'transfer-task', manifest: './tasks/transfer/manifest.yaml', path: './tasks/transfer/src/task.ts' }, + { name: 'swap-task', manifest: './tasks/swap/manifest.yaml', task: './tasks/swap/src/task.ts' }, + { name: 'transfer-task', manifest: './tasks/transfer/manifest.yaml', task: './tasks/transfer/src/task.ts' }, ], } @@ -93,16 +93,16 @@ describe('MimicConfigHandler', () => { context('when task name is missing', () => { itReturnsAnError( - { ...mimicConfig, tasks: [{ manifest: './manifest.yaml', path: './src/task.ts' }] }, + { ...mimicConfig, tasks: [{ manifest: './manifest.yaml', task: './src/task.ts' }] }, 'Required' ) }) context('when task manifest is missing', () => { - itReturnsAnError({ ...mimicConfig, tasks: [{ name: 'task', path: './src/task.ts' }] }, 'Required') + itReturnsAnError({ ...mimicConfig, tasks: [{ name: 'task', task: './src/task.ts' }] }, 'Required') }) - context('when task path is missing', () => { + context('when task task is missing', () => { itReturnsAnError({ ...mimicConfig, tasks: [{ name: 'task', manifest: './manifest.yaml' }] }, 'Required') }) }) diff --git a/packages/cli/tests/commands/build.spec.ts b/packages/cli/tests/commands/build.spec.ts index 350e5117..026a4d3e 100644 --- a/packages/cli/tests/commands/build.spec.ts +++ b/packages/cli/tests/commands/build.spec.ts @@ -52,7 +52,7 @@ describe('build', () => { beforeEach('create mimic.yaml', () => { fs.writeFileSync( mimicConfigPath, - `tasks:\n - name: test-task\n manifest: ${manifestPath}\n path: ${taskPath}\n output: ${outputDir}\n types: ${typesDir}\n` + `tasks:\n - name: test-task\n manifest: ${manifestPath}\n task: ${taskPath}\n output: ${outputDir}\n types: ${typesDir}\n` ) }) diff --git a/packages/cli/tests/commands/codegen.spec.ts b/packages/cli/tests/commands/codegen.spec.ts index 7c71eda3..144520e9 100644 --- a/packages/cli/tests/commands/codegen.spec.ts +++ b/packages/cli/tests/commands/codegen.spec.ts @@ -21,7 +21,7 @@ describe('codegen', () => { beforeEach('create mimic.yaml', () => { fs.writeFileSync( mimicConfigPath, - `tasks:\n - name: test-task\n manifest: ${manifestPath}\n path: ${basePath}/tasks/task.ts\n types: ${outputDir}\n` + `tasks:\n - name: test-task\n manifest: ${manifestPath}\n task: ${basePath}/tasks/task.ts\n types: ${outputDir}\n` ) }) diff --git a/packages/cli/tests/commands/compile.spec.ts b/packages/cli/tests/commands/compile.spec.ts index 57f08eea..fa3838f3 100644 --- a/packages/cli/tests/commands/compile.spec.ts +++ b/packages/cli/tests/commands/compile.spec.ts @@ -42,7 +42,7 @@ describe('compile', () => { beforeEach('create mimic.yaml', () => { fs.writeFileSync( mimicConfigPath, - `tasks:\n - name: test-task\n manifest: ${manifestPath}\n path: ${taskPath}\n output: ${outputDir}\n` + `tasks:\n - name: test-task\n manifest: ${manifestPath}\n task: ${taskPath}\n output: ${outputDir}\n` ) }) diff --git a/packages/cli/tests/commands/deploy.spec.ts b/packages/cli/tests/commands/deploy.spec.ts index ab538c3a..01eccd35 100644 --- a/packages/cli/tests/commands/deploy.spec.ts +++ b/packages/cli/tests/commands/deploy.spec.ts @@ -30,7 +30,7 @@ describe('deploy', () => { beforeEach('create mimic.yaml', () => { fs.writeFileSync( mimicConfigPath, - `tasks:\n - name: test-task\n manifest: ${manifestPath}\n path: ${taskPath}\n output: ${inputDir}\n` + `tasks:\n - name: test-task\n manifest: ${manifestPath}\n task: ${taskPath}\n output: ${inputDir}\n` ) }) diff --git a/packages/cli/tests/helpers.spec.ts b/packages/cli/tests/helpers.spec.ts index 49d8e825..887bb656 100644 --- a/packages/cli/tests/helpers.spec.ts +++ b/packages/cli/tests/helpers.spec.ts @@ -13,7 +13,7 @@ describe('filterTasks', () => { const createTask = (name: string): RequiredTaskConfig => ({ name, manifest: `./tasks/${name}/manifest.yaml`, - path: `./tasks/${name}/src/task.ts`, + task: `./tasks/${name}/src/task.ts`, output: `build/${name}`, types: `./tasks/${name}/src/types`, }) From 774813842146ffd979c52d9780264efc2d2f9bcf Mon Sep 17 00:00:00 2001 From: ncomerci Date: Wed, 14 Jan 2026 15:23:12 -0300 Subject: [PATCH 24/27] refactor: improve error handling and simplify task execution in CLI commands --- packages/cli/src/commands/build.ts | 25 ++++------- packages/cli/src/commands/codegen.ts | 13 ++---- packages/cli/src/commands/compile.ts | 13 ++---- packages/cli/src/commands/deploy.ts | 28 +++--------- packages/cli/src/commands/test.ts | 67 +++++++++++----------------- packages/cli/src/core/errors.ts | 30 ------------- packages/cli/src/core/test.ts | 3 +- packages/cli/src/helpers.ts | 11 ++++- 8 files changed, 59 insertions(+), 131 deletions(-) diff --git a/packages/cli/src/commands/build.ts b/packages/cli/src/commands/build.ts index d2b79895..1ec6ae6a 100644 --- a/packages/cli/src/commands/build.ts +++ b/packages/cli/src/commands/build.ts @@ -1,10 +1,9 @@ import { Command, Flags } from '@oclif/core' import { build } from '../core' -import { createConfirmClean, filterTasks, handleCoreError, runTasks, taskFilterFlags } from '../helpers' +import { createConfirmClean, filterTasks, runTasks, taskFilterFlags } from '../helpers' import MimicConfigHandler from '../lib/MimicConfigHandler' import { coreLogger } from '../log' -import { RequiredTaskConfig } from '../types' export default class Build extends Command { static override description = 'Runs code generation and then compiles the task' @@ -37,28 +36,22 @@ export default class Build extends Command { types, }) const tasks = filterTasks(this, allTasks, include, exclude) - await runTasks(this, tasks, (taskConfig) => this.runForTask(taskConfig, clean)) - } - - private async runForTask(task: Omit, clean: boolean): Promise { - try { + await runTasks(this, tasks, async (taskConfig) => { const result = await build( { - manifestPath: task.manifest, - taskPath: task.task, - outputDir: task.output, - typesDir: task.types, + manifestPath: taskConfig.manifest, + taskPath: taskConfig.task, + outputDir: taskConfig.output, + typesDir: taskConfig.types, clean, - confirmClean: createConfirmClean(task.types, coreLogger), + confirmClean: createConfirmClean(taskConfig.types, coreLogger), }, coreLogger ) if (clean && !result.success) this.exit(0) - coreLogger.info(`Build complete! Artifacts in ${task.output}/`) - } catch (error) { - handleCoreError(error) - } + coreLogger.info(`Build complete! Artifacts in ${taskConfig.output}/`) + }) } } diff --git a/packages/cli/src/commands/codegen.ts b/packages/cli/src/commands/codegen.ts index 883f9d12..941cc6da 100644 --- a/packages/cli/src/commands/codegen.ts +++ b/packages/cli/src/commands/codegen.ts @@ -1,10 +1,9 @@ import { Command, Flags } from '@oclif/core' import { codegen } from '../core' -import { createConfirmClean, filterTasks, handleCoreError, runTasks, taskFilterFlags } from '../helpers' +import { createConfirmClean, filterTasks, runTasks, taskFilterFlags } from '../helpers' import MimicConfigHandler from '../lib/MimicConfigHandler' import { coreLogger } from '../log' -import { RequiredTaskConfig } from '../types' export default class Codegen extends Command { static override description = 'Generates typed interfaces for declared inputs and ABIs from your manifest.yaml file' @@ -33,11 +32,7 @@ export default class Codegen extends Command { output: '', }) const tasks = filterTasks(this, allTasks, include, exclude) - await runTasks(this, tasks, (task) => this.runForTask(task, clean)) - } - - private async runForTask(task: Omit, clean: boolean): Promise { - try { + await runTasks(this, tasks, async (task) => { const result = await codegen( { manifestPath: task.manifest, @@ -49,8 +44,6 @@ export default class Codegen extends Command { ) if (clean && !result.success) this.exit(0) - } catch (error) { - handleCoreError(error) - } + }) } } diff --git a/packages/cli/src/commands/compile.ts b/packages/cli/src/commands/compile.ts index f9558425..44abf846 100644 --- a/packages/cli/src/commands/compile.ts +++ b/packages/cli/src/commands/compile.ts @@ -1,10 +1,9 @@ import { Command, Flags } from '@oclif/core' import { compile } from '../core' -import { filterTasks, handleCoreError, runTasks, taskFilterFlags } from '../helpers' +import { filterTasks, runTasks, taskFilterFlags } from '../helpers' import MimicConfigHandler from '../lib/MimicConfigHandler' import { coreLogger } from '../log' -import { RequiredTaskConfig } from '../types' export default class Compile extends Command { static override description = 'Compiles task' @@ -29,11 +28,7 @@ export default class Compile extends Command { types: '', }) const tasks = filterTasks(this, allTasks, include, exclude) - await runTasks(this, tasks, (task) => this.runForTask(task)) - } - - private async runForTask(task: Omit): Promise { - try { + await runTasks(this, tasks, async (task) => { await compile( { manifestPath: task.manifest, @@ -44,8 +39,6 @@ export default class Compile extends Command { ) coreLogger.info(`Build complete! Artifacts in ${task.output}/`) - } catch (error) { - handleCoreError(error) - } + }) } } diff --git a/packages/cli/src/commands/deploy.ts b/packages/cli/src/commands/deploy.ts index 528d8583..a237c4d2 100644 --- a/packages/cli/src/commands/deploy.ts +++ b/packages/cli/src/commands/deploy.ts @@ -3,10 +3,9 @@ import { resolve } from 'path' import { DEFAULT_TASK } from '../constants' import { build, deploy, MIMIC_REGISTRY_DEFAULT } from '../core' -import { filterTasks, handleCoreError, runTasks, taskFilterFlags } from '../helpers' +import { filterTasks, runTasks, taskFilterFlags } from '../helpers' import MimicConfigHandler from '../lib/MimicConfigHandler' import log, { coreLogger } from '../log' -import { RequiredTaskConfig } from '../types' import Authenticate from './authenticate' @@ -34,23 +33,12 @@ export default class Deploy extends Authenticate { const allTasks = MimicConfigHandler.loadOrDefault(this, { ...DEFAULT_TASK, output }) const tasks = filterTasks(this, allTasks, include, exclude) - await runTasks(this, tasks, (task) => this.runForTask(task, url, skipCompile, input, profile, apiKey)) - } - - private async runForTask( - task: Omit, - registryUrl: string, - skipCompile: boolean, - inputDir?: string, - profile?: string, - apiKey?: string - ): Promise { - const inputPath = resolve(inputDir ?? task.output) - const outputPath = resolve(task.output) + await runTasks(this, tasks, async (task) => { + const inputPath = resolve(input ?? task.output) + const outputPath = resolve(task.output) - const credentials = this.authenticate({ profile, 'api-key': apiKey }) + const credentials = this.authenticate({ profile, 'api-key': apiKey }) - try { if (!skipCompile) { await build( { @@ -69,7 +57,7 @@ export default class Deploy extends Authenticate { inputDir: inputPath, outputDir: outputPath, apiKey: credentials.apiKey, - registryUrl, + registryUrl: url, }, coreLogger ) @@ -77,8 +65,6 @@ export default class Deploy extends Authenticate { coreLogger.info(`IPFS CID: ${log.highlightText(result.cid)}`) coreLogger.info(`CID saved at ${log.highlightText(outputPath)}`) coreLogger.info(`Task deployed!`) - } catch (error) { - handleCoreError(error) - } + }) } } diff --git a/packages/cli/src/commands/test.ts b/packages/cli/src/commands/test.ts index 2fd4ab31..5e973709 100644 --- a/packages/cli/src/commands/test.ts +++ b/packages/cli/src/commands/test.ts @@ -2,11 +2,10 @@ import { Command, Flags } from '@oclif/core' import * as path from 'path' import { DEFAULT_TASK } from '../constants' -import { buildForTest, getTestPath, runTests, TestError } from '../core' -import { filterTasks, handleCoreError, runTasks, taskFilterFlags } from '../helpers' +import { buildForTest, getTestPath, runTests } from '../core' +import { filterTasks, runTasks, taskFilterFlags } from '../helpers' import MimicConfigHandler from '../lib/MimicConfigHandler' import { coreLogger } from '../log' -import { RequiredTaskConfig } from '../types' export default class Test extends Command { static override description = 'Runs task tests' @@ -26,45 +25,33 @@ export default class Test extends Command { const testPaths = new Set() - try { - const allTasks = MimicConfigHandler.loadOrDefault(this, DEFAULT_TASK, baseDir) - const tasks = filterTasks(this, allTasks, include, exclude) - - if (!skipCompile) { - await runTasks(this, tasks, async (task) => { - await this.compileTask(task, baseDir) - testPaths.add(getTestPath(baseDir)) - }) - } else { + const allTasks = MimicConfigHandler.loadOrDefault(this, DEFAULT_TASK, baseDir) + const tasks = filterTasks(this, allTasks, include, exclude) + + if (!skipCompile) { + await runTasks(this, tasks, async (task) => { + const originalCwd = process.cwd() + try { + process.chdir(baseDir) + await buildForTest( + { + manifestPath: task.manifest, + taskPath: task.task, + outputDir: task.output, + typesDir: task.types, + cwd: baseDir, + }, + coreLogger + ) + } finally { + process.chdir(originalCwd) + } testPaths.add(getTestPath(baseDir)) - } - - if (testPaths.size > 0) runTests({ testPaths: Array.from(testPaths), baseDir }, coreLogger) - } catch (error) { - if (error instanceof TestError) this.exit(error.exitCode) - - handleCoreError(error) + }) + } else { + testPaths.add(getTestPath(baseDir)) } - } - - private async compileTask(task: Omit, baseDir: string): Promise { - // Change to baseDir for compilation - const originalCwd = process.cwd() - try { - process.chdir(baseDir) - await buildForTest( - { - manifestPath: task.manifest, - taskPath: task.task, - outputDir: task.output, - typesDir: task.types, - cwd: baseDir, - }, - coreLogger - ) - } finally { - process.chdir(originalCwd) - } + if (testPaths.size > 0) runTests({ testPaths: Array.from(testPaths), baseDir }, coreLogger) } } diff --git a/packages/cli/src/core/errors.ts b/packages/cli/src/core/errors.ts index 54229e7a..0385a181 100644 --- a/packages/cli/src/core/errors.ts +++ b/packages/cli/src/core/errors.ts @@ -66,15 +66,6 @@ export class CompilationError extends CoreError { } } -export class BuildError extends CoreError { - constructor(message: string, suggestions?: string[]) { - super(message, { - code: 'BuildError', - suggestions: suggestions ?? ['Check the task source code and manifest'], - }) - } -} - export class DeployError extends CoreError { public statusCode?: number @@ -86,24 +77,3 @@ export class DeployError extends CoreError { this.statusCode = options?.statusCode } } - -export class AuthenticationError extends CoreError { - constructor(message: string, suggestions?: string[]) { - super(message, { - code: 'AuthenticationError', - suggestions: suggestions ?? ['Check your API key or login credentials'], - }) - } -} - -export class TestError extends CoreError { - public exitCode: number - - constructor(message: string, exitCode: number, suggestions?: string[]) { - super(message, { - code: 'TestError', - suggestions: suggestions ?? ['Check the test output for details'], - }) - this.exitCode = exitCode - } -} diff --git a/packages/cli/src/core/test.ts b/packages/cli/src/core/test.ts index c1233e80..674b2857 100644 --- a/packages/cli/src/core/test.ts +++ b/packages/cli/src/core/test.ts @@ -4,7 +4,6 @@ import { execBinCommand } from '../lib/packageManager' import { defaultLogger } from '../log' import { build } from './build' -import { TestError } from './errors' import { BuildOptions, Logger, RunTestsOptions } from './types' export function getTestPath(baseDir: string): string { @@ -36,5 +35,5 @@ export function runTests(options: RunTestsOptions, logger: Logger = defaultLogge logger.stopAction() - if (!success) throw new TestError('Tests failed', exitCode, ['Check the test output for details']) + if (!success) process.exit(exitCode) } diff --git a/packages/cli/src/helpers.ts b/packages/cli/src/helpers.ts index 9f466004..8537b9fd 100644 --- a/packages/cli/src/helpers.ts +++ b/packages/cli/src/helpers.ts @@ -102,9 +102,16 @@ export async function runTasks( try { await runTask(task) } catch (error) { - if (isSingleTask) throw error + const err = + error instanceof CoreError + ? new CommandError(error.message, { + code: error.code, + suggestions: error.suggestions, + }) + : (error as Error) + + if (isSingleTask) throw err - const err = error as Error console.error(log.warnText(`Task "${task.name}" failed: ${err.message}`)) if (err instanceof CommandError) { From a476220c4b235e32111c71ac5d2a3eb3543960a3 Mon Sep 17 00:00:00 2001 From: ncomerci Date: Wed, 14 Jan 2026 15:45:47 -0300 Subject: [PATCH 25/27] refactor: consolidate task filtering logic --- packages/cli/src/commands/build.ts | 33 ++-- packages/cli/src/commands/codegen.ts | 27 +-- packages/cli/src/commands/compile.ts | 29 +-- packages/cli/src/commands/deploy.ts | 23 ++- packages/cli/src/commands/test.ts | 22 ++- packages/cli/src/helpers.ts | 99 ++-------- packages/cli/src/lib/MimicConfigHandler.ts | 84 +++++++- packages/cli/tests/MimicConfigHandler.spec.ts | 186 +++++++++++++++++- packages/cli/tests/helpers.spec.ts | 143 -------------- 9 files changed, 350 insertions(+), 296 deletions(-) delete mode 100644 packages/cli/tests/helpers.spec.ts diff --git a/packages/cli/src/commands/build.ts b/packages/cli/src/commands/build.ts index 1ec6ae6a..c0b74cf8 100644 --- a/packages/cli/src/commands/build.ts +++ b/packages/cli/src/commands/build.ts @@ -1,8 +1,8 @@ import { Command, Flags } from '@oclif/core' import { build } from '../core' -import { createConfirmClean, filterTasks, runTasks, taskFilterFlags } from '../helpers' -import MimicConfigHandler from '../lib/MimicConfigHandler' +import { createConfirmClean, runTasks } from '../helpers' +import MimicConfigHandler, { taskFilterFlags } from '../lib/MimicConfigHandler' import { coreLogger } from '../log' export default class Build extends Command { @@ -29,29 +29,32 @@ export default class Build extends Command { const { flags } = await this.parse(Build) const { manifest, task, output, types, clean, include, exclude } = flags - const allTasks = MimicConfigHandler.loadOrDefault(this, { - manifest, - task: task, - output, - types, + const tasks = MimicConfigHandler.getFilteredTasks(this, { + defaultTask: { + manifest, + task: task, + output, + types, + }, + include, + exclude, }) - const tasks = filterTasks(this, allTasks, include, exclude) - await runTasks(this, tasks, async (taskConfig) => { + await runTasks(this, tasks, async (config) => { const result = await build( { - manifestPath: taskConfig.manifest, - taskPath: taskConfig.task, - outputDir: taskConfig.output, - typesDir: taskConfig.types, + manifestPath: config.manifest, + taskPath: config.task, + outputDir: config.output, + typesDir: config.types, clean, - confirmClean: createConfirmClean(taskConfig.types, coreLogger), + confirmClean: createConfirmClean(config.types, coreLogger), }, coreLogger ) if (clean && !result.success) this.exit(0) - coreLogger.info(`Build complete! Artifacts in ${taskConfig.output}/`) + coreLogger.info(`Build complete! Artifacts in ${config.output}/`) }) } } diff --git a/packages/cli/src/commands/codegen.ts b/packages/cli/src/commands/codegen.ts index 941cc6da..0a4d3e67 100644 --- a/packages/cli/src/commands/codegen.ts +++ b/packages/cli/src/commands/codegen.ts @@ -1,8 +1,8 @@ import { Command, Flags } from '@oclif/core' import { codegen } from '../core' -import { createConfirmClean, filterTasks, runTasks, taskFilterFlags } from '../helpers' -import MimicConfigHandler from '../lib/MimicConfigHandler' +import { createConfirmClean, runTasks } from '../helpers' +import MimicConfigHandler, { taskFilterFlags } from '../lib/MimicConfigHandler' import { coreLogger } from '../log' export default class Codegen extends Command { @@ -25,20 +25,23 @@ export default class Codegen extends Command { const { flags } = await this.parse(Codegen) const { manifest, output, clean, include, exclude } = flags - const allTasks = MimicConfigHandler.loadOrDefault(this, { - manifest, - types: output, - task: '', - output: '', + const tasks = MimicConfigHandler.getFilteredTasks(this, { + defaultTask: { + manifest, + types: output, + task: '', + output: '', + }, + include, + exclude, }) - const tasks = filterTasks(this, allTasks, include, exclude) - await runTasks(this, tasks, async (task) => { + await runTasks(this, tasks, async (config) => { const result = await codegen( { - manifestPath: task.manifest, - outputDir: task.types, + manifestPath: config.manifest, + outputDir: config.types, clean, - confirmClean: createConfirmClean(task.types, coreLogger), + confirmClean: createConfirmClean(config.types, coreLogger), }, coreLogger ) diff --git a/packages/cli/src/commands/compile.ts b/packages/cli/src/commands/compile.ts index 44abf846..c0c53ea4 100644 --- a/packages/cli/src/commands/compile.ts +++ b/packages/cli/src/commands/compile.ts @@ -1,8 +1,8 @@ import { Command, Flags } from '@oclif/core' import { compile } from '../core' -import { filterTasks, runTasks, taskFilterFlags } from '../helpers' -import MimicConfigHandler from '../lib/MimicConfigHandler' +import { runTasks } from '../helpers' +import MimicConfigHandler, { taskFilterFlags } from '../lib/MimicConfigHandler' import { coreLogger } from '../log' export default class Compile extends Command { @@ -21,24 +21,27 @@ export default class Compile extends Command { const { flags } = await this.parse(Compile) const { task: taskPath, output, manifest, include, exclude } = flags - const allTasks = MimicConfigHandler.loadOrDefault(this, { - manifest, - task: taskPath, - output, - types: '', + const tasks = MimicConfigHandler.getFilteredTasks(this, { + defaultTask: { + manifest, + task: taskPath, + output, + types: '', + }, + include, + exclude, }) - const tasks = filterTasks(this, allTasks, include, exclude) - await runTasks(this, tasks, async (task) => { + await runTasks(this, tasks, async (config) => { await compile( { - manifestPath: task.manifest, - taskPath: task.task, - outputDir: task.output, + manifestPath: config.manifest, + taskPath: config.task, + outputDir: config.output, }, coreLogger ) - coreLogger.info(`Build complete! Artifacts in ${task.output}/`) + coreLogger.info(`Build complete! Artifacts in ${config.output}/`) }) } } diff --git a/packages/cli/src/commands/deploy.ts b/packages/cli/src/commands/deploy.ts index a237c4d2..8cf496fc 100644 --- a/packages/cli/src/commands/deploy.ts +++ b/packages/cli/src/commands/deploy.ts @@ -3,8 +3,8 @@ import { resolve } from 'path' import { DEFAULT_TASK } from '../constants' import { build, deploy, MIMIC_REGISTRY_DEFAULT } from '../core' -import { filterTasks, runTasks, taskFilterFlags } from '../helpers' -import MimicConfigHandler from '../lib/MimicConfigHandler' +import { runTasks } from '../helpers' +import MimicConfigHandler, { taskFilterFlags } from '../lib/MimicConfigHandler' import log, { coreLogger } from '../log' import Authenticate from './authenticate' @@ -31,21 +31,24 @@ export default class Deploy extends Authenticate { const { flags } = await this.parse(Deploy) const { profile, 'api-key': apiKey, input, output, 'skip-compile': skipCompile, url, include, exclude } = flags - const allTasks = MimicConfigHandler.loadOrDefault(this, { ...DEFAULT_TASK, output }) - const tasks = filterTasks(this, allTasks, include, exclude) - await runTasks(this, tasks, async (task) => { - const inputPath = resolve(input ?? task.output) - const outputPath = resolve(task.output) + const tasks = MimicConfigHandler.getFilteredTasks(this, { + defaultTask: { ...DEFAULT_TASK, output }, + include, + exclude, + }) + await runTasks(this, tasks, async (config) => { + const inputPath = resolve(input ?? config.output) + const outputPath = resolve(config.output) const credentials = this.authenticate({ profile, 'api-key': apiKey }) if (!skipCompile) { await build( { - manifestPath: task.manifest, - taskPath: task.task, + manifestPath: config.manifest, + taskPath: config.task, outputDir: inputPath, - typesDir: task.types, + typesDir: config.types, clean: false, }, coreLogger diff --git a/packages/cli/src/commands/test.ts b/packages/cli/src/commands/test.ts index 5e973709..843d093b 100644 --- a/packages/cli/src/commands/test.ts +++ b/packages/cli/src/commands/test.ts @@ -3,8 +3,8 @@ import * as path from 'path' import { DEFAULT_TASK } from '../constants' import { buildForTest, getTestPath, runTests } from '../core' -import { filterTasks, runTasks, taskFilterFlags } from '../helpers' -import MimicConfigHandler from '../lib/MimicConfigHandler' +import { runTasks } from '../helpers' +import MimicConfigHandler, { taskFilterFlags } from '../lib/MimicConfigHandler' import { coreLogger } from '../log' export default class Test extends Command { @@ -25,20 +25,24 @@ export default class Test extends Command { const testPaths = new Set() - const allTasks = MimicConfigHandler.loadOrDefault(this, DEFAULT_TASK, baseDir) - const tasks = filterTasks(this, allTasks, include, exclude) + const tasks = MimicConfigHandler.getFilteredTasks(this, { + defaultTask: DEFAULT_TASK, + include, + exclude, + baseDir, + }) if (!skipCompile) { - await runTasks(this, tasks, async (task) => { + await runTasks(this, tasks, async (config) => { const originalCwd = process.cwd() try { process.chdir(baseDir) await buildForTest( { - manifestPath: task.manifest, - taskPath: task.task, - outputDir: task.output, - typesDir: task.types, + manifestPath: config.manifest, + taskPath: config.task, + outputDir: config.output, + typesDir: config.types, cwd: baseDir, }, coreLogger diff --git a/packages/cli/src/helpers.ts b/packages/cli/src/helpers.ts index 8537b9fd..5e0533ac 100644 --- a/packages/cli/src/helpers.ts +++ b/packages/cli/src/helpers.ts @@ -1,11 +1,10 @@ import { confirm } from '@inquirer/prompts' -import { Command, Flags } from '@oclif/core' +import { Command } from '@oclif/core' import { Interface } from 'ethers' import camelCase from 'lodash/camelCase' import startCase from 'lodash/startCase' import { Logger } from './core/types' -import { MIMIC_CONFIG_FILE } from './lib/MimicConfigHandler' import { DEFAULT_TASK_NAME } from './constants' import { CoreError } from './core' import { CommandError } from './errors' @@ -21,86 +20,22 @@ export function pascalCase(str: string): string { return startCase(camelCase(str)).replace(/\s/g, '') } -export const taskFilterFlags = { - include: Flags.string({ - description: `When ${MIMIC_CONFIG_FILE} exists, only run tasks with these names (space-separated)`, - multiple: true, - exclusive: ['exclude'], - }), - exclude: Flags.string({ - description: `When ${MIMIC_CONFIG_FILE} exists, exclude tasks with these names (space-separated)`, - multiple: true, - exclusive: ['include'], - }), -} - -export function filterTasks( - command: Command, - tasks: RequiredTaskConfig[], - include?: string[], - exclude?: string[] -): RequiredTaskConfig[] { - if (include && exclude) { - command.error('Cannot use both --include and --exclude flags simultaneously', { - code: 'ConflictingFlags', - suggestions: ['Use either --include or --exclude, but not both'], - }) - } - - if (!include && !exclude) return tasks - - const taskNames = new Set(tasks.map((task) => task.name)) - - const warnInvalidTaskNames = (names: string[]): void => { - if (names.length > 0) { - console.warn(`${log.warnText('Warning:')} The following task names were not found: ${names.join(', ')}`) - } - } - - if (include) { - const invalidNames = include.filter((name) => !taskNames.has(name)) - warnInvalidTaskNames(invalidNames) - - const validNames = new Set(include.filter((name) => taskNames.has(name))) - if (validNames.size === 0) { - console.warn(`${log.warnText('Warning:')} No valid tasks to include. All tasks will be skipped.`) - return [] - } - - return tasks.filter((task) => validNames.has(task.name)) - } - - if (exclude) { - const invalidNames = exclude.filter((name) => !taskNames.has(name)) - warnInvalidTaskNames(invalidNames) - - const excludeSet = new Set(exclude) - const filteredTasks = tasks.filter((task) => !excludeSet.has(task.name)) - if (filteredTasks.length === 0) { - console.warn(`${log.warnText('Warning:')} All tasks are excluded.`) - } - return filteredTasks - } - - return tasks -} - export async function runTasks( command: Command, - tasks: RequiredTaskConfig[], - runTask: (task: RequiredTaskConfig) => Promise + configs: RequiredTaskConfig[], + runTask: (config: RequiredTaskConfig) => Promise ): Promise { const errors: Array<{ task: string; error: Error; code?: string; suggestions?: string[] }> = [] - const shouldLogHeader = tasks.length > 1 || tasks[0].name !== DEFAULT_TASK_NAME - const isSingleTask = tasks.length === 1 + const shouldLogHeader = configs.length > 1 || configs[0].name !== DEFAULT_TASK_NAME + const isSingleTask = configs.length === 1 - for (const task of tasks) { + for (const config of configs) { if (shouldLogHeader) { - console.log(`\n${log.highlightText(`[${task.name}]`)}`) + console.log(`\n${log.highlightText(`[${config.name}]`)}`) } try { - await runTask(task) + await runTask(config) } catch (error) { const err = error instanceof CoreError @@ -112,7 +47,7 @@ export async function runTasks( if (isSingleTask) throw err - console.error(log.warnText(`Task "${task.name}" failed: ${err.message}`)) + console.error(log.warnText(`Task "${config.name}" failed: ${err.message}`)) if (err instanceof CommandError) { if (err.code) console.error(` Code: ${err.code}`) @@ -120,15 +55,15 @@ export async function runTasks( console.error(` Suggestions:`) err.suggestions.forEach((s) => console.error(` - ${s}`)) } - errors.push({ task: task.name, error: err, code: err.code, suggestions: err.suggestions }) + errors.push({ task: config.name, error: err, code: err.code, suggestions: err.suggestions }) } else { - errors.push({ task: task.name, error: err }) + errors.push({ task: config.name, error: err }) } } } if (errors.length > 0) { - console.log(`\n${log.warnText('Summary:')} ${errors.length}/${tasks.length} task(s) failed`) + console.log(`\n${log.warnText('Summary:')} ${errors.length}/${configs.length} task(s) failed`) errors.forEach(({ task, code }) => { console.log(code ? ` - ${task} (${code})` : ` - ${task}`) }) @@ -136,16 +71,6 @@ export async function runTasks( } } -export function handleCoreError(error: unknown): void { - if (error instanceof CoreError) { - throw new CommandError(error.message, { - code: error.code, - suggestions: error.suggestions, - }) - } - throw error -} - export function createConfirmClean(directory: string, logger: Logger): () => Promise { return async () => { const shouldDelete = await confirm({ diff --git a/packages/cli/src/lib/MimicConfigHandler.ts b/packages/cli/src/lib/MimicConfigHandler.ts index 1acb5dab..09d414a7 100644 --- a/packages/cli/src/lib/MimicConfigHandler.ts +++ b/packages/cli/src/lib/MimicConfigHandler.ts @@ -1,4 +1,4 @@ -import { Command } from '@oclif/core' +import { Command, Flags } from '@oclif/core' import * as fs from 'fs' import { load } from 'js-yaml' import * as path from 'path' @@ -6,11 +6,25 @@ import { ZodError } from 'zod' import { DEFAULT_TASK_NAME } from '../constants' import { GENERIC_SUGGESTION } from '../errors' +import log from '../log' import { MimicConfig, RequiredTaskConfig } from '../types' import { MimicConfigValidator } from '../validators' export const MIMIC_CONFIG_FILE = 'mimic.yaml' +export const taskFilterFlags = { + include: Flags.string({ + description: `When ${MIMIC_CONFIG_FILE} exists, only run tasks with these names (space-separated)`, + multiple: true, + exclusive: ['exclude'], + }), + exclude: Flags.string({ + description: `When ${MIMIC_CONFIG_FILE} exists, exclude tasks with these names (space-separated)`, + multiple: true, + exclusive: ['include'], + }), +} + export default { exists(baseDir: string = process.cwd()): boolean { return fs.existsSync(path.join(baseDir, MIMIC_CONFIG_FILE)) @@ -44,7 +58,7 @@ export default { } }, - getTasks(mimicConfig: MimicConfig): RequiredTaskConfig[] { + normalizeTaskConfigs(mimicConfig: MimicConfig): RequiredTaskConfig[] { return mimicConfig.tasks.map((task) => ({ ...task, output: task.output ?? `build/${task.name}`, @@ -59,7 +73,7 @@ export default { ): RequiredTaskConfig[] { if (this.exists(baseDir)) { const mimicConfig = this.load(command, baseDir) - return this.getTasks(mimicConfig) + return this.normalizeTaskConfigs(mimicConfig) } return [ @@ -69,6 +83,70 @@ export default { }, ] }, + + getFilteredTasks( + command: Command, + options: { + defaultTask: Omit + include?: string[] + exclude?: string[] + baseDir?: string + } + ): RequiredTaskConfig[] { + const allTasks = this.loadOrDefault(command, options.defaultTask, options.baseDir) + return filterTasks(command, allTasks, options.include, options.exclude) + }, +} + +function filterTasks( + command: Command, + tasks: RequiredTaskConfig[], + include?: string[], + exclude?: string[] +): RequiredTaskConfig[] { + if (include && exclude) { + command.error('Cannot use both --include and --exclude flags simultaneously', { + code: 'ConflictingFlags', + suggestions: ['Use either --include or --exclude, but not both'], + }) + } + + if (!include && !exclude) return tasks + + const taskNames = new Set(tasks.map((task) => task.name)) + + const warnInvalidTaskNames = (names: string[]): void => { + if (names.length > 0) { + console.warn(`${log.warnText('Warning:')} The following task names were not found: ${names.join(', ')}`) + } + } + + if (include) { + const invalidNames = include.filter((name) => !taskNames.has(name)) + warnInvalidTaskNames(invalidNames) + + const validNames = new Set(include.filter((name) => taskNames.has(name))) + if (validNames.size === 0) { + console.warn(`${log.warnText('Warning:')} No valid tasks to include. All tasks will be skipped.`) + return [] + } + + return tasks.filter((task) => validNames.has(task.name)) + } + + if (exclude) { + const invalidNames = exclude.filter((name) => !taskNames.has(name)) + warnInvalidTaskNames(invalidNames) + + const excludeSet = new Set(exclude) + const filteredTasks = tasks.filter((task) => !excludeSet.has(task.name)) + if (filteredTasks.length === 0) { + console.warn(`${log.warnText('Warning:')} All tasks are excluded.`) + } + return filteredTasks + } + + return tasks } function handleValidationError(command: Command, err: unknown): never { diff --git a/packages/cli/tests/MimicConfigHandler.spec.ts b/packages/cli/tests/MimicConfigHandler.spec.ts index b4b1a0eb..3f61cb0f 100644 --- a/packages/cli/tests/MimicConfigHandler.spec.ts +++ b/packages/cli/tests/MimicConfigHandler.spec.ts @@ -1,8 +1,11 @@ +import { Command } from '@oclif/core' import { expect } from 'chai' import * as fs from 'fs' import * as path from 'path' +import * as sinon from 'sinon' import MimicConfigHandler from '../src/lib/MimicConfigHandler' +import { RequiredTaskConfig } from '../src/types' import { MimicConfigValidator } from '../src/validators' describe('MimicConfigHandler', () => { @@ -108,7 +111,7 @@ describe('MimicConfigHandler', () => { }) }) - describe('getTasks', () => { + describe('normalizeTaskConfigs', () => { context('when dealing with optional fields', () => { context('when optional fields are provided', () => { it('returns tasks with the provided values', () => { @@ -117,7 +120,7 @@ describe('MimicConfigHandler', () => { tasks: [{ ...mimicConfig.tasks[0], output: './custom-build', types: './custom-types' }], } - const tasks = MimicConfigHandler.getTasks(mimicConfigWithOptionals) + const tasks = MimicConfigHandler.normalizeTaskConfigs(mimicConfigWithOptionals) expect(tasks).to.have.length(1) expect(tasks[0].output).to.equal('./custom-build') @@ -127,7 +130,7 @@ describe('MimicConfigHandler', () => { context('when optional fields are not provided', () => { it('returns tasks with computed default values', () => { - const tasks = MimicConfigHandler.getTasks(mimicConfig) + const tasks = MimicConfigHandler.normalizeTaskConfigs(mimicConfig) expect(tasks).to.have.length(2) expect(tasks[0].output).to.equal('build/swap-task') @@ -144,7 +147,7 @@ describe('MimicConfigHandler', () => { tasks: [mimicConfig.tasks[0], { ...mimicConfig.tasks[1], output: './custom-output' }], } - const tasks = MimicConfigHandler.getTasks(mixedMimicConfig) + const tasks = MimicConfigHandler.normalizeTaskConfigs(mixedMimicConfig) expect(tasks).to.have.length(2) expect(tasks[0].output).to.equal('build/swap-task') @@ -155,4 +158,179 @@ describe('MimicConfigHandler', () => { }) }) }) + + describe('getFilteredTasks', () => { + let mockCommand: Command + let warnSpy: sinon.SinonSpy + let errorStub: sinon.SinonStub + let loadOrDefaultStub: sinon.SinonStub + + const createTask = (name: string): RequiredTaskConfig => ({ + name, + manifest: `./tasks/${name}/manifest.yaml`, + task: `./tasks/${name}/src/task.ts`, + output: `build/${name}`, + types: `./tasks/${name}/src/types`, + }) + + const tasks: RequiredTaskConfig[] = [createTask('swap-task'), createTask('transfer-task'), createTask('call-task')] + + const defaultTask = { + manifest: 'manifest.yaml', + task: 'src/task.ts', + output: './build', + types: './src/types', + } + + beforeEach('setup mocks', () => { + errorStub = sinon.stub().throws(new Error('Command error')) + mockCommand = { + error: errorStub, + } as unknown as Command + + warnSpy = sinon.spy(console, 'warn') + loadOrDefaultStub = sinon.stub(MimicConfigHandler, 'loadOrDefault').returns(tasks) + }) + + afterEach('restore mocks', () => { + warnSpy.restore() + loadOrDefaultStub.restore() + }) + + context('when no filter flags are provided', () => { + it('returns all tasks', () => { + const result = MimicConfigHandler.getFilteredTasks(mockCommand, { + defaultTask, + }) + + expect(result).to.have.length(3) + expect(result).to.deep.equal(tasks) + expect(warnSpy.called).to.be.false + expect(loadOrDefaultStub.calledOnce).to.be.true + }) + }) + + context('when --include flag is provided', () => { + context('when all task names are valid', () => { + context('when including a single task', () => { + it('returns only the included task', () => { + const result = MimicConfigHandler.getFilteredTasks(mockCommand, { + defaultTask, + include: ['swap-task'], + }) + + expect(result).to.have.length(1) + expect(result[0].name).to.equal('swap-task') + expect(warnSpy.called).to.be.false + }) + }) + + context('when including multiple tasks', () => { + it('returns all included tasks', () => { + const result = MimicConfigHandler.getFilteredTasks(mockCommand, { + defaultTask, + include: ['swap-task', 'transfer-task'], + }) + + expect(result).to.have.length(2) + expect(result.map((t) => t.name)).to.include.members(['swap-task', 'transfer-task']) + expect(warnSpy.called).to.be.false + }) + }) + }) + + context('when some task names are invalid', () => { + it('logs a warning and returns valid tasks', () => { + const result = MimicConfigHandler.getFilteredTasks(mockCommand, { + defaultTask, + include: ['swap-task', 'invalid-task'], + }) + + expect(result).to.have.length(1) + expect(result[0].name).to.equal('swap-task') + expect(warnSpy.calledOnce).to.be.true + expect(warnSpy.firstCall.args[0]).to.include('invalid-task') + }) + }) + + context('when all task names are invalid', () => { + it('logs a warning and returns empty array', () => { + const result = MimicConfigHandler.getFilteredTasks(mockCommand, { + defaultTask, + include: ['invalid-task-1', 'invalid-task-2'], + }) + + expect(result).to.have.length(0) + expect(warnSpy.calledTwice).to.be.true + expect(warnSpy.firstCall.args[0]).to.include('invalid-task-1') + expect(warnSpy.secondCall.args[0]).to.include('No valid tasks to include') + }) + }) + }) + + context('when --exclude flag is provided', () => { + context('when all task names are valid', () => { + context('when excluding a single task', () => { + it('returns tasks except the excluded one', () => { + const result = MimicConfigHandler.getFilteredTasks(mockCommand, { + defaultTask, + exclude: ['swap-task'], + }) + + expect(result).to.have.length(2) + expect(result.map((t) => t.name)).to.include.members(['transfer-task', 'call-task']) + expect(result.map((t) => t.name)).to.not.include('swap-task') + expect(warnSpy.called).to.be.false + }) + }) + + context('when excluding multiple tasks', () => { + it('returns tasks except the excluded ones', () => { + const result = MimicConfigHandler.getFilteredTasks(mockCommand, { + defaultTask, + exclude: ['swap-task', 'transfer-task'], + }) + + expect(result).to.have.length(1) + expect(result[0].name).to.equal('call-task') + expect(warnSpy.called).to.be.false + }) + }) + }) + + context('when some task names are invalid', () => { + it('logs a warning and excludes valid tasks', () => { + const result = MimicConfigHandler.getFilteredTasks(mockCommand, { + defaultTask, + exclude: ['swap-task', 'invalid-task'], + }) + + expect(result).to.have.length(2) + expect(result.map((t) => t.name)).to.include.members(['transfer-task', 'call-task']) + expect(result.map((t) => t.name)).to.not.include('swap-task') + expect(warnSpy.calledOnce).to.be.true + expect(warnSpy.firstCall.args[0]).to.include('invalid-task') + }) + }) + }) + + context('when both flags are provided', () => { + it('throws a ConflictingFlags error', () => { + expect(() => { + MimicConfigHandler.getFilteredTasks(mockCommand, { + defaultTask, + include: ['swap-task'], + exclude: ['transfer-task'], + }) + }).to.throw('Command error') + + expect(errorStub.calledOnce).to.be.true + expect(errorStub.firstCall.args[0]).to.equal('Cannot use both --include and --exclude flags simultaneously') + expect(errorStub.firstCall.args[1]).to.deep.equal({ + code: 'ConflictingFlags', + suggestions: ['Use either --include or --exclude, but not both'], + }) + }) + }) + }) }) diff --git a/packages/cli/tests/helpers.spec.ts b/packages/cli/tests/helpers.spec.ts deleted file mode 100644 index 887bb656..00000000 --- a/packages/cli/tests/helpers.spec.ts +++ /dev/null @@ -1,143 +0,0 @@ -import { Command } from '@oclif/core' -import { expect } from 'chai' -import * as sinon from 'sinon' - -import { filterTasks } from '../src/helpers' -import { RequiredTaskConfig } from '../src/types' - -describe('filterTasks', () => { - let mockCommand: Command - let warnSpy: sinon.SinonSpy - let errorStub: sinon.SinonStub - - const createTask = (name: string): RequiredTaskConfig => ({ - name, - manifest: `./tasks/${name}/manifest.yaml`, - task: `./tasks/${name}/src/task.ts`, - output: `build/${name}`, - types: `./tasks/${name}/src/types`, - }) - - const tasks: RequiredTaskConfig[] = [createTask('swap-task'), createTask('transfer-task'), createTask('call-task')] - - beforeEach('setup mocks', () => { - errorStub = sinon.stub().throws(new Error('Command error')) - mockCommand = { - error: errorStub, - } as unknown as Command - - warnSpy = sinon.spy(console, 'warn') - }) - - afterEach('restore mocks', () => { - warnSpy.restore() - }) - - context('when no filter flags are provided', () => { - it('returns all tasks', () => { - const result = filterTasks(mockCommand, tasks) - - expect(result).to.have.length(3) - expect(result).to.deep.equal(tasks) - expect(warnSpy.called).to.be.false - }) - }) - - context('when --include flag is provided', () => { - context('when all task names are valid', () => { - context('when including a single task', () => { - it('returns only the included task', () => { - const result = filterTasks(mockCommand, tasks, ['swap-task']) - - expect(result).to.have.length(1) - expect(result[0].name).to.equal('swap-task') - expect(warnSpy.called).to.be.false - }) - }) - - context('when including multiple tasks', () => { - it('returns all included tasks', () => { - const result = filterTasks(mockCommand, tasks, ['swap-task', 'transfer-task']) - - expect(result).to.have.length(2) - expect(result.map((t) => t.name)).to.include.members(['swap-task', 'transfer-task']) - expect(warnSpy.called).to.be.false - }) - }) - }) - - context('when some task names are invalid', () => { - it('logs a warning and returns valid tasks', () => { - const result = filterTasks(mockCommand, tasks, ['swap-task', 'invalid-task']) - - expect(result).to.have.length(1) - expect(result[0].name).to.equal('swap-task') - expect(warnSpy.calledOnce).to.be.true - expect(warnSpy.firstCall.args[0]).to.include('invalid-task') - }) - }) - - context('when all task names are invalid', () => { - it('logs a warning and returns empty array', () => { - const result = filterTasks(mockCommand, tasks, ['invalid-task-1', 'invalid-task-2']) - - expect(result).to.have.length(0) - expect(warnSpy.calledTwice).to.be.true - expect(warnSpy.firstCall.args[0]).to.include('invalid-task-1') - expect(warnSpy.secondCall.args[0]).to.include('No valid tasks to include') - }) - }) - }) - - context('when --exclude flag is provided', () => { - context('when all task names are valid', () => { - context('when excluding a single task', () => { - it('returns tasks except the excluded one', () => { - const result = filterTasks(mockCommand, tasks, undefined, ['swap-task']) - - expect(result).to.have.length(2) - expect(result.map((t) => t.name)).to.include.members(['transfer-task', 'call-task']) - expect(result.map((t) => t.name)).to.not.include('swap-task') - expect(warnSpy.called).to.be.false - }) - }) - - context('when excluding multiple tasks', () => { - it('returns tasks except the excluded ones', () => { - const result = filterTasks(mockCommand, tasks, undefined, ['swap-task', 'transfer-task']) - - expect(result).to.have.length(1) - expect(result[0].name).to.equal('call-task') - expect(warnSpy.called).to.be.false - }) - }) - }) - - context('when some task names are invalid', () => { - it('logs a warning and excludes valid tasks', () => { - const result = filterTasks(mockCommand, tasks, undefined, ['swap-task', 'invalid-task']) - - expect(result).to.have.length(2) - expect(result.map((t) => t.name)).to.include.members(['transfer-task', 'call-task']) - expect(result.map((t) => t.name)).to.not.include('swap-task') - expect(warnSpy.calledOnce).to.be.true - expect(warnSpy.firstCall.args[0]).to.include('invalid-task') - }) - }) - }) - - context('when both flags are provided', () => { - it('throws a ConflictingFlags error', () => { - expect(() => { - filterTasks(mockCommand, tasks, ['swap-task'], ['transfer-task']) - }).to.throw('Command error') - - expect(errorStub.calledOnce).to.be.true - expect(errorStub.firstCall.args[0]).to.equal('Cannot use both --include and --exclude flags simultaneously') - expect(errorStub.firstCall.args[1]).to.deep.equal({ - code: 'ConflictingFlags', - suggestions: ['Use either --include or --exclude, but not both'], - }) - }) - }) -}) From 1530c942dfc4e57bbf1d65012e313a73e131778f Mon Sep 17 00:00:00 2001 From: ncomerci Date: Wed, 14 Jan 2026 15:59:32 -0300 Subject: [PATCH 26/27] refactor: code improvements --- packages/cli/src/core/codegen.ts | 35 ++++++++---------- packages/cli/src/core/deploy.ts | 11 ++++-- packages/cli/src/core/errors.ts | 2 +- packages/cli/src/helpers.ts | 41 ++++++++++++++-------- packages/cli/src/lib/MimicConfigHandler.ts | 34 +++++++++--------- 5 files changed, 67 insertions(+), 56 deletions(-) diff --git a/packages/cli/src/core/codegen.ts b/packages/cli/src/core/codegen.ts index d5c27deb..4a6996db 100644 --- a/packages/cli/src/core/codegen.ts +++ b/packages/cli/src/core/codegen.ts @@ -47,7 +47,7 @@ function validateManifest(manifest: any): Manifest { return ManifestValidator.parse(mergedManifest) } -function mergeIfUnique(list: Record[]) { +function mergeIfUnique(list: Record[]): Record { const merged: Record = {} for (const obj of list || []) { const entries = Object.entries(obj) @@ -60,17 +60,19 @@ function mergeIfUnique(list: Record[]) { } function getLibVersion(): string { - try { - let currentDir = process.cwd() - while (currentDir !== path.dirname(currentDir)) { - const libPackagePath = path.join(currentDir, 'node_modules', '@mimicprotocol', 'lib-ts', 'package.json') - if (fs.existsSync(libPackagePath)) return JSON.parse(fs.readFileSync(libPackagePath, 'utf-8')).version - currentDir = path.dirname(currentDir) + let currentDir = process.cwd() + while (currentDir !== path.dirname(currentDir)) { + const libPackagePath = path.join(currentDir, 'node_modules', '@mimicprotocol', 'lib-ts', 'package.json') + if (fs.existsSync(libPackagePath)) { + try { + return JSON.parse(fs.readFileSync(libPackagePath, 'utf-8')).version + } catch (error) { + throw new Error(`Failed to read @mimicprotocol/lib-ts version: ${error}`) + } } - throw new Error('Could not find @mimicprotocol/lib-ts package') - } catch (error) { - throw new Error(`Failed to read @mimicprotocol/lib-ts version: ${error}`) + currentDir = path.dirname(currentDir) } + throw new Error('Could not find @mimicprotocol/lib-ts package') } function convertManifestError(err: unknown): ManifestValidationError { @@ -94,9 +96,7 @@ function convertManifestError(err: unknown): ManifestValidationError { return new ManifestValidationError(`Unknown Error: ${err}`) } -function generateAbisCode(manifest: Manifest, outputDir: string, manifestDir: string): string[] { - const generatedFiles: string[] = [] - +function generateAbisCode(manifest: Manifest, outputDir: string, manifestDir: string): void { for (const [contractName, abiRelativePath] of Object.entries(manifest.abis)) { const abiPath = path.join(manifestDir, '../', abiRelativePath) if (!fs.existsSync(abiPath)) { @@ -111,24 +111,17 @@ function generateAbisCode(manifest: Manifest, outputDir: string, manifestDir: st if (abiInterface.length > 0) { const outputPath = `${outputDir}/${contractName}.ts` fs.writeFileSync(outputPath, abiInterface) - generatedFiles.push(outputPath) } } - - return generatedFiles } -function generateInputsCode(manifest: Manifest, outputDir: string): string[] { - const generatedFiles: string[] = [] +function generateInputsCode(manifest: Manifest, outputDir: string): void { const inputsInterface = InputsInterfaceGenerator.generate(manifest.inputs) if (inputsInterface.length > 0) { const outputPath = `${outputDir}/index.ts` fs.writeFileSync(outputPath, inputsInterface) - generatedFiles.push(outputPath) } - - return generatedFiles } export async function codegen(options: CodegenOptions, logger: Logger = defaultLogger): Promise { diff --git a/packages/cli/src/core/deploy.ts b/packages/cli/src/core/deploy.ts index d730052b..ef5cd80f 100644 --- a/packages/cli/src/core/deploy.ts +++ b/packages/cli/src/core/deploy.ts @@ -50,7 +50,10 @@ async function uploadToRegistry(files: string[], apiKey: string, registryUrl: st } function handleUploadError(err: unknown): never { - if (!(err instanceof AxiosError)) throw new DeployError(err instanceof Error ? err.message : String(err)) + if (!(err instanceof AxiosError)) { + const message = err instanceof Error ? err.message : String(err) + throw new DeployError(message) + } const statusCode = err.response?.status @@ -72,10 +75,12 @@ function handleUploadError(err: unknown): never { statusCode, suggestions: ['Review your API key'], }) - default: - throw new DeployError(`Upload failed: ${err.message}`, { + default: { + const message = err.message ? `Upload failed: ${err.message}` : 'Upload failed' + throw new DeployError(message, { statusCode, }) + } } } diff --git a/packages/cli/src/core/errors.ts b/packages/cli/src/core/errors.ts index 0385a181..8542469b 100644 --- a/packages/cli/src/core/errors.ts +++ b/packages/cli/src/core/errors.ts @@ -1,4 +1,4 @@ -export const GENERIC_SUGGESTION = [ +const GENERIC_SUGGESTION = [ 'Contact the Mimic team for further assistance at our website https://www.mimic.fi or discord https://discord.mimic.fi', ] diff --git a/packages/cli/src/helpers.ts b/packages/cli/src/helpers.ts index 5e0533ac..707e1f47 100644 --- a/packages/cli/src/helpers.ts +++ b/packages/cli/src/helpers.ts @@ -20,6 +20,30 @@ export function pascalCase(str: string): string { return startCase(camelCase(str)).replace(/\s/g, '') } +function convertToCommandError(error: unknown): Error { + if (error instanceof CoreError) { + return new CommandError(error.message, { + code: error.code, + suggestions: error.suggestions, + }) + } + return error as Error +} + +function logTaskError(taskName: string, err: Error): void { + console.error(log.warnText(`Task "${taskName}" failed: ${err.message}`)) + + if (err instanceof CommandError) { + if (err.code) { + console.error(` Code: ${err.code}`) + } + if (err.suggestions?.length) { + console.error(` Suggestions:`) + err.suggestions.forEach((suggestion) => console.error(` - ${suggestion}`)) + } + } +} + export async function runTasks( command: Command, configs: RequiredTaskConfig[], @@ -37,24 +61,13 @@ export async function runTasks( try { await runTask(config) } catch (error) { - const err = - error instanceof CoreError - ? new CommandError(error.message, { - code: error.code, - suggestions: error.suggestions, - }) - : (error as Error) + const err = convertToCommandError(error) if (isSingleTask) throw err - console.error(log.warnText(`Task "${config.name}" failed: ${err.message}`)) + logTaskError(config.name, err) if (err instanceof CommandError) { - if (err.code) console.error(` Code: ${err.code}`) - if (err.suggestions?.length) { - console.error(` Suggestions:`) - err.suggestions.forEach((s) => console.error(` - ${s}`)) - } errors.push({ task: config.name, error: err, code: err.code, suggestions: err.suggestions }) } else { errors.push({ task: config.name, error: err }) @@ -72,7 +85,7 @@ export async function runTasks( } export function createConfirmClean(directory: string, logger: Logger): () => Promise { - return async () => { + return async function confirmClean(): Promise { const shouldDelete = await confirm({ message: `Are you sure you want to ${log.warnText('delete')} all the contents in ${log.highlightText( directory diff --git a/packages/cli/src/lib/MimicConfigHandler.ts b/packages/cli/src/lib/MimicConfigHandler.ts index 09d414a7..0e6df665 100644 --- a/packages/cli/src/lib/MimicConfigHandler.ts +++ b/packages/cli/src/lib/MimicConfigHandler.ts @@ -10,7 +10,7 @@ import log from '../log' import { MimicConfig, RequiredTaskConfig } from '../types' import { MimicConfigValidator } from '../validators' -export const MIMIC_CONFIG_FILE = 'mimic.yaml' +const MIMIC_CONFIG_FILE = 'mimic.yaml' export const taskFilterFlags = { include: Flags.string({ @@ -98,6 +98,12 @@ export default { }, } +function warnInvalidTaskNames(names: string[]): void { + if (names.length > 0) { + console.warn(`${log.warnText('Warning:')} The following task names were not found: ${names.join(', ')}`) + } +} + function filterTasks( command: Command, tasks: RequiredTaskConfig[], @@ -111,16 +117,12 @@ function filterTasks( }) } - if (!include && !exclude) return tasks + if (!include && !exclude) { + return tasks + } const taskNames = new Set(tasks.map((task) => task.name)) - const warnInvalidTaskNames = (names: string[]): void => { - if (names.length > 0) { - console.warn(`${log.warnText('Warning:')} The following task names were not found: ${names.join(', ')}`) - } - } - if (include) { const invalidNames = include.filter((name) => !taskNames.has(name)) warnInvalidTaskNames(invalidNames) @@ -150,17 +152,15 @@ function filterTasks( } function handleValidationError(command: Command, err: unknown): never { - let message: string - let code: string - let suggestions: string[] - if (err instanceof ZodError) { - ;[message, code] = [`Invalid ${MIMIC_CONFIG_FILE} configuration`, 'ValidationError'] - suggestions = err.errors.map((e) => `Fix Field "${e.path.join('.')}" -- ${e.message}`) - } else { - ;[message, code] = [`Unknown Error: ${err}`, 'UnknownError'] - suggestions = GENERIC_SUGGESTION + const message = `Invalid ${MIMIC_CONFIG_FILE} configuration` + const code = 'ValidationError' + const suggestions = err.errors.map((e) => `Fix Field "${e.path.join('.')}" -- ${e.message}`) + command.error(message, { code, suggestions }) } + const message = `Unknown Error: ${err}` + const code = 'UnknownError' + const suggestions = GENERIC_SUGGESTION command.error(message, { code, suggestions }) } From 92153eed063d1ba0c2641d3b35e8d1ac606b8996 Mon Sep 17 00:00:00 2001 From: ncomerci Date: Wed, 14 Jan 2026 16:33:46 -0300 Subject: [PATCH 27/27] chore: requested change --- packages/cli/src/commands/build.ts | 6 ++---- packages/cli/src/commands/codegen.ts | 6 ++---- packages/cli/src/helpers.ts | 3 ++- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/packages/cli/src/commands/build.ts b/packages/cli/src/commands/build.ts index c0b74cf8..d0a719a2 100644 --- a/packages/cli/src/commands/build.ts +++ b/packages/cli/src/commands/build.ts @@ -40,20 +40,18 @@ export default class Build extends Command { exclude, }) await runTasks(this, tasks, async (config) => { - const result = await build( + await build( { manifestPath: config.manifest, taskPath: config.task, outputDir: config.output, typesDir: config.types, clean, - confirmClean: createConfirmClean(config.types, coreLogger), + confirmClean: createConfirmClean(this, config.types, coreLogger), }, coreLogger ) - if (clean && !result.success) this.exit(0) - coreLogger.info(`Build complete! Artifacts in ${config.output}/`) }) } diff --git a/packages/cli/src/commands/codegen.ts b/packages/cli/src/commands/codegen.ts index 0a4d3e67..664dc87b 100644 --- a/packages/cli/src/commands/codegen.ts +++ b/packages/cli/src/commands/codegen.ts @@ -36,17 +36,15 @@ export default class Codegen extends Command { exclude, }) await runTasks(this, tasks, async (config) => { - const result = await codegen( + await codegen( { manifestPath: config.manifest, outputDir: config.types, clean, - confirmClean: createConfirmClean(config.types, coreLogger), + confirmClean: createConfirmClean(this, config.types, coreLogger), }, coreLogger ) - - if (clean && !result.success) this.exit(0) }) } } diff --git a/packages/cli/src/helpers.ts b/packages/cli/src/helpers.ts index 707e1f47..a0c5cc2d 100644 --- a/packages/cli/src/helpers.ts +++ b/packages/cli/src/helpers.ts @@ -84,7 +84,7 @@ export async function runTasks( } } -export function createConfirmClean(directory: string, logger: Logger): () => Promise { +export function createConfirmClean(command: Command, directory: string, logger: Logger): () => Promise { return async function confirmClean(): Promise { const shouldDelete = await confirm({ message: `Are you sure you want to ${log.warnText('delete')} all the contents in ${log.highlightText( @@ -95,6 +95,7 @@ export function createConfirmClean(directory: string, logger: Logger): () => Pro if (!shouldDelete) { logger.info('You can remove the --clean flag from your command') logger.info('Stopping initialization...') + command.exit(0) } return shouldDelete }