peaks-cli 1.0.12 → 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/config-commands.js +1 -17
- 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/commands/workflow-commands.js +56 -94
- 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/codegraph/codegraph-service.js +26 -45
- package/dist/src/services/config/config-service.js +2 -22
- 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 +2 -1
- package/dist/src/services/recommendations/capability-seed-mappings.js +1 -1
- package/dist/src/services/recommendations/capability-seed-sources.js +1 -1
- package/dist/src/services/shadcn/shadcn-service.d.ts +4 -0
- package/dist/src/services/shadcn/shadcn-service.js +15 -30
- 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 +2 -8
- 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 +74 -8
- package/skills/peaks-qa/references/artifact-contracts.md +2 -2
- 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 +2 -2
- package/skills/peaks-rd/SKILL.md +96 -9
- package/skills/peaks-rd/references/artifact-contracts.md +2 -2
- 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-rd/references/refactor-workflow.md +2 -2
- 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 +90 -9
- package/skills/peaks-solo/references/artifact-contracts.md +2 -2
- 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/refactor-mode.md +2 -2
- 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/scripts/strip-internal-exports.mjs +0 -33
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
import { mkdir, readFile, readdir, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { isDirectory, listDirectories, pathExists } from '../../shared/fs.js';
|
|
4
|
+
const REQUEST_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
5
|
+
const VALID_ROLES = new Set(['prd', 'ui', 'rd', 'qa']);
|
|
6
|
+
function defaultClock() {
|
|
7
|
+
return new Date().toISOString();
|
|
8
|
+
}
|
|
9
|
+
function dateSlugFromIso(iso) {
|
|
10
|
+
return iso.slice(0, 10);
|
|
11
|
+
}
|
|
12
|
+
function defaultSessionId(iso) {
|
|
13
|
+
return `${dateSlugFromIso(iso)}-session`;
|
|
14
|
+
}
|
|
15
|
+
function renderPrdTemplate(requestId, sessionId, timestamp) {
|
|
16
|
+
return `# PRD Request ${requestId}
|
|
17
|
+
|
|
18
|
+
- session: ${sessionId}
|
|
19
|
+
- type: feature | bug | refactor | clarification
|
|
20
|
+
- source: <ticket, message URL, or "verbal" with a short sanitized quote>
|
|
21
|
+
- raw input (sanitized): <one-paragraph restatement of what the user actually asked for>
|
|
22
|
+
|
|
23
|
+
## Goals
|
|
24
|
+
|
|
25
|
+
- ...
|
|
26
|
+
|
|
27
|
+
## Non-goals
|
|
28
|
+
|
|
29
|
+
- ...
|
|
30
|
+
|
|
31
|
+
## Preserved behavior
|
|
32
|
+
|
|
33
|
+
- ...
|
|
34
|
+
|
|
35
|
+
## Acceptance criteria
|
|
36
|
+
|
|
37
|
+
- ... (browser-verifiable when frontend is in scope)
|
|
38
|
+
|
|
39
|
+
## Frontend delta (only when target is frontend)
|
|
40
|
+
|
|
41
|
+
- pages / routes / components / states / permissions / data deps / edge cases
|
|
42
|
+
- 待联调态: ...
|
|
43
|
+
- API contracts pending: ...
|
|
44
|
+
|
|
45
|
+
## Risks and open questions
|
|
46
|
+
|
|
47
|
+
- ...
|
|
48
|
+
|
|
49
|
+
## Handoff
|
|
50
|
+
|
|
51
|
+
- to peaks-rd: .peaks/${sessionId}/rd/requests/${requestId}.md
|
|
52
|
+
- to peaks-qa: .peaks/${sessionId}/qa/requests/${requestId}.md
|
|
53
|
+
- to peaks-ui: .peaks/${sessionId}/ui/requests/${requestId}.md (when UI involved)
|
|
54
|
+
|
|
55
|
+
## Status
|
|
56
|
+
|
|
57
|
+
- created: ${timestamp}
|
|
58
|
+
- last update: ${timestamp}
|
|
59
|
+
- state: draft
|
|
60
|
+
`;
|
|
61
|
+
}
|
|
62
|
+
function renderUiTemplate(requestId, sessionId, timestamp) {
|
|
63
|
+
return `# UI Request ${requestId}
|
|
64
|
+
|
|
65
|
+
- session: ${sessionId}
|
|
66
|
+
- linked-prd: .peaks/${sessionId}/prd/requests/${requestId}.md
|
|
67
|
+
- scope: full new surface | iteration on existing surface | regression fix | visual refresh
|
|
68
|
+
- design direction: editorial | bento | Swiss | luxury | retro-futurist | glass | product-system | other-explicit-name
|
|
69
|
+
|
|
70
|
+
## Affected surfaces
|
|
71
|
+
|
|
72
|
+
- pages / routes / components / modals / states (loading, empty, error, hover, focus, active, responsive)
|
|
73
|
+
- explicit out-of-scope surfaces (do not modify)
|
|
74
|
+
|
|
75
|
+
## UX flow and page states
|
|
76
|
+
|
|
77
|
+
- entry points, primary flow, secondary flows, exit points
|
|
78
|
+
- state machine per surface when state transitions matter
|
|
79
|
+
|
|
80
|
+
## Visual constraints
|
|
81
|
+
|
|
82
|
+
- typography pair: ...
|
|
83
|
+
- palette: ...
|
|
84
|
+
- density and motion intensity dials: ...
|
|
85
|
+
- rejected generic patterns
|
|
86
|
+
|
|
87
|
+
## Interaction constraints
|
|
88
|
+
|
|
89
|
+
- keyboard, focus order, ARIA roles, gesture support, accessibility minima
|
|
90
|
+
|
|
91
|
+
## UI regression seeds
|
|
92
|
+
|
|
93
|
+
- list of visible regressions QA must check against the prior state
|
|
94
|
+
|
|
95
|
+
## Browser evidence
|
|
96
|
+
|
|
97
|
+
- sanitized observations only — no login URLs, cookies, headers, tokens, storage state, browser traces, or screenshots/logs with PII / SSO / MFA material
|
|
98
|
+
|
|
99
|
+
## Handoff
|
|
100
|
+
|
|
101
|
+
- to peaks-rd: .peaks/${sessionId}/rd/requests/${requestId}.md
|
|
102
|
+
- to peaks-qa: .peaks/${sessionId}/qa/requests/${requestId}.md
|
|
103
|
+
|
|
104
|
+
## Status
|
|
105
|
+
|
|
106
|
+
- created: ${timestamp}
|
|
107
|
+
- last update: ${timestamp}
|
|
108
|
+
- state: draft
|
|
109
|
+
`;
|
|
110
|
+
}
|
|
111
|
+
function renderRdTemplate(requestId, sessionId, timestamp) {
|
|
112
|
+
return `# RD Request ${requestId}
|
|
113
|
+
|
|
114
|
+
- session: ${sessionId}
|
|
115
|
+
- linked-prd: .peaks/${sessionId}/prd/requests/${requestId}.md
|
|
116
|
+
- linked-ui: .peaks/${sessionId}/ui/requests/${requestId}.md (when UI involved)
|
|
117
|
+
- type: feature | bug | refactor | clarification
|
|
118
|
+
|
|
119
|
+
## Red-line scope
|
|
120
|
+
|
|
121
|
+
- in-scope files / routes / API paths / data models
|
|
122
|
+
- explicit out-of-scope surfaces (do not modify, mock, delete, or replace)
|
|
123
|
+
|
|
124
|
+
## Standards preflight
|
|
125
|
+
|
|
126
|
+
- peaks standards init/update --project <path> --dry-run output paths and status
|
|
127
|
+
- planned application: apply | review-only | blocked
|
|
128
|
+
|
|
129
|
+
## OpenSpec linkage (when openspec/ exists)
|
|
130
|
+
|
|
131
|
+
- change-id: <openspec change id>
|
|
132
|
+
- entry validate: peaks openspec validate <change-id> data.valid status
|
|
133
|
+
- to-rd projection: peaks openspec to-rd <change-id> artifact path
|
|
134
|
+
- exit validate (after implementation): status
|
|
135
|
+
|
|
136
|
+
## Coverage status
|
|
137
|
+
|
|
138
|
+
- current total UT coverage: <percent>
|
|
139
|
+
- new/changed code coverage: <percent>
|
|
140
|
+
- gate verdict: pass | legacy-accepted | blocked
|
|
141
|
+
|
|
142
|
+
## Slice contract
|
|
143
|
+
|
|
144
|
+
- slice id, functional boundary, pre-refactor behavior, target structure, unit-test requirements, acceptance checks, rollback plan, commit boundary
|
|
145
|
+
|
|
146
|
+
## Implementation evidence
|
|
147
|
+
|
|
148
|
+
- diff paths, test commands + outputs, code review findings + fixes, security review findings + fixes, dry-run output
|
|
149
|
+
|
|
150
|
+
## MCP usage (when external docs lookup was used)
|
|
151
|
+
|
|
152
|
+
- capabilityId / tool / sanitized args
|
|
153
|
+
- artifact path of stored result
|
|
154
|
+
- no secrets, no full network bodies
|
|
155
|
+
|
|
156
|
+
## Handoff
|
|
157
|
+
|
|
158
|
+
- to peaks-qa: .peaks/${sessionId}/qa/requests/${requestId}.md
|
|
159
|
+
- to peaks-sc: .peaks/${sessionId}/sc/commit-boundaries/${requestId}.md
|
|
160
|
+
|
|
161
|
+
## Status
|
|
162
|
+
|
|
163
|
+
- created: ${timestamp}
|
|
164
|
+
- last update: ${timestamp}
|
|
165
|
+
- state: draft
|
|
166
|
+
`;
|
|
167
|
+
}
|
|
168
|
+
function renderQaTemplate(requestId, sessionId, timestamp) {
|
|
169
|
+
return `# QA Request ${requestId}
|
|
170
|
+
|
|
171
|
+
- session: ${sessionId}
|
|
172
|
+
- linked-prd: .peaks/${sessionId}/prd/requests/${requestId}.md
|
|
173
|
+
- linked-rd: .peaks/${sessionId}/rd/requests/${requestId}.md
|
|
174
|
+
- linked-ui: .peaks/${sessionId}/ui/requests/${requestId}.md (when UI involved)
|
|
175
|
+
- type: feature | bug | refactor | clarification
|
|
176
|
+
|
|
177
|
+
## Red-line boundary check
|
|
178
|
+
|
|
179
|
+
- in-scope changes seen in the diff (match PRD + RD scope)
|
|
180
|
+
- out-of-scope changes flagged (any extra file, route, mock, fixture, behavior)
|
|
181
|
+
- verdict: clean | boundary-violation
|
|
182
|
+
|
|
183
|
+
## OpenSpec exit gate (when openspec/ exists)
|
|
184
|
+
|
|
185
|
+
- change-id: <id>
|
|
186
|
+
- peaks openspec validate <id> data.valid: true | false
|
|
187
|
+
- issues: ...
|
|
188
|
+
|
|
189
|
+
## Acceptance checks
|
|
190
|
+
|
|
191
|
+
- per-criterion: check method, result (pass | fail | blocked), evidence path
|
|
192
|
+
|
|
193
|
+
## Mandatory validation gates
|
|
194
|
+
|
|
195
|
+
- unit tests: command + pass/fail + coverage delta
|
|
196
|
+
- API validation (when applicable): request paths exercised, evidence
|
|
197
|
+
- browser E2E (when frontend): headed gstack/browse/dist/browse visible-browser confirmation, sanitized route/actions, console/network observations
|
|
198
|
+
- browser-error feedback loop: page errors, console exceptions, broken network, hydration failures → return-to-RD evidence
|
|
199
|
+
- security check: tool used, findings, fixes, unresolved risks
|
|
200
|
+
- performance check: tool used, baseline vs after numbers when available
|
|
201
|
+
- validation report path
|
|
202
|
+
|
|
203
|
+
## Regression matrix
|
|
204
|
+
|
|
205
|
+
- list of surfaces / API paths / browser flows checked
|
|
206
|
+
- pass/fail per row
|
|
207
|
+
|
|
208
|
+
## Browser evidence
|
|
209
|
+
|
|
210
|
+
- sanitized observations only — no login URLs, cookies, headers, tokens, storage state, browser traces, or screenshots/logs with PII / SSO / MFA material
|
|
211
|
+
|
|
212
|
+
## Verdict
|
|
213
|
+
|
|
214
|
+
- overall: pass | return-to-rd | blocked
|
|
215
|
+
|
|
216
|
+
## Status
|
|
217
|
+
|
|
218
|
+
- created: ${timestamp}
|
|
219
|
+
- last update: ${timestamp}
|
|
220
|
+
- state: draft
|
|
221
|
+
`;
|
|
222
|
+
}
|
|
223
|
+
function renderTemplate(role, requestId, sessionId, timestamp) {
|
|
224
|
+
switch (role) {
|
|
225
|
+
case 'prd':
|
|
226
|
+
return renderPrdTemplate(requestId, sessionId, timestamp);
|
|
227
|
+
case 'ui':
|
|
228
|
+
return renderUiTemplate(requestId, sessionId, timestamp);
|
|
229
|
+
case 'rd':
|
|
230
|
+
return renderRdTemplate(requestId, sessionId, timestamp);
|
|
231
|
+
case 'qa':
|
|
232
|
+
return renderQaTemplate(requestId, sessionId, timestamp);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
export async function createRequestArtifact(options) {
|
|
236
|
+
if (!VALID_ROLES.has(options.role)) {
|
|
237
|
+
throw new Error(`Invalid role: ${String(options.role)} (expected prd, ui, rd, or qa)`);
|
|
238
|
+
}
|
|
239
|
+
if (!REQUEST_ID_PATTERN.test(options.requestId)) {
|
|
240
|
+
throw new Error(`Invalid request id: ${options.requestId} (expected letters, digits, dots, underscores, or dashes)`);
|
|
241
|
+
}
|
|
242
|
+
const clock = options.clock ?? defaultClock;
|
|
243
|
+
const timestamp = clock();
|
|
244
|
+
const sessionId = options.sessionId ?? defaultSessionId(timestamp);
|
|
245
|
+
const path = join(options.projectRoot, '.peaks', sessionId, options.role, 'requests', `${options.requestId}.md`);
|
|
246
|
+
const content = renderTemplate(options.role, options.requestId, sessionId, timestamp);
|
|
247
|
+
if (options.apply !== true) {
|
|
248
|
+
return { role: options.role, requestId: options.requestId, sessionId, path, content, applied: false };
|
|
249
|
+
}
|
|
250
|
+
if (await pathExists(path)) {
|
|
251
|
+
throw new Error(`Refusing to write: ${path} already exists. Update it in place or remove it before re-running peaks request init.`);
|
|
252
|
+
}
|
|
253
|
+
await mkdir(dirname(path), { recursive: true });
|
|
254
|
+
await writeFile(path, content, 'utf8');
|
|
255
|
+
return { role: options.role, requestId: options.requestId, sessionId, path, content, applied: true };
|
|
256
|
+
}
|
|
257
|
+
function extractStateAndCreated(markdown) {
|
|
258
|
+
let state = 'unknown';
|
|
259
|
+
let createdAt;
|
|
260
|
+
for (const rawLine of markdown.split(/\r?\n/)) {
|
|
261
|
+
const line = rawLine.trim();
|
|
262
|
+
const stateMatch = /^-\s*state:\s*(.+?)\s*$/.exec(line);
|
|
263
|
+
if (stateMatch !== null && stateMatch[1] !== undefined) {
|
|
264
|
+
state = stateMatch[1];
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
const createdMatch = /^-\s*created:\s*(.+?)\s*$/.exec(line);
|
|
268
|
+
if (createdMatch !== null && createdMatch[1] !== undefined) {
|
|
269
|
+
createdAt = createdMatch[1];
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return createdAt === undefined ? { state } : { state, createdAt };
|
|
273
|
+
}
|
|
274
|
+
async function readSummary(projectRoot, sessionId, role, fileName) {
|
|
275
|
+
const path = join(projectRoot, '.peaks', sessionId, role, 'requests', fileName);
|
|
276
|
+
const body = await readFile(path, 'utf8');
|
|
277
|
+
const { state, createdAt } = extractStateAndCreated(body);
|
|
278
|
+
const requestId = fileName.replace(/\.md$/, '');
|
|
279
|
+
const summary = { role, sessionId, requestId, path, state };
|
|
280
|
+
if (createdAt !== undefined) {
|
|
281
|
+
summary.createdAt = createdAt;
|
|
282
|
+
}
|
|
283
|
+
return summary;
|
|
284
|
+
}
|
|
285
|
+
async function listMarkdownFiles(dir) {
|
|
286
|
+
if (!(await isDirectory(dir))) {
|
|
287
|
+
return [];
|
|
288
|
+
}
|
|
289
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
290
|
+
return entries
|
|
291
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith('.md'))
|
|
292
|
+
.map((entry) => entry.name)
|
|
293
|
+
.sort();
|
|
294
|
+
}
|
|
295
|
+
export async function listRequestArtifacts(options) {
|
|
296
|
+
const peaksRoot = join(options.projectRoot, '.peaks');
|
|
297
|
+
if (!(await isDirectory(peaksRoot))) {
|
|
298
|
+
return [];
|
|
299
|
+
}
|
|
300
|
+
const sessions = options.sessionId !== undefined ? [options.sessionId] : await listDirectories(peaksRoot);
|
|
301
|
+
const roles = options.role !== undefined ? [options.role] : Array.from(VALID_ROLES);
|
|
302
|
+
const summaries = [];
|
|
303
|
+
for (const sessionId of sessions) {
|
|
304
|
+
for (const role of roles) {
|
|
305
|
+
const dir = join(peaksRoot, sessionId, role, 'requests');
|
|
306
|
+
const fileNames = await listMarkdownFiles(dir);
|
|
307
|
+
for (const fileName of fileNames) {
|
|
308
|
+
summaries.push(await readSummary(options.projectRoot, sessionId, role, fileName));
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return summaries;
|
|
313
|
+
}
|
|
314
|
+
export async function showRequestArtifact(options) {
|
|
315
|
+
if (!VALID_ROLES.has(options.role)) {
|
|
316
|
+
throw new Error(`Invalid role: ${String(options.role)} (expected prd, ui, rd, or qa)`);
|
|
317
|
+
}
|
|
318
|
+
if (!REQUEST_ID_PATTERN.test(options.requestId)) {
|
|
319
|
+
throw new Error(`Invalid request id: ${options.requestId} (expected letters, digits, dots, underscores, or dashes)`);
|
|
320
|
+
}
|
|
321
|
+
const fileName = `${options.requestId}.md`;
|
|
322
|
+
if (options.sessionId !== undefined) {
|
|
323
|
+
const path = join(options.projectRoot, '.peaks', options.sessionId, options.role, 'requests', fileName);
|
|
324
|
+
if (!(await pathExists(path))) {
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
const summary = await readSummary(options.projectRoot, options.sessionId, options.role, fileName);
|
|
328
|
+
const content = await readFile(path, 'utf8');
|
|
329
|
+
return { ...summary, content };
|
|
330
|
+
}
|
|
331
|
+
const peaksRoot = join(options.projectRoot, '.peaks');
|
|
332
|
+
if (!(await isDirectory(peaksRoot))) {
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
const sessions = await listDirectories(peaksRoot);
|
|
336
|
+
for (const sessionId of sessions) {
|
|
337
|
+
const path = join(peaksRoot, sessionId, options.role, 'requests', fileName);
|
|
338
|
+
if (await pathExists(path)) {
|
|
339
|
+
const summary = await readSummary(options.projectRoot, sessionId, options.role, fileName);
|
|
340
|
+
const content = await readFile(path, 'utf8');
|
|
341
|
+
return { ...summary, content };
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return null;
|
|
345
|
+
}
|
|
346
|
+
const ALLOWED_STATES_PER_ROLE = {
|
|
347
|
+
prd: ['draft', 'confirmed-by-user', 'handed-off', 'blocked'],
|
|
348
|
+
ui: ['draft', 'direction-locked', 'handed-off', 'blocked'],
|
|
349
|
+
rd: ['draft', 'spec-locked', 'implemented', 'qa-handoff', 'handed-off', 'blocked'],
|
|
350
|
+
qa: ['draft', 'running', 'verdict-issued', 'blocked']
|
|
351
|
+
};
|
|
352
|
+
export function allowedStatesForRole(role) {
|
|
353
|
+
return ALLOWED_STATES_PER_ROLE[role];
|
|
354
|
+
}
|
|
355
|
+
function updateStatusBlock(markdown, newState, timestamp, reason) {
|
|
356
|
+
const lines = markdown.split(/\r?\n/);
|
|
357
|
+
let previousState = 'unknown';
|
|
358
|
+
let stateLineIndex = -1;
|
|
359
|
+
let lastUpdateLineIndex = -1;
|
|
360
|
+
for (const [index, raw] of lines.entries()) {
|
|
361
|
+
const trimmed = raw.trim();
|
|
362
|
+
const stateMatch = /^-\s*state:\s*(.+?)\s*$/.exec(trimmed);
|
|
363
|
+
if (stateMatch !== null && stateMatch[1] !== undefined) {
|
|
364
|
+
previousState = stateMatch[1];
|
|
365
|
+
stateLineIndex = index;
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
if (/^-\s*last update:\s*/.test(trimmed)) {
|
|
369
|
+
lastUpdateLineIndex = index;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
if (stateLineIndex >= 0) {
|
|
373
|
+
lines[stateLineIndex] = `- state: ${newState}`;
|
|
374
|
+
}
|
|
375
|
+
else {
|
|
376
|
+
lines.push('', '## Status', '', `- state: ${newState}`);
|
|
377
|
+
}
|
|
378
|
+
if (lastUpdateLineIndex >= 0) {
|
|
379
|
+
lines[lastUpdateLineIndex] = `- last update: ${timestamp}`;
|
|
380
|
+
}
|
|
381
|
+
else if (stateLineIndex >= 0) {
|
|
382
|
+
lines.splice(stateLineIndex, 0, `- last update: ${timestamp}`);
|
|
383
|
+
}
|
|
384
|
+
else {
|
|
385
|
+
lines.push(`- last update: ${timestamp}`);
|
|
386
|
+
}
|
|
387
|
+
if (reason !== undefined && reason.length > 0) {
|
|
388
|
+
lines.push(`- transition note (${timestamp}): ${reason}`);
|
|
389
|
+
}
|
|
390
|
+
return { updated: lines.join('\n'), previousState };
|
|
391
|
+
}
|
|
392
|
+
export async function transitionRequestArtifact(options) {
|
|
393
|
+
if (!VALID_ROLES.has(options.role)) {
|
|
394
|
+
throw new Error(`Invalid role: ${String(options.role)} (expected prd, ui, rd, or qa)`);
|
|
395
|
+
}
|
|
396
|
+
if (!REQUEST_ID_PATTERN.test(options.requestId)) {
|
|
397
|
+
throw new Error(`Invalid request id: ${options.requestId} (expected letters, digits, dots, underscores, or dashes)`);
|
|
398
|
+
}
|
|
399
|
+
const allowed = ALLOWED_STATES_PER_ROLE[options.role];
|
|
400
|
+
if (!allowed.includes(options.newState)) {
|
|
401
|
+
throw new Error(`Invalid state for role ${options.role}: ${options.newState} (expected one of ${allowed.join(', ')})`);
|
|
402
|
+
}
|
|
403
|
+
const showOptions = {
|
|
404
|
+
projectRoot: options.projectRoot,
|
|
405
|
+
role: options.role,
|
|
406
|
+
requestId: options.requestId
|
|
407
|
+
};
|
|
408
|
+
if (options.sessionId !== undefined) {
|
|
409
|
+
showOptions.sessionId = options.sessionId;
|
|
410
|
+
}
|
|
411
|
+
const existing = await showRequestArtifact(showOptions);
|
|
412
|
+
if (existing === null) {
|
|
413
|
+
return null;
|
|
414
|
+
}
|
|
415
|
+
const clock = options.clock ?? defaultClock;
|
|
416
|
+
const timestamp = clock();
|
|
417
|
+
const { updated, previousState } = updateStatusBlock(existing.content, options.newState, timestamp, options.reason);
|
|
418
|
+
await writeFile(existing.path, updated, 'utf8');
|
|
419
|
+
const result = {
|
|
420
|
+
role: options.role,
|
|
421
|
+
sessionId: existing.sessionId,
|
|
422
|
+
requestId: options.requestId,
|
|
423
|
+
path: existing.path,
|
|
424
|
+
state: options.newState,
|
|
425
|
+
previousState,
|
|
426
|
+
content: updated
|
|
427
|
+
};
|
|
428
|
+
if (existing.createdAt !== undefined) {
|
|
429
|
+
result.createdAt = existing.createdAt;
|
|
430
|
+
}
|
|
431
|
+
return result;
|
|
432
|
+
}
|
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import { existsSync, realpathSync, statSync } from 'node:fs';
|
|
2
2
|
import { spawn } from 'node:child_process';
|
|
3
3
|
import { createRequire } from 'node:module';
|
|
4
|
-
import { dirname, isAbsolute, relative, resolve, sep
|
|
4
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
5
5
|
const CODEGRAPH_PACKAGE_NAME = '@colbymchenry/codegraph';
|
|
6
6
|
const CODEGRAPH_PACKAGE_VERSION = '0.7.10';
|
|
7
7
|
const CODEGRAPH_EXECUTABLE = process.execPath;
|
|
8
8
|
const CODEGRAPH_BINARY_PATH = resolveCodegraphBinaryPath();
|
|
9
9
|
const CODEGRAPH_PROCESS_TIMEOUT_MS = 600_000;
|
|
10
10
|
const CODEGRAPH_OUTPUT_LIMIT_BYTES = 10 * 1024 * 1024;
|
|
11
|
+
const NODE_OPTIONS_ENV_KEY = 'NODE_OPTIONS';
|
|
12
|
+
const NPM_CONFIG_PREFIX = 'npm_config_';
|
|
13
|
+
const NPM_CONFIG_UPPER_PREFIX = 'NPM_CONFIG_';
|
|
11
14
|
const POSITIONAL_ARGUMENT_PREFIX = '-';
|
|
12
15
|
const ALLOWED_SUBCOMMANDS = ['status', 'init', 'index', 'query', 'files', 'context', 'affected'];
|
|
16
|
+
const NUMERIC_FLAG_NAMES = ['limit', 'maxDepth'];
|
|
13
17
|
const COMMON_OPTION_KEYS = ['subcommand', 'project'];
|
|
14
18
|
const ALLOWED_OPTIONS_BY_SUBCOMMAND = {
|
|
15
19
|
status: [],
|
|
@@ -20,16 +24,13 @@ const ALLOWED_OPTIONS_BY_SUBCOMMAND = {
|
|
|
20
24
|
context: ['task'],
|
|
21
25
|
affected: ['files', 'json']
|
|
22
26
|
};
|
|
23
|
-
function assertCodegraphBinaryExists(binaryPath) {
|
|
24
|
-
if (!existsSync(binaryPath)) {
|
|
25
|
-
throw new Error('Unable to resolve local codegraph binary from @colbymchenry/codegraph');
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
27
|
function resolveCodegraphBinaryPath() {
|
|
29
28
|
const require = createRequire(import.meta.url);
|
|
30
29
|
const packageJsonPath = require.resolve('@colbymchenry/codegraph/package.json');
|
|
31
30
|
const binaryPath = resolve(dirname(packageJsonPath), 'dist', 'bin', 'codegraph.js');
|
|
32
|
-
|
|
31
|
+
if (!existsSync(binaryPath)) {
|
|
32
|
+
throw new Error('Unable to resolve local codegraph binary from @colbymchenry/codegraph');
|
|
33
|
+
}
|
|
33
34
|
return binaryPath;
|
|
34
35
|
}
|
|
35
36
|
function assertSupportedSubcommand(subcommand) {
|
|
@@ -37,15 +38,12 @@ function assertSupportedSubcommand(subcommand) {
|
|
|
37
38
|
throw new Error(`Unsupported codegraph subcommand: ${subcommand}`);
|
|
38
39
|
}
|
|
39
40
|
}
|
|
40
|
-
function assertProjectRootDirectory(projectRoot) {
|
|
41
|
-
if (!statSync(projectRoot).isDirectory()) {
|
|
42
|
-
throw new Error('Project path must exist and be a directory');
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
41
|
function resolveProjectRoot(project) {
|
|
46
42
|
const projectRoot = resolve(project);
|
|
47
43
|
try {
|
|
48
|
-
|
|
44
|
+
if (!statSync(projectRoot).isDirectory()) {
|
|
45
|
+
throw new Error('Project path must exist and be a directory');
|
|
46
|
+
}
|
|
49
47
|
return realpathSync.native(projectRoot);
|
|
50
48
|
}
|
|
51
49
|
catch {
|
|
@@ -90,8 +88,12 @@ function assertInsideProject(projectRoot, absolutePath) {
|
|
|
90
88
|
throw new Error('Affected files must stay inside the project');
|
|
91
89
|
}
|
|
92
90
|
}
|
|
93
|
-
function
|
|
94
|
-
|
|
91
|
+
function resolveExistingBoundary(absoluteFilePath) {
|
|
92
|
+
if (existsSync(absoluteFilePath)) {
|
|
93
|
+
return absoluteFilePath;
|
|
94
|
+
}
|
|
95
|
+
let currentPath = dirname(absoluteFilePath);
|
|
96
|
+
while (!existsSync(currentPath)) {
|
|
95
97
|
const parentPath = dirname(currentPath);
|
|
96
98
|
if (parentPath === currentPath) {
|
|
97
99
|
return currentPath;
|
|
@@ -100,9 +102,6 @@ function climbToExistingBoundary(currentPath, pathExists = existsSync) {
|
|
|
100
102
|
}
|
|
101
103
|
return currentPath;
|
|
102
104
|
}
|
|
103
|
-
function resolveExistingBoundary(absoluteFilePath) {
|
|
104
|
-
return existsSync(absoluteFilePath) ? absoluteFilePath : climbToExistingBoundary(dirname(absoluteFilePath));
|
|
105
|
-
}
|
|
106
105
|
function normalizeProjectRelativeFile(projectRoot, file) {
|
|
107
106
|
assertPositionalArgument(file, 'Affected files');
|
|
108
107
|
const absoluteFilePath = resolve(projectRoot, file);
|
|
@@ -111,13 +110,10 @@ function normalizeProjectRelativeFile(projectRoot, file) {
|
|
|
111
110
|
assertInsideProject(projectRoot, realBoundary);
|
|
112
111
|
return relative(projectRoot, absoluteFilePath).split(sep).join('/');
|
|
113
112
|
}
|
|
114
|
-
function
|
|
113
|
+
function buildAffectedFileArgs(projectRoot, files) {
|
|
115
114
|
if (!files || files.length < 1) {
|
|
116
115
|
throw new Error('affected requires at least one file');
|
|
117
116
|
}
|
|
118
|
-
}
|
|
119
|
-
function buildAffectedFileArgs(projectRoot, files) {
|
|
120
|
-
assertAffectedFiles(files);
|
|
121
117
|
return files.map((file) => normalizeProjectRelativeFile(projectRoot, file));
|
|
122
118
|
}
|
|
123
119
|
function buildCommandArgs(options, projectRoot) {
|
|
@@ -172,36 +168,24 @@ function assertOutputLimit(currentSize, chunkSize) {
|
|
|
172
168
|
}
|
|
173
169
|
return nextSize;
|
|
174
170
|
}
|
|
175
|
-
function
|
|
176
|
-
const candidates = [
|
|
177
|
-
win32.join('C:\\Windows', 'System32', 'taskkill.exe'),
|
|
178
|
-
win32.join('C:\\WINNT', 'System32', 'taskkill.exe')
|
|
179
|
-
];
|
|
180
|
-
return candidates.find((candidate) => fileExists(candidate)) ?? null;
|
|
181
|
-
}
|
|
182
|
-
function terminateCodegraphProcess(childProcess, platform = process.platform, killProcess = process.kill, spawnProcess = spawn, taskkillPath = getWindowsTaskkillPath()) {
|
|
171
|
+
function terminateCodegraphProcess(childProcess) {
|
|
183
172
|
if (childProcess.pid === undefined) {
|
|
184
173
|
childProcess.kill();
|
|
185
174
|
return;
|
|
186
175
|
}
|
|
187
|
-
if (platform === 'win32') {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
return;
|
|
191
|
-
}
|
|
192
|
-
const killerProcess = spawnProcess(taskkillPath, ['/pid', String(childProcess.pid), '/T', '/F'], { shell: false, stdio: 'ignore' });
|
|
193
|
-
killerProcess.on('error', () => childProcess.kill());
|
|
194
|
-
killerProcess.unref();
|
|
176
|
+
if (process.platform === 'win32') {
|
|
177
|
+
const taskkillPath = process.env.SystemRoot ? join(process.env.SystemRoot, 'System32', 'taskkill.exe') : 'taskkill.exe';
|
|
178
|
+
spawn(taskkillPath, ['/pid', String(childProcess.pid), '/T', '/F'], { shell: false, stdio: 'ignore' });
|
|
195
179
|
return;
|
|
196
180
|
}
|
|
197
181
|
try {
|
|
198
|
-
|
|
182
|
+
process.kill(-childProcess.pid, 'SIGTERM');
|
|
199
183
|
}
|
|
200
184
|
catch {
|
|
201
185
|
childProcess.kill('SIGTERM');
|
|
202
186
|
}
|
|
203
187
|
}
|
|
204
|
-
function
|
|
188
|
+
function defaultCodegraphProcessRunner(invocation) {
|
|
205
189
|
return new Promise((resolveResult, reject) => {
|
|
206
190
|
const childProcess = spawn(invocation.executable, invocation.args, {
|
|
207
191
|
cwd: invocation.cwd,
|
|
@@ -211,8 +195,8 @@ function runCodegraphProcess(invocation, timeoutMs = CODEGRAPH_PROCESS_TIMEOUT_M
|
|
|
211
195
|
});
|
|
212
196
|
const timeout = setTimeout(() => {
|
|
213
197
|
terminateCodegraphProcess(childProcess);
|
|
214
|
-
reject(new Error(`codegraph process timed out after ${
|
|
215
|
-
},
|
|
198
|
+
reject(new Error(`codegraph process timed out after ${CODEGRAPH_PROCESS_TIMEOUT_MS}ms`));
|
|
199
|
+
}, CODEGRAPH_PROCESS_TIMEOUT_MS);
|
|
216
200
|
const stdoutChunks = [];
|
|
217
201
|
const stderrChunks = [];
|
|
218
202
|
let stdoutSize = 0;
|
|
@@ -251,9 +235,6 @@ function runCodegraphProcess(invocation, timeoutMs = CODEGRAPH_PROCESS_TIMEOUT_M
|
|
|
251
235
|
});
|
|
252
236
|
});
|
|
253
237
|
}
|
|
254
|
-
function defaultCodegraphProcessRunner(invocation) {
|
|
255
|
-
return runCodegraphProcess(invocation);
|
|
256
|
-
}
|
|
257
238
|
export function createCodegraphInvocation(options) {
|
|
258
239
|
assertSupportedSubcommand(options.subcommand);
|
|
259
240
|
const projectRoot = resolveProjectRoot(options.project);
|
|
@@ -156,25 +156,6 @@ function validateUserConfigPathForWrite(configPath) {
|
|
|
156
156
|
}
|
|
157
157
|
}
|
|
158
158
|
}
|
|
159
|
-
function getExistingBoundary(path) {
|
|
160
|
-
let currentPath = resolve(path);
|
|
161
|
-
while (!existsSync(currentPath)) {
|
|
162
|
-
const parentPath = dirname(currentPath);
|
|
163
|
-
if (parentPath === currentPath) {
|
|
164
|
-
return currentPath;
|
|
165
|
-
}
|
|
166
|
-
currentPath = parentPath;
|
|
167
|
-
}
|
|
168
|
-
return currentPath;
|
|
169
|
-
}
|
|
170
|
-
function validateArtifactWorkspaceRootBeforeCreate(artifactRoot, workspaceRoot) {
|
|
171
|
-
const workspaceRootReal = realpathSync(workspaceRoot);
|
|
172
|
-
const existingBoundary = getExistingBoundary(artifactRoot);
|
|
173
|
-
const existingBoundaryReal = realpathSync(existingBoundary);
|
|
174
|
-
if (isInsidePath(resolve(artifactRoot), workspaceRoot) || isInsidePath(existingBoundaryReal, workspaceRootReal)) {
|
|
175
|
-
throw new Error('Artifact workspace must stay outside the project root');
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
159
|
function validateArtifactWorkspaceRoot(artifactRoot, workspaceRoot) {
|
|
179
160
|
const artifactStats = lstatSync(artifactRoot);
|
|
180
161
|
if (!artifactStats.isDirectory() || artifactStats.isSymbolicLink()) {
|
|
@@ -890,7 +871,6 @@ function ensureArtifactWorkspaceMarker(workspace) {
|
|
|
890
871
|
const artifactRoot = getWorkspaceArtifactRoot(workspace);
|
|
891
872
|
const peaksPath = resolve(artifactRoot, '.peaks');
|
|
892
873
|
const markerPath = resolve(peaksPath, 'config.json');
|
|
893
|
-
validateArtifactWorkspaceRootBeforeCreate(artifactRoot, workspace.rootPath);
|
|
894
874
|
ensureDir(artifactRoot);
|
|
895
875
|
validateArtifactWorkspaceRoot(artifactRoot, workspace.rootPath);
|
|
896
876
|
ensureDir(peaksPath);
|
|
@@ -907,7 +887,7 @@ export function ensureWorkspaceConfigForPath(path = process.cwd()) {
|
|
|
907
887
|
const existingWorkspace = findWorkspaceForPath(config.workspaces, path);
|
|
908
888
|
if (existingWorkspace) {
|
|
909
889
|
ensureArtifactWorkspaceMarker(existingWorkspace);
|
|
910
|
-
if (config.currentWorkspace
|
|
890
|
+
if (!config.currentWorkspace) {
|
|
911
891
|
writeConfig({ currentWorkspace: existingWorkspace.workspaceId }, 'user');
|
|
912
892
|
}
|
|
913
893
|
return existingWorkspace;
|
|
@@ -923,7 +903,7 @@ export function ensureWorkspaceConfigForPath(path = process.cwd()) {
|
|
|
923
903
|
};
|
|
924
904
|
ensureArtifactWorkspaceMarker(workspace);
|
|
925
905
|
const updatedWorkspaces = [...config.workspaces, workspace];
|
|
926
|
-
writeConfig({ workspaces: updatedWorkspaces, currentWorkspace: workspace.workspaceId }, 'user');
|
|
906
|
+
writeConfig({ workspaces: updatedWorkspaces, ...(!config.currentWorkspace ? { currentWorkspace: workspace.workspaceId } : {}) }, 'user');
|
|
927
907
|
return workspace;
|
|
928
908
|
}
|
|
929
909
|
export function getWorkspaceConfigForCurrentPath() {
|