Skip to content

Commit 01c791b

Browse files
author
Marvin Zhang
committed
feat: add JSON output option to various commands
1 parent 5e758c4 commit 01c791b

File tree

9 files changed

+190
-9
lines changed

9 files changed

+190
-9
lines changed

packages/cli/src/commands/backfill.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export interface BackfillOptions {
2929
includeAssignee?: boolean;
3030
includeTransitions?: boolean;
3131
specs?: string[]; // specific specs to target
32+
json?: boolean;
3233
}
3334

3435
/**
@@ -43,19 +44,22 @@ export function backfillCommand(): Command {
4344
.option('--assignee', 'Include assignee from first commit author')
4445
.option('--transitions', 'Include full status transition history')
4546
.option('--all', 'Include all optional fields (assignee + transitions)')
47+
.option('--json', 'Output as JSON')
4648
.action(async (specs: string[] | undefined, options: {
4749
dryRun?: boolean;
4850
force?: boolean;
4951
assignee?: boolean;
5052
transitions?: boolean;
5153
all?: boolean;
54+
json?: boolean;
5255
}) => {
5356
await backfillTimestamps({
5457
dryRun: options.dryRun,
5558
force: options.force,
5659
includeAssignee: options.assignee || options.all,
5760
includeTransitions: options.transitions || options.all,
5861
specs: specs && specs.length > 0 ? specs : undefined,
62+
json: options.json,
5963
});
6064
});
6165
}

packages/cli/src/commands/board.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,14 @@ export function boardCommand(): Command {
2121
.option('--completion-only', 'Show only completion summary (no kanban)')
2222
.option('--tag <tag>', 'Filter by tag')
2323
.option('--assignee <name>', 'Filter by assignee')
24+
.option('--json', 'Output as JSON')
2425
.action(async (options: {
2526
showComplete?: boolean;
2627
simple?: boolean;
2728
completionOnly?: boolean;
2829
tag?: string;
2930
assignee?: string;
31+
json?: boolean;
3032
}) => {
3133
await showBoard(options);
3234
});
@@ -38,6 +40,7 @@ export async function showBoard(options: {
3840
completionOnly?: boolean;
3941
tag?: string;
4042
assignee?: string;
43+
json?: boolean;
4144
}): Promise<void> {
4245
// Auto-check for conflicts before display
4346
await autoCheckIfEnabled();
@@ -61,7 +64,11 @@ export async function showBoard(options: {
6164
);
6265

6366
if (specs.length === 0) {
64-
console.log(chalk.dim('No specs found.'));
67+
if (options.json) {
68+
console.log(JSON.stringify({ columns: {}, total: 0 }, null, 2));
69+
} else {
70+
console.log(chalk.dim('No specs found.'));
71+
}
6572
return;
6673
}
6774

@@ -85,6 +92,32 @@ export async function showBoard(options: {
8592
}
8693
}
8794

95+
// JSON output
96+
if (options.json) {
97+
const completionMetrics = calculateCompletion(specs);
98+
const velocityMetrics = calculateVelocityMetrics(specs);
99+
100+
const jsonOutput = {
101+
columns: {
102+
planned: columns.planned.map(s => ({ path: s.path, priority: s.frontmatter.priority, assignee: s.frontmatter.assignee, tags: s.frontmatter.tags })),
103+
'in-progress': columns['in-progress'].map(s => ({ path: s.path, priority: s.frontmatter.priority, assignee: s.frontmatter.assignee, tags: s.frontmatter.tags })),
104+
complete: columns.complete.map(s => ({ path: s.path, priority: s.frontmatter.priority, assignee: s.frontmatter.assignee, tags: s.frontmatter.tags })),
105+
},
106+
summary: {
107+
total: completionMetrics.totalSpecs,
108+
active: completionMetrics.activeSpecs,
109+
complete: completionMetrics.completeSpecs,
110+
completionRate: completionMetrics.score,
111+
velocity: {
112+
avgCycleTime: velocityMetrics.cycleTime.average,
113+
throughputPerWeek: velocityMetrics.throughput.perWeek / 7 * 7,
114+
},
115+
},
116+
};
117+
console.log(JSON.stringify(jsonOutput, null, 2));
118+
return;
119+
}
120+
88121
// Display header
89122
console.log(chalk.bold.cyan('📋 Spec Kanban Board'));
90123

packages/cli/src/commands/check.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ export function checkCommand(): Command {
1313
return new Command('check')
1414
.description('Check for sequence conflicts')
1515
.option('-q, --quiet', 'Brief output')
16-
.action(async (options: { quiet?: boolean }) => {
16+
.option('--json', 'Output as JSON')
17+
.action(async (options: { quiet?: boolean; json?: boolean }) => {
1718
const hasNoConflicts = await checkSpecs(options);
1819
process.exit(hasNoConflicts ? 0 : 1);
1920
});
@@ -25,6 +26,7 @@ export function checkCommand(): Command {
2526
export async function checkSpecs(options: {
2627
quiet?: boolean;
2728
silent?: boolean;
29+
json?: boolean;
2830
} = {}): Promise<boolean> {
2931
const config = await loadConfig();
3032
const cwd = process.cwd();
@@ -59,11 +61,28 @@ export async function checkSpecs(options: {
5961

6062
if (conflicts.length === 0) {
6163
if (!options.quiet && !options.silent) {
62-
console.log(chalk.green('✓ No sequence conflicts detected'));
64+
if (options.json) {
65+
console.log(JSON.stringify({ conflicts: [], hasConflicts: false }, null, 2));
66+
} else {
67+
console.log(chalk.green('✓ No sequence conflicts detected'));
68+
}
6369
}
6470
return true;
6571
}
6672

73+
// JSON output
74+
if (options.json) {
75+
const jsonOutput = {
76+
hasConflicts: true,
77+
conflicts: conflicts.map(([seq, paths]) => ({
78+
sequence: seq,
79+
specs: paths,
80+
})),
81+
};
82+
console.log(JSON.stringify(jsonOutput, null, 2));
83+
return false;
84+
}
85+
6786
// Report conflicts
6887
if (!options.silent) {
6988
if (!options.quiet) {

packages/cli/src/commands/files.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { countTokens } from '@leanspec/core';
1212
export interface FilesOptions {
1313
type?: 'docs' | 'assets';
1414
tree?: boolean;
15+
json?: boolean;
1516
}
1617

1718
export function filesCommand(): Command;
@@ -26,6 +27,7 @@ export function filesCommand(specPath?: string, options: FilesOptions = {}): Com
2627
.argument('<spec>', 'Spec to list files for')
2728
.option('--type <type>', 'Filter by type: docs, assets')
2829
.option('--tree', 'Show tree structure')
30+
.option('--json', 'Output as JSON')
2931
.action(async (target: string, opts: FilesOptions) => {
3032
await showFiles(target, opts);
3133
});
@@ -57,6 +59,34 @@ export async function showFiles(
5759
// Load sub-files
5860
const subFiles = await loadSubFiles(spec.fullPath);
5961

62+
// JSON output
63+
if (options.json) {
64+
const readmeStat = await fs.stat(spec.filePath);
65+
const readmeContent = await fs.readFile(spec.filePath, 'utf-8');
66+
const readmeTokens = await countTokens({ content: readmeContent });
67+
68+
const jsonOutput = {
69+
spec: spec.name,
70+
path: spec.fullPath,
71+
files: [
72+
{
73+
name: 'README.md',
74+
type: 'required',
75+
size: readmeStat.size,
76+
tokens: readmeTokens.total,
77+
},
78+
...subFiles.map(f => ({
79+
name: f.name,
80+
type: f.type,
81+
size: f.size,
82+
})),
83+
],
84+
total: subFiles.length + 1,
85+
};
86+
console.log(JSON.stringify(jsonOutput, null, 2));
87+
return;
88+
}
89+
6090
console.log('');
6191
console.log(chalk.cyan(`📄 Files in ${sanitizeUserInput(spec.name)}`));
6292
console.log('');

packages/cli/src/commands/gantt.ts

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@ export function ganttCommand(): Command {
3939
.option('--weeks <n>', 'Show N weeks (default: 4)', parseInt)
4040
.option('--show-complete', 'Include completed specs')
4141
.option('--critical-path', 'Highlight critical path')
42-
.action(async (options: { weeks?: number; showComplete?: boolean; criticalPath?: boolean }) => {
42+
.option('--json', 'Output as JSON')
43+
.action(async (options: { weeks?: number; showComplete?: boolean; criticalPath?: boolean; json?: boolean }) => {
4344
await showGantt(options);
4445
});
4546
}
@@ -48,6 +49,7 @@ export async function showGantt(options: {
4849
weeks?: number;
4950
showComplete?: boolean;
5051
criticalPath?: boolean;
52+
json?: boolean;
5153
}): Promise<void> {
5254
// Auto-check for conflicts before display
5355
await autoCheckIfEnabled();
@@ -79,8 +81,30 @@ export async function showGantt(options: {
7981
});
8082

8183
if (relevantSpecs.length === 0) {
82-
console.log(chalk.dim('No active specs found.'));
83-
console.log(chalk.dim('Tip: Use --show-complete to include completed specs.'));
84+
if (options.json) {
85+
console.log(JSON.stringify({ specs: [], weeks }, null, 2));
86+
} else {
87+
console.log(chalk.dim('No active specs found.'));
88+
console.log(chalk.dim('Tip: Use --show-complete to include completed specs.'));
89+
}
90+
return;
91+
}
92+
93+
// JSON output
94+
if (options.json) {
95+
const jsonOutput = {
96+
weeks,
97+
specs: relevantSpecs.map(spec => ({
98+
path: spec.path,
99+
status: spec.frontmatter.status,
100+
priority: spec.frontmatter.priority,
101+
created: spec.frontmatter.created,
102+
completed: spec.frontmatter.completed,
103+
due: spec.frontmatter.due,
104+
dependsOn: spec.frontmatter.depends_on,
105+
})),
106+
};
107+
console.log(JSON.stringify(jsonOutput, null, 2));
84108
return;
85109
}
86110

packages/cli/src/commands/list.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export function listCommand(): Command {
2626
.option('--field <name=value...>', 'Filter by custom field (can specify multiple)')
2727
.option('--sort <field>', 'Sort by field (id, created, name, status, priority)', 'id')
2828
.option('--order <order>', 'Sort order (asc, desc)', 'desc')
29+
.option('--json', 'Output as JSON')
2930
.action(async (options: {
3031
archived?: boolean;
3132
status?: SpecStatus;
@@ -35,6 +36,7 @@ export function listCommand(): Command {
3536
field?: string[];
3637
sort?: string;
3738
order?: string;
39+
json?: boolean;
3840
}) => {
3941
const customFields = parseCustomFieldOptions(options.field);
4042
const listOptions: {
@@ -46,6 +48,7 @@ export function listCommand(): Command {
4648
customFields?: Record<string, unknown>;
4749
sortBy?: string;
4850
sortOrder?: 'asc' | 'desc';
51+
json?: boolean;
4952
} = {
5053
showArchived: options.archived,
5154
status: options.status,
@@ -55,6 +58,7 @@ export function listCommand(): Command {
5558
customFields: Object.keys(customFields).length > 0 ? customFields : undefined,
5659
sortBy: options.sort || 'id',
5760
sortOrder: (options.order as 'asc' | 'desc') || 'desc',
61+
json: options.json,
5862
};
5963
await listSpecs(listOptions);
6064
});
@@ -69,6 +73,7 @@ export async function listSpecs(options: {
6973
customFields?: Record<string, unknown>;
7074
sortBy?: string;
7175
sortOrder?: 'asc' | 'desc';
76+
json?: boolean;
7277
} = {}): Promise<void> {
7378
// Auto-check for conflicts before listing
7479
await autoCheckIfEnabled();
@@ -106,7 +111,32 @@ export async function listSpecs(options: {
106111
);
107112

108113
if (specs.length === 0) {
109-
console.log(chalk.dim('No specs found.'));
114+
if (options.json) {
115+
console.log(JSON.stringify({ specs: [], total: 0 }, null, 2));
116+
} else {
117+
console.log(chalk.dim('No specs found.'));
118+
}
119+
return;
120+
}
121+
122+
// JSON output
123+
if (options.json) {
124+
const jsonOutput = {
125+
specs: specs.map(spec => ({
126+
path: spec.path,
127+
name: spec.name,
128+
status: spec.frontmatter.status,
129+
priority: spec.frontmatter.priority,
130+
tags: spec.frontmatter.tags,
131+
assignee: spec.frontmatter.assignee,
132+
created: spec.frontmatter.created,
133+
completed: spec.frontmatter.completed,
134+
subFiles: spec.subFiles?.length || 0,
135+
})),
136+
total: specs.length,
137+
filter: options,
138+
};
139+
console.log(JSON.stringify(jsonOutput, null, 2));
110140
return;
111141
}
112142

packages/cli/src/commands/search.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,14 @@ export function searchCommand(): Command {
2222
.option('--priority <priority>', 'Filter by priority')
2323
.option('--assignee <name>', 'Filter by assignee')
2424
.option('--field <name=value...>', 'Filter by custom field (can specify multiple)')
25+
.option('--json', 'Output as JSON')
2526
.action(async (query: string, options: {
2627
status?: SpecStatus;
2728
tag?: string;
2829
priority?: SpecPriority;
2930
assignee?: string;
3031
field?: string[];
32+
json?: boolean;
3133
}) => {
3234
const customFields = parseCustomFieldOptions(options.field);
3335
await performSearch(query, {
@@ -36,6 +38,7 @@ export function searchCommand(): Command {
3638
priority: options.priority,
3739
assignee: options.assignee,
3840
customFields: Object.keys(customFields).length > 0 ? customFields : undefined,
41+
json: options.json,
3942
});
4043
});
4144
}
@@ -46,6 +49,7 @@ export async function performSearch(query: string, options: {
4649
priority?: SpecPriority;
4750
assignee?: string;
4851
customFields?: Record<string, unknown>;
52+
json?: boolean;
4953
}): Promise<void> {
5054
// Auto-check for conflicts before search
5155
await autoCheckIfEnabled();
@@ -93,6 +97,27 @@ export async function performSearch(query: string, options: {
9397

9498
const { results, metadata } = searchResult;
9599

100+
// JSON output
101+
if (options.json) {
102+
const jsonOutput = {
103+
query,
104+
results: results.map(r => ({
105+
spec: r.spec.path,
106+
score: r.score,
107+
totalMatches: r.totalMatches,
108+
matches: r.matches.map(m => ({
109+
field: m.field,
110+
text: m.text,
111+
lineNumber: m.lineNumber,
112+
})),
113+
})),
114+
metadata,
115+
filters: filter,
116+
};
117+
console.log(JSON.stringify(jsonOutput, null, 2));
118+
return;
119+
}
120+
96121
// Display results
97122
if (results.length === 0) {
98123
console.log('');

0 commit comments

Comments
 (0)