import { createRequire } from 'node:module' import { Command, Option } from 'commander' import { ASSET_TYPES, type AssetAccess, type AssetType } from './schemas.js' import { installCommand } from './commands/install.js' import { searchCommand } from './commands/search.js' import { agentCommand } from './commands/agent.js' import { generateCommand } from './commands/generate.js' import { generateInstallCommand } from './commands/generate-install.js' import { listCommand } from './commands/list.js' import { packCommand } from './commands/pack.js' import { syncCommand } from './commands/sync.js' import { previewCommand } from './commands/preview.js' import { urlsCommand } from './commands/urls.js' import { uploadCommand } from './commands/upload.js' import { typesCommand } from './commands/types.js' import { cliMarketSkill } from './skill.generated.js' export interface MarketCommandOptions { name?: string invocation?: string version?: string } export function createMarketCommand(options: MarketCommandOptions = {}): Command { const program = new Command() const invocation = options.invocation ?? options.name ?? 'market' /** Accumulate a repeatable option into an array. */ const collect = (value: string, previous: string[]): string[] => previous.concat(value) const typeOption = new Option('--type ', 'Asset type').choices([...ASSET_TYPES]) const referenceImageOption = new Option( '--reference-image ', 'Reference image the result should match: file path or http(s) URL (repeatable)', ) .argParser(collect) .default([]) // Omitted → the server picks the default from your entitlement (private if you hold market:private, // else public). `--access private` requires that entitlement. const accessOption = new Option('--access ', 'Asset visibility').choices([ 'public', 'private', ]) const apiOption = new Option('--api ', 'API URL').default( process.env.MARKET_API_URL, 'from MARKET_API_URL / config / default', ) program .name(options.name ?? 'market') .description('Find and use Drawcall Market assets') .version(options.version ?? readPackageVersion()) .enablePositionalOptions() .addHelpText( 'after', `\nAI agents that have not already seen the Market skill should run \`${invocation} skill\` before using this CLI.`, ) program .command('skill') .description('Print the Market agent skill') .action(() => { process.stdout.write(cliMarketSkill) }) program .command('types') .description('List current asset types, generation support, and search guidance') .addOption(apiOption) .action(async (opts: { api?: string }) => { await typesCommand(opts.api) }) program .command('install') .description( 'Install assets by name, or package.json assetDependencies when no names are given', ) .argument('[assets...]', 'Asset names, optionally with @range') .addOption(apiOption) .option('--unapproved', 'Include unapproved versions', false) .option('--force', 'Overwrite existing files', false) .option('--key ', 'Access key of a shared private asset (no account needed)') .option('--cwd ', 'Project directory') .option( '--redirect ', 'Do not download asset files destined for this static root (e.g. public); write /_redirects rules to their canonical URLs instead. Repeatable.', collect, [], ) .action( async ( args: string[], opts: { api?: string unapproved: boolean force: boolean key?: string cwd?: string redirect: string[] }, ) => { await installCommand(args, { baseUrl: opts.api, unapproved: opts.unapproved, force: opts.force, key: opts.key, cwd: opts.cwd, redirect: opts.redirect, }) }, ) program .command('list') .description('List asset dependencies declared in package.json, including file aliases') .option('--cwd ', 'Project directory') .action(async (opts: { cwd?: string }) => { await listCommand({ cwd: opts.cwd, }) }) program .command('sync') .description( 'Update package.json assetDependencies aliases to match where installed files live now', ) .addOption(apiOption) .option('--cwd ', 'Project directory') .action(async (opts: { api?: string; cwd?: string }) => { await syncCommand({ baseUrl: opts.api, cwd: opts.cwd, }) }) program .command('pack') .description('Create a Market asset zip using the same packaging step as upload') .argument('', 'project directory, .zip path, or glob') .addOption(typeOption) .option('--out ', 'Output zip path') .option('--cwd ', 'Project directory') .option('--npm ', 'npm dependency name@range (repeatable)', collect, []) .option('--asset ', 'asset dependency name@range (repeatable)', collect, []) .option('--skill ', 'skill dependency label=source (repeatable)', collect, []) .action( async ( zipFilter: string, opts: { type?: AssetType out?: string cwd?: string npm: string[] asset: string[] skill: string[] }, ) => { await packCommand(zipFilter, { type: opts.type, out: opts.out, cwd: opts.cwd, npm: opts.npm, asset: opts.asset, skill: opts.skill, }) }, ) program .command('search') .description('Find assets') .argument('', 'Search query') .addOption(typeOption) .addOption(apiOption) .option('--unapproved', 'Include unapproved versions', false) .option('--limit ', 'Max results, 1-5', parseSearchLimit, 5) .option('--json', 'Emit a machine-readable JSON array instead of the summary lines', false) .action( async ( query: string, opts: { type?: AssetType api?: string unapproved: boolean limit: number json: boolean }, ) => { requireType(opts.type, 'Search') await searchCommand(query, { type: opts.type, baseUrl: opts.api, unapproved: opts.unapproved, limit: opts.limit, json: opts.json, }) }, ) const agent = program .command('agent') .description('Plan, find, judge and generate a set of assets from a goal') .argument('[goal]', 'What you need, in plain language') .addOption(apiOption) .addOption(referenceImageOption) .option('--json', 'Output JSON', false) .action( async ( goal: string | undefined, opts: { api?: string; referenceImage: string[]; json: boolean }, ) => { if (!goal) { agent.help({ error: true }) return } await agentCommand(goal, { baseUrl: opts.api, referenceImages: opts.referenceImage, json: opts.json, }) }, ) program .command('upload') .description('Publish one asset') .argument('', 'Asset name') .argument('', '.zip path or glob') .argument('', 'Short description') .addOption(typeOption) .addOption(apiOption) .option('--version ', 'Explicit semver version') .option('--cwd ', 'Project directory') .option('--npm ', 'npm dependency name@range (repeatable)', collect, []) .option('--asset ', 'asset dependency name@range (repeatable)', collect, []) .option('--skill ', 'skill dependency label=source (repeatable)', collect, []) .addOption(accessOption) .action( async ( name: string, zipFilter: string, description: string, opts: { type?: AssetType api?: string version?: string cwd?: string npm: string[] asset: string[] skill: string[] access?: AssetAccess }, ) => { requireType(opts.type, 'Upload') await uploadCommand(name, zipFilter, description, { type: opts.type, version: opts.version, baseUrl: opts.api, cwd: opts.cwd, npm: opts.npm, asset: opts.asset, skill: opts.skill, access: opts.access, }) }, ) program .command('preview') .description("Save an asset's preview image") .argument('', 'Asset name, optionally with @version') .addOption(apiOption) .option('--unapproved', 'Include unapproved versions', false) .option('--out ', 'Output image path') .action(async (name: string, opts: { api?: string; unapproved: boolean; out?: string }) => { await previewCommand(name, { baseUrl: opts.api, unapproved: opts.unapproved, out: opts.out, }) }) program .command('urls') .description("Print an asset's remote file base URL, subpaths, and preview URL") .argument('', 'Asset name, optionally with @version') .addOption(apiOption) .option('--unapproved', 'Include unapproved versions', false) .option('--key ', 'Access key of a shared private asset (no account needed)') .action(async (ref: string, opts: { api?: string; unapproved: boolean; key?: string }) => { await urlsCommand(ref, { baseUrl: opts.api, unapproved: opts.unapproved, key: opts.key, }) }) const generate = program .command('generate') .description('Generate and install an asset, or check a generation job') .argument('[description]', 'Asset prompt (omit when using a subcommand)') .addOption(typeOption) .addOption(apiOption) .addOption(referenceImageOption) .option('--cwd ', 'Project directory') .addOption(accessOption) .action( async ( description: string | undefined, opts: { type?: AssetType api?: string referenceImage: string[] cwd?: string access?: AssetAccess }, ) => { if (!description) { generate.help({ error: true }) return } requireType(opts.type, 'Generate') await generateCommand(description, { type: opts.type, baseUrl: opts.api, referenceImages: opts.referenceImage, cwd: opts.cwd, access: opts.access, }) }, ) // `generate install ` finishes a slow (job-based) generation: it checks the job once and, when // it has completed, installs the produced asset into the project. Still running → prints a note and // exits 0 (run again later); failed → exits 1. Fast asset types never need this — `generate` installs // them inline. ("install" over "status": the command's job is to integrate the asset, not just report.) generate .command('install') .description('Install the asset from a generation job once it has completed') .argument('', `Job id printed by \`${invocation} generate\``) .addOption(apiOption) .option('--cwd ', 'Project directory') // Read merged options: `--cwd`/`--api` after `generate install` are otherwise captured by the // parent `generate` command (which declares the same options), leaving this subcommand's own opts // undefined. `optsWithGlobals()` surfaces whichever level parsed them. .action(async (jobId: string, _options, command: Command) => { const { api, cwd } = command.optsWithGlobals() await generateInstallCommand(jobId, { baseUrl: api, cwd }) }) return program } function requireType(type: AssetType | undefined, command: string): asserts type is AssetType { if (!type) { throw new Error(`${command} requires --type. Available types: ${ASSET_TYPES.join(', ')}`) } } function parseSearchLimit(value: string): number { const parsed = Number.parseInt(value, 10) if (!Number.isInteger(parsed) || parsed < 1) { throw new Error('--limit must be a positive integer') } return Math.min(parsed, 5) } function readPackageVersion(): string { const value: unknown = createRequire(import.meta.url)('../package.json') if ( !value || typeof value !== 'object' || Array.isArray(value) || !('version' in value) || typeof value.version !== 'string' ) { throw new Error('Market package.json is missing a valid version') } return value.version }