peaks-cli 1.2.7 → 1.2.9
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/README.md +12 -0
- package/dist/src/cli/commands/core-artifact-commands.js +36 -1
- package/dist/src/cli/commands/perf-commands.d.ts +3 -0
- package/dist/src/cli/commands/perf-commands.js +41 -0
- package/dist/src/cli/commands/progress-close-kill.d.ts +51 -0
- package/dist/src/cli/commands/progress-close-kill.js +152 -0
- package/dist/src/cli/commands/progress-commands.d.ts +3 -0
- package/dist/src/cli/commands/progress-commands.js +348 -0
- package/dist/src/cli/commands/progress-start-spawn.d.ts +59 -0
- package/dist/src/cli/commands/progress-start-spawn.js +114 -0
- package/dist/src/cli/commands/progress-watch-render.d.ts +80 -0
- package/dist/src/cli/commands/progress-watch-render.js +308 -0
- package/dist/src/cli/commands/project-commands.js +1 -1
- package/dist/src/cli/commands/scan-commands.js +22 -0
- package/dist/src/cli/program.js +4 -0
- package/dist/src/services/config/config-types.d.ts +20 -0
- package/dist/src/services/config/config-types.js +5 -1
- package/dist/src/services/memory/project-memory-service.d.ts +1 -1
- package/dist/src/services/memory/project-memory-service.js +52 -23
- package/dist/src/services/perf/perf-baseline-service.d.ts +70 -0
- package/dist/src/services/perf/perf-baseline-service.js +213 -0
- package/dist/src/services/progress/progress-service.d.ts +179 -0
- package/dist/src/services/progress/progress-service.js +276 -0
- package/dist/src/services/scan/libraries-service.d.ts +24 -0
- package/dist/src/services/scan/libraries-service.js +419 -0
- package/dist/src/services/scan/libraries-types.d.ts +59 -0
- package/dist/src/services/scan/libraries-types.js +9 -0
- package/dist/src/services/session/index.d.ts +1 -1
- package/dist/src/services/session/index.js +1 -1
- package/dist/src/services/session/session-manager.d.ts +53 -8
- package/dist/src/services/session/session-manager.js +150 -3
- package/dist/src/services/skills/skill-presence-service.d.ts +27 -1
- package/dist/src/services/skills/skill-presence-service.js +112 -9
- package/dist/src/services/skills/skill-runbook-service.js +34 -1
- package/dist/src/services/workflow/autonomous-resume-writer.js +7 -7
- package/dist/src/shared/change-id.d.ts +30 -0
- package/dist/src/shared/change-id.js +40 -6
- package/dist/src/shared/paths.d.ts +1 -1
- package/dist/src/shared/paths.js +2 -1
- package/dist/src/shared/version.d.ts +1 -1
- package/dist/src/shared/version.js +1 -1
- package/package.json +6 -2
- package/schemas/library-breaking-changes.data.json +141 -0
- package/schemas/library-breaking-changes.meta.json +6 -0
- package/schemas/library-breaking-changes.schema.json +50 -0
- package/skills/peaks-qa/SKILL.md +25 -0
- package/skills/peaks-rd/SKILL.md +221 -2
- package/skills/peaks-solo/SKILL.md +76 -316
- package/skills/peaks-solo/references/runbook.md +166 -0
- package/skills/peaks-solo/references/workflow-gates-and-types.md +177 -0
- package/skills/peaks-solo-resume/SKILL.md +81 -0
- package/skills/peaks-solo-status/SKILL.md +120 -0
- package/skills/peaks-solo-test/SKILL.md +84 -0
- package/skills/peaks-txt/SKILL.md +8 -5
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
import { readdir } from 'node:fs/promises';
|
|
2
|
+
import { join, sep } from 'node:path';
|
|
3
|
+
import { isDirectory, pathExists, readText } from '../../shared/fs.js';
|
|
4
|
+
const SCOPES = [
|
|
5
|
+
'dependencies',
|
|
6
|
+
'devDependencies',
|
|
7
|
+
'peerDependencies',
|
|
8
|
+
'optionalDependencies'
|
|
9
|
+
];
|
|
10
|
+
const SCOPE_ORDER = {
|
|
11
|
+
dependencies: 0,
|
|
12
|
+
devDependencies: 1,
|
|
13
|
+
peerDependencies: 2,
|
|
14
|
+
optionalDependencies: 3
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Parse the major version from a semver-ish spec.
|
|
18
|
+
*
|
|
19
|
+
* Handles the common shapes:
|
|
20
|
+
* "^5.18.0" → 5
|
|
21
|
+
* "~1.2.3" → 1
|
|
22
|
+
* "1.2.3" → 1
|
|
23
|
+
* ">=1.0.0" → 1
|
|
24
|
+
* "5" → 5
|
|
25
|
+
* "5.x" → 5
|
|
26
|
+
*
|
|
27
|
+
* Returns null for non-semver specs that the LLM should not assume a
|
|
28
|
+
* major for:
|
|
29
|
+
* "workspace:*" → null
|
|
30
|
+
* "file:../..." → null
|
|
31
|
+
* "git+https..." → null
|
|
32
|
+
* "npm:@scope/x@1" → 1 (alias spec, we extract what we can)
|
|
33
|
+
*/
|
|
34
|
+
export function parseMajorVersion(spec) {
|
|
35
|
+
// Strip npm alias prefix "npm:" and take whatever comes after the LAST "@"
|
|
36
|
+
// (since alias specs are "npm:<name>@<version>")
|
|
37
|
+
let versionPart = spec;
|
|
38
|
+
if (versionPart.startsWith('npm:')) {
|
|
39
|
+
const atIdx = versionPart.lastIndexOf('@');
|
|
40
|
+
versionPart = atIdx >= 0 ? versionPart.slice(atIdx + 1) : versionPart.slice(4);
|
|
41
|
+
}
|
|
42
|
+
// Strip semver range operators (caret, tilde, comparison ops, exact)
|
|
43
|
+
versionPart = versionPart.replace(/^[\^~>=<]+\s*/, '').trim();
|
|
44
|
+
// Take the first numeric component
|
|
45
|
+
const match = /^(\d+)/.exec(versionPart);
|
|
46
|
+
if (!match)
|
|
47
|
+
return null;
|
|
48
|
+
const n = Number(match[1]);
|
|
49
|
+
return Number.isSafeInteger(n) ? n : null;
|
|
50
|
+
}
|
|
51
|
+
async function readPackageJson(packageJsonPath) {
|
|
52
|
+
if (!(await pathExists(packageJsonPath))) {
|
|
53
|
+
return { exists: false, record: null };
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
const raw = await readText(packageJsonPath);
|
|
57
|
+
const record = JSON.parse(raw);
|
|
58
|
+
return { exists: true, record };
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
return { exists: true, record: null, error: error.message };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Hand-rolled YAML parser for `pnpm-workspace.yaml`.
|
|
66
|
+
*
|
|
67
|
+
* Only consumes the top-level `packages:` list. Ignores all other top-level
|
|
68
|
+
* keys (`allowBuilds:`, `catalog:`, etc.) and indented blocks. Returns
|
|
69
|
+
* `null` when the file is present but does not contain a `packages:`
|
|
70
|
+
* list — callers should fall through to the next detection source.
|
|
71
|
+
*
|
|
72
|
+
* Supports the common shapes:
|
|
73
|
+
* packages:
|
|
74
|
+
* - 'packages/*'
|
|
75
|
+
* - "apps/web"
|
|
76
|
+
* - tools
|
|
77
|
+
* - packages/hermes-agent/*
|
|
78
|
+
*/
|
|
79
|
+
function parsePnpmWorkspaceYaml(content) {
|
|
80
|
+
const lines = content.split(/\r?\n/);
|
|
81
|
+
let inPackagesList = false;
|
|
82
|
+
let packagesIndent = -1;
|
|
83
|
+
const result = [];
|
|
84
|
+
for (const rawLine of lines) {
|
|
85
|
+
// Strip comments (anything after # not inside quotes — close enough for our use)
|
|
86
|
+
const commentIdx = rawLine.indexOf('#');
|
|
87
|
+
const line = (commentIdx >= 0 ? rawLine.slice(0, commentIdx) : rawLine).replace(/\s+$/, '');
|
|
88
|
+
if (line.length === 0)
|
|
89
|
+
continue;
|
|
90
|
+
if (!inPackagesList) {
|
|
91
|
+
// Looking for the top-level `packages:` key.
|
|
92
|
+
const match = /^packages:\s*$/.exec(line);
|
|
93
|
+
if (match) {
|
|
94
|
+
inPackagesList = true;
|
|
95
|
+
}
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
// Inside the packages list. Items must be more indented than the key.
|
|
99
|
+
const indent = line.match(/^\s*/)?.[0].length ?? 0;
|
|
100
|
+
if (packagesIndent < 0) {
|
|
101
|
+
// First non-empty line after the key — this sets the indent.
|
|
102
|
+
if (indent === 0) {
|
|
103
|
+
// `packages:` was the last meaningful line; an unindented line
|
|
104
|
+
// means the list is empty or the format is unsupported.
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
packagesIndent = indent;
|
|
108
|
+
}
|
|
109
|
+
else if (indent < packagesIndent) {
|
|
110
|
+
// Outdented — the list ended.
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
else if (indent > packagesIndent) {
|
|
114
|
+
// Nested list (e.g. `packages: [{ ... }]`) — unsupported; stop.
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
// Expect `- <value>` or `- '<value>'` or `- "<value>"`.
|
|
118
|
+
const itemMatch = /^\s*-\s+(['"]?)(.+?)\1\s*$/.exec(line);
|
|
119
|
+
if (itemMatch && itemMatch[2] !== undefined) {
|
|
120
|
+
result.push(itemMatch[2]);
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
// Not a list item; the block ended.
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return result;
|
|
128
|
+
}
|
|
129
|
+
function extractNpmWorkspaces(value) {
|
|
130
|
+
if (Array.isArray(value))
|
|
131
|
+
return value;
|
|
132
|
+
if (value && typeof value === 'object' && Array.isArray(value.packages)) {
|
|
133
|
+
return value.packages;
|
|
134
|
+
}
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Discover workspace `package.json` paths from the supported monorepo
|
|
139
|
+
* manifest sources. Returns the list of absolute paths plus which
|
|
140
|
+
* detection source won. When no source is present, returns an empty
|
|
141
|
+
* list with `source: null` (caller should fall through to single-package
|
|
142
|
+
* behavior).
|
|
143
|
+
*/
|
|
144
|
+
async function discoverWorkspacePackageJsons(projectRoot, warnings) {
|
|
145
|
+
// 1. pnpm-workspace.yaml
|
|
146
|
+
const pnpmWsPath = join(projectRoot, 'pnpm-workspace.yaml');
|
|
147
|
+
if (await pathExists(pnpmWsPath)) {
|
|
148
|
+
try {
|
|
149
|
+
const yaml = await readText(pnpmWsPath);
|
|
150
|
+
const globs = parsePnpmWorkspaceYaml(yaml);
|
|
151
|
+
if (globs && globs.length > 0) {
|
|
152
|
+
const paths = await expandWorkspaceGlobs(projectRoot, globs, warnings);
|
|
153
|
+
return { paths, source: 'pnpm-workspace' };
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
warnings.push(`pnpm-workspace.yaml present but unreadable: ${error.message}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// 2. package.json `workspaces` field (npm or yarn classic).
|
|
161
|
+
const rootPkgPath = join(projectRoot, 'package.json');
|
|
162
|
+
if (await pathExists(rootPkgPath)) {
|
|
163
|
+
const { record } = await readPackageJson(rootPkgPath);
|
|
164
|
+
if (record) {
|
|
165
|
+
const globs = extractNpmWorkspaces(record.workspaces);
|
|
166
|
+
if (globs && globs.length > 0) {
|
|
167
|
+
const paths = await expandWorkspaceGlobs(projectRoot, globs, warnings);
|
|
168
|
+
return { paths, source: 'package-json-workspaces' };
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
// 3. lerna.json `packages` field.
|
|
173
|
+
const lernaPath = join(projectRoot, 'lerna.json');
|
|
174
|
+
if (await pathExists(lernaPath)) {
|
|
175
|
+
try {
|
|
176
|
+
const lernaRaw = await readText(lernaPath);
|
|
177
|
+
const lerna = JSON.parse(lernaRaw);
|
|
178
|
+
if (Array.isArray(lerna.packages) && lerna.packages.every((p) => typeof p === 'string')) {
|
|
179
|
+
const paths = await expandWorkspaceGlobs(projectRoot, lerna.packages, warnings);
|
|
180
|
+
return { paths, source: 'lerna' };
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
catch (error) {
|
|
184
|
+
warnings.push(`lerna.json present but unreadable: ${error.message}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return { paths: [], source: null };
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Resolve a list of workspace globs against the project tree.
|
|
191
|
+
*
|
|
192
|
+
* Supports shapes with up to two segments of `*` wildcards:
|
|
193
|
+
* - `packages/*`
|
|
194
|
+
* - `packages/hermes-agent/*`
|
|
195
|
+
* - `apps/web` (literal, no wildcard)
|
|
196
|
+
*
|
|
197
|
+
* Deeper patterns (more than one `*` segment) are silently skipped and a
|
|
198
|
+
* warning is emitted. This is by design (no new dependency, narrow scope).
|
|
199
|
+
*/
|
|
200
|
+
async function expandWorkspaceGlobs(projectRoot, globs, warnings) {
|
|
201
|
+
const found = new Set();
|
|
202
|
+
for (const glob of globs) {
|
|
203
|
+
const segments = glob.split('/').filter((s) => s.length > 0);
|
|
204
|
+
const wildcardCount = segments.filter((s) => s === '*').length;
|
|
205
|
+
if (wildcardCount > 1) {
|
|
206
|
+
warnings.push(`workspace glob "${glob}" has more than one wildcard; skipped (unsupported).`);
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
if (segments.length === 0)
|
|
210
|
+
continue;
|
|
211
|
+
if (wildcardCount === 0) {
|
|
212
|
+
// Literal path — treat the last segment as a directory containing package.json.
|
|
213
|
+
const literalDir = join(projectRoot, ...segments);
|
|
214
|
+
if (await isDirectory(literalDir)) {
|
|
215
|
+
const candidate = join(literalDir, 'package.json');
|
|
216
|
+
if (await pathExists(candidate)) {
|
|
217
|
+
found.add(candidate);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
// Exactly one wildcard — it must be the LAST segment.
|
|
223
|
+
const wildcardIdx = segments.indexOf('*');
|
|
224
|
+
if (wildcardIdx !== segments.length - 1) {
|
|
225
|
+
warnings.push(`workspace glob "${glob}" has non-trailing wildcard; skipped.`);
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
const parentSegments = segments.slice(0, wildcardIdx);
|
|
229
|
+
const parentDir = join(projectRoot, ...parentSegments);
|
|
230
|
+
if (!(await isDirectory(parentDir)))
|
|
231
|
+
continue;
|
|
232
|
+
let entries;
|
|
233
|
+
try {
|
|
234
|
+
entries = await readdir(parentDir, { withFileTypes: true });
|
|
235
|
+
}
|
|
236
|
+
catch (error) {
|
|
237
|
+
warnings.push(`workspace glob "${glob}": could not read ${parentDir}: ${error.message}`);
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
for (const entry of entries) {
|
|
241
|
+
if (!entry.isDirectory())
|
|
242
|
+
continue;
|
|
243
|
+
const childPkg = join(parentDir, entry.name, 'package.json');
|
|
244
|
+
if (await pathExists(childPkg)) {
|
|
245
|
+
found.add(childPkg);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return Array.from(found).sort();
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Parse a single `package.json` record into `LibraryEntry` rows.
|
|
253
|
+
* Returns the entries and the per-scope tallies for that one package.
|
|
254
|
+
*/
|
|
255
|
+
function extractEntriesFromPackageJson(record) {
|
|
256
|
+
const byScope = {
|
|
257
|
+
dependencies: 0,
|
|
258
|
+
devDependencies: 0,
|
|
259
|
+
peerDependencies: 0,
|
|
260
|
+
optionalDependencies: 0
|
|
261
|
+
};
|
|
262
|
+
const entries = [];
|
|
263
|
+
for (const scope of SCOPES) {
|
|
264
|
+
const deps = record[scope];
|
|
265
|
+
if (!deps)
|
|
266
|
+
continue;
|
|
267
|
+
for (const [name, version] of Object.entries(deps)) {
|
|
268
|
+
entries.push({
|
|
269
|
+
name,
|
|
270
|
+
version,
|
|
271
|
+
major: parseMajorVersion(version),
|
|
272
|
+
scope,
|
|
273
|
+
ecosystem: 'npm'
|
|
274
|
+
});
|
|
275
|
+
byScope[scope] += 1;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return { entries, byScope };
|
|
279
|
+
}
|
|
280
|
+
export async function scanLibraries(options) {
|
|
281
|
+
const { projectRoot } = options;
|
|
282
|
+
const warnings = [];
|
|
283
|
+
const libraries = [];
|
|
284
|
+
const byScope = {
|
|
285
|
+
dependencies: 0,
|
|
286
|
+
devDependencies: 0,
|
|
287
|
+
peerDependencies: 0,
|
|
288
|
+
optionalDependencies: 0
|
|
289
|
+
};
|
|
290
|
+
const workspaces = [];
|
|
291
|
+
// 1. Discover monorepo workspaces (if any). Discovery is independent of
|
|
292
|
+
// whether the root `package.json` exists; we still attempt it.
|
|
293
|
+
const { paths: workspacePkgPaths, source } = await discoverWorkspacePackageJsons(projectRoot, warnings);
|
|
294
|
+
const isMonorepo = source !== null && workspacePkgPaths.length > 0;
|
|
295
|
+
if (isMonorepo) {
|
|
296
|
+
// Monorepo mode: scan the root + every discovered workspace package.json.
|
|
297
|
+
const rootPkgPath = join(projectRoot, 'package.json');
|
|
298
|
+
const allPaths = [];
|
|
299
|
+
if (await pathExists(rootPkgPath)) {
|
|
300
|
+
allPaths.push(rootPkgPath);
|
|
301
|
+
}
|
|
302
|
+
// Dedupe: the root might also be matched by a glob (rare, but possible).
|
|
303
|
+
for (const p of workspacePkgPaths) {
|
|
304
|
+
if (!allPaths.includes(p))
|
|
305
|
+
allPaths.push(p);
|
|
306
|
+
}
|
|
307
|
+
// Recursive descent: for each discovered workspace dir, also pick up
|
|
308
|
+
// any nested `package.json` one level deeper. This matches the pnpm
|
|
309
|
+
// convention where a workspace package (e.g. `hermes-agent`) can be
|
|
310
|
+
// a container for sub-workspaces (e.g. `hermes-agent/ui-tui`). The
|
|
311
|
+
// outer pnpm-workspace.yaml often only declares the top-level glob,
|
|
312
|
+
// so a literal-glob-only scan would miss the nested packages.
|
|
313
|
+
const expanded = new Set(allPaths);
|
|
314
|
+
for (const pkgPath of allPaths) {
|
|
315
|
+
const dir = pkgPath.replace(/[\\/]package\.json$/, '');
|
|
316
|
+
if (!(await isDirectory(dir)))
|
|
317
|
+
continue;
|
|
318
|
+
let entries;
|
|
319
|
+
try {
|
|
320
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
321
|
+
}
|
|
322
|
+
catch {
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
for (const entry of entries) {
|
|
326
|
+
if (!entry.isDirectory())
|
|
327
|
+
continue;
|
|
328
|
+
const nestedPkg = join(dir, entry.name, 'package.json');
|
|
329
|
+
if (await pathExists(nestedPkg)) {
|
|
330
|
+
expanded.add(nestedPkg);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
allPaths.length = 0;
|
|
335
|
+
allPaths.push(...Array.from(expanded).sort());
|
|
336
|
+
for (const pkgPath of allPaths) {
|
|
337
|
+
const { exists, record, error } = await readPackageJson(pkgPath);
|
|
338
|
+
if (!exists)
|
|
339
|
+
continue;
|
|
340
|
+
if (record === null) {
|
|
341
|
+
warnings.push(`${normalizePathForDisplay(pkgPath)} is not valid JSON: ${error ?? 'unknown parse error'}`);
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
const { entries, byScope: pkgByScope } = extractEntriesFromPackageJson(record);
|
|
345
|
+
libraries.push(...entries);
|
|
346
|
+
for (const scope of SCOPES) {
|
|
347
|
+
byScope[scope] += pkgByScope[scope];
|
|
348
|
+
}
|
|
349
|
+
// For per-workspace `workspaces[]`, only list non-root paths; the
|
|
350
|
+
// root is implicit and lives at `projectRoot`.
|
|
351
|
+
const isRoot = pkgPath === rootPkgPath;
|
|
352
|
+
if (!isRoot) {
|
|
353
|
+
const entry = {
|
|
354
|
+
path: pkgPath,
|
|
355
|
+
count: entries.length
|
|
356
|
+
};
|
|
357
|
+
if (record.name !== undefined)
|
|
358
|
+
entry.name = record.name;
|
|
359
|
+
if (record.version !== undefined)
|
|
360
|
+
entry.version = record.version;
|
|
361
|
+
workspaces.push(entry);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
else {
|
|
366
|
+
// Single-package mode (today's behavior). Preserved byte-for-byte
|
|
367
|
+
// apart from the new additive `workspaces: []` field.
|
|
368
|
+
const { exists, record, error } = await readPackageJson(join(projectRoot, 'package.json'));
|
|
369
|
+
if (!exists) {
|
|
370
|
+
warnings.push('package.json not found; nothing to scan.');
|
|
371
|
+
return {
|
|
372
|
+
projectRoot,
|
|
373
|
+
libraries,
|
|
374
|
+
totalCount: 0,
|
|
375
|
+
byScope,
|
|
376
|
+
workspaces: [],
|
|
377
|
+
scannedAt: new Date().toISOString(),
|
|
378
|
+
warnings
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
if (record === null) {
|
|
382
|
+
warnings.push(`package.json is not valid JSON: ${error ?? 'unknown parse error'}`);
|
|
383
|
+
return {
|
|
384
|
+
projectRoot,
|
|
385
|
+
libraries,
|
|
386
|
+
totalCount: 0,
|
|
387
|
+
byScope,
|
|
388
|
+
workspaces: [],
|
|
389
|
+
scannedAt: new Date().toISOString(),
|
|
390
|
+
warnings
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
const { entries, byScope: pkgByScope } = extractEntriesFromPackageJson(record);
|
|
394
|
+
libraries.push(...entries);
|
|
395
|
+
for (const scope of SCOPES) {
|
|
396
|
+
byScope[scope] += pkgByScope[scope];
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
// Sort: by name (alphabetical), then by scope order (deps first, then dev, peer, optional)
|
|
400
|
+
libraries.sort((a, b) => {
|
|
401
|
+
if (a.name !== b.name)
|
|
402
|
+
return a.name.localeCompare(b.name);
|
|
403
|
+
return SCOPE_ORDER[a.scope] - SCOPE_ORDER[b.scope];
|
|
404
|
+
});
|
|
405
|
+
return {
|
|
406
|
+
projectRoot,
|
|
407
|
+
libraries,
|
|
408
|
+
totalCount: libraries.length,
|
|
409
|
+
byScope,
|
|
410
|
+
workspaces,
|
|
411
|
+
scannedAt: new Date().toISOString(),
|
|
412
|
+
warnings
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
function normalizePathForDisplay(p) {
|
|
416
|
+
// Surface posix-style separators in warnings for consistency with the
|
|
417
|
+
// existing 'package.json is not valid JSON' message shape.
|
|
418
|
+
return p.split(sep).join('/');
|
|
419
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Library version scan types.
|
|
3
|
+
* Joins the existing scan family (archetype, existing-system, type-sanity,
|
|
4
|
+
* acceptance-coverage, diff-vs-scope, file-size) but is intentionally scoped
|
|
5
|
+
* to library version enumeration only. Does NOT extend scan-types.ts —
|
|
6
|
+
* each scan service ships its own types to keep the scan-types module
|
|
7
|
+
* focused on the archetype + existing-system pair (see src/services/scan/scan-types.ts).
|
|
8
|
+
*/
|
|
9
|
+
export type Ecosystem = 'npm';
|
|
10
|
+
export type DependencyScope = 'dependencies' | 'devDependencies' | 'peerDependencies' | 'optionalDependencies';
|
|
11
|
+
export type LibraryEntry = {
|
|
12
|
+
/** npm package name (e.g. "antd", "@mui/material", "react-router-dom"). */
|
|
13
|
+
name: string;
|
|
14
|
+
/** Raw version spec as written in package.json (e.g. "^5.18.0", "workspace:*", "git+https://..."). */
|
|
15
|
+
version: string;
|
|
16
|
+
/**
|
|
17
|
+
* Parsed major version, or null when the spec is non-semver (e.g.
|
|
18
|
+
* "workspace:*", "file:../local", "git+https://..."). The LLM should
|
|
19
|
+
* treat null as "cannot determine major; consult breaking-changes table
|
|
20
|
+
* by other signals (e.g. lockfile or import statement shape)".
|
|
21
|
+
*/
|
|
22
|
+
major: number | null;
|
|
23
|
+
scope: DependencyScope;
|
|
24
|
+
ecosystem: Ecosystem;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Per-workspace provenance for monorepo scans.
|
|
28
|
+
*
|
|
29
|
+
* `path` is the absolute path of the `package.json` that contributed
|
|
30
|
+
* libraries. `count` is the number of `LibraryEntry` rows produced by
|
|
31
|
+
* reading that single `package.json` (i.e. NOT the aggregate across
|
|
32
|
+
* the whole monorepo — use `LibraryReport.totalCount` for the aggregate).
|
|
33
|
+
*
|
|
34
|
+
* `name` and `version` are the workspace's own `name` / `version` from
|
|
35
|
+
* its `package.json`, when present. They are optional because some
|
|
36
|
+
* workspace `package.json` files omit them.
|
|
37
|
+
*/
|
|
38
|
+
export type WorkspaceEntry = {
|
|
39
|
+
path: string;
|
|
40
|
+
count: number;
|
|
41
|
+
name?: string;
|
|
42
|
+
version?: string;
|
|
43
|
+
};
|
|
44
|
+
export type LibraryReport = {
|
|
45
|
+
projectRoot: string;
|
|
46
|
+
libraries: LibraryEntry[];
|
|
47
|
+
totalCount: number;
|
|
48
|
+
byScope: Record<DependencyScope, number>;
|
|
49
|
+
/**
|
|
50
|
+
* Per-workspace provenance for monorepo (pnpm / npm / yarn workspaces,
|
|
51
|
+
* lerna) projects. Empty for single-package projects so the field is
|
|
52
|
+
* always present (additive; consumers can rely on the shape).
|
|
53
|
+
*/
|
|
54
|
+
workspaces: WorkspaceEntry[];
|
|
55
|
+
/** ISO timestamp at scan time. */
|
|
56
|
+
scannedAt: string;
|
|
57
|
+
/** Soft signals — e.g. "package.json not found" or "package.json is not valid JSON". */
|
|
58
|
+
warnings: string[];
|
|
59
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Library version scan types.
|
|
3
|
+
* Joins the existing scan family (archetype, existing-system, type-sanity,
|
|
4
|
+
* acceptance-coverage, diff-vs-scope, file-size) but is intentionally scoped
|
|
5
|
+
* to library version enumeration only. Does NOT extend scan-types.ts —
|
|
6
|
+
* each scan service ships its own types to keep the scan-types module
|
|
7
|
+
* focused on the archetype + existing-system pair (see src/services/scan/scan-types.ts).
|
|
8
|
+
*/
|
|
9
|
+
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { ensureSession, getSessionId, getCurrentSessionDir, listSessions, getSessionMeta, setSessionMeta, setSessionTitle, listSessionMetas, getProjectScanPath, hasProjectScan, setCurrentSessionBinding, type SessionInfo, type SessionMeta } from './session-manager.js';
|
|
1
|
+
export { ensureSession, getSessionId, getCurrentSessionDir, listSessions, getSessionMeta, setSessionMeta, setSessionTitle, listSessionMetas, getProjectScanPath, hasProjectScan, setCurrentSessionBinding, rotateSessionBinding, type SessionInfo, type SessionMeta } from './session-manager.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { ensureSession, getSessionId, getCurrentSessionDir, listSessions, getSessionMeta, setSessionMeta, setSessionTitle, listSessionMetas, getProjectScanPath, hasProjectScan, setCurrentSessionBinding } from './session-manager.js';
|
|
1
|
+
export { ensureSession, getSessionId, getCurrentSessionDir, listSessions, getSessionMeta, setSessionMeta, setSessionTitle, listSessionMetas, getProjectScanPath, hasProjectScan, setCurrentSessionBinding, rotateSessionBinding } from './session-manager.js';
|
|
@@ -19,7 +19,32 @@ export type SessionMeta = {
|
|
|
19
19
|
createdAt: string;
|
|
20
20
|
lastActivity?: string;
|
|
21
21
|
projectRoot: string;
|
|
22
|
+
/**
|
|
23
|
+
* The outer (harness / IDE / plugin) session id that
|
|
24
|
+
* `ensureSession` was called from. Sourced from
|
|
25
|
+
* `PEAKS_OUTER_SESSION_ID` env var, with `CLAUDE_CODE_SESSION_ID`
|
|
26
|
+
* as a Claude-Code fallback. Stamped once at session creation;
|
|
27
|
+
* later presence writes can compare against this to detect an
|
|
28
|
+
* outer-session swap and AskUserQuestion the user about rolling
|
|
29
|
+
* a new peaks session. Sessions predating the field simply
|
|
30
|
+
* have it undefined; presence-mismatch detection skips those
|
|
31
|
+
* (no false positives on legacy data).
|
|
32
|
+
*/
|
|
33
|
+
outerSessionId?: string;
|
|
22
34
|
};
|
|
35
|
+
/**
|
|
36
|
+
* Drop the project-level session binding (`.peaks/.session.json`)
|
|
37
|
+
* so the next `ensureSession()` call auto-generates a fresh
|
|
38
|
+
* session id. The on-disk session directory is left intact —
|
|
39
|
+
* rotating does NOT delete the user's data, it just unbinds the
|
|
40
|
+
* project from that session.
|
|
41
|
+
*
|
|
42
|
+
* Returns the id of the session that was unbound, or `null` if
|
|
43
|
+
* no binding was present. The caller is expected to do something
|
|
44
|
+
* with that — at minimum surface it in the CLI response so the
|
|
45
|
+
* user can find the directory again if they need to.
|
|
46
|
+
*/
|
|
47
|
+
export declare function rotateSessionBinding(projectRoot: string): string | null;
|
|
23
48
|
/**
|
|
24
49
|
* Bind the project's current session to the given session id by writing
|
|
25
50
|
* `.peaks/.session.json`. The single-session binding is the source of truth
|
|
@@ -50,14 +75,6 @@ export declare function setSessionTitle(projectRoot: string, sessionId: string,
|
|
|
50
75
|
* Returns sessions sorted by sessionId descending (most recent first).
|
|
51
76
|
*/
|
|
52
77
|
export declare function listSessionMetas(projectRoot: string): SessionMeta[];
|
|
53
|
-
/**
|
|
54
|
-
* Get or create the current session for a project.
|
|
55
|
-
* If a valid session already exists, returns it.
|
|
56
|
-
* Otherwise, creates a new session with auto-generated ID.
|
|
57
|
-
*
|
|
58
|
-
* @param projectRoot - Root directory of the project
|
|
59
|
-
* @returns Session ID (e.g., "2026-05-26-session-a3f8b1")
|
|
60
|
-
*/
|
|
61
78
|
export declare function ensureSession(projectRoot: string): Promise<string>;
|
|
62
79
|
/**
|
|
63
80
|
* Get the current session ID without creating a new one.
|
|
@@ -67,6 +84,34 @@ export declare function ensureSession(projectRoot: string): Promise<string>;
|
|
|
67
84
|
* @returns Session ID or null
|
|
68
85
|
*/
|
|
69
86
|
export declare function getSessionId(projectRoot: string): string | null;
|
|
87
|
+
/**
|
|
88
|
+
* Resolve the current session id with canonicalize-on-read
|
|
89
|
+
* semantics. This is the variant the progress subcommands
|
|
90
|
+
* (step / watch / start / close) use, because the legacy
|
|
91
|
+
* `getSessionId` returns null any time the stored
|
|
92
|
+
* `projectRoot` form differs from the caller-passed form
|
|
93
|
+
* (e.g. stored is "." from inside the project dir; caller
|
|
94
|
+
* is the absolute realpath). When `getSessionId` returns
|
|
95
|
+
* null, callers like `ensureSession` create a brand-new
|
|
96
|
+
* session and overwrite the binding — which is what the
|
|
97
|
+
* user observed as the "mid-dogfood rebind" bug.
|
|
98
|
+
*
|
|
99
|
+
* The fix is to canonicalize both sides of the compare
|
|
100
|
+
* (realpath, then optionally resolve relative stored
|
|
101
|
+
* against the caller's project root). The two forms of
|
|
102
|
+
* the same physical directory now compare equal, and the
|
|
103
|
+
* existing binding is found instead of being overwritten.
|
|
104
|
+
*
|
|
105
|
+
* Use this instead of `getSessionId` only when the
|
|
106
|
+
* caller is operating on a user-supplied `--project` flag
|
|
107
|
+
* and the binding may have been written by a CLI invocation
|
|
108
|
+
* that was running from inside the project dir (the common
|
|
109
|
+
* peaks-solo / peaks-sop scenario). Other modules depend
|
|
110
|
+
* on the strict-equality semantics of `getSessionId` (the
|
|
111
|
+
* "no binding" fallback path is part of their contract),
|
|
112
|
+
* so this variant is opt-in.
|
|
113
|
+
*/
|
|
114
|
+
export declare function getSessionIdCanonical(projectRoot: string): string | null;
|
|
70
115
|
/**
|
|
71
116
|
* Get the absolute path to the current session directory.
|
|
72
117
|
* Creates the session if it doesn't exist.
|