create-harness-vibe-coding 0.8.7 → 0.8.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README-CN.md +163 -105
- package/README.md +179 -244
- package/bin/create-harness-vibe-coding.js +2 -2
- package/docs/images/harness-architecture-light.png +0 -0
- package/docs/images/harness-architecture.drawio +164 -0
- package/docs/images/harness-icon.png +0 -0
- package/package.json +47 -44
- package/src/generator.js +41 -5
- package/src/index.js +86 -13
- package/src/prompts.js +37 -37
- package/templates/common/.claude/agents/architect-manager.md +45 -45
- package/templates/common/.claude/agents/architect.md +31 -31
- package/templates/common/.claude/agents/codebase-explorer.md +45 -0
- package/templates/common/.claude/agents/context-master.md +75 -75
- package/templates/common/.claude/agents/debugger.md +41 -41
- package/templates/common/.claude/agents/docs-researcher.md +41 -41
- package/templates/common/.claude/agents/explore-manager.md +41 -41
- package/templates/common/.claude/agents/implement-manager.md +49 -49
- package/templates/common/.claude/agents/implementer.md +40 -40
- package/templates/common/.claude/agents/memory-master.md +82 -64
- package/templates/common/.claude/agents/planner.md +34 -34
- package/templates/common/.claude/agents/researcher.md +41 -41
- package/templates/common/.claude/agents/review-manager.md +56 -56
- package/templates/common/.claude/agents/reviewer.md +34 -34
- package/templates/common/.claude/agents/task-scribe.md +70 -0
- package/templates/common/.claude/agents/verifier.md +29 -29
- package/templates/common/.claude/commands/wf-help.md +9 -5
- package/templates/common/.claude/commands/wf-update.md +24 -0
- package/templates/common/.claude/rules/ecc/common.md +57 -44
- package/templates/common/.claude/settings.json +13 -0
- package/templates/common/.claude/skills/subagent-orchestrator/SKILL.md +8 -4
- package/templates/common/.claude/skills/wf/SKILL.md +15 -8
- package/templates/common/.claude/skills/wf-auto/SKILL.md +10 -7
- package/templates/common/.claude/skills/wf-learn/SKILL.md +9 -2
- package/templates/common/.claude/skills/wf-max/SKILL.md +23 -5
- package/templates/common/.claude/skills/wf-readme/SKILL.md +49 -49
- package/templates/common/.claude/skills/wf-remove/SKILL.md +7 -7
- package/templates/common/.claude/skills/wf-update/SKILL.md +15 -2
- package/templates/common/.codex/hooks.json +17 -0
- package/templates/common/.harness-version +130 -45
- package/templates/common/.opencode/agents/architect-manager.md +52 -0
- package/templates/common/.opencode/agents/architect.md +35 -0
- package/templates/common/.opencode/agents/codebase-explorer.md +45 -0
- package/templates/common/.opencode/agents/context-master.md +81 -0
- package/templates/common/.opencode/agents/debugger.md +43 -0
- package/templates/common/.opencode/agents/docs-researcher.md +42 -0
- package/templates/common/.opencode/agents/explore-manager.md +49 -0
- package/templates/common/.opencode/agents/implement-manager.md +56 -0
- package/templates/common/.opencode/agents/implementer.md +42 -0
- package/templates/common/.opencode/agents/memory-master.md +88 -0
- package/templates/common/.opencode/agents/planner.md +38 -0
- package/templates/common/.opencode/agents/reflector.md +39 -0
- package/templates/common/.opencode/agents/researcher.md +42 -0
- package/templates/common/.opencode/agents/review-manager.md +63 -0
- package/templates/common/.opencode/agents/reviewer.md +37 -0
- package/templates/common/.opencode/agents/task-scribe.md +70 -0
- package/templates/common/.opencode/agents/tdd-guide.md +83 -0
- package/templates/common/.opencode/agents/test-writer.md +54 -0
- package/templates/common/.opencode/agents/verifier.md +37 -0
- package/templates/common/.opencode/commands/wf-auto-spark.md +15 -0
- package/templates/common/.opencode/commands/wf-auto.md +15 -0
- package/templates/common/.opencode/commands/wf-help.md +27 -0
- package/templates/common/.opencode/commands/wf-learn.md +15 -0
- package/templates/common/.opencode/commands/wf-max.md +15 -0
- package/templates/common/.opencode/commands/wf-readme.md +15 -0
- package/templates/common/.opencode/commands/wf-remove.md +15 -0
- package/templates/common/.opencode/commands/wf-review.md +15 -0
- package/templates/common/.opencode/commands/wf-update.md +24 -0
- package/templates/common/.opencode/commands/wf.md +15 -0
- package/templates/common/.opencode/plugins/harness-wf-status.mjs +135 -0
- package/templates/common/AGENTS.md +2 -29
- package/templates/common/CLAUDE.md +114 -88
- package/templates/common/Harness/ACCEPTANCE_PROTOCOL.md +2 -2
- package/templates/common/{MEMORY.md → Harness/MEMORY.md} +17 -4
- package/templates/common/Harness/MEMORY_PROTOCOL.md +80 -30
- package/templates/common/Harness/PROGRESS.md +17 -17
- package/templates/common/Harness/README.md +58 -19
- package/templates/common/{SETUP.md → Harness/SETUP.md} +278 -276
- package/templates/common/Harness/TASK_ARCHIVE.md +56 -0
- package/templates/common/Harness/WF-AUTO-ANGLES.md +170 -0
- package/templates/common/Harness/WF-AUTO-SPARK.md +10 -19
- package/templates/common/Harness/WF-AUTO.md +93 -167
- package/templates/common/Harness/WF-KERNEL.md +189 -0
- package/templates/common/Harness/WF-MAX.md +60 -328
- package/templates/common/Harness/WF-STATE.md +83 -0
- package/templates/common/Harness/WF.md +117 -237
- package/templates/common/Harness/agent-workflow.md +2 -2
- package/templates/common/Harness/architecture.md +124 -124
- package/templates/common/Harness/context-loading.md +111 -111
- package/templates/common/Harness/dispatch.md +43 -35
- package/templates/common/Harness/extension.md +66 -66
- package/templates/common/Harness/lifecycle.md +20 -20
- package/templates/common/Harness/research/PRD.md +56 -56
- package/templates/common/Harness/research/README.md +169 -169
- package/templates/common/Harness/research/research-results.md +66 -66
- package/templates/common/Harness/scripts/archive-tasks.mjs +239 -0
- package/templates/common/{scripts → Harness/scripts}/scan-clean.mjs +443 -416
- package/templates/common/{scripts → Harness/scripts}/validate-harness.mjs +691 -452
- package/templates/common/Harness/scripts/wf-auto-update-prompt.mjs +258 -0
- package/templates/common/{scripts → Harness/scripts}/wf-remove.mjs +56 -39
- package/templates/common/{scripts → Harness/scripts}/wf-update-check.mjs +632 -599
- package/templates/common/Harness/subagents.md +215 -214
- package/templates/common/Harness/tasks/_template/ARTIFACTS.md +2 -2
- package/templates/common/Harness/tasks/_template/NOTES.md +2 -2
- package/templates/common/Harness/tasks/_template/PLAN.md +5 -0
- package/templates/common/Harness/tasks/_template/STATE.json +23 -0
- package/templates/common/README.md +37 -37
- package/templates/common/memory/agent-lessons-patterns.md +22 -21
- package/templates/common/memory/routes.md +43 -0
- package/templates/common/memory/startup-hints.md +32 -0
- package/templates/common/memory/tool-usage-reflections.md +22 -21
- package/templates/common/memory/user-corrections-preferences.md +23 -21
- package/templates/common/opencode.json +19 -0
- package/templates/optional/catalog.json +49 -33
- package/templates/optional/skills/browser-e2e/.claude/skills/browser-e2e/SKILL.md +42 -42
- package/templates/optional/skills/browser-e2e/.claude/skills/wf-browser/SKILL.md +193 -193
- package/templates/optional/skills/browser-e2e/.opencode/commands/wf-browser.md +15 -0
- package/templates/optional/skills/browser-e2e/Harness/workflows/browser-e2e.md +48 -48
- package/templates/optional/skills/github-pr-review/.claude/skills/github-pr-review/SKILL.md +40 -40
- package/templates/optional/skills/github-pr-review/Harness/workflows/github-pr-review.md +28 -28
- package/templates/optional/skills/python-backend/.claude/skills/python-backend/SKILL.md +40 -40
- package/templates/optional/skills/python-backend/Harness/workflows/python-backend.md +34 -34
- package/templates/optional/skills/ts-react-frontend/.claude/skills/ts-react-frontend/SKILL.md +43 -43
- package/templates/optional/skills/ts-react-frontend/Harness/workflows/ts-react-frontend.md +34 -34
- package/templates/optional/skills/ui-ux-review/.claude/skills/ui-ux-review/SKILL.md +40 -40
- package/templates/optional/skills/ui-ux-review/Harness/workflows/ui-ux-review.md +26 -26
|
@@ -1,43 +1,47 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* wf-update-check.mjs — Fast harness update comparison.
|
|
4
|
-
* Fetches remote checksums, compares locally, classifies all files instantly.
|
|
5
|
-
* Only CONFLICT files need AI/user decision.
|
|
6
|
-
*
|
|
7
|
-
* Usage:
|
|
8
|
-
* node Harness/scripts/wf-update-check.mjs # full dry-run plan
|
|
9
|
-
* node Harness/scripts/wf-update-check.mjs --json # JSON output for AI consumption
|
|
10
|
-
* node Harness/scripts/wf-update-check.mjs --
|
|
11
|
-
* node Harness/scripts/wf-update-check.mjs --
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
/^Harness\/
|
|
33
|
-
/^Harness\/
|
|
34
|
-
/^
|
|
35
|
-
|
|
36
|
-
/^
|
|
37
|
-
/^
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* wf-update-check.mjs — Fast harness update comparison.
|
|
4
|
+
* Fetches remote checksums, compares locally, classifies all files instantly.
|
|
5
|
+
* Only CONFLICT files need AI/user decision.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* node Harness/scripts/wf-update-check.mjs # full dry-run plan
|
|
9
|
+
* node Harness/scripts/wf-update-check.mjs --json # JSON output for AI consumption
|
|
10
|
+
* node Harness/scripts/wf-update-check.mjs --json # summary/counts + short agent hints only (token-safe)
|
|
11
|
+
* node Harness/scripts/wf-update-check.mjs --json --full-plan # also include full plan + conflict details (verbose)
|
|
12
|
+
* node Harness/scripts/wf-update-check.mjs --apply-safe # apply SAFE+NEW, leave CONFLICT for AI
|
|
13
|
+
* node Harness/scripts/wf-update-check.mjs --apply # apply only when no CONFLICT exists
|
|
14
|
+
*
|
|
15
|
+
* --full-plan and --verbose are aliases.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, lstatSync } from 'fs';
|
|
19
|
+
import { createHash } from 'crypto';
|
|
20
|
+
import { resolve, dirname, sep } from 'path';
|
|
21
|
+
import { fileURLToPath } from 'url';
|
|
22
|
+
|
|
23
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
24
|
+
const ROOT = process.env.WF_ROOT ? resolve(process.env.WF_ROOT) : resolve(__dirname, '..', '..');
|
|
25
|
+
const VERSION_FILE = resolve(ROOT, 'Harness', '.harness-version');
|
|
26
|
+
const DEFAULT_SOURCE_BASE = 'https://raw.githubusercontent.com/zingspark/create-harness-vibe-coding/main/templates/common/';
|
|
27
|
+
|
|
28
|
+
// ── Tier classification ──────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
/** Files we NEVER overwrite or delete. */
|
|
31
|
+
const PRESERVE_PATTERNS = [
|
|
32
|
+
/^Harness\/PROGRESS\.md$/,
|
|
33
|
+
/^Harness\/tasks\//,
|
|
34
|
+
/^Harness\/memory\//,
|
|
35
|
+
/^Harness\/research\/PRD\.md$/,
|
|
36
|
+
/^Harness\/research\/research-results\.md$/,
|
|
37
|
+
/^Harness\/architecture\.md$/,
|
|
38
|
+
/^README\.md$/,
|
|
39
|
+
/^\.gitignore$/,
|
|
40
|
+
/^package\.json$/,
|
|
41
|
+
/^package-lock\.json$/,
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
/** Files that are safe to overwrite if checksums match, otherwise need merge. */
|
|
41
45
|
const MERGE_PATTERNS = [
|
|
42
46
|
/^CLAUDE\.md$/,
|
|
43
47
|
/^AGENTS\.md$/,
|
|
@@ -51,48 +55,48 @@ const OPTIONAL_REGISTRATION_FILES = new Set([
|
|
|
51
55
|
'Harness/MEMORY.md',
|
|
52
56
|
'Harness/README.md',
|
|
53
57
|
]);
|
|
54
|
-
|
|
55
|
-
// ── Helpers ────────────────────────────────────────────────────────
|
|
56
|
-
|
|
57
|
-
/** Reject paths that escape ROOT (traversal, absolute, .., etc.). */
|
|
58
|
-
function safePath(file) {
|
|
59
|
-
let normalized = file.replace(/\\/g, '/').replace(/^\/+/, '');
|
|
60
|
-
if (/\/\//.test(normalized)) return null;
|
|
61
|
-
if (normalized.split('/').some(p => p === '..')) return null;
|
|
62
|
-
if (file.startsWith('/') || file.startsWith('\\')) return null;
|
|
63
|
-
if (normalized === '.' || normalized === '') return null;
|
|
64
|
-
const resolved = resolve(ROOT, normalized);
|
|
65
|
-
if (!resolved.startsWith(ROOT + sep) && resolved !== ROOT) return null;
|
|
66
|
-
return resolved;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** Canonical normalization for classification matching. */
|
|
70
|
-
function canonicalPath(file) {
|
|
71
|
-
return file.replace(/\\/g, '/').replace(/\/+/g, '/');
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function readFlagValue(args, flagName) {
|
|
75
|
-
const idx = args.indexOf(flagName);
|
|
76
|
-
if (idx === -1) return null;
|
|
77
|
-
const value = args[idx + 1];
|
|
78
|
-
if (!value || value.startsWith('--')) return null;
|
|
79
|
-
return value;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function readRepeatedFlagValues(args, flagName) {
|
|
83
|
-
const values = [];
|
|
84
|
-
for (let i = 0; i < args.length; i += 1) {
|
|
85
|
-
const arg = args[i];
|
|
86
|
-
if (arg === flagName && args[i + 1] && !args[i + 1].startsWith('--')) {
|
|
87
|
-
values.push(args[i + 1]);
|
|
88
|
-
i += 1;
|
|
89
|
-
} else if (arg.startsWith(`${flagName}=`)) {
|
|
90
|
-
values.push(arg.slice(flagName.length + 1));
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
return [...new Set(values.map(canonicalPath).filter(Boolean))];
|
|
94
|
-
}
|
|
95
|
-
|
|
58
|
+
|
|
59
|
+
// ── Helpers ────────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
/** Reject paths that escape ROOT (traversal, absolute, .., etc.). */
|
|
62
|
+
function safePath(file) {
|
|
63
|
+
let normalized = file.replace(/\\/g, '/').replace(/^\/+/, '');
|
|
64
|
+
if (/\/\//.test(normalized)) return null;
|
|
65
|
+
if (normalized.split('/').some(p => p === '..')) return null;
|
|
66
|
+
if (file.startsWith('/') || file.startsWith('\\')) return null;
|
|
67
|
+
if (normalized === '.' || normalized === '') return null;
|
|
68
|
+
const resolved = resolve(ROOT, normalized);
|
|
69
|
+
if (!resolved.startsWith(ROOT + sep) && resolved !== ROOT) return null;
|
|
70
|
+
return resolved;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Canonical normalization for classification matching. */
|
|
74
|
+
function canonicalPath(file) {
|
|
75
|
+
return file.replace(/\\/g, '/').replace(/\/+/g, '/');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function readFlagValue(args, flagName) {
|
|
79
|
+
const idx = args.indexOf(flagName);
|
|
80
|
+
if (idx === -1) return null;
|
|
81
|
+
const value = args[idx + 1];
|
|
82
|
+
if (!value || value.startsWith('--')) return null;
|
|
83
|
+
return value;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function readRepeatedFlagValues(args, flagName) {
|
|
87
|
+
const values = [];
|
|
88
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
89
|
+
const arg = args[i];
|
|
90
|
+
if (arg === flagName && args[i + 1] && !args[i + 1].startsWith('--')) {
|
|
91
|
+
values.push(args[i + 1]);
|
|
92
|
+
i += 1;
|
|
93
|
+
} else if (arg.startsWith(`${flagName}=`)) {
|
|
94
|
+
values.push(arg.slice(flagName.length + 1));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return [...new Set(values.map(canonicalPath).filter(Boolean))];
|
|
98
|
+
}
|
|
99
|
+
|
|
96
100
|
function normalizeSourceBase(sourceBase) {
|
|
97
101
|
return sourceBase.endsWith('/') ? sourceBase : sourceBase + '/';
|
|
98
102
|
}
|
|
@@ -119,312 +123,339 @@ function isInstalledOptionalFile(file, localVersion) {
|
|
|
119
123
|
function hasInstalledOptions(localVersion) {
|
|
120
124
|
return selectedOptionIds(localVersion).size > 0;
|
|
121
125
|
}
|
|
122
|
-
|
|
123
|
-
/** Detect template placeholders in remote content. */
|
|
124
|
-
function isTemplate(raw) {
|
|
125
|
-
return /\{\{[a-zA-Z]+\}\}/.test(raw);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function sha256(content) {
|
|
129
|
-
return 'sha256-' + createHash('sha256').update(content).digest('hex');
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function sha256File(path) {
|
|
133
|
-
if (!existsSync(path)) return null;
|
|
134
|
-
let content = readFileSync(path, 'utf-8');
|
|
135
|
-
// Normalize CRLF → LF
|
|
136
|
-
content = content.replace(/\r\n/g, '\n');
|
|
137
|
-
return sha256(content);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function classify(file, localHash, storedHash) {
|
|
141
|
-
// PRESERVE
|
|
142
|
-
for (const p of PRESERVE_PATTERNS) {
|
|
143
|
-
if (p.test(file)) return 'PRESERVE';
|
|
144
|
-
}
|
|
145
|
-
// MERGE — dual-purpose, check if user modified
|
|
146
|
-
for (const p of MERGE_PATTERNS) {
|
|
147
|
-
if (p.test(file)) {
|
|
148
|
-
if (localHash === storedHash) return 'SAFE'; // unmodified, safe
|
|
149
|
-
return 'CONFLICT'; // user modified, needs decision
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
// Everything else is SAFE runtime file
|
|
153
|
-
if (localHash === storedHash || localHash === null) return 'SAFE';
|
|
154
|
-
return 'CONFLICT'; // modified runtime file — unexpected
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
async function fetchRemote(url, timeoutMs = 30000) {
|
|
158
|
-
if (url.startsWith('file://')) {
|
|
159
|
-
return readFileSync(fileURLToPath(url), 'utf-8');
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const controller = new AbortController();
|
|
163
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
164
|
-
try {
|
|
165
|
-
const res = await fetch(url, { signal: controller.signal });
|
|
166
|
-
if (!res.ok) throw new Error(`HTTP ${res.status}: ${url}`);
|
|
167
|
-
return res.text();
|
|
168
|
-
} finally {
|
|
169
|
-
clearTimeout(timer);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// ── Main ───────────────────────────────────────────────────────────
|
|
174
|
-
|
|
175
|
-
async function main() {
|
|
176
|
-
const args = process.argv.slice(2);
|
|
177
|
-
const apply = args.includes('--apply');
|
|
178
|
-
const applySafe = args.includes('--apply-safe') || args.includes('--safe-only');
|
|
179
|
-
const jsonOut = args.includes('--json');
|
|
180
|
-
const finalize = args.includes('--finalize');
|
|
181
|
-
const acceptLocal = readRepeatedFlagValues(args, '--accept-local');
|
|
182
|
-
const acceptMerged = readRepeatedFlagValues(args, '--accept-merged');
|
|
183
|
-
const acceptTemplate = readRepeatedFlagValues(args, '--accept-template');
|
|
184
|
-
const ignoreVersion = args.includes('--ignore-version') || args.includes('--force-check');
|
|
185
|
-
const sourceBase = normalizeSourceBase(readFlagValue(args, '--source-base') || process.env.WF_SOURCE_BASE || DEFAULT_SOURCE_BASE);
|
|
186
|
-
|
|
187
|
-
// 1. Read local state
|
|
188
|
-
if (!existsSync(VERSION_FILE)) {
|
|
189
|
-
if (jsonOut) {
|
|
190
|
-
console.log(JSON.stringify({
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
126
|
+
|
|
127
|
+
/** Detect template placeholders in remote content. */
|
|
128
|
+
function isTemplate(raw) {
|
|
129
|
+
return /\{\{[a-zA-Z]+\}\}/.test(raw);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function sha256(content) {
|
|
133
|
+
return 'sha256-' + createHash('sha256').update(content).digest('hex');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function sha256File(path) {
|
|
137
|
+
if (!existsSync(path)) return null;
|
|
138
|
+
let content = readFileSync(path, 'utf-8');
|
|
139
|
+
// Normalize CRLF → LF
|
|
140
|
+
content = content.replace(/\r\n/g, '\n');
|
|
141
|
+
return sha256(content);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function classify(file, localHash, storedHash) {
|
|
145
|
+
// PRESERVE
|
|
146
|
+
for (const p of PRESERVE_PATTERNS) {
|
|
147
|
+
if (p.test(file)) return 'PRESERVE';
|
|
148
|
+
}
|
|
149
|
+
// MERGE — dual-purpose, check if user modified
|
|
150
|
+
for (const p of MERGE_PATTERNS) {
|
|
151
|
+
if (p.test(file)) {
|
|
152
|
+
if (localHash === storedHash) return 'SAFE'; // unmodified, safe
|
|
153
|
+
return 'CONFLICT'; // user modified, needs decision
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// Everything else is SAFE runtime file
|
|
157
|
+
if (localHash === storedHash || localHash === null) return 'SAFE';
|
|
158
|
+
return 'CONFLICT'; // modified runtime file — unexpected
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function fetchRemote(url, timeoutMs = 30000) {
|
|
162
|
+
if (url.startsWith('file://')) {
|
|
163
|
+
return readFileSync(fileURLToPath(url), 'utf-8');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const controller = new AbortController();
|
|
167
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
168
|
+
try {
|
|
169
|
+
const res = await fetch(url, { signal: controller.signal });
|
|
170
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}: ${url}`);
|
|
171
|
+
return res.text();
|
|
172
|
+
} finally {
|
|
173
|
+
clearTimeout(timer);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ── Main ───────────────────────────────────────────────────────────
|
|
178
|
+
|
|
179
|
+
async function main() {
|
|
180
|
+
const args = process.argv.slice(2);
|
|
181
|
+
const apply = args.includes('--apply');
|
|
182
|
+
const applySafe = args.includes('--apply-safe') || args.includes('--safe-only');
|
|
183
|
+
const jsonOut = args.includes('--json');
|
|
184
|
+
const finalize = args.includes('--finalize');
|
|
185
|
+
const acceptLocal = readRepeatedFlagValues(args, '--accept-local');
|
|
186
|
+
const acceptMerged = readRepeatedFlagValues(args, '--accept-merged');
|
|
187
|
+
const acceptTemplate = readRepeatedFlagValues(args, '--accept-template');
|
|
188
|
+
const ignoreVersion = args.includes('--ignore-version') || args.includes('--force-check');
|
|
189
|
+
const sourceBase = normalizeSourceBase(readFlagValue(args, '--source-base') || process.env.WF_SOURCE_BASE || DEFAULT_SOURCE_BASE);
|
|
190
|
+
|
|
191
|
+
// 1. Read local state
|
|
192
|
+
if (!existsSync(VERSION_FILE)) {
|
|
193
|
+
if (jsonOut) {
|
|
194
|
+
console.log(JSON.stringify({
|
|
195
|
+
status: 'error',
|
|
196
|
+
message: 'Local Harness/.harness-version not found. Is Harness installed?',
|
|
197
|
+
recovery: 'If this is an old Harness install that predates .harness-version, do not reinstall blindly. Restore Harness/.harness-version from backup if available. If the updater script is also missing, run from the project root: npx create-harness-vibe-coding@latest <name> . -y --on-conflict skip --json to restore missing updater infrastructure without overwriting user files.',
|
|
198
|
+
}));
|
|
199
|
+
} else {
|
|
200
|
+
console.error('ERROR: Harness/.harness-version not found. Is Harness installed?');
|
|
201
|
+
console.error(' If this is an old Harness install without version tracking, do not reinstall blindly.');
|
|
202
|
+
console.error(' Restore Harness/.harness-version from backup if available.');
|
|
203
|
+
console.error(' If the updater script is also missing, recover with:');
|
|
204
|
+
console.error(' npx create-harness-vibe-coding@latest <project-name> . -y --on-conflict skip --json');
|
|
205
|
+
console.error(' This restores missing updater infrastructure without overwriting user data.');
|
|
206
|
+
}
|
|
194
207
|
process.exitCode = 1;
|
|
195
208
|
return;
|
|
196
209
|
}
|
|
197
|
-
|
|
198
|
-
let localVersion;
|
|
199
|
-
try {
|
|
200
|
-
localVersion = JSON.parse(readFileSync(VERSION_FILE, 'utf-8'));
|
|
201
|
-
} catch (e) {
|
|
202
|
-
if (jsonOut) {
|
|
203
|
-
console.log(JSON.stringify({
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
210
|
+
|
|
211
|
+
let localVersion;
|
|
212
|
+
try {
|
|
213
|
+
localVersion = JSON.parse(readFileSync(VERSION_FILE, 'utf-8'));
|
|
214
|
+
} catch (e) {
|
|
215
|
+
if (jsonOut) {
|
|
216
|
+
console.log(JSON.stringify({
|
|
217
|
+
status: 'error',
|
|
218
|
+
message: 'Failed to parse Harness/.harness-version: ' + e.message,
|
|
219
|
+
recovery: 'The .harness-version file may be corrupted. Do not overwrite user files. Back up or move the corrupted Harness/.harness-version before manual recovery. If the updater script is also missing, run from the project root: npx create-harness-vibe-coding@latest <name> . -y --on-conflict skip --json to restore missing updater infrastructure.',
|
|
220
|
+
}));
|
|
221
|
+
} else {
|
|
222
|
+
console.error('ERROR: Failed to parse Harness/.harness-version:', e.message);
|
|
223
|
+
console.error(' The file may be corrupted. Back up or move Harness/.harness-version before manual recovery.');
|
|
224
|
+
console.error(' If the updater script is also missing, recover with:');
|
|
225
|
+
console.error(' npx create-harness-vibe-coding@latest <project-name> . -y --on-conflict skip --json');
|
|
226
|
+
}
|
|
208
227
|
process.exitCode = 1;
|
|
209
|
-
return;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
if (!localVersion || typeof localVersion !== 'object') {
|
|
213
|
-
if (jsonOut) {
|
|
214
|
-
console.log(JSON.stringify({ status: 'error', message: 'Invalid Harness/.harness-version structure.' }));
|
|
215
|
-
} else {
|
|
216
|
-
console.error('ERROR: Harness/.harness-version has unexpected structure.');
|
|
217
|
-
}
|
|
218
|
-
process.exitCode = 1;
|
|
219
|
-
return;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
const localChecksums = localVersion.checksums || {};
|
|
223
|
-
|
|
224
|
-
// 2. Fetch remote version file
|
|
225
|
-
let remoteVersion;
|
|
226
|
-
try {
|
|
227
|
-
const raw = await fetchRemote(sourceBase + '.harness-version');
|
|
228
|
-
if (isTemplate(raw)) {
|
|
229
|
-
if (jsonOut) {
|
|
230
|
-
console.log(JSON.stringify({ status: 'template-remote', message: 'Remote .harness-version has not been generated yet.' }));
|
|
231
|
-
} else {
|
|
232
|
-
console.log('⚠ Remote .harness-version is a template (contains {{placeholders}}).');
|
|
233
|
-
console.log(' The generate step has not been run on the remote repo. No update possible.');
|
|
234
|
-
console.log(' This is expected during development — the update mechanism works once the remote is live.');
|
|
235
|
-
}
|
|
236
|
-
process.exitCode = 1;
|
|
237
|
-
return;
|
|
238
|
-
}
|
|
239
|
-
remoteVersion = JSON.parse(raw);
|
|
240
|
-
} catch (e) {
|
|
241
|
-
if (jsonOut) {
|
|
242
|
-
console.log(JSON.stringify({ status: 'offline', message: 'Cannot reach GitHub.' }));
|
|
243
|
-
} else {
|
|
244
|
-
console.error('ERROR: Cannot reach GitHub or invalid JSON. Offline?');
|
|
245
|
-
console.error(e.message);
|
|
246
|
-
}
|
|
247
|
-
process.exitCode = 1;
|
|
248
|
-
return;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// Compare versions — warn if remote is older (downgrade prevention)
|
|
252
|
-
function parseSemver(v) {
|
|
253
|
-
if (!v || typeof v !== 'string') return [0, 0, 0];
|
|
254
|
-
return v.replace(/^[^0-9]*/, '').split('-')[0].split('.').map(Number);
|
|
255
|
-
}
|
|
256
|
-
function cmpSemver(a, b) {
|
|
257
|
-
const va = parseSemver(a), vb = parseSemver(b);
|
|
258
|
-
for (let i = 0; i < 3; i++) { if ((va[i]||0) > (vb[i]||0)) return 1; if ((va[i]||0) < (vb[i]||0)) return -1; }
|
|
259
|
-
return 0;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
const localGen = localVersion.generator || '0.0.0';
|
|
263
|
-
const remoteGen = remoteVersion.generator || '0.0.0';
|
|
264
|
-
const versionCmp = cmpSemver(remoteGen, localGen);
|
|
265
|
-
|
|
266
|
-
if (!ignoreVersion && versionCmp <= 0) {
|
|
267
|
-
if (jsonOut) {
|
|
268
|
-
console.log(JSON.stringify({
|
|
269
|
-
status: versionCmp < 0 ? 'downgrade-refused' : 'up-to-date',
|
|
270
|
-
version: localGen,
|
|
271
|
-
remote: remoteGen,
|
|
272
|
-
sourceBase,
|
|
273
|
-
}));
|
|
274
|
-
} else if (versionCmp < 0) {
|
|
275
|
-
console.log(`⚠ Remote (v${remoteGen}) is OLDER than local (v${localGen}). Downgrade refused.`);
|
|
276
|
-
} else {
|
|
277
|
-
console.log(`✅ Already up to date (v${localGen})`);
|
|
278
|
-
}
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (!localVersion || typeof localVersion !== 'object') {
|
|
232
|
+
if (jsonOut) {
|
|
233
|
+
console.log(JSON.stringify({ status: 'error', message: 'Invalid Harness/.harness-version structure.' }));
|
|
234
|
+
} else {
|
|
235
|
+
console.error('ERROR: Harness/.harness-version has unexpected structure.');
|
|
236
|
+
}
|
|
237
|
+
process.exitCode = 1;
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const localChecksums = localVersion.checksums || {};
|
|
242
|
+
|
|
243
|
+
// 2. Fetch remote version file
|
|
244
|
+
let remoteVersion;
|
|
245
|
+
try {
|
|
246
|
+
const raw = await fetchRemote(sourceBase + '.harness-version');
|
|
247
|
+
if (isTemplate(raw)) {
|
|
248
|
+
if (jsonOut) {
|
|
249
|
+
console.log(JSON.stringify({ status: 'template-remote', message: 'Remote .harness-version has not been generated yet.' }));
|
|
250
|
+
} else {
|
|
251
|
+
console.log('⚠ Remote .harness-version is a template (contains {{placeholders}}).');
|
|
252
|
+
console.log(' The generate step has not been run on the remote repo. No update possible.');
|
|
253
|
+
console.log(' This is expected during development — the update mechanism works once the remote is live.');
|
|
254
|
+
}
|
|
255
|
+
process.exitCode = 1;
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
remoteVersion = JSON.parse(raw);
|
|
259
|
+
} catch (e) {
|
|
260
|
+
if (jsonOut) {
|
|
261
|
+
console.log(JSON.stringify({ status: 'offline', message: 'Cannot reach GitHub.' }));
|
|
262
|
+
} else {
|
|
263
|
+
console.error('ERROR: Cannot reach GitHub or invalid JSON. Offline?');
|
|
264
|
+
console.error(e.message);
|
|
265
|
+
}
|
|
266
|
+
process.exitCode = 1;
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Compare versions — warn if remote is older (downgrade prevention)
|
|
271
|
+
function parseSemver(v) {
|
|
272
|
+
if (!v || typeof v !== 'string') return [0, 0, 0];
|
|
273
|
+
return v.replace(/^[^0-9]*/, '').split('-')[0].split('.').map(Number);
|
|
274
|
+
}
|
|
275
|
+
function cmpSemver(a, b) {
|
|
276
|
+
const va = parseSemver(a), vb = parseSemver(b);
|
|
277
|
+
for (let i = 0; i < 3; i++) { if ((va[i]||0) > (vb[i]||0)) return 1; if ((va[i]||0) < (vb[i]||0)) return -1; }
|
|
278
|
+
return 0;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const localGen = localVersion.generator || '0.0.0';
|
|
282
|
+
const remoteGen = remoteVersion.generator || '0.0.0';
|
|
283
|
+
const versionCmp = cmpSemver(remoteGen, localGen);
|
|
284
|
+
|
|
285
|
+
if (!ignoreVersion && versionCmp <= 0) {
|
|
286
|
+
if (jsonOut) {
|
|
287
|
+
console.log(JSON.stringify({
|
|
288
|
+
status: versionCmp < 0 ? 'downgrade-refused' : 'up-to-date',
|
|
289
|
+
version: localGen,
|
|
290
|
+
remote: remoteGen,
|
|
291
|
+
sourceBase,
|
|
292
|
+
}));
|
|
293
|
+
} else if (versionCmp < 0) {
|
|
294
|
+
console.log(`⚠ Remote (v${remoteGen}) is OLDER than local (v${localGen}). Downgrade refused.`);
|
|
295
|
+
} else {
|
|
296
|
+
console.log(`✅ Already up to date (v${localGen})`);
|
|
297
|
+
}
|
|
279
298
|
if (versionCmp < 0) process.exitCode = 1;
|
|
280
299
|
return;
|
|
281
300
|
}
|
|
282
301
|
|
|
283
302
|
if (ignoreVersion) {
|
|
284
|
-
if (!jsonOut) console.log('🔧 Version check bypassed (--ignore-version). Comparing files anyway.');
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
const remoteChecksums = remoteVersion.checksums || {};
|
|
288
|
-
const remoteSources = remoteVersion.sources || {};
|
|
289
|
-
localVersion.acceptedConflicts = localVersion.acceptedConflicts || {};
|
|
290
|
-
|
|
291
|
-
/** Resolve the remote template-relative path for a dest-keyed file. Falls back to the key itself for back-compat. */
|
|
292
|
-
function remotePath(file) {
|
|
293
|
-
return remoteSources[file] || file;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
function withRemoteMeta(entry) {
|
|
297
|
-
if (!entry || !entry.file || !remoteChecksums[entry.file]) return entry;
|
|
298
|
-
const templateHint = remotePath(entry.file);
|
|
299
|
-
return {
|
|
300
|
-
...entry,
|
|
301
|
-
templateHint,
|
|
302
|
-
remoteUrl: sourceBase + templateHint,
|
|
303
|
-
};
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
function withConflictActions(entry) {
|
|
307
|
-
return {
|
|
308
|
-
...withRemoteMeta(entry),
|
|
309
|
-
choices: ['merge', 'keep-local', 'overwrite-from-template'],
|
|
310
|
-
decisionCommands: {
|
|
311
|
-
keepLocal: `node Harness/scripts/wf-update-check.mjs --accept-local ${shellArg(entry.file)} --finalize`,
|
|
312
|
-
acceptMerged: `node Harness/scripts/wf-update-check.mjs --accept-merged ${shellArg(entry.file)} --finalize`,
|
|
313
|
-
overwriteFromTemplate: `node Harness/scripts/wf-update-check.mjs --accept-template ${shellArg(entry.file)} --finalize`,
|
|
314
|
-
},
|
|
315
|
-
};
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
function shellArg(value) {
|
|
319
|
-
return /^[A-Za-z0-9@._/\\:-]+$/.test(value) ? value : JSON.stringify(value);
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
function buildJsonPlan() {
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
const
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
};
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
decision
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
303
|
+
if (!jsonOut) console.log('🔧 Version check bypassed (--ignore-version). Comparing files anyway.');
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const remoteChecksums = remoteVersion.checksums || {};
|
|
307
|
+
const remoteSources = remoteVersion.sources || {};
|
|
308
|
+
localVersion.acceptedConflicts = localVersion.acceptedConflicts || {};
|
|
309
|
+
|
|
310
|
+
/** Resolve the remote template-relative path for a dest-keyed file. Falls back to the key itself for back-compat. */
|
|
311
|
+
function remotePath(file) {
|
|
312
|
+
return remoteSources[file] || file;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function withRemoteMeta(entry) {
|
|
316
|
+
if (!entry || !entry.file || !remoteChecksums[entry.file]) return entry;
|
|
317
|
+
const templateHint = remotePath(entry.file);
|
|
318
|
+
return {
|
|
319
|
+
...entry,
|
|
320
|
+
templateHint,
|
|
321
|
+
remoteUrl: sourceBase + templateHint,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function withConflictActions(entry) {
|
|
326
|
+
return {
|
|
327
|
+
...withRemoteMeta(entry),
|
|
328
|
+
choices: ['merge', 'keep-local', 'overwrite-from-template'],
|
|
329
|
+
decisionCommands: {
|
|
330
|
+
keepLocal: `node Harness/scripts/wf-update-check.mjs --accept-local ${shellArg(entry.file)} --finalize`,
|
|
331
|
+
acceptMerged: `node Harness/scripts/wf-update-check.mjs --accept-merged ${shellArg(entry.file)} --finalize`,
|
|
332
|
+
overwriteFromTemplate: `node Harness/scripts/wf-update-check.mjs --accept-template ${shellArg(entry.file)} --finalize`,
|
|
333
|
+
},
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function shellArg(value) {
|
|
338
|
+
return /^[A-Za-z0-9@._/\\:-]+$/.test(value) ? value : JSON.stringify(value);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function buildJsonPlan() {
|
|
342
|
+
const verbose = args.includes('--verbose') || args.includes('--full-plan');
|
|
343
|
+
return {
|
|
344
|
+
updated: plan.updated.map(withRemoteMeta),
|
|
345
|
+
created: plan.created.map(withRemoteMeta),
|
|
346
|
+
adopted: plan.adopted.map(withRemoteMeta),
|
|
347
|
+
conflict: verbose ? plan.conflict.map(withConflictActions) : plan.conflict.map(withConflictActions).slice(0, 5),
|
|
348
|
+
conflictTruncated: !verbose && plan.conflict.length > 5 ? plan.conflict.length - 5 : undefined,
|
|
349
|
+
skipped: plan.skipped.map(withRemoteMeta),
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function buildAgentHints(jsonPlan) {
|
|
354
|
+
// Token-safe by default: omit the conflict array; full list available via --json --verbose (or --full-plan)
|
|
355
|
+
const verbose = args.includes('--verbose') || args.includes('--full-plan');
|
|
356
|
+
const maxConflictDetails = verbose ? Infinity : 5;
|
|
357
|
+
const totalConflicts = plan.conflict.length;
|
|
358
|
+
const conflicts = jsonPlan.conflict.slice(0, maxConflictDetails);
|
|
359
|
+
const truncated = totalConflicts > maxConflictDetails
|
|
360
|
+
? totalConflicts - maxConflictDetails
|
|
361
|
+
: 0;
|
|
362
|
+
return {
|
|
363
|
+
mode: 'script-first-ai-conflicts',
|
|
364
|
+
dryRunJsonCommand: 'node Harness/scripts/wf-update-check.mjs --json',
|
|
365
|
+
safeApplyCommand: 'node Harness/scripts/wf-update-check.mjs --apply-safe',
|
|
366
|
+
strictApplyCommand: 'node Harness/scripts/wf-update-check.mjs --apply',
|
|
367
|
+
finalizeCommand: 'node Harness/scripts/wf-update-check.mjs --finalize',
|
|
368
|
+
partialUpdate: localVersion.partialUpdate || null,
|
|
369
|
+
acceptedConflicts: localVersion.acceptedConflicts || {},
|
|
370
|
+
// aiMergeRequired array is attached only in verbose mode to keep default output token-safe.
|
|
371
|
+
...(verbose ? { aiMergeRequired: conflicts } : {}),
|
|
372
|
+
aiMergeRequiredCount: totalConflicts,
|
|
373
|
+
aiMergeRequiredTruncated: truncated > 0 ? truncated : undefined,
|
|
374
|
+
conflictPolicy: 'Use the script for SAFE/NEW/adopted files. For each CONFLICT file, compare local content with templateHint/remoteUrl, then record the decision with --accept-local, --accept-merged, or --accept-template. Do not hand-edit Harness/.harness-version.',
|
|
375
|
+
postUpdateCommands: [
|
|
376
|
+
'node Harness/scripts/validate-harness.mjs',
|
|
377
|
+
'node Harness/scripts/scan-clean.mjs --json',
|
|
378
|
+
],
|
|
379
|
+
bootstrapDebtCommand: 'node Harness/scripts/validate-harness.mjs --strict',
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
async function applyTemplateFile(file) {
|
|
384
|
+
const remoteHash = remoteChecksums[file];
|
|
385
|
+
if (!remoteHash) throw new Error(`No remote checksum for ${file}`);
|
|
386
|
+
const dest = safePath(file);
|
|
387
|
+
if (!dest) throw new Error(`Traversal rejected: ${file}`);
|
|
388
|
+
if (existsSync(dest) && lstatSync(dest).isSymbolicLink()) throw new Error(`Symlink rejected: ${file}`);
|
|
389
|
+
const content = await fetchRemote(sourceBase + remotePath(file));
|
|
390
|
+
const normalized = content.replace(/\r\n/g, '\n');
|
|
391
|
+
const fetchedHash = sha256(normalized);
|
|
392
|
+
if (fetchedHash !== remoteHash) throw new Error(`Hash mismatch: ${file}`);
|
|
393
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
394
|
+
writeFileSync(dest, normalized, 'utf-8');
|
|
395
|
+
localVersion.checksums[file] = remoteHash;
|
|
396
|
+
delete localVersion.acceptedConflicts[file];
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function recordLocalDecision(file, decision, appliedAt) {
|
|
400
|
+
const remoteHash = remoteChecksums[file];
|
|
401
|
+
if (!remoteHash) throw new Error(`No remote checksum for ${file}`);
|
|
402
|
+
const diskPath = safePath(file);
|
|
403
|
+
if (!diskPath) throw new Error(`Traversal rejected: ${file}`);
|
|
404
|
+
const localHash = sha256File(diskPath);
|
|
405
|
+
if (!localHash) throw new Error(`File not found for ${decision}: ${file}`);
|
|
406
|
+
localVersion.acceptedConflicts[file] = {
|
|
407
|
+
decision,
|
|
408
|
+
targetGenerator: remoteGen,
|
|
409
|
+
localHash,
|
|
410
|
+
remoteHash,
|
|
411
|
+
templateHint: remotePath(file),
|
|
412
|
+
updatedAt: appliedAt,
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function acceptedDecisionMatches(file, localHash, remoteHash) {
|
|
417
|
+
const decision = localVersion.acceptedConflicts?.[file];
|
|
418
|
+
return Boolean(
|
|
419
|
+
decision
|
|
420
|
+
&& decision.targetGenerator === remoteGen
|
|
421
|
+
&& decision.localHash === localHash
|
|
422
|
+
&& decision.remoteHash === remoteHash
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
let decisionMetadataDirty = false;
|
|
427
|
+
if (acceptLocal.length || acceptMerged.length || acceptTemplate.length) {
|
|
428
|
+
const appliedAt = new Date().toISOString();
|
|
429
|
+
try {
|
|
430
|
+
for (const file of acceptLocal) recordLocalDecision(file, 'accept-local', appliedAt);
|
|
431
|
+
for (const file of acceptMerged) recordLocalDecision(file, 'accept-merged', appliedAt);
|
|
432
|
+
for (const file of acceptTemplate) await applyTemplateFile(file);
|
|
433
|
+
decisionMetadataDirty = true;
|
|
434
|
+
} catch (e) {
|
|
435
|
+
if (jsonOut) {
|
|
436
|
+
console.log(JSON.stringify({ status: 'error', message: e.message }, null, 2));
|
|
437
|
+
} else {
|
|
438
|
+
console.error(`ERROR: ${e.message}`);
|
|
439
|
+
}
|
|
440
|
+
process.exitCode = 1;
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const allFiles = new Set([...Object.keys(localChecksums), ...Object.keys(remoteChecksums)]);
|
|
446
|
+
|
|
447
|
+
const plan = { updated: [], created: [], adopted: [], conflict: [], skipped: [] };
|
|
448
|
+
|
|
449
|
+
for (const file of [...allFiles].sort()) {
|
|
450
|
+
const canonical = canonicalPath(file);
|
|
451
|
+
|
|
452
|
+
// Reject paths that escape ROOT before any file access
|
|
453
|
+
const diskPath = safePath(file);
|
|
454
|
+
if (!diskPath) {
|
|
455
|
+
plan.skipped.push({ file, reason: 'path traversal rejected' });
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
|
|
428
459
|
const localHash = sha256File(diskPath);
|
|
429
460
|
const storedHash = localChecksums[file];
|
|
430
461
|
const remoteHash = remoteChecksums[file];
|
|
@@ -437,31 +468,31 @@ async function main() {
|
|
|
437
468
|
plan.skipped.push({ file, reason: 'not in remote' });
|
|
438
469
|
continue;
|
|
439
470
|
}
|
|
440
|
-
|
|
441
|
-
if (!storedHash) {
|
|
442
|
-
// New file from remote — if local file exists, it's a CONFLICT
|
|
443
|
-
if (localHash) {
|
|
444
|
-
if (localHash === remoteHash) {
|
|
445
|
-
plan.adopted.push({ file, localHash, remoteHash, reason: 'new remote file already matches local file' });
|
|
446
|
-
continue;
|
|
447
|
-
}
|
|
448
|
-
if (acceptedDecisionMatches(file, localHash, remoteHash)) {
|
|
449
|
-
plan.skipped.push({ file, reason: `accepted conflict decision: ${localVersion.acceptedConflicts[file].decision}` });
|
|
450
|
-
continue;
|
|
451
|
-
}
|
|
452
|
-
plan.conflict.push({ file, localHash, storedHash: 'none', remoteHash, reason: 'new remote file conflicts with existing local file' });
|
|
453
|
-
continue;
|
|
454
|
-
}
|
|
455
|
-
// New file — still respect PRESERVE classification
|
|
456
|
-
const tier = classify(canonical, null, null);
|
|
457
|
-
if (tier === 'PRESERVE') {
|
|
458
|
-
plan.skipped.push({ file, reason: 'PRESERVE — new file would overwrite user data' });
|
|
459
|
-
} else {
|
|
460
|
-
plan.created.push({ file, remoteHash });
|
|
461
|
-
}
|
|
462
|
-
continue;
|
|
463
|
-
}
|
|
464
|
-
|
|
471
|
+
|
|
472
|
+
if (!storedHash) {
|
|
473
|
+
// New file from remote — if local file exists, it's a CONFLICT
|
|
474
|
+
if (localHash) {
|
|
475
|
+
if (localHash === remoteHash) {
|
|
476
|
+
plan.adopted.push({ file, localHash, remoteHash, reason: 'new remote file already matches local file' });
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
if (acceptedDecisionMatches(file, localHash, remoteHash)) {
|
|
480
|
+
plan.skipped.push({ file, reason: `accepted conflict decision: ${localVersion.acceptedConflicts[file].decision}` });
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
plan.conflict.push({ file, localHash, storedHash: 'none', remoteHash, reason: 'new remote file conflicts with existing local file' });
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
// New file — still respect PRESERVE classification
|
|
487
|
+
const tier = classify(canonical, null, null);
|
|
488
|
+
if (tier === 'PRESERVE') {
|
|
489
|
+
plan.skipped.push({ file, reason: 'PRESERVE — new file would overwrite user data' });
|
|
490
|
+
} else {
|
|
491
|
+
plan.created.push({ file, remoteHash });
|
|
492
|
+
}
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
|
|
465
496
|
if (
|
|
466
497
|
storedHash
|
|
467
498
|
&& hasInstalledOptions(localVersion)
|
|
@@ -484,204 +515,206 @@ async function main() {
|
|
|
484
515
|
}
|
|
485
516
|
|
|
486
517
|
const tier = classify(canonical, localHash, storedHash);
|
|
487
|
-
|
|
488
|
-
if (tier === 'PRESERVE') {
|
|
489
|
-
plan.skipped.push({ file, reason: 'PRESERVE — user data' });
|
|
490
|
-
} else if (tier === 'SAFE') {
|
|
491
|
-
if (localHash === remoteHash) {
|
|
492
|
-
plan.skipped.push({ file, reason: 'already current' });
|
|
493
|
-
} else {
|
|
494
|
-
plan.updated.push({ file, remoteHash });
|
|
495
|
-
}
|
|
496
|
-
} else if (tier === 'CONFLICT') {
|
|
497
|
-
if (acceptedDecisionMatches(file, localHash, remoteHash)) {
|
|
498
|
-
plan.skipped.push({ file, reason: `accepted conflict decision: ${localVersion.acceptedConflicts[file].decision}` });
|
|
499
|
-
continue;
|
|
500
|
-
}
|
|
501
|
-
plan.conflict.push({
|
|
502
|
-
file,
|
|
503
|
-
localHash,
|
|
504
|
-
storedHash,
|
|
505
|
-
remoteHash,
|
|
506
|
-
reason: MERGE_PATTERNS.some(p => p.test(canonical))
|
|
507
|
-
? 'user modified MERGE file'
|
|
508
|
-
: 'user modified runtime file',
|
|
509
|
-
});
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
// 3. Output
|
|
514
|
-
if (jsonOut) {
|
|
515
|
-
const
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
console.log(
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
if (
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
const
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
if (
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
failed++; continue;
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
const
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
for (const
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
localVersion.partialUpdate
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
console.log(`✅ Applied ${applied}
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
}
|
|
662
|
-
process.exitCode = 1;
|
|
663
|
-
return plan;
|
|
664
|
-
}
|
|
665
|
-
if (plan.conflict.length > 0) {
|
|
666
|
-
console.log(`Cannot finalize: ${plan.conflict.length} conflicts still need a script-recorded decision.`);
|
|
518
|
+
|
|
519
|
+
if (tier === 'PRESERVE') {
|
|
520
|
+
plan.skipped.push({ file, reason: 'PRESERVE — user data' });
|
|
521
|
+
} else if (tier === 'SAFE') {
|
|
522
|
+
if (localHash === remoteHash) {
|
|
523
|
+
plan.skipped.push({ file, reason: 'already current' });
|
|
524
|
+
} else {
|
|
525
|
+
plan.updated.push({ file, remoteHash });
|
|
526
|
+
}
|
|
527
|
+
} else if (tier === 'CONFLICT') {
|
|
528
|
+
if (acceptedDecisionMatches(file, localHash, remoteHash)) {
|
|
529
|
+
plan.skipped.push({ file, reason: `accepted conflict decision: ${localVersion.acceptedConflicts[file].decision}` });
|
|
530
|
+
continue;
|
|
531
|
+
}
|
|
532
|
+
plan.conflict.push({
|
|
533
|
+
file,
|
|
534
|
+
localHash,
|
|
535
|
+
storedHash,
|
|
536
|
+
remoteHash,
|
|
537
|
+
reason: MERGE_PATTERNS.some(p => p.test(canonical))
|
|
538
|
+
? 'user modified MERGE file'
|
|
539
|
+
: 'user modified runtime file',
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// 3. Output
|
|
545
|
+
if (jsonOut) {
|
|
546
|
+
const verbose = args.includes('--verbose') || args.includes('--full-plan');
|
|
547
|
+
const jsonPlan = buildJsonPlan();
|
|
548
|
+
console.log(JSON.stringify({
|
|
549
|
+
status: localVersion.partialUpdate ? 'partial-update' : 'update-available',
|
|
550
|
+
from: localGen,
|
|
551
|
+
to: remoteGen,
|
|
552
|
+
sourceBase,
|
|
553
|
+
partialUpdate: localVersion.partialUpdate || null,
|
|
554
|
+
acceptedConflicts: localVersion.acceptedConflicts || {},
|
|
555
|
+
updated: plan.updated.length,
|
|
556
|
+
created: plan.created.length,
|
|
557
|
+
adopted: plan.adopted.length,
|
|
558
|
+
conflict: plan.conflict.length,
|
|
559
|
+
skipped: plan.skipped.length,
|
|
560
|
+
// Token-safe by default: attach the full plan only when --verbose / --full-plan is passed.
|
|
561
|
+
...(verbose ? { plan: jsonPlan } : {}),
|
|
562
|
+
agent: buildAgentHints(jsonPlan),
|
|
563
|
+
}, null, 2));
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
console.log(`\n🔄 Update: v${localGen} → v${remoteGen}`);
|
|
568
|
+
console.log(` ${plan.updated.length} safe update, ${plan.created.length} new, ${plan.conflict.length} conflict, ${plan.skipped.length} skipped\n`);
|
|
569
|
+
|
|
570
|
+
// Show conflicts (these need AI/user decision)
|
|
571
|
+
if (plan.conflict.length > 0) {
|
|
572
|
+
console.log('⚠ CONFLICTS (need your decision):');
|
|
573
|
+
for (const c of plan.conflict) {
|
|
574
|
+
console.log(` 📄 ${c.file} [${c.reason}]`);
|
|
575
|
+
}
|
|
576
|
+
if (plan.updated.length + plan.created.length > 0) {
|
|
577
|
+
console.log(' Tip: run --apply-safe to apply SAFE/NEW files first, then merge conflicts.');
|
|
578
|
+
}
|
|
579
|
+
console.log('');
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Show what will be auto-updated
|
|
583
|
+
if (plan.updated.length + plan.created.length > 0) {
|
|
584
|
+
console.log('✅ AUTO (safe to apply):');
|
|
585
|
+
for (const u of plan.updated) console.log(` ↑ ${u.file}`);
|
|
586
|
+
for (const c of plan.created) console.log(` + ${c.file}`);
|
|
587
|
+
console.log('');
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// 4. Apply if requested
|
|
591
|
+
if (apply || applySafe) {
|
|
592
|
+
// Refuse to apply when conflicts exist — must resolve first
|
|
593
|
+
if (apply && !applySafe && plan.conflict.length > 0) {
|
|
594
|
+
console.log(`❌ Cannot apply: ${plan.conflict.length} conflicts must be resolved first.`);
|
|
595
|
+
console.log(' Run --apply-safe to apply SAFE/NEW files first, or resolve conflicts manually then re-run --apply.');
|
|
596
|
+
process.exitCode = 1;
|
|
597
|
+
return plan;
|
|
598
|
+
}
|
|
599
|
+
if (applySafe && plan.conflict.length > 0) {
|
|
600
|
+
console.log(`Applying SAFE/NEW files only; ${plan.conflict.length} conflicts will remain for AI/user merge.`);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
const lexists = existsSync;
|
|
604
|
+
let applied = 0;
|
|
605
|
+
let failed = 0;
|
|
606
|
+
|
|
607
|
+
for (const u of plan.updated) {
|
|
608
|
+
try {
|
|
609
|
+
const dest = safePath(u.file);
|
|
610
|
+
if (!dest) { console.error(` ✗ Traversal rejected: ${u.file}`); failed++; continue; }
|
|
611
|
+
// Symlink rejection — don't follow symlinks
|
|
612
|
+
if (lexists(dest)) {
|
|
613
|
+
try { if (lstatSync(dest).isSymbolicLink()) { console.error(` ✗ Symlink rejected: ${u.file}`); failed++; continue; } } catch (_) {}
|
|
614
|
+
}
|
|
615
|
+
const content = await fetchRemote(sourceBase + remotePath(u.file));
|
|
616
|
+
const normalized = content.replace(/\r\n/g, '\n');
|
|
617
|
+
const fetchedHash = sha256(normalized);
|
|
618
|
+
if (fetchedHash !== u.remoteHash) {
|
|
619
|
+
console.error(` ✗ Hash mismatch: ${u.file}`);
|
|
620
|
+
failed++; continue;
|
|
621
|
+
}
|
|
622
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
623
|
+
writeFileSync(dest, normalized, 'utf-8');
|
|
624
|
+
applied++;
|
|
625
|
+
} catch (e) {
|
|
626
|
+
console.error(` ✗ Failed: ${u.file} — ${e.message}`);
|
|
627
|
+
failed++;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
for (const c of plan.created) {
|
|
632
|
+
try {
|
|
633
|
+
const dest = safePath(c.file);
|
|
634
|
+
if (!dest) { console.error(` ✗ Traversal rejected: ${c.file}`); failed++; continue; }
|
|
635
|
+
// TOCTOU: recheck file didn't appear since planning
|
|
636
|
+
if (lexists(dest)) {
|
|
637
|
+
try { if (lstatSync(dest).isSymbolicLink()) { console.error(` ✗ Symlink rejected: ${c.file}`); failed++; continue; } } catch (_) {}
|
|
638
|
+
console.error(` ✗ File created since plan: ${c.file} — treating as CONFLICT`);
|
|
639
|
+
failed++; continue;
|
|
640
|
+
}
|
|
641
|
+
const content = await fetchRemote(sourceBase + remotePath(c.file));
|
|
642
|
+
const normalized = content.replace(/\r\n/g, '\n');
|
|
643
|
+
const fetchedHash = sha256(normalized);
|
|
644
|
+
if (fetchedHash !== c.remoteHash) {
|
|
645
|
+
console.error(` ✗ Hash mismatch: ${c.file}`);
|
|
646
|
+
failed++; continue;
|
|
647
|
+
}
|
|
648
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
649
|
+
writeFileSync(dest, normalized, 'utf-8');
|
|
650
|
+
applied++;
|
|
651
|
+
} catch (e) {
|
|
652
|
+
console.error(` ✗ Failed: ${c.file} — ${e.message}`);
|
|
653
|
+
failed++;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// Only version-track applied files after complete script success.
|
|
658
|
+
if (failed === 0) {
|
|
659
|
+
const appliedAt = new Date().toISOString();
|
|
660
|
+
localVersion.checksums = localVersion.checksums || {};
|
|
661
|
+
for (const u of plan.updated) localVersion.checksums[u.file] = u.remoteHash;
|
|
662
|
+
for (const c of plan.created) localVersion.checksums[c.file] = c.remoteHash;
|
|
663
|
+
for (const a of plan.adopted) localVersion.checksums[a.file] = a.remoteHash;
|
|
664
|
+
if (plan.conflict.length === 0) {
|
|
665
|
+
localVersion.generator = remoteGen;
|
|
666
|
+
localVersion.generated = appliedAt;
|
|
667
|
+
delete localVersion.partialUpdate;
|
|
668
|
+
} else {
|
|
669
|
+
localVersion.partialUpdate = {
|
|
670
|
+
targetGenerator: remoteGen,
|
|
671
|
+
updatedAt: appliedAt,
|
|
672
|
+
appliedFiles: [...plan.updated, ...plan.created].map(x => x.file),
|
|
673
|
+
adoptedFiles: plan.adopted.map(x => x.file),
|
|
674
|
+
conflicts: plan.conflict.map(x => x.file),
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
writeFileSync(VERSION_FILE, JSON.stringify(localVersion, null, 2) + '\n', 'utf-8');
|
|
678
|
+
if (plan.conflict.length === 0) {
|
|
679
|
+
console.log(`✅ Applied ${applied} files. Version updated to ${remoteVersion.generator}.`);
|
|
680
|
+
} else {
|
|
681
|
+
console.log(`✅ Applied ${applied} SAFE/NEW files. Version remains ${localGen}; ${plan.conflict.length} conflicts still need merge.`);
|
|
682
|
+
}
|
|
683
|
+
} else {
|
|
684
|
+
console.log(`❌ ${failed} failures. NO files were version-tracked. Fix and re-run.`);
|
|
685
|
+
process.exitCode = 1;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
if (finalize) {
|
|
690
|
+
if (plan.updated.length > 0 || plan.created.length > 0 || plan.adopted.length > 0) {
|
|
691
|
+
console.log(`Cannot finalize: ${plan.updated.length} safe updates, ${plan.created.length} new files, and ${plan.adopted.length} adopted metadata entries still need --apply-safe.`);
|
|
667
692
|
if (decisionMetadataDirty) {
|
|
668
693
|
writeFileSync(VERSION_FILE, JSON.stringify(localVersion, null, 2) + '\n', 'utf-8');
|
|
669
694
|
}
|
|
670
695
|
process.exitCode = 1;
|
|
671
696
|
return plan;
|
|
672
697
|
}
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
698
|
+
if (plan.conflict.length > 0) {
|
|
699
|
+
console.log(`Cannot finalize: ${plan.conflict.length} conflicts still need a script-recorded decision.`);
|
|
700
|
+
if (decisionMetadataDirty) {
|
|
701
|
+
writeFileSync(VERSION_FILE, JSON.stringify(localVersion, null, 2) + '\n', 'utf-8');
|
|
702
|
+
}
|
|
703
|
+
process.exitCode = 1;
|
|
704
|
+
return plan;
|
|
705
|
+
}
|
|
706
|
+
const finalizedAt = new Date().toISOString();
|
|
707
|
+
localVersion.generator = remoteGen;
|
|
708
|
+
localVersion.generated = finalizedAt;
|
|
709
|
+
delete localVersion.partialUpdate;
|
|
710
|
+
writeFileSync(VERSION_FILE, JSON.stringify(localVersion, null, 2) + '\n', 'utf-8');
|
|
711
|
+
console.log(`Finalized Harness update to ${remoteGen}.`);
|
|
712
|
+
} else if (decisionMetadataDirty) {
|
|
713
|
+
writeFileSync(VERSION_FILE, JSON.stringify(localVersion, null, 2) + '\n', 'utf-8');
|
|
714
|
+
console.log('Recorded conflict decision metadata.');
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
return plan;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
main().catch(e => { console.error(e); process.exit(1); });
|