peaks-cli 1.4.2 → 2.0.0
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/.claude-plugin/marketplace.json +51 -0
- package/CHANGELOG.md +238 -0
- package/README-en.md +226 -0
- package/README.md +152 -122
- package/dist/src/cli/commands/agent-commands.d.ts +20 -0
- package/dist/src/cli/commands/agent-commands.js +48 -0
- package/dist/src/cli/commands/audit-commands.d.ts +18 -0
- package/dist/src/cli/commands/audit-commands.js +138 -0
- package/dist/src/cli/commands/classify-classify-commands.d.ts +19 -0
- package/dist/src/cli/commands/classify-classify-commands.js +151 -0
- package/dist/src/cli/commands/code-review-commands.d.ts +34 -0
- package/dist/src/cli/commands/code-review-commands.js +83 -0
- package/dist/src/cli/commands/config-commands.js +90 -0
- package/dist/src/cli/commands/context-commands.d.ts +21 -0
- package/dist/src/cli/commands/context-commands.js +167 -0
- package/dist/src/cli/commands/core-artifact-commands.js +60 -2
- package/dist/src/cli/commands/hook-handle.js +50 -0
- package/dist/src/cli/commands/loop-commands.d.ts +21 -0
- package/dist/src/cli/commands/loop-commands.js +128 -0
- package/dist/src/cli/commands/openspec-commands.js +37 -0
- package/dist/src/cli/commands/preferences-commands.d.ts +2 -0
- package/dist/src/cli/commands/preferences-commands.js +147 -0
- package/dist/src/cli/commands/skill-conformance-commands.d.ts +9 -0
- package/dist/src/cli/commands/skill-conformance-commands.js +39 -0
- package/dist/src/cli/commands/understand-commands.js +34 -0
- package/dist/src/cli/commands/upgrade-commands.d.ts +23 -0
- package/dist/src/cli/commands/upgrade-commands.js +57 -0
- package/dist/src/cli/commands/workflow-commands.js +70 -0
- package/dist/src/cli/commands/workspace-commands.js +86 -0
- package/dist/src/cli/program.js +30 -0
- package/dist/src/services/agent/ecc-agent-service.d.ts +47 -0
- package/dist/src/services/agent/ecc-agent-service.js +143 -0
- package/dist/src/services/artifacts/request-artifact-service.js +14 -0
- package/dist/src/services/audit/backing-detector.d.ts +24 -0
- package/dist/src/services/audit/backing-detector.js +59 -0
- package/dist/src/services/audit/classifier.d.ts +38 -0
- package/dist/src/services/audit/classifier.js +127 -0
- package/dist/src/services/audit/enforcers/active-skill-resolver.d.ts +29 -0
- package/dist/src/services/audit/enforcers/active-skill-resolver.js +71 -0
- package/dist/src/services/audit/enforcers/design-draft-confirm.d.ts +25 -0
- package/dist/src/services/audit/enforcers/design-draft-confirm.js +54 -0
- package/dist/src/services/audit/enforcers/lint-audit-regression.d.ts +21 -0
- package/dist/src/services/audit/enforcers/lint-audit-regression.js +86 -0
- package/dist/src/services/audit/enforcers/lint-catalog-governance.d.ts +27 -0
- package/dist/src/services/audit/enforcers/lint-catalog-governance.js +38 -0
- package/dist/src/services/audit/enforcers/lint-cli-back.d.ts +16 -0
- package/dist/src/services/audit/enforcers/lint-cli-back.js +35 -0
- package/dist/src/services/audit/enforcers/lint-output-style.d.ts +11 -0
- package/dist/src/services/audit/enforcers/lint-output-style.js +94 -0
- package/dist/src/services/audit/enforcers/lint-reference-integrity.d.ts +6 -0
- package/dist/src/services/audit/enforcers/lint-reference-integrity.js +83 -0
- package/dist/src/services/audit/enforcers/lint-reference-shape.d.ts +30 -0
- package/dist/src/services/audit/enforcers/lint-reference-shape.js +272 -0
- package/dist/src/services/audit/enforcers/lint-style.d.ts +49 -0
- package/dist/src/services/audit/enforcers/lint-style.js +173 -0
- package/dist/src/services/audit/enforcers/lint-workflow-shape.d.ts +5 -0
- package/dist/src/services/audit/enforcers/lint-workflow-shape.js +141 -0
- package/dist/src/services/audit/enforcers/login-gate.d.ts +23 -0
- package/dist/src/services/audit/enforcers/login-gate.js +40 -0
- package/dist/src/services/audit/enforcers/mock-placement.d.ts +25 -0
- package/dist/src/services/audit/enforcers/mock-placement.js +48 -0
- package/dist/src/services/audit/enforcers/no-root-pollution.d.ts +21 -0
- package/dist/src/services/audit/enforcers/no-root-pollution.js +56 -0
- package/dist/src/services/audit/enforcers/pre-rd-scan.d.ts +22 -0
- package/dist/src/services/audit/enforcers/pre-rd-scan.js +23 -0
- package/dist/src/services/audit/enforcers/prototype-fidelity.d.ts +25 -0
- package/dist/src/services/audit/enforcers/prototype-fidelity.js +75 -0
- package/dist/src/services/audit/enforcers/resume-detection.d.ts +21 -0
- package/dist/src/services/audit/enforcers/resume-detection.js +52 -0
- package/dist/src/services/audit/enforcers/solo-code-ban.d.ts +23 -0
- package/dist/src/services/audit/enforcers/solo-code-ban.js +27 -0
- package/dist/src/services/audit/enforcers/sub-agent-sid.d.ts +25 -0
- package/dist/src/services/audit/enforcers/sub-agent-sid.js +63 -0
- package/dist/src/services/audit/enforcers/tech-doc-presence.d.ts +28 -0
- package/dist/src/services/audit/enforcers/tech-doc-presence.js +35 -0
- package/dist/src/services/audit/red-line-catalog-p2-a.d.ts +21 -0
- package/dist/src/services/audit/red-line-catalog-p2-a.js +233 -0
- package/dist/src/services/audit/red-line-catalog-p2-b.d.ts +19 -0
- package/dist/src/services/audit/red-line-catalog-p2-b.js +225 -0
- package/dist/src/services/audit/red-line-catalog.d.ts +51 -0
- package/dist/src/services/audit/red-line-catalog.js +210 -0
- package/dist/src/services/audit/red-lines-service.d.ts +23 -0
- package/dist/src/services/audit/red-lines-service.js +486 -0
- package/dist/src/services/audit/scanners/openspec-scanner.d.ts +15 -0
- package/dist/src/services/audit/scanners/openspec-scanner.js +55 -0
- package/dist/src/services/audit/scanners/rules-tree-scanner.d.ts +16 -0
- package/dist/src/services/audit/scanners/rules-tree-scanner.js +56 -0
- package/dist/src/services/audit/scanners/skills-tree-scanner.d.ts +17 -0
- package/dist/src/services/audit/scanners/skills-tree-scanner.js +46 -0
- package/dist/src/services/audit/static-service.d.ts +57 -0
- package/dist/src/services/audit/static-service.js +125 -0
- package/dist/src/services/audit/types.d.ts +69 -0
- package/dist/src/services/audit/types.js +13 -0
- package/dist/src/services/classify/classify-service.d.ts +42 -0
- package/dist/src/services/classify/classify-service.js +122 -0
- package/dist/src/services/classify/classify-types.d.ts +79 -0
- package/dist/src/services/classify/classify-types.js +90 -0
- package/dist/src/services/code-review/ocr-service.d.ts +129 -0
- package/dist/src/services/code-review/ocr-service.js +362 -0
- package/dist/src/services/config/config-migration.d.ts +32 -0
- package/dist/src/services/config/config-migration.js +92 -0
- package/dist/src/services/config/config-restore.d.ts +10 -0
- package/dist/src/services/config/config-restore.js +47 -0
- package/dist/src/services/config/config-rollback.d.ts +13 -0
- package/dist/src/services/config/config-rollback.js +26 -0
- package/dist/src/services/config/config-service.d.ts +35 -2
- package/dist/src/services/config/config-service.js +81 -0
- package/dist/src/services/config/config-types.d.ts +58 -0
- package/dist/src/services/config/config-types.js +6 -0
- package/dist/src/services/doctor/doctor-service.js +96 -0
- package/dist/src/services/ide/adapters/hermes-adapter.d.ts +21 -0
- package/dist/src/services/ide/adapters/hermes-adapter.js +51 -0
- package/dist/src/services/ide/adapters/openclaw-adapter.d.ts +14 -0
- package/dist/src/services/ide/adapters/openclaw-adapter.js +42 -0
- package/dist/src/services/ide/ide-registry.js +7 -0
- package/dist/src/services/ide/ide-types.d.ts +1 -1
- package/dist/src/services/openspec/openspec-propose-from-doctor-service.d.ts +31 -0
- package/dist/src/services/openspec/openspec-propose-from-doctor-service.js +95 -0
- package/dist/src/services/preferences/preferences-service.d.ts +6 -0
- package/dist/src/services/preferences/preferences-service.js +43 -0
- package/dist/src/services/preferences/preferences-types.d.ts +90 -0
- package/dist/src/services/preferences/preferences-types.js +38 -0
- package/dist/src/services/skills/skill-conformance-service.d.ts +40 -0
- package/dist/src/services/skills/skill-conformance-service.js +136 -0
- package/dist/src/services/skills/skill-runbook-service.js +44 -10
- package/dist/src/services/skills/sync-service.d.ts +43 -0
- package/dist/src/services/skills/sync-service.js +99 -0
- package/dist/src/services/slice/slice-check-service.js +166 -13
- package/dist/src/services/slice/slice-check-types.d.ts +1 -1
- package/dist/src/services/standards/migrate-claude-rules-service.d.ts +19 -0
- package/dist/src/services/standards/migrate-claude-rules-service.js +193 -0
- package/dist/src/services/understand/understand-scan-service.js +15 -2
- package/dist/src/services/understand/understand-types.d.ts +26 -0
- package/dist/src/services/upgrade/1x-detector-service.d.ts +7 -0
- package/dist/src/services/upgrade/1x-detector-service.js +94 -0
- package/dist/src/services/upgrade/gitignore-migrate-service.d.ts +56 -0
- package/dist/src/services/upgrade/gitignore-migrate-service.js +170 -0
- package/dist/src/services/upgrade/upgrade-service.d.ts +47 -0
- package/dist/src/services/upgrade/upgrade-service.js +381 -0
- package/dist/src/services/workspace/sid-naming-guard.d.ts +14 -0
- package/dist/src/services/workspace/sid-naming-guard.js +31 -0
- package/dist/src/services/workspace/workspace-archive-service.d.ts +19 -0
- package/dist/src/services/workspace/workspace-archive-service.js +32 -0
- package/dist/src/services/workspace/workspace-clean-service.d.ts +41 -0
- package/dist/src/services/workspace/workspace-clean-service.js +86 -0
- package/dist/src/services/workspace/workspace-state-service.d.ts +7 -0
- package/dist/src/services/workspace/workspace-state-service.js +43 -0
- package/dist/src/shared/change-id.js +4 -1
- package/dist/src/shared/version.d.ts +1 -1
- package/dist/src/shared/version.js +1 -1
- package/package.json +8 -2
- package/schemas/doctor-report.schema.json +1 -1
- package/scripts/install-skills.mjs +296 -12
- package/skills/peaks-doctor/SKILL.md +59 -0
- package/skills/peaks-doctor/references/doctor-check-catalog.md +31 -0
- package/skills/peaks-doctor/references/from-doctor-flow.md +64 -0
- package/skills/peaks-doctor/test_prompts.json +17 -0
- package/skills/peaks-ide/SKILL.md +2 -0
- package/skills/peaks-qa/SKILL.md +9 -7
- package/skills/peaks-qa/references/artifact-per-request.md +19 -5
- package/skills/peaks-qa/references/qa-perf-test-plan.md +6 -6
- package/skills/peaks-qa/references/qa-runbook.md +1 -1
- package/skills/peaks-rd/SKILL.md +25 -10
- package/skills/peaks-rd/references/ocr-integration.md +214 -0
- package/skills/peaks-rd/references/rd-fanout-contracts.md +70 -0
- package/skills/peaks-rd/references/rd-runbook.md +1 -1
- package/skills/peaks-solo/SKILL.md +10 -4
- package/skills/peaks-solo/references/step-0-55-1x-detection.md +82 -0
- package/skills/peaks-solo/references/workflow-gates-and-types.md +9 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SID naming guard. Enforces the "two-axis" convention from spec §0:
|
|
3
|
+
* session id: YYYY-MM-DD-session-<3-6 chars lowercase alnum>
|
|
4
|
+
* change id: kebab-case
|
|
5
|
+
*
|
|
6
|
+
* Spec §8.7 — bare forms (sid-3 / sid-h / sid-r / unknown-sid) are
|
|
7
|
+
* migrated to `_archive/invalid-sids/`, NOT tolerated.
|
|
8
|
+
*/
|
|
9
|
+
export const SID_FORMAT_DESCRIPTION = '<YYYY-MM-DD>-session-<3-6 chars lowercase alnum>, e.g. 2026-06-11-session-abc123';
|
|
10
|
+
const VALID_SID_REGEX = /^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])-session-[0-9a-z]{3,6}$/;
|
|
11
|
+
const BARE_SID_REGEX = /^(sid-[a-z0-9]+|unknown-sid)$/;
|
|
12
|
+
const VALID_CHANGE_ID_REGEX = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
13
|
+
export function isValidSessionId(sid) {
|
|
14
|
+
return VALID_SID_REGEX.test(sid);
|
|
15
|
+
}
|
|
16
|
+
export function isValidChangeId(cid) {
|
|
17
|
+
return VALID_CHANGE_ID_REGEX.test(cid);
|
|
18
|
+
}
|
|
19
|
+
export function isBareSid(name) {
|
|
20
|
+
return BARE_SID_REGEX.test(name);
|
|
21
|
+
}
|
|
22
|
+
export function assertValidSessionId(sid) {
|
|
23
|
+
if (!isValidSessionId(sid)) {
|
|
24
|
+
throw new Error(`NAMING_INVALID: session id "${sid}" does not match required format ${SID_FORMAT_DESCRIPTION}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export function assertValidChangeId(cid) {
|
|
28
|
+
if (!isValidChangeId(cid)) {
|
|
29
|
+
throw new Error(`NAMING_INVALID: change id "${cid}" must be kebab-case (lowercase alnum and dashes only)`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface ArchivePlan {
|
|
2
|
+
sid: string;
|
|
3
|
+
sourcePath: string;
|
|
4
|
+
targetPath: string;
|
|
5
|
+
sourceExists: boolean;
|
|
6
|
+
}
|
|
7
|
+
export interface ArchiveOptions {
|
|
8
|
+
sid: string;
|
|
9
|
+
apply: boolean;
|
|
10
|
+
}
|
|
11
|
+
export interface ArchiveResult {
|
|
12
|
+
moved: string[];
|
|
13
|
+
skipped: {
|
|
14
|
+
sid: string;
|
|
15
|
+
reason: string;
|
|
16
|
+
}[];
|
|
17
|
+
}
|
|
18
|
+
export declare function planArchive(projectRoot: string, sid: string): ArchivePlan;
|
|
19
|
+
export declare function archiveSession(projectRoot: string, options: ArchiveOptions): ArchiveResult;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, renameSync } from 'node:fs';
|
|
2
|
+
import { join, sep } from 'node:path';
|
|
3
|
+
import { assertValidSessionId } from './sid-naming-guard.js';
|
|
4
|
+
const ARCHIVE_ROOT = '_archive';
|
|
5
|
+
const RUNTIME_DIR = '_runtime';
|
|
6
|
+
function toPosix(p) {
|
|
7
|
+
return p.split(sep).join('/');
|
|
8
|
+
}
|
|
9
|
+
export function planArchive(projectRoot, sid) {
|
|
10
|
+
assertValidSessionId(sid);
|
|
11
|
+
const yyyyMm = sid.slice(0, 7);
|
|
12
|
+
const sourcePath = join(projectRoot, '.peaks', RUNTIME_DIR, sid);
|
|
13
|
+
const targetPath = join(projectRoot, '.peaks', ARCHIVE_ROOT, yyyyMm, sid);
|
|
14
|
+
return {
|
|
15
|
+
sid,
|
|
16
|
+
sourcePath: toPosix(sourcePath),
|
|
17
|
+
targetPath: toPosix(targetPath),
|
|
18
|
+
sourceExists: existsSync(sourcePath),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export function archiveSession(projectRoot, options) {
|
|
22
|
+
const plan = planArchive(projectRoot, options.sid);
|
|
23
|
+
if (!plan.sourceExists) {
|
|
24
|
+
return { moved: [], skipped: [{ sid: options.sid, reason: 'source does not exist' }] };
|
|
25
|
+
}
|
|
26
|
+
if (!options.apply) {
|
|
27
|
+
return { moved: [], skipped: [{ sid: options.sid, reason: 'dry-run' }] };
|
|
28
|
+
}
|
|
29
|
+
mkdirSync(join(plan.targetPath, '..'), { recursive: true });
|
|
30
|
+
renameSync(plan.sourcePath, plan.targetPath);
|
|
31
|
+
return { moved: [options.sid], skipped: [] };
|
|
32
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export interface RuntimeSessionInfo {
|
|
2
|
+
sid: string;
|
|
3
|
+
mtimeMs: number;
|
|
4
|
+
ageHours: number;
|
|
5
|
+
}
|
|
6
|
+
export interface CleanupOptions {
|
|
7
|
+
olderThanHours: number;
|
|
8
|
+
graceHours: number;
|
|
9
|
+
}
|
|
10
|
+
export interface CleanupResult {
|
|
11
|
+
deleted: string[];
|
|
12
|
+
skipped: {
|
|
13
|
+
sid: string;
|
|
14
|
+
reason: string;
|
|
15
|
+
}[];
|
|
16
|
+
}
|
|
17
|
+
export declare function runtimeDirPath(projectRoot: string): string;
|
|
18
|
+
export declare function listRuntimeSessions(projectRoot: string): RuntimeSessionInfo[];
|
|
19
|
+
export declare function planRuntimeCleanup(sessions: RuntimeSessionInfo[], options: CleanupOptions): {
|
|
20
|
+
eligible: string[];
|
|
21
|
+
skipped: {
|
|
22
|
+
sid: string;
|
|
23
|
+
reason: string;
|
|
24
|
+
}[];
|
|
25
|
+
};
|
|
26
|
+
export declare function executeRuntimeCleanup(projectRoot: string, options: CleanupOptions & {
|
|
27
|
+
apply: boolean;
|
|
28
|
+
}): CleanupResult;
|
|
29
|
+
export interface SubAgentInvalidPlan {
|
|
30
|
+
invalid: string[];
|
|
31
|
+
invalidSidFormat: string[];
|
|
32
|
+
}
|
|
33
|
+
export declare function subAgentDirPath(projectRoot: string): string;
|
|
34
|
+
export declare function invalidSidsArchivePath(projectRoot: string): string;
|
|
35
|
+
export declare function listInvalidSubAgentSids(projectRoot: string): string[];
|
|
36
|
+
export declare function executeSubAgentClean(projectRoot: string, options: {
|
|
37
|
+
apply: boolean;
|
|
38
|
+
}): {
|
|
39
|
+
moved: string[];
|
|
40
|
+
skipped: string[];
|
|
41
|
+
};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, renameSync, rmSync, statSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { isBareSid, isValidSessionId } from './sid-naming-guard.js';
|
|
4
|
+
const RUNTIME_DIR = '_runtime';
|
|
5
|
+
export function runtimeDirPath(projectRoot) {
|
|
6
|
+
return join(projectRoot, '.peaks', RUNTIME_DIR);
|
|
7
|
+
}
|
|
8
|
+
export function listRuntimeSessions(projectRoot) {
|
|
9
|
+
const dir = runtimeDirPath(projectRoot);
|
|
10
|
+
if (!existsSync(dir))
|
|
11
|
+
return [];
|
|
12
|
+
const now = Date.now();
|
|
13
|
+
return readdirSync(dir, { withFileTypes: true })
|
|
14
|
+
.filter((e) => e.isDirectory())
|
|
15
|
+
.map((e) => {
|
|
16
|
+
const sid = e.name;
|
|
17
|
+
const fullPath = join(dir, sid);
|
|
18
|
+
const stat = statSync(fullPath);
|
|
19
|
+
const ageHours = (now - stat.mtimeMs) / (1000 * 3600);
|
|
20
|
+
return { sid, mtimeMs: stat.mtimeMs, ageHours };
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
export function planRuntimeCleanup(sessions, options) {
|
|
24
|
+
const eligible = [];
|
|
25
|
+
const skipped = [];
|
|
26
|
+
const cutoffHours = options.olderThanHours + options.graceHours;
|
|
27
|
+
for (const s of sessions) {
|
|
28
|
+
if (s.ageHours >= cutoffHours) {
|
|
29
|
+
eligible.push(s.sid);
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
skipped.push({ sid: s.sid, reason: `fresh: age=${s.ageHours.toFixed(1)}h < cutoff=${cutoffHours}h` });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return { eligible, skipped };
|
|
36
|
+
}
|
|
37
|
+
export function executeRuntimeCleanup(projectRoot, options) {
|
|
38
|
+
const sessions = listRuntimeSessions(projectRoot);
|
|
39
|
+
const plan = planRuntimeCleanup(sessions, options);
|
|
40
|
+
if (options.apply) {
|
|
41
|
+
const dir = runtimeDirPath(projectRoot);
|
|
42
|
+
for (const sid of plan.eligible) {
|
|
43
|
+
rmSync(join(dir, sid), { recursive: true, force: true });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return { deleted: plan.eligible, skipped: plan.skipped };
|
|
47
|
+
}
|
|
48
|
+
const SUBAGENT_DIR = '_sub_agents';
|
|
49
|
+
const INVALID_ARCHIVE = '_archive/invalid-sids';
|
|
50
|
+
export function subAgentDirPath(projectRoot) {
|
|
51
|
+
return join(projectRoot, '.peaks', SUBAGENT_DIR);
|
|
52
|
+
}
|
|
53
|
+
export function invalidSidsArchivePath(projectRoot) {
|
|
54
|
+
return join(projectRoot, '.peaks', INVALID_ARCHIVE);
|
|
55
|
+
}
|
|
56
|
+
export function listInvalidSubAgentSids(projectRoot) {
|
|
57
|
+
const dir = subAgentDirPath(projectRoot);
|
|
58
|
+
if (!existsSync(dir))
|
|
59
|
+
return [];
|
|
60
|
+
return readdirSync(dir, { withFileTypes: true })
|
|
61
|
+
.filter((e) => e.isDirectory())
|
|
62
|
+
.map((e) => e.name)
|
|
63
|
+
.filter((name) => isBareSid(name) || !isValidSessionId(name));
|
|
64
|
+
}
|
|
65
|
+
export function executeSubAgentClean(projectRoot, options) {
|
|
66
|
+
const invalid = listInvalidSubAgentSids(projectRoot);
|
|
67
|
+
const moved = [];
|
|
68
|
+
if (options.apply && invalid.length > 0) {
|
|
69
|
+
const archiveDir = invalidSidsArchivePath(projectRoot);
|
|
70
|
+
mkdirSync(archiveDir, { recursive: true });
|
|
71
|
+
for (const sid of invalid) {
|
|
72
|
+
const from = join(subAgentDirPath(projectRoot), sid);
|
|
73
|
+
const to = join(archiveDir, sid);
|
|
74
|
+
if (existsSync(to)) {
|
|
75
|
+
// collision — append timestamp suffix
|
|
76
|
+
const stamped = `${sid}-${Date.now()}`;
|
|
77
|
+
renameSync(from, join(archiveDir, stamped));
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
renameSync(from, to);
|
|
81
|
+
}
|
|
82
|
+
moved.push(sid);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return { moved: options.apply ? moved : invalid, skipped: [] };
|
|
86
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare function isLegacyDecisionDotfile(name: string): boolean;
|
|
2
|
+
export declare function stateDirPath(projectRoot: string): string;
|
|
3
|
+
export interface CollectResult {
|
|
4
|
+
moved: string[];
|
|
5
|
+
skipped: string[];
|
|
6
|
+
}
|
|
7
|
+
export declare function collectLegacyDecisionDotfiles(projectRoot: string): CollectResult;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, renameSync } from 'node:fs';
|
|
2
|
+
import { join, posix } from 'node:path';
|
|
3
|
+
/**
|
|
4
|
+
* Spec §8.4 + §8.5 — `.peaks/_state/` collects one-time decision dotfiles.
|
|
5
|
+
* Migrates from legacy `.peaks/<name>` flat layout.
|
|
6
|
+
*
|
|
7
|
+
* `stateDirPath` returns a POSIX-normalized logical path so callers and tests
|
|
8
|
+
* can compare against a platform-independent string (the .peaks tree is a
|
|
9
|
+
* config key, not a direct fs call). `mkdirSync` / `renameSync` below use
|
|
10
|
+
* the platform-native `join` so files actually land on disk.
|
|
11
|
+
*/
|
|
12
|
+
const LEGACY_DOTFILES = [
|
|
13
|
+
'.peaks-init-hooks-decision.json',
|
|
14
|
+
'.peaks-openspec-opt-in.json',
|
|
15
|
+
];
|
|
16
|
+
const STATE_DIR_NAME = '_state';
|
|
17
|
+
export function isLegacyDecisionDotfile(name) {
|
|
18
|
+
return LEGACY_DOTFILES.includes(name);
|
|
19
|
+
}
|
|
20
|
+
export function stateDirPath(projectRoot) {
|
|
21
|
+
return posix.join(projectRoot, '.peaks', STATE_DIR_NAME);
|
|
22
|
+
}
|
|
23
|
+
export function collectLegacyDecisionDotfiles(projectRoot) {
|
|
24
|
+
const peaksDir = join(projectRoot, '.peaks');
|
|
25
|
+
// Use the platform-native join for actual filesystem operations;
|
|
26
|
+
// `stateDirPath` (POSIX) is only the public/portable surface for callers.
|
|
27
|
+
const stateDir = join(projectRoot, '.peaks', STATE_DIR_NAME);
|
|
28
|
+
mkdirSync(stateDir, { recursive: true });
|
|
29
|
+
const moved = [];
|
|
30
|
+
const skipped = [];
|
|
31
|
+
for (const name of LEGACY_DOTFILES) {
|
|
32
|
+
const from = join(peaksDir, name);
|
|
33
|
+
const to = join(stateDir, name);
|
|
34
|
+
if (!existsSync(from))
|
|
35
|
+
continue;
|
|
36
|
+
if (existsSync(to)) {
|
|
37
|
+
throw new Error(`DOTFILE_COLLISION: ${name} already exists in ${stateDir} (${readFileSync(to, 'utf8').length} bytes); refusing to overwrite`);
|
|
38
|
+
}
|
|
39
|
+
renameSync(from, to);
|
|
40
|
+
moved.push(name);
|
|
41
|
+
}
|
|
42
|
+
return { moved, skipped };
|
|
43
|
+
}
|
|
@@ -303,7 +303,10 @@ export function setCurrentChangeId(projectRoot, changeId, options = {}) {
|
|
|
303
303
|
catch { /* best effort */ }
|
|
304
304
|
}
|
|
305
305
|
}
|
|
306
|
-
|
|
306
|
+
// On Windows, use a 'junction' (directory hard link) which doesn't
|
|
307
|
+
// require developer mode / admin. POSIX uses a regular 'dir' symlink.
|
|
308
|
+
const linkType = process.platform === 'win32' ? 'junction' : 'dir';
|
|
309
|
+
symlinkSync(targetDir, bindingPath, linkType);
|
|
307
310
|
}
|
|
308
311
|
/**
|
|
309
312
|
* Canonical on-disk path to a change-id's reviewable artifacts
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const CLI_VERSION = "
|
|
1
|
+
export declare const CLI_VERSION = "2.0.0";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const CLI_VERSION = "
|
|
1
|
+
export const CLI_VERSION = "2.0.0";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "peaks-cli",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
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",
|
|
@@ -33,7 +33,12 @@
|
|
|
33
33
|
"!skills/**/test-prompts.json",
|
|
34
34
|
"!skills/**/.DS_Store",
|
|
35
35
|
"output-styles/**",
|
|
36
|
-
"schemas/*.json"
|
|
36
|
+
"schemas/*.json",
|
|
37
|
+
".claude-plugin/**",
|
|
38
|
+
"README.md",
|
|
39
|
+
"README-en.md",
|
|
40
|
+
"CHANGELOG.md",
|
|
41
|
+
"LICENSE"
|
|
37
42
|
],
|
|
38
43
|
"scripts": {
|
|
39
44
|
"build": "node ./scripts/sync-version.mjs && node ./scripts/clean-dist.mjs && tsc -p tsconfig.json",
|
|
@@ -54,6 +59,7 @@
|
|
|
54
59
|
"node": ">=20.0.0"
|
|
55
60
|
},
|
|
56
61
|
"dependencies": {
|
|
62
|
+
"@alibaba-group/open-code-review": "1.3.1",
|
|
57
63
|
"@colbymchenry/codegraph": "0.7.10",
|
|
58
64
|
"commander": "^12.1.0",
|
|
59
65
|
"fzf": "^0.5.2",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"properties": {
|
|
14
14
|
"id": {
|
|
15
15
|
"type": "string",
|
|
16
|
-
"pattern": "^(skill|skill-name|skill-parse|skill-runbook|skill-apply-note|skill-presence|statusline|schema|config|doctor-self|capability|build|integration):[A-Za-z0-9][A-Za-z0-9._-]*$",
|
|
16
|
+
"pattern": "^(skill|skill-name|skill-parse|skill-runbook|skill-apply-note|skill-presence|statusline|schema|config|doctor-self|capability|build|integration|L3):[A-Za-z0-9][A-Za-z0-9._-]*$",
|
|
17
17
|
"description": "Stable check id. Known prefixes: skill:<name> (required skill present), skill-name:<dir> (directory matches declared name), skill-parse:<dir> (skill metadata parsed), skill-runbook:<name> (Default runbook section exists), skill-apply-note:<name> (destructive --apply lines carry an authorization/--dry-run note), skill-presence:<topic> (status of .peaks/.active-skill.json — current/freshness/workspace), statusline:<topic> (out-of-band Claude Code statusLine — install/runtime), schema:<file> (schema file exists and is valid JSON), config:<scope> (optional config locations), doctor-self:<topic> (doctor validates its own output against this schema), capability:<name> (third-party capability is resolvable at the pinned version), build:<topic> (build-hygiene checks — dist/source version consistency), integration:<name> (third-party integration probe — e.g. gateguard-fact-force PreToolUse hook conflict on .peaks/**)."
|
|
18
18
|
},
|
|
19
19
|
"ok": { "type": "boolean" },
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { closeSync, constants, existsSync, fchmodSync, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, readlinkSync, realpathSync, readdirSync, renameSync, symlinkSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
3
4
|
import { createHash, randomUUID } from 'node:crypto';
|
|
4
5
|
import { homedir } from 'node:os';
|
|
5
6
|
import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
@@ -389,6 +390,19 @@ const IDE_DETECTION_DIRS = [
|
|
|
389
390
|
{ id: 'tongyi-lingma', dir: '.tongyi-lingma' },
|
|
390
391
|
];
|
|
391
392
|
|
|
393
|
+
/**
|
|
394
|
+
* Per-IDE skill install paths. Per peaks-cli tenet
|
|
395
|
+
* "minimal-user-operation" (2026-06-11), the user should
|
|
396
|
+
* never have to run a per-platform install command — the
|
|
397
|
+
* `npm i -g peaks-cli` postinstall iterates ALL of these
|
|
398
|
+
* and symlinks the peaks-* skill family to every platform
|
|
399
|
+
* the user might be on.
|
|
400
|
+
*
|
|
401
|
+
* 1.x had only `claude-code` (the other 5 entries were
|
|
402
|
+
* `null`); real Trae users reported the Trae skill
|
|
403
|
+
* directory was never populated. 2.0 fixes this by giving
|
|
404
|
+
* all 8 platforms canonical install paths.
|
|
405
|
+
*/
|
|
392
406
|
const IDE_SKILL_INSTALL_PROFILES = {
|
|
393
407
|
'claude-code': {
|
|
394
408
|
skillsDir: join(homedir(), '.claude', 'skills'),
|
|
@@ -396,11 +410,48 @@ const IDE_SKILL_INSTALL_PROFILES = {
|
|
|
396
410
|
envVar: 'PEAKS_CLAUDE_SKILLS_DIR',
|
|
397
411
|
outputStylesEnvVar: 'PEAKS_CLAUDE_OUTPUT_STYLES_DIR',
|
|
398
412
|
},
|
|
399
|
-
'trae':
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
413
|
+
'trae': {
|
|
414
|
+
skillsDir: join(homedir(), '.trae', 'skills'),
|
|
415
|
+
outputStylesDir: join(homedir(), '.trae', 'output-styles'),
|
|
416
|
+
envVar: 'PEAKS_TRAE_SKILLS_DIR',
|
|
417
|
+
outputStylesEnvVar: 'PEAKS_TRAE_OUTPUT_STYLES_DIR',
|
|
418
|
+
},
|
|
419
|
+
'codex': {
|
|
420
|
+
skillsDir: join(homedir(), '.codex', 'skills'),
|
|
421
|
+
outputStylesDir: join(homedir(), '.codex', 'output-styles'),
|
|
422
|
+
envVar: 'PEAKS_CODEX_SKILLS_DIR',
|
|
423
|
+
outputStylesEnvVar: 'PEAKS_CODEX_OUTPUT_STYLES_DIR',
|
|
424
|
+
},
|
|
425
|
+
'cursor': {
|
|
426
|
+
skillsDir: join(homedir(), '.cursor', 'skills'),
|
|
427
|
+
outputStylesDir: join(homedir(), '.cursor', 'output-styles'),
|
|
428
|
+
envVar: 'PEAKS_CURSOR_SKILLS_DIR',
|
|
429
|
+
outputStylesEnvVar: 'PEAKS_CURSOR_OUTPUT_STYLES_DIR',
|
|
430
|
+
},
|
|
431
|
+
'qoder': {
|
|
432
|
+
skillsDir: join(homedir(), '.qoder', 'skills'),
|
|
433
|
+
outputStylesDir: join(homedir(), '.qoder', 'output-styles'),
|
|
434
|
+
envVar: 'PEAKS_QODER_SKILLS_DIR',
|
|
435
|
+
outputStylesEnvVar: 'PEAKS_QODER_OUTPUT_STYLES_DIR',
|
|
436
|
+
},
|
|
437
|
+
'tongyi-lingma': {
|
|
438
|
+
skillsDir: join(homedir(), '.tongyi-lingma', 'skills'),
|
|
439
|
+
outputStylesDir: join(homedir(), '.tongyi-lingma', 'output-styles'),
|
|
440
|
+
envVar: 'PEAKS_TONGYI_SKILLS_DIR',
|
|
441
|
+
outputStylesEnvVar: 'PEAKS_TONGYI_OUTPUT_STYLES_DIR',
|
|
442
|
+
},
|
|
443
|
+
'hermes': {
|
|
444
|
+
skillsDir: join(homedir(), '.hermes', 'skills'),
|
|
445
|
+
outputStylesDir: join(homedir(), '.hermes', 'output-styles'),
|
|
446
|
+
envVar: 'PEAKS_HERMES_SKILLS_DIR',
|
|
447
|
+
outputStylesEnvVar: 'PEAKS_HERMES_OUTPUT_STYLES_DIR',
|
|
448
|
+
},
|
|
449
|
+
'openclaw': {
|
|
450
|
+
skillsDir: join(homedir(), '.openclaw', 'skills'),
|
|
451
|
+
outputStylesDir: join(homedir(), '.openclaw', 'output-styles'),
|
|
452
|
+
envVar: 'PEAKS_OPENCLAW_SKILLS_DIR',
|
|
453
|
+
outputStylesEnvVar: 'PEAKS_OPENCLAW_OUTPUT_STYLES_DIR',
|
|
454
|
+
},
|
|
404
455
|
};
|
|
405
456
|
|
|
406
457
|
function detectInstalledIdeId(projectRoot) {
|
|
@@ -657,9 +708,228 @@ export function installBundledOutputStyles(options = {}) {
|
|
|
657
708
|
return { installed, skipped };
|
|
658
709
|
}
|
|
659
710
|
|
|
711
|
+
/**
|
|
712
|
+
* Per-platform fan-out — iterate ALL 8 IdeIds and call
|
|
713
|
+
* `installBundledSkills` for each. Per peaks-cli tenet
|
|
714
|
+
* "minimal-user-operation" (2026-06-11): the user should
|
|
715
|
+
* never have to run a per-platform install command. The
|
|
716
|
+
* 1.x postinstall only handled the auto-detected single
|
|
717
|
+
* IDE; 2.0 fixes this so the peaks-* skill family is
|
|
718
|
+
* symlinked to every platform the user might be on.
|
|
719
|
+
*
|
|
720
|
+
* Returns an array of { ideId, skillsDir, installed, skipped }
|
|
721
|
+
* per platform. Symlink failures are soft (logged to stderr,
|
|
722
|
+
* never throw) so one platform's failure doesn't block the
|
|
723
|
+
* other 7.
|
|
724
|
+
*/
|
|
725
|
+
export function installBundledSkillsForAllPlatforms(options = {}) {
|
|
726
|
+
const platforms = Object.keys(IDE_SKILL_INSTALL_PROFILES);
|
|
727
|
+
const perPlatform = [];
|
|
728
|
+
// Back-compat precedence (regression fix 2026-06-12,
|
|
729
|
+
// slice 2026-06-12-postinstall-1x-detector-tdd):
|
|
730
|
+
// when iterating the 8 platforms, the claude-code install
|
|
731
|
+
// must still honor the PEAKS_CLAUDE_SKILLS_DIR env var
|
|
732
|
+
// (the legacy back-compat surface from 1.x). The other 7
|
|
733
|
+
// platforms use their per-IDE profile paths unconditionally.
|
|
734
|
+
// Without this fix the 8-IDE fan-out regresses the
|
|
735
|
+
// `peaks install-skills` env-var override contract that
|
|
736
|
+
// user CI / 1.x → 2.0 migration scripts depend on.
|
|
737
|
+
const claudeEnv = process.env.PEAKS_CLAUDE_SKILLS_DIR;
|
|
738
|
+
for (const ideId of platforms) {
|
|
739
|
+
try {
|
|
740
|
+
const platformOpts =
|
|
741
|
+
ideId === 'claude-code' && claudeEnv !== undefined && claudeEnv.length > 0
|
|
742
|
+
? { ...options, ideId, targetRoot: claudeEnv }
|
|
743
|
+
: { ...options, ideId };
|
|
744
|
+
const result = installBundledSkills(platformOpts);
|
|
745
|
+
perPlatform.push({
|
|
746
|
+
ideId,
|
|
747
|
+
skillsDir: IDE_SKILL_INSTALL_PROFILES[ideId]?.skillsDir ?? '(unknown)',
|
|
748
|
+
installed: result.installed,
|
|
749
|
+
skipped: result.skipped,
|
|
750
|
+
});
|
|
751
|
+
} catch (err) {
|
|
752
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
753
|
+
process.stderr.write(
|
|
754
|
+
`peaks install-skills: ${ideId} platform failed (continuing): ${message}\n`
|
|
755
|
+
);
|
|
756
|
+
perPlatform.push({
|
|
757
|
+
ideId,
|
|
758
|
+
skillsDir: IDE_SKILL_INSTALL_PROFILES[ideId]?.skillsDir ?? '(unknown)',
|
|
759
|
+
installed: [],
|
|
760
|
+
skipped: [],
|
|
761
|
+
error: message,
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
return perPlatform;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
/**
|
|
769
|
+
* 1.x → 2.0 detection — sniff for legacy 1.x project state
|
|
770
|
+
* in `cwd`. Returns a 1.x detection envelope with the
|
|
771
|
+
* detected signals (so the postinstall can decide whether
|
|
772
|
+
* to auto-upgrade).
|
|
773
|
+
*
|
|
774
|
+
* 1.x signals (any one fires the detection):
|
|
775
|
+
* - `~/.peaks/config.json` exists with `version: '1.4.2'` (or
|
|
776
|
+
* any '1.x' version that predates the 2.0 schema)
|
|
777
|
+
* - `.claude/rules/common/dev-preference.md` exists and
|
|
778
|
+
* references "peaks progress" (the 1.x CLI surface
|
|
779
|
+
* removed in slice #014)
|
|
780
|
+
* - `<cwd>/.peaks/preferences.json` missing OR has no
|
|
781
|
+
* `schema_version: '2.0.0'` field
|
|
782
|
+
*
|
|
783
|
+
* Returns:
|
|
784
|
+
* { isOneX: boolean, signals: string[], projectRoot: string|null,
|
|
785
|
+
* configPath: string|null }
|
|
786
|
+
*/
|
|
787
|
+
export function detect1xProjectState(cwd = process.cwd()) {
|
|
788
|
+
const home = homedir();
|
|
789
|
+
const signals = [];
|
|
790
|
+
let projectRoot = null;
|
|
791
|
+
let configPath = null;
|
|
792
|
+
|
|
793
|
+
// Walk up from cwd looking for .peaks/_runtime (signals
|
|
794
|
+
// we're inside a peaks project).
|
|
795
|
+
let dir = cwd;
|
|
796
|
+
for (let i = 0; i < 8; i += 1) {
|
|
797
|
+
const peaksRuntime = join(dir, '.peaks', '_runtime');
|
|
798
|
+
if (existsSync(peaksRuntime)) {
|
|
799
|
+
projectRoot = dir;
|
|
800
|
+
break;
|
|
801
|
+
}
|
|
802
|
+
const parent = dirname(dir);
|
|
803
|
+
if (parent === dir) break;
|
|
804
|
+
dir = parent;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
// Signal 1: ~/.peaks/config.json with 1.x version
|
|
808
|
+
const globalConfig = join(home, '.peaks', 'config.json');
|
|
809
|
+
if (existsSync(globalConfig)) {
|
|
810
|
+
try {
|
|
811
|
+
const raw = JSON.parse(readFileSync(globalConfig, 'utf8'));
|
|
812
|
+
if (typeof raw.version === 'string' && /^1\./.test(raw.version)) {
|
|
813
|
+
signals.push(`global config at ${globalConfig} is 1.x (${raw.version})`);
|
|
814
|
+
if (configPath === null) configPath = globalConfig;
|
|
815
|
+
}
|
|
816
|
+
} catch {
|
|
817
|
+
// ignore parse error — the 1.x detection is best-effort
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// Signal 2: .claude/rules/common/dev-preference.md with peaks progress
|
|
822
|
+
if (projectRoot !== null) {
|
|
823
|
+
const devPref = join(projectRoot, '.claude', 'rules', 'common', 'dev-preference.md');
|
|
824
|
+
if (existsSync(devPref)) {
|
|
825
|
+
try {
|
|
826
|
+
const body = readFileSync(devPref, 'utf8');
|
|
827
|
+
if (/peaks progress/i.test(body)) {
|
|
828
|
+
signals.push(`${devPref} references "peaks progress" (1.x CLI surface, removed in slice #014)`);
|
|
829
|
+
}
|
|
830
|
+
} catch {
|
|
831
|
+
// ignore
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
// Signal 3: project preferences.json missing or 1.x
|
|
835
|
+
const prefs = join(projectRoot, '.peaks', 'preferences.json');
|
|
836
|
+
if (!existsSync(prefs)) {
|
|
837
|
+
signals.push(`${prefs} does not exist (1.x project never migrated)`);
|
|
838
|
+
} else {
|
|
839
|
+
try {
|
|
840
|
+
const raw = JSON.parse(readFileSync(prefs, 'utf8'));
|
|
841
|
+
if (raw.schema_version !== '2.0.0') {
|
|
842
|
+
signals.push(`${prefs} has schema_version ${JSON.stringify(raw.schema_version)}, expected '2.0.0'`);
|
|
843
|
+
}
|
|
844
|
+
} catch {
|
|
845
|
+
signals.push(`${prefs} exists but is not valid JSON`);
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
return {
|
|
851
|
+
isOneX: signals.length > 0,
|
|
852
|
+
signals,
|
|
853
|
+
projectRoot,
|
|
854
|
+
configPath,
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
/**
|
|
859
|
+
* Postinstall auto-upgrade — when the user just ran
|
|
860
|
+
* `npm i -g peaks-cli@2.0` and `cwd` is a 1.x peaks-cli
|
|
861
|
+
* project, this shells out to the installed `peaks`
|
|
862
|
+
* binary to run the umbrella `peaks upgrade --to 2.0 --auto`.
|
|
863
|
+
*
|
|
864
|
+
* Per the "minimal-user-operation" tenet, the user should
|
|
865
|
+
* never have to run a second command after `npm i -g`. The
|
|
866
|
+
* upgrade CLI (if installed) is at the resolved `peaks`
|
|
867
|
+
* binary path; if not, the user gets a hint to run it
|
|
868
|
+
* manually.
|
|
869
|
+
*
|
|
870
|
+
* The auto-upgrade is opt-out via:
|
|
871
|
+
* PEAKS_SKIP_AUTO_UPGRADE=1
|
|
872
|
+
* (so a CI box that installs 2.0 but never wants the
|
|
873
|
+
* project-level migration can suppress the auto-step).
|
|
874
|
+
*/
|
|
875
|
+
export async function autoUpgrade1xProjectIfPresent(options = {}) {
|
|
876
|
+
if (process.env.PEAKS_SKIP_AUTO_UPGRADE === '1') {
|
|
877
|
+
return { ran: false, reason: 'PEAKS_SKIP_AUTO_UPGRADE=1' };
|
|
878
|
+
}
|
|
879
|
+
const state = detect1xProjectState(options.cwd ?? process.cwd());
|
|
880
|
+
if (!state.isOneX) {
|
|
881
|
+
return { ran: false, reason: 'no 1.x project state detected' };
|
|
882
|
+
}
|
|
883
|
+
if (state.projectRoot === null) {
|
|
884
|
+
return { ran: false, reason: 'cwd is not a peaks project (no .peaks/_runtime/)' };
|
|
885
|
+
}
|
|
886
|
+
// The peaks binary should be on PATH after `npm i -g`.
|
|
887
|
+
// We shell out via spawnSync (synchronous; the postinstall
|
|
888
|
+
// is already synchronous and the umbrella is fast).
|
|
889
|
+
try {
|
|
890
|
+
const result = spawnSync('peaks', ['upgrade', '--to', '2.0', '--auto', '--project', state.projectRoot], {
|
|
891
|
+
encoding: 'utf8',
|
|
892
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
893
|
+
timeout: 120_000,
|
|
894
|
+
});
|
|
895
|
+
return {
|
|
896
|
+
ran: true,
|
|
897
|
+
reason: 'auto-upgrade dispatched',
|
|
898
|
+
signals: state.signals,
|
|
899
|
+
projectRoot: state.projectRoot,
|
|
900
|
+
exitCode: result.status,
|
|
901
|
+
stdout: result.stdout ?? '',
|
|
902
|
+
stderr: result.stderr ?? '',
|
|
903
|
+
};
|
|
904
|
+
} catch (err) {
|
|
905
|
+
return {
|
|
906
|
+
ran: true,
|
|
907
|
+
reason: 'auto-upgrade dispatched but failed',
|
|
908
|
+
signals: state.signals,
|
|
909
|
+
projectRoot: state.projectRoot,
|
|
910
|
+
error: err instanceof Error ? err.message : String(err),
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
|
|
660
915
|
if (process.argv[1] !== undefined && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) {
|
|
661
916
|
try {
|
|
662
|
-
|
|
917
|
+
// 2.0 fix for the 1.x Trae bug (per real user feedback
|
|
918
|
+
// 2026-06-11): iterate ALL 8 platforms, not just the
|
|
919
|
+
// auto-detected one. Per the "minimal-user-operation"
|
|
920
|
+
// tenet, the user should never have to run a
|
|
921
|
+
// per-platform install command.
|
|
922
|
+
const perPlatform = installBundledSkillsForAllPlatforms();
|
|
923
|
+
let totalInstalled = 0;
|
|
924
|
+
for (const p of perPlatform) {
|
|
925
|
+
totalInstalled += p.installed.length;
|
|
926
|
+
}
|
|
927
|
+
if (totalInstalled > 0) {
|
|
928
|
+
process.stdout.write(
|
|
929
|
+
`Peaks skills linked across ${perPlatform.length} platforms ` +
|
|
930
|
+
`(${totalInstalled} total symlinks)\n`
|
|
931
|
+
);
|
|
932
|
+
}
|
|
663
933
|
const outputStylesResult = installBundledOutputStyles();
|
|
664
934
|
let userConfigResult = createConfigResult({ skipped: true });
|
|
665
935
|
try {
|
|
@@ -668,12 +938,6 @@ if (process.argv[1] !== undefined && import.meta.url === pathToFileURL(resolve(p
|
|
|
668
938
|
const message = error instanceof Error ? error.message : String(error);
|
|
669
939
|
process.stderr.write(`Peaks user config was not installed: ${message}\n`);
|
|
670
940
|
}
|
|
671
|
-
if (skillsResult.installed.length > 0) {
|
|
672
|
-
process.stdout.write(`Peaks skills linked: ${skillsResult.installed.join(', ')}\n`);
|
|
673
|
-
}
|
|
674
|
-
if (skillsResult.skipped.length > 0) {
|
|
675
|
-
process.stderr.write(`Peaks skills skipped because local files already exist: ${skillsResult.skipped.join(', ')}\n`);
|
|
676
|
-
}
|
|
677
941
|
if (outputStylesResult.installed.length > 0) {
|
|
678
942
|
process.stdout.write(`Peaks output styles installed: ${outputStylesResult.installed.join(', ')}\n`);
|
|
679
943
|
}
|
|
@@ -683,6 +947,26 @@ if (process.argv[1] !== undefined && import.meta.url === pathToFileURL(resolve(p
|
|
|
683
947
|
if (userConfigResult.created) {
|
|
684
948
|
process.stdout.write('Peaks user config created: ~/.peaks/config.json\n');
|
|
685
949
|
}
|
|
950
|
+
|
|
951
|
+
// 2.0 postinstall: auto-detect 1.x project state in cwd
|
|
952
|
+
// and dispatch the upgrade umbrella. This makes the
|
|
953
|
+
// user's `npm i -g peaks-cli@2.0` truly one-key.
|
|
954
|
+
if (process.env.PEAKS_SKIP_AUTO_UPGRADE !== '1') {
|
|
955
|
+
// Fire-and-forget; the upgrade is async by design so
|
|
956
|
+
// the npm install output isn't blocked. We print a
|
|
957
|
+
// one-line hint so the user knows the auto-step
|
|
958
|
+
// happened.
|
|
959
|
+
autoUpgrade1xProjectIfPresent().then((result) => {
|
|
960
|
+
if (result.ran) {
|
|
961
|
+
process.stdout.write(
|
|
962
|
+
`\n✓ Detected 1.x peaks-cli project at ${result.projectRoot}\n` +
|
|
963
|
+
` → auto-upgraded to 2.0 (${result.signals?.length ?? 0} signals resolved)\n` +
|
|
964
|
+
` Run \`peaks audit red-lines --project .\` to verify.\n`
|
|
965
|
+
);
|
|
966
|
+
}
|
|
967
|
+
// When !result.ran we say nothing — silent on success.
|
|
968
|
+
});
|
|
969
|
+
}
|
|
686
970
|
if (userConfigResult.updated) {
|
|
687
971
|
process.stdout.write('Peaks user config updated: ~/.peaks/config.json\n');
|
|
688
972
|
}
|