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,129 @@
|
|
|
1
|
+
import type { OcrLlmConfig } from '../config/config-types.js';
|
|
2
|
+
export type OcrDetectState = 'ready' | 'package-missing' | 'binary-missing' | 'config-missing' | 'detection-failed';
|
|
3
|
+
export interface OcrDetectResult {
|
|
4
|
+
readonly state: OcrDetectState;
|
|
5
|
+
readonly packageInstalled: boolean;
|
|
6
|
+
readonly binaryPath: string | null;
|
|
7
|
+
readonly version: string | null;
|
|
8
|
+
/**
|
|
9
|
+
* The peaks-cli config path that holds `peaksConfig.ocr.llm`.
|
|
10
|
+
* The user pastes the `peaks code-review config-template` output
|
|
11
|
+
* here. Empty string when peaks-cli has not been bootstrapped.
|
|
12
|
+
*/
|
|
13
|
+
readonly configPath: string;
|
|
14
|
+
readonly configValid: boolean;
|
|
15
|
+
readonly missingKeys: readonly string[];
|
|
16
|
+
readonly warnings: readonly string[];
|
|
17
|
+
readonly nextActions: readonly string[];
|
|
18
|
+
}
|
|
19
|
+
export interface OcrReviewInput {
|
|
20
|
+
readonly projectRoot: string;
|
|
21
|
+
readonly from?: string;
|
|
22
|
+
readonly to?: string;
|
|
23
|
+
readonly commit?: string;
|
|
24
|
+
}
|
|
25
|
+
export interface OcrReviewResult {
|
|
26
|
+
readonly spawned: boolean;
|
|
27
|
+
readonly state: OcrDetectState;
|
|
28
|
+
readonly exitCode: number | null;
|
|
29
|
+
readonly stdout: string;
|
|
30
|
+
readonly stderr: string;
|
|
31
|
+
readonly durationMs: number;
|
|
32
|
+
/** Parsed JSON envelope if the ocr subprocess emitted valid JSON. */
|
|
33
|
+
readonly parsed: unknown;
|
|
34
|
+
readonly warnings: readonly string[];
|
|
35
|
+
readonly nextActions: readonly string[];
|
|
36
|
+
}
|
|
37
|
+
export interface SubprocessRunner {
|
|
38
|
+
run(command: string, args: readonly string[], options: {
|
|
39
|
+
cwd?: string;
|
|
40
|
+
timeoutMs: number;
|
|
41
|
+
env?: NodeJS.ProcessEnv;
|
|
42
|
+
}): {
|
|
43
|
+
status: number | null;
|
|
44
|
+
stdout: string;
|
|
45
|
+
stderr: string;
|
|
46
|
+
error?: string;
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Locate the ocr launcher script (`bin/ocr.js`) inside our own
|
|
51
|
+
* node_modules tree. Returns null when the npm package is not
|
|
52
|
+
* present (peaks-cli was installed but its dependency tree is
|
|
53
|
+
* corrupt, or the user removed it).
|
|
54
|
+
*
|
|
55
|
+
* Walks up from this file (dist/src/services/code-review/) to
|
|
56
|
+
* find the project root, then checks node_modules.
|
|
57
|
+
*/
|
|
58
|
+
export declare function resolveOcrLauncher(searchRoots: readonly string[]): string | null;
|
|
59
|
+
/**
|
|
60
|
+
* Resolve search roots for the ocr launcher. We look in two
|
|
61
|
+
* places: (1) the peaks-cli install root (next to our own dist/),
|
|
62
|
+
* (2) the user's cwd.
|
|
63
|
+
*/
|
|
64
|
+
export declare function defaultOcrSearchRoots(currentDirPath: string, cwd: string): readonly string[];
|
|
65
|
+
/**
|
|
66
|
+
* Validate the `peaksConfig.ocr.llm` block the caller (CLI / test)
|
|
67
|
+
* read out of `~/.peaks/config.json`. Returns the list of missing
|
|
68
|
+
* required keys (`url`, `authToken`, `model`); empty array means
|
|
69
|
+
* the block is ready to drive the ocr subprocess.
|
|
70
|
+
*
|
|
71
|
+
* The block is independent of the OCR package's own
|
|
72
|
+
* `~/.opencodereview/config.json` file. peaks-cli only ever reads
|
|
73
|
+
* from its own config; the env-var injection in `runOcrReview`
|
|
74
|
+
* makes the legacy file irrelevant.
|
|
75
|
+
*/
|
|
76
|
+
export declare function getOcrLlmMissingFields(llm: OcrLlmConfig | null): readonly string[];
|
|
77
|
+
/**
|
|
78
|
+
* Build the env-var overlay peaks-cli injects into the ocr
|
|
79
|
+
* subprocess. Maps `peaksConfig.ocr.llm` onto the OCR package's
|
|
80
|
+
* env-var surface (its highest-priority config source).
|
|
81
|
+
*/
|
|
82
|
+
export declare function buildOcrEnv(llm: OcrLlmConfig): NodeJS.ProcessEnv;
|
|
83
|
+
/**
|
|
84
|
+
* The JSON template the user pastes into their peaks-cli config
|
|
85
|
+
* (`peaksConfig.ocr.llm`). Returned as a stable string so the
|
|
86
|
+
* `peaks code-review config-template` CLI command can print it
|
|
87
|
+
* verbatim, and so the detector's `nextActions` payload embeds
|
|
88
|
+
* the same shape the user is told to add.
|
|
89
|
+
*/
|
|
90
|
+
export declare function getOcrConfigTemplate(): string;
|
|
91
|
+
export interface OcrDetectOptions {
|
|
92
|
+
readonly cwd: string;
|
|
93
|
+
/**
|
|
94
|
+
* The peaks-cli config path that holds `peaksConfig.ocr.llm`.
|
|
95
|
+
* Surfaced in the detect result so the user knows where to
|
|
96
|
+
* paste the template.
|
|
97
|
+
*/
|
|
98
|
+
readonly peaksConfigPath: string;
|
|
99
|
+
/**
|
|
100
|
+
* The parsed `peaksConfig.ocr.llm` block. `null` when the user
|
|
101
|
+
* has not yet populated the config; the detector surfaces the
|
|
102
|
+
* `config-missing` state with a templated `nextActions`.
|
|
103
|
+
*/
|
|
104
|
+
readonly peaksOcrConfig: OcrLlmConfig | null;
|
|
105
|
+
readonly searchRoots?: readonly string[];
|
|
106
|
+
readonly runner?: SubprocessRunner;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Detect the full ocr install + config state. The 5 reason states
|
|
110
|
+
* are unchanged from the soft-optional 2.0.0 contract; only the
|
|
111
|
+
* source of `config-missing` moved from `~/.opencodereview/config.json`
|
|
112
|
+
* to `peaksConfig.ocr.llm`.
|
|
113
|
+
*/
|
|
114
|
+
export declare function detectOcr(options: OcrDetectOptions): OcrDetectResult;
|
|
115
|
+
export interface OcrReviewOptions extends OcrDetectOptions {
|
|
116
|
+
readonly input: OcrReviewInput;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Run ocr review and return the structured result. Detects state
|
|
120
|
+
* first; soft-fails when ocr isn't ready (the caller — typically
|
|
121
|
+
* peaks-rd — proceeds without the second-opinion review).
|
|
122
|
+
*
|
|
123
|
+
* The LLM endpoint config from `peaksConfig.ocr.llm` is injected
|
|
124
|
+
* as env vars (`OCR_LLM_URL` / `OCR_LLM_TOKEN` / ...), which the
|
|
125
|
+
* ocr package treats as the highest-priority config source. This
|
|
126
|
+
* is how peaks-cli wires the user-managed config into the ocr
|
|
127
|
+
* subprocess without ever writing `~/.opencodereview/config.json`.
|
|
128
|
+
*/
|
|
129
|
+
export declare function runOcrReview(options: OcrReviewOptions): OcrReviewResult;
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Open Code Review (ocr) integration — soft-optional augmentation
|
|
3
|
+
* for peaks-rd's Gate B3 (code review evidence).
|
|
4
|
+
*
|
|
5
|
+
* Per the "skill-first / CLI-auxiliary" tenet, peaks-rd SKILL.md
|
|
6
|
+
* is the primary surface; this CLI primitive returns a structured
|
|
7
|
+
* JSON envelope the skill consumes to produce a second-opinion
|
|
8
|
+
* code review alongside its own LLM review.
|
|
9
|
+
*
|
|
10
|
+
* Mirrors the ECC 64-agents soft-optional pattern
|
|
11
|
+
* (`src/services/agent/ecc-agent-service.ts`):
|
|
12
|
+
* - same subprocessRunner injection seam (testability),
|
|
13
|
+
* - same multi-state reason enum (clear failure-mode reporting),
|
|
14
|
+
* - same soft-fail policy (never blocks the umbrella).
|
|
15
|
+
*
|
|
16
|
+
* === Source of truth: peaks-cli's own config.json ===
|
|
17
|
+
*
|
|
18
|
+
* peaks-cli does NOT auto-configure the LLM endpoint, and it does
|
|
19
|
+
* NOT write `~/.opencodereview/config.json`. The user is the only
|
|
20
|
+
* party that touches their LLM token / URL / model. The single
|
|
21
|
+
* discoverable place they declare those values is
|
|
22
|
+
* `peaksConfig.ocr.llm` under their `~/.peaks/config.json` (or
|
|
23
|
+
* `.peaks/config.json` in a project root).
|
|
24
|
+
*
|
|
25
|
+
* peaksConfig.ocr.llm.url → OCR_LLM_URL
|
|
26
|
+
* peaksConfig.ocr.llm.authToken → OCR_LLM_TOKEN
|
|
27
|
+
* peaksConfig.ocr.llm.model → OCR_LLM_MODEL
|
|
28
|
+
* peaksConfig.ocr.llm.useAnthropic → OCR_USE_ANTHROPIC
|
|
29
|
+
* peaksConfig.ocr.llm.authHeader → OCR_LLM_AUTH_HEADER
|
|
30
|
+
*
|
|
31
|
+
* When `runOcrReview` spawns the ocr subprocess it injects those
|
|
32
|
+
* values as env vars. The ocr package treats env vars as the
|
|
33
|
+
* highest-priority config source, so peaks-cli never has to
|
|
34
|
+
* materialise `~/.opencodereview/config.json` itself.
|
|
35
|
+
*
|
|
36
|
+
* To see the JSON template to paste, run:
|
|
37
|
+
* `peaks code-review config-template`
|
|
38
|
+
*
|
|
39
|
+
* The ocr package is declared in package.json:dependencies (was
|
|
40
|
+
* previously optionalDependencies) so `npm i -g peaks-cli@2.0.x`
|
|
41
|
+
* pulls it automatically (npm runs the ocr postinstall by default,
|
|
42
|
+
* which downloads the Go binary). pnpm-based installs need
|
|
43
|
+
* `pnpm approve-builds @alibaba-group/open-code-review`. Either
|
|
44
|
+
* way, peaks-cli detects the install state and reports it.
|
|
45
|
+
*/
|
|
46
|
+
import { spawnSync } from 'node:child_process';
|
|
47
|
+
import { existsSync } from 'node:fs';
|
|
48
|
+
import { join } from 'node:path';
|
|
49
|
+
import { fileURLToPath } from 'node:url';
|
|
50
|
+
import { dirname } from 'node:path';
|
|
51
|
+
const OCR_DETECT_TIMEOUT_MS = 5000;
|
|
52
|
+
const OCR_REVIEW_TIMEOUT_MS = 180000;
|
|
53
|
+
const OCR_INSTALL_HINT = 'Install: `npm i -g @alibaba-group/open-code-review` (it is a hard dependency of peaks-cli 2.0.x and ships in the regular `npm install` flow). Then add your LLM endpoint to ~/.peaks/config.json — run `peaks code-review config-template` for the JSON snippet to paste.';
|
|
54
|
+
const OCR_CONFIG_TEMPLATE = JSON.stringify({
|
|
55
|
+
ocr: {
|
|
56
|
+
llm: {
|
|
57
|
+
url: 'https://api.example.com/v1/messages',
|
|
58
|
+
authToken: '<your-api-key>',
|
|
59
|
+
model: 'claude-3-5-sonnet-latest',
|
|
60
|
+
useAnthropic: true,
|
|
61
|
+
authHeader: 'x-api-key'
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}, null, 2);
|
|
65
|
+
const DEFAULT_RUNNER = {
|
|
66
|
+
run(command, args, options) {
|
|
67
|
+
try {
|
|
68
|
+
const r = spawnSync(command, args, {
|
|
69
|
+
encoding: 'utf8',
|
|
70
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
71
|
+
timeout: options.timeoutMs,
|
|
72
|
+
cwd: options.cwd,
|
|
73
|
+
env: options.env,
|
|
74
|
+
});
|
|
75
|
+
return {
|
|
76
|
+
status: r.status,
|
|
77
|
+
stdout: r.stdout ?? '',
|
|
78
|
+
stderr: r.stderr ?? '',
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
return {
|
|
83
|
+
status: null,
|
|
84
|
+
stdout: '',
|
|
85
|
+
stderr: '',
|
|
86
|
+
error: err instanceof Error ? err.message : String(err),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
/**
|
|
92
|
+
* Locate the ocr launcher script (`bin/ocr.js`) inside our own
|
|
93
|
+
* node_modules tree. Returns null when the npm package is not
|
|
94
|
+
* present (peaks-cli was installed but its dependency tree is
|
|
95
|
+
* corrupt, or the user removed it).
|
|
96
|
+
*
|
|
97
|
+
* Walks up from this file (dist/src/services/code-review/) to
|
|
98
|
+
* find the project root, then checks node_modules.
|
|
99
|
+
*/
|
|
100
|
+
export function resolveOcrLauncher(searchRoots) {
|
|
101
|
+
const candidates = [];
|
|
102
|
+
for (const root of searchRoots) {
|
|
103
|
+
candidates.push(join(root, 'node_modules', '@alibaba-group', 'open-code-review', 'bin', 'ocr.js'));
|
|
104
|
+
}
|
|
105
|
+
for (const candidate of candidates) {
|
|
106
|
+
if (existsSync(candidate)) {
|
|
107
|
+
return candidate;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Resolve search roots for the ocr launcher. We look in two
|
|
114
|
+
* places: (1) the peaks-cli install root (next to our own dist/),
|
|
115
|
+
* (2) the user's cwd.
|
|
116
|
+
*/
|
|
117
|
+
export function defaultOcrSearchRoots(currentDirPath, cwd) {
|
|
118
|
+
// currentDirPath is dist/src/services/code-review/; walk up 4 to repo root.
|
|
119
|
+
const peaksRoot = join(currentDirPath, '..', '..', '..', '..');
|
|
120
|
+
return [peaksRoot, cwd];
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Validate the `peaksConfig.ocr.llm` block the caller (CLI / test)
|
|
124
|
+
* read out of `~/.peaks/config.json`. Returns the list of missing
|
|
125
|
+
* required keys (`url`, `authToken`, `model`); empty array means
|
|
126
|
+
* the block is ready to drive the ocr subprocess.
|
|
127
|
+
*
|
|
128
|
+
* The block is independent of the OCR package's own
|
|
129
|
+
* `~/.opencodereview/config.json` file. peaks-cli only ever reads
|
|
130
|
+
* from its own config; the env-var injection in `runOcrReview`
|
|
131
|
+
* makes the legacy file irrelevant.
|
|
132
|
+
*/
|
|
133
|
+
export function getOcrLlmMissingFields(llm) {
|
|
134
|
+
if (llm === null) {
|
|
135
|
+
return ['ocr.llm.url', 'ocr.llm.authToken', 'ocr.llm.model'];
|
|
136
|
+
}
|
|
137
|
+
const missing = [];
|
|
138
|
+
if (typeof llm.url !== 'string' || llm.url.length === 0)
|
|
139
|
+
missing.push('ocr.llm.url');
|
|
140
|
+
if (typeof llm.authToken !== 'string' || llm.authToken.length === 0)
|
|
141
|
+
missing.push('ocr.llm.authToken');
|
|
142
|
+
if (typeof llm.model !== 'string' || llm.model.length === 0)
|
|
143
|
+
missing.push('ocr.llm.model');
|
|
144
|
+
return missing;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Build the env-var overlay peaks-cli injects into the ocr
|
|
148
|
+
* subprocess. Maps `peaksConfig.ocr.llm` onto the OCR package's
|
|
149
|
+
* env-var surface (its highest-priority config source).
|
|
150
|
+
*/
|
|
151
|
+
export function buildOcrEnv(llm) {
|
|
152
|
+
const env = {};
|
|
153
|
+
if (typeof llm.url === 'string' && llm.url.length > 0)
|
|
154
|
+
env.OCR_LLM_URL = llm.url;
|
|
155
|
+
if (typeof llm.authToken === 'string' && llm.authToken.length > 0)
|
|
156
|
+
env.OCR_LLM_TOKEN = llm.authToken;
|
|
157
|
+
if (typeof llm.model === 'string' && llm.model.length > 0)
|
|
158
|
+
env.OCR_LLM_MODEL = llm.model;
|
|
159
|
+
if (typeof llm.useAnthropic === 'boolean')
|
|
160
|
+
env.OCR_USE_ANTHROPIC = String(llm.useAnthropic);
|
|
161
|
+
if (typeof llm.authHeader === 'string' && llm.authHeader.length > 0)
|
|
162
|
+
env.OCR_LLM_AUTH_HEADER = llm.authHeader;
|
|
163
|
+
return env;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* The JSON template the user pastes into their peaks-cli config
|
|
167
|
+
* (`peaksConfig.ocr.llm`). Returned as a stable string so the
|
|
168
|
+
* `peaks code-review config-template` CLI command can print it
|
|
169
|
+
* verbatim, and so the detector's `nextActions` payload embeds
|
|
170
|
+
* the same shape the user is told to add.
|
|
171
|
+
*/
|
|
172
|
+
export function getOcrConfigTemplate() {
|
|
173
|
+
return OCR_CONFIG_TEMPLATE;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Detect the full ocr install + config state. The 5 reason states
|
|
177
|
+
* are unchanged from the soft-optional 2.0.0 contract; only the
|
|
178
|
+
* source of `config-missing` moved from `~/.opencodereview/config.json`
|
|
179
|
+
* to `peaksConfig.ocr.llm`.
|
|
180
|
+
*/
|
|
181
|
+
export function detectOcr(options) {
|
|
182
|
+
const currentDirPath = dirname(fileURLToPath(import.meta.url));
|
|
183
|
+
const roots = options.searchRoots ?? defaultOcrSearchRoots(currentDirPath, options.cwd);
|
|
184
|
+
const launcher = resolveOcrLauncher(roots);
|
|
185
|
+
const warnings = [];
|
|
186
|
+
const nextActions = [];
|
|
187
|
+
if (launcher === null) {
|
|
188
|
+
const missing = getOcrLlmMissingFields(options.peaksOcrConfig);
|
|
189
|
+
return {
|
|
190
|
+
state: 'package-missing',
|
|
191
|
+
packageInstalled: false,
|
|
192
|
+
binaryPath: null,
|
|
193
|
+
version: null,
|
|
194
|
+
configPath: options.peaksConfigPath,
|
|
195
|
+
configValid: missing.length === 0,
|
|
196
|
+
missingKeys: missing,
|
|
197
|
+
warnings,
|
|
198
|
+
nextActions: [
|
|
199
|
+
'@alibaba-group/open-code-review is not installed in this project or peaks-cli root.',
|
|
200
|
+
OCR_INSTALL_HINT,
|
|
201
|
+
],
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
// Check whether the platform-specific binary downloaded
|
|
205
|
+
// successfully. The launcher's own check is identical:
|
|
206
|
+
// bin/opencodereview(.exe) next to bin/ocr.js.
|
|
207
|
+
const isWindows = process.platform === 'win32';
|
|
208
|
+
const binaryName = isWindows ? 'opencodereview.exe' : 'opencodereview';
|
|
209
|
+
const binaryPath = join(dirname(launcher), binaryName);
|
|
210
|
+
if (!existsSync(binaryPath)) {
|
|
211
|
+
const missing = getOcrLlmMissingFields(options.peaksOcrConfig);
|
|
212
|
+
return {
|
|
213
|
+
state: 'binary-missing',
|
|
214
|
+
packageInstalled: true,
|
|
215
|
+
binaryPath: null,
|
|
216
|
+
version: null,
|
|
217
|
+
configPath: options.peaksConfigPath,
|
|
218
|
+
configValid: missing.length === 0,
|
|
219
|
+
missingKeys: missing,
|
|
220
|
+
warnings: [
|
|
221
|
+
'ocr npm package is installed but the platform binary failed to download (likely network or postinstall blocked).',
|
|
222
|
+
],
|
|
223
|
+
nextActions: [
|
|
224
|
+
'For npm installs the binary downloads automatically; for pnpm run: `pnpm approve-builds @alibaba-group/open-code-review`.',
|
|
225
|
+
'Or run the installer directly: `node node_modules/@alibaba-group/open-code-review/scripts/install.js`.',
|
|
226
|
+
'Network-blocked installs can pre-download from https://github.com/alibaba/open-code-review/releases and place the binary at: ' + binaryPath,
|
|
227
|
+
],
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
// Probe the binary for its version. We invoke through the
|
|
231
|
+
// node launcher so the upstream's update-check + arg-parse
|
|
232
|
+
// logic stays canonical.
|
|
233
|
+
const runner = options.runner ?? DEFAULT_RUNNER;
|
|
234
|
+
const probe = runner.run('node', [launcher, 'version'], { timeoutMs: OCR_DETECT_TIMEOUT_MS });
|
|
235
|
+
let version = null;
|
|
236
|
+
if (probe.status === 0 && probe.stdout.length > 0) {
|
|
237
|
+
const match = /(\d+\.\d+\.\d+)/.exec(probe.stdout);
|
|
238
|
+
version = match !== null ? match[1] ?? null : probe.stdout.trim().slice(0, 32);
|
|
239
|
+
}
|
|
240
|
+
const missing = getOcrLlmMissingFields(options.peaksOcrConfig);
|
|
241
|
+
if (missing.length > 0) {
|
|
242
|
+
return {
|
|
243
|
+
state: 'config-missing',
|
|
244
|
+
packageInstalled: true,
|
|
245
|
+
binaryPath,
|
|
246
|
+
version,
|
|
247
|
+
configPath: options.peaksConfigPath,
|
|
248
|
+
configValid: false,
|
|
249
|
+
missingKeys: missing,
|
|
250
|
+
warnings: [
|
|
251
|
+
`ocr is installed but peaks-cli's ocr.llm config is incomplete: missing ${missing.join(', ')}.`,
|
|
252
|
+
],
|
|
253
|
+
nextActions: [
|
|
254
|
+
`Paste the following into ${options.peaksConfigPath} under "ocr.llm":`,
|
|
255
|
+
OCR_CONFIG_TEMPLATE,
|
|
256
|
+
'Or run `peaks code-review config-template` to print the snippet again.',
|
|
257
|
+
'Until configured, peaks-rd skips the ocr second-opinion step and proceeds with its own LLM review only.',
|
|
258
|
+
],
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
return {
|
|
262
|
+
state: 'ready',
|
|
263
|
+
packageInstalled: true,
|
|
264
|
+
binaryPath,
|
|
265
|
+
version,
|
|
266
|
+
configPath: options.peaksConfigPath,
|
|
267
|
+
configValid: true,
|
|
268
|
+
missingKeys: [],
|
|
269
|
+
warnings,
|
|
270
|
+
nextActions,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Run ocr review and return the structured result. Detects state
|
|
275
|
+
* first; soft-fails when ocr isn't ready (the caller — typically
|
|
276
|
+
* peaks-rd — proceeds without the second-opinion review).
|
|
277
|
+
*
|
|
278
|
+
* The LLM endpoint config from `peaksConfig.ocr.llm` is injected
|
|
279
|
+
* as env vars (`OCR_LLM_URL` / `OCR_LLM_TOKEN` / ...), which the
|
|
280
|
+
* ocr package treats as the highest-priority config source. This
|
|
281
|
+
* is how peaks-cli wires the user-managed config into the ocr
|
|
282
|
+
* subprocess without ever writing `~/.opencodereview/config.json`.
|
|
283
|
+
*/
|
|
284
|
+
export function runOcrReview(options) {
|
|
285
|
+
const detect = detectOcr(options);
|
|
286
|
+
if (detect.state !== 'ready') {
|
|
287
|
+
return {
|
|
288
|
+
spawned: false,
|
|
289
|
+
state: detect.state,
|
|
290
|
+
exitCode: null,
|
|
291
|
+
stdout: '',
|
|
292
|
+
stderr: '',
|
|
293
|
+
durationMs: 0,
|
|
294
|
+
parsed: null,
|
|
295
|
+
warnings: detect.warnings,
|
|
296
|
+
nextActions: detect.nextActions,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
// Resolve the launcher path again (we know it exists because detect.state === 'ready')
|
|
300
|
+
const currentDirPath = dirname(fileURLToPath(import.meta.url));
|
|
301
|
+
const roots = options.searchRoots ?? defaultOcrSearchRoots(currentDirPath, options.cwd);
|
|
302
|
+
const launcher = resolveOcrLauncher(roots);
|
|
303
|
+
if (launcher === null) {
|
|
304
|
+
// Should never happen given state === 'ready', but stay safe.
|
|
305
|
+
return {
|
|
306
|
+
spawned: false,
|
|
307
|
+
state: 'detection-failed',
|
|
308
|
+
exitCode: null,
|
|
309
|
+
stdout: '',
|
|
310
|
+
stderr: 'ocr launcher disappeared between detect and run',
|
|
311
|
+
durationMs: 0,
|
|
312
|
+
parsed: null,
|
|
313
|
+
warnings: ['ocr was detected as ready but the launcher path is no longer resolvable.'],
|
|
314
|
+
nextActions: ['Re-run `peaks code-review detect-ocr --json` to refresh.'],
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
// Inject the LLM endpoint config from peaks-cli's config.json
|
|
318
|
+
// as env vars — the ocr package's highest-priority config path.
|
|
319
|
+
const env = options.peaksOcrConfig === null
|
|
320
|
+
? process.env
|
|
321
|
+
: { ...process.env, ...buildOcrEnv(options.peaksOcrConfig) };
|
|
322
|
+
const args = ['review', '--format', 'json'];
|
|
323
|
+
if (typeof options.input.from === 'string' && options.input.from.length > 0) {
|
|
324
|
+
args.push('--from', options.input.from);
|
|
325
|
+
}
|
|
326
|
+
if (typeof options.input.to === 'string' && options.input.to.length > 0) {
|
|
327
|
+
args.push('--to', options.input.to);
|
|
328
|
+
}
|
|
329
|
+
if (typeof options.input.commit === 'string' && options.input.commit.length > 0) {
|
|
330
|
+
args.push('--commit', options.input.commit);
|
|
331
|
+
}
|
|
332
|
+
const runner = options.runner ?? DEFAULT_RUNNER;
|
|
333
|
+
const start = Date.now();
|
|
334
|
+
const r = runner.run('node', [launcher, ...args], {
|
|
335
|
+
cwd: options.input.projectRoot,
|
|
336
|
+
timeoutMs: OCR_REVIEW_TIMEOUT_MS,
|
|
337
|
+
env,
|
|
338
|
+
});
|
|
339
|
+
const durationMs = Date.now() - start;
|
|
340
|
+
let parsed = null;
|
|
341
|
+
if (r.status === 0 && r.stdout.length > 0) {
|
|
342
|
+
try {
|
|
343
|
+
parsed = JSON.parse(r.stdout);
|
|
344
|
+
}
|
|
345
|
+
catch {
|
|
346
|
+
// Leave parsed=null; the caller can read raw stdout.
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return {
|
|
350
|
+
spawned: true,
|
|
351
|
+
state: 'ready',
|
|
352
|
+
exitCode: r.status,
|
|
353
|
+
stdout: r.stdout,
|
|
354
|
+
stderr: r.stderr,
|
|
355
|
+
durationMs,
|
|
356
|
+
parsed,
|
|
357
|
+
warnings: r.status === 0 ? [] : [`ocr review exited with status ${r.status}`],
|
|
358
|
+
nextActions: r.status === 0
|
|
359
|
+
? []
|
|
360
|
+
: ['Inspect stderr for the failure. Re-run `peaks code-review detect-ocr --json` to verify config still valid.'],
|
|
361
|
+
};
|
|
362
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export declare const CONFIG_SCHEMA_VERSION_V2 = "2.0.0";
|
|
2
|
+
/**
|
|
3
|
+
* Per-project fields that the 1.x → 2.0 migration moves
|
|
4
|
+
* from `~/.peaks/config.json` (global) to `<project>/.peaks/preferences.json`
|
|
5
|
+
* (per-project). Per spec §10.4 line 1215: "economyMode / swarmMode
|
|
6
|
+
* 字段从 global 迁移到当前 workspace 的 .peaks/preferences.json".
|
|
7
|
+
*/
|
|
8
|
+
declare const PER_PROJECT_FIELDS: readonly ["economyMode", "swarmMode"];
|
|
9
|
+
type PerProjectField = (typeof PER_PROJECT_FIELDS)[number];
|
|
10
|
+
export interface MigrationOptions {
|
|
11
|
+
currentProjectRoot: string;
|
|
12
|
+
}
|
|
13
|
+
export interface MigrationPlan {
|
|
14
|
+
alreadyAtV2: boolean;
|
|
15
|
+
detectedSchemaVersion: string | null;
|
|
16
|
+
newConfigSchemaVersion: string;
|
|
17
|
+
/** Field names that would be migrated on apply (1.x → 2.0). */
|
|
18
|
+
willMigrateFields: readonly PerProjectField[];
|
|
19
|
+
}
|
|
20
|
+
export interface MigrationResult extends MigrationPlan {
|
|
21
|
+
applied: boolean;
|
|
22
|
+
backupPath?: string;
|
|
23
|
+
newConfigPath?: string;
|
|
24
|
+
error?: string;
|
|
25
|
+
}
|
|
26
|
+
export declare function globalConfigPath(): string;
|
|
27
|
+
export declare function backupConfigPath(): string;
|
|
28
|
+
export declare function planMigration(opts: MigrationOptions): MigrationPlan;
|
|
29
|
+
export declare function executeMigration(opts: MigrationOptions & {
|
|
30
|
+
apply: boolean;
|
|
31
|
+
}): MigrationResult;
|
|
32
|
+
export {};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { savePreferences } from '../preferences/preferences-service.js';
|
|
5
|
+
export const CONFIG_SCHEMA_VERSION_V2 = '2.0.0';
|
|
6
|
+
const BACKUP_NAME = 'config.json.1.x.bak';
|
|
7
|
+
/**
|
|
8
|
+
* Per-project fields that the 1.x → 2.0 migration moves
|
|
9
|
+
* from `~/.peaks/config.json` (global) to `<project>/.peaks/preferences.json`
|
|
10
|
+
* (per-project). Per spec §10.4 line 1215: "economyMode / swarmMode
|
|
11
|
+
* 字段从 global 迁移到当前 workspace 的 .peaks/preferences.json".
|
|
12
|
+
*/
|
|
13
|
+
const PER_PROJECT_FIELDS = ['economyMode', 'swarmMode'];
|
|
14
|
+
export function globalConfigPath() {
|
|
15
|
+
return join(homedir(), '.peaks', 'config.json');
|
|
16
|
+
}
|
|
17
|
+
export function backupConfigPath() {
|
|
18
|
+
return join(homedir(), '.peaks', BACKUP_NAME);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Inspect the on-disk 1.x config and return the subset of
|
|
22
|
+
* per-project fields that are present and would be migrated on
|
|
23
|
+
* apply. The list is used both by `planMigration` (to populate
|
|
24
|
+
* `willMigrateFields`) and by `executeMigration` (to know which
|
|
25
|
+
* fields to forward to `savePreferences`).
|
|
26
|
+
*/
|
|
27
|
+
function discoverMigratableFields(raw) {
|
|
28
|
+
const out = [];
|
|
29
|
+
for (const f of PER_PROJECT_FIELDS) {
|
|
30
|
+
if (typeof raw[f] === 'boolean')
|
|
31
|
+
out.push(f);
|
|
32
|
+
}
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
export function planMigration(opts) {
|
|
36
|
+
const configPath = globalConfigPath();
|
|
37
|
+
let detectedSchemaVersion = null;
|
|
38
|
+
let willMigrateFields = [];
|
|
39
|
+
if (existsSync(configPath)) {
|
|
40
|
+
const raw = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
41
|
+
detectedSchemaVersion = raw.version ?? null;
|
|
42
|
+
willMigrateFields = discoverMigratableFields(raw);
|
|
43
|
+
}
|
|
44
|
+
if (detectedSchemaVersion === CONFIG_SCHEMA_VERSION_V2) {
|
|
45
|
+
return {
|
|
46
|
+
alreadyAtV2: true,
|
|
47
|
+
detectedSchemaVersion,
|
|
48
|
+
newConfigSchemaVersion: CONFIG_SCHEMA_VERSION_V2,
|
|
49
|
+
willMigrateFields: [],
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
alreadyAtV2: false,
|
|
54
|
+
detectedSchemaVersion,
|
|
55
|
+
newConfigSchemaVersion: CONFIG_SCHEMA_VERSION_V2,
|
|
56
|
+
willMigrateFields,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
export function executeMigration(opts) {
|
|
60
|
+
const configPath = globalConfigPath();
|
|
61
|
+
if (!existsSync(configPath)) {
|
|
62
|
+
throw new Error('NO_CONFIG: ~/.peaks/config.json not found');
|
|
63
|
+
}
|
|
64
|
+
const plan = planMigration(opts);
|
|
65
|
+
if (plan.alreadyAtV2) {
|
|
66
|
+
return { ...plan, applied: false };
|
|
67
|
+
}
|
|
68
|
+
if (!opts.apply) {
|
|
69
|
+
return { ...plan, applied: false };
|
|
70
|
+
}
|
|
71
|
+
const original = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
72
|
+
// 1. Backup
|
|
73
|
+
const bak = backupConfigPath();
|
|
74
|
+
writeFileSync(bak, JSON.stringify(original, null, 2) + '\n', 'utf8');
|
|
75
|
+
// 2. Forward per-project fields to <projectRoot>/.peaks/preferences.json
|
|
76
|
+
// (per spec §10.4). The preferences service merges over the existing
|
|
77
|
+
// schema-valid preferences.json; boolean fields only, with the
|
|
78
|
+
// discoverMigratableFields() allowlist as the source of truth.
|
|
79
|
+
if (plan.willMigrateFields.length > 0) {
|
|
80
|
+
const overrides = {};
|
|
81
|
+
for (const f of plan.willMigrateFields) {
|
|
82
|
+
const v = original[f];
|
|
83
|
+
if (typeof v === 'boolean')
|
|
84
|
+
overrides[f] = v;
|
|
85
|
+
}
|
|
86
|
+
savePreferences(opts.currentProjectRoot, overrides);
|
|
87
|
+
}
|
|
88
|
+
// 3. Slim config.json — only the schema version remains.
|
|
89
|
+
mkdirSync(join(homedir(), '.peaks'), { recursive: true });
|
|
90
|
+
writeFileSync(configPath, JSON.stringify({ version: CONFIG_SCHEMA_VERSION_V2 }, null, 2) + '\n', 'utf8');
|
|
91
|
+
return { ...plan, applied: true, backupPath: bak, newConfigPath: configPath };
|
|
92
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { backupConfigPath } from './config-migration.js';
|
|
5
|
+
/**
|
|
6
|
+
* Spec §8.6 — restore a single archived field from `config.json.1.x.bak`.
|
|
7
|
+
* Does NOT modify `config.json` itself (which is now slim v2).
|
|
8
|
+
* Instead writes a sidecar `config.json.restore-<field>.json` so the user
|
|
9
|
+
* can review before adopting. Fields in the deferred-design set
|
|
10
|
+
* (workspaces, providers, proxy) throw RESTORE_GUARDED so the user has
|
|
11
|
+
* to acknowledge explicitly.
|
|
12
|
+
*/
|
|
13
|
+
const GUARDED_FIELDS = new Set(['workspaces', 'providers', 'proxy']);
|
|
14
|
+
function readBakContent() {
|
|
15
|
+
const bak = backupConfigPath();
|
|
16
|
+
if (!existsSync(bak)) {
|
|
17
|
+
throw new Error('NO_BACKUP: ~/.peaks/config.json.1.x.bak not found');
|
|
18
|
+
}
|
|
19
|
+
return JSON.parse(readFileSync(bak, 'utf8'));
|
|
20
|
+
}
|
|
21
|
+
export function listAvailableFields() {
|
|
22
|
+
const bak = readBakContent();
|
|
23
|
+
return Object.keys(bak).filter((k) => k !== 'version');
|
|
24
|
+
}
|
|
25
|
+
export function restoreField(opts) {
|
|
26
|
+
const bak = readBakContent();
|
|
27
|
+
if (!(opts.field in bak)) {
|
|
28
|
+
throw new Error(`FIELD_NOT_FOUND: ${opts.field} is not in config.json.1.x.bak`);
|
|
29
|
+
}
|
|
30
|
+
if (GUARDED_FIELDS.has(opts.field)) {
|
|
31
|
+
throw new Error(`RESTORE_GUARDED: restoring "${opts.field}" is discouraged because the deferred design intentionally avoids it; add it to config.json manually if you really need it`);
|
|
32
|
+
}
|
|
33
|
+
const home = homedir();
|
|
34
|
+
const sidecar = join(home, '.peaks', `config.json.restore-${opts.field}.json`);
|
|
35
|
+
if (!opts.apply) {
|
|
36
|
+
return { field: opts.field, applied: false };
|
|
37
|
+
}
|
|
38
|
+
mkdirSync(join(home, '.peaks'), { recursive: true });
|
|
39
|
+
const payload = {
|
|
40
|
+
field: opts.field,
|
|
41
|
+
value: bak[opts.field],
|
|
42
|
+
source: 'config.json.1.x.bak',
|
|
43
|
+
restoredAt: new Date().toISOString(),
|
|
44
|
+
};
|
|
45
|
+
writeFileSync(sidecar, JSON.stringify(payload, null, 2) + '\n', 'utf8');
|
|
46
|
+
return { field: opts.field, applied: true, sidecarPath: sidecar };
|
|
47
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface RollbackPlan {
|
|
2
|
+
available: boolean;
|
|
3
|
+
detectedVersion: string | null;
|
|
4
|
+
backupPath: string;
|
|
5
|
+
}
|
|
6
|
+
export interface RollbackResult extends RollbackPlan {
|
|
7
|
+
applied: boolean;
|
|
8
|
+
restoredConfigPath?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function planRollback(): RollbackPlan;
|
|
11
|
+
export declare function executeRollback(opts: {
|
|
12
|
+
apply: boolean;
|
|
13
|
+
}): RollbackResult;
|