peaks-cli 1.0.11 → 1.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/peaks.js +0 -0
- package/dist/src/cli/commands/core-artifact-commands.js +23 -0
- package/dist/src/cli/commands/mcp-commands.d.ts +3 -0
- package/dist/src/cli/commands/mcp-commands.js +144 -0
- package/dist/src/cli/commands/openspec-commands.d.ts +3 -0
- package/dist/src/cli/commands/openspec-commands.js +169 -0
- package/dist/src/cli/commands/project-commands.d.ts +3 -0
- package/dist/src/cli/commands/project-commands.js +37 -0
- package/dist/src/cli/commands/request-commands.d.ts +3 -0
- package/dist/src/cli/commands/request-commands.js +140 -0
- package/dist/src/cli/commands/understand-commands.d.ts +3 -0
- package/dist/src/cli/commands/understand-commands.js +78 -0
- package/dist/src/cli/program.js +10 -0
- package/dist/src/services/artifacts/request-artifact-service.d.ts +58 -0
- package/dist/src/services/artifacts/request-artifact-service.js +432 -0
- package/dist/src/services/dashboard/project-dashboard-service.d.ts +64 -0
- package/dist/src/services/dashboard/project-dashboard-service.js +112 -0
- package/dist/src/services/doctor/doctor-service.d.ts +7 -0
- package/dist/src/services/doctor/doctor-service.js +139 -0
- package/dist/src/services/mcp/mcp-apply-service.d.ts +31 -0
- package/dist/src/services/mcp/mcp-apply-service.js +112 -0
- package/dist/src/services/mcp/mcp-call-service.d.ts +17 -0
- package/dist/src/services/mcp/mcp-call-service.js +34 -0
- package/dist/src/services/mcp/mcp-client-service.d.ts +14 -0
- package/dist/src/services/mcp/mcp-client-service.js +49 -0
- package/dist/src/services/mcp/mcp-install-registry.d.ts +11 -0
- package/dist/src/services/mcp/mcp-install-registry.js +38 -0
- package/dist/src/services/mcp/mcp-plan-service.d.ts +29 -0
- package/dist/src/services/mcp/mcp-plan-service.js +109 -0
- package/dist/src/services/mcp/mcp-protocol.d.ts +24 -0
- package/dist/src/services/mcp/mcp-protocol.js +41 -0
- package/dist/src/services/mcp/mcp-scan-service.d.ts +8 -0
- package/dist/src/services/mcp/mcp-scan-service.js +214 -0
- package/dist/src/services/mcp/mcp-stdio-transport.d.ts +10 -0
- package/dist/src/services/mcp/mcp-stdio-transport.js +50 -0
- package/dist/src/services/mcp/mcp-types.d.ts +31 -0
- package/dist/src/services/mcp/mcp-types.js +1 -0
- package/dist/src/services/openspec/openspec-archive-service.d.ts +12 -0
- package/dist/src/services/openspec/openspec-archive-service.js +28 -0
- package/dist/src/services/openspec/openspec-bridge-service.d.ts +16 -0
- package/dist/src/services/openspec/openspec-bridge-service.js +76 -0
- package/dist/src/services/openspec/openspec-render-service.d.ts +38 -0
- package/dist/src/services/openspec/openspec-render-service.js +130 -0
- package/dist/src/services/openspec/openspec-scan-service.d.ts +6 -0
- package/dist/src/services/openspec/openspec-scan-service.js +123 -0
- package/dist/src/services/openspec/openspec-types.d.ts +39 -0
- package/dist/src/services/openspec/openspec-types.js +1 -0
- package/dist/src/services/openspec/openspec-validate-service.d.ts +27 -0
- package/dist/src/services/openspec/openspec-validate-service.js +77 -0
- package/dist/src/services/recommendations/capability-seed-items.js +1 -0
- package/dist/src/services/skills/skill-runbook-service.d.ts +11 -0
- package/dist/src/services/skills/skill-runbook-service.js +60 -0
- package/dist/src/services/standards/project-standards-service.js +4 -9
- package/dist/src/services/understand/understand-scan-service.d.ts +28 -0
- package/dist/src/services/understand/understand-scan-service.js +157 -0
- package/dist/src/services/understand/understand-types.d.ts +24 -0
- package/dist/src/services/understand/understand-types.js +1 -0
- package/dist/src/shared/json-schema-mini.d.ts +10 -0
- package/dist/src/shared/json-schema-mini.js +113 -0
- package/dist/src/shared/paths.d.ts +1 -1
- package/dist/src/shared/paths.js +9 -1
- package/dist/src/shared/version.d.ts +1 -1
- package/dist/src/shared/version.js +1 -1
- package/package.json +1 -6
- package/schemas/doctor-report.schema.json +34 -0
- package/schemas/mcp-apply-result.schema.json +46 -0
- package/schemas/mcp-install-plan.schema.json +71 -0
- package/schemas/mcp-install-spec.schema.json +29 -0
- package/schemas/mcp-server.schema.json +29 -0
- package/schemas/openspec-change-summary.schema.json +68 -0
- package/schemas/openspec-render-request.schema.json +61 -0
- package/schemas/openspec-validation-result.schema.json +36 -0
- package/skills/peaks-prd/SKILL.md +59 -8
- package/skills/peaks-prd/references/artifact-per-request.md +78 -0
- package/skills/peaks-prd/references/workflow.md +7 -5
- package/skills/peaks-qa/SKILL.md +73 -7
- package/skills/peaks-qa/references/artifact-contracts.md +1 -1
- package/skills/peaks-qa/references/artifact-per-request.md +83 -0
- package/skills/peaks-qa/references/openspec-validation-gate.md +55 -0
- package/skills/peaks-qa/references/regression-gates.md +1 -1
- package/skills/peaks-rd/SKILL.md +94 -7
- package/skills/peaks-rd/references/artifact-per-request.md +90 -0
- package/skills/peaks-rd/references/openspec-mcp-cli.md +65 -0
- package/skills/peaks-sc/SKILL.md +44 -0
- package/skills/peaks-sc/references/openspec-commit-boundaries.md +33 -0
- package/skills/peaks-solo/SKILL.md +87 -4
- package/skills/peaks-solo/references/browser-workflow.md +114 -0
- package/skills/peaks-solo/references/external-skill-invocation.md +70 -0
- package/skills/peaks-solo/references/openspec-mcp-workflow.md +53 -0
- package/skills/peaks-solo/references/workflow.md +1 -1
- package/skills/peaks-txt/SKILL.md +42 -0
- package/skills/peaks-ui/SKILL.md +57 -33
- package/skills/peaks-ui/references/artifact-per-request.md +71 -0
- package/skills/peaks-ui/references/workflow.md +8 -11
package/bin/peaks.js
CHANGED
|
File without changes
|
|
@@ -6,6 +6,7 @@ import { listProfiles } from '../../services/profiles/profile-service.js';
|
|
|
6
6
|
import { planProxyTest } from '../../services/proxy/proxy-service.js';
|
|
7
7
|
import { runDoctor } from '../../services/doctor/doctor-service.js';
|
|
8
8
|
import { listSkills } from '../../services/skills/skill-registry.js';
|
|
9
|
+
import { inspectSkillRunbook } from '../../services/skills/skill-runbook-service.js';
|
|
9
10
|
import { fail, ok } from '../../shared/result.js';
|
|
10
11
|
import { addJsonOption, failUnsupportedNonDryRun, getErrorMessage, isArtifactProvider, isArtifactSetupStep, printResult } from '../cli-helpers.js';
|
|
11
12
|
export function registerCoreAndArtifactCommands(program, io) {
|
|
@@ -33,6 +34,28 @@ export function registerCoreAndArtifactCommands(program, io) {
|
|
|
33
34
|
process.exitCode = 1;
|
|
34
35
|
}
|
|
35
36
|
});
|
|
37
|
+
addJsonOption(skill
|
|
38
|
+
.command('runbook <name>')
|
|
39
|
+
.description('Inspect a skill Default runbook section and its --apply authorization-note status')).action(async (name, options) => {
|
|
40
|
+
try {
|
|
41
|
+
const inspection = await inspectSkillRunbook(name);
|
|
42
|
+
const result = inspection.ok
|
|
43
|
+
? ok('skill.runbook', inspection)
|
|
44
|
+
: fail('skill.runbook', inspection.hasRunbook ? 'SKILL_RUNBOOK_APPLY_UNGATED' : 'SKILL_RUNBOOK_MISSING', inspection.hasRunbook
|
|
45
|
+
? `Skill ${inspection.name} has ${inspection.destructiveApplyLines.length} destructive --apply command(s) without an authorization/dry-run note`
|
|
46
|
+
: `Skill ${inspection.name} is missing a ## Default runbook section`, inspection, inspection.hasRunbook
|
|
47
|
+
? ['Add an authorization or --dry-run note next to destructive --apply lines in the runbook section']
|
|
48
|
+
: ['Add a `## Default runbook` section to the skill SKILL.md']);
|
|
49
|
+
printResult(io, result, options.json);
|
|
50
|
+
if (!inspection.ok) {
|
|
51
|
+
process.exitCode = 1;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
printResult(io, fail('skill.runbook', 'SKILL_NOT_FOUND', getErrorMessage(error), { name }), options.json);
|
|
56
|
+
process.exitCode = 1;
|
|
57
|
+
}
|
|
58
|
+
});
|
|
36
59
|
const profile = program.command('profile').description('Manage runtime profiles');
|
|
37
60
|
addJsonOption(profile.command('list').description('List available profiles')).action((options) => {
|
|
38
61
|
printResult(io, ok('profile.list', { profiles: listProfiles() }), options.json);
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { InvalidArgumentError } from 'commander';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { scanMcpServers } from '../../services/mcp/mcp-scan-service.js';
|
|
4
|
+
import { planMcpInstall } from '../../services/mcp/mcp-plan-service.js';
|
|
5
|
+
import { applyMcpInstall, rollbackMcpInstall } from '../../services/mcp/mcp-apply-service.js';
|
|
6
|
+
import { callMcpTool } from '../../services/mcp/mcp-call-service.js';
|
|
7
|
+
import { createStdioTransportFromSpec } from '../../services/mcp/mcp-stdio-transport.js';
|
|
8
|
+
import { fail, ok } from '../../shared/result.js';
|
|
9
|
+
import { addJsonOption, failUnsupportedNonDryRun, getErrorMessage, printResult } from '../cli-helpers.js';
|
|
10
|
+
function parsePositiveInteger(value) {
|
|
11
|
+
if (!/^\d+$/.test(value)) {
|
|
12
|
+
throw new InvalidArgumentError('must be a positive integer');
|
|
13
|
+
}
|
|
14
|
+
const parsed = Number(value);
|
|
15
|
+
if (!Number.isSafeInteger(parsed) || parsed < 1) {
|
|
16
|
+
throw new InvalidArgumentError('must be a positive integer');
|
|
17
|
+
}
|
|
18
|
+
return parsed;
|
|
19
|
+
}
|
|
20
|
+
async function resolveCallArgs(options) {
|
|
21
|
+
if (options.argsJson !== undefined && options.args !== undefined) {
|
|
22
|
+
throw new Error('Pass either --args-json or --args, not both');
|
|
23
|
+
}
|
|
24
|
+
const raw = options.argsJson !== undefined
|
|
25
|
+
? options.argsJson
|
|
26
|
+
: options.args !== undefined ? await readFile(options.args, 'utf8') : '{}';
|
|
27
|
+
const parsed = JSON.parse(raw);
|
|
28
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
29
|
+
throw new Error('MCP tool arguments must be a JSON object');
|
|
30
|
+
}
|
|
31
|
+
return parsed;
|
|
32
|
+
}
|
|
33
|
+
export function registerMcpCommands(program, io) {
|
|
34
|
+
const mcp = program.command('mcp').description('Manage Claude Code MCP servers');
|
|
35
|
+
addJsonOption(mcp
|
|
36
|
+
.command('list')
|
|
37
|
+
.alias('scan')
|
|
38
|
+
.description('Scan Claude Code settings for configured MCP servers')
|
|
39
|
+
.option('--project <path>', 'project root to also scan project-level .claude/settings.json')).action(async (options) => {
|
|
40
|
+
try {
|
|
41
|
+
const report = await scanMcpServers(options.project !== undefined ? { projectRoot: options.project } : {});
|
|
42
|
+
printResult(io, ok('mcp.list', report), options.json);
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
printResult(io, fail('mcp.list', 'MCP_LIST_FAILED', getErrorMessage(error), {}, ['Check Claude settings path and permissions before retrying']), options.json);
|
|
46
|
+
process.exitCode = 1;
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
addJsonOption(mcp
|
|
50
|
+
.command('plan')
|
|
51
|
+
.description('Plan an MCP server install diff for a capability (dry-run only)')
|
|
52
|
+
.requiredOption('--capability <id>', 'capability id from the MCP install registry')
|
|
53
|
+
.option('--project <path>', 'project root for scoped scan')
|
|
54
|
+
.option('--dry-run', 'preview the install diff (always true)', true)
|
|
55
|
+
.option('--no-dry-run', 'unsupported: peaks mcp plan never writes settings')).action(async (options) => {
|
|
56
|
+
if (options.dryRun === false) {
|
|
57
|
+
failUnsupportedNonDryRun(io, 'mcp.plan', options.json);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
const planOptions = options.project !== undefined ? { projectRoot: options.project } : {};
|
|
62
|
+
const plan = await planMcpInstall(options.capability, planOptions);
|
|
63
|
+
if (plan.action === 'unknown-capability') {
|
|
64
|
+
printResult(io, fail('mcp.plan', 'MCP_UNKNOWN_CAPABILITY', `No MCP install spec registered for capability ${options.capability}`, plan, plan.nextActions), options.json);
|
|
65
|
+
process.exitCode = 1;
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
printResult(io, ok('mcp.plan', plan, [], plan.nextActions), options.json);
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
printResult(io, fail('mcp.plan', 'MCP_PLAN_FAILED', getErrorMessage(error), { capabilityId: options.capability }, ['Check Claude settings path and the capability id before retrying']), options.json);
|
|
72
|
+
process.exitCode = 1;
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
addJsonOption(mcp
|
|
76
|
+
.command('apply')
|
|
77
|
+
.description('Apply an MCP server install for a capability (writes .claude/settings.json with backup)')
|
|
78
|
+
.requiredOption('--capability <id>', 'capability id from the MCP install registry')
|
|
79
|
+
.option('--yes', 'confirm the write — required for any real side effect')
|
|
80
|
+
.option('--claim', 'take ownership of an existing non-peaks-managed server entry')
|
|
81
|
+
.option('--project <path>', 'project root for scoped scan')).action(async (options) => {
|
|
82
|
+
if (options.yes !== true) {
|
|
83
|
+
printResult(io, fail('mcp.apply', 'MCP_APPLY_REQUIRES_YES', 'Refusing to apply without --yes', { capabilityId: options.capability }, ['Re-run with --yes to confirm the write']), options.json);
|
|
84
|
+
process.exitCode = 1;
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
88
|
+
const applyOptions = {};
|
|
89
|
+
if (options.project !== undefined) {
|
|
90
|
+
applyOptions.projectRoot = options.project;
|
|
91
|
+
}
|
|
92
|
+
if (options.claim === true) {
|
|
93
|
+
applyOptions.claim = true;
|
|
94
|
+
}
|
|
95
|
+
const result = await applyMcpInstall(options.capability, applyOptions);
|
|
96
|
+
printResult(io, ok('mcp.apply', result), options.json);
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
printResult(io, fail('mcp.apply', 'MCP_APPLY_FAILED', getErrorMessage(error), { capabilityId: options.capability }, ['Check the plan first with peaks mcp plan, then re-run apply']), options.json);
|
|
100
|
+
process.exitCode = 1;
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
addJsonOption(mcp
|
|
104
|
+
.command('rollback')
|
|
105
|
+
.description('Restore Claude Code settings.json from a peaks-managed MCP backup file')
|
|
106
|
+
.requiredOption('--backup <path>', 'path to a previously created backup settings.json')).action(async (options) => {
|
|
107
|
+
try {
|
|
108
|
+
const result = await rollbackMcpInstall({ backupPath: options.backup });
|
|
109
|
+
printResult(io, ok('mcp.rollback', result), options.json);
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
printResult(io, fail('mcp.rollback', 'MCP_ROLLBACK_FAILED', getErrorMessage(error), { backupPath: options.backup }, ['Verify the backup path and rerun']), options.json);
|
|
113
|
+
process.exitCode = 1;
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
addJsonOption(mcp
|
|
117
|
+
.command('call')
|
|
118
|
+
.description('Invoke a tool on an installed MCP server via stdio (spawns the server, calls tools/call, closes)')
|
|
119
|
+
.requiredOption('--capability <id>', 'capability id from the MCP install registry')
|
|
120
|
+
.requiredOption('--tool <name>', 'MCP tool name to invoke')
|
|
121
|
+
.option('--args <path>', 'path to a JSON file describing the tool arguments object')
|
|
122
|
+
.option('--args-json <jsonString>', 'inline JSON object describing the tool arguments')
|
|
123
|
+
.option('--timeout <ms>', 'per-request timeout in milliseconds', parsePositiveInteger)).action(async (options) => {
|
|
124
|
+
try {
|
|
125
|
+
const args = await resolveCallArgs(options);
|
|
126
|
+
const factory = createStdioTransportFromSpec;
|
|
127
|
+
const callOptions = {
|
|
128
|
+
capabilityId: options.capability,
|
|
129
|
+
toolName: options.tool,
|
|
130
|
+
args,
|
|
131
|
+
transportFactory: factory
|
|
132
|
+
};
|
|
133
|
+
if (options.timeout !== undefined) {
|
|
134
|
+
callOptions.timeoutMs = Number(options.timeout);
|
|
135
|
+
}
|
|
136
|
+
const result = await callMcpTool(callOptions);
|
|
137
|
+
printResult(io, ok('mcp.call', result), options.json);
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
printResult(io, fail('mcp.call', 'MCP_CALL_FAILED', getErrorMessage(error), { capabilityId: options.capability, toolName: options.tool }, ['Check the capability id, tool name, args JSON, and required env vars before retrying']), options.json);
|
|
141
|
+
process.exitCode = 1;
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { loadOpenSpecChange, scanOpenSpec } from '../../services/openspec/openspec-scan-service.js';
|
|
3
|
+
import { projectOpenSpecToRdInput } from '../../services/openspec/openspec-bridge-service.js';
|
|
4
|
+
import { renderOpenSpecChange } from '../../services/openspec/openspec-render-service.js';
|
|
5
|
+
import { validateOpenSpecChange } from '../../services/openspec/openspec-validate-service.js';
|
|
6
|
+
import { archiveOpenSpecChange } from '../../services/openspec/openspec-archive-service.js';
|
|
7
|
+
import { fail, ok } from '../../shared/result.js';
|
|
8
|
+
import { addJsonOption, getErrorMessage, printResult } from '../cli-helpers.js';
|
|
9
|
+
function resolveScanOptions(project) {
|
|
10
|
+
if (project === undefined) {
|
|
11
|
+
return {};
|
|
12
|
+
}
|
|
13
|
+
return { openspecRoot: `${project.replace(/\\/g, '/').replace(/\/$/, '')}/openspec` };
|
|
14
|
+
}
|
|
15
|
+
async function loadRenderRequest(requestPath) {
|
|
16
|
+
const raw = await readFile(requestPath, 'utf8');
|
|
17
|
+
const parsed = JSON.parse(raw);
|
|
18
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
19
|
+
throw new Error('Render request file must contain a JSON object');
|
|
20
|
+
}
|
|
21
|
+
return parsed;
|
|
22
|
+
}
|
|
23
|
+
export function registerOpenSpecCommands(program, io) {
|
|
24
|
+
const openspec = program.command('openspec').description('Inspect OpenSpec changes inside the target project');
|
|
25
|
+
addJsonOption(openspec
|
|
26
|
+
.command('list')
|
|
27
|
+
.description('List OpenSpec changes detected under <project>/openspec/changes')
|
|
28
|
+
.option('--project <path>', 'project root containing an openspec/ directory')).action(async (options) => {
|
|
29
|
+
try {
|
|
30
|
+
const report = await scanOpenSpec(resolveScanOptions(options.project));
|
|
31
|
+
printResult(io, ok('openspec.list', report), options.json);
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
printResult(io, fail('openspec.list', 'OPENSPEC_LIST_FAILED', getErrorMessage(error), {}, ['Check the project path and openspec/ layout before retrying']), options.json);
|
|
35
|
+
process.exitCode = 1;
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
addJsonOption(openspec
|
|
39
|
+
.command('show')
|
|
40
|
+
.description('Show parsed proposal and tasks progress for a single OpenSpec change')
|
|
41
|
+
.argument('<changeId>', 'OpenSpec change directory name under openspec/changes')
|
|
42
|
+
.option('--project <path>', 'project root containing an openspec/ directory')).action(async (changeId, options) => {
|
|
43
|
+
try {
|
|
44
|
+
const detail = await loadOpenSpecChange(changeId, resolveScanOptions(options.project));
|
|
45
|
+
if (detail === null) {
|
|
46
|
+
printResult(io, fail('openspec.show', 'OPENSPEC_CHANGE_NOT_FOUND', `OpenSpec change ${changeId} was not found`, { changeId }, [`Verify openspec/changes/${changeId}/ exists`]), options.json);
|
|
47
|
+
process.exitCode = 1;
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
printResult(io, ok('openspec.show', detail), options.json);
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
printResult(io, fail('openspec.show', 'OPENSPEC_SHOW_FAILED', getErrorMessage(error), { changeId }, ['Check the project path and openspec/ layout before retrying']), options.json);
|
|
54
|
+
process.exitCode = 1;
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
addJsonOption(openspec
|
|
58
|
+
.command('to-rd')
|
|
59
|
+
.description('Project an OpenSpec change into an RD/SC input shape (acceptance, what-changes, commit boundaries)')
|
|
60
|
+
.argument('<changeId>', 'OpenSpec change directory name under openspec/changes')
|
|
61
|
+
.option('--project <path>', 'project root containing an openspec/ directory')).action(async (changeId, options) => {
|
|
62
|
+
try {
|
|
63
|
+
const projection = await projectOpenSpecToRdInput(changeId, resolveScanOptions(options.project));
|
|
64
|
+
if (projection === null) {
|
|
65
|
+
printResult(io, fail('openspec.to-rd', 'OPENSPEC_CHANGE_NOT_FOUND', `OpenSpec change ${changeId} was not found`, { changeId }, [`Verify openspec/changes/${changeId}/ exists`]), options.json);
|
|
66
|
+
process.exitCode = 1;
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
printResult(io, ok('openspec.to-rd', projection), options.json);
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
printResult(io, fail('openspec.to-rd', 'OPENSPEC_TO_RD_FAILED', getErrorMessage(error), { changeId }, ['Check the project path and openspec/ layout before retrying']), options.json);
|
|
73
|
+
process.exitCode = 1;
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
addJsonOption(openspec
|
|
77
|
+
.command('render')
|
|
78
|
+
.description('Render an OpenSpec change pack from a JSON request file (dry-run by default)')
|
|
79
|
+
.requiredOption('--request <path>', 'path to a JSON file describing the render request')
|
|
80
|
+
.option('--project <path>', 'project root containing an openspec/ directory')
|
|
81
|
+
.option('--apply', 'write the rendered files into openspec/changes/<id>/')
|
|
82
|
+
.option('--overwrite', 'overwrite an existing change directory when --apply is set')).action(async (options) => {
|
|
83
|
+
try {
|
|
84
|
+
const request = await loadRenderRequest(options.request);
|
|
85
|
+
const scan = resolveScanOptions(options.project);
|
|
86
|
+
const renderOptions = {};
|
|
87
|
+
if (scan.openspecRoot !== undefined) {
|
|
88
|
+
renderOptions.openspecRoot = scan.openspecRoot;
|
|
89
|
+
}
|
|
90
|
+
if (options.apply === true) {
|
|
91
|
+
renderOptions.apply = true;
|
|
92
|
+
}
|
|
93
|
+
if (options.overwrite === true) {
|
|
94
|
+
renderOptions.overwrite = true;
|
|
95
|
+
}
|
|
96
|
+
const result = await renderOpenSpecChange(request, renderOptions);
|
|
97
|
+
printResult(io, ok('openspec.render', result), options.json);
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
printResult(io, fail('openspec.render', 'OPENSPEC_RENDER_FAILED', getErrorMessage(error), { requestPath: options.request }, ['Check the request JSON shape and the openspec root before retrying']), options.json);
|
|
101
|
+
process.exitCode = 1;
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
addJsonOption(openspec
|
|
105
|
+
.command('validate')
|
|
106
|
+
.description('Validate an OpenSpec change against internal lint rules (and optionally the external openspec CLI)')
|
|
107
|
+
.argument('<changeId>', 'OpenSpec change directory name under openspec/changes')
|
|
108
|
+
.option('--project <path>', 'project root containing an openspec/ directory')
|
|
109
|
+
.option('--prefer-external', 'use the external openspec CLI when available, fall back to internal lint')).action(async (changeId, options) => {
|
|
110
|
+
try {
|
|
111
|
+
const scan = resolveScanOptions(options.project);
|
|
112
|
+
const validateOptions = {};
|
|
113
|
+
if (scan.openspecRoot !== undefined) {
|
|
114
|
+
validateOptions.openspecRoot = scan.openspecRoot;
|
|
115
|
+
}
|
|
116
|
+
if (options.preferExternal === true) {
|
|
117
|
+
validateOptions.preferExternal = true;
|
|
118
|
+
}
|
|
119
|
+
const result = await validateOpenSpecChange(changeId, validateOptions);
|
|
120
|
+
if (result === null) {
|
|
121
|
+
printResult(io, fail('openspec.validate', 'OPENSPEC_CHANGE_NOT_FOUND', `OpenSpec change ${changeId} was not found`, { changeId }, [`Verify openspec/changes/${changeId}/ exists`]), options.json);
|
|
122
|
+
process.exitCode = 1;
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (!result.valid) {
|
|
126
|
+
printResult(io, fail('openspec.validate', 'OPENSPEC_VALIDATE_INVALID', `OpenSpec change ${changeId} failed validation`, result, result.issues.map((issue) => `${issue.level}: ${issue.rule}: ${issue.message}`)), options.json);
|
|
127
|
+
process.exitCode = 1;
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
printResult(io, ok('openspec.validate', result, result.issues.filter((issue) => issue.level === 'warning').map((issue) => `${issue.rule}: ${issue.message}`)), options.json);
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
printResult(io, fail('openspec.validate', 'OPENSPEC_VALIDATE_FAILED', getErrorMessage(error), { changeId }, ['Check the project path and openspec/ layout before retrying']), options.json);
|
|
134
|
+
process.exitCode = 1;
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
addJsonOption(openspec
|
|
138
|
+
.command('archive')
|
|
139
|
+
.description('Move an OpenSpec change under openspec/changes/<archiveDir>/<id>/ (dry-run by default)')
|
|
140
|
+
.argument('<changeId>', 'OpenSpec change directory name under openspec/changes')
|
|
141
|
+
.option('--project <path>', 'project root containing an openspec/ directory')
|
|
142
|
+
.option('--apply', 'actually move the change directory')
|
|
143
|
+
.option('--archive-dir <name>', 'archive subdirectory name (default: archive)')).action(async (changeId, options) => {
|
|
144
|
+
try {
|
|
145
|
+
const scan = resolveScanOptions(options.project);
|
|
146
|
+
const archiveOptions = {};
|
|
147
|
+
if (scan.openspecRoot !== undefined) {
|
|
148
|
+
archiveOptions.openspecRoot = scan.openspecRoot;
|
|
149
|
+
}
|
|
150
|
+
if (options.apply === true) {
|
|
151
|
+
archiveOptions.apply = true;
|
|
152
|
+
}
|
|
153
|
+
if (options.archiveDir !== undefined) {
|
|
154
|
+
archiveOptions.archiveDirName = options.archiveDir;
|
|
155
|
+
}
|
|
156
|
+
const result = await archiveOpenSpecChange(changeId, archiveOptions);
|
|
157
|
+
if (result === null) {
|
|
158
|
+
printResult(io, fail('openspec.archive', 'OPENSPEC_CHANGE_NOT_FOUND', `OpenSpec change ${changeId} was not found`, { changeId }, [`Verify openspec/changes/${changeId}/ exists`]), options.json);
|
|
159
|
+
process.exitCode = 1;
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
printResult(io, ok('openspec.archive', result, [], result.applied ? [] : [`Re-run with --apply to move ${result.from} → ${result.to}`]), options.json);
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
printResult(io, fail('openspec.archive', 'OPENSPEC_ARCHIVE_FAILED', getErrorMessage(error), { changeId }, ['Check the project path and openspec/ layout before retrying']), options.json);
|
|
166
|
+
process.exitCode = 1;
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { loadProjectDashboard } from '../../services/dashboard/project-dashboard-service.js';
|
|
2
|
+
import { fail, ok } from '../../shared/result.js';
|
|
3
|
+
import { addJsonOption, getErrorMessage, printResult } from '../cli-helpers.js';
|
|
4
|
+
export function registerProjectCommands(program, io) {
|
|
5
|
+
const project = program.command('project').description('Aggregate Peaks state for a target project (read-only)');
|
|
6
|
+
addJsonOption(project
|
|
7
|
+
.command('dashboard')
|
|
8
|
+
.description('One-call snapshot of doctor / MCP / OpenSpec / requests / Understand Anything / capabilities for a project')
|
|
9
|
+
.requiredOption('--project <path>', 'target project root')).action(async (options) => {
|
|
10
|
+
try {
|
|
11
|
+
const dashboard = await loadProjectDashboard({ projectRoot: options.project });
|
|
12
|
+
if (!dashboard.runbookHealth.ok) {
|
|
13
|
+
const suggestions = [
|
|
14
|
+
dashboard.runbookHealth.missingRunbook.length > 0
|
|
15
|
+
? `Add a ## Default runbook section to: ${dashboard.runbookHealth.missingRunbook.join(', ')}`
|
|
16
|
+
: null,
|
|
17
|
+
dashboard.runbookHealth.applyNoteFailed.length > 0
|
|
18
|
+
? `Add authorization/--dry-run notes next to destructive --apply lines in: ${dashboard.runbookHealth.applyNoteFailed.join(', ')}`
|
|
19
|
+
: null
|
|
20
|
+
].filter((line) => line !== null);
|
|
21
|
+
printResult(io, fail('project.dashboard', 'PROJECT_DASHBOARD_RUNBOOK_UNHEALTHY', `Skill runbook health failing: ${dashboard.runbookHealth.healthy}/${dashboard.runbookHealth.required} healthy`, dashboard, suggestions), options.json);
|
|
22
|
+
process.exitCode = 1;
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (!dashboard.doctor.ok) {
|
|
26
|
+
printResult(io, fail('project.dashboard', 'PROJECT_DASHBOARD_DOCTOR_FAILED', `Doctor reports ${dashboard.doctor.failed} failed check(s) (${dashboard.doctor.passed} passed)`, dashboard, ['Run `peaks doctor --json` and resolve the failing checks before re-running the dashboard']), options.json);
|
|
27
|
+
process.exitCode = 1;
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
printResult(io, ok('project.dashboard', dashboard), options.json);
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
printResult(io, fail('project.dashboard', 'PROJECT_DASHBOARD_FAILED', getErrorMessage(error), { projectRoot: options.project }, ['Check the project path before retrying']), options.json);
|
|
34
|
+
process.exitCode = 1;
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { InvalidArgumentError } from 'commander';
|
|
2
|
+
import { allowedStatesForRole, createRequestArtifact, listRequestArtifacts, showRequestArtifact, transitionRequestArtifact } from '../../services/artifacts/request-artifact-service.js';
|
|
3
|
+
import { fail, ok } from '../../shared/result.js';
|
|
4
|
+
import { addJsonOption, getErrorMessage, printResult } from '../cli-helpers.js';
|
|
5
|
+
const VALID_ROLES = ['prd', 'ui', 'rd', 'qa'];
|
|
6
|
+
function parseRole(value) {
|
|
7
|
+
if (!VALID_ROLES.includes(value)) {
|
|
8
|
+
throw new InvalidArgumentError(`must be one of ${VALID_ROLES.join(', ')}`);
|
|
9
|
+
}
|
|
10
|
+
return value;
|
|
11
|
+
}
|
|
12
|
+
function parseStateForRole(role, value) {
|
|
13
|
+
const allowed = allowedStatesForRole(role);
|
|
14
|
+
if (!allowed.includes(value)) {
|
|
15
|
+
throw new InvalidArgumentError(`must be one of ${allowed.join(', ')} for role ${role}`);
|
|
16
|
+
}
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
export function registerRequestCommands(program, io) {
|
|
20
|
+
const request = program.command('request').description('Manage per-request Peaks role artifacts (PRD / UI / RD / QA)');
|
|
21
|
+
addJsonOption(request
|
|
22
|
+
.command('init')
|
|
23
|
+
.description('Create the per-request artifact template for a Peaks role (dry-run by default)')
|
|
24
|
+
.requiredOption('--role <role>', `target role (${VALID_ROLES.join(' | ')})`, parseRole)
|
|
25
|
+
.requiredOption('--id <request-id>', 'request id, e.g. 2026-05-23-add-foo')
|
|
26
|
+
.requiredOption('--project <path>', 'target project root')
|
|
27
|
+
.option('--session-id <session>', 'override the default date-stamped session id')
|
|
28
|
+
.option('--apply', 'write the artifact file (default: preview only)')).action(async (options) => {
|
|
29
|
+
try {
|
|
30
|
+
const serviceOptions = {
|
|
31
|
+
role: options.role,
|
|
32
|
+
requestId: options.id,
|
|
33
|
+
projectRoot: options.project
|
|
34
|
+
};
|
|
35
|
+
if (options.sessionId !== undefined) {
|
|
36
|
+
serviceOptions.sessionId = options.sessionId;
|
|
37
|
+
}
|
|
38
|
+
if (options.apply === true) {
|
|
39
|
+
serviceOptions.apply = true;
|
|
40
|
+
}
|
|
41
|
+
const result = await createRequestArtifact(serviceOptions);
|
|
42
|
+
printResult(io, ok('request.init', result, [], result.applied ? [] : [`Re-run with --apply to write ${result.path}`]), options.json);
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
printResult(io, fail('request.init', 'REQUEST_INIT_FAILED', getErrorMessage(error), { role: options.role, requestId: options.id }, ['Check role, request id, and project path before retrying']), options.json);
|
|
46
|
+
process.exitCode = 1;
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
addJsonOption(request
|
|
50
|
+
.command('list')
|
|
51
|
+
.description('List per-request artifacts under a project workspace')
|
|
52
|
+
.requiredOption('--project <path>', 'target project root')
|
|
53
|
+
.option('--session-id <session>', 'limit to a specific session id')
|
|
54
|
+
.option('--role <role>', `limit to a single role (${VALID_ROLES.join(' | ')})`, parseRole)).action(async (options) => {
|
|
55
|
+
try {
|
|
56
|
+
const listOptions = { projectRoot: options.project };
|
|
57
|
+
if (options.sessionId !== undefined) {
|
|
58
|
+
listOptions.sessionId = options.sessionId;
|
|
59
|
+
}
|
|
60
|
+
if (options.role !== undefined) {
|
|
61
|
+
listOptions.role = options.role;
|
|
62
|
+
}
|
|
63
|
+
const items = await listRequestArtifacts(listOptions);
|
|
64
|
+
printResult(io, ok('request.list', { count: items.length, items }), options.json);
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
printResult(io, fail('request.list', 'REQUEST_LIST_FAILED', getErrorMessage(error), { projectRoot: options.project }, ['Check project path before retrying']), options.json);
|
|
68
|
+
process.exitCode = 1;
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
addJsonOption(request
|
|
72
|
+
.command('show')
|
|
73
|
+
.description('Show a single per-request artifact, optionally scoped to a session')
|
|
74
|
+
.argument('<request-id>', 'request id, e.g. 2026-05-23-add-foo')
|
|
75
|
+
.requiredOption('--role <role>', `target role (${VALID_ROLES.join(' | ')})`, parseRole)
|
|
76
|
+
.requiredOption('--project <path>', 'target project root')
|
|
77
|
+
.option('--session-id <session>', 'restrict to a specific session id')).action(async (requestId, options) => {
|
|
78
|
+
try {
|
|
79
|
+
const showOptions = {
|
|
80
|
+
projectRoot: options.project,
|
|
81
|
+
role: options.role,
|
|
82
|
+
requestId
|
|
83
|
+
};
|
|
84
|
+
if (options.sessionId !== undefined) {
|
|
85
|
+
showOptions.sessionId = options.sessionId;
|
|
86
|
+
}
|
|
87
|
+
const result = await showRequestArtifact(showOptions);
|
|
88
|
+
if (result === null) {
|
|
89
|
+
printResult(io, fail('request.show', 'REQUEST_NOT_FOUND', `No artifact found for role=${options.role} requestId=${requestId}`, { role: options.role, requestId }, ['Verify the request id, role, and session id']), options.json);
|
|
90
|
+
process.exitCode = 1;
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
printResult(io, ok('request.show', result), options.json);
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
printResult(io, fail('request.show', 'REQUEST_SHOW_FAILED', getErrorMessage(error), { role: options.role, requestId }, ['Check role, request id, and project path before retrying']), options.json);
|
|
97
|
+
process.exitCode = 1;
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
addJsonOption(request
|
|
101
|
+
.command('transition')
|
|
102
|
+
.description('Move a per-request artifact to a new state defined by its role state machine')
|
|
103
|
+
.argument('<request-id>', 'request id, e.g. 2026-05-23-add-foo')
|
|
104
|
+
.requiredOption('--role <role>', `target role (${VALID_ROLES.join(' | ')})`, parseRole)
|
|
105
|
+
.requiredOption('--state <state>', 'new state name; allowed values depend on role')
|
|
106
|
+
.requiredOption('--project <path>', 'target project root')
|
|
107
|
+
.option('--session-id <session>', 'restrict to a specific session id')
|
|
108
|
+
.option('--reason <text>', 'optional reason appended as a transition note')).action(async (requestId, options) => {
|
|
109
|
+
try {
|
|
110
|
+
const role = options.role;
|
|
111
|
+
const newState = parseStateForRole(role, options.state);
|
|
112
|
+
const transitionOptions = {
|
|
113
|
+
role,
|
|
114
|
+
requestId,
|
|
115
|
+
projectRoot: options.project,
|
|
116
|
+
newState
|
|
117
|
+
};
|
|
118
|
+
if (options.sessionId !== undefined) {
|
|
119
|
+
transitionOptions.sessionId = options.sessionId;
|
|
120
|
+
}
|
|
121
|
+
if (options.reason !== undefined) {
|
|
122
|
+
transitionOptions.reason = options.reason;
|
|
123
|
+
}
|
|
124
|
+
const result = await transitionRequestArtifact(transitionOptions);
|
|
125
|
+
if (result === null) {
|
|
126
|
+
printResult(io, fail('request.transition', 'REQUEST_NOT_FOUND', `No artifact found for role=${role} requestId=${requestId}`, { role, requestId }, ['Verify the request id, role, and session id']), options.json);
|
|
127
|
+
process.exitCode = 1;
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
printResult(io, ok('request.transition', result), options.json);
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
if (error instanceof InvalidArgumentError) {
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
printResult(io, fail('request.transition', 'REQUEST_TRANSITION_FAILED', getErrorMessage(error), { role: options.role, requestId }, ['Check role, request id, state, and project path before retrying']), options.json);
|
|
137
|
+
process.exitCode = 1;
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { InvalidArgumentError } from 'commander';
|
|
2
|
+
import { scanUnderstandAnything, summarizeKnowledgeGraph } from '../../services/understand/understand-scan-service.js';
|
|
3
|
+
import { fail, ok } from '../../shared/result.js';
|
|
4
|
+
import { addJsonOption, getErrorMessage, printResult } from '../cli-helpers.js';
|
|
5
|
+
function parsePositiveInteger(value) {
|
|
6
|
+
if (!/^\d+$/.test(value)) {
|
|
7
|
+
throw new InvalidArgumentError('must be a positive integer');
|
|
8
|
+
}
|
|
9
|
+
const parsed = Number(value);
|
|
10
|
+
if (!Number.isSafeInteger(parsed) || parsed < 1) {
|
|
11
|
+
throw new InvalidArgumentError('must be a positive integer');
|
|
12
|
+
}
|
|
13
|
+
return parsed;
|
|
14
|
+
}
|
|
15
|
+
const INSTALL_HINT = 'Install Understand Anything in Claude Code: `/plugin marketplace add Lum1104/Understand-Anything` then `/plugin install understand-anything`, then run `/understand` in the target project to generate .understand-anything/knowledge-graph.json.';
|
|
16
|
+
export function registerUnderstandCommands(program, io) {
|
|
17
|
+
const understand = program.command('understand').description('Inspect Understand Anything artifacts inside a project (read-only)');
|
|
18
|
+
addJsonOption(understand
|
|
19
|
+
.command('status')
|
|
20
|
+
.description('Report whether Understand Anything has produced a knowledge graph in the target project')
|
|
21
|
+
.requiredOption('--project <path>', 'target project root')
|
|
22
|
+
.option('--artifact-dir <path>', 'override the default .understand-anything directory')).action(async (options) => {
|
|
23
|
+
try {
|
|
24
|
+
const scanOptions = { projectRoot: options.project };
|
|
25
|
+
if (options.artifactDir !== undefined) {
|
|
26
|
+
scanOptions.artifactDir = options.artifactDir;
|
|
27
|
+
}
|
|
28
|
+
const report = await scanUnderstandAnything(scanOptions);
|
|
29
|
+
const nextActions = [];
|
|
30
|
+
if (!report.exists) {
|
|
31
|
+
nextActions.push(INSTALL_HINT);
|
|
32
|
+
}
|
|
33
|
+
else if (!report.graph.exists) {
|
|
34
|
+
nextActions.push('Run `/understand` inside Claude Code on this project to generate .understand-anything/knowledge-graph.json.');
|
|
35
|
+
}
|
|
36
|
+
else if (report.graph.parseError !== undefined) {
|
|
37
|
+
nextActions.push('Re-run `/understand` to regenerate the knowledge graph; the current file is not valid JSON.');
|
|
38
|
+
}
|
|
39
|
+
printResult(io, ok('understand.status', report, [], nextActions), options.json);
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
printResult(io, fail('understand.status', 'UNDERSTAND_STATUS_FAILED', getErrorMessage(error), { projectRoot: options.project }, ['Check the project path and .understand-anything directory before retrying']), options.json);
|
|
43
|
+
process.exitCode = 1;
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
addJsonOption(understand
|
|
47
|
+
.command('show')
|
|
48
|
+
.description('Summarize the Understand Anything knowledge graph (counts, layers, tours, sample nodes) for RD/TXT consumption')
|
|
49
|
+
.requiredOption('--project <path>', 'target project root')
|
|
50
|
+
.option('--artifact-dir <path>', 'override the default .understand-anything directory')
|
|
51
|
+
.option('--sample <n>', 'maximum number of sample node ids to return (default 5)', parsePositiveInteger)).action(async (options) => {
|
|
52
|
+
try {
|
|
53
|
+
const summaryOptions = { projectRoot: options.project };
|
|
54
|
+
if (options.artifactDir !== undefined) {
|
|
55
|
+
summaryOptions.artifactDir = options.artifactDir;
|
|
56
|
+
}
|
|
57
|
+
if (options.sample !== undefined) {
|
|
58
|
+
summaryOptions.sampleSize = options.sample;
|
|
59
|
+
}
|
|
60
|
+
const summary = await summarizeKnowledgeGraph(summaryOptions);
|
|
61
|
+
if (!summary.exists) {
|
|
62
|
+
printResult(io, fail('understand.show', 'UNDERSTAND_GRAPH_MISSING', `No knowledge graph found at ${summary.path}`, summary, [INSTALL_HINT]), options.json);
|
|
63
|
+
process.exitCode = 1;
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (summary.parseError !== undefined) {
|
|
67
|
+
printResult(io, fail('understand.show', 'UNDERSTAND_GRAPH_PARSE_ERROR', `Knowledge graph at ${summary.path} is not valid JSON: ${summary.parseError}`, summary, ['Re-run `/understand` to regenerate the knowledge graph']), options.json);
|
|
68
|
+
process.exitCode = 1;
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
printResult(io, ok('understand.show', summary), options.json);
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
printResult(io, fail('understand.show', 'UNDERSTAND_SHOW_FAILED', getErrorMessage(error), { projectRoot: options.project }, ['Check the project path and .understand-anything directory before retrying']), options.json);
|
|
75
|
+
process.exitCode = 1;
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
}
|