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.
- package/dist/src/cli/commands/core-artifact-commands.js +21 -0
- package/dist/src/cli/commands/memory-commands.d.ts +13 -0
- package/dist/src/cli/commands/memory-commands.js +60 -0
- package/dist/src/cli/commands/migrate-1-4-1-command.d.ts +11 -0
- package/dist/src/cli/commands/migrate-1-4-1-command.js +34 -0
- package/dist/src/cli/commands/retrospective-commands.d.ts +9 -0
- package/dist/src/cli/commands/retrospective-commands.js +58 -0
- package/dist/src/cli/commands/workspace-commands.js +8 -0
- package/dist/src/cli/program.js +16 -22
- package/dist/src/services/fuzzy-matching/fuzzy-match-service.d.ts +15 -0
- package/dist/src/services/fuzzy-matching/fuzzy-match-service.js +56 -0
- package/dist/src/services/fuzzy-matching/types.d.ts +20 -0
- package/dist/src/services/fuzzy-matching/types.js +1 -0
- package/dist/src/services/memory/memory-search-service.d.ts +61 -0
- package/dist/src/services/memory/memory-search-service.js +80 -0
- package/dist/src/services/recommendations/capability-seed-items.js +0 -1
- package/dist/src/services/recommendations/capability-seed-mappings.js +0 -1
- package/dist/src/services/recommendations/capability-seed-sources.js +0 -1
- package/dist/src/services/retrospective/retrospective-search-service.d.ts +37 -0
- package/dist/src/services/retrospective/retrospective-search-service.js +75 -0
- package/dist/src/services/standards/project-context.d.ts +1 -1
- package/dist/src/services/standards/project-context.js +0 -4
- package/dist/src/services/standards/project-standards-service.js +1 -3
- package/dist/src/services/workspace/migrate-1-4-1-service.d.ts +44 -0
- package/dist/src/services/workspace/migrate-1-4-1-service.js +195 -0
- package/dist/src/shared/version.d.ts +1 -1
- package/dist/src/shared/version.js +1 -1
- package/package.json +3 -7
- package/skills/peaks-solo/SKILL.md +1 -1
- package/skills/peaks-solo/references/completion-handoff.md +3 -1
- package/dist/src/cli/commands/shadcn-commands.d.ts +0 -3
- package/dist/src/cli/commands/shadcn-commands.js +0 -35
- package/dist/src/cli/commands/skill-scope-commands.d.ts +0 -49
- package/dist/src/cli/commands/skill-scope-commands.js +0 -305
- package/dist/src/services/shadcn/shadcn-service.d.ts +0 -27
- package/dist/src/services/shadcn/shadcn-service.js +0 -128
- package/dist/src/services/skill-scope/adapters/_stub-helper.d.ts +0 -39
- package/dist/src/services/skill-scope/adapters/_stub-helper.js +0 -98
- package/dist/src/services/skill-scope/adapters/claude-code.d.ts +0 -59
- package/dist/src/services/skill-scope/adapters/claude-code.js +0 -304
- package/dist/src/services/skill-scope/adapters/codex.d.ts +0 -2
- package/dist/src/services/skill-scope/adapters/codex.js +0 -12
- package/dist/src/services/skill-scope/adapters/cursor.d.ts +0 -2
- package/dist/src/services/skill-scope/adapters/cursor.js +0 -13
- package/dist/src/services/skill-scope/adapters/qoder.d.ts +0 -2
- package/dist/src/services/skill-scope/adapters/qoder.js +0 -13
- package/dist/src/services/skill-scope/adapters/tongyi.d.ts +0 -2
- package/dist/src/services/skill-scope/adapters/tongyi.js +0 -13
- package/dist/src/services/skill-scope/adapters/trae.d.ts +0 -2
- package/dist/src/services/skill-scope/adapters/trae.js +0 -12
- package/dist/src/services/skill-scope/detect.d.ts +0 -75
- package/dist/src/services/skill-scope/detect.js +0 -480
- package/dist/src/services/skill-scope/registry.d.ts +0 -41
- package/dist/src/services/skill-scope/registry.js +0 -83
- package/dist/src/services/skill-scope/source-of-truth.d.ts +0 -44
- package/dist/src/services/skill-scope/source-of-truth.js +0 -118
- package/dist/src/services/skill-scope/types.d.ts +0 -176
- package/dist/src/services/skill-scope/types.js +0 -74
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export type { RetrospectiveType, RetrospectiveOutcome, RetrospectiveEntry } from './retrospective-index.js';
|
|
2
|
+
import type { RetrospectiveType, RetrospectiveOutcome } from './retrospective-index.js';
|
|
3
|
+
/**
|
|
4
|
+
* Input to `searchRetrospective`. `projectRoot` defaults to `process.cwd()`.
|
|
5
|
+
* `query` is required and non-empty. `type` and `outcome` are optional
|
|
6
|
+
* structured filters that compose with AND.
|
|
7
|
+
*/
|
|
8
|
+
export interface RetrospectiveSearchInput {
|
|
9
|
+
query: string;
|
|
10
|
+
projectRoot?: string;
|
|
11
|
+
limit?: number;
|
|
12
|
+
type?: RetrospectiveType;
|
|
13
|
+
outcome?: RetrospectiveOutcome;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* One hit returned by `searchRetrospective`. The `artifactPaths` are
|
|
17
|
+
* preserved so the LLM can follow up with `peaks retrospective show
|
|
18
|
+
* <id>` (or read the artifact directly).
|
|
19
|
+
*/
|
|
20
|
+
export interface RetrospectiveSearchResult {
|
|
21
|
+
id: string;
|
|
22
|
+
sessionId: string;
|
|
23
|
+
type: RetrospectiveType;
|
|
24
|
+
title: string;
|
|
25
|
+
summary: string;
|
|
26
|
+
outcome: RetrospectiveOutcome;
|
|
27
|
+
artifactPaths: string[];
|
|
28
|
+
score: number;
|
|
29
|
+
positions: number[];
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Run the generic fuzzy kernel against the on-disk retrospective index.
|
|
33
|
+
* Searchable text is `title + " " + summary` per spec §Component Details.
|
|
34
|
+
* `--type` and `--outcome` filters compose with AND before the kernel
|
|
35
|
+
* runs (cheaper to filter, then fuzzy on a smaller set).
|
|
36
|
+
*/
|
|
37
|
+
export declare function searchRetrospective(input: RetrospectiveSearchInput): RetrospectiveSearchResult[];
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
3
|
+
import { fuzzyMatchWithKey } from '../fuzzy-matching/fuzzy-match-service.js';
|
|
4
|
+
const DEFAULT_LIMIT = 6;
|
|
5
|
+
/**
|
|
6
|
+
* Read `.peaks/retrospective/index.json` and return the entries.
|
|
7
|
+
* Throws structured errors with stable codes that the CLI converts
|
|
8
|
+
* to the peaks envelope.
|
|
9
|
+
*/
|
|
10
|
+
function readIndex(projectRoot) {
|
|
11
|
+
const resolvedRoot = resolve(projectRoot);
|
|
12
|
+
const indexPath = join(resolvedRoot, '.peaks', 'retrospective', 'index.json');
|
|
13
|
+
if (!existsSync(indexPath)) {
|
|
14
|
+
const err = new Error(`INDEX_MISSING: retrospective index not found at ${indexPath}`);
|
|
15
|
+
err.code = 'INDEX_MISSING';
|
|
16
|
+
throw err;
|
|
17
|
+
}
|
|
18
|
+
let raw;
|
|
19
|
+
try {
|
|
20
|
+
raw = readFileSync(indexPath, 'utf8');
|
|
21
|
+
}
|
|
22
|
+
catch (cause) {
|
|
23
|
+
const err = new Error(`INDEX_INVALID: failed to read retrospective index at ${indexPath}: ${cause.message}`);
|
|
24
|
+
err.code = 'INDEX_INVALID';
|
|
25
|
+
throw err;
|
|
26
|
+
}
|
|
27
|
+
let parsed;
|
|
28
|
+
try {
|
|
29
|
+
parsed = JSON.parse(raw);
|
|
30
|
+
}
|
|
31
|
+
catch (cause) {
|
|
32
|
+
const err = new Error(`INDEX_INVALID: malformed retrospective index at ${indexPath}: ${cause.message}`);
|
|
33
|
+
err.code = 'INDEX_INVALID';
|
|
34
|
+
throw err;
|
|
35
|
+
}
|
|
36
|
+
const index = parsed;
|
|
37
|
+
return Array.isArray(index.entries) ? index.entries : [];
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Run the generic fuzzy kernel against the on-disk retrospective index.
|
|
41
|
+
* Searchable text is `title + " " + summary` per spec §Component Details.
|
|
42
|
+
* `--type` and `--outcome` filters compose with AND before the kernel
|
|
43
|
+
* runs (cheaper to filter, then fuzzy on a smaller set).
|
|
44
|
+
*/
|
|
45
|
+
export function searchRetrospective(input) {
|
|
46
|
+
if (input.query === '') {
|
|
47
|
+
const err = new Error('EMPTY_QUERY: searchRetrospective requires a non-empty query (use `peaks retrospective index` to list all)');
|
|
48
|
+
err.code = 'EMPTY_QUERY';
|
|
49
|
+
throw err;
|
|
50
|
+
}
|
|
51
|
+
const projectRoot = input.projectRoot ?? process.cwd();
|
|
52
|
+
const limit = input.limit ?? DEFAULT_LIMIT;
|
|
53
|
+
let candidates = readIndex(projectRoot);
|
|
54
|
+
if (input.type !== undefined) {
|
|
55
|
+
candidates = candidates.filter((e) => e.type === input.type);
|
|
56
|
+
}
|
|
57
|
+
if (input.outcome !== undefined) {
|
|
58
|
+
candidates = candidates.filter((e) => e.outcome === input.outcome);
|
|
59
|
+
}
|
|
60
|
+
const matches = fuzzyMatchWithKey(input.query, candidates, { keyFn: (e) => `${e.title} ${e.summary}`, limit, caseSensitive: false });
|
|
61
|
+
return matches.map((m) => {
|
|
62
|
+
const entry = m.item;
|
|
63
|
+
return {
|
|
64
|
+
id: entry.id,
|
|
65
|
+
sessionId: entry.sessionId,
|
|
66
|
+
type: entry.type,
|
|
67
|
+
title: entry.title,
|
|
68
|
+
summary: entry.summary,
|
|
69
|
+
outcome: entry.outcome,
|
|
70
|
+
artifactPaths: entry.artifactPaths,
|
|
71
|
+
score: m.score,
|
|
72
|
+
positions: m.positions,
|
|
73
|
+
};
|
|
74
|
+
});
|
|
75
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export type BuildTool = 'umi' | 'next' | 'vite' | 'rsbuild' | 'rspack' | 'farm' | 'craco' | 'webpack' | 'gulp' | 'angular' | 'custom' | 'unknown';
|
|
2
2
|
export type ComponentLibrary = {
|
|
3
|
-
readonly name: 'antd' | 'antd-pro' | 'mui' | '
|
|
3
|
+
readonly name: 'antd' | 'antd-pro' | 'mui' | 'element-plus' | 'element-ui' | 'arco' | 'tdesign' | 'semi' | 'nextui' | 'chakra' | 'vant' | 'none';
|
|
4
4
|
readonly majorVersion?: string;
|
|
5
5
|
readonly hasProSuite?: boolean;
|
|
6
6
|
};
|
|
@@ -64,8 +64,6 @@ function detectComponentLibrary(deps) {
|
|
|
64
64
|
}
|
|
65
65
|
if ('@mui/material' in deps)
|
|
66
66
|
return { name: 'mui', ...(majorOf(deps['@mui/material']) !== undefined ? { majorVersion: majorOf(deps['@mui/material']) } : {}) };
|
|
67
|
-
if ('tailwindcss' in deps && Object.keys(deps).some((d) => d.startsWith('@radix-ui/')))
|
|
68
|
-
return { name: 'shadcn' };
|
|
69
67
|
if ('element-plus' in deps)
|
|
70
68
|
return { name: 'element-plus' };
|
|
71
69
|
if ('element-ui' in deps)
|
|
@@ -279,8 +277,6 @@ export function componentLibraryLabel(lib) {
|
|
|
279
277
|
return 'Ant Design + Ant Design Pro';
|
|
280
278
|
case 'mui':
|
|
281
279
|
return 'Material UI';
|
|
282
|
-
case 'shadcn':
|
|
283
|
-
return 'shadcn/ui (Tailwind + Radix)';
|
|
284
280
|
case 'element-plus':
|
|
285
281
|
return 'Element Plus';
|
|
286
282
|
case 'element-ui':
|
|
@@ -159,8 +159,6 @@ function renderCommonCodingStyle(ctx) {
|
|
|
159
159
|
}
|
|
160
160
|
if (lib === 'mui')
|
|
161
161
|
stackRules.push('- Style MUI via `sx`, `styled()`, and `theme`. Do NOT apply TailwindCSS utility classes directly to MUI components.');
|
|
162
|
-
if (lib === 'shadcn')
|
|
163
|
-
stackRules.push('- Use existing shadcn component variants and Tailwind utility classes. Do not introduce a competing component library.');
|
|
164
162
|
if (ctx.cssFrameworks.includes('tailwind') && (lib === 'antd' || lib === 'antd-pro' || lib === 'mui')) {
|
|
165
163
|
stackRules.push('- TailwindCSS is for layout/utility only; component-library tokens own component styling.');
|
|
166
164
|
}
|
|
@@ -185,7 +183,7 @@ function renderCodeReview(ctx) {
|
|
|
185
183
|
const extra = [];
|
|
186
184
|
const lib = ctx.componentLibrary.name;
|
|
187
185
|
if (lib === 'antd' || lib === 'antd-pro') {
|
|
188
|
-
extra.push('- Block PRs that introduce a second component library (MUI/
|
|
186
|
+
extra.push('- Block PRs that introduce a second component library (MUI/Chakra) alongside antd.');
|
|
189
187
|
extra.push('- Block PRs that import antd v3/v4 APIs in this v5 project, or vice versa.');
|
|
190
188
|
}
|
|
191
189
|
if (ctx.cssFrameworks.includes('tailwind') && (lib === 'antd' || lib === 'antd-pro' || lib === 'mui')) {
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `peaks workspace migrate-1-4-1` — R004.
|
|
3
|
+
*
|
|
4
|
+
* Cleanup command for projects upgraded from peaks-cli 1.4.1 → 1.4.2.
|
|
5
|
+
*
|
|
6
|
+
* Slice 006 (1.4.0) moved the canonical per-session root to
|
|
7
|
+
* `.peaks/_runtime/<sid>/`. Per-request artifacts (PRD, RD, QA, SC requests)
|
|
8
|
+
* were written to the new root, but per-session artifacts (tech-doc.md,
|
|
9
|
+
* code-review.md, test-cases/<rid>.md, etc.) were kept at the legacy
|
|
10
|
+
* `.peaks/<sid>/<role>/<file>.md` path. The 2-tier fallback in
|
|
11
|
+
* `resolvePrerequisiteAbsolutePathWithFallback` accepts either location, so
|
|
12
|
+
* the functional behavior is correct, but the user's filesystem has visible
|
|
13
|
+
* dual-path duplication ("飘逸" — the user's term for the UX).
|
|
14
|
+
*
|
|
15
|
+
* R004 ships this command to physically move the legacy per-session files
|
|
16
|
+
* into the canonical `_runtime/<sid>/<role>/` location. After this runs,
|
|
17
|
+
* the project has a single canonical tree; the legacy `<sid>/<role>/`
|
|
18
|
+
* directories are removed (only if empty after the move).
|
|
19
|
+
*
|
|
20
|
+
* Default: dry-run. Pass `--apply` to actually move.
|
|
21
|
+
*/
|
|
22
|
+
export declare const PER_SESSION_ARTIFACT_TYPES: readonly ["rd/tech-doc.md", "rd/code-review.md", "rd/security-review.md", "rd/perf-baseline.md", "rd/bug-analysis.md", "qa/security-findings.md", "qa/performance-findings.md"];
|
|
23
|
+
export declare const PER_REQUEST_ARTIFACT_TYPES: readonly ["qa/test-cases/<rid>.md", "qa/test-reports/<rid>.md"];
|
|
24
|
+
export type MigrationPlanEntry = {
|
|
25
|
+
readonly sessionId: string;
|
|
26
|
+
readonly relativePath: string;
|
|
27
|
+
readonly from: string;
|
|
28
|
+
readonly to: string;
|
|
29
|
+
readonly sha256: string;
|
|
30
|
+
readonly reason: 'legacy-only' | 'identical-content-already-canonical' | 'content-mismatch' | 'no-legacy-file';
|
|
31
|
+
};
|
|
32
|
+
export type MigrationResult = {
|
|
33
|
+
readonly plan: ReadonlyArray<MigrationPlanEntry>;
|
|
34
|
+
readonly applied: boolean;
|
|
35
|
+
readonly movedCount: number;
|
|
36
|
+
readonly conflictCount: number;
|
|
37
|
+
readonly deletedEmptyDirs: ReadonlyArray<string>;
|
|
38
|
+
readonly errors: ReadonlyArray<{
|
|
39
|
+
path: string;
|
|
40
|
+
message: string;
|
|
41
|
+
}>;
|
|
42
|
+
};
|
|
43
|
+
export declare function planMigrate1_4_1(projectRoot: string): MigrationResult;
|
|
44
|
+
export declare function applyMigrate1_4_1(projectRoot: string): MigrationResult;
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `peaks workspace migrate-1-4-1` — R004.
|
|
3
|
+
*
|
|
4
|
+
* Cleanup command for projects upgraded from peaks-cli 1.4.1 → 1.4.2.
|
|
5
|
+
*
|
|
6
|
+
* Slice 006 (1.4.0) moved the canonical per-session root to
|
|
7
|
+
* `.peaks/_runtime/<sid>/`. Per-request artifacts (PRD, RD, QA, SC requests)
|
|
8
|
+
* were written to the new root, but per-session artifacts (tech-doc.md,
|
|
9
|
+
* code-review.md, test-cases/<rid>.md, etc.) were kept at the legacy
|
|
10
|
+
* `.peaks/<sid>/<role>/<file>.md` path. The 2-tier fallback in
|
|
11
|
+
* `resolvePrerequisiteAbsolutePathWithFallback` accepts either location, so
|
|
12
|
+
* the functional behavior is correct, but the user's filesystem has visible
|
|
13
|
+
* dual-path duplication ("飘逸" — the user's term for the UX).
|
|
14
|
+
*
|
|
15
|
+
* R004 ships this command to physically move the legacy per-session files
|
|
16
|
+
* into the canonical `_runtime/<sid>/<role>/` location. After this runs,
|
|
17
|
+
* the project has a single canonical tree; the legacy `<sid>/<role>/`
|
|
18
|
+
* directories are removed (only if empty after the move).
|
|
19
|
+
*
|
|
20
|
+
* Default: dry-run. Pass `--apply` to actually move.
|
|
21
|
+
*/
|
|
22
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync } from 'node:fs';
|
|
23
|
+
import { join } from 'node:path';
|
|
24
|
+
export const PER_SESSION_ARTIFACT_TYPES = [
|
|
25
|
+
'rd/tech-doc.md',
|
|
26
|
+
'rd/code-review.md',
|
|
27
|
+
'rd/security-review.md',
|
|
28
|
+
'rd/perf-baseline.md',
|
|
29
|
+
'rd/bug-analysis.md',
|
|
30
|
+
'qa/security-findings.md',
|
|
31
|
+
'qa/performance-findings.md',
|
|
32
|
+
];
|
|
33
|
+
export const PER_REQUEST_ARTIFACT_TYPES = [
|
|
34
|
+
'qa/test-cases/<rid>.md',
|
|
35
|
+
'qa/test-reports/<rid>.md',
|
|
36
|
+
];
|
|
37
|
+
import { createHash } from 'node:crypto';
|
|
38
|
+
function sha256(content) {
|
|
39
|
+
return createHash('sha256').update(content).digest('hex');
|
|
40
|
+
}
|
|
41
|
+
function enumerateLegacySessions(projectRoot) {
|
|
42
|
+
// Legacy per-session dirs: `.peaks/<sid>/` (NOT `.peaks/_runtime/<sid>/`).
|
|
43
|
+
// We skip well-known non-session entries.
|
|
44
|
+
const SKIP = new Set(['memory', 'PROJECT.md', 'retrospective', 'scope', '.peaks-init-hooks-decision.json', 'session.json', '.session.json', '_runtime', '_sub_agents', 'change', 'caller', 'callers', 'sop-state', 'system', 'active-skill.json', '.active-skill.json']);
|
|
45
|
+
const peaksRoot = join(projectRoot, '.peaks');
|
|
46
|
+
if (!existsSync(peaksRoot))
|
|
47
|
+
return [];
|
|
48
|
+
const out = [];
|
|
49
|
+
for (const entry of readdirSync(peaksRoot)) {
|
|
50
|
+
if (SKIP.has(entry))
|
|
51
|
+
continue;
|
|
52
|
+
if (entry.startsWith('.'))
|
|
53
|
+
continue;
|
|
54
|
+
// Treat any directory under .peaks/ that doesn't match a known non-session
|
|
55
|
+
// directory as a candidate session id. The session id is a dated prefix.
|
|
56
|
+
if (entry.match(/^\d{4}-\d{2}-\d{2}-session-/))
|
|
57
|
+
out.push(entry);
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
function listRequestIdsForSession(legacySessionRoot) {
|
|
62
|
+
// Per-request artifacts use file id like `001-r001.md`. We scan the
|
|
63
|
+
// requests dir to find existing rids.
|
|
64
|
+
const ids = new Set();
|
|
65
|
+
for (const role of ['prd', 'rd', 'qa', 'sc']) {
|
|
66
|
+
const dir = join(legacySessionRoot, role, 'requests');
|
|
67
|
+
if (!existsSync(dir))
|
|
68
|
+
continue;
|
|
69
|
+
for (const f of readdirSync(dir)) {
|
|
70
|
+
const m = f.match(/^\d+-(r\d+)\.md$/);
|
|
71
|
+
if (m && m[1])
|
|
72
|
+
ids.add(m[1]);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return [...ids];
|
|
76
|
+
}
|
|
77
|
+
function buildPlan(projectRoot) {
|
|
78
|
+
const plan = [];
|
|
79
|
+
for (const sid of enumerateLegacySessions(projectRoot)) {
|
|
80
|
+
const legacyRoot = join(projectRoot, '.peaks', sid);
|
|
81
|
+
const canonicalRoot = join(projectRoot, '.peaks', '_runtime', sid);
|
|
82
|
+
// Per-session files (no <rid> template).
|
|
83
|
+
for (const rel of PER_SESSION_ARTIFACT_TYPES) {
|
|
84
|
+
const from = join(legacyRoot, rel);
|
|
85
|
+
if (!existsSync(from))
|
|
86
|
+
continue;
|
|
87
|
+
const content = readFileSync(from, 'utf8');
|
|
88
|
+
const hash = sha256(content);
|
|
89
|
+
const to = join(canonicalRoot, rel);
|
|
90
|
+
if (existsSync(to)) {
|
|
91
|
+
const existing = readFileSync(to, 'utf8');
|
|
92
|
+
if (sha256(existing) === hash) {
|
|
93
|
+
plan.push({ sessionId: sid, relativePath: rel, from, to, sha256: hash, reason: 'identical-content-already-canonical' });
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
plan.push({ sessionId: sid, relativePath: rel, from, to, sha256: hash, reason: 'content-mismatch' });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
plan.push({ sessionId: sid, relativePath: rel, from, to, sha256: hash, reason: 'legacy-only' });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// Per-request files (with <rid> template).
|
|
104
|
+
const rids = listRequestIdsForSession(legacyRoot);
|
|
105
|
+
for (const rel of PER_REQUEST_ARTIFACT_TYPES) {
|
|
106
|
+
for (const rid of rids) {
|
|
107
|
+
const expanded = rel.replace('<rid>', rid);
|
|
108
|
+
const from = join(legacyRoot, expanded);
|
|
109
|
+
if (!existsSync(from))
|
|
110
|
+
continue;
|
|
111
|
+
const content = readFileSync(from, 'utf8');
|
|
112
|
+
const hash = sha256(content);
|
|
113
|
+
const to = join(canonicalRoot, expanded);
|
|
114
|
+
if (existsSync(to)) {
|
|
115
|
+
const existing = readFileSync(to, 'utf8');
|
|
116
|
+
if (sha256(existing) === hash) {
|
|
117
|
+
plan.push({ sessionId: sid, relativePath: expanded, from, to, sha256: hash, reason: 'identical-content-already-canonical' });
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
plan.push({ sessionId: sid, relativePath: expanded, from, to, sha256: hash, reason: 'content-mismatch' });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
plan.push({ sessionId: sid, relativePath: expanded, from, to, sha256: hash, reason: 'legacy-only' });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return plan;
|
|
130
|
+
}
|
|
131
|
+
export function planMigrate1_4_1(projectRoot) {
|
|
132
|
+
const plan = buildPlan(projectRoot);
|
|
133
|
+
return {
|
|
134
|
+
plan,
|
|
135
|
+
applied: false,
|
|
136
|
+
movedCount: 0,
|
|
137
|
+
conflictCount: plan.filter((p) => p.reason === 'content-mismatch').length,
|
|
138
|
+
deletedEmptyDirs: [],
|
|
139
|
+
errors: [],
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
export function applyMigrate1_4_1(projectRoot) {
|
|
143
|
+
const plan = buildPlan(projectRoot);
|
|
144
|
+
const errors = [];
|
|
145
|
+
let movedCount = 0;
|
|
146
|
+
const deletedEmptyDirs = [];
|
|
147
|
+
const movedFiles = [];
|
|
148
|
+
for (const entry of plan) {
|
|
149
|
+
try {
|
|
150
|
+
if (entry.reason === 'legacy-only') {
|
|
151
|
+
// Move: ensure target dir exists, then rename.
|
|
152
|
+
mkdirSync(join(entry.to, '..'), { recursive: true });
|
|
153
|
+
renameSync(entry.from, entry.to);
|
|
154
|
+
movedFiles.push(entry.from);
|
|
155
|
+
movedCount++;
|
|
156
|
+
}
|
|
157
|
+
else if (entry.reason === 'identical-content-already-canonical') {
|
|
158
|
+
// Skip the move but delete the duplicate source.
|
|
159
|
+
try {
|
|
160
|
+
rmSync(entry.from);
|
|
161
|
+
}
|
|
162
|
+
catch { /* best-effort */ }
|
|
163
|
+
}
|
|
164
|
+
else if (entry.reason === 'content-mismatch') {
|
|
165
|
+
// Conflict: do NOT delete the source. Mark for manual review.
|
|
166
|
+
errors.push({ path: entry.from, message: `content mismatch; review manually (target ${entry.to})` });
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
catch (err) {
|
|
170
|
+
errors.push({ path: entry.from, message: err.message });
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
// After all moves, clean up legacy session dirs. They're now empty
|
|
174
|
+
// (all files moved; per-role subdirs are also empty). Force-remove the
|
|
175
|
+
// entire tree — if any non-empty dir remains it's a pre-existing file
|
|
176
|
+
// that wasn't part of the migrate plan, which is fine to keep.
|
|
177
|
+
for (const sid of new Set(plan.map((p) => p.sessionId))) {
|
|
178
|
+
const legacyRoot = join(projectRoot, '.peaks', sid);
|
|
179
|
+
try {
|
|
180
|
+
if (existsSync(legacyRoot)) {
|
|
181
|
+
rmSync(legacyRoot, { recursive: true, force: true });
|
|
182
|
+
deletedEmptyDirs.push(legacyRoot);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
catch { /* best-effort */ }
|
|
186
|
+
}
|
|
187
|
+
return {
|
|
188
|
+
plan,
|
|
189
|
+
applied: true,
|
|
190
|
+
movedCount,
|
|
191
|
+
conflictCount: plan.filter((p) => p.reason === 'content-mismatch').length,
|
|
192
|
+
deletedEmptyDirs,
|
|
193
|
+
errors,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const CLI_VERSION = "1.4.
|
|
1
|
+
export declare const CLI_VERSION = "1.4.2";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const CLI_VERSION = "1.4.
|
|
1
|
+
export const CLI_VERSION = "1.4.2";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "peaks-cli",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.2",
|
|
4
4
|
"description": "Cross-AI-IDE workflow-gating CLI + skill family (Claude Code shipped, Trae in progress; Codex / Cursor / Qoder / Tongyi Lingma on the roadmap).",
|
|
5
5
|
"author": "SquabbyZ",
|
|
6
6
|
"license": "MIT",
|
|
@@ -55,15 +55,11 @@
|
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
57
|
"@colbymchenry/codegraph": "0.7.10",
|
|
58
|
-
"chalk": "^5.6.2",
|
|
59
58
|
"commander": "^12.1.0",
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
"shadcn": "4.7.0",
|
|
63
|
-
"terminal-kit": "^3.1.2"
|
|
59
|
+
"fzf": "^0.5.2",
|
|
60
|
+
"headroom-ai": "0.22.4"
|
|
64
61
|
},
|
|
65
62
|
"devDependencies": {
|
|
66
|
-
"@types/chalk": "^2.2.4",
|
|
67
63
|
"@types/node": "^22.10.2",
|
|
68
64
|
"@vitest/coverage-v8": "^2.1.8",
|
|
69
65
|
"tsx": "^4.19.2",
|
|
@@ -185,7 +185,7 @@ Five CLI commands harden the workflow against silent skips: `peaks request lint`
|
|
|
185
185
|
|
|
186
186
|
## Peaks-Cli Completion handoff
|
|
187
187
|
|
|
188
|
-
After final validation, refresh project-local standards via `peaks standards init/update` (never hand-write). Use Peaks-Cli TXT for the compact handoff capsule: mode, validated decisions, artifact paths, standards deltas, open questions, next action.
|
|
188
|
+
After final validation, refresh project-local standards via `peaks standards init/update` (never hand-write). Use Peaks-Cli TXT for the compact handoff capsule: mode, validated decisions, artifact paths, standards deltas, open questions, next action. **Presence management is delegated to the last downstream skill in the workflow** — peaks-solo does not call `peaks skill presence:clear` itself, and does not enforce a "no clear" rule. The downstream skills (peaks-rd, peaks-qa, peaks-txt) each manage their own presence per their respective SKILL.md.
|
|
189
189
|
|
|
190
190
|
→ see `references/completion-handoff.md` for the full handoff + "no auto-exit" rule.
|
|
191
191
|
|
|
@@ -8,7 +8,9 @@ Use Peaks-Cli TXT for the compact handoff capsule: mode, validated decisions, ar
|
|
|
8
8
|
|
|
9
9
|
## Workflow completion (no auto-exit)
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
peaks-solo does NOT itself call `peaks skill presence:clear --project <repo>` at workflow end. Presence management is delegated to the last downstream skill in the workflow (peaks-rd, peaks-qa, peaks-txt); each of those skills owns its own presence:clear step per its SKILL.md. peaks-solo only sets presence: it does not unset it.
|
|
12
|
+
|
|
13
|
+
The user can continue with follow-up requirements naturally — no need to re-invoke `/peaks-solo` to do so. The header continues to display whatever skill is active; the user can `/peaks-solo` again to re-anchor.
|
|
12
14
|
|
|
13
15
|
Before ending, extract durable memories from this session:
|
|
14
16
|
```bash
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import { createShadcnInvocation, executeShadcnInvocation } from '../../services/shadcn/shadcn-service.js';
|
|
2
|
-
import { fail } from '../../shared/result.js';
|
|
3
|
-
import { getErrorMessage, printResult, redactSensitiveErrorMessage } from '../cli-helpers.js';
|
|
4
|
-
function printShadcnFailure(io, error, exitCode = 1) {
|
|
5
|
-
printResult(io, fail('shadcn', 'SHADCN_COMMAND_FAILED', redactSensitiveErrorMessage(getErrorMessage(error)), {}, ['Check the shadcn command arguments before retrying']), false);
|
|
6
|
-
process.exitCode = exitCode;
|
|
7
|
-
}
|
|
8
|
-
async function runShadcnCommand(io, args) {
|
|
9
|
-
try {
|
|
10
|
-
const invocation = createShadcnInvocation({ args });
|
|
11
|
-
const result = await executeShadcnInvocation(invocation);
|
|
12
|
-
const didFail = result.exitCode !== null && result.exitCode !== 0;
|
|
13
|
-
if (result.stdout.length > 0) {
|
|
14
|
-
io.stdout((didFail ? redactSensitiveErrorMessage(result.stdout) : result.stdout).trimEnd());
|
|
15
|
-
}
|
|
16
|
-
if (result.stderr.length > 0) {
|
|
17
|
-
io.stderr((didFail ? redactSensitiveErrorMessage(result.stderr) : result.stderr).trimEnd());
|
|
18
|
-
}
|
|
19
|
-
if (didFail) {
|
|
20
|
-
process.exitCode = result.exitCode;
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
catch (error) {
|
|
24
|
-
printShadcnFailure(io, error);
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
export function registerShadcnCommands(program, io) {
|
|
28
|
-
program
|
|
29
|
-
.command('shadcn')
|
|
30
|
-
.description('Run the pinned shadcn CLI bundled with Peaks')
|
|
31
|
-
.allowUnknownOption(true)
|
|
32
|
-
.helpOption(false)
|
|
33
|
-
.argument('<args...>', 'arguments forwarded to shadcn')
|
|
34
|
-
.action((args) => runShadcnCommand(io, args));
|
|
35
|
-
}
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `peaks skill scope` CLI surface (slice 025.1).
|
|
3
|
-
*
|
|
4
|
-
* Four subcommands (mutually exclusive):
|
|
5
|
-
* - `--detect` — dry-run; prints the relevance matrix, never touches files.
|
|
6
|
-
* - `--apply` — writes the source-of-truth + IDE-native config.
|
|
7
|
-
* - `--show` — reads the source-of-truth + native config back.
|
|
8
|
-
* - `--reset` — removes the source-of-truth + IDE-native config.
|
|
9
|
-
*
|
|
10
|
-
* Exit code matrix (tech-doc §6.3):
|
|
11
|
-
* 0 success
|
|
12
|
-
* 1 uncaught error
|
|
13
|
-
* 2 invalid usage (missing/incompatible flags)
|
|
14
|
-
* 3 source-of-truth written but adapter returned NOT_SUPPORTED
|
|
15
|
-
* 4 adapter failure other than NOT_SUPPORTED
|
|
16
|
-
*/
|
|
17
|
-
import { Command } from 'commander';
|
|
18
|
-
import { type ResultEnvelope } from '../../shared/result.js';
|
|
19
|
-
import { type ProgramIO } from '../cli-helpers.js';
|
|
20
|
-
export type SkillScopeAction = 'detect' | 'apply' | 'show' | 'reset';
|
|
21
|
-
export interface RunSkillScopeInput {
|
|
22
|
-
readonly subcommand: SkillScopeAction;
|
|
23
|
-
readonly project: string;
|
|
24
|
-
readonly strict?: boolean;
|
|
25
|
-
readonly loose?: boolean;
|
|
26
|
-
readonly ide?: string;
|
|
27
|
-
readonly shadowFallback?: boolean;
|
|
28
|
-
readonly json?: boolean;
|
|
29
|
-
/** Test seam: override the detected allowlist (CLI re-adds peaks-* per G6). */
|
|
30
|
-
readonly overrideAllowlist?: readonly string[];
|
|
31
|
-
/** Test seam: force the source-of-truth write to fail (simulates atomicity test). */
|
|
32
|
-
readonly simulateSourceOfTruthWriteFailure?: boolean;
|
|
33
|
-
}
|
|
34
|
-
export interface RunSkillScopeResult {
|
|
35
|
-
readonly exitCode: number;
|
|
36
|
-
readonly envelope: ResultEnvelope<unknown> | null;
|
|
37
|
-
readonly stdout: string;
|
|
38
|
-
readonly stderr: string;
|
|
39
|
-
}
|
|
40
|
-
/**
|
|
41
|
-
* Programmatic entry point for `peaks skill scope`. Used by the CLI shim
|
|
42
|
-
* AND by the unit tests.
|
|
43
|
-
*/
|
|
44
|
-
export declare function runSkillScopeCommand(input: RunSkillScopeInput): Promise<RunSkillScopeResult>;
|
|
45
|
-
/**
|
|
46
|
-
* Register the `peaks skill scope` subcommand on the `skill` command group.
|
|
47
|
-
* Mutually-exclusive flags: exactly one of --detect / --apply / --show / --reset.
|
|
48
|
-
*/
|
|
49
|
-
export declare function registerSkillScopeCommands(program: Command, io: ProgramIO): void;
|