@indigoai-us/hq-cloud 6.4.0 → 6.6.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/dist/bin/sync-runner.d.ts +4 -35
- package/dist/bin/sync-runner.d.ts.map +1 -1
- package/dist/bin/sync-runner.js +14 -104
- package/dist/bin/sync-runner.js.map +1 -1
- package/dist/bin/sync-runner.test.js +19 -0
- package/dist/bin/sync-runner.test.js.map +1 -1
- package/dist/cli/rescue-classify-ordering.test.d.ts +2 -0
- package/dist/cli/rescue-classify-ordering.test.d.ts.map +1 -0
- package/dist/cli/rescue-classify-ordering.test.js +241 -0
- package/dist/cli/rescue-classify-ordering.test.js.map +1 -0
- package/dist/cli/rescue-core.js +68 -22
- package/dist/cli/rescue-core.js.map +1 -1
- package/dist/cli/sync-scope.test.js +67 -0
- package/dist/cli/sync-scope.test.js.map +1 -1
- package/dist/cli/sync.d.ts +19 -0
- package/dist/cli/sync.d.ts.map +1 -1
- package/dist/cli/sync.js +62 -19
- package/dist/cli/sync.js.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/dist/s3.d.ts.map +1 -1
- package/dist/s3.js +15 -5
- package/dist/s3.js.map +1 -1
- package/dist/s3.test.js +71 -2
- package/dist/s3.test.js.map +1 -1
- package/dist/scope-shrink.d.ts +70 -7
- package/dist/scope-shrink.d.ts.map +1 -1
- package/dist/scope-shrink.js +102 -23
- package/dist/scope-shrink.js.map +1 -1
- package/dist/scope-shrink.test.js +63 -0
- package/dist/scope-shrink.test.js.map +1 -1
- package/dist/sync/pull-scope.d.ts +50 -0
- package/dist/sync/pull-scope.d.ts.map +1 -0
- package/dist/sync/pull-scope.js +129 -0
- package/dist/sync/pull-scope.js.map +1 -0
- package/package.json +1 -1
- package/src/bin/sync-runner.test.ts +23 -0
- package/src/bin/sync-runner.ts +19 -116
- package/src/cli/rescue-classify-ordering.test.ts +263 -0
- package/src/cli/rescue-core.ts +82 -26
- package/src/cli/sync-scope.test.ts +84 -0
- package/src/cli/sync.ts +90 -17
- package/src/index.ts +11 -0
- package/src/s3.test.ts +91 -1
- package/src/s3.ts +15 -5
- package/src/scope-shrink.test.ts +71 -0
- package/src/scope-shrink.ts +164 -20
- package/src/sync/pull-scope.ts +161 -0
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration tests for the rescue classifier's fail-safe ordering and its
|
|
3
|
+
* handling of nested directory symlinks (src/cli/rescue-core.ts).
|
|
4
|
+
*
|
|
5
|
+
* Regression for DEV-1767 (feedback_aa768683): `hq rescue` choked on a
|
|
6
|
+
* symlinked DIRECTORY under core/ — a reindex artifact such as
|
|
7
|
+
* `core/knowledge/ad-creative-engine -> ../../personal/knowledge/...` — throwing
|
|
8
|
+
* `Error: Path is a directory` mid-pass *after* it had already deleted ~10 core
|
|
9
|
+
* scaffold files, leaving HQ half-applied. `--check` classified the same tree
|
|
10
|
+
* cleanly, so the dry-run gave false confidence.
|
|
11
|
+
*
|
|
12
|
+
* Two invariants are locked here:
|
|
13
|
+
* 1. A directory symlink under core/ is classified as a droppable reindex
|
|
14
|
+
* symlink (never followed, never file-read, never a crash) — exactly like
|
|
15
|
+
* a file symlink. The personal/ target it points at is left untouched.
|
|
16
|
+
* 2. Classification is read-only and runs to completion BEFORE any destructive
|
|
17
|
+
* apply. A classifier exception therefore deletes NOTHING (no half-applied
|
|
18
|
+
* wipe), and the same classify code path backs both the live run and the
|
|
19
|
+
* `--dry-run` plan (parity).
|
|
20
|
+
*
|
|
21
|
+
* Like the sibling rescue tests, the source clone is shimmed to a local fixture
|
|
22
|
+
* repo and the rescue runs in-process so we can assert its on-disk effects.
|
|
23
|
+
*/
|
|
24
|
+
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
25
|
+
import { execFileSync } from "child_process";
|
|
26
|
+
import * as fs from "fs";
|
|
27
|
+
import * as os from "os";
|
|
28
|
+
import * as path from "path";
|
|
29
|
+
import { runRescue } from "./rescue-core.js";
|
|
30
|
+
function hasGit() {
|
|
31
|
+
try {
|
|
32
|
+
execFileSync("git", ["--version"], { stdio: "ignore" });
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const gitAvailable = hasGit();
|
|
40
|
+
function runRescueCapture(argv, env) {
|
|
41
|
+
let stdout = "";
|
|
42
|
+
let stderr = "";
|
|
43
|
+
const origOut = process.stdout.write.bind(process.stdout);
|
|
44
|
+
const origErr = process.stderr.write.bind(process.stderr);
|
|
45
|
+
process.stdout.write = ((chunk) => {
|
|
46
|
+
stdout += String(chunk);
|
|
47
|
+
return true;
|
|
48
|
+
});
|
|
49
|
+
process.stderr.write = ((chunk) => {
|
|
50
|
+
stderr += String(chunk);
|
|
51
|
+
return true;
|
|
52
|
+
});
|
|
53
|
+
let status;
|
|
54
|
+
let threw;
|
|
55
|
+
try {
|
|
56
|
+
status = runRescue(argv, { env }).status;
|
|
57
|
+
}
|
|
58
|
+
catch (e) {
|
|
59
|
+
threw = e;
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
process.stdout.write = origOut;
|
|
63
|
+
process.stderr.write = origErr;
|
|
64
|
+
}
|
|
65
|
+
return { status, stdout, stderr, threw };
|
|
66
|
+
}
|
|
67
|
+
const lexists = (p) => {
|
|
68
|
+
try {
|
|
69
|
+
fs.lstatSync(p);
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
describe.skipIf(!gitAvailable)("rescue classify-before-delete + dir-symlink handling", () => {
|
|
77
|
+
let workDir;
|
|
78
|
+
let upstream;
|
|
79
|
+
let shimDir;
|
|
80
|
+
let floorSha;
|
|
81
|
+
let baseEnv;
|
|
82
|
+
let caseSeq = 0;
|
|
83
|
+
const git = (cwd, ...args) => execFileSync("git", args, {
|
|
84
|
+
cwd,
|
|
85
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
86
|
+
env: {
|
|
87
|
+
...process.env,
|
|
88
|
+
GIT_AUTHOR_NAME: "t",
|
|
89
|
+
GIT_AUTHOR_EMAIL: "t@t",
|
|
90
|
+
GIT_COMMITTER_NAME: "t",
|
|
91
|
+
GIT_COMMITTER_EMAIL: "t@t",
|
|
92
|
+
},
|
|
93
|
+
})
|
|
94
|
+
.toString()
|
|
95
|
+
.trim();
|
|
96
|
+
beforeAll(() => {
|
|
97
|
+
workDir = fs.mkdtempSync(path.join(os.tmpdir(), "hq-rescue-ordering-"));
|
|
98
|
+
// Upstream: floor has core/a.md="v1"; HEAD advances it to "v2". core/b.md is
|
|
99
|
+
// stable across both. The symlink fixtures never appear upstream — they only
|
|
100
|
+
// exist in the local HQ root as reindex artifacts.
|
|
101
|
+
upstream = path.join(workDir, "upstream");
|
|
102
|
+
fs.mkdirSync(path.join(upstream, "core"), { recursive: true });
|
|
103
|
+
git(workDir, "init", "-b", "main", "upstream");
|
|
104
|
+
fs.writeFileSync(path.join(upstream, "core/a.md"), "v1\n");
|
|
105
|
+
fs.writeFileSync(path.join(upstream, "core/b.md"), "beta\n");
|
|
106
|
+
git(upstream, "add", "-A");
|
|
107
|
+
git(upstream, "commit", "-m", "floor");
|
|
108
|
+
floorSha = git(upstream, "rev-parse", "HEAD");
|
|
109
|
+
fs.writeFileSync(path.join(upstream, "core/a.md"), "v2\n");
|
|
110
|
+
git(upstream, "add", "-A");
|
|
111
|
+
git(upstream, "commit", "-m", "head");
|
|
112
|
+
const realGit = execFileSync("bash", ["-lc", "command -v git"]).toString().trim() || "/usr/bin/git";
|
|
113
|
+
shimDir = path.join(workDir, "shim");
|
|
114
|
+
fs.mkdirSync(shimDir, { recursive: true });
|
|
115
|
+
const shim = `#!/usr/bin/env bash
|
|
116
|
+
if [ "$1" = "clone" ]; then
|
|
117
|
+
args=()
|
|
118
|
+
for a in "$@"; do
|
|
119
|
+
case "$a" in
|
|
120
|
+
https://github.com/*) a=${JSON.stringify(upstream)} ;;
|
|
121
|
+
esac
|
|
122
|
+
args+=("$a")
|
|
123
|
+
done
|
|
124
|
+
exec ${JSON.stringify(realGit)} "\${args[@]}"
|
|
125
|
+
fi
|
|
126
|
+
exec ${JSON.stringify(realGit)} "$@"
|
|
127
|
+
`;
|
|
128
|
+
fs.writeFileSync(path.join(shimDir, "git"), shim, { mode: 0o755 });
|
|
129
|
+
baseEnv = { ...process.env, PATH: `${shimDir}:${process.env.PATH ?? ""}` };
|
|
130
|
+
});
|
|
131
|
+
afterAll(() => {
|
|
132
|
+
if (workDir)
|
|
133
|
+
fs.rmSync(workDir, { recursive: true, force: true });
|
|
134
|
+
});
|
|
135
|
+
/**
|
|
136
|
+
* Fresh HQ root per case (live runs mutate). core/a.md matches the floor
|
|
137
|
+
* (unchanged vs baseline, differs from HEAD -> "delete + replace"); core/b.md
|
|
138
|
+
* is byte-identical to upstream ("unchanged, preserved in place"). The caller
|
|
139
|
+
* adds whatever symlink/extra files the case needs.
|
|
140
|
+
*/
|
|
141
|
+
function makeHqRoot() {
|
|
142
|
+
caseSeq += 1;
|
|
143
|
+
const hqRoot = path.join(workDir, `hq-${caseSeq}`);
|
|
144
|
+
fs.mkdirSync(path.join(hqRoot, "core"), { recursive: true });
|
|
145
|
+
fs.mkdirSync(path.join(hqRoot, "companies"), { recursive: true });
|
|
146
|
+
fs.mkdirSync(path.join(hqRoot, "personal/knowledge"), { recursive: true });
|
|
147
|
+
fs.writeFileSync(path.join(hqRoot, "core/a.md"), "v1\n");
|
|
148
|
+
fs.writeFileSync(path.join(hqRoot, "core/b.md"), "beta\n");
|
|
149
|
+
return hqRoot;
|
|
150
|
+
}
|
|
151
|
+
const liveArgs = (hqRoot, extra = []) => [
|
|
152
|
+
"--hq-root", hqRoot,
|
|
153
|
+
"--source", "test/repo",
|
|
154
|
+
"--ref", "main",
|
|
155
|
+
"--floor-sha", floorSha,
|
|
156
|
+
"--paths", "core",
|
|
157
|
+
"--yes",
|
|
158
|
+
"--no-backup",
|
|
159
|
+
...extra,
|
|
160
|
+
];
|
|
161
|
+
it("drops a DIRECTORY symlink under core/ as a reindex artifact (no crash, no half-apply)", () => {
|
|
162
|
+
const hqRoot = makeHqRoot();
|
|
163
|
+
// The exact DEV-1767 shape: a directory symlink nested under core/ pointing
|
|
164
|
+
// into personal/. Its target exists and must survive untouched.
|
|
165
|
+
const target = path.join(hqRoot, "personal/knowledge/ad-creative-engine");
|
|
166
|
+
fs.mkdirSync(target, { recursive: true });
|
|
167
|
+
fs.writeFileSync(path.join(target, "keep.md"), "owned by personal\n");
|
|
168
|
+
fs.mkdirSync(path.join(hqRoot, "core/knowledge"), { recursive: true });
|
|
169
|
+
const link = path.join(hqRoot, "core/knowledge/ad-creative-engine");
|
|
170
|
+
fs.symlinkSync("../../personal/knowledge/ad-creative-engine", link);
|
|
171
|
+
const r = runRescueCapture(liveArgs(hqRoot), baseEnv);
|
|
172
|
+
const out = `${r.stdout}\n${r.stderr}`;
|
|
173
|
+
expect(r.threw, out).toBeUndefined();
|
|
174
|
+
expect(r.status, out).toBe(0);
|
|
175
|
+
// The bash-era failure signature must never appear.
|
|
176
|
+
expect(out).not.toMatch(/Path is a directory/);
|
|
177
|
+
expect(out).not.toMatch(/EISDIR/);
|
|
178
|
+
// Classified + dropped as exactly one reindex symlink.
|
|
179
|
+
expect(out).toMatch(/reindex symlinks dropped:\s+1 entries/);
|
|
180
|
+
// Symlink itself is gone; its personal/ target is untouched.
|
|
181
|
+
expect(lexists(link)).toBe(false);
|
|
182
|
+
expect(fs.readFileSync(path.join(target, "keep.md"), "utf-8")).toBe("owned by personal\n");
|
|
183
|
+
// Real core files still classified correctly around it.
|
|
184
|
+
expect(fs.existsSync(path.join(hqRoot, "core/a.md"))).toBe(true);
|
|
185
|
+
expect(fs.existsSync(path.join(hqRoot, "core/b.md"))).toBe(true);
|
|
186
|
+
});
|
|
187
|
+
it("drops a FILE symlink under core/ the same way (file-symlink behavior unchanged)", () => {
|
|
188
|
+
const hqRoot = makeHqRoot();
|
|
189
|
+
fs.writeFileSync(path.join(hqRoot, "personal/knowledge/note.md"), "personal note\n");
|
|
190
|
+
const link = path.join(hqRoot, "core/note.md");
|
|
191
|
+
fs.symlinkSync("../personal/knowledge/note.md", link);
|
|
192
|
+
const r = runRescueCapture(liveArgs(hqRoot), baseEnv);
|
|
193
|
+
const out = `${r.stdout}\n${r.stderr}`;
|
|
194
|
+
expect(r.threw, out).toBeUndefined();
|
|
195
|
+
expect(r.status, out).toBe(0);
|
|
196
|
+
expect(out).toMatch(/reindex symlinks dropped:\s+1 entries/);
|
|
197
|
+
expect(lexists(link)).toBe(false);
|
|
198
|
+
expect(fs.readFileSync(path.join(hqRoot, "personal/knowledge/note.md"), "utf-8")).toBe("personal note\n");
|
|
199
|
+
});
|
|
200
|
+
it("a classifier exception deletes NOTHING (classify fully before any destructive apply)", () => {
|
|
201
|
+
const hqRoot = makeHqRoot();
|
|
202
|
+
// core/a.md is classified BEFORE core/zzz.md (sorted walk) as a
|
|
203
|
+
// "delete + replace". core/zzz.md then throws during classification. If the
|
|
204
|
+
// wipe were interleaved (old behavior), a.md would already be gone.
|
|
205
|
+
fs.writeFileSync(path.join(hqRoot, "core/zzz.md"), "trigger\n");
|
|
206
|
+
const r = runRescueCapture(liveArgs(hqRoot), {
|
|
207
|
+
...baseEnv,
|
|
208
|
+
HQ_RESCUE_FAULT_AT_REL: "core/zzz.md",
|
|
209
|
+
});
|
|
210
|
+
const out = `${r.stdout}\n${r.stderr}`;
|
|
211
|
+
// The injected classifier fault aborts the run.
|
|
212
|
+
expect(r.threw, out).toBeDefined();
|
|
213
|
+
expect(String(r.threw)).toMatch(/fault injected at core\/zzz\.md/);
|
|
214
|
+
// Crucially: the file that was already classified for deletion still exists,
|
|
215
|
+
// untouched — no half-applied wipe.
|
|
216
|
+
expect(fs.existsSync(path.join(hqRoot, "core/a.md"))).toBe(true);
|
|
217
|
+
expect(fs.readFileSync(path.join(hqRoot, "core/a.md"), "utf-8")).toBe("v1\n");
|
|
218
|
+
expect(fs.existsSync(path.join(hqRoot, "core/b.md"))).toBe(true);
|
|
219
|
+
expect(fs.existsSync(path.join(hqRoot, "core/zzz.md"))).toBe(true);
|
|
220
|
+
});
|
|
221
|
+
it("dry-run classifies the dir-symlink identically to the live run and changes nothing", () => {
|
|
222
|
+
const hqRoot = makeHqRoot();
|
|
223
|
+
const target = path.join(hqRoot, "personal/knowledge/ad-creative-engine");
|
|
224
|
+
fs.mkdirSync(target, { recursive: true });
|
|
225
|
+
fs.mkdirSync(path.join(hqRoot, "core/knowledge"), { recursive: true });
|
|
226
|
+
const link = path.join(hqRoot, "core/knowledge/ad-creative-engine");
|
|
227
|
+
fs.symlinkSync("../../personal/knowledge/ad-creative-engine", link);
|
|
228
|
+
const r = runRescueCapture(liveArgs(hqRoot, ["--dry-run"]), baseEnv);
|
|
229
|
+
const out = `${r.stdout}\n${r.stderr}`;
|
|
230
|
+
expect(r.threw, out).toBeUndefined();
|
|
231
|
+
expect(r.status, out).toBe(0);
|
|
232
|
+
// Dry-run surfaces the SAME classification the live pass acts on.
|
|
233
|
+
expect(out).toContain("drop reindex symlink: core/knowledge/ad-creative-engine");
|
|
234
|
+
expect(out).toMatch(/reindex symlinks dropped:\s+1 entries/);
|
|
235
|
+
expect(out).toMatch(/DRY RUN complete\. No filesystem changes made\./);
|
|
236
|
+
// ...but touches nothing: the symlink (and everything else) is still on disk.
|
|
237
|
+
expect(lexists(link)).toBe(true);
|
|
238
|
+
expect(fs.existsSync(path.join(hqRoot, "core/a.md"))).toBe(true);
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
//# sourceMappingURL=rescue-classify-ordering.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rescue-classify-ordering.test.js","sourceRoot":"","sources":["../../src/cli/rescue-classify-ordering.test.ts"],"names":[],"mappings":"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"}
|
package/dist/cli/rescue-core.js
CHANGED
|
@@ -609,6 +609,7 @@ function doRescue(cfg, env, out, err, setTmp) {
|
|
|
609
609
|
unchangedList,
|
|
610
610
|
appendLog,
|
|
611
611
|
out,
|
|
612
|
+
actions: [],
|
|
612
613
|
};
|
|
613
614
|
// --- Pre-operation safety snapshot (BEFORE any destructive op) ---
|
|
614
615
|
let backupDir = "";
|
|
@@ -636,7 +637,17 @@ function doRescue(cfg, env, out, err, setTmp) {
|
|
|
636
637
|
}
|
|
637
638
|
out(` snapshot complete (restore any file: cp "${backupDir}/<relpath>" "${hqRoot}/<relpath>")\n`);
|
|
638
639
|
}
|
|
639
|
-
// --- Walk + classify ---
|
|
640
|
+
// --- Walk + classify (PHASE 1: read-only) ---
|
|
641
|
+
// Classification mutates nothing on disk: every destructive op (delete,
|
|
642
|
+
// rescue-move, conflict-quarantine, symlink-drop) is recorded as a deferred
|
|
643
|
+
// closure in `ctx.actions` and executed later (PHASE 2). This is the
|
|
644
|
+
// ordering-safety invariant: if classification throws partway through the
|
|
645
|
+
// wipe set (e.g. an unreadable entry, a future unhandled file shape), it
|
|
646
|
+
// aborts HERE — before a single file has been touched — instead of leaving a
|
|
647
|
+
// half-applied wipe like the pre-port bash classifier did (DEV-1767:
|
|
648
|
+
// `Error: Path is a directory` on a nested dir-symlink, thrown after ~10 core
|
|
649
|
+
// files were already deleted). Dry-run runs this exact same pass and then
|
|
650
|
+
// returns, so the `--check` plan can never miss a condition the live pass hits.
|
|
640
651
|
out("\n");
|
|
641
652
|
if (wipeToplevel.length === 0) {
|
|
642
653
|
out("==> Wipe set is empty; nothing to process or overlay.\n");
|
|
@@ -708,6 +719,13 @@ function doRescue(cfg, env, out, err, setTmp) {
|
|
|
708
719
|
out("==> DRY RUN complete. No filesystem changes made.\n");
|
|
709
720
|
return { status: 0 };
|
|
710
721
|
}
|
|
722
|
+
// --- Apply classified actions (PHASE 2: destructive) ---
|
|
723
|
+
// The full wipe set classified without throwing, so it is now safe to mutate.
|
|
724
|
+
// Actions run in classification (walk) order — the same order the old
|
|
725
|
+
// interleaved walk mutated in — so per-file output and on-disk results are
|
|
726
|
+
// unchanged versus before the two-phase split.
|
|
727
|
+
for (const act of ctx.actions)
|
|
728
|
+
act();
|
|
711
729
|
// --- Back up preserve-subpaths to a mktemp shuttle ---
|
|
712
730
|
const shuttle = path.join(tmpdir, "preserve");
|
|
713
731
|
fs.mkdirSync(shuttle, { recursive: true });
|
|
@@ -1139,10 +1157,21 @@ function conflictOne(ctx, rel) {
|
|
|
1139
1157
|
ctx.appendLog(`conflicted\t${rel}\t-> ${destRel}\n`);
|
|
1140
1158
|
}
|
|
1141
1159
|
// --- classify + act on one file (the per-file workhorse) ---
|
|
1160
|
+
//
|
|
1161
|
+
// Read-only by contract: this records intent (counts + dry-run plan lines +
|
|
1162
|
+
// deferred `ctx.actions` closures) but never mutates the filesystem itself. The
|
|
1163
|
+
// closures are run later by the PHASE 2 apply loop, only once the whole wipe
|
|
1164
|
+
// set has classified without throwing. Keep it that way — a stray `fs.rmSync`
|
|
1165
|
+
// here re-opens the half-applied-wipe hole this split closed (DEV-1767).
|
|
1142
1166
|
function processOne(ctx, rel) {
|
|
1143
1167
|
const { cfg } = ctx;
|
|
1144
1168
|
const localPath = path.join(ctx.hqRoot, rel);
|
|
1145
1169
|
const srcPath = path.join(ctx.srcDir, rel);
|
|
1170
|
+
// Test-only fault seam: prove the classify-before-delete ordering invariant
|
|
1171
|
+
// by forcing a classifier throw at a chosen entry. Never set in production.
|
|
1172
|
+
if (ctx.env.HQ_RESCUE_FAULT_AT_REL && rel === ctx.env.HQ_RESCUE_FAULT_AT_REL) {
|
|
1173
|
+
throw new Error(`rescue classifier fault injected at ${rel} (HQ_RESCUE_FAULT_AT_REL)`);
|
|
1174
|
+
}
|
|
1146
1175
|
if (isUnderPreserve(cfg, rel))
|
|
1147
1176
|
return;
|
|
1148
1177
|
// Conflict-resolution artifacts (`<name>.conflict-<ts>-<peer>.<ext>`).
|
|
@@ -1152,7 +1181,7 @@ function processOne(ctx, rel) {
|
|
|
1152
1181
|
ctx.out(` drop conflict artifact: ${rel}\n`);
|
|
1153
1182
|
}
|
|
1154
1183
|
else {
|
|
1155
|
-
fs.rmSync(localPath, { force: true });
|
|
1184
|
+
ctx.actions.push(() => fs.rmSync(localPath, { force: true }));
|
|
1156
1185
|
}
|
|
1157
1186
|
return;
|
|
1158
1187
|
}
|
|
@@ -1162,7 +1191,7 @@ function processOne(ctx, rel) {
|
|
|
1162
1191
|
ctx.out(` skip script-managed (rewrites at stamp step): ${rel}\n`);
|
|
1163
1192
|
}
|
|
1164
1193
|
else {
|
|
1165
|
-
fs.rmSync(localPath, { force: true });
|
|
1194
|
+
ctx.actions.push(() => fs.rmSync(localPath, { force: true }));
|
|
1166
1195
|
}
|
|
1167
1196
|
return;
|
|
1168
1197
|
}
|
|
@@ -1182,8 +1211,10 @@ function processOne(ctx, rel) {
|
|
|
1182
1211
|
ctx.out(` drop reindex symlink: ${rel} -> ${tgt}\n`);
|
|
1183
1212
|
}
|
|
1184
1213
|
else {
|
|
1185
|
-
|
|
1186
|
-
|
|
1214
|
+
ctx.actions.push(() => {
|
|
1215
|
+
fs.rmSync(localPath, { force: true });
|
|
1216
|
+
ctx.appendLog(`symlink-dropped\t${rel}\t(reindex regenerable)\n`);
|
|
1217
|
+
});
|
|
1187
1218
|
}
|
|
1188
1219
|
}
|
|
1189
1220
|
return;
|
|
@@ -1233,8 +1264,10 @@ function processOne(ctx, rel) {
|
|
|
1233
1264
|
ctx.out(` cloud-symlink reconciled (unchanged): ${rel} (hq-symlink: marker matches upstream target)\n`);
|
|
1234
1265
|
}
|
|
1235
1266
|
else {
|
|
1236
|
-
|
|
1237
|
-
|
|
1267
|
+
ctx.actions.push(() => {
|
|
1268
|
+
fs.rmSync(localPath, { force: true });
|
|
1269
|
+
ctx.appendLog(`cloud-symlink-reconciled\t${rel}\t(hq-symlink: marker matches upstream target; overlay re-lays symlink)\n`);
|
|
1270
|
+
});
|
|
1238
1271
|
}
|
|
1239
1272
|
return;
|
|
1240
1273
|
}
|
|
@@ -1245,8 +1278,10 @@ function processOne(ctx, rel) {
|
|
|
1245
1278
|
ctx.out(` drift reconciled (identical to upstream HEAD; no rescue): ${rel}\n`);
|
|
1246
1279
|
}
|
|
1247
1280
|
else {
|
|
1248
|
-
|
|
1249
|
-
|
|
1281
|
+
ctx.actions.push(() => {
|
|
1282
|
+
fs.rmSync(localPath, { force: true });
|
|
1283
|
+
ctx.appendLog(`drift-reconciled\t${rel}\t(identical to upstream HEAD; drifted from floor only — no personal/ copy)\n`);
|
|
1284
|
+
});
|
|
1250
1285
|
}
|
|
1251
1286
|
return;
|
|
1252
1287
|
}
|
|
@@ -1271,18 +1306,20 @@ function processOne(ctx, rel) {
|
|
|
1271
1306
|
}
|
|
1272
1307
|
else {
|
|
1273
1308
|
if (rel === ".claude/CLAUDE.md") {
|
|
1274
|
-
rescueOne(ctx, rel);
|
|
1309
|
+
ctx.actions.push(() => rescueOne(ctx, rel));
|
|
1275
1310
|
}
|
|
1276
1311
|
else if (isOverwriteSafe(rel)) {
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1312
|
+
ctx.actions.push(() => {
|
|
1313
|
+
fs.rmSync(localPath, { force: true });
|
|
1314
|
+
ctx.counts.userOverwrite += 1;
|
|
1315
|
+
ctx.appendLog(`overwritten\t${rel}\t(overwrite-safe; upstream wins, no copy preserved)\n`);
|
|
1316
|
+
});
|
|
1280
1317
|
}
|
|
1281
1318
|
else if (isConflictClass(rel)) {
|
|
1282
|
-
conflictOne(ctx, rel);
|
|
1319
|
+
ctx.actions.push(() => conflictOne(ctx, rel));
|
|
1283
1320
|
}
|
|
1284
1321
|
else {
|
|
1285
|
-
rescueOne(ctx, rel);
|
|
1322
|
+
ctx.actions.push(() => rescueOne(ctx, rel));
|
|
1286
1323
|
}
|
|
1287
1324
|
}
|
|
1288
1325
|
}
|
|
@@ -1294,8 +1331,10 @@ function processOne(ctx, rel) {
|
|
|
1294
1331
|
ctx.out(` unchanged (preserved in place): ${rel}\n`);
|
|
1295
1332
|
}
|
|
1296
1333
|
else {
|
|
1297
|
-
|
|
1298
|
-
|
|
1334
|
+
ctx.actions.push(() => {
|
|
1335
|
+
fs.appendFileSync(ctx.unchangedList, `/${rel}\n`);
|
|
1336
|
+
ctx.appendLog(`unchanged\t${rel}\t(identical to upstream; left in place, mtime preserved)\n`);
|
|
1337
|
+
});
|
|
1299
1338
|
}
|
|
1300
1339
|
}
|
|
1301
1340
|
else {
|
|
@@ -1303,8 +1342,10 @@ function processOne(ctx, rel) {
|
|
|
1303
1342
|
ctx.out(` unchanged (delete + replace): ${rel}\n`);
|
|
1304
1343
|
}
|
|
1305
1344
|
else {
|
|
1306
|
-
|
|
1307
|
-
|
|
1345
|
+
ctx.actions.push(() => {
|
|
1346
|
+
fs.rmSync(localPath, { force: true });
|
|
1347
|
+
ctx.appendLog(`deleted\t${rel}\t(unchanged vs baseline; re-laid by overlay if still upstream)\n`);
|
|
1348
|
+
});
|
|
1308
1349
|
}
|
|
1309
1350
|
}
|
|
1310
1351
|
}
|
|
@@ -1321,7 +1362,10 @@ function walkAndProcess(ctx, rootRel) {
|
|
|
1321
1362
|
ctx.out(" wholesale-replace: companies/_template (template carve-out)\n");
|
|
1322
1363
|
}
|
|
1323
1364
|
else {
|
|
1324
|
-
fs.rmSync(path.join(ctx.hqRoot, "companies", "_template"), {
|
|
1365
|
+
ctx.actions.push(() => fs.rmSync(path.join(ctx.hqRoot, "companies", "_template"), {
|
|
1366
|
+
recursive: true,
|
|
1367
|
+
force: true,
|
|
1368
|
+
}));
|
|
1325
1369
|
}
|
|
1326
1370
|
return;
|
|
1327
1371
|
}
|
|
@@ -1341,8 +1385,10 @@ function walkAndProcess(ctx, rootRel) {
|
|
|
1341
1385
|
ctx.out(` drop reindex symlink: ${rootRel} -> ${tgt}\n`);
|
|
1342
1386
|
}
|
|
1343
1387
|
else {
|
|
1344
|
-
|
|
1345
|
-
|
|
1388
|
+
ctx.actions.push(() => {
|
|
1389
|
+
fs.rmSync(rootAbs, { force: true });
|
|
1390
|
+
ctx.appendLog(`symlink-dropped\t${rootRel}\t(reindex regenerable)\n`);
|
|
1391
|
+
});
|
|
1346
1392
|
}
|
|
1347
1393
|
}
|
|
1348
1394
|
return;
|