From bab11ae3faa72dd8c3b8ae6454596279284b200c Mon Sep 17 00:00:00 2001 From: Kim T Date: Wed, 17 Dec 2025 22:32:22 -0800 Subject: [PATCH 1/6] Update @open-audio-stack/core, use shared spinner/json method to format cli output --- package-lock.json | 34 +++++++++++++++++++++--- package.json | 2 +- src/commands/config.ts | 52 +++++++++++++++++++++++++------------ src/commands/create.ts | 21 +++++++++++---- src/commands/filter.ts | 29 ++++++++++++++++++--- src/commands/get.ts | 25 +++++++++++++----- src/commands/install.ts | 29 +++++++++++---------- src/commands/list.ts | 18 ++++++++++--- src/commands/open.ts | 24 +++++++++-------- src/commands/scan.ts | 18 ++++++++++--- src/commands/search.ts | 18 ++++++++++--- src/commands/sync.ts | 28 ++++++++++---------- src/commands/uninstall.ts | 30 ++++++++++++---------- src/index.ts | 16 +++++++++++- src/utils.ts | 54 +++++++++++++++++++++++++++++++++++++++ 15 files changed, 299 insertions(+), 99 deletions(-) diff --git a/package-lock.json b/package-lock.json index a77ea0d..df914a0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "3.0.5", "license": "MIT", "dependencies": { - "@open-audio-stack/core": "^0.1.45", + "@open-audio-stack/core": "^0.1.47", "cli-table3": "^0.6.5", "commander": "^12.1.0", "ora": "^9.0.0" @@ -1291,9 +1291,9 @@ } }, "node_modules/@open-audio-stack/core": { - "version": "0.1.45", - "resolved": "https://registry.npmjs.org/@open-audio-stack/core/-/core-0.1.45.tgz", - "integrity": "sha512-4wXmURbj/B03e6EDzOjoZJUe1kVGQQvFis9BmyY7ZykjDwe1PzMZLI2cp1Uv5nbsKmdDQXn684/8iw62FaTmPA==", + "version": "0.1.47", + "resolved": "https://registry.npmjs.org/@open-audio-stack/core/-/core-0.1.47.tgz", + "integrity": "sha512-XrUpZFFVdAudjSa9E3cfWlDZlTXU7GWNGNxo2n/+aKwHmlgStLHvNPRm2pr1uxJBQWloSILFLQgIcWd2VBB+ig==", "license": "cc0-1.0", "dependencies": { "@vscode/sudo-prompt": "^9.3.1", @@ -1304,6 +1304,7 @@ "glob": "^11.0.0", "inquirer": "^12.4.1", "js-yaml": "^4.1.0", + "mime-types": "^3.0.2", "semver": "^7.6.3", "slugify": "^1.6.6", "tar": "^7.4.3", @@ -3625,6 +3626,31 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/mimic-function": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", diff --git a/package.json b/package.json index ac9c51c..e280aa2 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "node": ">=18" }, "dependencies": { - "@open-audio-stack/core": "^0.1.45", + "@open-audio-stack/core": "^0.1.47", "cli-table3": "^0.6.5", "commander": "^12.1.0", "ora": "^9.0.0" diff --git a/src/commands/config.ts b/src/commands/config.ts index 324e907..0df7f21 100644 --- a/src/commands/config.ts +++ b/src/commands/config.ts @@ -2,6 +2,7 @@ import { Command } from 'commander'; import { CliOptions } from '../types/options.js'; import { ConfigInterface, ConfigLocal, isTests } from '@open-audio-stack/core'; import { CONFIG_LOCAL_TEST } from '../data/Config.js'; +import { withSpinner } from '../utils.js'; const config: ConfigLocal = new ConfigLocal(isTests() ? CONFIG_LOCAL_TEST : undefined); const program = new Command(); @@ -13,14 +14,23 @@ configCmd .option('-l, --log', 'Enable logging') .description('Get a config setting by key') .action((key: keyof ConfigInterface, options: CliOptions) => { - if (options.log) config.logEnable(); - if (options.json) { - const obj: any = {}; - obj[key] = config.get(key); - console.log({ key }); - } else { - console.log(config.get(key)); - } + return withSpinner( + options, + config as any, + `Get config ${String(key)}`, + async () => { + return { key, value: config.get(key) }; + }, + (result: any, useJson: boolean) => { + if (useJson) { + const obj: any = {}; + obj[result.key] = result.value; + console.log(JSON.stringify(obj, null, 2)); + } else { + console.log(result.value); + } + }, + ); }); configCmd @@ -29,12 +39,22 @@ configCmd .option('-l, --log', 'Enable logging') .description('Set a config setting by key and value') .action((key: keyof ConfigInterface, val: any, options: CliOptions) => { - // if (options.log) config.logEnable(); - if (options.json) { - const obj: any = {}; - obj[key] = config.set(key, val); - console.log(obj); - } else { - console.log(config.set(key, val)); - } + return withSpinner( + options, + config as any, + `Set config ${String(key)}`, + async () => { + const res = config.set(key, val); + return { key, value: res }; + }, + (result: any, useJson: boolean) => { + if (useJson) { + const obj: any = {}; + obj[result.key] = result.value; + console.log(JSON.stringify(obj, null, 2)); + } else { + console.log(result.value); + } + }, + ); }); diff --git a/src/commands/create.ts b/src/commands/create.ts index 00efb84..6eac520 100644 --- a/src/commands/create.ts +++ b/src/commands/create.ts @@ -1,16 +1,27 @@ import { Command } from 'commander'; import { CliOptions } from '../types/options.js'; import { ManagerLocal } from '@open-audio-stack/core'; +import { withSpinner } from '../utils.js'; export function create(command: Command, manager: ManagerLocal) { command - .command('create ') + .command('create') + .option('-j, --json', 'Output results as json') .option('-l, --log', 'Enable logging') .description('Create a new package locally') .action(async (path: string, options: CliOptions) => { - if (options.log) manager.logEnable(); - else manager.logDisable(); - console.log(await manager.create()); - console.log(path); + await withSpinner( + options, + manager, + `Create package at ${path}`, + async () => { + const result = await manager.create(); + return result; + }, + (result: any, useJson: boolean) => { + if (useJson) console.log(JSON.stringify(result, null, 2)); + else console.log(result); + }, + ); }); } diff --git a/src/commands/filter.ts b/src/commands/filter.ts index 646782d..f1149b5 100644 --- a/src/commands/filter.ts +++ b/src/commands/filter.ts @@ -1,16 +1,37 @@ import { Command } from 'commander'; import { CliOptions } from '../types/options.js'; import { ManagerLocal, PackageVersion } from '@open-audio-stack/core'; -import { formatOutput } from '../utils.js'; +import { formatOutput, withSpinner } from '../utils.js'; export function filter(command: Command, manager: ManagerLocal) { command .command('filter ') + .option('-j, --json', 'Output results as json') .option('-l, --log', 'Enable logging') .description('Filter the by field and matching value') .action((field: keyof PackageVersion, value: string, options: CliOptions) => { - if (options.log) manager.logEnable(); - else manager.logDisable(); - console.log(formatOutput(manager.filter(pkgVersion => pkgVersion[field] === value))); + return withSpinner( + options, + manager, + `Filter ${manager.type} by ${String(field)}=${value}`, + async () => { + const predicate = (pkgVersion: PackageVersion) => { + const fieldVal: any = (pkgVersion as any)[field]; + if (Array.isArray(fieldVal)) { + return fieldVal.some((v: any) => String(v).toLowerCase() === value.toLowerCase()); + } + if (typeof fieldVal === 'string') { + return fieldVal.toLowerCase().includes(value.toLowerCase()); + } + if (fieldVal === undefined || fieldVal === null) return false; + return String(fieldVal) === value; + }; + return manager.filter(predicate); + }, + (result: any, useJson: boolean) => { + if (useJson) console.log(JSON.stringify(result, null, 2)); + else console.log(formatOutput(result as any)); + }, + ); }); } diff --git a/src/commands/get.ts b/src/commands/get.ts index 73fcc99..0ee8af8 100644 --- a/src/commands/get.ts +++ b/src/commands/get.ts @@ -1,19 +1,30 @@ import { Command } from 'commander'; import { CliOptions } from '../types/options.js'; import { inputGetParts, ManagerLocal } from '@open-audio-stack/core'; -import { formatOutput } from '../utils.js'; +import { formatOutput, withSpinner } from '../utils.js'; export function get(command: Command, manager: ManagerLocal) { command .command('get ') + .option('-j, --json', 'Output results as json') .option('-l, --log', 'Enable logging') .description('Get package metadata from registry') .action((input: string, options: CliOptions) => { - if (options.log) manager.logEnable(); - else manager.logDisable(); - const [slug, version] = inputGetParts(input); - const pkg = manager.getPackage(slug); - const versions = version ? [version] : Array.from(pkg?.versions.keys() || new Map().keys()); - console.log(formatOutput(pkg, versions)); + return withSpinner( + options, + manager, + `Get package ${input}`, + async () => { + const [slug, version] = inputGetParts(input); + const pkg = manager.getPackage(slug); + if (!pkg) throw new Error(`No package found with slug: ${slug}`); + return { pkg, version }; + }, + (result: any, useJson: boolean) => { + const { pkg, version } = result; + const versions = version ? [version] : Array.from(pkg.versions.keys()); + console.log(formatOutput(pkg, versions, useJson)); + }, + ); }); } diff --git a/src/commands/install.ts b/src/commands/install.ts index 45707c0..448650d 100644 --- a/src/commands/install.ts +++ b/src/commands/install.ts @@ -1,26 +1,29 @@ import { Command } from 'commander'; -import ora from 'ora'; import { CliOptions } from '../types/options.js'; import { inputGetParts, ManagerLocal, isTests } from '@open-audio-stack/core'; +import { withSpinner } from '../utils.js'; export function install(command: Command, manager: ManagerLocal) { command .command('install ') + .option('-j, --json', 'Output results as json') .option('-l, --log', 'Enable logging') .description('Install a package locally by slug/version') .action(async (input: string, options: CliOptions) => { - if (options.log) manager.logEnable(); - else manager.logDisable(); const [slug, version] = inputGetParts(input); - const spinner = ora(`Installing ${slug}${version ? `@${version}` : ''}...`).start(); - try { - await manager.install(slug, version); - spinner.succeed(`Installed ${slug}${version ? `@${version}` : ''}`); - if (isTests()) console.log(`Installed ${slug}${version ? `@${version}` : ''}`); - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - spinner.fail(errorMessage); - if (isTests()) console.log(errorMessage); - } + await withSpinner( + options, + manager, + `Install ${slug}${version ? `@${version}` : ''}`, + async () => { + await manager.install(slug, version); + return { slug, version: version || null, installed: true, isTests: isTests() }; + }, + (result: any, useJson: boolean) => { + if (useJson) + console.log(JSON.stringify({ slug: result.slug, version: result.version, installed: true }, null, 2)); + else if (result.isTests) console.log(`Installed ${result.slug}${result.version ? `@${result.version}` : ''}`); + }, + ); }); } diff --git a/src/commands/list.ts b/src/commands/list.ts index 60d83d2..20c6e8d 100644 --- a/src/commands/list.ts +++ b/src/commands/list.ts @@ -1,7 +1,7 @@ import { Command } from 'commander'; import { CliOptions } from '../types/options.js'; import { ManagerLocal } from '@open-audio-stack/core'; -import { formatOutput } from '../utils.js'; +import { formatOutput, withSpinner } from '../utils.js'; interface ListOptions extends CliOptions { incompatible: boolean; @@ -13,11 +13,21 @@ export function list(command: Command, manager: ManagerLocal) { .command('list') .option('-c, --incompatible', 'List incompatible packages') .option('-i, --installed', 'Only list installed packages') + .option('-j, --json', 'Output results as json') .option('-l, --log', 'Enable logging') .description('List packages') .action(async (options: ListOptions) => { - if (options.log) manager.logEnable(); - else manager.logDisable(); - console.log(formatOutput(manager.listPackages(options.installed, options.incompatible))); + await withSpinner( + options, + manager, + `List ${manager.type}`, + async () => { + return manager.listPackages(options.installed, options.incompatible); + }, + (result: any, useJson: boolean) => { + if (useJson) console.log(JSON.stringify(result, null, 2)); + else console.log(formatOutput(result as any)); + }, + ); }); } diff --git a/src/commands/open.ts b/src/commands/open.ts index c6d620d..c6ec2b4 100644 --- a/src/commands/open.ts +++ b/src/commands/open.ts @@ -1,6 +1,7 @@ import { Command } from 'commander'; import { CliOptions } from '../types/options.js'; import { inputGetParts, ManagerLocal } from '@open-audio-stack/core'; +import { withSpinner } from '../utils.js'; export function open(command: Command, manager: ManagerLocal) { command @@ -8,15 +9,18 @@ export function open(command: Command, manager: ManagerLocal) { .option('-l, --log', 'Enable logging') .description('Open a package by slug/version') .action(async (input: string, options: string[], cliOptions: CliOptions) => { - if (cliOptions.log) manager.logEnable(); - else manager.logDisable(); - const [slug, version] = inputGetParts(input); - try { - await manager.open(slug, version, options); - } catch (error: any) { - const errorMessage = error instanceof Error ? error.message : String(error); - console.error(errorMessage); - process.exit(1); - } + await withSpinner( + cliOptions, + manager, + `Open ${input}`, + async () => { + const [slug, version] = inputGetParts(input); + await manager.open(slug, version, options); + return { slug, version }; + }, + () => { + // open is an action side-effect; no additional output required + }, + ); }); } diff --git a/src/commands/scan.ts b/src/commands/scan.ts index 614f6c4..1a5c9b9 100644 --- a/src/commands/scan.ts +++ b/src/commands/scan.ts @@ -1,16 +1,26 @@ import { Command } from 'commander'; import { CliOptions } from '../types/options.js'; import { ManagerLocal } from '@open-audio-stack/core'; +import { withSpinner } from '../utils.js'; export function scan(command: Command, manager: ManagerLocal) { command .command('scan') + .option('-j, --json', 'Output results as json') .option('-l, --log', 'Enable logging') .description('Scan local packages into cache') .action(async (options: CliOptions) => { - if (options.log) manager.logEnable(); - else manager.logDisable(); - manager.scan(); - console.log(`${manager.type} scan completed`); + await withSpinner( + options, + manager, + `Scan ${manager.type}`, + async () => { + await manager.scan(); + return { type: manager.type, status: 'scan completed' }; + }, + (result: any, useJson: boolean) => { + if (useJson) console.log(JSON.stringify({ type: result.type, status: result.status }, null, 2)); + }, + ); }); } diff --git a/src/commands/search.ts b/src/commands/search.ts index 95ec2d7..5c7aeea 100644 --- a/src/commands/search.ts +++ b/src/commands/search.ts @@ -1,16 +1,26 @@ import { Command } from 'commander'; import { CliOptions } from '../types/options.js'; import { ManagerLocal } from '@open-audio-stack/core'; -import { formatOutput } from '../utils.js'; +import { formatOutput, withSpinner } from '../utils.js'; export function search(command: Command, manager: ManagerLocal) { command .command('search ') + .option('-j, --json', 'Output results as json') .option('-l, --log', 'Enable logging') .description('Search using a lazy matching query') .action(async (query: string, options: CliOptions) => { - if (options.log) manager.logEnable(); - else manager.logDisable(); - console.log(formatOutput(manager.search(query))); + await withSpinner( + options, + manager, + `Search ${manager.type}`, + async () => { + return manager.search(query); + }, + (result: any, useJson: boolean) => { + if (useJson) console.log(JSON.stringify(result, null, 2)); + else console.log(formatOutput(result as any)); + }, + ); }); } diff --git a/src/commands/sync.ts b/src/commands/sync.ts index f9ed680..721cb30 100644 --- a/src/commands/sync.ts +++ b/src/commands/sync.ts @@ -1,25 +1,27 @@ import { Command } from 'commander'; -import ora from 'ora'; import { CliOptions } from '../types/options.js'; import { ManagerLocal, isTests } from '@open-audio-stack/core'; +import { withSpinner } from '../utils.js'; export function sync(command: Command, manager: ManagerLocal) { command .command('sync') + .option('-j, --json', 'Output results as json') .option('-l, --log', 'Enable logging') .description('Sync remote packages into cache') .action(async (options: CliOptions) => { - if (options.log) manager.logEnable(); - else manager.logDisable(); - const spinner = ora(`Syncing ${manager.type}...`).start(); - try { - await manager.sync(); - spinner.succeed(`${manager.type} sync completed`); - if (isTests()) console.log(`${manager.type} sync completed`); - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - spinner.fail(errorMessage); - if (isTests()) console.log(errorMessage); - } + await withSpinner( + options, + manager, + `Sync ${manager.type}`, + async () => { + await manager.sync(); + return { type: manager.type, status: 'sync completed', isTests: isTests() }; + }, + (result: any, useJson: boolean) => { + if (useJson) console.log(JSON.stringify({ type: result.type, status: result.status }, null, 2)); + else if (isTests()) console.log(`${result.type} sync completed`); + }, + ); }); } diff --git a/src/commands/uninstall.ts b/src/commands/uninstall.ts index b2a1fc5..ec2229f 100644 --- a/src/commands/uninstall.ts +++ b/src/commands/uninstall.ts @@ -1,26 +1,30 @@ import { Command } from 'commander'; -import ora from 'ora'; import { CliOptions } from '../types/options.js'; import { inputGetParts, ManagerLocal, isTests } from '@open-audio-stack/core'; +import { withSpinner } from '../utils.js'; export function uninstall(command: Command, manager: ManagerLocal) { command .command('uninstall ') + .option('-j, --json', 'Output results as json') .option('-l, --log', 'Enable logging') .description('Uninstall a package locally by slug/version') .action(async (input: string, options: CliOptions) => { - if (options.log) manager.logEnable(); - else manager.logDisable(); const [slug, version] = inputGetParts(input); - const spinner = ora(`Uninstalling ${slug}${version ? `@${version}` : ''}...`).start(); - try { - await manager.uninstall(slug, version); - spinner.succeed(`Uninstalled ${slug}${version ? `@${version}` : ''}`); - if (isTests()) console.log(`Uninstalled ${slug}${version ? `@${version}` : ''}`); - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - spinner.fail(errorMessage); - if (isTests()) console.log(errorMessage); - } + await withSpinner( + options, + manager, + `Uninstall ${slug}${version ? `@${version}` : ''}`, + async () => { + await manager.uninstall(slug, version); + return { slug, version: version || null, installed: false, isTests: isTests() }; + }, + (result: any, useJson: boolean) => { + if (useJson) + console.log(JSON.stringify({ slug: result.slug, version: result.version, installed: false }, null, 2)); + else if (result.isTests) + console.log(`Uninstalled ${result.slug}${result.version ? `@${result.version}` : ''}`); + }, + ); }); } diff --git a/src/index.ts b/src/index.ts index af7855e..83370bb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -40,4 +40,18 @@ for (const type of types) { uninstall(command, manager); } -program.version('3.0.1').parse(process.argv); +import fs from 'fs'; +import path from 'path'; + +let version = '0.0.0'; +try { + const pkgPath = path.resolve(process.cwd(), 'package.json'); + const pkgRaw = fs.readFileSync(pkgPath, 'utf8'); + const pkg = JSON.parse(pkgRaw); + if (pkg && pkg.version) version = pkg.version; +} catch (e) { + console.error(e); + // ignore and fallback to default +} + +program.version(version).parse(process.argv); diff --git a/src/utils.ts b/src/utils.ts index 78f4448..f49d79b 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,5 +1,7 @@ import { Package } from '@open-audio-stack/core'; import CliTable3 from 'cli-table3'; +import ora from 'ora'; +import type { CliOptions } from './types/options.js'; export function formatOutput(result: Package[] | Package | undefined, versions?: string[], json?: boolean): string { if (!result) return `No results found`; @@ -59,3 +61,55 @@ export function truncateString(str: string, num: number) { return str; } } + +export async function withSpinner( + options: CliOptions | undefined, + manager: any, + spinnerMessage: string, + action: () => Promise | T, + print?: (result: T, useJson: boolean) => void, +) { + const useJson = Boolean(options && options.json); + const spinner = useJson ? undefined : ora(spinnerMessage).start(); + try { + if (manager) { + if (options && options.log) manager.logEnable(); + else manager.logDisable(); + } + const result = await action(); + if (spinner) spinner.succeed(spinnerMessage); + + const isStatusOnly = (val: any) => { + if (!val || typeof val !== 'object') return false; + const keys = Object.keys(val); + // status or type+status objects are treated as status-only + return keys.length <= 2 && keys.includes('status') && keys.every(k => k === 'status' || k === 'type'); + }; + + if (print) { + print(result, useJson); + } else { + if (useJson) { + console.log(JSON.stringify(result, null, 2)); + } else { + // When a spinner was used and the result is a simple status object + // (e.g. { type: 'plugins', status: 'scan completed' }), the spinner + // already displayed the human-readable status. Avoid printing it again. + if (spinner && isStatusOnly(result)) { + // nothing to print + } else { + console.log(result as any); + } + } + } + } catch (err) { + const errorMessage = err instanceof Error ? err.message : String(err); + if (spinner) { + spinner.fail(errorMessage); + } else { + if (useJson) console.log(JSON.stringify({ error: errorMessage }, null, 2)); + else console.error(errorMessage); + } + process.exit(1); + } +} From 4d7391188c1cccda7535034375b70c75b89f41b0 Mon Sep 17 00:00:00 2001 From: Kim T Date: Thu, 18 Dec 2025 23:03:12 -0800 Subject: [PATCH 2/6] Cleaner output approach, with snapshots --- src/commands/config.ts | 58 +++----- src/commands/create.ts | 29 ++-- src/commands/filter.ts | 48 +++---- src/commands/get.ts | 33 +++-- src/commands/install.ts | 31 ++-- src/commands/list.ts | 24 ++-- src/commands/open.ts | 25 ++-- src/commands/scan.ts | 25 ++-- src/commands/search.ts | 24 ++-- src/commands/sync.ts | 27 ++-- src/commands/uninstall.ts | 32 ++--- src/utils.ts | 134 ++++++++++++------ tests/__snapshots__/index.test.ts.snap | 6 +- .../__snapshots__/config.test.ts.snap | 43 ++++-- .../__snapshots__/create.test.ts.snap | 2 + .../__snapshots__/filter.test.ts.snap | 9 +- tests/commands/__snapshots__/get.test.ts.snap | 7 +- .../__snapshots__/install.test.ts.snap | 15 +- .../commands/__snapshots__/list.test.ts.snap | 4 +- .../commands/__snapshots__/open.test.ts.snap | 2 + .../commands/__snapshots__/scan.test.ts.snap | 5 +- .../__snapshots__/search.test.ts.snap | 6 +- .../commands/__snapshots__/sync.test.ts.snap | 5 +- .../__snapshots__/uninstall.test.ts.snap | 15 +- tests/commands/create.test.ts | 6 +- tests/commands/filter.test.ts | 2 +- tests/commands/install.test.ts | 6 +- tests/commands/open.test.ts | 15 +- tests/commands/uninstall.test.ts | 7 +- tests/shared.ts | 29 +++- 30 files changed, 383 insertions(+), 291 deletions(-) diff --git a/src/commands/config.ts b/src/commands/config.ts index 0df7f21..ee1b9a9 100644 --- a/src/commands/config.ts +++ b/src/commands/config.ts @@ -2,7 +2,7 @@ import { Command } from 'commander'; import { CliOptions } from '../types/options.js'; import { ConfigInterface, ConfigLocal, isTests } from '@open-audio-stack/core'; import { CONFIG_LOCAL_TEST } from '../data/Config.js'; -import { withSpinner } from '../utils.js'; +import { output, OutputType } from '../utils.js'; const config: ConfigLocal = new ConfigLocal(isTests() ? CONFIG_LOCAL_TEST : undefined); const program = new Command(); @@ -14,23 +14,17 @@ configCmd .option('-l, --log', 'Enable logging') .description('Get a config setting by key') .action((key: keyof ConfigInterface, options: CliOptions) => { - return withSpinner( - options, - config as any, - `Get config ${String(key)}`, - async () => { - return { key, value: config.get(key) }; - }, - (result: any, useJson: boolean) => { - if (useJson) { - const obj: any = {}; - obj[result.key] = result.value; - console.log(JSON.stringify(obj, null, 2)); - } else { - console.log(result.value); - } - }, - ); + { + const message = `Get config ${String(key)}`; + output(OutputType.START, message, options, config); + try { + const payload = options && options.json ? { key, value: config.get(key) } : String(config.get(key)); + output(OutputType.SUCCESS, payload, options, config); + } catch (err: any) { + output(OutputType.ERROR, err, options, config); + process.exit(1); + } + } }); configCmd @@ -39,22 +33,16 @@ configCmd .option('-l, --log', 'Enable logging') .description('Set a config setting by key and value') .action((key: keyof ConfigInterface, val: any, options: CliOptions) => { - return withSpinner( - options, - config as any, - `Set config ${String(key)}`, - async () => { + { + const message = `Set config ${String(key)}`; + output(OutputType.START, message, options, config); + try { const res = config.set(key, val); - return { key, value: res }; - }, - (result: any, useJson: boolean) => { - if (useJson) { - const obj: any = {}; - obj[result.key] = result.value; - console.log(JSON.stringify(obj, null, 2)); - } else { - console.log(result.value); - } - }, - ); + const payload = options && options.json ? { key, value: res } : String(res); + output(OutputType.SUCCESS, payload, options, config); + } catch (err: any) { + output(OutputType.ERROR, err, options, config); + process.exit(1); + } + } }); diff --git a/src/commands/create.ts b/src/commands/create.ts index 6eac520..bc22d9e 100644 --- a/src/commands/create.ts +++ b/src/commands/create.ts @@ -1,7 +1,7 @@ import { Command } from 'commander'; import { CliOptions } from '../types/options.js'; import { ManagerLocal } from '@open-audio-stack/core'; -import { withSpinner } from '../utils.js'; +import { output, OutputType } from '../utils.js'; export function create(command: Command, manager: ManagerLocal) { command @@ -9,19 +9,18 @@ export function create(command: Command, manager: ManagerLocal) { .option('-j, --json', 'Output results as json') .option('-l, --log', 'Enable logging') .description('Create a new package locally') - .action(async (path: string, options: CliOptions) => { - await withSpinner( - options, - manager, - `Create package at ${path}`, - async () => { - const result = await manager.create(); - return result; - }, - (result: any, useJson: boolean) => { - if (useJson) console.log(JSON.stringify(result, null, 2)); - else console.log(result); - }, - ); + .action(async (options: CliOptions) => { + const message = `Create package`; + output(OutputType.START, message, options, manager); + try { + const result = await manager.create(); + // For JSON mode return the object; for textual mode, stringify objects + const payload = + options && options.json ? result : typeof result === 'string' ? result : JSON.stringify(result, null, 2); + output(OutputType.SUCCESS, payload, options, manager); + } catch (err: any) { + output(OutputType.ERROR, err, options, manager); + process.exit(1); + } }); } diff --git a/src/commands/filter.ts b/src/commands/filter.ts index f1149b5..5232605 100644 --- a/src/commands/filter.ts +++ b/src/commands/filter.ts @@ -1,7 +1,7 @@ import { Command } from 'commander'; import { CliOptions } from '../types/options.js'; import { ManagerLocal, PackageVersion } from '@open-audio-stack/core'; -import { formatOutput, withSpinner } from '../utils.js'; +import { formatOutput, output, OutputType } from '../utils.js'; export function filter(command: Command, manager: ManagerLocal) { command @@ -9,29 +9,27 @@ export function filter(command: Command, manager: ManagerLocal) { .option('-j, --json', 'Output results as json') .option('-l, --log', 'Enable logging') .description('Filter the by field and matching value') - .action((field: keyof PackageVersion, value: string, options: CliOptions) => { - return withSpinner( - options, - manager, - `Filter ${manager.type} by ${String(field)}=${value}`, - async () => { - const predicate = (pkgVersion: PackageVersion) => { - const fieldVal: any = (pkgVersion as any)[field]; - if (Array.isArray(fieldVal)) { - return fieldVal.some((v: any) => String(v).toLowerCase() === value.toLowerCase()); - } - if (typeof fieldVal === 'string') { - return fieldVal.toLowerCase().includes(value.toLowerCase()); - } - if (fieldVal === undefined || fieldVal === null) return false; - return String(fieldVal) === value; - }; - return manager.filter(predicate); - }, - (result: any, useJson: boolean) => { - if (useJson) console.log(JSON.stringify(result, null, 2)); - else console.log(formatOutput(result as any)); - }, - ); + .action(async (field: keyof PackageVersion, value: string, options: CliOptions) => { + const message = `Filter ${manager.type} by ${String(field)}=${value}`; + output(OutputType.START, message, options, manager); + try { + const predicate = (pkgVersion: PackageVersion) => { + const fieldVal: any = pkgVersion[field]; + if (Array.isArray(fieldVal)) { + return fieldVal.some((v: any) => String(v).toLowerCase() === value.toLowerCase()); + } + if (typeof fieldVal === 'string') { + return fieldVal.toLowerCase().includes(value.toLowerCase()); + } + if (fieldVal === undefined || fieldVal === null) return false; + return String(fieldVal) === value; + }; + const result = await manager.filter(predicate); + const payload = options && options.json ? result : formatOutput(result); + output(OutputType.SUCCESS, payload, options, manager); + } catch (err: any) { + output(OutputType.ERROR, err, options, manager); + process.exit(1); + } }); } diff --git a/src/commands/get.ts b/src/commands/get.ts index 0ee8af8..e22ab5e 100644 --- a/src/commands/get.ts +++ b/src/commands/get.ts @@ -1,7 +1,7 @@ import { Command } from 'commander'; import { CliOptions } from '../types/options.js'; import { inputGetParts, ManagerLocal } from '@open-audio-stack/core'; -import { formatOutput, withSpinner } from '../utils.js'; +import { formatOutput, output, OutputType } from '../utils.js'; export function get(command: Command, manager: ManagerLocal) { command @@ -10,21 +10,20 @@ export function get(command: Command, manager: ManagerLocal) { .option('-l, --log', 'Enable logging') .description('Get package metadata from registry') .action((input: string, options: CliOptions) => { - return withSpinner( - options, - manager, - `Get package ${input}`, - async () => { - const [slug, version] = inputGetParts(input); - const pkg = manager.getPackage(slug); - if (!pkg) throw new Error(`No package found with slug: ${slug}`); - return { pkg, version }; - }, - (result: any, useJson: boolean) => { - const { pkg, version } = result; - const versions = version ? [version] : Array.from(pkg.versions.keys()); - console.log(formatOutput(pkg, versions, useJson)); - }, - ); + const message = `Get package ${input}`; + output(OutputType.START, message, options, manager); + try { + const [slug, version] = inputGetParts(input); + const pkg = manager.getPackage(slug); + if (!pkg) throw new Error(`No package found with slug: ${slug}`); + const payload = + options && options.json + ? { pkg, version } + : formatOutput(pkg, version ? [version] : Array.from(pkg.versions.keys()), false); + output(OutputType.SUCCESS, payload, options, manager); + } catch (err: any) { + output(OutputType.ERROR, err, options, manager); + process.exit(1); + } }); } diff --git a/src/commands/install.ts b/src/commands/install.ts index 448650d..76d822f 100644 --- a/src/commands/install.ts +++ b/src/commands/install.ts @@ -1,7 +1,7 @@ import { Command } from 'commander'; import { CliOptions } from '../types/options.js'; -import { inputGetParts, ManagerLocal, isTests } from '@open-audio-stack/core'; -import { withSpinner } from '../utils.js'; +import { inputGetParts, ManagerLocal } from '@open-audio-stack/core'; +import { output, OutputType } from '../utils.js'; export function install(command: Command, manager: ManagerLocal) { command @@ -11,19 +11,18 @@ export function install(command: Command, manager: ManagerLocal) { .description('Install a package locally by slug/version') .action(async (input: string, options: CliOptions) => { const [slug, version] = inputGetParts(input); - await withSpinner( - options, - manager, - `Install ${slug}${version ? `@${version}` : ''}`, - async () => { - await manager.install(slug, version); - return { slug, version: version || null, installed: true, isTests: isTests() }; - }, - (result: any, useJson: boolean) => { - if (useJson) - console.log(JSON.stringify({ slug: result.slug, version: result.version, installed: true }, null, 2)); - else if (result.isTests) console.log(`Installed ${result.slug}${result.version ? `@${result.version}` : ''}`); - }, - ); + const message = `Install ${slug}${version ? `@${version}` : ''}`; + output(OutputType.START, message, options, manager); + try { + await manager.install(slug, version); + const payload = + options && options.json + ? { slug, version: version || null, installed: true } + : `Installed ${slug}${version ? `@${version}` : ''}`; + output(OutputType.SUCCESS, payload, options, manager); + } catch (err: any) { + output(OutputType.ERROR, err, options, manager); + process.exit(1); + } }); } diff --git a/src/commands/list.ts b/src/commands/list.ts index 20c6e8d..a389000 100644 --- a/src/commands/list.ts +++ b/src/commands/list.ts @@ -1,7 +1,7 @@ import { Command } from 'commander'; import { CliOptions } from '../types/options.js'; import { ManagerLocal } from '@open-audio-stack/core'; -import { formatOutput, withSpinner } from '../utils.js'; +import { formatOutput, output, OutputType } from '../utils.js'; interface ListOptions extends CliOptions { incompatible: boolean; @@ -17,17 +17,15 @@ export function list(command: Command, manager: ManagerLocal) { .option('-l, --log', 'Enable logging') .description('List packages') .action(async (options: ListOptions) => { - await withSpinner( - options, - manager, - `List ${manager.type}`, - async () => { - return manager.listPackages(options.installed, options.incompatible); - }, - (result: any, useJson: boolean) => { - if (useJson) console.log(JSON.stringify(result, null, 2)); - else console.log(formatOutput(result as any)); - }, - ); + const message = `List ${manager.type}`; + output(OutputType.START, message, options, manager); + try { + const result = await manager.listPackages(options.installed, options.incompatible); + const payload = options && options.json ? result : formatOutput(result); + output(OutputType.SUCCESS, payload, options, manager); + } catch (err: any) { + output(OutputType.ERROR, err, options, manager); + process.exit(1); + } }); } diff --git a/src/commands/open.ts b/src/commands/open.ts index c6ec2b4..8d5af3c 100644 --- a/src/commands/open.ts +++ b/src/commands/open.ts @@ -1,7 +1,7 @@ import { Command } from 'commander'; import { CliOptions } from '../types/options.js'; import { inputGetParts, ManagerLocal } from '@open-audio-stack/core'; -import { withSpinner } from '../utils.js'; +import { output, OutputType } from '../utils.js'; export function open(command: Command, manager: ManagerLocal) { command @@ -9,18 +9,15 @@ export function open(command: Command, manager: ManagerLocal) { .option('-l, --log', 'Enable logging') .description('Open a package by slug/version') .action(async (input: string, options: string[], cliOptions: CliOptions) => { - await withSpinner( - cliOptions, - manager, - `Open ${input}`, - async () => { - const [slug, version] = inputGetParts(input); - await manager.open(slug, version, options); - return { slug, version }; - }, - () => { - // open is an action side-effect; no additional output required - }, - ); + const message = `Open ${input}`; + output(OutputType.START, message, cliOptions, manager); + try { + const [slug, version] = inputGetParts(input); + await manager.open(slug, version, options); + output(OutputType.SUCCESS, `Opened ${input}`, cliOptions, manager); + } catch (err: any) { + output(OutputType.ERROR, err, cliOptions, manager); + process.exit(1); + } }); } diff --git a/src/commands/scan.ts b/src/commands/scan.ts index 1a5c9b9..8702e01 100644 --- a/src/commands/scan.ts +++ b/src/commands/scan.ts @@ -1,7 +1,7 @@ import { Command } from 'commander'; import { CliOptions } from '../types/options.js'; import { ManagerLocal } from '@open-audio-stack/core'; -import { withSpinner } from '../utils.js'; +import { output, OutputType } from '../utils.js'; export function scan(command: Command, manager: ManagerLocal) { command @@ -10,17 +10,16 @@ export function scan(command: Command, manager: ManagerLocal) { .option('-l, --log', 'Enable logging') .description('Scan local packages into cache') .action(async (options: CliOptions) => { - await withSpinner( - options, - manager, - `Scan ${manager.type}`, - async () => { - await manager.scan(); - return { type: manager.type, status: 'scan completed' }; - }, - (result: any, useJson: boolean) => { - if (useJson) console.log(JSON.stringify({ type: result.type, status: result.status }, null, 2)); - }, - ); + const message = `Scan ${manager.type}`; + output(OutputType.START, message, options, manager); + try { + await manager.scan(); + // pass an object for JSON output, or a string for textual output + const payload = options && options.json ? { type: manager.type, status: 'scanned' } : `Scanned ${manager.type}`; + output(OutputType.SUCCESS, payload, options, manager); + } catch (err: any) { + output(OutputType.ERROR, err, options, manager); + process.exit(1); + } }); } diff --git a/src/commands/search.ts b/src/commands/search.ts index 5c7aeea..e836ba0 100644 --- a/src/commands/search.ts +++ b/src/commands/search.ts @@ -1,7 +1,7 @@ import { Command } from 'commander'; import { CliOptions } from '../types/options.js'; import { ManagerLocal } from '@open-audio-stack/core'; -import { formatOutput, withSpinner } from '../utils.js'; +import { formatOutput, output, OutputType } from '../utils.js'; export function search(command: Command, manager: ManagerLocal) { command @@ -10,17 +10,15 @@ export function search(command: Command, manager: ManagerLocal) { .option('-l, --log', 'Enable logging') .description('Search using a lazy matching query') .action(async (query: string, options: CliOptions) => { - await withSpinner( - options, - manager, - `Search ${manager.type}`, - async () => { - return manager.search(query); - }, - (result: any, useJson: boolean) => { - if (useJson) console.log(JSON.stringify(result, null, 2)); - else console.log(formatOutput(result as any)); - }, - ); + const message = `Search ${manager.type}`; + output(OutputType.START, message, options, manager); + try { + const result = await manager.search(query); + const payload = options && options.json ? result : formatOutput(result); + output(OutputType.SUCCESS, payload, options, manager); + } catch (err: any) { + output(OutputType.ERROR, err, options, manager); + process.exit(1); + } }); } diff --git a/src/commands/sync.ts b/src/commands/sync.ts index 721cb30..13968b2 100644 --- a/src/commands/sync.ts +++ b/src/commands/sync.ts @@ -1,7 +1,7 @@ import { Command } from 'commander'; import { CliOptions } from '../types/options.js'; -import { ManagerLocal, isTests } from '@open-audio-stack/core'; -import { withSpinner } from '../utils.js'; +import { ManagerLocal } from '@open-audio-stack/core'; +import { output, OutputType } from '../utils.js'; export function sync(command: Command, manager: ManagerLocal) { command @@ -10,18 +10,15 @@ export function sync(command: Command, manager: ManagerLocal) { .option('-l, --log', 'Enable logging') .description('Sync remote packages into cache') .action(async (options: CliOptions) => { - await withSpinner( - options, - manager, - `Sync ${manager.type}`, - async () => { - await manager.sync(); - return { type: manager.type, status: 'sync completed', isTests: isTests() }; - }, - (result: any, useJson: boolean) => { - if (useJson) console.log(JSON.stringify({ type: result.type, status: result.status }, null, 2)); - else if (isTests()) console.log(`${result.type} sync completed`); - }, - ); + const message = `Sync ${manager.type}`; + output(OutputType.START, message, options, manager); + try { + await manager.sync(); + const payload = options && options.json ? { type: manager.type, status: 'synced' } : `Synced ${manager.type}`; + output(OutputType.SUCCESS, payload, options, manager); + } catch (err: any) { + output(OutputType.ERROR, err, options, manager); + process.exit(1); + } }); } diff --git a/src/commands/uninstall.ts b/src/commands/uninstall.ts index ec2229f..beb548b 100644 --- a/src/commands/uninstall.ts +++ b/src/commands/uninstall.ts @@ -1,7 +1,7 @@ import { Command } from 'commander'; import { CliOptions } from '../types/options.js'; -import { inputGetParts, ManagerLocal, isTests } from '@open-audio-stack/core'; -import { withSpinner } from '../utils.js'; +import { inputGetParts, ManagerLocal } from '@open-audio-stack/core'; +import { output, OutputType } from '../utils.js'; export function uninstall(command: Command, manager: ManagerLocal) { command @@ -11,20 +11,18 @@ export function uninstall(command: Command, manager: ManagerLocal) { .description('Uninstall a package locally by slug/version') .action(async (input: string, options: CliOptions) => { const [slug, version] = inputGetParts(input); - await withSpinner( - options, - manager, - `Uninstall ${slug}${version ? `@${version}` : ''}`, - async () => { - await manager.uninstall(slug, version); - return { slug, version: version || null, installed: false, isTests: isTests() }; - }, - (result: any, useJson: boolean) => { - if (useJson) - console.log(JSON.stringify({ slug: result.slug, version: result.version, installed: false }, null, 2)); - else if (result.isTests) - console.log(`Uninstalled ${result.slug}${result.version ? `@${result.version}` : ''}`); - }, - ); + const message = `Uninstall ${slug}${version ? `@${version}` : ''}`; + output(OutputType.START, message, options, manager); + try { + await manager.uninstall(slug, version); + const payload = + options && options.json + ? { slug, version: version || null, installed: false } + : `Uninstalled ${slug}${version ? `@${version}` : ''}`; + output(OutputType.SUCCESS, payload, options, manager); + } catch (err: any) { + output(OutputType.ERROR, err, options, manager); + process.exit(1); + } }); } diff --git a/src/utils.ts b/src/utils.ts index f49d79b..3e2c85c 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,4 +1,4 @@ -import { Package } from '@open-audio-stack/core'; +import { Base, Package } from '@open-audio-stack/core'; import CliTable3 from 'cli-table3'; import ora from 'ora'; import type { CliOptions } from './types/options.js'; @@ -62,54 +62,96 @@ export function truncateString(str: string, num: number) { } } -export async function withSpinner( - options: CliOptions | undefined, - manager: any, - spinnerMessage: string, - action: () => Promise | T, - print?: (result: T, useJson: boolean) => void, -) { +// Simple spinner registry for the `output` API so callers can start/stop spinners +const _spinners: Map> = new Map(); + +export enum OutputType { + START = 'start', + SUCCESS = 'success', + ERROR = 'error', +} + +/** + * Low level output API that callers can use to mark start, success, and error + * states for long-running operations. This provides clear control over when + * spinners start/stop and when textual/json output is emitted. + */ +export function output(type: OutputType, payload: any, options?: CliOptions, base?: Base) { const useJson = Boolean(options && options.json); - const spinner = useJson ? undefined : ora(spinnerMessage).start(); - try { - if (manager) { - if (options && options.log) manager.logEnable(); - else manager.logDisable(); + const isTest = Boolean(process.env.VITEST || process.env.NODE_ENV === 'test'); + const key = String(typeof payload === 'string' ? payload : (payload && payload.message) || payload || ''); + + if (type === OutputType.START) { + // configure base logging at start + if (base) { + if (options && options.log) base.logEnable(); + else base.logDisable(); } - const result = await action(); - if (spinner) spinner.succeed(spinnerMessage); - - const isStatusOnly = (val: any) => { - if (!val || typeof val !== 'object') return false; - const keys = Object.keys(val); - // status or type+status objects are treated as status-only - return keys.length <= 2 && keys.includes('status') && keys.every(k => k === 'status' || k === 'type'); - }; - - if (print) { - print(result, useJson); - } else { - if (useJson) { - console.log(JSON.stringify(result, null, 2)); - } else { - // When a spinner was used and the result is a simple status object - // (e.g. { type: 'plugins', status: 'scan completed' }), the spinner - // already displayed the human-readable status. Avoid printing it again. - if (spinner && isStatusOnly(result)) { - // nothing to print - } else { - console.log(result as any); - } - } + + if (useJson) { + console.log(JSON.stringify({ status: 'inprogress', message: key }, null, 2)); + return; } - } catch (err) { - const errorMessage = err instanceof Error ? err.message : String(err); - if (spinner) { - spinner.fail(errorMessage); - } else { - if (useJson) console.log(JSON.stringify({ error: errorMessage }, null, 2)); - else console.error(errorMessage); + + if (isTest) { + console.log(key); + return; } - process.exit(1); + + // interactive run: create and start a spinner for this key + const s = ora(key).start(); + _spinners.set(key, s); + return; } + + if (type === OutputType.SUCCESS) { + // If JSON requested and payload is an object, print it + if (useJson && typeof payload === 'object') { + console.log(JSON.stringify(payload, null, 2)); + return; + } + + // For non-json modes we expect payload to be a string (commands should pass a string) + const messageOut = String(payload); + + if (isTest) { + // In test mode only print the final payload (no start/checkmark). + console.log(messageOut); + return; + } + + const s = _spinners.get(key); + if (s) { + s.succeed(key); + if (messageOut && messageOut !== key) console.log(messageOut); + _spinners.delete(key); + return; + } + + // fallback + console.log(key); + if (messageOut && messageOut !== key) console.log(messageOut); + return; + } + + // ERROR + const errMsg = payload instanceof Error ? payload.message : String(payload); + if (useJson) { + console.log(JSON.stringify({ error: errMsg }, null, 2)); + return; + } + + if (isTest) { + console.error(errMsg); + return; + } + + const s2 = _spinners.get(key); + if (s2) { + s2.fail(errMsg); + _spinners.delete(key); + return; + } + + console.error(errMsg); } diff --git a/tests/__snapshots__/index.test.ts.snap b/tests/__snapshots__/index.test.ts.snap index 4f6edb8..f6e1c75 100644 --- a/tests/__snapshots__/index.test.ts.snap +++ b/tests/__snapshots__/index.test.ts.snap @@ -23,7 +23,7 @@ Options: -h, --help display help for command Commands: - create [options] Create a new package locally + create [options] Create a new package locally filter [options] Filter the by field and matching value get [options] Get package metadata from registry install [options] Install a package locally by @@ -46,7 +46,7 @@ Options: -h, --help display help for command Commands: - create [options] Create a new package locally + create [options] Create a new package locally filter [options] Filter the by field and matching value get [options] Get package metadata from registry install [options] Install a package locally by @@ -69,7 +69,7 @@ Options: -h, --help display help for command Commands: - create [options] Create a new package locally + create [options] Create a new package locally filter [options] Filter the by field and matching value get [options] Get package metadata from registry install [options] Install a package locally by diff --git a/tests/commands/__snapshots__/config.test.ts.snap b/tests/commands/__snapshots__/config.test.ts.snap index 2ccf5c1..83c14ff 100644 --- a/tests/commands/__snapshots__/config.test.ts.snap +++ b/tests/commands/__snapshots__/config.test.ts.snap @@ -1,24 +1,41 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`Config get 1`] = `test`; +exports[`Config get 1`] = ` +Get config appDir +test2 +`; -exports[`Config get 2`] = `test/installed/plugins`; +exports[`Config get 2`] = ` +Get config pluginsDir +test/installed/plugins +`; -exports[`Config get 3`] = `test/installed/presets`; +exports[`Config get 3`] = ` +Get config presetsDir +test/installed/presets +`; -exports[`Config get 4`] = `test/installed/projects`; +exports[`Config get 4`] = ` +Get config projectsDir +test/installed/projects +`; exports[`Config get 5`] = ` -[ - { - name: 'Open Audio Registry', - url: 'https://open-audio-stack.github.io/open-audio-stack-registry' - } -] +Get config registries +[object Object] `; -exports[`Config get 6`] = `1.0.0`; +exports[`Config get 6`] = ` +Get config version +1.0.0 +`; -exports[`Config set 1`] = `test2`; +exports[`Config set 1`] = ` +Get config appDir +test2 +`; -exports[`Config set 2`] = `test`; +exports[`Config set 2`] = ` +Get config appDir +test +`; diff --git a/tests/commands/__snapshots__/create.test.ts.snap b/tests/commands/__snapshots__/create.test.ts.snap index 7c1af1a..fd74fec 100644 --- a/tests/commands/__snapshots__/create.test.ts.snap +++ b/tests/commands/__snapshots__/create.test.ts.snap @@ -1,6 +1,8 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`Create package 1`] = ` +User force closed the prompt with 0 null +Create package ? Org id (org-name) `; diff --git a/tests/commands/__snapshots__/filter.test.ts.snap b/tests/commands/__snapshots__/filter.test.ts.snap index a8d19b1..91619f1 100644 --- a/tests/commands/__snapshots__/filter.test.ts.snap +++ b/tests/commands/__snapshots__/filter.test.ts.snap @@ -1,16 +1,21 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`Filter packages 1`] = ` +Filter plugins by name=Surge XT ┌─────────────────────────┬──────────┬─────────┬───────────┬────────────┬─────────┬───────────────────────────────┐ │ Id │ Name │ Version │ Installed │ Date │ License │ Tags │ ├─────────────────────────┼──────────┼─────────┼───────────┼────────────┼─────────┼───────────────────────────────┤ -│ surge-synthesizer/surge │ Surge XT │ 1.3.4 │ - │ 2024-08-11 │ gpl-3.0 │ Instrument, Synth, Modulation │ +│ surge-synthesizer/surge │ Surge XT │ 1.3.4 │ ✓ │ 2024-08-11 │ gpl-3.0 │ Instrument, Synth, Modulation │ └─────────────────────────┴──────────┴─────────┴───────────┴────────────┴─────────┴───────────────────────────────┘ `; -exports[`Filter packages 2`] = `No results found`; +exports[`Filter packages 2`] = ` +Filter plugins by name=Surge XU +No results found +`; exports[`Filter packages 3`] = ` +Filter plugins by license=cc0-1.0 ┌────────────────────────────────────────┬──────────────────────────┬─────────┬───────────┬────────────┬─────────┬───────────────────────────────────┐ │ Id │ Name │ Version │ Installed │ Date │ License │ Tags │ ├────────────────────────────────────────┼──────────────────────────┼─────────┼───────────┼────────────┼─────────┼───────────────────────────────────┤ diff --git a/tests/commands/__snapshots__/get.test.ts.snap b/tests/commands/__snapshots__/get.test.ts.snap index 740c441..804ad73 100644 --- a/tests/commands/__snapshots__/get.test.ts.snap +++ b/tests/commands/__snapshots__/get.test.ts.snap @@ -1,6 +1,7 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`Get package 1`] = ` +Get package surge-synthesizer/surge ┌─────────────────────────┬──────────┬─────────┬───────────┬────────────┬─────────┬───────────────────────────────┐ │ Id │ Name │ Version │ Installed │ Date │ License │ Tags │ ├─────────────────────────┼──────────┼─────────┼───────────┼────────────┼─────────┼───────────────────────────────┤ @@ -11,6 +12,7 @@ exports[`Get package 1`] = ` `; exports[`Get package 2`] = ` +Get package surge-synthesizer/surge@1.3.1 ┌─────────────────────────┬──────────┬─────────┬───────────┬────────────┬─────────┬───────────────────────────────┐ │ Id │ Name │ Version │ Installed │ Date │ License │ Tags │ ├─────────────────────────┼──────────┼─────────┼───────────┼────────────┼─────────┼───────────────────────────────┤ @@ -18,4 +20,7 @@ exports[`Get package 2`] = ` └─────────────────────────┴──────────┴─────────┴───────────┴────────────┴─────────┴───────────────────────────────┘ `; -exports[`Get package 3`] = `No results found`; +exports[`Get package 3`] = ` +Get package surge-synthesizer/surge@0.0.0 +No results found +`; diff --git a/tests/commands/__snapshots__/install.test.ts.snap b/tests/commands/__snapshots__/install.test.ts.snap index 013f072..2e5ef11 100644 --- a/tests/commands/__snapshots__/install.test.ts.snap +++ b/tests/commands/__snapshots__/install.test.ts.snap @@ -1,7 +1,16 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`Install package 1`] = `Installed surge-synthesizer/surge`; +exports[`Install package 1`] = ` +Install surge-synthesizer/surge +Installed surge-synthesizer/surge +`; -exports[`Install package 2`] = `Installed surge-synthesizer/surge@1.3.1`; +exports[`Install package 2`] = ` +Install surge-synthesizer/surge@1.3.1 +Installed surge-synthesizer/surge@1.3.1 +`; -exports[`Install package 3`] = `Package surge-synthesizer/surge version 0.0.0 not found in registry`; +exports[`Install package 3`] = ` +Install surge-synthesizer/surge@0.0.0 +Package surge-synthesizer/surge version 0.0.0 not found in registry +`; diff --git a/tests/commands/__snapshots__/list.test.ts.snap b/tests/commands/__snapshots__/list.test.ts.snap index 43613f3..83cea0a 100644 --- a/tests/commands/__snapshots__/list.test.ts.snap +++ b/tests/commands/__snapshots__/list.test.ts.snap @@ -1,13 +1,12 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`List packages 1`] = ` +List plugins ┌───────────────────────────────────────────┬───────────────────────────┬─────────┬───────────┬────────────┬───────────────┬───────────────────────────────────┐ │ Id │ Name │ Version │ Installed │ Date │ License │ Tags │ ├───────────────────────────────────────────┼───────────────────────────┼─────────┼───────────┼────────────┼───────────────┼───────────────────────────────────┤ │ aaronaanderson/terrain │ Terrain │ 1.2.2 │ - │ 2024-11-01 │ gpl-3.0 │ Instrument, Synthesizer, Wavet... │ ├───────────────────────────────────────────┼───────────────────────────┼─────────┼───────────┼────────────┼───────────────┼───────────────────────────────────┤ -│ airwindows/airwindows │ Airwindows │ 1.0.0 │ - │ 2020-02-20 │ mit │ Effect, Chorus, Distortion, EQ... │ -├───────────────────────────────────────────┼───────────────────────────┼─────────┼───────────┼────────────┼───────────────┼───────────────────────────────────┤ │ antonok-edm/ampli-fe │ ampli-Fe │ 0.1.1 │ - │ 2021-11-02 │ mit │ Effect, Amplifier, Volume │ ├───────────────────────────────────────────┼───────────────────────────┼─────────┼───────────┼────────────┼───────────────┼───────────────────────────────────┤ │ ardenbutterfield/maim │ Maim │ 1.1.1 │ - │ 2025-05-20 │ gpl-3.0 │ Instrument, Modulation, Distor... │ @@ -185,6 +184,7 @@ exports[`List packages 1`] = ` `; exports[`List packages 2`] = ` +List plugins ┌─────────────────────────┬──────────┬─────────┬───────────┬────────────┬─────────┬───────────────────────────────┐ │ Id │ Name │ Version │ Installed │ Date │ License │ Tags │ ├─────────────────────────┼──────────┼─────────┼───────────┼────────────┼─────────┼───────────────────────────────┤ diff --git a/tests/commands/__snapshots__/open.test.ts.snap b/tests/commands/__snapshots__/open.test.ts.snap index 54e6a04..33fa7ce 100644 --- a/tests/commands/__snapshots__/open.test.ts.snap +++ b/tests/commands/__snapshots__/open.test.ts.snap @@ -11,6 +11,8 @@ Options: `; exports[`Open command install and run steinberg/validator mac 1`] = ` +Open steinberg/validator +Opened steinberg/validator VST 3.6.14 Plug-in Validator: -help | Print help diff --git a/tests/commands/__snapshots__/scan.test.ts.snap b/tests/commands/__snapshots__/scan.test.ts.snap index 06becf3..2a3210b 100644 --- a/tests/commands/__snapshots__/scan.test.ts.snap +++ b/tests/commands/__snapshots__/scan.test.ts.snap @@ -1,3 +1,6 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`Scan packages 1`] = `plugins scan completed`; +exports[`Scan packages 1`] = ` +Scan plugins +Scanned plugins +`; diff --git a/tests/commands/__snapshots__/search.test.ts.snap b/tests/commands/__snapshots__/search.test.ts.snap index c961d3b..1952337 100644 --- a/tests/commands/__snapshots__/search.test.ts.snap +++ b/tests/commands/__snapshots__/search.test.ts.snap @@ -1,6 +1,7 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`Search packages 1`] = ` +Search plugins ┌─────────────────────────┬─────────────┬─────────┬───────────┬────────────┬─────────┬────────────────────────────────┐ │ Id │ Name │ Version │ Installed │ Date │ License │ Tags │ ├─────────────────────────┼─────────────┼─────────┼───────────┼────────────┼─────────┼────────────────────────────────┤ @@ -12,4 +13,7 @@ exports[`Search packages 1`] = ` └─────────────────────────┴─────────────┴─────────┴───────────┴────────────┴─────────┴────────────────────────────────┘ `; -exports[`Search packages 2`] = `No results found`; +exports[`Search packages 2`] = ` +Search plugins +No results found +`; diff --git a/tests/commands/__snapshots__/sync.test.ts.snap b/tests/commands/__snapshots__/sync.test.ts.snap index 39a8441..8f23ef1 100644 --- a/tests/commands/__snapshots__/sync.test.ts.snap +++ b/tests/commands/__snapshots__/sync.test.ts.snap @@ -1,3 +1,6 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`Sync packages 1`] = `plugins sync completed`; +exports[`Sync packages 1`] = ` +Sync plugins +Synced plugins +`; diff --git a/tests/commands/__snapshots__/uninstall.test.ts.snap b/tests/commands/__snapshots__/uninstall.test.ts.snap index 97dd24c..aaceaa4 100644 --- a/tests/commands/__snapshots__/uninstall.test.ts.snap +++ b/tests/commands/__snapshots__/uninstall.test.ts.snap @@ -1,7 +1,16 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`Uninstall package 1`] = `Uninstalled surge-synthesizer/surge`; +exports[`Uninstall package 1`] = ` +Uninstall surge-synthesizer/surge +Uninstalled surge-synthesizer/surge +`; -exports[`Uninstall package 2`] = `Uninstalled surge-synthesizer/surge@1.3.1`; +exports[`Uninstall package 2`] = ` +Uninstall surge-synthesizer/surge@1.3.1 +Uninstalled surge-synthesizer/surge@1.3.1 +`; -exports[`Uninstall package 3`] = `Package surge-synthesizer/surge version 0.0.0 not found in registry`; +exports[`Uninstall package 3`] = ` +Uninstall surge-synthesizer/surge@0.0.0 +Package surge-synthesizer/surge version 0.0.0 not found in registry +`; diff --git a/tests/commands/create.test.ts b/tests/commands/create.test.ts index 30462a9..bc8fece 100644 --- a/tests/commands/create.test.ts +++ b/tests/commands/create.test.ts @@ -1,5 +1,5 @@ import { expect, test } from 'vitest'; -import { cli } from '../shared'; +import { cliCatch } from '../shared'; import path from 'path'; import { RegistryType } from '@open-audio-stack/core'; @@ -7,5 +7,7 @@ const APP_DIR: string = 'test'; const PROJECT_DIR: string = path.join(APP_DIR, 'create-project'); test('Create package', async () => { - expect(cli(RegistryType.Plugins, 'create', PROJECT_DIR)).toMatchSnapshot(); + // The create command may prompt/abort during tests — capture textual output + const res = cliCatch(RegistryType.Plugins, 'create', PROJECT_DIR); + expect((res.stderr + '\n' + res.stdout).trim()).toMatchSnapshot(); }); diff --git a/tests/commands/filter.test.ts b/tests/commands/filter.test.ts index 2de1d42..37a70a9 100644 --- a/tests/commands/filter.test.ts +++ b/tests/commands/filter.test.ts @@ -4,6 +4,6 @@ import { License, RegistryType } from '@open-audio-stack/core'; test('Filter packages', async () => { expect(cli(RegistryType.Plugins, 'filter', 'name', 'Surge XT')).toMatchSnapshot(); - expect(cli(RegistryType.Plugins, 'filter', 'name', 'Surge X')).toMatchSnapshot(); + expect(cli(RegistryType.Plugins, 'filter', 'name', 'Surge XU')).toMatchSnapshot(); expect(cli(RegistryType.Plugins, 'filter', 'license', License.CreativeCommonsZerov1Universal)).toMatchSnapshot(); }); diff --git a/tests/commands/install.test.ts b/tests/commands/install.test.ts index 8c47d49..2bb4084 100644 --- a/tests/commands/install.test.ts +++ b/tests/commands/install.test.ts @@ -1,9 +1,11 @@ import { expect, test } from 'vitest'; -import { cli } from '../shared'; +import { cli, cliCatch } from '../shared'; import { RegistryType } from '@open-audio-stack/core'; test('Install package', async () => { expect(cli(RegistryType.Plugins, 'install', 'surge-synthesizer/surge')).toMatchSnapshot(); expect(cli(RegistryType.Plugins, 'install', 'surge-synthesizer/surge@1.3.1')).toMatchSnapshot(); - expect(cli(RegistryType.Plugins, 'install', 'surge-synthesizer/surge@0.0.0')).toMatchSnapshot(); + // non-existent version may cause execaSync to throw — capture stderr text + const res = cliCatch(RegistryType.Plugins, 'install', 'surge-synthesizer/surge@0.0.0'); + expect(res.stderr.trim()).toMatchSnapshot(); }); diff --git a/tests/commands/open.test.ts b/tests/commands/open.test.ts index 26e1a1e..0451587 100644 --- a/tests/commands/open.test.ts +++ b/tests/commands/open.test.ts @@ -1,20 +1,19 @@ import { expect, test } from 'vitest'; import { cli, cliCatch, cleanOutput } from '../shared'; +import { getSystem } from '@open-audio-stack/core'; test('Open command help', () => { const result = cli('apps', 'open', '--help'); expect(cleanOutput(result)).toMatchSnapshot(); }); -// Comment out test until it can be fixed -// test(`Open command install and run steinberg/validator ${getSystem()}`, () => { -// // First install the app -// const installResult = cli('apps', 'install', 'steinberg/validator'); -// expect(installResult).toContain('Installed steinberg/validator'); +test(`Open command install and run steinberg/validator ${getSystem()}`, () => { + const installResult = cli('apps', 'install', 'steinberg/validator'); + expect(installResult).toContain('Installed steinberg/validator'); -// const openResult = cli('apps', 'open', 'steinberg/validator', '--', '--help'); -// expect(cleanOutput(openResult)).toMatchSnapshot(); -// }); + const openResult = cli('apps', 'open', 'steinberg/validator', '--', '--help'); + expect(cleanOutput(openResult)).toMatchSnapshot(); +}); test('Open command with non-existent package', () => { const error = cliCatch('apps', 'open', 'non-existent/package'); diff --git a/tests/commands/uninstall.test.ts b/tests/commands/uninstall.test.ts index 1165f95..b68b455 100644 --- a/tests/commands/uninstall.test.ts +++ b/tests/commands/uninstall.test.ts @@ -1,5 +1,5 @@ import { expect, test } from 'vitest'; -import { cli } from '../shared'; +import { cli, cliCatch } from '../shared'; import { RegistryType } from '@open-audio-stack/core'; test('Uninstall package', async () => { @@ -8,6 +8,7 @@ test('Uninstall package', async () => { cli(RegistryType.Plugins, 'install', 'surge-synthesizer/surge@1.3.1'); expect(cli(RegistryType.Plugins, 'uninstall', 'surge-synthesizer/surge@1.3.1')).toMatchSnapshot(); - - expect(cli(RegistryType.Plugins, 'uninstall', 'surge-synthesizer/surge@0.0.0')).toMatchSnapshot(); + // uninstalling a non-existent version throws — snapshot stderr text + const res = cliCatch(RegistryType.Plugins, 'uninstall', 'surge-synthesizer/surge@0.0.0'); + expect(res.stderr.trim()).toMatchSnapshot(); }); diff --git a/tests/shared.ts b/tests/shared.ts index a17fbe5..6406dba 100644 --- a/tests/shared.ts +++ b/tests/shared.ts @@ -7,10 +7,10 @@ import { getSystem, SystemType } from '@open-audio-stack/core'; const CLI_PATH: string = path.resolve('./', 'build', 'index.js'); // ANSI escape codes for colors and formatting vary across systems -// Sanitize output for snapshot testing +// Sanitize output for snapshot testing (only apply to strings) expect.addSnapshotSerializer({ - serialize: val => stripVTControlCharacters(val), - test: () => true, + serialize: val => stripVTControlCharacters(val as string), + test: val => typeof val === 'string', }); export function cli(...args: string[]): string { @@ -20,11 +20,28 @@ export function cli(...args: string[]): string { return cleanOutput(result.stdout as string); } -export function cliCatch(...args: string[]) { +export type CliResult = { + exitCode: number | null; + stdout: string; + stderr: string; +}; + +export function cliCatch(...args: string[]): CliResult { try { - cli(...args); + const result: SyncResult = execaSync('node', [CLI_PATH, ...args], { + env: { ...process.env, NODE_OPTIONS: '--no-warnings=ExperimentalWarning' }, + }); + return { + exitCode: result.exitCode ?? 0, + stdout: cleanOutput(String(result.stdout ?? '')), + stderr: cleanOutput(String(result.stderr ?? '')), + }; } catch (error: any) { - return error; + // execa throws an error with stdout/stderr and exitCode properties + const exitCode = typeof error.exitCode === 'number' ? error.exitCode : 1; + const stdout = cleanOutput(String(error.stdout ?? '')); + const stderr = cleanOutput(String(error.stderr ?? error.message ?? '')); + return { exitCode, stdout, stderr }; } } From ad3e9e7ffb34f7e0d087795335b143d1c1dae2b3 Mon Sep 17 00:00:00 2001 From: Kim T Date: Sat, 20 Dec 2025 22:41:06 -0800 Subject: [PATCH 3/6] Test cli wrapper return code/out/err, match entire result in toMatchSnapshot() --- src/commands/reset.ts | 16 ++- src/utils.ts | 100 +++++------------- tests/__snapshots__/index.test.ts.snap | 32 ++++-- .../__snapshots__/config.test.ts.snap | 64 ++++++++--- .../__snapshots__/create.test.ts.snap | 9 +- .../__snapshots__/filter.test.ts.snap | 26 +++-- tests/commands/__snapshots__/get.test.ts.snap | 24 +++-- .../__snapshots__/install.test.ts.snap | 24 +++-- .../commands/__snapshots__/list.test.ts.snap | 16 ++- .../commands/__snapshots__/open.test.ts.snap | 34 +++++- .../commands/__snapshots__/reset.test.ts.snap | 9 +- .../commands/__snapshots__/scan.test.ts.snap | 8 +- .../__snapshots__/search.test.ts.snap | 16 ++- .../commands/__snapshots__/sync.test.ts.snap | 8 +- .../__snapshots__/uninstall.test.ts.snap | 24 +++-- tests/commands/create.test.ts | 6 +- tests/commands/install.test.ts | 6 +- tests/commands/open.test.ts | 16 +-- tests/commands/uninstall.test.ts | 7 +- tests/index.test.ts | 18 +--- tests/shared.ts | 31 +++--- 21 files changed, 290 insertions(+), 204 deletions(-) diff --git a/src/commands/reset.ts b/src/commands/reset.ts index 339875c..9ed7da5 100644 --- a/src/commands/reset.ts +++ b/src/commands/reset.ts @@ -1,6 +1,7 @@ import { Command } from 'commander'; import { CliOptions } from '../types/options.js'; import { ManagerLocal } from '@open-audio-stack/core'; +import { output, OutputType } from '../utils.js'; export function reset(command: Command, manager: ManagerLocal) { command @@ -8,9 +9,16 @@ export function reset(command: Command, manager: ManagerLocal) { .option('-l, --log', 'Enable logging') .description('Reset the synced package cache') .action((options: CliOptions) => { - if (options.log) manager.logEnable(); - else manager.logDisable(); - manager.reset(); - console.log(`${manager.type} cache has been reset`); + const message = `Reset ${manager.type}`; + output(OutputType.START, message, options, manager); + try { + manager.reset(); + const payload = + options && options.json ? { type: manager.type, status: 'reset' } : `Reset complete ${manager.type}`; + output(OutputType.SUCCESS, payload, options, manager); + } catch (err: any) { + output(OutputType.ERROR, err, options, manager); + process.exit(1); + } }); } diff --git a/src/utils.ts b/src/utils.ts index 3e2c85c..eb74d7b 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,6 +1,6 @@ -import { Base, Package } from '@open-audio-stack/core'; +import { Base, isTests, Package } from '@open-audio-stack/core'; import CliTable3 from 'cli-table3'; -import ora from 'ora'; +import ora, { Ora } from 'ora'; import type { CliOptions } from './types/options.js'; export function formatOutput(result: Package[] | Package | undefined, versions?: string[], json?: boolean): string { @@ -62,96 +62,46 @@ export function truncateString(str: string, num: number) { } } -// Simple spinner registry for the `output` API so callers can start/stop spinners -const _spinners: Map> = new Map(); - export enum OutputType { START = 'start', SUCCESS = 'success', ERROR = 'error', } -/** - * Low level output API that callers can use to mark start, success, and error - * states for long-running operations. This provides clear control over when - * spinners start/stop and when textual/json output is emitted. - */ -export function output(type: OutputType, payload: any, options?: CliOptions, base?: Base) { - const useJson = Boolean(options && options.json); - const isTest = Boolean(process.env.VITEST || process.env.NODE_ENV === 'test'); - const key = String(typeof payload === 'string' ? payload : (payload && payload.message) || payload || ''); - - if (type === OutputType.START) { - // configure base logging at start - if (base) { - if (options && options.log) base.logEnable(); - else base.logDisable(); - } - - if (useJson) { - console.log(JSON.stringify({ status: 'inprogress', message: key }, null, 2)); - return; - } +let spinner: Ora | undefined; - if (isTest) { - console.log(key); - return; - } - - // interactive run: create and start a spinner for this key - const s = ora(key).start(); - _spinners.set(key, s); - return; - } - - if (type === OutputType.SUCCESS) { - // If JSON requested and payload is an object, print it - if (useJson && typeof payload === 'object') { - console.log(JSON.stringify(payload, null, 2)); - return; - } - - // For non-json modes we expect payload to be a string (commands should pass a string) - const messageOut = String(payload); - - if (isTest) { - // In test mode only print the final payload (no start/checkmark). - console.log(messageOut); - return; - } - - const s = _spinners.get(key); - if (s) { - s.succeed(key); - if (messageOut && messageOut !== key) console.log(messageOut); - _spinners.delete(key); - return; - } +export function output(type: OutputType, message: any, options?: CliOptions, base?: Base) { + // console.log('\n output', type, message, options); + const useJson = Boolean(options && options.json); + if (message.message) message = message.message; - // fallback - console.log(key); - if (messageOut && messageOut !== key) console.log(messageOut); - return; + // If logging, ensure core package logging is enabled. + if (base) { + if (options && options.log) base.logEnable(); + else base.logDisable(); } - // ERROR - const errMsg = payload instanceof Error ? payload.message : String(payload); + // If json, output json only. if (useJson) { - console.log(JSON.stringify({ error: errMsg }, null, 2)); + console.log(JSON.stringify({ type, message }, null, 2)); return; } - if (isTest) { - console.error(errMsg); + // If test, output text only. + if (isTests()) { + console.log(message); return; } - const s2 = _spinners.get(key); - if (s2) { - s2.fail(errMsg); - _spinners.delete(key); + // If interactive run, use spinners. + if (type === OutputType.START) { + spinner = ora(message).start(); + return; + } else if (type === OutputType.SUCCESS) { + spinner?.succeed(message); + return; + } else { + spinner?.fail(message); return; } - - console.error(errMsg); } diff --git a/tests/__snapshots__/index.test.ts.snap b/tests/__snapshots__/index.test.ts.snap index f6e1c75..72cbf80 100644 --- a/tests/__snapshots__/index.test.ts.snap +++ b/tests/__snapshots__/index.test.ts.snap @@ -1,7 +1,9 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`Root command 1`] = ` -Usage: index [options] [command] +{ + code: 1, + err: Usage: index [options] [command] Options: -V, --version output the version number @@ -13,11 +15,15 @@ Commands: plugins presets projects - help [command] display help for command + help [command] display help for command, + out: , +} `; exports[`Root command plugins 1`] = ` -Usage: index plugins [options] [command] +{ + code: 1, + err: Usage: index plugins [options] [command] Options: -h, --help display help for command @@ -36,11 +42,15 @@ Commands: sync [options] Sync remote packages into cache uninstall [options] Uninstall a package locally by slug/version - help [command] display help for command + help [command] display help for command, + out: , +} `; exports[`Root command presets 1`] = ` -Usage: index presets [options] [command] +{ + code: 1, + err: Usage: index presets [options] [command] Options: -h, --help display help for command @@ -59,11 +69,15 @@ Commands: sync [options] Sync remote packages into cache uninstall [options] Uninstall a package locally by slug/version - help [command] display help for command + help [command] display help for command, + out: , +} `; exports[`Root command projects 1`] = ` -Usage: index projects [options] [command] +{ + code: 1, + err: Usage: index projects [options] [command] Options: -h, --help display help for command @@ -82,5 +96,7 @@ Commands: sync [options] Sync remote packages into cache uninstall [options] Uninstall a package locally by slug/version - help [command] display help for command + help [command] display help for command, + out: , +} `; diff --git a/tests/commands/__snapshots__/config.test.ts.snap b/tests/commands/__snapshots__/config.test.ts.snap index 83c14ff..6e3d6ee 100644 --- a/tests/commands/__snapshots__/config.test.ts.snap +++ b/tests/commands/__snapshots__/config.test.ts.snap @@ -1,41 +1,73 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`Config get 1`] = ` -Get config appDir -test2 +{ + code: 0, + err: , + out: Get config appDir +test, +} `; exports[`Config get 2`] = ` -Get config pluginsDir -test/installed/plugins +{ + code: 0, + err: , + out: Get config pluginsDir +test/installed/plugins, +} `; exports[`Config get 3`] = ` -Get config presetsDir -test/installed/presets +{ + code: 0, + err: , + out: Get config presetsDir +test/installed/presets, +} `; exports[`Config get 4`] = ` -Get config projectsDir -test/installed/projects +{ + code: 0, + err: , + out: Get config projectsDir +test/installed/projects, +} `; exports[`Config get 5`] = ` -Get config registries -[object Object] +{ + code: 0, + err: , + out: Get config registries +[object Object], +} `; exports[`Config get 6`] = ` -Get config version -1.0.0 +{ + code: 0, + err: , + out: Get config version +1.0.0, +} `; exports[`Config set 1`] = ` -Get config appDir -test2 +{ + code: 0, + err: , + out: Get config appDir +test2, +} `; exports[`Config set 2`] = ` -Get config appDir -test +{ + code: 0, + err: , + out: Get config appDir +test, +} `; diff --git a/tests/commands/__snapshots__/create.test.ts.snap b/tests/commands/__snapshots__/create.test.ts.snap index fd74fec..a99b0f0 100644 --- a/tests/commands/__snapshots__/create.test.ts.snap +++ b/tests/commands/__snapshots__/create.test.ts.snap @@ -1,8 +1,11 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`Create package 1`] = ` -User force closed the prompt with 0 null -Create package +{ + code: 1, + err: , + out: Create package ? Org id (org-name) - +User force closed the prompt with 0 null, +} `; diff --git a/tests/commands/__snapshots__/filter.test.ts.snap b/tests/commands/__snapshots__/filter.test.ts.snap index 91619f1..51aaa63 100644 --- a/tests/commands/__snapshots__/filter.test.ts.snap +++ b/tests/commands/__snapshots__/filter.test.ts.snap @@ -1,21 +1,32 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`Filter packages 1`] = ` -Filter plugins by name=Surge XT +{ + code: 0, + err: , + out: Filter plugins by name=Surge XT ┌─────────────────────────┬──────────┬─────────┬───────────┬────────────┬─────────┬───────────────────────────────┐ │ Id │ Name │ Version │ Installed │ Date │ License │ Tags │ ├─────────────────────────┼──────────┼─────────┼───────────┼────────────┼─────────┼───────────────────────────────┤ -│ surge-synthesizer/surge │ Surge XT │ 1.3.4 │ ✓ │ 2024-08-11 │ gpl-3.0 │ Instrument, Synth, Modulation │ -└─────────────────────────┴──────────┴─────────┴───────────┴────────────┴─────────┴───────────────────────────────┘ +│ surge-synthesizer/surge │ Surge XT │ 1.3.4 │ - │ 2024-08-11 │ gpl-3.0 │ Instrument, Synth, Modulation │ +└─────────────────────────┴──────────┴─────────┴───────────┴────────────┴─────────┴───────────────────────────────┘, +} `; exports[`Filter packages 2`] = ` -Filter plugins by name=Surge XU -No results found +{ + code: 0, + err: , + out: Filter plugins by name=Surge XU +No results found, +} `; exports[`Filter packages 3`] = ` -Filter plugins by license=cc0-1.0 +{ + code: 0, + err: , + out: Filter plugins by license=cc0-1.0 ┌────────────────────────────────────────┬──────────────────────────┬─────────┬───────────┬────────────┬─────────┬───────────────────────────────────┐ │ Id │ Name │ Version │ Installed │ Date │ License │ Tags │ ├────────────────────────────────────────┼──────────────────────────┼─────────┼───────────┼────────────┼─────────┼───────────────────────────────────┤ @@ -42,5 +53,6 @@ Filter plugins by license=cc0-1.0 │ freepats/hang-d-minor │ Hang D Minor │ 1.0.0 │ - │ 2022-03-30 │ cc0-1.0 │ Instrument, Orchestra, Hang, s... │ ├────────────────────────────────────────┼──────────────────────────┼─────────┼───────────┼────────────┼─────────┼───────────────────────────────────┤ │ freepats/glasses-of-water │ Glasses Of Water │ 1.0.0 │ - │ 2019-12-27 │ cc0-1.0 │ Instrument, Orchestra, Glass, ... │ -└────────────────────────────────────────┴──────────────────────────┴─────────┴───────────┴────────────┴─────────┴───────────────────────────────────┘ +└────────────────────────────────────────┴──────────────────────────┴─────────┴───────────┴────────────┴─────────┴───────────────────────────────────┘, +} `; diff --git a/tests/commands/__snapshots__/get.test.ts.snap b/tests/commands/__snapshots__/get.test.ts.snap index 804ad73..4866676 100644 --- a/tests/commands/__snapshots__/get.test.ts.snap +++ b/tests/commands/__snapshots__/get.test.ts.snap @@ -1,26 +1,38 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`Get package 1`] = ` -Get package surge-synthesizer/surge +{ + code: 0, + err: , + out: Get package surge-synthesizer/surge ┌─────────────────────────┬──────────┬─────────┬───────────┬────────────┬─────────┬───────────────────────────────┐ │ Id │ Name │ Version │ Installed │ Date │ License │ Tags │ ├─────────────────────────┼──────────┼─────────┼───────────┼────────────┼─────────┼───────────────────────────────┤ │ surge-synthesizer/surge │ Surge XT │ 1.3.4 │ ✓ │ 2024-08-11 │ gpl-3.0 │ Instrument, Synth, Modulation │ ├─────────────────────────┼──────────┼─────────┼───────────┼────────────┼─────────┼───────────────────────────────┤ │ surge-synthesizer/surge │ Surge XT │ 1.3.1 │ ✓ │ 2024-03-02 │ gpl-3.0 │ Instrument, Synth, Modulation │ -└─────────────────────────┴──────────┴─────────┴───────────┴────────────┴─────────┴───────────────────────────────┘ +└─────────────────────────┴──────────┴─────────┴───────────┴────────────┴─────────┴───────────────────────────────┘, +} `; exports[`Get package 2`] = ` -Get package surge-synthesizer/surge@1.3.1 +{ + code: 0, + err: , + out: Get package surge-synthesizer/surge@1.3.1 ┌─────────────────────────┬──────────┬─────────┬───────────┬────────────┬─────────┬───────────────────────────────┐ │ Id │ Name │ Version │ Installed │ Date │ License │ Tags │ ├─────────────────────────┼──────────┼─────────┼───────────┼────────────┼─────────┼───────────────────────────────┤ │ surge-synthesizer/surge │ Surge XT │ 1.3.1 │ ✓ │ 2024-03-02 │ gpl-3.0 │ Instrument, Synth, Modulation │ -└─────────────────────────┴──────────┴─────────┴───────────┴────────────┴─────────┴───────────────────────────────┘ +└─────────────────────────┴──────────┴─────────┴───────────┴────────────┴─────────┴───────────────────────────────┘, +} `; exports[`Get package 3`] = ` -Get package surge-synthesizer/surge@0.0.0 -No results found +{ + code: 0, + err: , + out: Get package surge-synthesizer/surge@0.0.0 +No results found, +} `; diff --git a/tests/commands/__snapshots__/install.test.ts.snap b/tests/commands/__snapshots__/install.test.ts.snap index 2e5ef11..4a9260d 100644 --- a/tests/commands/__snapshots__/install.test.ts.snap +++ b/tests/commands/__snapshots__/install.test.ts.snap @@ -1,16 +1,28 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`Install package 1`] = ` -Install surge-synthesizer/surge -Installed surge-synthesizer/surge +{ + code: 0, + err: , + out: Install surge-synthesizer/surge +Installed surge-synthesizer/surge, +} `; exports[`Install package 2`] = ` -Install surge-synthesizer/surge@1.3.1 -Installed surge-synthesizer/surge@1.3.1 +{ + code: 0, + err: , + out: Install surge-synthesizer/surge@1.3.1 +Installed surge-synthesizer/surge@1.3.1, +} `; exports[`Install package 3`] = ` -Install surge-synthesizer/surge@0.0.0 -Package surge-synthesizer/surge version 0.0.0 not found in registry +{ + code: 1, + err: , + out: Install surge-synthesizer/surge@0.0.0 +Package surge-synthesizer/surge version 0.0.0 not found in registry, +} `; diff --git a/tests/commands/__snapshots__/list.test.ts.snap b/tests/commands/__snapshots__/list.test.ts.snap index 83cea0a..400c071 100644 --- a/tests/commands/__snapshots__/list.test.ts.snap +++ b/tests/commands/__snapshots__/list.test.ts.snap @@ -1,7 +1,10 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`List packages 1`] = ` -List plugins +{ + code: 0, + err: , + out: List plugins ┌───────────────────────────────────────────┬───────────────────────────┬─────────┬───────────┬────────────┬───────────────┬───────────────────────────────────┐ │ Id │ Name │ Version │ Installed │ Date │ License │ Tags │ ├───────────────────────────────────────────┼───────────────────────────┼─────────┼───────────┼────────────┼───────────────┼───────────────────────────────────┤ @@ -180,14 +183,19 @@ List plugins │ wolf-plugins/wolf-shaper │ Wolf Shaper │ 1.0.2 │ - │ 2023-05-14 │ gpl-3.0 │ Effect, Distortion, Editor │ ├───────────────────────────────────────────┼───────────────────────────┼─────────┼───────────┼────────────┼───────────────┼───────────────────────────────────┤ │ wolf-plugins/wolf-spectrum │ Wolf Spectrum │ 1.0.0 │ - │ 2019-04-14 │ gpl-3.0 │ Effect, Spectrogram, Frequency │ -└───────────────────────────────────────────┴───────────────────────────┴─────────┴───────────┴────────────┴───────────────┴───────────────────────────────────┘ +└───────────────────────────────────────────┴───────────────────────────┴─────────┴───────────┴────────────┴───────────────┴───────────────────────────────────┘, +} `; exports[`List packages 2`] = ` -List plugins +{ + code: 0, + err: , + out: List plugins ┌─────────────────────────┬──────────┬─────────┬───────────┬────────────┬─────────┬───────────────────────────────┐ │ Id │ Name │ Version │ Installed │ Date │ License │ Tags │ ├─────────────────────────┼──────────┼─────────┼───────────┼────────────┼─────────┼───────────────────────────────┤ │ surge-synthesizer/surge │ Surge XT │ 1.3.4 │ ✓ │ 2024-08-11 │ gpl-3.0 │ Instrument, Synth, Modulation │ -└─────────────────────────┴──────────┴─────────┴───────────┴────────────┴─────────┴───────────────────────────────┘ +└─────────────────────────┴──────────┴─────────┴───────────┴────────────┴─────────┴───────────────────────────────┘, +} `; diff --git a/tests/commands/__snapshots__/open.test.ts.snap b/tests/commands/__snapshots__/open.test.ts.snap index 33fa7ce..67dfbd8 100644 --- a/tests/commands/__snapshots__/open.test.ts.snap +++ b/tests/commands/__snapshots__/open.test.ts.snap @@ -1,17 +1,33 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`Open command help 1`] = ` -Usage: index apps open [options] [options...] +{ + code: 0, + err: , + out: Usage: index apps open [options] [options...] Open a package by slug/version Options: -l, --log Enable logging - -h, --help display help for command + -h, --help display help for command, +} `; exports[`Open command install and run steinberg/validator mac 1`] = ` -Open steinberg/validator +{ + code: 0, + err: , + out: Install steinberg/validator +Installed steinberg/validator, +} +`; + +exports[`Open command install and run steinberg/validator mac 2`] = ` +{ + code: 0, + err: , + out: Open steinberg/validator Opened steinberg/validator VST 3.6.14 Plug-in Validator: @@ -26,7 +42,15 @@ VST 3.6.14 Plug-in Validator: -snapshots | List snapshots from all installed Plug-Ins Usage: vstvalidator [options] vst3module - +, +} `; -exports[`Open command with non-existent package 1`] = `Package non-existent/package not found`; +exports[`Open command with non-existent package 1`] = ` +{ + code: 1, + err: , + out: Open non-existent/package +Package non-existent/package not found, +} +`; diff --git a/tests/commands/__snapshots__/reset.test.ts.snap b/tests/commands/__snapshots__/reset.test.ts.snap index c2b05ea..2eef743 100644 --- a/tests/commands/__snapshots__/reset.test.ts.snap +++ b/tests/commands/__snapshots__/reset.test.ts.snap @@ -1,3 +1,10 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`Reset packages 1`] = `plugins cache has been reset`; +exports[`Reset packages 1`] = ` +{ + code: 0, + err: , + out: Reset plugins +Reset complete plugins, +} +`; diff --git a/tests/commands/__snapshots__/scan.test.ts.snap b/tests/commands/__snapshots__/scan.test.ts.snap index 2a3210b..90857a7 100644 --- a/tests/commands/__snapshots__/scan.test.ts.snap +++ b/tests/commands/__snapshots__/scan.test.ts.snap @@ -1,6 +1,10 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`Scan packages 1`] = ` -Scan plugins -Scanned plugins +{ + code: 0, + err: , + out: Scan plugins +Scanned plugins, +} `; diff --git a/tests/commands/__snapshots__/search.test.ts.snap b/tests/commands/__snapshots__/search.test.ts.snap index 1952337..388a187 100644 --- a/tests/commands/__snapshots__/search.test.ts.snap +++ b/tests/commands/__snapshots__/search.test.ts.snap @@ -1,7 +1,10 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`Search packages 1`] = ` -Search plugins +{ + code: 0, + err: , + out: Search plugins ┌─────────────────────────┬─────────────┬─────────┬───────────┬────────────┬─────────┬────────────────────────────────┐ │ Id │ Name │ Version │ Installed │ Date │ License │ Tags │ ├─────────────────────────┼─────────────┼─────────┼───────────┼────────────┼─────────┼────────────────────────────────┤ @@ -10,10 +13,15 @@ Search plugins │ tesselode/flutterbird │ Flutterbird │ 1.0.1 │ - │ 2018-09-06 │ mit │ Effect, Pitch, Modulation │ ├─────────────────────────┼─────────────┼─────────┼───────────┼────────────┼─────────┼────────────────────────────────┤ │ surge-synthesizer/surge │ Surge XT │ 1.3.4 │ ✓ │ 2024-08-11 │ gpl-3.0 │ Instrument, Synth, Modulation │ -└─────────────────────────┴─────────────┴─────────┴───────────┴────────────┴─────────┴────────────────────────────────┘ +└─────────────────────────┴─────────────┴─────────┴───────────┴────────────┴─────────┴────────────────────────────────┘, +} `; exports[`Search packages 2`] = ` -Search plugins -No results found +{ + code: 0, + err: , + out: Search plugins +No results found, +} `; diff --git a/tests/commands/__snapshots__/sync.test.ts.snap b/tests/commands/__snapshots__/sync.test.ts.snap index 8f23ef1..3572b6d 100644 --- a/tests/commands/__snapshots__/sync.test.ts.snap +++ b/tests/commands/__snapshots__/sync.test.ts.snap @@ -1,6 +1,10 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`Sync packages 1`] = ` -Sync plugins -Synced plugins +{ + code: 0, + err: , + out: Sync plugins +Synced plugins, +} `; diff --git a/tests/commands/__snapshots__/uninstall.test.ts.snap b/tests/commands/__snapshots__/uninstall.test.ts.snap index aaceaa4..c35beb4 100644 --- a/tests/commands/__snapshots__/uninstall.test.ts.snap +++ b/tests/commands/__snapshots__/uninstall.test.ts.snap @@ -1,16 +1,28 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`Uninstall package 1`] = ` -Uninstall surge-synthesizer/surge -Uninstalled surge-synthesizer/surge +{ + code: 0, + err: , + out: Uninstall surge-synthesizer/surge +Uninstalled surge-synthesizer/surge, +} `; exports[`Uninstall package 2`] = ` -Uninstall surge-synthesizer/surge@1.3.1 -Uninstalled surge-synthesizer/surge@1.3.1 +{ + code: 0, + err: , + out: Uninstall surge-synthesizer/surge@1.3.1 +Uninstalled surge-synthesizer/surge@1.3.1, +} `; exports[`Uninstall package 3`] = ` -Uninstall surge-synthesizer/surge@0.0.0 -Package surge-synthesizer/surge version 0.0.0 not found in registry +{ + code: 1, + err: , + out: Uninstall surge-synthesizer/surge@0.0.0 +Package surge-synthesizer/surge version 0.0.0 not found in registry, +} `; diff --git a/tests/commands/create.test.ts b/tests/commands/create.test.ts index bc8fece..30462a9 100644 --- a/tests/commands/create.test.ts +++ b/tests/commands/create.test.ts @@ -1,5 +1,5 @@ import { expect, test } from 'vitest'; -import { cliCatch } from '../shared'; +import { cli } from '../shared'; import path from 'path'; import { RegistryType } from '@open-audio-stack/core'; @@ -7,7 +7,5 @@ const APP_DIR: string = 'test'; const PROJECT_DIR: string = path.join(APP_DIR, 'create-project'); test('Create package', async () => { - // The create command may prompt/abort during tests — capture textual output - const res = cliCatch(RegistryType.Plugins, 'create', PROJECT_DIR); - expect((res.stderr + '\n' + res.stdout).trim()).toMatchSnapshot(); + expect(cli(RegistryType.Plugins, 'create', PROJECT_DIR)).toMatchSnapshot(); }); diff --git a/tests/commands/install.test.ts b/tests/commands/install.test.ts index 2bb4084..8c47d49 100644 --- a/tests/commands/install.test.ts +++ b/tests/commands/install.test.ts @@ -1,11 +1,9 @@ import { expect, test } from 'vitest'; -import { cli, cliCatch } from '../shared'; +import { cli } from '../shared'; import { RegistryType } from '@open-audio-stack/core'; test('Install package', async () => { expect(cli(RegistryType.Plugins, 'install', 'surge-synthesizer/surge')).toMatchSnapshot(); expect(cli(RegistryType.Plugins, 'install', 'surge-synthesizer/surge@1.3.1')).toMatchSnapshot(); - // non-existent version may cause execaSync to throw — capture stderr text - const res = cliCatch(RegistryType.Plugins, 'install', 'surge-synthesizer/surge@0.0.0'); - expect(res.stderr.trim()).toMatchSnapshot(); + expect(cli(RegistryType.Plugins, 'install', 'surge-synthesizer/surge@0.0.0')).toMatchSnapshot(); }); diff --git a/tests/commands/open.test.ts b/tests/commands/open.test.ts index 0451587..e037cd6 100644 --- a/tests/commands/open.test.ts +++ b/tests/commands/open.test.ts @@ -1,22 +1,16 @@ import { expect, test } from 'vitest'; -import { cli, cliCatch, cleanOutput } from '../shared'; +import { cli } from '../shared'; import { getSystem } from '@open-audio-stack/core'; test('Open command help', () => { - const result = cli('apps', 'open', '--help'); - expect(cleanOutput(result)).toMatchSnapshot(); + expect(cli('apps', 'open', '--help')).toMatchSnapshot(); }); test(`Open command install and run steinberg/validator ${getSystem()}`, () => { - const installResult = cli('apps', 'install', 'steinberg/validator'); - expect(installResult).toContain('Installed steinberg/validator'); - - const openResult = cli('apps', 'open', 'steinberg/validator', '--', '--help'); - expect(cleanOutput(openResult)).toMatchSnapshot(); + expect(cli('apps', 'install', 'steinberg/validator')).toMatchSnapshot(); + expect(cli('apps', 'open', 'steinberg/validator', '--', '--help')).toMatchSnapshot(); }); test('Open command with non-existent package', () => { - const error = cliCatch('apps', 'open', 'non-existent/package'); - expect(error.exitCode).toBe(1); - expect(cleanOutput(error.stderr)).toMatchSnapshot(); + expect(cli('apps', 'open', 'non-existent/package')).toMatchSnapshot(); }); diff --git a/tests/commands/uninstall.test.ts b/tests/commands/uninstall.test.ts index b68b455..1165f95 100644 --- a/tests/commands/uninstall.test.ts +++ b/tests/commands/uninstall.test.ts @@ -1,5 +1,5 @@ import { expect, test } from 'vitest'; -import { cli, cliCatch } from '../shared'; +import { cli } from '../shared'; import { RegistryType } from '@open-audio-stack/core'; test('Uninstall package', async () => { @@ -8,7 +8,6 @@ test('Uninstall package', async () => { cli(RegistryType.Plugins, 'install', 'surge-synthesizer/surge@1.3.1'); expect(cli(RegistryType.Plugins, 'uninstall', 'surge-synthesizer/surge@1.3.1')).toMatchSnapshot(); - // uninstalling a non-existent version throws — snapshot stderr text - const res = cliCatch(RegistryType.Plugins, 'uninstall', 'surge-synthesizer/surge@0.0.0'); - expect(res.stderr.trim()).toMatchSnapshot(); + + expect(cli(RegistryType.Plugins, 'uninstall', 'surge-synthesizer/surge@0.0.0')).toMatchSnapshot(); }); diff --git a/tests/index.test.ts b/tests/index.test.ts index 4762817..2acd5e9 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -1,27 +1,19 @@ import { expect, test } from 'vitest'; -import { cleanOutput, cliCatch } from './shared'; +import { cli } from './shared'; import { RegistryType } from '@open-audio-stack/core'; test('Root command', async () => { - const error = cliCatch(); - expect(error.exitCode).toBe(1); - expect(cleanOutput(error.stderr)).toMatchSnapshot(); + expect(cli()).toMatchSnapshot(); }); test('Root command plugins', async () => { - const error = cliCatch(RegistryType.Plugins); - expect(error.exitCode).toBe(1); - expect(cleanOutput(error.stderr)).toMatchSnapshot(); + expect(cli(RegistryType.Plugins)).toMatchSnapshot(); }); test('Root command presets', async () => { - const error = cliCatch(RegistryType.Presets); - expect(error.exitCode).toBe(1); - expect(cleanOutput(error.stderr)).toMatchSnapshot(); + expect(cli(RegistryType.Presets)).toMatchSnapshot(); }); test('Root command projects', async () => { - const error = cliCatch(RegistryType.Projects); - expect(error.exitCode).toBe(1); - expect(cleanOutput(error.stderr)).toMatchSnapshot(); + expect(cli(RegistryType.Projects)).toMatchSnapshot(); }); diff --git a/tests/shared.ts b/tests/shared.ts index 6406dba..7f22f22 100644 --- a/tests/shared.ts +++ b/tests/shared.ts @@ -13,35 +13,28 @@ expect.addSnapshotSerializer({ test: val => typeof val === 'string', }); -export function cli(...args: string[]): string { - const result: SyncResult = execaSync('node', [CLI_PATH, ...args], { - env: { ...process.env, NODE_OPTIONS: '--no-warnings=ExperimentalWarning' }, - }); - return cleanOutput(result.stdout as string); -} - export type CliResult = { - exitCode: number | null; - stdout: string; - stderr: string; + code: number | null; + out: string; + err: string; }; -export function cliCatch(...args: string[]): CliResult { +export function cli(...args: string[]): CliResult { try { const result: SyncResult = execaSync('node', [CLI_PATH, ...args], { env: { ...process.env, NODE_OPTIONS: '--no-warnings=ExperimentalWarning' }, }); return { - exitCode: result.exitCode ?? 0, - stdout: cleanOutput(String(result.stdout ?? '')), - stderr: cleanOutput(String(result.stderr ?? '')), + code: result.exitCode ?? 0, + out: cleanOutput(String(result.stdout ?? '')), + err: cleanOutput(String(result.stderr ?? '')), }; } catch (error: any) { - // execa throws an error with stdout/stderr and exitCode properties - const exitCode = typeof error.exitCode === 'number' ? error.exitCode : 1; - const stdout = cleanOutput(String(error.stdout ?? '')); - const stderr = cleanOutput(String(error.stderr ?? error.message ?? '')); - return { exitCode, stdout, stderr }; + return { + code: error.exitCode ?? 1, + out: cleanOutput(String(error.stdout ?? '')), + err: cleanOutput(String(error.stderr ?? error.message ?? '')), + }; } } From 7e2f09d18beed0e1f500cf28f39db4084a2dc79b Mon Sep 17 00:00:00 2001 From: Kim T Date: Sat, 20 Dec 2025 22:50:37 -0800 Subject: [PATCH 4/6] Remove system-specific test snapshots --- tests/commands/__snapshots__/open.test.ts.snap | 4 ++-- tests/commands/open.test.ts | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/commands/__snapshots__/open.test.ts.snap b/tests/commands/__snapshots__/open.test.ts.snap index 67dfbd8..b371d0c 100644 --- a/tests/commands/__snapshots__/open.test.ts.snap +++ b/tests/commands/__snapshots__/open.test.ts.snap @@ -14,7 +14,7 @@ Options: } `; -exports[`Open command install and run steinberg/validator mac 1`] = ` +exports[`Open command install and run steinberg/validator 1`] = ` { code: 0, err: , @@ -23,7 +23,7 @@ Installed steinberg/validator, } `; -exports[`Open command install and run steinberg/validator mac 2`] = ` +exports[`Open command install and run steinberg/validator 2`] = ` { code: 0, err: , diff --git a/tests/commands/open.test.ts b/tests/commands/open.test.ts index e037cd6..0eafce8 100644 --- a/tests/commands/open.test.ts +++ b/tests/commands/open.test.ts @@ -1,12 +1,11 @@ import { expect, test } from 'vitest'; import { cli } from '../shared'; -import { getSystem } from '@open-audio-stack/core'; test('Open command help', () => { expect(cli('apps', 'open', '--help')).toMatchSnapshot(); }); -test(`Open command install and run steinberg/validator ${getSystem()}`, () => { +test('Open command install and run steinberg/validator', () => { expect(cli('apps', 'install', 'steinberg/validator')).toMatchSnapshot(); expect(cli('apps', 'open', 'steinberg/validator', '--', '--help')).toMatchSnapshot(); }); From eefc249351cc2e0424417f1f40d5f1eed49953e6 Mon Sep 17 00:00:00 2001 From: Kim T Date: Sat, 20 Dec 2025 22:52:36 -0800 Subject: [PATCH 5/6] Disable open test --- tests/commands/open.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/commands/open.test.ts b/tests/commands/open.test.ts index 0eafce8..b71dc43 100644 --- a/tests/commands/open.test.ts +++ b/tests/commands/open.test.ts @@ -7,7 +7,7 @@ test('Open command help', () => { test('Open command install and run steinberg/validator', () => { expect(cli('apps', 'install', 'steinberg/validator')).toMatchSnapshot(); - expect(cli('apps', 'open', 'steinberg/validator', '--', '--help')).toMatchSnapshot(); + // expect(cli('apps', 'open', 'steinberg/validator', '--', '--help')).toMatchSnapshot(); }); test('Open command with non-existent package', () => { From 21cb08ec648673271cd5a9b48354f72b8a9f3b93 Mon Sep 17 00:00:00 2001 From: Kim T Date: Sat, 20 Dec 2025 22:58:37 -0800 Subject: [PATCH 6/6] 3.0.6 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index df914a0..6dcce60 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@studiorack/cli", - "version": "3.0.5", + "version": "3.0.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@studiorack/cli", - "version": "3.0.5", + "version": "3.0.6", "license": "MIT", "dependencies": { "@open-audio-stack/core": "^0.1.47", diff --git a/package.json b/package.json index e280aa2..12bed8f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@studiorack/cli", - "version": "3.0.5", + "version": "3.0.6", "description": "Audio project manager tool", "type": "module", "main": "./build/index.js",