diff --git a/src/cli/index.ts b/src/cli/index.ts index 0cd5885f..9ae2c239 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -3,7 +3,6 @@ import { createRequire } from 'module'; import ora from 'ora'; import path from 'path'; import { promises as fs } from 'fs'; -import { InitCommand } from '../core/init.js'; import { AI_TOOLS } from '../core/config.js'; import { UpdateCommand } from '../core/update.js'; import { ListCommand } from '../core/list.js'; @@ -30,7 +29,7 @@ program.option('--no-color', 'Disable color output'); // Apply global flags before any command runs program.hook('preAction', (thisCommand) => { const opts = thisCommand.opts(); - if (opts.noColor) { + if (opts.color === false) { process.env.NO_COLOR = '1'; } }); @@ -63,6 +62,7 @@ program } } + const { InitCommand } = await import('../core/init.js'); const initCommand = new InitCommand({ tools: options?.tools, }); diff --git a/src/commands/change.ts b/src/commands/change.ts index 3d10e831..051b4697 100644 --- a/src/commands/change.ts +++ b/src/commands/change.ts @@ -1,6 +1,5 @@ import { promises as fs } from 'fs'; import path from 'path'; -import { select } from '@inquirer/prompts'; import { JsonConverter } from '../core/converters/json-converter.js'; import { Validator } from '../core/validation/validator.js'; import { ChangeParser } from '../core/parsers/change-parser.js'; @@ -30,9 +29,10 @@ export class ChangeCommand { const changesPath = path.join(process.cwd(), 'openspec', 'changes'); if (!changeName) { - const canPrompt = isInteractive(options?.noInteractive); + const canPrompt = isInteractive(options); const changes = await this.getActiveChanges(changesPath); if (canPrompt && changes.length > 0) { + const { select } = await import('@inquirer/prompts'); const selected = await select({ message: 'Select a change to show', choices: changes.map(id => ({ name: id, value: id })), @@ -186,9 +186,10 @@ export class ChangeCommand { const changesPath = path.join(process.cwd(), 'openspec', 'changes'); if (!changeName) { - const canPrompt = isInteractive(options?.noInteractive); + const canPrompt = isInteractive(options); const changes = await getActiveChangeIds(); if (canPrompt && changes.length > 0) { + const { select } = await import('@inquirer/prompts'); const selected = await select({ message: 'Select a change to validate', choices: changes.map(id => ({ name: id, value: id })), diff --git a/src/commands/completion.ts b/src/commands/completion.ts index 2baedc0c..56c075ed 100644 --- a/src/commands/completion.ts +++ b/src/commands/completion.ts @@ -1,5 +1,4 @@ import ora from 'ora'; -import { confirm } from '@inquirer/prompts'; import { CompletionFactory } from '../core/completions/factory.js'; import { COMMAND_REGISTRY } from '../core/completions/command-registry.js'; import { detectShell, SupportedShell } from '../utils/shell-detection.js'; @@ -179,6 +178,7 @@ export class CompletionCommand { // Prompt for confirmation unless --yes flag is provided if (!skipConfirmation) { + const { confirm } = await import('@inquirer/prompts'); const confirmed = await confirm({ message: 'Remove OpenSpec configuration from ~/.zshrc?', default: false, diff --git a/src/commands/show.ts b/src/commands/show.ts index 4aac38d7..6413b595 100644 --- a/src/commands/show.ts +++ b/src/commands/show.ts @@ -1,4 +1,3 @@ -import { select } from '@inquirer/prompts'; import path from 'path'; import { isInteractive } from '../utils/interactive.js'; import { getActiveChangeIds, getSpecIds } from '../utils/item-discovery.js'; @@ -13,11 +12,12 @@ const SPEC_FLAG_KEYS = new Set(['requirements', 'scenarios', 'requirement']); export class ShowCommand { async execute(itemName?: string, options: { json?: boolean; type?: string; noInteractive?: boolean; [k: string]: any } = {}): Promise { - const interactive = isInteractive(options.noInteractive); + const interactive = isInteractive(options); const typeOverride = this.normalizeType(options.type); if (!itemName) { if (interactive) { + const { select } = await import('@inquirer/prompts'); const type = await select({ message: 'What would you like to show?', choices: [ @@ -44,6 +44,7 @@ export class ShowCommand { } private async runInteractiveByType(type: ItemType, options: { json?: boolean; noInteractive?: boolean; [k: string]: any }): Promise { + const { select } = await import('@inquirer/prompts'); if (type === 'change') { const changes = await getActiveChangeIds(); if (changes.length === 0) { @@ -135,5 +136,3 @@ export class ShowCommand { return false; } } - - diff --git a/src/commands/spec.ts b/src/commands/spec.ts index 8f4c9a4c..d28052f1 100644 --- a/src/commands/spec.ts +++ b/src/commands/spec.ts @@ -4,7 +4,6 @@ import { join } from 'path'; import { MarkdownParser } from '../core/parsers/markdown-parser.js'; import { Validator } from '../core/validation/validator.js'; import type { Spec } from '../core/schemas/index.js'; -import { select } from '@inquirer/prompts'; import { isInteractive } from '../utils/interactive.js'; import { getSpecIds } from '../utils/item-discovery.js'; @@ -70,9 +69,10 @@ export class SpecCommand { async show(specId?: string, options: ShowOptions = {}): Promise { if (!specId) { - const canPrompt = isInteractive(options?.noInteractive); + const canPrompt = isInteractive(options); const specIds = await getSpecIds(); if (canPrompt && specIds.length > 0) { + const { select } = await import('@inquirer/prompts'); specId = await select({ message: 'Select a spec to show', choices: specIds.map(id => ({ name: id, value: id })), @@ -204,9 +204,10 @@ export function registerSpecCommand(rootProgram: typeof program) { .action(async (specId: string | undefined, options: { strict?: boolean; json?: boolean; noInteractive?: boolean }) => { try { if (!specId) { - const canPrompt = isInteractive(options?.noInteractive); + const canPrompt = isInteractive(options); const specIds = await getSpecIds(); if (canPrompt && specIds.length > 0) { + const { select } = await import('@inquirer/prompts'); specId = await select({ message: 'Select a spec to validate', choices: specIds.map(id => ({ name: id, value: id })), @@ -247,4 +248,4 @@ export function registerSpecCommand(rootProgram: typeof program) { }); return specCommand; -} \ No newline at end of file +} diff --git a/src/commands/validate.ts b/src/commands/validate.ts index 26545509..a5323a64 100644 --- a/src/commands/validate.ts +++ b/src/commands/validate.ts @@ -1,4 +1,3 @@ -import { select } from '@inquirer/prompts'; import ora from 'ora'; import path from 'path'; import { Validator } from '../core/validation/validator.js'; @@ -29,7 +28,7 @@ interface BulkItemResult { export class ValidateCommand { async execute(itemName: string | undefined, options: ExecuteOptions = {}): Promise { - const interactive = isInteractive(options.noInteractive); + const interactive = isInteractive(options); // Handle bulk flags first if (options.all || options.changes || options.specs) { @@ -64,6 +63,7 @@ export class ValidateCommand { } private async runInteractiveSelector(opts: { strict: boolean; json: boolean; concurrency?: string }): Promise { + const { select } = await import('@inquirer/prompts'); const choice = await select({ message: 'What would you like to validate?', choices: [ @@ -212,6 +212,28 @@ export class ValidateCommand { }); } + if (queue.length === 0) { + spinner?.stop(); + + const summary = { + totals: { items: 0, passed: 0, failed: 0 }, + byType: { + ...(scope.changes ? { change: { items: 0, passed: 0, failed: 0 } } : {}), + ...(scope.specs ? { spec: { items: 0, passed: 0, failed: 0 } } : {}), + }, + } as const; + + if (opts.json) { + const out = { items: [] as BulkItemResult[], summary, version: '1.0' }; + console.log(JSON.stringify(out, null, 2)); + } else { + console.log('No items found to validate.'); + } + + process.exitCode = 0; + return; + } + const results: BulkItemResult[] = []; let index = 0; let running = 0; @@ -301,5 +323,3 @@ function getPlannedType(index: number, changeIds: string[], specIds: string[]): if (specIndex >= 0 && specIndex < specIds.length) return 'spec'; return undefined; } - - diff --git a/src/core/archive.ts b/src/core/archive.ts index bf94ee59..c9756024 100644 --- a/src/core/archive.ts +++ b/src/core/archive.ts @@ -1,6 +1,5 @@ import { promises as fs } from 'fs'; import path from 'path'; -import { select, confirm } from '@inquirer/prompts'; import { FileSystemUtils } from '../utils/file-system.js'; import { getTaskProgressForChange, formatTaskStatus } from '../utils/task-progress.js'; import { Validator } from './validation/validator.js'; @@ -125,6 +124,7 @@ export class ArchiveCommand { const timestamp = new Date().toISOString(); if (!options.yes) { + const { confirm } = await import('@inquirer/prompts'); const proceed = await confirm({ message: chalk.yellow('⚠️ WARNING: Skipping validation may archive invalid specs. Continue? (y/N)'), default: false @@ -149,6 +149,7 @@ export class ArchiveCommand { const incompleteTasks = Math.max(progress.total - progress.completed, 0); if (incompleteTasks > 0) { if (!options.yes) { + const { confirm } = await import('@inquirer/prompts'); const proceed = await confirm({ message: `Warning: ${incompleteTasks} incomplete task(s) found. Continue?`, default: false @@ -179,6 +180,7 @@ export class ArchiveCommand { let shouldUpdateSpecs = true; if (!options.yes) { + const { confirm } = await import('@inquirer/prompts'); shouldUpdateSpecs = await confirm({ message: 'Proceed with spec updates?', default: true @@ -256,6 +258,7 @@ export class ArchiveCommand { } private async selectChange(changesDir: string): Promise { + const { select } = await import('@inquirer/prompts'); // Get all directories in changes (excluding archive) const entries = await fs.readdir(changesDir, { withFileTypes: true }); const changeDirs = entries diff --git a/src/utils/interactive.ts b/src/utils/interactive.ts index d383382b..3b73fa3b 100644 --- a/src/utils/interactive.ts +++ b/src/utils/interactive.ts @@ -1,7 +1,22 @@ -export function isInteractive(noInteractiveFlag?: boolean): boolean { - if (noInteractiveFlag) return false; +type InteractiveOptions = { + /** + * Explicit "disable prompts" flag passed by internal callers. + */ + noInteractive?: boolean; + /** + * Commander-style negated option: `--no-interactive` sets this to false. + */ + interactive?: boolean; +}; + +function resolveNoInteractive(value?: boolean | InteractiveOptions): boolean { + if (typeof value === 'boolean') return value; + return value?.noInteractive === true || value?.interactive === false; +} + +export function isInteractive(value?: boolean | InteractiveOptions): boolean { + if (resolveNoInteractive(value)) return false; if (process.env.OPEN_SPEC_INTERACTIVE === '0') return false; return !!process.stdin.isTTY; } -