peaks-cli 1.4.0 → 1.4.2

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.
Files changed (58) hide show
  1. package/dist/src/cli/commands/core-artifact-commands.js +21 -0
  2. package/dist/src/cli/commands/memory-commands.d.ts +13 -0
  3. package/dist/src/cli/commands/memory-commands.js +60 -0
  4. package/dist/src/cli/commands/migrate-1-4-1-command.d.ts +11 -0
  5. package/dist/src/cli/commands/migrate-1-4-1-command.js +34 -0
  6. package/dist/src/cli/commands/retrospective-commands.d.ts +9 -0
  7. package/dist/src/cli/commands/retrospective-commands.js +58 -0
  8. package/dist/src/cli/commands/workspace-commands.js +8 -0
  9. package/dist/src/cli/program.js +16 -22
  10. package/dist/src/services/fuzzy-matching/fuzzy-match-service.d.ts +15 -0
  11. package/dist/src/services/fuzzy-matching/fuzzy-match-service.js +56 -0
  12. package/dist/src/services/fuzzy-matching/types.d.ts +20 -0
  13. package/dist/src/services/fuzzy-matching/types.js +1 -0
  14. package/dist/src/services/memory/memory-search-service.d.ts +61 -0
  15. package/dist/src/services/memory/memory-search-service.js +80 -0
  16. package/dist/src/services/recommendations/capability-seed-items.js +0 -1
  17. package/dist/src/services/recommendations/capability-seed-mappings.js +0 -1
  18. package/dist/src/services/recommendations/capability-seed-sources.js +0 -1
  19. package/dist/src/services/retrospective/retrospective-search-service.d.ts +37 -0
  20. package/dist/src/services/retrospective/retrospective-search-service.js +75 -0
  21. package/dist/src/services/standards/project-context.d.ts +1 -1
  22. package/dist/src/services/standards/project-context.js +0 -4
  23. package/dist/src/services/standards/project-standards-service.js +1 -3
  24. package/dist/src/services/workspace/migrate-1-4-1-service.d.ts +44 -0
  25. package/dist/src/services/workspace/migrate-1-4-1-service.js +195 -0
  26. package/dist/src/shared/version.d.ts +1 -1
  27. package/dist/src/shared/version.js +1 -1
  28. package/package.json +3 -7
  29. package/skills/peaks-solo/SKILL.md +1 -1
  30. package/skills/peaks-solo/references/completion-handoff.md +3 -1
  31. package/dist/src/cli/commands/shadcn-commands.d.ts +0 -3
  32. package/dist/src/cli/commands/shadcn-commands.js +0 -35
  33. package/dist/src/cli/commands/skill-scope-commands.d.ts +0 -49
  34. package/dist/src/cli/commands/skill-scope-commands.js +0 -305
  35. package/dist/src/services/shadcn/shadcn-service.d.ts +0 -27
  36. package/dist/src/services/shadcn/shadcn-service.js +0 -128
  37. package/dist/src/services/skill-scope/adapters/_stub-helper.d.ts +0 -39
  38. package/dist/src/services/skill-scope/adapters/_stub-helper.js +0 -98
  39. package/dist/src/services/skill-scope/adapters/claude-code.d.ts +0 -59
  40. package/dist/src/services/skill-scope/adapters/claude-code.js +0 -304
  41. package/dist/src/services/skill-scope/adapters/codex.d.ts +0 -2
  42. package/dist/src/services/skill-scope/adapters/codex.js +0 -12
  43. package/dist/src/services/skill-scope/adapters/cursor.d.ts +0 -2
  44. package/dist/src/services/skill-scope/adapters/cursor.js +0 -13
  45. package/dist/src/services/skill-scope/adapters/qoder.d.ts +0 -2
  46. package/dist/src/services/skill-scope/adapters/qoder.js +0 -13
  47. package/dist/src/services/skill-scope/adapters/tongyi.d.ts +0 -2
  48. package/dist/src/services/skill-scope/adapters/tongyi.js +0 -13
  49. package/dist/src/services/skill-scope/adapters/trae.d.ts +0 -2
  50. package/dist/src/services/skill-scope/adapters/trae.js +0 -12
  51. package/dist/src/services/skill-scope/detect.d.ts +0 -75
  52. package/dist/src/services/skill-scope/detect.js +0 -480
  53. package/dist/src/services/skill-scope/registry.d.ts +0 -41
  54. package/dist/src/services/skill-scope/registry.js +0 -83
  55. package/dist/src/services/skill-scope/source-of-truth.d.ts +0 -44
  56. package/dist/src/services/skill-scope/source-of-truth.js +0 -118
  57. package/dist/src/services/skill-scope/types.d.ts +0 -176
  58. package/dist/src/services/skill-scope/types.js +0 -74
