peaks-cli 1.0.12 → 1.0.14
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 +48 -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-process-runner.d.ts +2 -0
- package/dist/src/services/codegraph/codegraph-process-runner.js +93 -0
- package/dist/src/services/codegraph/codegraph-service.js +13 -128
- 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-presence-service.d.ts +10 -0
- package/dist/src/services/skills/skill-presence-service.js +54 -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/services/workflow/workflow-autonomous-service.js +7 -13
- 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 +61 -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 +76 -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 +98 -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 +46 -0
- package/skills/peaks-sc/references/openspec-commit-boundaries.md +33 -0
- package/skills/peaks-solo/SKILL.md +92 -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 +44 -0
- package/skills/peaks-ui/SKILL.md +59 -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
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
import { existsSync, realpathSync, statSync } from 'node:fs';
|
|
2
|
-
import { spawn } from 'node:child_process';
|
|
3
2
|
import { createRequire } from 'node:module';
|
|
4
|
-
import { dirname, isAbsolute, relative, resolve, sep
|
|
3
|
+
import { dirname, isAbsolute, relative, resolve, sep } from 'node:path';
|
|
4
|
+
import { defaultCodegraphProcessRunner } from './codegraph-process-runner.js';
|
|
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
|
-
const CODEGRAPH_PROCESS_TIMEOUT_MS = 600_000;
|
|
10
|
-
const CODEGRAPH_OUTPUT_LIMIT_BYTES = 10 * 1024 * 1024;
|
|
11
9
|
const POSITIONAL_ARGUMENT_PREFIX = '-';
|
|
12
10
|
const ALLOWED_SUBCOMMANDS = ['status', 'init', 'index', 'query', 'files', 'context', 'affected'];
|
|
11
|
+
const NUMERIC_FLAG_NAMES = ['limit', 'maxDepth'];
|
|
13
12
|
const COMMON_OPTION_KEYS = ['subcommand', 'project'];
|
|
14
13
|
const ALLOWED_OPTIONS_BY_SUBCOMMAND = {
|
|
15
14
|
status: [],
|
|
@@ -20,16 +19,10 @@ const ALLOWED_OPTIONS_BY_SUBCOMMAND = {
|
|
|
20
19
|
context: ['task'],
|
|
21
20
|
affected: ['files', 'json']
|
|
22
21
|
};
|
|
23
|
-
function assertCodegraphBinaryExists(binaryPath) {
|
|
24
|
-
if (!existsSync(binaryPath)) {
|
|
25
|
-
throw new Error('Unable to resolve local codegraph binary from @colbymchenry/codegraph');
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
22
|
function resolveCodegraphBinaryPath() {
|
|
29
23
|
const require = createRequire(import.meta.url);
|
|
30
24
|
const packageJsonPath = require.resolve('@colbymchenry/codegraph/package.json');
|
|
31
25
|
const binaryPath = resolve(dirname(packageJsonPath), 'dist', 'bin', 'codegraph.js');
|
|
32
|
-
assertCodegraphBinaryExists(binaryPath);
|
|
33
26
|
return binaryPath;
|
|
34
27
|
}
|
|
35
28
|
function assertSupportedSubcommand(subcommand) {
|
|
@@ -37,15 +30,12 @@ function assertSupportedSubcommand(subcommand) {
|
|
|
37
30
|
throw new Error(`Unsupported codegraph subcommand: ${subcommand}`);
|
|
38
31
|
}
|
|
39
32
|
}
|
|
40
|
-
function assertProjectRootDirectory(projectRoot) {
|
|
41
|
-
if (!statSync(projectRoot).isDirectory()) {
|
|
42
|
-
throw new Error('Project path must exist and be a directory');
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
33
|
function resolveProjectRoot(project) {
|
|
46
34
|
const projectRoot = resolve(project);
|
|
47
35
|
try {
|
|
48
|
-
|
|
36
|
+
if (!statSync(projectRoot).isDirectory()) {
|
|
37
|
+
throw new Error('Project path must exist and be a directory');
|
|
38
|
+
}
|
|
49
39
|
return realpathSync.native(projectRoot);
|
|
50
40
|
}
|
|
51
41
|
catch {
|
|
@@ -90,19 +80,17 @@ function assertInsideProject(projectRoot, absolutePath) {
|
|
|
90
80
|
throw new Error('Affected files must stay inside the project');
|
|
91
81
|
}
|
|
92
82
|
}
|
|
93
|
-
function
|
|
94
|
-
|
|
83
|
+
function resolveExistingBoundary(absoluteFilePath) {
|
|
84
|
+
if (existsSync(absoluteFilePath)) {
|
|
85
|
+
return absoluteFilePath;
|
|
86
|
+
}
|
|
87
|
+
let currentPath = dirname(absoluteFilePath);
|
|
88
|
+
while (!existsSync(currentPath)) {
|
|
95
89
|
const parentPath = dirname(currentPath);
|
|
96
|
-
if (parentPath === currentPath) {
|
|
97
|
-
return currentPath;
|
|
98
|
-
}
|
|
99
90
|
currentPath = parentPath;
|
|
100
91
|
}
|
|
101
92
|
return currentPath;
|
|
102
93
|
}
|
|
103
|
-
function resolveExistingBoundary(absoluteFilePath) {
|
|
104
|
-
return existsSync(absoluteFilePath) ? absoluteFilePath : climbToExistingBoundary(dirname(absoluteFilePath));
|
|
105
|
-
}
|
|
106
94
|
function normalizeProjectRelativeFile(projectRoot, file) {
|
|
107
95
|
assertPositionalArgument(file, 'Affected files');
|
|
108
96
|
const absoluteFilePath = resolve(projectRoot, file);
|
|
@@ -111,13 +99,10 @@ function normalizeProjectRelativeFile(projectRoot, file) {
|
|
|
111
99
|
assertInsideProject(projectRoot, realBoundary);
|
|
112
100
|
return relative(projectRoot, absoluteFilePath).split(sep).join('/');
|
|
113
101
|
}
|
|
114
|
-
function
|
|
102
|
+
function buildAffectedFileArgs(projectRoot, files) {
|
|
115
103
|
if (!files || files.length < 1) {
|
|
116
104
|
throw new Error('affected requires at least one file');
|
|
117
105
|
}
|
|
118
|
-
}
|
|
119
|
-
function buildAffectedFileArgs(projectRoot, files) {
|
|
120
|
-
assertAffectedFiles(files);
|
|
121
106
|
return files.map((file) => normalizeProjectRelativeFile(projectRoot, file));
|
|
122
107
|
}
|
|
123
108
|
function buildCommandArgs(options, projectRoot) {
|
|
@@ -154,106 +139,6 @@ function buildCommandArgs(options, projectRoot) {
|
|
|
154
139
|
}
|
|
155
140
|
return args;
|
|
156
141
|
}
|
|
157
|
-
function createCodegraphEnvironment(sourceEnv = process.env) {
|
|
158
|
-
const preservedKeys = ['PATH', 'Path', 'HOME', 'USERPROFILE', 'APPDATA', 'LOCALAPPDATA', 'TEMP', 'TMP', 'SystemRoot', 'WINDIR'];
|
|
159
|
-
const environment = {};
|
|
160
|
-
for (const key of preservedKeys) {
|
|
161
|
-
const value = sourceEnv[key];
|
|
162
|
-
if (value !== undefined) {
|
|
163
|
-
environment[key] = value;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
return environment;
|
|
167
|
-
}
|
|
168
|
-
function assertOutputLimit(currentSize, chunkSize) {
|
|
169
|
-
const nextSize = currentSize + chunkSize;
|
|
170
|
-
if (nextSize > CODEGRAPH_OUTPUT_LIMIT_BYTES) {
|
|
171
|
-
throw new Error(`codegraph output exceeded ${CODEGRAPH_OUTPUT_LIMIT_BYTES} bytes`);
|
|
172
|
-
}
|
|
173
|
-
return nextSize;
|
|
174
|
-
}
|
|
175
|
-
function getWindowsTaskkillPath(fileExists = existsSync) {
|
|
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()) {
|
|
183
|
-
if (childProcess.pid === undefined) {
|
|
184
|
-
childProcess.kill();
|
|
185
|
-
return;
|
|
186
|
-
}
|
|
187
|
-
if (platform === 'win32') {
|
|
188
|
-
if (!taskkillPath) {
|
|
189
|
-
childProcess.kill();
|
|
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();
|
|
195
|
-
return;
|
|
196
|
-
}
|
|
197
|
-
try {
|
|
198
|
-
killProcess(-childProcess.pid, 'SIGTERM');
|
|
199
|
-
}
|
|
200
|
-
catch {
|
|
201
|
-
childProcess.kill('SIGTERM');
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
function runCodegraphProcess(invocation, timeoutMs = CODEGRAPH_PROCESS_TIMEOUT_MS) {
|
|
205
|
-
return new Promise((resolveResult, reject) => {
|
|
206
|
-
const childProcess = spawn(invocation.executable, invocation.args, {
|
|
207
|
-
cwd: invocation.cwd,
|
|
208
|
-
detached: process.platform !== 'win32',
|
|
209
|
-
env: createCodegraphEnvironment(),
|
|
210
|
-
shell: false
|
|
211
|
-
});
|
|
212
|
-
const timeout = setTimeout(() => {
|
|
213
|
-
terminateCodegraphProcess(childProcess);
|
|
214
|
-
reject(new Error(`codegraph process timed out after ${timeoutMs}ms`));
|
|
215
|
-
}, timeoutMs);
|
|
216
|
-
const stdoutChunks = [];
|
|
217
|
-
const stderrChunks = [];
|
|
218
|
-
let stdoutSize = 0;
|
|
219
|
-
let stderrSize = 0;
|
|
220
|
-
childProcess.stdout.on('data', (chunk) => {
|
|
221
|
-
try {
|
|
222
|
-
stdoutSize = assertOutputLimit(stdoutSize, chunk.length);
|
|
223
|
-
stdoutChunks.push(chunk);
|
|
224
|
-
}
|
|
225
|
-
catch (error) {
|
|
226
|
-
terminateCodegraphProcess(childProcess);
|
|
227
|
-
reject(error);
|
|
228
|
-
}
|
|
229
|
-
});
|
|
230
|
-
childProcess.stderr.on('data', (chunk) => {
|
|
231
|
-
try {
|
|
232
|
-
stderrSize = assertOutputLimit(stderrSize, chunk.length);
|
|
233
|
-
stderrChunks.push(chunk);
|
|
234
|
-
}
|
|
235
|
-
catch (error) {
|
|
236
|
-
terminateCodegraphProcess(childProcess);
|
|
237
|
-
reject(error);
|
|
238
|
-
}
|
|
239
|
-
});
|
|
240
|
-
childProcess.on('error', (error) => {
|
|
241
|
-
clearTimeout(timeout);
|
|
242
|
-
reject(error);
|
|
243
|
-
});
|
|
244
|
-
childProcess.on('close', (exitCode) => {
|
|
245
|
-
clearTimeout(timeout);
|
|
246
|
-
resolveResult({
|
|
247
|
-
exitCode,
|
|
248
|
-
stdout: Buffer.concat(stdoutChunks).toString('utf8'),
|
|
249
|
-
stderr: Buffer.concat(stderrChunks).toString('utf8')
|
|
250
|
-
});
|
|
251
|
-
});
|
|
252
|
-
});
|
|
253
|
-
}
|
|
254
|
-
function defaultCodegraphProcessRunner(invocation) {
|
|
255
|
-
return runCodegraphProcess(invocation);
|
|
256
|
-
}
|
|
257
142
|
export function createCodegraphInvocation(options) {
|
|
258
143
|
assertSupportedSubcommand(options.subcommand);
|
|
259
144
|
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() {
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { type RequestArtifactRole, type RequestArtifactSummary } from '../artifacts/request-artifact-service.js';
|
|
2
|
+
import type { OpenSpecChangeSummary } from '../openspec/openspec-types.js';
|
|
3
|
+
import type { McpScanReport } from '../mcp/mcp-types.js';
|
|
4
|
+
import type { CapabilityItem } from '../recommendations/recommendation-types.js';
|
|
5
|
+
export type ProjectDashboardRequests = {
|
|
6
|
+
count: number;
|
|
7
|
+
byRole: Record<RequestArtifactRole, RequestArtifactSummary[]>;
|
|
8
|
+
byState: Record<string, number>;
|
|
9
|
+
};
|
|
10
|
+
export type ProjectDashboardOpenSpec = {
|
|
11
|
+
exists: boolean;
|
|
12
|
+
count: number;
|
|
13
|
+
changes: OpenSpecChangeSummary[];
|
|
14
|
+
};
|
|
15
|
+
export type ProjectDashboardUnderstand = {
|
|
16
|
+
exists: boolean;
|
|
17
|
+
graphExists: boolean;
|
|
18
|
+
graphPath: string;
|
|
19
|
+
parseError?: string;
|
|
20
|
+
};
|
|
21
|
+
export type ProjectDashboardMcp = {
|
|
22
|
+
servers: McpScanReport['servers'];
|
|
23
|
+
scopes: McpScanReport['scopes'];
|
|
24
|
+
};
|
|
25
|
+
export type ProjectDashboardDoctor = {
|
|
26
|
+
ok: boolean;
|
|
27
|
+
passed: number;
|
|
28
|
+
failed: number;
|
|
29
|
+
};
|
|
30
|
+
export type ProjectDashboardRunbookHealth = {
|
|
31
|
+
ok: boolean;
|
|
32
|
+
required: number;
|
|
33
|
+
healthy: number;
|
|
34
|
+
missingRunbook: string[];
|
|
35
|
+
applyNoteFailed: string[];
|
|
36
|
+
};
|
|
37
|
+
export type ProjectDashboardCapabilities = {
|
|
38
|
+
count: number;
|
|
39
|
+
mcpCount: number;
|
|
40
|
+
sample: Array<Pick<CapabilityItem, 'capabilityId' | 'name' | 'itemType' | 'category'>>;
|
|
41
|
+
};
|
|
42
|
+
export type ProjectDashboard = {
|
|
43
|
+
generatedAt: string;
|
|
44
|
+
projectRoot: string;
|
|
45
|
+
requests: ProjectDashboardRequests;
|
|
46
|
+
openspec: ProjectDashboardOpenSpec;
|
|
47
|
+
understand: ProjectDashboardUnderstand;
|
|
48
|
+
mcp: ProjectDashboardMcp;
|
|
49
|
+
doctor: ProjectDashboardDoctor;
|
|
50
|
+
runbookHealth: ProjectDashboardRunbookHealth;
|
|
51
|
+
capabilities: ProjectDashboardCapabilities;
|
|
52
|
+
};
|
|
53
|
+
export type LoadProjectDashboardOptions = {
|
|
54
|
+
projectRoot: string;
|
|
55
|
+
sampleCapabilities?: number;
|
|
56
|
+
clock?: () => string;
|
|
57
|
+
doctorReport?: {
|
|
58
|
+
ok: boolean;
|
|
59
|
+
passed: number;
|
|
60
|
+
failed: number;
|
|
61
|
+
};
|
|
62
|
+
runbookHealth?: ProjectDashboardRunbookHealth;
|
|
63
|
+
};
|
|
64
|
+
export declare function loadProjectDashboard(options: LoadProjectDashboardOptions): Promise<ProjectDashboard>;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { listRequestArtifacts } from '../artifacts/request-artifact-service.js';
|
|
2
|
+
import { scanOpenSpec } from '../openspec/openspec-scan-service.js';
|
|
3
|
+
import { scanMcpServers } from '../mcp/mcp-scan-service.js';
|
|
4
|
+
import { scanUnderstandAnything } from '../understand/understand-scan-service.js';
|
|
5
|
+
import { seedCapabilityItems } from '../recommendations/capability-seed-items.js';
|
|
6
|
+
import { requiredSkillNames } from '../../shared/paths.js';
|
|
7
|
+
function defaultClock() {
|
|
8
|
+
return new Date().toISOString();
|
|
9
|
+
}
|
|
10
|
+
function groupRequestsByRole(items) {
|
|
11
|
+
const byRole = { prd: [], ui: [], rd: [], qa: [] };
|
|
12
|
+
for (const item of items) {
|
|
13
|
+
byRole[item.role].push(item);
|
|
14
|
+
}
|
|
15
|
+
return byRole;
|
|
16
|
+
}
|
|
17
|
+
function countRequestsByState(items) {
|
|
18
|
+
const counts = {};
|
|
19
|
+
for (const item of items) {
|
|
20
|
+
counts[item.state] = (counts[item.state] ?? 0) + 1;
|
|
21
|
+
}
|
|
22
|
+
return counts;
|
|
23
|
+
}
|
|
24
|
+
async function loadDoctorAndRunbookHealth(doctorOverride, runbookOverride) {
|
|
25
|
+
if (doctorOverride !== undefined && runbookOverride !== undefined) {
|
|
26
|
+
return { doctor: doctorOverride, runbookHealth: runbookOverride };
|
|
27
|
+
}
|
|
28
|
+
if (doctorOverride !== undefined) {
|
|
29
|
+
return {
|
|
30
|
+
doctor: doctorOverride,
|
|
31
|
+
runbookHealth: { ok: true, required: 0, healthy: 0, missingRunbook: [], applyNoteFailed: [] }
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
const { runDoctor } = await import('../doctor/doctor-service.js');
|
|
35
|
+
const report = await runDoctor();
|
|
36
|
+
return {
|
|
37
|
+
doctor: { ok: report.summary.ok, passed: report.summary.passed, failed: report.summary.failed },
|
|
38
|
+
runbookHealth: runbookOverride ?? summarizeRunbookHealth(report.checks)
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function summarizeRunbookHealth(checks) {
|
|
42
|
+
const missingRunbook = [];
|
|
43
|
+
const applyNoteFailed = [];
|
|
44
|
+
for (const check of checks) {
|
|
45
|
+
if (!check.ok && check.id.startsWith('skill-runbook:')) {
|
|
46
|
+
missingRunbook.push(check.id.slice('skill-runbook:'.length));
|
|
47
|
+
}
|
|
48
|
+
if (!check.ok && check.id.startsWith('skill-apply-note:')) {
|
|
49
|
+
applyNoteFailed.push(check.id.slice('skill-apply-note:'.length));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const required = requiredSkillNames.length;
|
|
53
|
+
const healthy = Math.max(0, required - missingRunbook.length - applyNoteFailed.length);
|
|
54
|
+
return {
|
|
55
|
+
ok: missingRunbook.length === 0 && applyNoteFailed.length === 0,
|
|
56
|
+
required,
|
|
57
|
+
healthy,
|
|
58
|
+
missingRunbook,
|
|
59
|
+
applyNoteFailed
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function buildCapabilitiesSummary(sampleSize) {
|
|
63
|
+
const items = seedCapabilityItems;
|
|
64
|
+
return {
|
|
65
|
+
count: items.length,
|
|
66
|
+
mcpCount: items.filter((item) => item.itemType === 'mcp').length,
|
|
67
|
+
sample: items.slice(0, sampleSize).map((item) => ({
|
|
68
|
+
capabilityId: item.capabilityId,
|
|
69
|
+
name: item.name,
|
|
70
|
+
itemType: item.itemType,
|
|
71
|
+
category: item.category
|
|
72
|
+
}))
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
export async function loadProjectDashboard(options) {
|
|
76
|
+
const clock = options.clock ?? defaultClock;
|
|
77
|
+
const sampleSize = options.sampleCapabilities ?? 8;
|
|
78
|
+
const [items, openspecReport, mcpReport, understandReport, doctorAndRunbook] = await Promise.all([
|
|
79
|
+
listRequestArtifacts({ projectRoot: options.projectRoot }),
|
|
80
|
+
scanOpenSpec({ openspecRoot: `${options.projectRoot}/openspec` }),
|
|
81
|
+
scanMcpServers({ projectRoot: options.projectRoot }),
|
|
82
|
+
scanUnderstandAnything({ projectRoot: options.projectRoot }),
|
|
83
|
+
loadDoctorAndRunbookHealth(options.doctorReport, options.runbookHealth)
|
|
84
|
+
]);
|
|
85
|
+
return {
|
|
86
|
+
generatedAt: clock(),
|
|
87
|
+
projectRoot: options.projectRoot,
|
|
88
|
+
requests: {
|
|
89
|
+
count: items.length,
|
|
90
|
+
byRole: groupRequestsByRole(items),
|
|
91
|
+
byState: countRequestsByState(items)
|
|
92
|
+
},
|
|
93
|
+
openspec: {
|
|
94
|
+
exists: openspecReport.exists,
|
|
95
|
+
count: openspecReport.changes.length,
|
|
96
|
+
changes: openspecReport.changes
|
|
97
|
+
},
|
|
98
|
+
understand: {
|
|
99
|
+
exists: understandReport.exists,
|
|
100
|
+
graphExists: understandReport.graph.exists,
|
|
101
|
+
graphPath: understandReport.graph.path,
|
|
102
|
+
...(understandReport.graph.parseError !== undefined ? { parseError: understandReport.graph.parseError } : {})
|
|
103
|
+
},
|
|
104
|
+
mcp: {
|
|
105
|
+
servers: mcpReport.servers,
|
|
106
|
+
scopes: mcpReport.scopes
|
|
107
|
+
},
|
|
108
|
+
doctor: doctorAndRunbook.doctor,
|
|
109
|
+
runbookHealth: doctorAndRunbook.runbookHealth,
|
|
110
|
+
capabilities: buildCapabilitiesSummary(sampleSize)
|
|
111
|
+
};
|
|
112
|
+
}
|
|
@@ -11,8 +11,15 @@ export type DoctorReport = {
|
|
|
11
11
|
failed: number;
|
|
12
12
|
};
|
|
13
13
|
};
|
|
14
|
+
export type CodegraphCapabilityProbe = {
|
|
15
|
+
packagePath: string;
|
|
16
|
+
version: string;
|
|
17
|
+
binaryPath: string;
|
|
18
|
+
binaryExists: boolean;
|
|
19
|
+
};
|
|
14
20
|
export type DoctorOptions = {
|
|
15
21
|
schemasBaseDir?: string;
|
|
16
22
|
skillsBaseDir?: string;
|
|
23
|
+
codegraphProbe?: () => CodegraphCapabilityProbe;
|
|
17
24
|
};
|
|
18
25
|
export declare function runDoctor(options?: DoctorOptions): Promise<DoctorReport>;
|
|
@@ -1,10 +1,41 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { existsSync } from 'node:fs';
|
|
4
|
+
import { createRequire } from 'node:module';
|
|
5
|
+
import { dirname, resolve as resolvePath } from 'node:path';
|
|
4
6
|
import { readText } from '../../shared/fs.js';
|
|
5
7
|
import { requiredSchemaFiles, requiredSkillNames, schemasDir } from '../../shared/paths.js';
|
|
6
8
|
import { getErrorMessage } from '../../shared/result.js';
|
|
7
9
|
import { loadSkillRegistry } from '../skills/skill-registry.js';
|
|
10
|
+
const CODEGRAPH_EXPECTED_VERSION = '0.7.10';
|
|
11
|
+
function defaultCodegraphProbe() {
|
|
12
|
+
const require = createRequire(import.meta.url);
|
|
13
|
+
const packagePath = require.resolve('@colbymchenry/codegraph/package.json');
|
|
14
|
+
const pkg = require(packagePath);
|
|
15
|
+
const binaryPath = resolvePath(dirname(packagePath), 'dist', 'bin', 'codegraph.js');
|
|
16
|
+
return {
|
|
17
|
+
packagePath,
|
|
18
|
+
version: pkg.version ?? 'unknown',
|
|
19
|
+
binaryPath,
|
|
20
|
+
binaryExists: existsSync(binaryPath)
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
const DESTRUCTIVE_APPLY_PATTERNS = [
|
|
24
|
+
/peaks\s+memory\s+sync[^\n]*--apply/,
|
|
25
|
+
/peaks\s+memory\s+extract[^\n]*--apply/,
|
|
26
|
+
/peaks\s+artifacts\s+sync[^\n]*--apply/,
|
|
27
|
+
/peaks\s+openspec\s+archive[^\n]*--apply/,
|
|
28
|
+
/peaks\s+standards\s+(?:init|update)[^\n]*--apply/
|
|
29
|
+
];
|
|
30
|
+
const AUTHORIZATION_KEYWORDS_PATTERN = /authoriz|explicit|--dry-run|approv|only after|only when/i;
|
|
31
|
+
function extractRunbookSection(body) {
|
|
32
|
+
const match = /## Default runbook\n+([\s\S]*?)(?=\n## |$)/.exec(body);
|
|
33
|
+
return match === null ? null : (match[1] ?? null);
|
|
34
|
+
}
|
|
35
|
+
function findDestructiveApplyLines(section) {
|
|
36
|
+
const lines = section.split(/\r?\n/);
|
|
37
|
+
return lines.filter((line) => DESTRUCTIVE_APPLY_PATTERNS.some((pattern) => pattern.test(line)));
|
|
38
|
+
}
|
|
8
39
|
export async function runDoctor(options = {}) {
|
|
9
40
|
const checks = [];
|
|
10
41
|
const registry = await loadSkillRegistry(options.skillsBaseDir);
|
|
@@ -35,6 +66,51 @@ export async function runDoctor(options = {}) {
|
|
|
35
66
|
message: `Skill ${failure.directory} has invalid metadata: ${failure.message}`
|
|
36
67
|
});
|
|
37
68
|
}
|
|
69
|
+
const requiredSkillNameSet = new Set(requiredSkillNames);
|
|
70
|
+
for (const skill of skills) {
|
|
71
|
+
if (!requiredSkillNameSet.has(skill.name)) {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
const body = await readText(skill.skillPath);
|
|
76
|
+
const hasRunbook = /## Default runbook\s/.test(body);
|
|
77
|
+
checks.push({
|
|
78
|
+
id: `skill-runbook:${skill.name}`,
|
|
79
|
+
ok: hasRunbook,
|
|
80
|
+
message: hasRunbook
|
|
81
|
+
? `Skill ${skill.name} declares a Default runbook`
|
|
82
|
+
: `Skill ${skill.name} is missing a ## Default runbook section`
|
|
83
|
+
});
|
|
84
|
+
const runbookSection = extractRunbookSection(body);
|
|
85
|
+
if (runbookSection !== null) {
|
|
86
|
+
const destructiveLines = findDestructiveApplyLines(runbookSection);
|
|
87
|
+
if (destructiveLines.length === 0) {
|
|
88
|
+
checks.push({
|
|
89
|
+
id: `skill-apply-note:${skill.name}`,
|
|
90
|
+
ok: true,
|
|
91
|
+
message: `Skill ${skill.name} runbook has no destructive --apply commands to gate`
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
const hasAuthorizationNote = AUTHORIZATION_KEYWORDS_PATTERN.test(runbookSection);
|
|
96
|
+
checks.push({
|
|
97
|
+
id: `skill-apply-note:${skill.name}`,
|
|
98
|
+
ok: hasAuthorizationNote,
|
|
99
|
+
message: hasAuthorizationNote
|
|
100
|
+
? `Skill ${skill.name} gates ${destructiveLines.length} destructive --apply command(s) with an authorization note`
|
|
101
|
+
: `Skill ${skill.name} has ${destructiveLines.length} destructive --apply command(s) without an authorization/dry-run note in the runbook section`
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
checks.push({
|
|
108
|
+
id: `skill-runbook:${skill.name}`,
|
|
109
|
+
ok: false,
|
|
110
|
+
message: `Skill ${skill.name} runbook check failed: ${getErrorMessage(error)}`
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
38
114
|
const schemaRoot = options.schemasBaseDir ?? schemasDir;
|
|
39
115
|
for (const schemaFile of requiredSchemaFiles) {
|
|
40
116
|
try {
|
|
@@ -56,6 +132,69 @@ export async function runDoctor(options = {}) {
|
|
|
56
132
|
ok: true,
|
|
57
133
|
message: hasUserConfig ? 'User config exists at ~/.peaks/config.json' : 'Optional user config not found at ~/.peaks/config.json'
|
|
58
134
|
});
|
|
135
|
+
const probe = options.codegraphProbe ?? defaultCodegraphProbe;
|
|
136
|
+
try {
|
|
137
|
+
const result = probe();
|
|
138
|
+
const versionOk = result.version === CODEGRAPH_EXPECTED_VERSION;
|
|
139
|
+
if (!versionOk) {
|
|
140
|
+
checks.push({
|
|
141
|
+
id: 'capability:codegraph',
|
|
142
|
+
ok: false,
|
|
143
|
+
message: `@colbymchenry/codegraph version mismatch: expected ${CODEGRAPH_EXPECTED_VERSION}, resolved ${result.version} at ${result.packagePath}`
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
else if (!result.binaryExists) {
|
|
147
|
+
checks.push({
|
|
148
|
+
id: 'capability:codegraph',
|
|
149
|
+
ok: false,
|
|
150
|
+
message: `@colbymchenry/codegraph@${result.version} resolved at ${result.packagePath} but binary is missing at ${result.binaryPath}`
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
checks.push({
|
|
155
|
+
id: 'capability:codegraph',
|
|
156
|
+
ok: true,
|
|
157
|
+
message: `@colbymchenry/codegraph@${result.version} resolves with binary at ${result.binaryPath}`
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
checks.push({
|
|
163
|
+
id: 'capability:codegraph',
|
|
164
|
+
ok: false,
|
|
165
|
+
message: `@colbymchenry/codegraph not resolvable: ${getErrorMessage(error)}`
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
try {
|
|
169
|
+
const schemaText = await readText(join(schemaRoot, 'doctor-report.schema.json'));
|
|
170
|
+
const schema = JSON.parse(schemaText);
|
|
171
|
+
const patternSource = schema.properties?.checks?.items?.properties?.id?.pattern;
|
|
172
|
+
if (typeof patternSource === 'string') {
|
|
173
|
+
const pattern = new RegExp(patternSource);
|
|
174
|
+
const mismatches = checks.filter((check) => !pattern.test(check.id)).map((check) => check.id);
|
|
175
|
+
checks.push({
|
|
176
|
+
id: 'doctor-self:check-id-pattern',
|
|
177
|
+
ok: mismatches.length === 0,
|
|
178
|
+
message: mismatches.length === 0
|
|
179
|
+
? 'All doctor check IDs match the doctor-report schema pattern'
|
|
180
|
+
: `Doctor check IDs missing from schema pattern: ${mismatches.join(', ')}`
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
checks.push({
|
|
185
|
+
id: 'doctor-self:check-id-pattern',
|
|
186
|
+
ok: false,
|
|
187
|
+
message: 'doctor-report.schema.json does not declare a check.id pattern'
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
checks.push({
|
|
193
|
+
id: 'doctor-self:check-id-pattern',
|
|
194
|
+
ok: false,
|
|
195
|
+
message: `Failed to load doctor-report.schema.json for self-validation: ${getErrorMessage(error)}`
|
|
196
|
+
});
|
|
197
|
+
}
|
|
59
198
|
const failed = checks.filter((check) => !check.ok).length;
|
|
60
199
|
return {
|
|
61
200
|
checks,
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type PlanMcpInstallOptions, type McpInstallEnvCheck } from './mcp-plan-service.js';
|
|
2
|
+
export type McpApplyAction = 'add' | 'update' | 'claimed' | 'noop';
|
|
3
|
+
export type McpApplyBackupInfo = {
|
|
4
|
+
path: string | null;
|
|
5
|
+
skipped: boolean;
|
|
6
|
+
};
|
|
7
|
+
export type McpApplyResult = {
|
|
8
|
+
capabilityId: string;
|
|
9
|
+
action: McpApplyAction;
|
|
10
|
+
backup: McpApplyBackupInfo;
|
|
11
|
+
written: {
|
|
12
|
+
settingsPath: string;
|
|
13
|
+
managedMarkerPath: string;
|
|
14
|
+
};
|
|
15
|
+
envCheck: McpInstallEnvCheck;
|
|
16
|
+
};
|
|
17
|
+
export type McpApplyOptions = PlanMcpInstallOptions & {
|
|
18
|
+
claim?: boolean;
|
|
19
|
+
backupRoot?: string;
|
|
20
|
+
clock?: () => string;
|
|
21
|
+
};
|
|
22
|
+
export type McpRollbackOptions = {
|
|
23
|
+
backupPath: string;
|
|
24
|
+
globalSettingsPath?: string;
|
|
25
|
+
};
|
|
26
|
+
export type McpRollbackResult = {
|
|
27
|
+
restoredFrom: string;
|
|
28
|
+
restoredTo: string;
|
|
29
|
+
};
|
|
30
|
+
export declare function applyMcpInstall(capabilityId: string, options?: McpApplyOptions): Promise<McpApplyResult>;
|
|
31
|
+
export declare function rollbackMcpInstall(options: McpRollbackOptions): Promise<McpRollbackResult>;
|