feat(cli): add prdy, quality-rails, and wizard subcommands (#104)
Some checks failed
ci/woodpecker/push/ci Pipeline failed

Co-authored-by: Jason Woltje <jason@diversecanvas.com>
Co-committed-by: Jason Woltje <jason@diversecanvas.com>
This commit was merged in pull request #104.
This commit is contained in:
2026-03-15 01:05:31 +00:00
committed by jason.woltje
parent c4e52085e3
commit 9f036242fa
4 changed files with 112 additions and 2 deletions

View File

@@ -21,6 +21,9 @@
"test": "vitest run --passWithNoTests"
},
"dependencies": {
"@mosaic/mosaic": "workspace:^",
"@mosaic/prdy": "workspace:^",
"@mosaic/quality-rails": "workspace:^",
"ink": "^5.0.0",
"ink-text-input": "^6.0.0",
"ink-spinner": "^5.0.0",
@@ -29,6 +32,7 @@
"commander": "^13.0.0"
},
"devDependencies": {
"@types/node": "^22.0.0",
"@types/react": "^18.3.0",
"tsx": "^4.0.0",
"typescript": "^5.8.0",

View File

@@ -1,6 +1,8 @@
#!/usr/bin/env node
import { Command } from 'commander';
import { buildPrdyCli } from '@mosaic/prdy';
import { createQualityRailsCli } from '@mosaic/quality-rails';
const program = new Command();
@@ -25,4 +27,85 @@ program
);
});
// prdy subcommand
// buildPrdyCli() returns a wrapper Command; extract the 'prdy' subcommand from it.
// Type cast is required because @mosaic/prdy uses commander@12 while @mosaic/cli uses commander@13.
const prdyWrapper = buildPrdyCli();
const prdyCmd = prdyWrapper.commands.find((c) => c.name() === 'prdy');
if (prdyCmd !== undefined) {
program.addCommand(prdyCmd as unknown as Command);
}
// quality-rails subcommand
// createQualityRailsCli() returns a wrapper Command; extract the 'quality-rails' subcommand.
const qrWrapper = createQualityRailsCli();
const qrCmd = qrWrapper.commands.find((c) => c.name() === 'quality-rails');
if (qrCmd !== undefined) {
program.addCommand(qrCmd as unknown as Command);
}
// wizard subcommand — wraps @mosaic/mosaic installation wizard
program
.command('wizard')
.description('Run the Mosaic installation wizard')
.option('--non-interactive', 'Run without prompts (uses defaults + flags)')
.option('--source-dir <path>', 'Source directory for framework files')
.option('--mosaic-home <path>', 'Target config directory')
.option('--name <name>', 'Agent name')
.option('--role <description>', 'Agent role description')
.option('--style <style>', 'Communication style: direct|friendly|formal')
.option('--accessibility <prefs>', 'Accessibility preferences')
.option('--guardrails <rules>', 'Custom guardrails')
.option('--user-name <name>', 'Your name')
.option('--pronouns <pronouns>', 'Your pronouns')
.option('--timezone <tz>', 'Your timezone')
.action(async (opts: Record<string, string | boolean | undefined>) => {
// Dynamic import to avoid loading wizard deps for other commands
const {
runWizard,
ClackPrompter,
HeadlessPrompter,
createConfigService,
WizardCancelledError,
DEFAULT_MOSAIC_HOME,
} = await import('@mosaic/mosaic');
try {
const mosaicHome = (opts['mosaicHome'] as string | undefined) ?? DEFAULT_MOSAIC_HOME;
const sourceDir = (opts['sourceDir'] as string | undefined) ?? mosaicHome;
const prompter = opts['nonInteractive'] ? new HeadlessPrompter() : new ClackPrompter();
const configService = createConfigService(mosaicHome, sourceDir);
await runWizard({
mosaicHome,
sourceDir,
prompter,
configService,
cliOverrides: {
soul: {
agentName: opts['name'] as string | undefined,
roleDescription: opts['role'] as string | undefined,
communicationStyle: opts['style'] as 'direct' | 'friendly' | 'formal' | undefined,
accessibility: opts['accessibility'] as string | undefined,
customGuardrails: opts['guardrails'] as string | undefined,
},
user: {
userName: opts['userName'] as string | undefined,
pronouns: opts['pronouns'] as string | undefined,
timezone: opts['timezone'] as string | undefined,
},
},
});
} catch (err) {
if (err instanceof WizardCancelledError) {
console.log('\nWizard cancelled.');
process.exit(0);
}
console.error('Wizard failed:', err);
process.exit(1);
}
});
program.parse();

View File

@@ -1,4 +1,7 @@
#!/usr/bin/env node
import { resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { Command } from 'commander';
import { ClackPrompter } from './prompter/clack-prompter.js';
import { HeadlessPrompter } from './prompter/headless-prompter.js';
@@ -8,7 +11,12 @@ import { WizardCancelledError } from './errors.js';
import { VERSION, DEFAULT_MOSAIC_HOME } from './constants.js';
import type { CommunicationStyle } from './types.js';
export { VERSION };
export { VERSION, DEFAULT_MOSAIC_HOME };
export { runWizard } from './wizard.js';
export { ClackPrompter } from './prompter/clack-prompter.js';
export { HeadlessPrompter } from './prompter/headless-prompter.js';
export { createConfigService } from './config/config-service.js';
export { WizardCancelledError } from './errors.js';
const program = new Command()
.name('mosaic-wizard')
@@ -70,4 +78,7 @@ program
}
});
const entryPath = process.argv[1] ? resolve(process.argv[1]) : '';
if (entryPath.length > 0 && entryPath === fileURLToPath(import.meta.url)) {
program.parse();
}

12
pnpm-lock.yaml generated
View File

@@ -268,6 +268,15 @@ importers:
packages/cli:
dependencies:
'@mosaic/mosaic':
specifier: workspace:^
version: link:../mosaic
'@mosaic/prdy':
specifier: workspace:^
version: link:../prdy
'@mosaic/quality-rails':
specifier: workspace:^
version: link:../quality-rails
commander:
specifier: ^13.0.0
version: 13.1.0
@@ -287,6 +296,9 @@ importers:
specifier: ^4.8.0
version: 4.8.3
devDependencies:
'@types/node':
specifier: ^22.0.0
version: 22.19.15
'@types/react':
specifier: ^18.3.0
version: 18.3.28