@@ -1,480 +0,0 @@
1
- /**
2
- * Detection algorithm for `peaks skill scope`.
3
- *
4
- * Pure function: given a project root and the installed-skills path, the
5
- * algorithm produces a `DetectResult` (signals + per-skill classification
6
- * + counts). No filesystem writes, no randomness, no time-of-day. AC11.
7
- *
8
- * Three layers:
9
- * 1. `extractProjectSignals(projectRoot)` — read package.json + tsconfig +
10
- * file tree (top-50 extensions).
11
- * 2. `classifySkill(skill, signals, hardcodedRules)` — keyword matching
12
- * against the skill's SKILL.md description.
13
- * 3. `detectSkillScope({ projectRoot, installedSkillsPath })` — top-level
14
- * orchestrator that returns the JSON envelope (AC1).
15
- */
16
- import { existsSync, readdirSync, statSync } from 'node:fs';
17
- import { readFile } from 'node:fs/promises';
18
- import { join } from 'node:path';
19
- import { ALWAYS_RELEVANT_SKILLS, NON_TS_SKILL_PREFIXES, TRACKED_EXTENSIONS, } from './types.js';
20
- function hasAnyDep(pkg, names) {
21
- const all = { ...(pkg.dependencies ?? {}), ...(pkg.devDependencies ?? {}) };
22
- return names.some((name) => Object.prototype.hasOwnProperty.call(all, name));
23
- }
24
- function parseNodeEngineMajor(enginesNode) {
25
- if (enginesNode === undefined)
26
- return null;
27
- // Match patterns like '>=20', '^20.0.0', '>=20.0.0 <21.0.0'
28
- const match = enginesNode.match(/(\d+)/);
29
- return match === null ? null : Number(match[1]);
30
- }
31
- /**
32
- * Read and parse a JSON file. Returns null on parse error or missing file.
33
- */
34
- async function readJson(path) {
35
- if (!existsSync(path))
36
- return null;
37
- try {
38
- const raw = await readFile(path, 'utf8');
39
- return JSON.parse(raw);
40
- }
41
- catch {
42
- return null;
43
- }
44
- }
45
- function asPackageJson(value) {
46
- if (value === null || typeof value !== 'object')
47
- return null;
48
- return value;
49
- }
50
- function asTsConfig(value) {
51
- if (value === null || typeof value !== 'object')
52
- return null;
53
- return value;
54
- }
55
- /**
56
- * Walk `src/` (recursively) AND the project root, collecting the top-50
57
- * unique file extensions. Sorted lexicographically.
58
- */
59
- export function scanFileTree(projectRoot, maxExtensions = 50) {
60
- const roots = [];
61
- if (existsSync(join(projectRoot, 'src')))
62
- roots.push(join(projectRoot, 'src'));
63
- roots.push(projectRoot);
64
- const exts = new Set();
65
- // Bound the walk: at most 2000 files, 5 levels deep.
66
- let visited = 0;
67
- const MAX_FILES = 2000;
68
- const MAX_DEPTH = 5;
69
- for (const root of roots) {
70
- const stack = [root];
71
- while (stack.length > 0 && visited < MAX_FILES) {
72
- const dir = stack.pop();
73
- if (!existsSync(dir))
74
- continue;
75
- let entries;
76
- try {
77
- entries = readdirSync(dir, { withFileTypes: true, encoding: 'utf8' });
78
- }
79
- catch {
80
- continue;
81
- }
82
- for (const entry of entries) {
83
- const name = entry.name;
84
- if (typeof name !== 'string')
85
- continue;
86
- const full = join(dir, name);
87
- // Skip hidden dirs (e.g. node_modules, .git) and the fixture `skills/` dir.
88
- if (name === 'node_modules' || name === '.git' || name === 'skills' || name === 'dist')
89
- continue;
90
- if (entry.isDirectory()) {
91
- const depth = full.split(/[/\\]/).length - projectRoot.split(/[/\\]/).length;
92
- if (depth < MAX_DEPTH)
93
- stack.push(full);
94
- }
95
- else if (entry.isFile()) {
96
- visited += 1;
97
- if (visited >= MAX_FILES)
98
- break;
99
- const dot = name.lastIndexOf('.');
100
- if (dot < 0)
101
- continue;
102
- const ext = name.slice(dot).toLowerCase();
103
- exts.add(ext);
104
- if (exts.size >= maxExtensions)
105
- break;
106
- }
107
- }
108
- }
109
- }
110
- return [...exts].sort().slice(0, maxExtensions);
111
- }
112
- function hasExt(exts, ext) {
113
- return exts.includes(ext);
114
- }
115
- /**
116
- * Build the `ProjectSignals` object from the project root.
117
- */
118
- export async function extractProjectSignals(projectRoot) {
119
- const pkgRaw = await readJson(join(projectRoot, 'package.json'));
120
- const pkg = asPackageJson(pkgRaw);
121
- const hasPackageJson = pkg !== null;
122
- const isTypeScript = hasPackageJson &&
123
- (hasAnyDep(pkg, ['typescript', 'tsx', '@types/node']) ||
124
- existsSync(join(projectRoot, 'tsconfig.json')));
125
- const tsRaw = await readJson(join(projectRoot, 'tsconfig.json'));
126
- const tsConfig = asTsConfig(tsRaw);
127
- const isTypeScriptESM = (pkg?.type === 'module') ||
128
- (tsConfig?.compilerOptions?.module !== undefined &&
129
- ['ESNext', 'NodeNext', 'ES2022'].includes(tsConfig.compilerOptions.module));
130
- const isReact = pkg !== null && hasAnyDep(pkg, ['react', 'react-dom', 'preact']);
131
- const isVue = pkg !== null && hasAnyDep(pkg, ['vue']);
132
- const isSvelte = pkg !== null && hasAnyDep(pkg, ['svelte']);
133
- const isNext = pkg !== null && hasAnyDep(pkg, ['next']);
134
- const isNestJS = pkg !== null && hasAnyDep(pkg, ['@nestjs/core', '@nestjs/common']);
135
- const isExpress = pkg !== null && hasAnyDep(pkg, ['express']);
136
- const isFastify = pkg !== null && hasAnyDep(pkg, ['fastify']);
137
- const isPostgres = pkg !== null && (hasAnyDep(pkg, ['pg', 'postgres', 'postgresql', 'prisma', '@prisma/client']));
138
- const isMysql = pkg !== null && hasAnyDep(pkg, ['mysql', 'mysql2']);
139
- const isMongo = pkg !== null && hasAnyDep(pkg, ['mongodb', 'mongoose']);
140
- const isRedis = pkg !== null && hasAnyDep(pkg, ['redis', 'ioredis']);
141
- const isDocker = existsSync(join(projectRoot, 'Dockerfile')) ||
142
- existsSync(join(projectRoot, 'docker-compose.yml')) ||
143
- existsSync(join(projectRoot, 'docker-compose.yaml'));
144
- const isK8s = existsSync(join(projectRoot, 'k8s')) ||
145
- existsSync(join(projectRoot, 'kubernetes')) ||
146
- existsSync(join(projectRoot, 'deployment.yaml')) ||
147
- existsSync(join(projectRoot, 'deployment.yml'));
148
- const isCommander = pkg !== null && hasAnyDep(pkg, ['commander']);
149
- // Detect Python projects (requirements.txt, pyproject.toml, setup.py, .py presence).
150
- const isPython = !hasPackageJson ||
151
- existsSync(join(projectRoot, 'requirements.txt')) ||
152
- existsSync(join(projectRoot, 'pyproject.toml')) ||
153
- existsSync(join(projectRoot, 'setup.py'));
154
- const isCodegraph = pkg !== null && hasAnyDep(pkg, ['@colbymchenry/codegraph']);
155
- const isHeadroom = pkg !== null && (hasAnyDep(pkg, ['headroom-ai']) ||
156
- Object.keys({ ...(pkg.dependencies ?? {}), ...(pkg.devDependencies ?? {}) }).some((k) => k.startsWith('@headroom/')));
157
- const nodeEngineMajor = parseNodeEngineMajor(pkg?.engines?.node);
158
- const topExtensions = scanFileTree(projectRoot);
159
- // Build the per-extension presence flag map.
160
- const hasFileExtension = {};
161
- for (const ext of TRACKED_EXTENSIONS) {
162
- hasFileExtension[ext.slice(1)] = hasExt(topExtensions, ext);
163
- }
164
- return {
165
- hasPackageJson,
166
- isTypeScript,
167
- isTypeScriptESM,
168
- isReact,
169
- isVue,
170
- isSvelte,
171
- isNext,
172
- isNestJS,
173
- isExpress,
174
- isFastify,
175
- isPostgres,
176
- isMysql,
177
- isMongo,
178
- isRedis,
179
- isDocker,
180
- isK8s,
181
- isCommander,
182
- isCodegraph,
183
- isHeadroom,
184
- isPython,
185
- nodeEngineMajor,
186
- topExtensions,
187
- hasFileExtension,
188
- };
189
- }
190
- /**
191
- * Infer the `SkillKind` for a skill by name. Used for the JSON envelope.
192
- */
193
- export function inferSkillKind(name, alwaysRelevant) {
194
- if (alwaysRelevant.has(name) && name.startsWith('peaks-'))
195
- return 'peaks-family';
196
- if (alwaysRelevant.has(name))
197
- return 'generic-ai';
198
- if (NON_TS_SKILL_PREFIXES.some((p) => name.startsWith(p)))
199
- return 'language-specific';
200
- return 'other';
201
- }
202
- /**
203
- * Classify a single skill given the project signals. Returns the relevance
204
- * + a list of human-readable reasons (stable for fixtures, so unit tests
205
- * can assert exact strings).
206
- */
207
- export function classifySkill(skill, signals, rules) {
208
- const reasons = [];
209
- // 1. Hard-coded allowlist always wins.
210
- if (rules.alwaysRelevant.has(skill.name)) {
211
- reasons.push('hard-coded always-relevant');
212
- return {
213
- name: skill.name,
214
- kind: inferSkillKind(skill.name, rules.alwaysRelevant),
215
- relevance: 'relevant',
216
- reasons,
217
- };
218
- }
219
- // 2. Non-TS prefix → irrelevant when the project is TS.
220
- if (rules.nonTsPrefixes.some((prefix) => skill.name.startsWith(prefix))) {
221
- if (signals.isTypeScript && !isNonTsProject(signals)) {
222
- reasons.push('non-TS skill prefix; project is TS');
223
- return {
224
- name: skill.name,
225
- kind: 'language-specific',
226
- relevance: 'irrelevant',
227
- reasons,
228
- };
229
- }
230
- }
231
- // 3. Keyword matching against the description (strong + weak hits).
232
- const desc = skill.description.toLowerCase();
233
- // Special-case: when the project is a non-TS project (Python, etc.),
234
- // language-specific skills with matching keywords should be relevant.
235
- if (isNonTsProject(signals)) {
236
- const langMatch = languageKeywordMatch(desc);
237
- if (langMatch !== null) {
238
- reasons.push(`${langMatch} keyword + non-TS project`);
239
- return {
240
- name: skill.name,
241
- kind: inferSkillKind(skill.name, rules.alwaysRelevant),
242
- relevance: 'relevant',
243
- reasons,
244
- };
245
- }
246
- }
247
- const strong = strongMatches(desc, signals);
248
- const weak = weakMatches(desc, signals);
249
- if (strong.length > 0) {
250
- reasons.push(...strong);
251
- return {
252
- name: skill.name,
253
- kind: inferSkillKind(skill.name, rules.alwaysRelevant),
254
- relevance: 'relevant',
255
- reasons,
256
- };
257
- }
258
- if (weak.length > 0) {
259
- reasons.push(...weak);
260
- return {
261
- name: skill.name,
262
- kind: inferSkillKind(skill.name, rules.alwaysRelevant),
263
- relevance: 'borderline',
264
- reasons,
265
- };
266
- }
267
- return {
268
- name: skill.name,
269
- kind: inferSkillKind(skill.name, rules.alwaysRelevant),
270
- relevance: 'irrelevant',
271
- reasons: ['no project-signal match'],
272
- };
273
- }
274
- /**
275
- * Strong matches: keyword in description that maps to a confirmed project signal.
276
- */
277
- function strongMatches(description, signals) {
278
- const matches = [];
279
- if (signals.isReact && /\breact\b/.test(description))
280
- matches.push('react project + react skill');
281
- if (signals.isVue && /\bvue\b/.test(description))
282
- matches.push('vue project + vue skill');
283
- if (signals.isSvelte && /\bsvelte\b/.test(description))
284
- matches.push('svelte project + svelte skill');
285
- if (signals.isNext && /\bnext\.?js\b|\bnextjs\b/.test(description))
286
- matches.push('next project + nextjs skill');
287
- if (signals.isNestJS && /\bnest\.?js\b|\bnestjs\b/.test(description))
288
- matches.push('nestjs project + nestjs skill');
289
- if (signals.isExpress && /\bexpress\b/.test(description))
290
- matches.push('express project + express skill');
291
- if (signals.isFastify && /\bfastify\b/.test(description))
292
- matches.push('fastify project + fastify skill');
293
- if (signals.isPostgres && /\bpostgres|\bpostgresql\b/.test(description))
294
- matches.push('postgres project + postgres skill');
295
- if (signals.isMysql && /\bmysql\b/.test(description))
296
- matches.push('mysql project + mysql skill');
297
- if (signals.isMongo && /\bmongo(?:db)?\b/.test(description))
298
- matches.push('mongo project + mongo skill');
299
- if (signals.isRedis && /\bredis\b/.test(description))
300
- matches.push('redis project + redis skill');
301
- if (signals.isDocker && /\bdocker\b/.test(description))
302
- matches.push('docker project + docker skill');
303
- if (signals.isK8s && /\bkubernetes\b|\bk8s\b/.test(description))
304
- matches.push('k8s project + k8s skill');
305
- if (signals.isCommander && /\bcommander\b|\bcli\b/.test(description))
306
- matches.push('cli project + cli skill');
307
- if (/\btdd\b|\btest-driven\b/.test(description))
308
- matches.push('tdd keyword (always relevant)');
309
- if (/\brefactor\b/.test(description))
310
- matches.push('refactor keyword (always relevant)');
311
- return matches;
312
- }
313
- /**
314
- * Weak matches: keyword that's a hint but not a confirmed signal.
315
- */
316
- function weakMatches(description, signals) {
317
- const matches = [];
318
- if (/\bfrontend\b/.test(description) &&
319
- (signals.isReact || signals.isVue || signals.isSvelte || signals.isNext)) {
320
- matches.push('frontend keyword + frontend project');
321
- }
322
- if (/\bbackend\b/.test(description) &&
323
- (signals.isNestJS || signals.isExpress || signals.isFastify)) {
324
- matches.push('backend keyword + backend project');
325
- }
326
- if (/\bdatabase\b/.test(description) &&
327
- (signals.isPostgres || signals.isMysql || signals.isMongo || signals.isRedis)) {
328
- matches.push('database keyword + db project');
329
- }
330
- return matches;
331
- }
332
- /**
333
- * Map a skill description to a non-TS language when the description
334
- * explicitly mentions that language. Returns null when there's no match.
335
- */
336
- function languageKeywordMatch(description) {
337
- if (/\bpython\b/.test(description))
338
- return 'python';
339
- if (/\bkotlin\b/.test(description))
340
- return 'kotlin';
341
- if (/\bjava\b/.test(description))
342
- return 'java';
343
- if (/\brust\b/.test(description))
344
- return 'rust';
345
- if (/\bgo\b|\bgolang\b/.test(description))
346
- return 'go';
347
- if (/\bruby\b/.test(description))
348
- return 'ruby';
349
- if (/\bswift\b/.test(description))
350
- return 'swift';
351
- if (/\bc#\b|\bcsharp\b/.test(description))
352
- return 'csharp';
353
- if (/\bc\+\+|\bcpp\b/.test(description))
354
- return 'cpp';
355
- return null;
356
- }
357
- /**
358
- * Multi-language project heuristic: if the project's file tree contains
359
- * extensions matching a non-TS language, OR the project is a Python project,
360
- * treat it as a non-TS project and let the language-specific skills be relevant.
361
- */
362
- function isNonTsProject(signals) {
363
- if (signals.isPython)
364
- return true;
365
- const nonTsExts = ['swift', 'kt', 'kts', 'java', 'scala', 'py', 'pyx', 'go', 'rs', 'rb', 'cs'];
366
- return nonTsExts.some((ext) => signals.hasFileExtension[ext] === true);
367
- }
368
- /**
369
- * Discover the installed skills under `installedSkillsPath` (default:
370
- * `~/.claude/skills`). Each subdir containing a SKILL.md counts as an
371
- * installed skill.
372
- */
373
- export async function listInstalledSkills(installedSkillsPath) {
374
- if (!existsSync(installedSkillsPath))
375
- return [];
376
- let entries;
377
- try {
378
- entries = readdirSync(installedSkillsPath, { withFileTypes: true, encoding: 'utf8' });
379
- }
380
- catch {
381
- return [];
382
- }
383
- const skills = [];
384
- for (const entry of entries) {
385
- if (!entry.isDirectory())
386
- continue;
387
- const name = entry.name;
388
- if (typeof name !== 'string')
389
- continue;
390
- const skillPath = join(installedSkillsPath, name, 'SKILL.md');
391
- if (!existsSync(skillPath))
392
- continue;
393
- try {
394
- const raw = await readFile(skillPath, 'utf8');
395
- const frontmatter = parseFrontmatterLoose(raw);
396
- skills.push({
397
- name: frontmatter.name ?? name,
398
- description: frontmatter.description ?? '',
399
- skillPath,
400
- });
401
- }
402
- catch {
403
- skills.push({ name, description: '', skillPath });
404
- }
405
- }
406
- skills.sort((a, b) => a.name.localeCompare(b.name));
407
- return skills;
408
- }
409
- /**
410
- * Lightweight YAML frontmatter parser (good enough for `name` + `description`).
411
- * Falls back to regex when the file is malformed.
412
- */
413
- function parseFrontmatterLoose(content) {
414
- const lines = content.split(/\r?\n/);
415
- if (lines[0] !== '---')
416
- return {};
417
- const end = lines.findIndex((line, index) => index > 0 && line === '---');
418
- if (end === -1)
419
- return {};
420
- const out = {};
421
- for (let i = 1; i < end; i += 1) {
422
- const line = lines[i];
423
- if (line === undefined)
424
- continue;
425
- const match = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
426
- if (match === null || match[1] === undefined)
427
- continue;
428
- out[match[1]] = (match[2] ?? '').replace(/^['"]|['"]$/g, '').trim();
429
- }
430
- return out;
431
- }
432
- /**
433
- * The default installed-skills path: `~/.claude/skills`. Resolved at call
434
- * time so the orchestrator stays pure-ish (no module-level side effects).
435
- */
436
- export function defaultInstalledSkillsPath() {
437
- const home = process.env.HOME ?? process.env.USERPROFILE ?? '';
438
- return join(home, '.claude', 'skills');
439
- }
440
- const ALWAYS_RELEVANT_SET = new Set(ALWAYS_RELEVANT_SKILLS);
441
- /**
442
- * Top-level orchestrator. Reads package.json + tsconfig + file tree,
443
- * discovers installed skills, classifies each one, returns the JSON
444
- * envelope. Idempotent: same input → same output. No filesystem writes.
445
- */
446
- export async function detectSkillScope(input) {
447
- const projectRoot = input.projectRoot;
448
- const skillsPath = input.installedSkillsPath ?? defaultInstalledSkillsPath();
449
- const signals = await extractProjectSignals(projectRoot);
450
- const installed = await listInstalledSkills(skillsPath);
451
- const rules = {
452
- alwaysRelevant: ALWAYS_RELEVANT_SET,
453
- nonTsPrefixes: NON_TS_SKILL_PREFIXES,
454
- };
455
- const skills = installed.map((skill) => classifySkill(skill, signals, rules));
456
- const counts = {
457
- relevant: skills.filter((s) => s.relevance === 'relevant').length,
458
- borderline: skills.filter((s) => s.relevance === 'borderline').length,
459
- irrelevant: skills.filter((s) => s.relevance === 'irrelevant').length,
460
- };
461
- return {
462
- detectedIde: input.detectedIde ?? null,
463
- projectSignals: signals,
464
- skills,
465
- counts,
466
- };
467
- }
468
- // ---------------------------------------------------------------------------
469
- // Idempotency guard helper for tests
470
- // ---------------------------------------------------------------------------
471
- /** Compute a stable summary hash (used by tests to assert no time-dependent fields). */
472
- export function detectSummary(result) {
473
- const sorted = [...result.skills].sort((a, b) => a.name.localeCompare(b.name));
474
- return JSON.stringify({
475
- counts: result.counts,
476
- skills: sorted.map((s) => ({ name: s.name, relevance: s.relevance })),
477
- });
478
- }
479
- // Quiet the "unused" warning on statSync when used only in tests paths
480
- void statSync;
@@ -1,41 +0,0 @@
1
- /**
2
- * `peaks skill scope` — adapter registry.
3
- *
4
- * The registry owns the map from IdeId → SkillScopeAdapter. It exposes two
5
- * functions:
6
- * - `getScopeAdapter(ide)` — direct lookup. Throws on unknown ide.
7
- * - `resolveActiveAdapter(projectRoot)` — discover the best adapter by
8
- * probing every registered adapter's `detect(projectRoot)`. Falls back
9
- * to Claude Code with a synthetic score of 0.5 when no adapter scores
10
- * ≥ 0.5 (R3: "Claude Code shipped, Trae in progress" per package.json).
11
- *
12
- * See tech-doc-025 §7 for the discovery flow + fallback semantics.
13
- */
14
- import type { IdeId } from '../ide/ide-types.js';
15
- import type { SkillScopeAdapter } from './types.js';
16
- /** Get the adapter for a given IDE id. Throws on unsupported IDE. */
17
- export declare function getScopeAdapter(ide: IdeId): SkillScopeAdapter;
18
- /** All registered adapter ids (insertion order). */
19
- export declare function listScopeAdapterIds(): readonly IdeId[];
20
- /** All registered adapters (insertion order). */
21
- export declare function listScopeAdapters(): readonly SkillScopeAdapter[];
22
- export interface ResolvedAdapter {
23
- readonly adapter: SkillScopeAdapter;
24
- readonly score: number;
25
- /** True when the score is synthetic (no real adapter hit ≥ 0.5). */
26
- readonly isFallback: boolean;
27
- }
28
- /**
29
- * Discover the active adapter for a project root. Returns the highest-
30
- * scoring adapter; if all adapters score < 0.5, falls back to the Claude
31
- * Code adapter with a synthetic score of 0.5 (R3). Stubs (Trae, Cursor,
32
- * Codex, Qoder, Tongyi) return 0.0 from `detect()` so they never win.
33
- */
34
- export declare function resolveActiveAdapter(projectRoot: string): Promise<ResolvedAdapter>;
35
- /**
36
- * Test seam: replace the registry (used by stub-adapter tests to inject
37
- * a fresh adapter for an IDE without restarting the module).
38
- */
39
- export declare function _setScopeAdapterForTesting(ide: IdeId, adapter: SkillScopeAdapter): void;
40
- /** Test seam: reset to built-in defaults. */
41
- export declare function _resetScopeAdaptersForTesting(): void;
@@ -1,83 +0,0 @@
1
- /**
2
- * `peaks skill scope` — adapter registry.
3
- *
4
- * The registry owns the map from IdeId → SkillScopeAdapter. It exposes two
5
- * functions:
6
- * - `getScopeAdapter(ide)` — direct lookup. Throws on unknown ide.
7
- * - `resolveActiveAdapter(projectRoot)` — discover the best adapter by
8
- * probing every registered adapter's `detect(projectRoot)`. Falls back
9
- * to Claude Code with a synthetic score of 0.5 when no adapter scores
10
- * ≥ 0.5 (R3: "Claude Code shipped, Trae in progress" per package.json).
11
- *
12
- * See tech-doc-025 §7 for the discovery flow + fallback semantics.
13
- */
14
- import { CLAUDE_CODE_SKILL_SCOPE } from './adapters/claude-code.js';
15
- import { CODEX_SKILL_SCOPE } from './adapters/codex.js';
16
- import { CURSOR_SKILL_SCOPE } from './adapters/cursor.js';
17
- import { QODER_SKILL_SCOPE } from './adapters/qoder.js';
18
- import { TONGYI_SKILL_SCOPE } from './adapters/tongyi.js';
19
- import { TRAE_SKILL_SCOPE } from './adapters/trae.js';
20
- /**
21
- * Insertion order: Claude Code first (shipped), then Trae (in progress),
22
- * then the four roadmap IDEs. The CLI's `--ide <name>` overrides this map.
23
- */
24
- const SCOPE_ADAPTERS = new Map([
25
- ['claude-code', CLAUDE_CODE_SKILL_SCOPE],
26
- ['trae', TRAE_SKILL_SCOPE],
27
- ['codex', CODEX_SKILL_SCOPE],
28
- ['cursor', CURSOR_SKILL_SCOPE],
29
- ['qoder', QODER_SKILL_SCOPE],
30
- ['tongyi-lingma', TONGYI_SKILL_SCOPE],
31
- ]);
32
- /** Get the adapter for a given IDE id. Throws on unsupported IDE. */
33
- export function getScopeAdapter(ide) {
34
- const adapter = SCOPE_ADAPTERS.get(ide);
35
- if (adapter === undefined) {
36
- throw new Error(`No SkillScopeAdapter for IDE: ${ide}. Registered: ${listScopeAdapterIds().join(', ') || '(none)'}`);
37
- }
38
- return adapter;
39
- }
40
- /** All registered adapter ids (insertion order). */
41
- export function listScopeAdapterIds() {
42
- return Array.from(SCOPE_ADAPTERS.keys());
43
- }
44
- /** All registered adapters (insertion order). */
45
- export function listScopeAdapters() {
46
- return Array.from(SCOPE_ADAPTERS.values());
47
- }
48
- /**
49
- * Discover the active adapter for a project root. Returns the highest-
50
- * scoring adapter; if all adapters score < 0.5, falls back to the Claude
51
- * Code adapter with a synthetic score of 0.5 (R3). Stubs (Trae, Cursor,
52
- * Codex, Qoder, Tongyi) return 0.0 from `detect()` so they never win.
53
- */
54
- export async function resolveActiveAdapter(projectRoot) {
55
- let best = null;
56
- for (const adapter of SCOPE_ADAPTERS.values()) {
57
- const score = await adapter.detect(projectRoot);
58
- if (best === null || score > best.score) {
59
- best = { adapter, score };
60
- }
61
- }
62
- if (best === null || best.score < 0.5) {
63
- return { adapter: CLAUDE_CODE_SKILL_SCOPE, score: 0.5, isFallback: true };
64
- }
65
- return { adapter: best.adapter, score: best.score, isFallback: false };
66
- }
67
- /**
68
- * Test seam: replace the registry (used by stub-adapter tests to inject
69
- * a fresh adapter for an IDE without restarting the module).
70
- */
71
- export function _setScopeAdapterForTesting(ide, adapter) {
72
- SCOPE_ADAPTERS.set(ide, adapter);
73
- }
74
- /** Test seam: reset to built-in defaults. */
75
- export function _resetScopeAdaptersForTesting() {
76
- SCOPE_ADAPTERS.clear();
77
- SCOPE_ADAPTERS.set('claude-code', CLAUDE_CODE_SKILL_SCOPE);
78
- SCOPE_ADAPTERS.set('trae', TRAE_SKILL_SCOPE);
79
- SCOPE_ADAPTERS.set('codex', CODEX_SKILL_SCOPE);
80
- SCOPE_ADAPTERS.set('cursor', CURSOR_SKILL_SCOPE);
81
- SCOPE_ADAPTERS.set('qoder', QODER_SKILL_SCOPE);
82
- SCOPE_ADAPTERS.set('tongyi-lingma', TONGYI_SKILL_SCOPE);
83
- }
@@ -1,44 +0,0 @@
1
- /**
2
- * Source-of-truth helpers for `peaks skill scope`.
3
- *
4
- * The source-of-truth file `.peaks/scope/skills.json` is the canonical
5
- * record of the user's scope intent. Adapters translate it to their
6
- * IDE-native config; the CLI always reads back from this file on `--show`.
7
- *
8
- * Atomicity: every write goes through `.peaks-tmp` first, then `rename`
9
- * (POSIX-atomic; on Windows `rename` is atomic for files on the same volume).
10
- * See tech-doc-025 §3.1.
11
- */
12
- import type { ScopeConfig } from './types.js';
13
- /** File name for the canonical source-of-truth. */
14
- export declare const SCOPE_FILE_NAME = "skills.json";
15
- /** Resolve the canonical source-of-truth path for a project root. */
16
- export declare function scopeFilePath(projectRoot: string): string;
17
- /** Resolve the per-IDE companion file path (kebab-case). */
18
- export declare function ideCompanionFilePath(projectRoot: string, ide: string): string;
19
- /** Resolve the `.peaks/scope/` directory for a project root. */
20
- export declare function scopeDir(projectRoot: string): string;
21
- /**
22
- * Read the source-of-truth scope config, or null if it does not exist.
23
- * Returns null on parse error too — the caller decides whether to surface.
24
- */
25
- export declare function readSourceOfTruth(projectRoot: string): Promise<ScopeConfig | null>;
26
- /**
27
- * Read the per-IDE companion file (`.peaks/scope/<ide>-skills.json`), or
28
- * null if it does not exist or is unparseable.
29
- */
30
- export declare function readIdeCompanion(projectRoot: string, ide: string): Promise<unknown>;
31
- /**
32
- * Write the canonical source-of-truth atomically. The `.peaks-tmp` file
33
- * is cleaned up in a finally block on partial failure.
34
- */
35
- export declare function writeSourceOfTruth(projectRoot: string, config: ScopeConfig): Promise<string>;
36
- /**
37
- * Write a generic JSON document atomically. Used by stub adapters for
38
- * their `<ide>-skills.json` companion file (G3 §4.1).
39
- */
40
- export declare function writeJsonAtomic(file: string, data: unknown): Promise<void>;
41
- /**
42
- * Remove a file if it exists. Returns true if it was removed.
43
- */
44
- export declare function removeIfExists(file: string): Promise<boolean>;