Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
}
});
Expand Down Expand Up @@ -63,6 +62,7 @@ program
}
}

const { InitCommand } = await import('../core/init.js');
const initCommand = new InitCommand({
tools: options?.tools,
});
Expand Down
7 changes: 4 additions & 3 deletions src/commands/change.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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 })),
Expand Down Expand Up @@ -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 })),
Expand Down
2 changes: 1 addition & 1 deletion src/commands/completion.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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,
Expand Down
7 changes: 3 additions & 4 deletions src/commands/show.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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<void> {
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<ItemType>({
message: 'What would you like to show?',
choices: [
Expand All @@ -44,6 +44,7 @@ export class ShowCommand {
}

private async runInteractiveByType(type: ItemType, options: { json?: boolean; noInteractive?: boolean; [k: string]: any }): Promise<void> {
const { select } = await import('@inquirer/prompts');
if (type === 'change') {
const changes = await getActiveChangeIds();
if (changes.length === 0) {
Expand Down Expand Up @@ -135,5 +136,3 @@ export class ShowCommand {
return false;
}
}


9 changes: 5 additions & 4 deletions src/commands/spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -70,9 +69,10 @@ export class SpecCommand {

async show(specId?: string, options: ShowOptions = {}): Promise<void> {
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 })),
Expand Down Expand Up @@ -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 })),
Expand Down Expand Up @@ -247,4 +248,4 @@ export function registerSpecCommand(rootProgram: typeof program) {
});

return specCommand;
}
}
28 changes: 24 additions & 4 deletions src/commands/validate.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { select } from '@inquirer/prompts';
import ora from 'ora';
import path from 'path';
import { Validator } from '../core/validation/validator.js';
Expand Down Expand Up @@ -29,7 +28,7 @@ interface BulkItemResult {

export class ValidateCommand {
async execute(itemName: string | undefined, options: ExecuteOptions = {}): Promise<void> {
const interactive = isInteractive(options.noInteractive);
const interactive = isInteractive(options);

// Handle bulk flags first
if (options.all || options.changes || options.specs) {
Expand Down Expand Up @@ -64,6 +63,7 @@ export class ValidateCommand {
}

private async runInteractiveSelector(opts: { strict: boolean; json: boolean; concurrency?: string }): Promise<void> {
const { select } = await import('@inquirer/prompts');
const choice = await select({
message: 'What would you like to validate?',
choices: [
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -301,5 +323,3 @@ function getPlannedType(index: number, changeIds: string[], specIds: string[]):
if (specIndex >= 0 && specIndex < specIds.length) return 'spec';
return undefined;
}


5 changes: 4 additions & 1 deletion src/core/archive.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -256,6 +258,7 @@ export class ArchiveCommand {
}

private async selectChange(changesDir: string): Promise<string | null> {
const { select } = await import('@inquirer/prompts');
// Get all directories in changes (excluding archive)
const entries = await fs.readdir(changesDir, { withFileTypes: true });
const changeDirs = entries
Expand Down
21 changes: 18 additions & 3 deletions src/utils/interactive.ts
Original file line number Diff line number Diff line change
@@ -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;
}


Loading