From 2c2ec7268fc3319524e387f61dcb45075070e0d4 Mon Sep 17 00:00:00 2001 From: Agustincito Date: Thu, 5 Feb 2026 14:50:41 -0300 Subject: [PATCH 1/6] Add mimic config --- packages/cli/src/commands/build.ts | 18 +++++++- packages/cli/src/commands/compile.ts | 17 +++++++- packages/cli/src/commands/functions.ts | 58 ++++++++++++++++++++++++++ 3 files changed, 91 insertions(+), 2 deletions(-) create mode 100644 packages/cli/src/commands/functions.ts diff --git a/packages/cli/src/commands/build.ts b/packages/cli/src/commands/build.ts index a9b617ce..f51f4b5f 100644 --- a/packages/cli/src/commands/build.ts +++ b/packages/cli/src/commands/build.ts @@ -4,6 +4,9 @@ import { FlagsType } from '../types' import Codegen from './codegen' import Compile from './compile' +import Functions, { FunctionConfig } from './functions' +import path from 'path/win32' +import log from '../log' export type BuildFlags = FlagsType @@ -15,13 +18,26 @@ export default class Build extends Command { ] static override flags = { + ...Functions.flags, ...Codegen.flags, ...Compile.flags, } public async run(): Promise { const { flags } = await this.parse(Build) - await Build.build(this, flags) + await Build.buildFunctions(this, Functions.filterFunctions(this, flags), flags) + } + + public static async buildFunctions(cmd: Command, functions: FunctionConfig[], flags: BuildFlags): Promise { + for (const func of functions) { + log.startAction(`Starting building for function ${func.name}`) + await Build.build(cmd, { + ...flags, + function: func.function, + 'build-directory': path.join(flags['build-directory'], func.name), + manifest: func.manifest, + }) + } } public static async build(cmd: Command, flags: BuildFlags): Promise { diff --git a/packages/cli/src/commands/compile.ts b/packages/cli/src/commands/compile.ts index ef6130ca..26dc0c0b 100644 --- a/packages/cli/src/commands/compile.ts +++ b/packages/cli/src/commands/compile.ts @@ -7,6 +7,8 @@ import { execBinCommand } from '../lib/packageManager' import log from '../log' import { FlagsType } from '../types' +import Functions, { FunctionConfig } from './functions' + export type CompileFlags = FlagsType export default class Compile extends Command { @@ -17,6 +19,7 @@ export default class Compile extends Command { ] static override flags = { + ...Functions.flags, function: Flags.string({ char: 'f', description: 'Function to compile', default: 'src/function.ts' }), manifest: Flags.string({ char: 'm', description: 'Manifest to validate', default: 'manifest.yaml' }), 'build-directory': Flags.string({ char: 'b', description: 'Output directory for compilation', default: './build' }), @@ -24,7 +27,19 @@ export default class Compile extends Command { public async run(): Promise { const { flags } = await this.parse(Compile) - await Compile.compile(this, flags) + await Compile.compileFunctions(this, Functions.filterFunctions(this, flags), flags) + } + + public static async compileFunctions(cmd: Command, functions: FunctionConfig[], flags: CompileFlags): Promise { + for (const func of functions) { + log.startAction(`Starting compilation for function ${func.name}`) + await this.compile(cmd, { + ...flags, + function: func.function, + 'build-directory': path.join(flags['build-directory'], func.name), + manifest: func.manifest, + }) + } } public static async compile( diff --git a/packages/cli/src/commands/functions.ts b/packages/cli/src/commands/functions.ts new file mode 100644 index 00000000..d9fd12f5 --- /dev/null +++ b/packages/cli/src/commands/functions.ts @@ -0,0 +1,58 @@ +import { Command, Flags } from '@oclif/core' + +import { FlagsType } from '../types' + +export type FunctionsFlags = FlagsType + +export type FunctionConfig = { + name: string + manifest: string + function: string + buildDirectory: string +} + +export default class Functions extends Command { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + run(): Promise { + throw new Error('Method not implemented.') + } + + static override description = 'Authenticate with Mimic by storing your API key locally' + + static override examples = [ + '<%= config.bin %> <%= command.id %>', + '<%= config.bin %> <%= command.id %> --profile staging', + '<%= config.bin %> <%= command.id %> --profile production --api-key YOUR_API_KEY', + ] + + static MIMIC_CONFIG_FILE = 'mimic.yaml' + + static flags = { + 'config-file': Flags.string({ + char: 'f', + description: `Path to the ${Functions.MIMIC_CONFIG_FILE} file, this overrides other parameters like build-directory and function`, + default: Functions.MIMIC_CONFIG_FILE, + }), + include: Flags.string({ + description: `When ${Functions.MIMIC_CONFIG_FILE} exists, only run tasks with these names (space-separated)`, + multiple: true, + exclusive: ['exclude'], + }), + exclude: Flags.string({ + description: `When ${Functions.MIMIC_CONFIG_FILE} exists, exclude tasks with these names (space-separated)`, + multiple: true, + exclusive: ['include'], + }), + } + + public static filterFunctions(cmd: Command, flags: FunctionsFlags): FunctionConfig[] { + return [ + { + name: 'Pepe', + manifest: 'pepito.yaml', + function: 'src/pepe.ts', + buildDirectory: 'build/pepe', + }, + ] + } +} From 71c9b6d761d11b7d1214be5a31ffc0971fd386f3 Mon Sep 17 00:00:00 2001 From: Agustincito Date: Thu, 5 Feb 2026 17:35:21 -0300 Subject: [PATCH 2/6] Implement multi task --- packages/cli/src/commands/build.ts | 18 +------ packages/cli/src/commands/codegen.ts | 13 +++-- packages/cli/src/commands/compile.ts | 26 ++++------ packages/cli/src/commands/deploy.ts | 4 +- packages/cli/src/commands/functions.ts | 69 +++++++++++++++++++------- packages/cli/src/commands/test.ts | 4 +- 6 files changed, 78 insertions(+), 56 deletions(-) diff --git a/packages/cli/src/commands/build.ts b/packages/cli/src/commands/build.ts index f51f4b5f..96518fa8 100644 --- a/packages/cli/src/commands/build.ts +++ b/packages/cli/src/commands/build.ts @@ -4,9 +4,7 @@ import { FlagsType } from '../types' import Codegen from './codegen' import Compile from './compile' -import Functions, { FunctionConfig } from './functions' -import path from 'path/win32' -import log from '../log' +import Functions from './functions' export type BuildFlags = FlagsType @@ -25,19 +23,7 @@ export default class Build extends Command { public async run(): Promise { const { flags } = await this.parse(Build) - await Build.buildFunctions(this, Functions.filterFunctions(this, flags), flags) - } - - public static async buildFunctions(cmd: Command, functions: FunctionConfig[], flags: BuildFlags): Promise { - for (const func of functions) { - log.startAction(`Starting building for function ${func.name}`) - await Build.build(cmd, { - ...flags, - function: func.function, - 'build-directory': path.join(flags['build-directory'], func.name), - manifest: func.manifest, - }) - } + await Functions.runFunctions(this, flags, Build.build, 'build') } public static async build(cmd: Command, flags: BuildFlags): Promise { diff --git a/packages/cli/src/commands/codegen.ts b/packages/cli/src/commands/codegen.ts index 7e3594cc..e359c2af 100644 --- a/packages/cli/src/commands/codegen.ts +++ b/packages/cli/src/commands/codegen.ts @@ -7,6 +7,8 @@ import { AbisInterfaceGenerator, InputsInterfaceGenerator, ManifestHandler } fro import log from '../log' import { FlagsType, Manifest } from '../types' +import Functions, { DefaultFunctionConfig } from './functions' + export type CodegenFlags = FlagsType export default class Codegen extends Command { @@ -17,11 +19,16 @@ export default class Codegen extends Command { ] static override flags = { - manifest: Flags.string({ char: 'm', description: 'Specify a custom manifest file path', default: 'manifest.yaml' }), + ...Functions.flags, + manifest: Flags.string({ + char: 'm', + description: 'Specify a custom manifest file path', + default: DefaultFunctionConfig.manifest, + }), 'types-directory': Flags.string({ char: 't', description: 'Output directory for generated types', - default: './src/types', + default: DefaultFunctionConfig['types-directory'], }), clean: Flags.boolean({ char: 'c', @@ -32,7 +39,7 @@ export default class Codegen extends Command { public async run(): Promise { const { flags } = await this.parse(Codegen) - await Codegen.codegen(this, flags) + await Functions.runFunctions(this, flags, Codegen.codegen, 'code generation') } public static async codegen(cmd: Command, flags: CodegenFlags): Promise { diff --git a/packages/cli/src/commands/compile.ts b/packages/cli/src/commands/compile.ts index 26dc0c0b..ee2f08dc 100644 --- a/packages/cli/src/commands/compile.ts +++ b/packages/cli/src/commands/compile.ts @@ -7,7 +7,7 @@ import { execBinCommand } from '../lib/packageManager' import log from '../log' import { FlagsType } from '../types' -import Functions, { FunctionConfig } from './functions' +import Functions, { DefaultFunctionConfig } from './functions' export type CompileFlags = FlagsType @@ -20,26 +20,18 @@ export default class Compile extends Command { static override flags = { ...Functions.flags, - function: Flags.string({ char: 'f', description: 'Function to compile', default: 'src/function.ts' }), - manifest: Flags.string({ char: 'm', description: 'Manifest to validate', default: 'manifest.yaml' }), - 'build-directory': Flags.string({ char: 'b', description: 'Output directory for compilation', default: './build' }), + function: Flags.string({ char: 'f', description: 'Function to compile', default: DefaultFunctionConfig.function }), + manifest: Flags.string({ char: 'm', description: 'Manifest to validate', default: DefaultFunctionConfig.manifest }), + 'build-directory': Flags.string({ + char: 'b', + description: 'Output directory for compilation', + default: DefaultFunctionConfig['build-directory'], + }), } public async run(): Promise { const { flags } = await this.parse(Compile) - await Compile.compileFunctions(this, Functions.filterFunctions(this, flags), flags) - } - - public static async compileFunctions(cmd: Command, functions: FunctionConfig[], flags: CompileFlags): Promise { - for (const func of functions) { - log.startAction(`Starting compilation for function ${func.name}`) - await this.compile(cmd, { - ...flags, - function: func.function, - 'build-directory': path.join(flags['build-directory'], func.name), - manifest: func.manifest, - }) - } + await Functions.runFunctions(this, flags, Compile.compile, 'compilation') } public static async compile( diff --git a/packages/cli/src/commands/deploy.ts b/packages/cli/src/commands/deploy.ts index d25ca9b3..f8d62b71 100644 --- a/packages/cli/src/commands/deploy.ts +++ b/packages/cli/src/commands/deploy.ts @@ -11,6 +11,7 @@ import { FlagsType } from '../types' import Authenticate from './authenticate' import Build from './build' +import Functions from './functions' const MIMIC_REGISTRY_DEFAULT = 'https://api-protocol.mimic.fi' @@ -27,6 +28,7 @@ export default class Deploy extends Command { ] static override flags = { + ...Functions.flags, ...Authenticate.flags, ...Build.flags, 'build-directory': Flags.string({ @@ -40,7 +42,7 @@ export default class Deploy extends Command { public async run(): Promise { const { flags } = await this.parse(Deploy) - await Deploy.deploy(this, flags) + await Functions.runFunctions(this, flags, Deploy.deploy, 'deployment') } public static async deploy(cmd: Command, flags: DeployFlags): Promise { diff --git a/packages/cli/src/commands/functions.ts b/packages/cli/src/commands/functions.ts index d9fd12f5..0965510c 100644 --- a/packages/cli/src/commands/functions.ts +++ b/packages/cli/src/commands/functions.ts @@ -1,14 +1,26 @@ import { Command, Flags } from '@oclif/core' +import * as fs from 'fs' +import * as yaml from 'js-yaml' +import log from '../log' import { FlagsType } from '../types' export type FunctionsFlags = FlagsType +export const DefaultFunctionConfig = { + name: '', + manifest: 'manifest.yaml', + function: 'src/function.ts', + 'build-directory': './build', + 'types-directory': './src/types', +} as const + export type FunctionConfig = { name: string manifest: string function: string - buildDirectory: string + 'build-directory': string + 'types-directory': string } export default class Functions extends Command { @@ -17,19 +29,12 @@ export default class Functions extends Command { throw new Error('Method not implemented.') } - static override description = 'Authenticate with Mimic by storing your API key locally' - - static override examples = [ - '<%= config.bin %> <%= command.id %>', - '<%= config.bin %> <%= command.id %> --profile staging', - '<%= config.bin %> <%= command.id %> --profile production --api-key YOUR_API_KEY', - ] + static override description = 'Filters tasks based on a mimic.yaml configuration file' static MIMIC_CONFIG_FILE = 'mimic.yaml' static flags = { 'config-file': Flags.string({ - char: 'f', description: `Path to the ${Functions.MIMIC_CONFIG_FILE} file, this overrides other parameters like build-directory and function`, default: Functions.MIMIC_CONFIG_FILE, }), @@ -45,14 +50,42 @@ export default class Functions extends Command { }), } - public static filterFunctions(cmd: Command, flags: FunctionsFlags): FunctionConfig[] { - return [ - { - name: 'Pepe', - manifest: 'pepito.yaml', - function: 'src/pepe.ts', - buildDirectory: 'build/pepe', - }, - ] + public static async runFunctions>( + cmd: Command, + flags: T, + cmdLogic: (cmd: Command, flags: T) => Promise, + cmdActions: string + ): Promise { + const functions = Functions.filterFunctions(cmd, flags) + for (const func of functions) { + log.startAction(`Starting ${cmdActions} for function ${func.name}`) + await cmdLogic(cmd, { ...flags, ...func } as T) + } + } + + public static filterFunctions(cmd: Command, flags: FunctionsFlags & Partial): FunctionConfig[] { + if (!fs.existsSync(flags['config-file'])) { + // If doesn't exists return the default with the flags the user added + return [{ ...DefaultFunctionConfig, ...flags }] + } + + // Read and parse YAML file + const fileContents = fs.readFileSync(flags['config-file'], 'utf8') + const config = yaml.load(fileContents) as { tasks: FunctionConfig[] } + + // Get all tasks + let tasks = config.tasks || [] + + // Apply include filter if specified + if (flags.include && flags.include.length > 0) { + tasks = tasks.filter((task) => flags.include!.includes(task.name)) + } + + // Apply exclude filter if specified + if (flags.exclude && flags.exclude.length > 0) { + tasks = tasks.filter((task) => !flags.exclude!.includes(task.name)) + } + + return tasks } } diff --git a/packages/cli/src/commands/test.ts b/packages/cli/src/commands/test.ts index e742f8e8..b12a6f39 100644 --- a/packages/cli/src/commands/test.ts +++ b/packages/cli/src/commands/test.ts @@ -5,6 +5,7 @@ import { execBinCommand } from '../lib/packageManager' import { FlagsType } from '../types' import Build from './build' +import Functions from './functions' export type TestFlags = FlagsType @@ -14,6 +15,7 @@ export default class Test extends Command { static override examples = ['<%= config.bin %> <%= command.id %> --directory ./tests'] static override flags = { + ...Functions.flags, ...Build.flags, directory: Flags.string({ char: 'd', description: 'Path to the testing directory', default: './tests' }), 'skip-build': Flags.boolean({ description: 'Skip build before testing', default: false }), @@ -21,7 +23,7 @@ export default class Test extends Command { public async run(): Promise { const { flags } = await this.parse(Test) - await Test.test(this, flags) + await Functions.runFunctions(this, flags, Test.test, 'testing') } public static async test(cmd: Command, flags: TestFlags): Promise { From b86be303bea407f3e4777e16536ea1f19b071004 Mon Sep 17 00:00:00 2001 From: Agustincito Date: Fri, 6 Feb 2026 09:20:28 -0300 Subject: [PATCH 3/6] Fix tests --- packages/cli/src/commands/codegen.ts | 4 ++-- packages/cli/src/commands/deploy.ts | 2 +- packages/cli/src/commands/init.ts | 4 ++-- packages/cli/src/commands/login.ts | 2 +- packages/cli/src/commands/test.ts | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/cli/src/commands/codegen.ts b/packages/cli/src/commands/codegen.ts index e359c2af..7255efc9 100644 --- a/packages/cli/src/commands/codegen.ts +++ b/packages/cli/src/commands/codegen.ts @@ -66,8 +66,8 @@ export default class Codegen extends Command { } if (!fs.existsSync(typesDir)) fs.mkdirSync(typesDir, { recursive: true }) - this.generateAbisCode(manifest, typesDir, manifestDir) - this.generateInputsCode(manifest, typesDir) + Codegen.generateAbisCode(manifest, typesDir, manifestDir) + Codegen.generateInputsCode(manifest, typesDir) log.stopAction() } diff --git a/packages/cli/src/commands/deploy.ts b/packages/cli/src/commands/deploy.ts index f8d62b71..d62f006d 100644 --- a/packages/cli/src/commands/deploy.ts +++ b/packages/cli/src/commands/deploy.ts @@ -71,7 +71,7 @@ export default class Deploy extends Command { } log.startAction('Uploading to Mimic Registry') - const CID = await this.uploadToRegistry(cmd, neededFiles, credentials, registryUrl) + const CID = await Deploy.uploadToRegistry(cmd, neededFiles, credentials, registryUrl) console.log(`IPFS CID: ${log.highlightText(CID)}`) log.stopAction() diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts index c6b0f6ef..ac25fe29 100644 --- a/packages/cli/src/commands/init.ts +++ b/packages/cli/src/commands/init.ts @@ -79,8 +79,8 @@ export default class Init extends Command { cmd.error(`Failed to clone template repository. Details: ${error}`) } - this.installDependencies(absDir) - this.runCodegen(absDir) + Init.installDependencies(absDir) + Init.runCodegen(absDir) log.stopAction() console.log('New project initialized!') } diff --git a/packages/cli/src/commands/login.ts b/packages/cli/src/commands/login.ts index 3ddd9df2..47e372a8 100644 --- a/packages/cli/src/commands/login.ts +++ b/packages/cli/src/commands/login.ts @@ -74,7 +74,7 @@ export default class Login extends Command { } } - this.saveAndConfirm(cmd, profileName || CredentialsManager.getDefaultProfileName(), apiKey, flags['force-login']) + Login.saveAndConfirm(cmd, profileName || CredentialsManager.getDefaultProfileName(), apiKey, flags['force-login']) } private static async saveAndConfirm( diff --git a/packages/cli/src/commands/test.ts b/packages/cli/src/commands/test.ts index b12a6f39..44a7bcaa 100644 --- a/packages/cli/src/commands/test.ts +++ b/packages/cli/src/commands/test.ts @@ -23,7 +23,7 @@ export default class Test extends Command { public async run(): Promise { const { flags } = await this.parse(Test) - await Functions.runFunctions(this, flags, Test.test, 'testing') + await Test.test(this, flags) } public static async test(cmd: Command, flags: TestFlags): Promise { @@ -31,7 +31,7 @@ export default class Test extends Command { const baseDir = path.resolve('./') const testPath = path.join(baseDir, directory) - if (!skipBuild) await Build.build(cmd, flags) + if (!skipBuild) await Functions.runFunctions(cmd, flags, Build.build, 'building') const result = execBinCommand('tsx', ['./node_modules/mocha/bin/mocha.js', `${testPath}/**/*.spec.ts`], baseDir) cmd.exit(result.status ?? 1) From b8a602748e8c639630afbdca94300a9bca07383a Mon Sep 17 00:00:00 2001 From: Agustincito Date: Fri, 6 Feb 2026 13:21:01 -0300 Subject: [PATCH 4/6] Improve parsing and add test --- packages/cli/src/commands/functions.ts | 47 ++- packages/cli/tests/commands/functions.spec.ts | 365 ++++++++++++++++++ 2 files changed, 399 insertions(+), 13 deletions(-) create mode 100644 packages/cli/tests/commands/functions.spec.ts diff --git a/packages/cli/src/commands/functions.ts b/packages/cli/src/commands/functions.ts index 0965510c..91d99d95 100644 --- a/packages/cli/src/commands/functions.ts +++ b/packages/cli/src/commands/functions.ts @@ -1,12 +1,25 @@ import { Command, Flags } from '@oclif/core' import * as fs from 'fs' import * as yaml from 'js-yaml' +import { z } from 'zod' import log from '../log' import { FlagsType } from '../types' export type FunctionsFlags = FlagsType +export const FunctionConfigSchema = z.object({ + name: z.string().min(1, 'Function name is required'), + manifest: z.string().min(1, 'Manifest path is required'), + function: z.string().min(1, 'Function path is required'), + 'build-directory': z.string().min(1, 'Build directory is required'), + 'types-directory': z.string().min(1, 'Types directory is required'), +}) + +export const MimicConfigSchema = z.object({ + tasks: z.array(FunctionConfigSchema).min(1, 'At least one task is required'), +}) + export const DefaultFunctionConfig = { name: '', manifest: 'manifest.yaml', @@ -42,11 +55,13 @@ export default class Functions extends Command { description: `When ${Functions.MIMIC_CONFIG_FILE} exists, only run tasks with these names (space-separated)`, multiple: true, exclusive: ['exclude'], + char: 'i', }), exclude: Flags.string({ description: `When ${Functions.MIMIC_CONFIG_FILE} exists, exclude tasks with these names (space-separated)`, multiple: true, exclusive: ['include'], + char: 'e', }), } @@ -69,23 +84,29 @@ export default class Functions extends Command { return [{ ...DefaultFunctionConfig, ...flags }] } - // Read and parse YAML file const fileContents = fs.readFileSync(flags['config-file'], 'utf8') - const config = yaml.load(fileContents) as { tasks: FunctionConfig[] } + const rawConfig = yaml.load(fileContents) - // Get all tasks - let tasks = config.tasks || [] + try { + const config = MimicConfigSchema.parse(rawConfig) - // Apply include filter if specified - if (flags.include && flags.include.length > 0) { - tasks = tasks.filter((task) => flags.include!.includes(task.name)) - } + let tasks = config.tasks || [] - // Apply exclude filter if specified - if (flags.exclude && flags.exclude.length > 0) { - tasks = tasks.filter((task) => !flags.exclude!.includes(task.name)) - } + if (flags.include && flags.include.length > 0) { + tasks = tasks.filter((task) => flags.include!.includes(task.name)) + } + + if (flags.exclude && flags.exclude.length > 0) { + tasks = tasks.filter((task) => !flags.exclude!.includes(task.name)) + } - return tasks + return tasks + } catch (error) { + if (error instanceof z.ZodError) { + const errors = error.errors.map((e) => `${e.path.join('.')}: ${e.message}`).join('\n') + cmd.error(`Invalid ${Functions.MIMIC_CONFIG_FILE} configuration:\n${errors}`, { code: 'InvalidConfig' }) + } + throw error + } } } diff --git a/packages/cli/tests/commands/functions.spec.ts b/packages/cli/tests/commands/functions.spec.ts new file mode 100644 index 00000000..cbe52cc6 --- /dev/null +++ b/packages/cli/tests/commands/functions.spec.ts @@ -0,0 +1,365 @@ +import { Command } from '@oclif/core' +import { expect } from 'chai' +import * as fs from 'fs' +import * as sinon from 'sinon' + +import Functions, { FunctionConfigSchema, MimicConfigSchema } from '../../src/commands/functions' + +describe('Functions', () => { + const basePath = `${__dirname}/../fixtures` + const configFilePath = `${basePath}/mimic.yaml` + const validConfig = { + tasks: [ + { + name: 'task1', + manifest: 'manifest.yaml', + function: 'src/function.ts', + 'build-directory': './build', + 'types-directory': './src/types', + }, + { + name: 'task2', + manifest: 'src/task2/manifest.yaml', + function: 'src/task2/function.ts', + 'build-directory': './build/task2', + 'types-directory': './src/task2/types', + }, + ], + } + + describe('FunctionConfigSchema', () => { + context('when all required fields are present', () => { + it('validates successfully', () => { + const config = validConfig.tasks[0] + expect(() => FunctionConfigSchema.parse(config)).to.not.throw() + }) + }) + + context('when required fields are missing', () => { + context('when name is missing', () => { + it('throws error', () => { + const config = { ...validConfig.tasks[0], name: '' } + expect(() => FunctionConfigSchema.parse(config)).to.throw() + }) + }) + + context('when manifest is missing', () => { + it('throws error', () => { + const config = { ...validConfig.tasks[0], manifest: '' } + expect(() => FunctionConfigSchema.parse(config)).to.throw() + }) + }) + context('when function is missing', () => { + it('throws error', () => { + const config = { ...validConfig.tasks[0], function: '' } + expect(() => FunctionConfigSchema.parse(config)).to.throw() + }) + }) + context('when build-directory is missing', () => { + it('throws error', () => { + const config = { ...validConfig.tasks[0], 'build-directory': '' } + expect(() => FunctionConfigSchema.parse(config)).to.throw() + }) + }) + context('when types-directory is missing', () => { + it('throws error', () => { + const config = { ...validConfig.tasks[0], 'types-directory': '' } + expect(() => FunctionConfigSchema.parse(config)).to.throw() + }) + }) + }) + }) + + describe('MimicConfigSchema', () => { + context('when config has valid tasks array', () => { + it('validates successfully with single task', () => { + const config = { tasks: [validConfig.tasks[0]] } + expect(() => MimicConfigSchema.parse(config)).to.not.throw() + }) + + it('validates successfully with multiple tasks', () => { + expect(() => MimicConfigSchema.parse(validConfig)).to.not.throw() + }) + }) + + context('when tasks array is empty', () => { + it('throws validation error', () => { + const config = { tasks: [] } + expect(() => MimicConfigSchema.parse(config)).to.throw() + }) + }) + + context('when tasks array is missing', () => { + it('throws validation error', () => { + const config = {} + expect(() => MimicConfigSchema.parse(config)).to.throw() + }) + }) + + context('when a task in the array is invalid', () => { + it('throws validation error for invalid task', () => { + const config = { + tasks: [ + validConfig.tasks[0], + { ...validConfig.tasks[1], name: '' }, // Invalid: empty name + ], + } + expect(() => MimicConfigSchema.parse(config)).to.throw() + }) + }) + }) + + describe('filterFunctions', () => { + let cmdStub: sinon.SinonStubbedInstance + + beforeEach(() => { + cmdStub = sinon.createStubInstance(Command) + }) + + context('when config file does not exist', () => { + const flags = { + 'config-file': `${basePath}/nonexistent-mimic.yaml`, + include: [], + exclude: [], + } + + context('when no flags are provided', () => { + it('returns default', () => { + const result = Functions.filterFunctions(cmdStub, flags) + + expect(result).to.have.lengthOf(1) + expect(result[0].name).to.equal('') + expect(result[0].manifest).to.equal('manifest.yaml') + expect(result[0].function).to.equal('src/function.ts') + expect(result[0]['build-directory']).to.equal('./build') + expect(result[0]['types-directory']).to.equal('./src/types') + }) + }) + + context('when flags are provided', () => { + it('returns default config with overridden manifest', () => { + const result = Functions.filterFunctions(cmdStub, { + 'config-file': `${basePath}/nonexistent-mimic.yaml`, + manifest: 'custom-manifest.yaml', + include: [], + exclude: [], + }) + + expect(result[0].manifest).to.equal('custom-manifest.yaml') + }) + + it('returns default config with overridden types-directory', () => { + const result = Functions.filterFunctions(cmdStub, { + 'config-file': `${basePath}/nonexistent-mimic.yaml`, + 'types-directory': './custom/types', + include: [], + exclude: [], + }) + + expect(result[0]['types-directory']).to.equal('./custom/types') + }) + + it('returns default config with overridden build-directory', () => { + const result = Functions.filterFunctions(cmdStub, { + 'config-file': `${basePath}/nonexistent-mimic.yaml`, + 'build-directory': './custom/build', + include: [], + exclude: [], + }) + + expect(result[0]['build-directory']).to.equal('./custom/build') + }) + + it('returns default config with overridden function', () => { + const result = Functions.filterFunctions(cmdStub, { + 'config-file': `${basePath}/nonexistent-mimic.yaml`, + function: 'src/custom/function.ts', + include: [], + exclude: [], + }) + + expect(result[0].function).to.equal('src/custom/function.ts') + }) + }) + }) + + context('when config file exists', () => { + beforeEach(() => { + fs.mkdirSync(basePath, { recursive: true }) + fs.writeFileSync( + configFilePath, + ` +tasks: + - name: task1 + manifest: manifest.yaml + function: src/function.ts + build-directory: ./build + types-directory: ./src/types + - name: task2 + manifest: src/task2/manifest.yaml + function: src/task2/function.ts + build-directory: ./build/task2 + types-directory: ./src/task2/types + ` + ) + }) + + afterEach(() => { + if (fs.existsSync(configFilePath)) fs.unlinkSync(configFilePath) + }) + + context('when config is valid', () => { + it('returns all tasks', () => { + const flags = { + 'config-file': configFilePath, + include: [], + exclude: [], + } + + const result = Functions.filterFunctions(cmdStub, flags) + + expect(result).to.have.lengthOf(2) + expect(result[0].name).to.equal('task1') + expect(result[1].name).to.equal('task2') + }) + + context('when include filter is provided', () => { + it('returns only included tasks', () => { + const flags = { + 'config-file': configFilePath, + include: ['task1'], + exclude: [], + } + + const result = Functions.filterFunctions(cmdStub, flags) + + expect(result).to.have.lengthOf(1) + expect(result[0].name).to.equal('task1') + }) + + it('returns multiple included tasks', () => { + const flags = { + 'config-file': configFilePath, + include: ['task1', 'task2'], + exclude: [], + } + + const result = Functions.filterFunctions(cmdStub, flags) + + expect(result).to.have.lengthOf(2) + }) + + it('returns empty array when included task does not exist', () => { + const flags = { + 'config-file': configFilePath, + include: ['nonexistent'], + exclude: [], + } + + const result = Functions.filterFunctions(cmdStub, flags) + + expect(result).to.have.lengthOf(0) + }) + }) + + context('when exclude filter is provided', () => { + it('excludes specified tasks', () => { + const flags = { + 'config-file': configFilePath, + include: [], + exclude: ['task1'], + } + + const result = Functions.filterFunctions(cmdStub, flags) + + expect(result).to.have.lengthOf(1) + expect(result[0].name).to.equal('task2') + }) + + it('excludes multiple tasks', () => { + const flags = { + 'config-file': configFilePath, + include: [], + exclude: ['task1', 'task2'], + } + + const result = Functions.filterFunctions(cmdStub, flags) + + expect(result).to.have.lengthOf(0) + }) + + it('returns all tasks when excluding non-existent task', () => { + const flags = { + 'config-file': configFilePath, + include: [], + exclude: ['nonexistent'], + } + + const result = Functions.filterFunctions(cmdStub, flags) + + expect(result).to.have.lengthOf(2) + }) + }) + + context('when config is invalid', () => { + beforeEach(() => { + fs.writeFileSync( + configFilePath, + `tasks: + - name: task1 + manifest: manifest.yaml` + ) + }) + + it('throws error with validation message', () => { + const flags = { + 'config-file': configFilePath, + include: [], + exclude: [], + } + + expect(() => Functions.filterFunctions(cmdStub, flags)).to.throw() + expect(cmdStub.error.calledOnce).to.be.true + }) + + it('displays helpful error message for missing fields', () => { + const flags = { + 'config-file': configFilePath, + include: [], + exclude: [], + } + + try { + Functions.filterFunctions(cmdStub, flags) + } catch { + expect(cmdStub.error.calledOnce).to.be.true + const errorCall = cmdStub.error.getCall(0) + expect(errorCall.args[0]).to.include('Invalid mimic.yaml configuration') + } + }) + }) + + context('when YAML is malformed', () => { + beforeEach(() => { + fs.writeFileSync( + configFilePath, + `tasks: + - name: task1 + invalid yaml: [` + ) + }) + + it('throws error when parsing YAML', () => { + const flags = { + 'config-file': configFilePath, + include: [], + exclude: [], + } + + expect(() => Functions.filterFunctions(cmdStub, flags)).to.throw() + }) + }) + }) + }) + }) +}) From b3157ad922a292d77a67a5845ae1aa4671b4ba67 Mon Sep 17 00:00:00 2001 From: Agustincito Date: Fri, 6 Feb 2026 15:52:56 -0300 Subject: [PATCH 5/6] Add --no-config --- packages/cli/src/commands/functions.ts | 12 +++++++ packages/cli/tests/commands/functions.spec.ts | 36 ++++++++++++++++++- 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/commands/functions.ts b/packages/cli/src/commands/functions.ts index 91d99d95..22c6666d 100644 --- a/packages/cli/src/commands/functions.ts +++ b/packages/cli/src/commands/functions.ts @@ -51,6 +51,10 @@ export default class Functions extends Command { description: `Path to the ${Functions.MIMIC_CONFIG_FILE} file, this overrides other parameters like build-directory and function`, default: Functions.MIMIC_CONFIG_FILE, }), + 'no-config': Flags.boolean({ + description: `Do not read ${Functions.MIMIC_CONFIG_FILE}; use defaults and explicit flags instead`, + default: false, + }), include: Flags.string({ description: `When ${Functions.MIMIC_CONFIG_FILE} exists, only run tasks with these names (space-separated)`, multiple: true, @@ -79,7 +83,15 @@ export default class Functions extends Command { } public static filterFunctions(cmd: Command, flags: FunctionsFlags & Partial): FunctionConfig[] { + if (flags['no-config']) { + return [{ ...DefaultFunctionConfig, ...flags }] + } + if (!fs.existsSync(flags['config-file'])) { + if (flags['config-file'] !== Functions.MIMIC_CONFIG_FILE) { + cmd.error(`Could not find ${flags['config-file']}`, { code: 'ConfigNotFound' }) + } + // If doesn't exists return the default with the flags the user added return [{ ...DefaultFunctionConfig, ...flags }] } diff --git a/packages/cli/tests/commands/functions.spec.ts b/packages/cli/tests/commands/functions.spec.ts index cbe52cc6..886f2d2e 100644 --- a/packages/cli/tests/commands/functions.spec.ts +++ b/packages/cli/tests/commands/functions.spec.ts @@ -5,7 +5,7 @@ import * as sinon from 'sinon' import Functions, { FunctionConfigSchema, MimicConfigSchema } from '../../src/commands/functions' -describe('Functions', () => { +describe.only('Functions', () => { const basePath = `${__dirname}/../fixtures` const configFilePath = `${basePath}/mimic.yaml` const validConfig = { @@ -181,6 +181,20 @@ describe('Functions', () => { expect(result[0].function).to.equal('src/custom/function.ts') }) }) + + context('when a non-default config path is provided', () => { + it('throws an error', () => { + const customFlags = { + 'config-file': `${basePath}/custom-mimic.yaml`, + include: [], + exclude: [], + } + + cmdStub.error.throws(new Error('ConfigNotFound')) + expect(() => Functions.filterFunctions(cmdStub, customFlags)).to.throw('ConfigNotFound') + expect(cmdStub.error.calledOnce).to.be.true + }) + }) }) context('when config file exists', () => { @@ -209,6 +223,26 @@ tasks: }) context('when config is valid', () => { + context('when --no-config is provided', () => { + it('returns default config without reading the file', () => { + const flags = { + 'config-file': configFilePath, + 'no-config': true, + include: [], + exclude: [], + } + + const result = Functions.filterFunctions(cmdStub, flags) + + expect(result).to.have.lengthOf(1) + expect(result[0].name).to.equal('') + expect(result[0].manifest).to.equal('manifest.yaml') + expect(result[0].function).to.equal('src/function.ts') + expect(result[0]['build-directory']).to.equal('./build') + expect(result[0]['types-directory']).to.equal('./src/types') + }) + }) + it('returns all tasks', () => { const flags = { 'config-file': configFilePath, From a4e4b5d2ecf5e8562949c3d4d484a0e1cb830f2f Mon Sep 17 00:00:00 2001 From: Agustincito Date: Fri, 6 Feb 2026 15:54:23 -0300 Subject: [PATCH 6/6] Remove .only --- packages/cli/tests/commands/functions.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/tests/commands/functions.spec.ts b/packages/cli/tests/commands/functions.spec.ts index 886f2d2e..ab0f31d5 100644 --- a/packages/cli/tests/commands/functions.spec.ts +++ b/packages/cli/tests/commands/functions.spec.ts @@ -5,7 +5,7 @@ import * as sinon from 'sinon' import Functions, { FunctionConfigSchema, MimicConfigSchema } from '../../src/commands/functions' -describe.only('Functions', () => { +describe('Functions', () => { const basePath = `${__dirname}/../fixtures` const configFilePath = `${basePath}/mimic.yaml` const validConfig = {