create-harness-vibe-coding 0.8.1 → 0.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README-CN.md +15 -11
- package/README.md +21 -12
- package/package.json +1 -1
- package/src/index.js +210 -10
- package/templates/common/.claude/agents/tdd-guide.md +55 -0
- package/templates/common/.claude/settings.json +22 -0
- package/templates/common/.claude/skills/tdd/SKILL.md +30 -0
- package/templates/common/.claude/skills/wf-auto/SKILL.md +107 -0
- package/templates/common/.claude/skills/wf-auto-spark/SKILL.md +39 -0
- package/templates/common/.claude/skills/wf-max/SKILL.md +10 -2
- package/templates/common/.claude/skills/wf-remove/SKILL.md +12 -5
- package/templates/common/.claude/skills/wf-update/SKILL.md +12 -5
- package/templates/common/.codex/hooks.json +59 -37
- package/templates/common/.harness-version +48 -25
- package/templates/common/AGENTS.md +5 -5
- package/templates/common/CLAUDE.md +12 -17
- package/templates/common/Harness/ECC-GUIDE.md +246 -0
- package/templates/common/Harness/README.md +129 -130
- package/templates/common/Harness/TDD-GUIDE.md +83 -0
- package/templates/common/Harness/WF-AUTO-SPARK.md +297 -0
- package/templates/common/Harness/WF-AUTO.md +508 -0
- package/templates/common/Harness/WF-MAX.md +24 -0
- package/templates/common/Harness/context-loading.md +38 -1
- package/templates/common/Harness/dispatch.md +40 -40
- package/templates/common/Harness/subagents.md +7 -21
- package/templates/common/Harness/tasks/_template/NAMING.md +47 -0
- package/templates/common/MEMORY.md +73 -66
- package/templates/common/SETUP.md +100 -78
- package/templates/common/scripts/validate-harness.mjs +92 -58
- package/templates/common/scripts/wf-mode-hook.mjs +895 -318
- package/templates/common/scripts/wf-remove.mjs +301 -81
- package/templates/common/scripts/wf-statusline.ps1 +62 -38
- package/templates/common/scripts/wf-statusline.sh +67 -48
- package/templates/common/scripts/wf-update-check.mjs +179 -81
|
@@ -1,48 +1,67 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
# wf-statusline.sh — Unix statusline badge for WF-MAX / WF-REVIEW
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
MODE_FILE="Harness/.runtime/current-mode.json"
|
|
16
|
-
fi
|
|
17
|
-
|
|
18
|
-
[ -L "$MODE_FILE" ] && exit 0 # Refuse symlinks
|
|
19
|
-
[ ! -f "$MODE_FILE" ] && exit 0 # Missing file
|
|
20
|
-
|
|
21
|
-
# Read with python for safe JSON parse (jq may not be installed).
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
fpath
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# wf-statusline.sh — Unix statusline badge for WF-MAX / WF-REVIEW
|
|
3
|
+
# Displays mode + agentRole from Harness/.runtime/current-mode.json
|
|
4
|
+
# Role-aware: shows all roles (CEO/MGR/WRK/REV), not just CEO.
|
|
5
|
+
#
|
|
6
|
+
# Usage in .claude/settings.json:
|
|
7
|
+
# "statusLine": { "type": "command", "command": "bash Harness/scripts/wf-statusline.sh" }
|
|
8
|
+
#
|
|
9
|
+
# Security: refuses symlinks, validates JSON, caps read size.
|
|
10
|
+
|
|
11
|
+
MODE_FILE="$(dirname "$0")/../.runtime/current-mode.json"
|
|
12
|
+
|
|
13
|
+
# Resolve relative path to absolute (from project root or script location)
|
|
14
|
+
if [ ! -f "$MODE_FILE" ]; then
|
|
15
|
+
MODE_FILE="Harness/.runtime/current-mode.json"
|
|
16
|
+
fi
|
|
17
|
+
|
|
18
|
+
[ -L "$MODE_FILE" ] && exit 0 # Refuse symlinks
|
|
19
|
+
[ ! -f "$MODE_FILE" ] && exit 0 # Missing file
|
|
20
|
+
|
|
21
|
+
# Read with python for safe JSON parse (jq may not be installed).
|
|
22
|
+
MODE=$(python3 -c "
|
|
23
|
+
import json, os, sys
|
|
24
|
+
try:
|
|
25
|
+
fpath = sys.argv[1]
|
|
26
|
+
if os.path.getsize(fpath) > 16384: sys.exit(1)
|
|
27
|
+
with open(fpath, 'r') as f:
|
|
28
|
+
d = json.load(f)
|
|
29
|
+
if not d.get('active'): sys.exit(1)
|
|
30
|
+
agent_role = d.get('agentRole') or d.get('role') or ''
|
|
31
|
+
role_short = {'ceo':'CEO','manager':'MGR','worker':'WRK','reviewer':'REV'}.get(agent_role, '')
|
|
32
|
+
print(d.get('mode','') + '|' + d.get('phase','') + '|' + role_short)
|
|
33
|
+
except: pass
|
|
34
|
+
" "$MODE_FILE" 2>/dev/null)
|
|
35
|
+
|
|
36
|
+
[ -z "$MODE" ] && exit 0
|
|
37
|
+
|
|
38
|
+
MODE_NAME="${MODE%%|*}"
|
|
39
|
+
REST="${MODE#*|}"
|
|
40
|
+
MODE_PHASE="${REST%%|*}"
|
|
41
|
+
MODE_ROLE="${REST##*|}"
|
|
42
|
+
|
|
43
|
+
case "$MODE_NAME" in
|
|
44
|
+
wf-max)
|
|
45
|
+
PHASE_SHORT=$(echo "$MODE_PHASE" | sed 's/W._/W/')
|
|
46
|
+
if [ -n "$MODE_ROLE" ]; then
|
|
47
|
+
printf '\033[38;5;75m[WF-MAX:%s:%s]\033[0m' "$MODE_ROLE" "$PHASE_SHORT"
|
|
48
|
+
else
|
|
49
|
+
printf '\033[38;5;75m[WF-MAX:%s]\033[0m' "$PHASE_SHORT"
|
|
50
|
+
fi
|
|
51
|
+
;;
|
|
52
|
+
wf-review)
|
|
53
|
+
printf '\033[38;5;178m[WF-REVIEW]\033[0m'
|
|
54
|
+
;;
|
|
55
|
+
wf-auto-spark)
|
|
56
|
+
printf '\033[38;5;213m[SPARK]\033[0m'
|
|
57
|
+
;;
|
|
58
|
+
wf-auto)
|
|
59
|
+
printf '\033[38;5;111m[WF-AUTO]\033[0m'
|
|
60
|
+
;;
|
|
61
|
+
wf)
|
|
62
|
+
printf '\033[38;5;108m[WF]\033[0m'
|
|
63
|
+
;;
|
|
64
|
+
wf-learn)
|
|
65
|
+
printf '\033[38;5;222m[WF-LEARN]\033[0m'
|
|
66
|
+
;;
|
|
67
|
+
esac
|
|
@@ -4,21 +4,22 @@
|
|
|
4
4
|
* Fetches remote checksums, compares locally, classifies all files instantly.
|
|
5
5
|
* Only CONFLICT files need AI/user decision.
|
|
6
6
|
*
|
|
7
|
-
* Usage:
|
|
8
|
-
* node Harness/scripts/wf-update-check.mjs
|
|
9
|
-
* node Harness/scripts/wf-update-check.mjs --
|
|
10
|
-
* node Harness/scripts/wf-update-check.mjs --
|
|
11
|
-
|
|
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 --apply-safe # apply SAFE+NEW, leave CONFLICT for AI
|
|
11
|
+
* node Harness/scripts/wf-update-check.mjs --apply # apply only when no CONFLICT exists
|
|
12
|
+
*/
|
|
12
13
|
|
|
13
14
|
import { readFileSync, writeFileSync, existsSync, mkdirSync, lstatSync } from 'fs';
|
|
14
15
|
import { createHash } from 'crypto';
|
|
15
16
|
import { resolve, dirname, sep } from 'path';
|
|
16
17
|
import { fileURLToPath } from 'url';
|
|
17
18
|
|
|
18
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
19
|
-
const ROOT = process.env.WF_ROOT ? resolve(process.env.WF_ROOT) : resolve(__dirname, '..', '..');
|
|
20
|
-
const VERSION_FILE = resolve(ROOT, 'Harness', '.harness-version');
|
|
21
|
-
const
|
|
19
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
const ROOT = process.env.WF_ROOT ? resolve(process.env.WF_ROOT) : resolve(__dirname, '..', '..');
|
|
21
|
+
const VERSION_FILE = resolve(ROOT, 'Harness', '.harness-version');
|
|
22
|
+
const DEFAULT_SOURCE_BASE = 'https://raw.githubusercontent.com/zingspark/create-harness-vibe-coding/main/templates/common/';
|
|
22
23
|
|
|
23
24
|
// ── Tier classification ──────────────────────────────────────────
|
|
24
25
|
|
|
@@ -60,14 +61,26 @@ function safePath(file) {
|
|
|
60
61
|
}
|
|
61
62
|
|
|
62
63
|
/** Canonical normalization for classification matching. */
|
|
63
|
-
function canonicalPath(file) {
|
|
64
|
-
return file.replace(/\\/g, '/').replace(/\/+/g, '/');
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
64
|
+
function canonicalPath(file) {
|
|
65
|
+
return file.replace(/\\/g, '/').replace(/\/+/g, '/');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function readFlagValue(args, flagName) {
|
|
69
|
+
const idx = args.indexOf(flagName);
|
|
70
|
+
if (idx === -1) return null;
|
|
71
|
+
const value = args[idx + 1];
|
|
72
|
+
if (!value || value.startsWith('--')) return null;
|
|
73
|
+
return value;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function normalizeSourceBase(sourceBase) {
|
|
77
|
+
return sourceBase.endsWith('/') ? sourceBase : sourceBase + '/';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Detect template placeholders in remote content. */
|
|
81
|
+
function isTemplate(raw) {
|
|
82
|
+
return /\{\{[a-zA-Z]+\}\}/.test(raw);
|
|
83
|
+
}
|
|
71
84
|
|
|
72
85
|
function sha256(content) {
|
|
73
86
|
return 'sha256-' + createHash('sha256').update(content).digest('hex');
|
|
@@ -98,9 +111,13 @@ function classify(file, localHash, storedHash) {
|
|
|
98
111
|
return 'CONFLICT'; // modified runtime file — unexpected
|
|
99
112
|
}
|
|
100
113
|
|
|
101
|
-
async function fetchRemote(url, timeoutMs = 30000) {
|
|
102
|
-
|
|
103
|
-
|
|
114
|
+
async function fetchRemote(url, timeoutMs = 30000) {
|
|
115
|
+
if (url.startsWith('file://')) {
|
|
116
|
+
return readFileSync(fileURLToPath(url), 'utf-8');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const controller = new AbortController();
|
|
120
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
104
121
|
try {
|
|
105
122
|
const res = await fetch(url, { signal: controller.signal });
|
|
106
123
|
if (!res.ok) throw new Error(`HTTP ${res.status}: ${url}`);
|
|
@@ -112,11 +129,13 @@ async function fetchRemote(url, timeoutMs = 30000) {
|
|
|
112
129
|
|
|
113
130
|
// ── Main ───────────────────────────────────────────────────────────
|
|
114
131
|
|
|
115
|
-
async function main() {
|
|
116
|
-
const args = process.argv.slice(2);
|
|
117
|
-
const apply = args.includes('--apply');
|
|
118
|
-
const
|
|
119
|
-
const
|
|
132
|
+
async function main() {
|
|
133
|
+
const args = process.argv.slice(2);
|
|
134
|
+
const apply = args.includes('--apply');
|
|
135
|
+
const applySafe = args.includes('--apply-safe') || args.includes('--safe-only');
|
|
136
|
+
const jsonOut = args.includes('--json');
|
|
137
|
+
const ignoreVersion = args.includes('--ignore-version') || args.includes('--force-check');
|
|
138
|
+
const sourceBase = normalizeSourceBase(readFlagValue(args, '--source-base') || process.env.WF_SOURCE_BASE || DEFAULT_SOURCE_BASE);
|
|
120
139
|
|
|
121
140
|
// 1. Read local state
|
|
122
141
|
if (!existsSync(VERSION_FILE)) {
|
|
@@ -155,7 +174,7 @@ async function main() {
|
|
|
155
174
|
// 2. Fetch remote version file
|
|
156
175
|
let remoteVersion;
|
|
157
176
|
try {
|
|
158
|
-
const raw = await fetchRemote(
|
|
177
|
+
const raw = await fetchRemote(sourceBase + '.harness-version');
|
|
159
178
|
if (isTemplate(raw)) {
|
|
160
179
|
if (jsonOut) {
|
|
161
180
|
console.log(JSON.stringify({ status: 'template-remote', message: 'Remote .harness-version has not been generated yet.' }));
|
|
@@ -188,15 +207,21 @@ async function main() {
|
|
|
188
207
|
return 0;
|
|
189
208
|
}
|
|
190
209
|
|
|
191
|
-
const localGen = localVersion.generator || '0.0.0';
|
|
192
|
-
const remoteGen = remoteVersion.generator || '0.0.0';
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
210
|
+
const localGen = localVersion.generator || '0.0.0';
|
|
211
|
+
const remoteGen = remoteVersion.generator || '0.0.0';
|
|
212
|
+
const versionCmp = cmpSemver(remoteGen, localGen);
|
|
213
|
+
|
|
214
|
+
if (!ignoreVersion && versionCmp <= 0) {
|
|
215
|
+
if (jsonOut) {
|
|
216
|
+
console.log(JSON.stringify({
|
|
217
|
+
status: versionCmp < 0 ? 'downgrade-refused' : 'up-to-date',
|
|
218
|
+
version: localGen,
|
|
219
|
+
remote: remoteGen,
|
|
220
|
+
sourceBase,
|
|
221
|
+
}));
|
|
222
|
+
} else if (versionCmp < 0) {
|
|
223
|
+
console.log(`⚠ Remote (v${remoteGen}) is OLDER than local (v${localGen}). Downgrade refused.`);
|
|
224
|
+
} else {
|
|
200
225
|
console.log(`✅ Already up to date (v${localGen})`);
|
|
201
226
|
}
|
|
202
227
|
return;
|
|
@@ -210,11 +235,54 @@ async function main() {
|
|
|
210
235
|
const remoteSources = remoteVersion.sources || {};
|
|
211
236
|
|
|
212
237
|
/** Resolve the remote template-relative path for a dest-keyed file. Falls back to the key itself for back-compat. */
|
|
213
|
-
function remotePath(file) {
|
|
214
|
-
return remoteSources[file] || file;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
|
|
238
|
+
function remotePath(file) {
|
|
239
|
+
return remoteSources[file] || file;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function withRemoteMeta(entry) {
|
|
243
|
+
if (!entry || !entry.file || !remoteChecksums[entry.file]) return entry;
|
|
244
|
+
const templateHint = remotePath(entry.file);
|
|
245
|
+
return {
|
|
246
|
+
...entry,
|
|
247
|
+
templateHint,
|
|
248
|
+
remoteUrl: sourceBase + templateHint,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function withConflictActions(entry) {
|
|
253
|
+
return {
|
|
254
|
+
...withRemoteMeta(entry),
|
|
255
|
+
choices: ['merge', 'keep-local', 'overwrite-from-template'],
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function buildJsonPlan() {
|
|
260
|
+
return {
|
|
261
|
+
updated: plan.updated.map(withRemoteMeta),
|
|
262
|
+
created: plan.created.map(withRemoteMeta),
|
|
263
|
+
conflict: plan.conflict.map(withConflictActions),
|
|
264
|
+
skipped: plan.skipped.map(withRemoteMeta),
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function buildAgentHints(jsonPlan) {
|
|
269
|
+
return {
|
|
270
|
+
mode: 'script-first-ai-conflicts',
|
|
271
|
+
dryRunJsonCommand: 'node Harness/scripts/wf-update-check.mjs --json',
|
|
272
|
+
safeApplyCommand: 'node Harness/scripts/wf-update-check.mjs --apply-safe',
|
|
273
|
+
strictApplyCommand: 'node Harness/scripts/wf-update-check.mjs --apply',
|
|
274
|
+
partialUpdate: localVersion.partialUpdate || null,
|
|
275
|
+
aiMergeRequired: jsonPlan.conflict,
|
|
276
|
+
aiMergeRequiredCount: jsonPlan.conflict.length,
|
|
277
|
+
conflictPolicy: 'Use the script for SAFE/NEW files. For each CONFLICT file, compare local content with templateHint/remoteUrl and choose merge, keep-local, or overwrite-from-template.',
|
|
278
|
+
postUpdateCommands: [
|
|
279
|
+
'node Harness/scripts/validate-harness.mjs --strict',
|
|
280
|
+
'node Harness/scripts/scan-clean.mjs',
|
|
281
|
+
],
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const allFiles = new Set([...Object.keys(localChecksums), ...Object.keys(remoteChecksums)]);
|
|
218
286
|
|
|
219
287
|
const plan = { updated: [], created: [], conflict: [], skipped: [] };
|
|
220
288
|
|
|
@@ -255,11 +323,15 @@ async function main() {
|
|
|
255
323
|
|
|
256
324
|
const tier = classify(canonical, localHash, storedHash);
|
|
257
325
|
|
|
258
|
-
if (tier === 'PRESERVE') {
|
|
259
|
-
plan.skipped.push({ file, reason: 'PRESERVE — user data' });
|
|
260
|
-
} else if (tier === 'SAFE') {
|
|
261
|
-
|
|
262
|
-
|
|
326
|
+
if (tier === 'PRESERVE') {
|
|
327
|
+
plan.skipped.push({ file, reason: 'PRESERVE — user data' });
|
|
328
|
+
} else if (tier === 'SAFE') {
|
|
329
|
+
if (localHash === remoteHash) {
|
|
330
|
+
plan.skipped.push({ file, reason: 'already current' });
|
|
331
|
+
} else {
|
|
332
|
+
plan.updated.push({ file, remoteHash });
|
|
333
|
+
}
|
|
334
|
+
} else if (tier === 'CONFLICT') {
|
|
263
335
|
plan.conflict.push({
|
|
264
336
|
file,
|
|
265
337
|
localHash,
|
|
@@ -272,20 +344,24 @@ async function main() {
|
|
|
272
344
|
}
|
|
273
345
|
}
|
|
274
346
|
|
|
275
|
-
// 3. Output
|
|
276
|
-
if (jsonOut) {
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
plan,
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
347
|
+
// 3. Output
|
|
348
|
+
if (jsonOut) {
|
|
349
|
+
const jsonPlan = buildJsonPlan();
|
|
350
|
+
console.log(JSON.stringify({
|
|
351
|
+
status: localVersion.partialUpdate ? 'partial-update' : 'update-available',
|
|
352
|
+
from: localGen,
|
|
353
|
+
to: remoteGen,
|
|
354
|
+
sourceBase,
|
|
355
|
+
partialUpdate: localVersion.partialUpdate || null,
|
|
356
|
+
updated: plan.updated.length,
|
|
357
|
+
created: plan.created.length,
|
|
358
|
+
conflict: plan.conflict.length,
|
|
359
|
+
skipped: plan.skipped.length,
|
|
360
|
+
plan: jsonPlan,
|
|
361
|
+
agent: buildAgentHints(jsonPlan),
|
|
362
|
+
}, null, 2));
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
289
365
|
|
|
290
366
|
console.log(`\n🔄 Update: v${localGen} → v${remoteGen}`);
|
|
291
367
|
console.log(` ${plan.updated.length} safe update, ${plan.created.length} new, ${plan.conflict.length} conflict, ${plan.skipped.length} skipped\n`);
|
|
@@ -296,10 +372,13 @@ async function main() {
|
|
|
296
372
|
for (const c of plan.conflict) {
|
|
297
373
|
console.log(` 📄 ${c.file} [${c.reason}]`);
|
|
298
374
|
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
375
|
+
if (plan.updated.length + plan.created.length > 0) {
|
|
376
|
+
console.log(' Tip: run --apply-safe to apply SAFE/NEW files first, then merge conflicts.');
|
|
377
|
+
}
|
|
378
|
+
console.log('');
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Show what will be auto-updated
|
|
303
382
|
if (plan.updated.length + plan.created.length > 0) {
|
|
304
383
|
console.log('✅ AUTO (safe to apply):');
|
|
305
384
|
for (const u of plan.updated) console.log(` ↑ ${u.file}`);
|
|
@@ -308,15 +387,18 @@ async function main() {
|
|
|
308
387
|
}
|
|
309
388
|
|
|
310
389
|
// 4. Apply if requested
|
|
311
|
-
if (apply) {
|
|
390
|
+
if (apply || applySafe) {
|
|
312
391
|
// Refuse to apply when conflicts exist — must resolve first
|
|
313
|
-
if (plan.conflict.length > 0) {
|
|
392
|
+
if (apply && !applySafe && plan.conflict.length > 0) {
|
|
314
393
|
console.log(`❌ Cannot apply: ${plan.conflict.length} conflicts must be resolved first.`);
|
|
315
|
-
console.log('
|
|
316
|
-
return plan;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
|
|
394
|
+
console.log(' Run --apply-safe to apply SAFE/NEW files first, or resolve conflicts manually then re-run --apply.');
|
|
395
|
+
return plan;
|
|
396
|
+
}
|
|
397
|
+
if (applySafe && plan.conflict.length > 0) {
|
|
398
|
+
console.log(`Applying SAFE/NEW files only; ${plan.conflict.length} conflicts will remain for AI/user merge.`);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const lexists = existsSync;
|
|
320
402
|
let applied = 0;
|
|
321
403
|
let failed = 0;
|
|
322
404
|
|
|
@@ -328,7 +410,7 @@ async function main() {
|
|
|
328
410
|
if (lexists(dest)) {
|
|
329
411
|
try { if (lstatSync(dest).isSymbolicLink()) { console.error(` ✗ Symlink rejected: ${u.file}`); failed++; continue; } } catch (_) {}
|
|
330
412
|
}
|
|
331
|
-
const content = await fetchRemote(
|
|
413
|
+
const content = await fetchRemote(sourceBase + remotePath(u.file));
|
|
332
414
|
const normalized = content.replace(/\r\n/g, '\n');
|
|
333
415
|
const fetchedHash = sha256(normalized);
|
|
334
416
|
if (fetchedHash !== u.remoteHash) {
|
|
@@ -354,7 +436,7 @@ async function main() {
|
|
|
354
436
|
console.error(` ✗ File created since plan: ${c.file} — treating as CONFLICT`);
|
|
355
437
|
failed++; continue;
|
|
356
438
|
}
|
|
357
|
-
const content = await fetchRemote(
|
|
439
|
+
const content = await fetchRemote(sourceBase + remotePath(c.file));
|
|
358
440
|
const normalized = content.replace(/\r\n/g, '\n');
|
|
359
441
|
const fetchedHash = sha256(normalized);
|
|
360
442
|
if (fetchedHash !== c.remoteHash) {
|
|
@@ -370,15 +452,31 @@ async function main() {
|
|
|
370
452
|
}
|
|
371
453
|
}
|
|
372
454
|
|
|
373
|
-
// Only
|
|
374
|
-
if (failed === 0) {
|
|
375
|
-
|
|
376
|
-
localVersion.
|
|
377
|
-
for (const u of plan.updated) localVersion.checksums[u.file] = u.remoteHash;
|
|
378
|
-
for (const c of plan.created) localVersion.checksums[c.file] = c.remoteHash;
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
455
|
+
// Only version-track applied files after complete script success.
|
|
456
|
+
if (failed === 0) {
|
|
457
|
+
const appliedAt = new Date().toISOString();
|
|
458
|
+
localVersion.checksums = localVersion.checksums || {};
|
|
459
|
+
for (const u of plan.updated) localVersion.checksums[u.file] = u.remoteHash;
|
|
460
|
+
for (const c of plan.created) localVersion.checksums[c.file] = c.remoteHash;
|
|
461
|
+
if (plan.conflict.length === 0) {
|
|
462
|
+
localVersion.generator = remoteGen;
|
|
463
|
+
localVersion.generated = appliedAt;
|
|
464
|
+
delete localVersion.partialUpdate;
|
|
465
|
+
} else {
|
|
466
|
+
localVersion.partialUpdate = {
|
|
467
|
+
targetGenerator: remoteGen,
|
|
468
|
+
updatedAt: appliedAt,
|
|
469
|
+
appliedFiles: [...plan.updated, ...plan.created].map(x => x.file),
|
|
470
|
+
conflicts: plan.conflict.map(x => x.file),
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
writeFileSync(VERSION_FILE, JSON.stringify(localVersion, null, 2) + '\n', 'utf-8');
|
|
474
|
+
if (plan.conflict.length === 0) {
|
|
475
|
+
console.log(`✅ Applied ${applied} files. Version updated to ${remoteVersion.generator}.`);
|
|
476
|
+
} else {
|
|
477
|
+
console.log(`✅ Applied ${applied} SAFE/NEW files. Version remains ${localGen}; ${plan.conflict.length} conflicts still need merge.`);
|
|
478
|
+
}
|
|
479
|
+
} else {
|
|
382
480
|
console.log(`❌ ${failed} failures. NO files were version-tracked. Fix and re-run.`);
|
|
383
481
|
}
|
|
384
482
|
}
|