@indigoai-us/hq-cloud 6.14.49 → 6.15.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/.github/workflows/unreleased-commits-nag.yml +256 -0
- package/dist/bin/sync-runner-watch-loop.d.ts +12 -0
- package/dist/bin/sync-runner-watch-loop.d.ts.map +1 -1
- package/dist/bin/sync-runner-watch-loop.js +153 -25
- package/dist/bin/sync-runner-watch-loop.js.map +1 -1
- package/dist/bin/sync-runner-watch-routes.d.ts +17 -0
- package/dist/bin/sync-runner-watch-routes.d.ts.map +1 -1
- package/dist/bin/sync-runner-watch-routes.js +11 -0
- package/dist/bin/sync-runner-watch-routes.js.map +1 -1
- package/dist/bin/sync-runner-watch-routes.test.d.ts +2 -0
- package/dist/bin/sync-runner-watch-routes.test.d.ts.map +1 -0
- package/dist/bin/sync-runner-watch-routes.test.js +51 -0
- package/dist/bin/sync-runner-watch-routes.test.js.map +1 -0
- package/dist/bin/sync-runner.d.ts +11 -12
- package/dist/bin/sync-runner.d.ts.map +1 -1
- package/dist/bin/sync-runner.js.map +1 -1
- package/dist/bin/sync-runner.test.js +173 -52
- package/dist/bin/sync-runner.test.js.map +1 -1
- package/dist/cli/reindex-knowledge.d.ts +7 -0
- package/dist/cli/reindex-knowledge.d.ts.map +1 -0
- package/dist/cli/reindex-knowledge.js +389 -0
- package/dist/cli/reindex-knowledge.js.map +1 -0
- package/dist/cli/reindex-knowledge.test.d.ts +12 -0
- package/dist/cli/reindex-knowledge.test.d.ts.map +1 -0
- package/dist/cli/reindex-knowledge.test.js +260 -0
- package/dist/cli/reindex-knowledge.test.js.map +1 -0
- package/dist/cli/reindex.d.ts.map +1 -1
- package/dist/cli/reindex.js +8 -0
- package/dist/cli/reindex.js.map +1 -1
- package/dist/cli/rescue-classify-ordering.test.js +8 -2
- package/dist/cli/rescue-classify-ordering.test.js.map +1 -1
- package/dist/cli/rescue-core.d.ts +85 -0
- package/dist/cli/rescue-core.d.ts.map +1 -1
- package/dist/cli/rescue-core.js +390 -1
- package/dist/cli/rescue-core.js.map +1 -1
- package/dist/cli/rescue-settings-reconcile.test.d.ts +2 -0
- package/dist/cli/rescue-settings-reconcile.test.d.ts.map +1 -0
- package/dist/cli/rescue-settings-reconcile.test.js +523 -0
- package/dist/cli/rescue-settings-reconcile.test.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/journal.d.ts.map +1 -1
- package/dist/journal.js +81 -51
- package/dist/journal.js.map +1 -1
- package/dist/journal.test.js +11 -19
- package/dist/journal.test.js.map +1 -1
- package/dist/sync/event-sync.d.ts +8 -25
- package/dist/sync/event-sync.d.ts.map +1 -1
- package/dist/sync/event-sync.js +23 -48
- package/dist/sync/event-sync.js.map +1 -1
- package/dist/sync/event-sync.test.js +6 -32
- package/dist/sync/event-sync.test.js.map +1 -1
- package/dist/sync/feature-flags.d.ts +20 -117
- package/dist/sync/feature-flags.d.ts.map +1 -1
- package/dist/sync/feature-flags.js +24 -147
- package/dist/sync/feature-flags.js.map +1 -1
- package/dist/sync/feature-flags.test.js +15 -61
- package/dist/sync/feature-flags.test.js.map +1 -1
- package/dist/sync/index.d.ts +1 -1
- package/dist/sync/index.d.ts.map +1 -1
- package/dist/sync/index.js +1 -1
- package/dist/sync/index.js.map +1 -1
- package/dist/sync/lease-client.d.ts +67 -0
- package/dist/sync/lease-client.d.ts.map +1 -0
- package/dist/sync/lease-client.js +158 -0
- package/dist/sync/lease-client.js.map +1 -0
- package/dist/sync/lease-client.test.d.ts +2 -0
- package/dist/sync/lease-client.test.d.ts.map +1 -0
- package/dist/sync/lease-client.test.js +119 -0
- package/dist/sync/lease-client.test.js.map +1 -0
- package/dist/sync/push-receiver.d.ts +8 -19
- package/dist/sync/push-receiver.d.ts.map +1 -1
- package/dist/sync/push-receiver.js +6 -50
- package/dist/sync/push-receiver.js.map +1 -1
- package/dist/sync/push-receiver.test.js +6 -57
- package/dist/sync/push-receiver.test.js.map +1 -1
- package/dist/sync/realtime-rollout.d.ts +85 -0
- package/dist/sync/realtime-rollout.d.ts.map +1 -0
- package/dist/sync/realtime-rollout.js +206 -0
- package/dist/sync/realtime-rollout.js.map +1 -0
- package/dist/sync/realtime-rollout.test.d.ts +2 -0
- package/dist/sync/realtime-rollout.test.d.ts.map +1 -0
- package/dist/sync/realtime-rollout.test.js +71 -0
- package/dist/sync/realtime-rollout.test.js.map +1 -0
- package/dist/sync/state-store.d.ts +40 -0
- package/dist/sync/state-store.d.ts.map +1 -0
- package/dist/sync/state-store.js +680 -0
- package/dist/sync/state-store.js.map +1 -0
- package/dist/sync/state-store.test.d.ts +2 -0
- package/dist/sync/state-store.test.d.ts.map +1 -0
- package/dist/sync/state-store.test.js +159 -0
- package/dist/sync/state-store.test.js.map +1 -0
- package/dist/types.d.ts +12 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/bin/sync-runner-watch-loop.ts +176 -32
- package/src/bin/sync-runner-watch-routes.test.ts +71 -0
- package/src/bin/sync-runner-watch-routes.ts +27 -0
- package/src/bin/sync-runner.test.ts +188 -53
- package/src/bin/sync-runner.ts +11 -12
- package/src/cli/reindex-knowledge.test.ts +307 -0
- package/src/cli/reindex-knowledge.ts +450 -0
- package/src/cli/reindex.ts +9 -0
- package/src/cli/rescue-classify-ordering.test.ts +9 -3
- package/src/cli/rescue-core.ts +459 -1
- package/src/cli/rescue-settings-reconcile.test.ts +637 -0
- package/src/index.ts +2 -4
- package/src/journal.test.ts +19 -21
- package/src/journal.ts +87 -54
- package/src/sync/event-sync.test.ts +9 -44
- package/src/sync/event-sync.ts +26 -52
- package/src/sync/feature-flags.test.ts +22 -72
- package/src/sync/feature-flags.ts +32 -199
- package/src/sync/index.ts +2 -4
- package/src/sync/lease-client.test.ts +128 -0
- package/src/sync/lease-client.ts +207 -0
- package/src/sync/push-receiver.test.ts +6 -58
- package/src/sync/push-receiver.ts +9 -74
- package/src/sync/realtime-rollout.test.ts +86 -0
- package/src/sync/realtime-rollout.ts +262 -0
- package/src/sync/state-store.test.ts +194 -0
- package/src/sync/state-store.ts +727 -0
- package/src/types.ts +14 -0
- package/test/e2e/sync/transient-company-leg.test.ts +8 -4
|
@@ -97,5 +97,90 @@ export type RescueDecision = {
|
|
|
97
97
|
kind: "wholesale-replace-template";
|
|
98
98
|
rel: "companies/_template";
|
|
99
99
|
};
|
|
100
|
+
type JsonValue = string | number | boolean | null | JsonValue[] | {
|
|
101
|
+
[k: string]: JsonValue;
|
|
102
|
+
};
|
|
103
|
+
/** Structural deep equality for JSON values (stable, order-sensitive for objects). */
|
|
104
|
+
export declare function jsonDeepEqual(a: JsonValue, b: JsonValue): boolean;
|
|
105
|
+
/**
|
|
106
|
+
* Compute the drift of `ours` relative to `base` (the three-way "ours − base").
|
|
107
|
+
* Returns a partial object holding only the values `ours` added or changed
|
|
108
|
+
* versus `base`, suitable for merging into settings.local.json so Claude Code's
|
|
109
|
+
* higher-precedence Local scope re-supplies them on top of the release baseline.
|
|
110
|
+
*
|
|
111
|
+
* - object: recurse per key; a key absent from `base` is drift wholesale.
|
|
112
|
+
* - array: drift = the elements present in `ours` but not deep-contained in
|
|
113
|
+
* `base`. This matches Claude Code's union merge for permissions/hooks, so a
|
|
114
|
+
* relocated allow/deny entry unions back into the effective settings.
|
|
115
|
+
* - scalar / type-mismatch: drift = ours's value when not deep-equal.
|
|
116
|
+
*
|
|
117
|
+
* `excludePath` names dotted key paths that are NEVER drift (env.PATH — a
|
|
118
|
+
* machine-local value re-composed onto the release baseline, not relocated).
|
|
119
|
+
* Returns `undefined` when there is no drift at this node.
|
|
120
|
+
*/
|
|
121
|
+
export declare function settingsDrift(ours: JsonValue, base: JsonValue | undefined, excludePath?: string[], pathPrefix?: string): JsonValue | undefined;
|
|
122
|
+
/**
|
|
123
|
+
* Deep-merge `patch` into `target` in place and return it. Objects merge
|
|
124
|
+
* per-key; arrays union (append patch elements not already deep-present, so a
|
|
125
|
+
* re-run is idempotent and no duplicate permission/hook entries accumulate);
|
|
126
|
+
* scalars are added ONLY when the key is absent from `target`. A scalar the
|
|
127
|
+
* user already set in Local is the higher-precedence value and MUST NOT be
|
|
128
|
+
* clobbered by the lower-precedence Project drift (e.g. Project `model: opus` +
|
|
129
|
+
* Local `model: sonnet` must stay `sonnet` after rescue). Mirrors how Claude
|
|
130
|
+
* Code's Local scope overrides/unions over Project.
|
|
131
|
+
*/
|
|
132
|
+
export declare function deepMergeSettings(target: {
|
|
133
|
+
[k: string]: JsonValue;
|
|
134
|
+
}, patch: {
|
|
135
|
+
[k: string]: JsonValue;
|
|
136
|
+
}): {
|
|
137
|
+
[k: string]: JsonValue;
|
|
138
|
+
};
|
|
139
|
+
/**
|
|
140
|
+
* Apply the user's array REMOVALS (elements present in `base` but absent from
|
|
141
|
+
* `ours`) to the corresponding arrays in `theirs`, in place. A union-style Local
|
|
142
|
+
* overlay cannot express a removal, so a permission/hook a user deliberately
|
|
143
|
+
* deleted from the generated settings.json would otherwise be restored by the
|
|
144
|
+
* release baseline and never removable. Filtering `theirs` — the Project file we
|
|
145
|
+
* now own — is the only representation of that intent. Recurses through objects;
|
|
146
|
+
* only acts when all three nodes are arrays (or objects to recurse). Requires a
|
|
147
|
+
* REAL floor base: never call with base = theirs (HEAD), or release-only entries
|
|
148
|
+
* would be mistaken for removals.
|
|
149
|
+
*/
|
|
150
|
+
export declare function applyArrayRemovals(theirs: {
|
|
151
|
+
[k: string]: JsonValue;
|
|
152
|
+
}, ours: {
|
|
153
|
+
[k: string]: JsonValue;
|
|
154
|
+
}, base: {
|
|
155
|
+
[k: string]: JsonValue;
|
|
156
|
+
}): void;
|
|
157
|
+
interface ReconcileCtx {
|
|
158
|
+
hqRoot: string;
|
|
159
|
+
srcDir: string;
|
|
160
|
+
env: NodeJS.ProcessEnv;
|
|
161
|
+
historyFloor: string;
|
|
162
|
+
baselineMode: "history_floor" | "head_compare";
|
|
163
|
+
/** cfg.preserveSubpaths — used to honor an explicit `--preserve-subpath` of
|
|
164
|
+
* settings.json (or the whole `.claude` tree). */
|
|
165
|
+
preserveSubpaths: string[];
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Post-overlay reconcile for the machine-local, generated `.claude/settings.json`.
|
|
169
|
+
*
|
|
170
|
+
* At entry the overlay has already re-laid the release template (theirs) over
|
|
171
|
+
* settings.json, and the user's settings.local.json has been restored from the
|
|
172
|
+
* preserve shuttle. This function:
|
|
173
|
+
* 1. re-composes env.PATH onto theirs (machine-specific; deliverable 3), and
|
|
174
|
+
* 2. extracts ours's drift versus the last-sync floor (base) — excluding
|
|
175
|
+
* env.PATH — and merges it into settings.local.json (deliverable 2),
|
|
176
|
+
* so the EFFECTIVE merged settings (Project ⊕ Local, per Claude Code's rules)
|
|
177
|
+
* are preserved while the Project base returns to the shipped composition with
|
|
178
|
+
* any newly-released hook registrations wired in (deliverable 1).
|
|
179
|
+
*
|
|
180
|
+
* Best-effort and non-fatal: unreadable/invalid JSON degrades to a
|
|
181
|
+
* PATH-recompose-only pass (never worse than the old behavior), matching
|
|
182
|
+
* rescue's posture elsewhere.
|
|
183
|
+
*/
|
|
184
|
+
export declare function reconcileClaudeSettings(ctx: ReconcileCtx, preRescueSettingsJson: string | null, out: (s: string) => void, err: (s: string) => void): void;
|
|
100
185
|
export {};
|
|
101
186
|
//# sourceMappingURL=rescue-core.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rescue-core.d.ts","sourceRoot":"","sources":["../../src/cli/rescue-core.ts"],"names":[],"mappings":"AA6BA,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAa7B,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;CAChB;AA0CD,UAAU,MAAM;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,OAAO,CAAC;IAChB,SAAS,EAAE,OAAO,CAAC;IACnB,WAAW,EAAE,OAAO,CAAC;IACrB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,EAAE,OAAO,CAAC;IACtB,gBAAgB,EAAE,MAAM,CAAC;IACzB,QAAQ,EAAE,OAAO,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,oBAAoB,EAAE,MAAM,CAAC;CAC9B;AA2BD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAS3D;AA0FD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,CAwJtD;AAED;;;;;GAKG;AACH,wBAAgB,SAAS,CACvB,IAAI,EAAE,MAAM,EAAE,EACd,IAAI,GAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAA;CAAO,GACrC,eAAe,CAkCjB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,YAAY,GAAE,IAAI,CAAC,YAAmB,GAAG,OAAO,CAGhG;
|
|
1
|
+
{"version":3,"file":"rescue-core.d.ts","sourceRoot":"","sources":["../../src/cli/rescue-core.ts"],"names":[],"mappings":"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"}
|
package/dist/cli/rescue-core.js
CHANGED
|
@@ -466,6 +466,25 @@ function doRescue(cfg, env, out, err, setTmp) {
|
|
|
466
466
|
// which merely starts but exits non-zero used to trigger an enormous cp -a
|
|
467
467
|
// fallback, then fail later at overlay. Stop before classification/allocation.
|
|
468
468
|
requireWorkingRsync(env, err);
|
|
469
|
+
// --- Capture the pre-rescue .claude/settings.json (BEFORE the wipe) ---
|
|
470
|
+
// This is `ours` for the post-overlay settings reconcile. It must be read now
|
|
471
|
+
// because the overlay-safe classification deletes/overwrites it before
|
|
472
|
+
// reconcileClaudeSettings runs. `null` when absent (fresh install / narrow
|
|
473
|
+
// mode that excludes .claude).
|
|
474
|
+
const claudeSettingsRel = ".claude/settings.json";
|
|
475
|
+
const claudeSettingsInScope = cfg.narrowPaths.length === 0 || cfg.narrowPaths.includes(".claude");
|
|
476
|
+
let preRescueSettingsJson = null;
|
|
477
|
+
if (claudeSettingsInScope) {
|
|
478
|
+
const p = path.join(hqRoot, claudeSettingsRel);
|
|
479
|
+
if (isFileFollow(p)) {
|
|
480
|
+
try {
|
|
481
|
+
preRescueSettingsJson = fs.readFileSync(p, "utf-8");
|
|
482
|
+
}
|
|
483
|
+
catch {
|
|
484
|
+
preRescueSettingsJson = null;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
469
488
|
// --- Read prior sync-point metadata (BEFORE the wipe) ---
|
|
470
489
|
let prevSyncSha = "";
|
|
471
490
|
let prevSyncSource = "";
|
|
@@ -957,6 +976,16 @@ function doRescue(cfg, env, out, err, setTmp) {
|
|
|
957
976
|
out(` restored ${rel}\n`);
|
|
958
977
|
}
|
|
959
978
|
}
|
|
979
|
+
// --- Reconcile the generated .claude/settings.json ---
|
|
980
|
+
// Runs AFTER the overlay (which re-laid the release template, wiring in any
|
|
981
|
+
// newly-released hook registrations) and AFTER the preserve-subpath restore
|
|
982
|
+
// (which brought back the user's .claude/settings.local.json). Re-composes the
|
|
983
|
+
// machine env.PATH onto the release baseline and relocates genuine user drift
|
|
984
|
+
// (vs the last-sync floor) into settings.local.json so the effective merged
|
|
985
|
+
// settings are preserved. No-op in narrow mode that excludes .claude.
|
|
986
|
+
if (claudeSettingsInScope) {
|
|
987
|
+
reconcileClaudeSettings({ hqRoot, srcDir, env, historyFloor, baselineMode, preserveSubpaths: cfg.preserveSubpaths }, preRescueSettingsJson, out, err);
|
|
988
|
+
}
|
|
960
989
|
// --- Restore executable bit on shipped shebang scripts (defense-in-depth) ---
|
|
961
990
|
// rsync -a already propagated each file's upstream git tree mode; this guards
|
|
962
991
|
// the case where upstream itself committed an executable script as 0644 -- a
|
|
@@ -1344,7 +1373,17 @@ function isOverwriteSafe(rel) {
|
|
|
1344
1373
|
return (rel === "AGENTS.md" ||
|
|
1345
1374
|
rel === "USER-GUIDE.md" ||
|
|
1346
1375
|
rel === "core/docs/hq/USER-GUIDE.md" ||
|
|
1347
|
-
rel === "core/policies/_digest.md"
|
|
1376
|
+
rel === "core/policies/_digest.md" ||
|
|
1377
|
+
// `.claude/settings.json` is a machine-local *generated* file (setup.sh
|
|
1378
|
+
// step 3b composes env.PATH into the release template). It always differs
|
|
1379
|
+
// from every release blob because of that PATH, so the generic classifier
|
|
1380
|
+
// would rescue it into personal/ and lay a PATH-less template on top —
|
|
1381
|
+
// dropping the machine PATH and (depending on drift/history state) failing
|
|
1382
|
+
// to wire in newly-released hook registrations. Instead let the overlay
|
|
1383
|
+
// always re-lay the release template (no personal/ copy), then
|
|
1384
|
+
// reconcileClaudeSettings() re-composes env.PATH and relocates genuine user
|
|
1385
|
+
// drift into settings.local.json. See reconcileClaudeSettings.
|
|
1386
|
+
rel === ".claude/settings.json");
|
|
1348
1387
|
}
|
|
1349
1388
|
function masterSyncSymlinkTarget(localPath) {
|
|
1350
1389
|
const st = lstatOrNull(localPath);
|
|
@@ -1509,6 +1548,356 @@ function diffAppendClaudeMd(ctx) {
|
|
|
1509
1548
|
ctx.out(" diff-appended: .claude/CLAUDE.md additions -> personal/CLAUDE.md\n");
|
|
1510
1549
|
ctx.counts.claudeDiffAppend += 1;
|
|
1511
1550
|
}
|
|
1551
|
+
function isPlainObject(v) {
|
|
1552
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1553
|
+
}
|
|
1554
|
+
/** Structural deep equality for JSON values (stable, order-sensitive for objects). */
|
|
1555
|
+
export function jsonDeepEqual(a, b) {
|
|
1556
|
+
if (a === b)
|
|
1557
|
+
return true;
|
|
1558
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
1559
|
+
if (a.length !== b.length)
|
|
1560
|
+
return false;
|
|
1561
|
+
return a.every((x, i) => jsonDeepEqual(x, b[i]));
|
|
1562
|
+
}
|
|
1563
|
+
if (isPlainObject(a) && isPlainObject(b)) {
|
|
1564
|
+
const ak = Object.keys(a);
|
|
1565
|
+
const bk = Object.keys(b);
|
|
1566
|
+
if (ak.length !== bk.length)
|
|
1567
|
+
return false;
|
|
1568
|
+
return ak.every((k) => k in b && jsonDeepEqual(a[k], b[k]));
|
|
1569
|
+
}
|
|
1570
|
+
return false;
|
|
1571
|
+
}
|
|
1572
|
+
function jsonParseOrNull(text) {
|
|
1573
|
+
if (text == null)
|
|
1574
|
+
return null;
|
|
1575
|
+
try {
|
|
1576
|
+
return JSON.parse(text);
|
|
1577
|
+
}
|
|
1578
|
+
catch {
|
|
1579
|
+
return null;
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
/**
|
|
1583
|
+
* Compute the drift of `ours` relative to `base` (the three-way "ours − base").
|
|
1584
|
+
* Returns a partial object holding only the values `ours` added or changed
|
|
1585
|
+
* versus `base`, suitable for merging into settings.local.json so Claude Code's
|
|
1586
|
+
* higher-precedence Local scope re-supplies them on top of the release baseline.
|
|
1587
|
+
*
|
|
1588
|
+
* - object: recurse per key; a key absent from `base` is drift wholesale.
|
|
1589
|
+
* - array: drift = the elements present in `ours` but not deep-contained in
|
|
1590
|
+
* `base`. This matches Claude Code's union merge for permissions/hooks, so a
|
|
1591
|
+
* relocated allow/deny entry unions back into the effective settings.
|
|
1592
|
+
* - scalar / type-mismatch: drift = ours's value when not deep-equal.
|
|
1593
|
+
*
|
|
1594
|
+
* `excludePath` names dotted key paths that are NEVER drift (env.PATH — a
|
|
1595
|
+
* machine-local value re-composed onto the release baseline, not relocated).
|
|
1596
|
+
* Returns `undefined` when there is no drift at this node.
|
|
1597
|
+
*/
|
|
1598
|
+
export function settingsDrift(ours, base, excludePath = ["env.PATH"], pathPrefix = "") {
|
|
1599
|
+
const here = pathPrefix;
|
|
1600
|
+
if (excludePath.includes(here))
|
|
1601
|
+
return undefined;
|
|
1602
|
+
if (base === undefined) {
|
|
1603
|
+
// Whole subtree is new in ours — but still honor exclusions nested under it.
|
|
1604
|
+
if (isPlainObject(ours)) {
|
|
1605
|
+
const out = {};
|
|
1606
|
+
let any = false;
|
|
1607
|
+
for (const k of Object.keys(ours)) {
|
|
1608
|
+
const child = settingsDrift(ours[k], undefined, excludePath, here ? `${here}.${k}` : k);
|
|
1609
|
+
if (child !== undefined) {
|
|
1610
|
+
out[k] = child;
|
|
1611
|
+
any = true;
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
return any ? out : undefined;
|
|
1615
|
+
}
|
|
1616
|
+
return ours;
|
|
1617
|
+
}
|
|
1618
|
+
if (isPlainObject(ours) && isPlainObject(base)) {
|
|
1619
|
+
const out = {};
|
|
1620
|
+
let any = false;
|
|
1621
|
+
for (const k of Object.keys(ours)) {
|
|
1622
|
+
const child = settingsDrift(ours[k], k in base ? base[k] : undefined, excludePath, here ? `${here}.${k}` : k);
|
|
1623
|
+
if (child !== undefined) {
|
|
1624
|
+
out[k] = child;
|
|
1625
|
+
any = true;
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
return any ? out : undefined;
|
|
1629
|
+
}
|
|
1630
|
+
if (Array.isArray(ours) && Array.isArray(base)) {
|
|
1631
|
+
const extra = ours.filter((el) => !base.some((b) => jsonDeepEqual(el, b)));
|
|
1632
|
+
return extra.length > 0 ? extra : undefined;
|
|
1633
|
+
}
|
|
1634
|
+
return jsonDeepEqual(ours, base) ? undefined : ours;
|
|
1635
|
+
}
|
|
1636
|
+
/**
|
|
1637
|
+
* Deep-merge `patch` into `target` in place and return it. Objects merge
|
|
1638
|
+
* per-key; arrays union (append patch elements not already deep-present, so a
|
|
1639
|
+
* re-run is idempotent and no duplicate permission/hook entries accumulate);
|
|
1640
|
+
* scalars are added ONLY when the key is absent from `target`. A scalar the
|
|
1641
|
+
* user already set in Local is the higher-precedence value and MUST NOT be
|
|
1642
|
+
* clobbered by the lower-precedence Project drift (e.g. Project `model: opus` +
|
|
1643
|
+
* Local `model: sonnet` must stay `sonnet` after rescue). Mirrors how Claude
|
|
1644
|
+
* Code's Local scope overrides/unions over Project.
|
|
1645
|
+
*/
|
|
1646
|
+
export function deepMergeSettings(target, patch) {
|
|
1647
|
+
for (const k of Object.keys(patch)) {
|
|
1648
|
+
const pv = patch[k];
|
|
1649
|
+
const tv = target[k];
|
|
1650
|
+
if (Array.isArray(pv) && Array.isArray(tv)) {
|
|
1651
|
+
for (const el of pv) {
|
|
1652
|
+
if (!tv.some((x) => jsonDeepEqual(x, el)))
|
|
1653
|
+
tv.push(el);
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
else if (isPlainObject(pv) && isPlainObject(tv)) {
|
|
1657
|
+
deepMergeSettings(tv, pv);
|
|
1658
|
+
}
|
|
1659
|
+
else if (!(k in target)) {
|
|
1660
|
+
// Key absent from Local — safe to introduce the relocated drift value.
|
|
1661
|
+
target[k] = pv;
|
|
1662
|
+
}
|
|
1663
|
+
// else: Local already defines this scalar (or a differently-typed value);
|
|
1664
|
+
// it is the higher-precedence override and is left untouched.
|
|
1665
|
+
}
|
|
1666
|
+
return target;
|
|
1667
|
+
}
|
|
1668
|
+
/**
|
|
1669
|
+
* Apply the user's array REMOVALS (elements present in `base` but absent from
|
|
1670
|
+
* `ours`) to the corresponding arrays in `theirs`, in place. A union-style Local
|
|
1671
|
+
* overlay cannot express a removal, so a permission/hook a user deliberately
|
|
1672
|
+
* deleted from the generated settings.json would otherwise be restored by the
|
|
1673
|
+
* release baseline and never removable. Filtering `theirs` — the Project file we
|
|
1674
|
+
* now own — is the only representation of that intent. Recurses through objects;
|
|
1675
|
+
* only acts when all three nodes are arrays (or objects to recurse). Requires a
|
|
1676
|
+
* REAL floor base: never call with base = theirs (HEAD), or release-only entries
|
|
1677
|
+
* would be mistaken for removals.
|
|
1678
|
+
*/
|
|
1679
|
+
export function applyArrayRemovals(theirs, ours, base) {
|
|
1680
|
+
for (const k of Object.keys(base)) {
|
|
1681
|
+
const bv = base[k];
|
|
1682
|
+
const ov = ours[k];
|
|
1683
|
+
const tv = theirs[k];
|
|
1684
|
+
if (Array.isArray(bv) && Array.isArray(ov) && Array.isArray(tv)) {
|
|
1685
|
+
const removed = bv.filter((b) => !ov.some((o) => jsonDeepEqual(o, b)));
|
|
1686
|
+
if (removed.length > 0) {
|
|
1687
|
+
theirs[k] = tv.filter((t) => !removed.some((r) => jsonDeepEqual(r, t)));
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
else if (isPlainObject(bv) && isPlainObject(ov) && isPlainObject(tv)) {
|
|
1691
|
+
applyArrayRemovals(tv, ov, bv);
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
/**
|
|
1696
|
+
* Re-compose the machine env.PATH for the freshly-overlaid release template.
|
|
1697
|
+
* Runs the SAME composer setup.sh uses (core/scripts/compose-settings-path.sh)
|
|
1698
|
+
* with the machine's own PATH as the base — preferring the pre-rescue snapshot
|
|
1699
|
+
* so a working PATH is never degraded by a possibly-truncated rescue-process
|
|
1700
|
+
* PATH — so the value is always machine-specific and never a hardcoded release
|
|
1701
|
+
* PATH (the release template ships none). Prefers the release clone's composer,
|
|
1702
|
+
* falls back to the overlaid one; if neither runs, carries `basePath` verbatim.
|
|
1703
|
+
*/
|
|
1704
|
+
function composeSettingsPath(srcDir, hqRoot, basePath, env) {
|
|
1705
|
+
const candidates = [
|
|
1706
|
+
path.join(srcDir, "core", "scripts", "compose-settings-path.sh"),
|
|
1707
|
+
path.join(hqRoot, "core", "scripts", "compose-settings-path.sh"),
|
|
1708
|
+
];
|
|
1709
|
+
for (const script of candidates) {
|
|
1710
|
+
if (!isFileFollow(script))
|
|
1711
|
+
continue;
|
|
1712
|
+
const r = run("bash", [script, basePath], { env });
|
|
1713
|
+
const composed = r.stdout.replace(/\n+$/, "");
|
|
1714
|
+
if (r.status === 0 && composed)
|
|
1715
|
+
return composed;
|
|
1716
|
+
}
|
|
1717
|
+
return basePath;
|
|
1718
|
+
}
|
|
1719
|
+
/** True when `.claude/settings.json` is covered by an EXPLICIT preserve subpath
|
|
1720
|
+
* (the caller asked rescue to keep it, so the reconcile must not rewrite it).
|
|
1721
|
+
* The always-on carve-outs never cover settings.json, so any match here is a
|
|
1722
|
+
* deliberate caller request. */
|
|
1723
|
+
function settingsExplicitlyPreserved(preserveSubpaths) {
|
|
1724
|
+
const rel = ".claude/settings.json";
|
|
1725
|
+
for (const sp of preserveSubpaths) {
|
|
1726
|
+
const s = sp.replace(/\/$/, "");
|
|
1727
|
+
if (s === rel || s === ".claude" || rel.startsWith(s + "/"))
|
|
1728
|
+
return true;
|
|
1729
|
+
}
|
|
1730
|
+
return false;
|
|
1731
|
+
}
|
|
1732
|
+
/**
|
|
1733
|
+
* Post-overlay reconcile for the machine-local, generated `.claude/settings.json`.
|
|
1734
|
+
*
|
|
1735
|
+
* At entry the overlay has already re-laid the release template (theirs) over
|
|
1736
|
+
* settings.json, and the user's settings.local.json has been restored from the
|
|
1737
|
+
* preserve shuttle. This function:
|
|
1738
|
+
* 1. re-composes env.PATH onto theirs (machine-specific; deliverable 3), and
|
|
1739
|
+
* 2. extracts ours's drift versus the last-sync floor (base) — excluding
|
|
1740
|
+
* env.PATH — and merges it into settings.local.json (deliverable 2),
|
|
1741
|
+
* so the EFFECTIVE merged settings (Project ⊕ Local, per Claude Code's rules)
|
|
1742
|
+
* are preserved while the Project base returns to the shipped composition with
|
|
1743
|
+
* any newly-released hook registrations wired in (deliverable 1).
|
|
1744
|
+
*
|
|
1745
|
+
* Best-effort and non-fatal: unreadable/invalid JSON degrades to a
|
|
1746
|
+
* PATH-recompose-only pass (never worse than the old behavior), matching
|
|
1747
|
+
* rescue's posture elsewhere.
|
|
1748
|
+
*/
|
|
1749
|
+
export function reconcileClaudeSettings(ctx, preRescueSettingsJson, out, err) {
|
|
1750
|
+
const { hqRoot, srcDir, env, historyFloor, baselineMode, preserveSubpaths } = ctx;
|
|
1751
|
+
const settingsPath = path.join(hqRoot, ".claude", "settings.json");
|
|
1752
|
+
const localPath = path.join(hqRoot, ".claude", "settings.local.json");
|
|
1753
|
+
// The caller explicitly asked to keep settings.json (via `--preserve-subpath
|
|
1754
|
+
// .claude/settings.json` or `--preserve-subpath .claude`). The shuttle already
|
|
1755
|
+
// restored their file; honor that and do not rewrite it (nor land new
|
|
1756
|
+
// registrations — that is the point of an explicit preserve).
|
|
1757
|
+
if (settingsExplicitlyPreserved(preserveSubpaths)) {
|
|
1758
|
+
out("==> Skipped .claude/settings.json reconcile (explicitly preserved by --preserve-subpath)\n");
|
|
1759
|
+
return;
|
|
1760
|
+
}
|
|
1761
|
+
const overlaidTemplateExists = isFileFollow(settingsPath);
|
|
1762
|
+
if (!overlaidTemplateExists) {
|
|
1763
|
+
// The release no longer ships .claude/settings.json, but the overwrite-safe
|
|
1764
|
+
// classification already deleted the local file. Restore the pre-rescue copy
|
|
1765
|
+
// so the machine PATH and every user customization survive (critical under
|
|
1766
|
+
// --no-backup, where no snapshot exists to recover from).
|
|
1767
|
+
if (preRescueSettingsJson !== null) {
|
|
1768
|
+
try {
|
|
1769
|
+
fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
|
|
1770
|
+
fs.writeFileSync(settingsPath, preRescueSettingsJson);
|
|
1771
|
+
out("==> Release no longer ships .claude/settings.json; restored your existing settings.json (no reconcile).\n");
|
|
1772
|
+
}
|
|
1773
|
+
catch {
|
|
1774
|
+
err(" WARN: could not restore pre-rescue .claude/settings.json after the release dropped the template\n");
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
return;
|
|
1778
|
+
}
|
|
1779
|
+
const theirsRaw = (() => {
|
|
1780
|
+
try {
|
|
1781
|
+
return fs.readFileSync(settingsPath, "utf-8");
|
|
1782
|
+
}
|
|
1783
|
+
catch {
|
|
1784
|
+
return null;
|
|
1785
|
+
}
|
|
1786
|
+
})();
|
|
1787
|
+
const theirs = jsonParseOrNull(theirsRaw);
|
|
1788
|
+
if (!isPlainObject(theirs)) {
|
|
1789
|
+
err(" WARN: overlaid .claude/settings.json is not valid JSON; skipping settings reconcile\n");
|
|
1790
|
+
return;
|
|
1791
|
+
}
|
|
1792
|
+
const ours = jsonParseOrNull(preRescueSettingsJson);
|
|
1793
|
+
const oursIsValid = isPlainObject(ours);
|
|
1794
|
+
// The pre-rescue settings.json existed but was not parseable, and the overlay
|
|
1795
|
+
// already replaced it. Preserve the raw bytes under personal/ (the same place
|
|
1796
|
+
// a user-edit rescue would have put them) so a temporarily-malformed file is
|
|
1797
|
+
// never destroyed by a --no-backup run. Then continue with a PATH-recompose
|
|
1798
|
+
// pass so the machine still works.
|
|
1799
|
+
if (preRescueSettingsJson !== null && !oursIsValid) {
|
|
1800
|
+
const backup = path.join(hqRoot, "personal", ".claude", "settings.json");
|
|
1801
|
+
try {
|
|
1802
|
+
fs.mkdirSync(path.dirname(backup), { recursive: true });
|
|
1803
|
+
fs.writeFileSync(backup, preRescueSettingsJson);
|
|
1804
|
+
err(" WARN: pre-rescue .claude/settings.json was not valid JSON; preserved a copy at personal/.claude/settings.json\n");
|
|
1805
|
+
}
|
|
1806
|
+
catch {
|
|
1807
|
+
err(" WARN: pre-rescue .claude/settings.json was not valid JSON and could not be backed up\n");
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
const oursEnvPath = oursIsValid && isPlainObject(ours.env) && typeof ours.env.PATH === "string"
|
|
1811
|
+
? ours.env.PATH
|
|
1812
|
+
: undefined;
|
|
1813
|
+
// 1) Re-compose env.PATH onto the release baseline. Base = the machine's own
|
|
1814
|
+
// prior PATH snapshot when present, else this process's PATH.
|
|
1815
|
+
const basePath = oursEnvPath ?? env.PATH ?? "";
|
|
1816
|
+
const composedPath = composeSettingsPath(srcDir, hqRoot, basePath, env);
|
|
1817
|
+
if (composedPath) {
|
|
1818
|
+
const envObj = isPlainObject(theirs.env) ? theirs.env : (theirs.env = {});
|
|
1819
|
+
envObj.PATH = composedPath;
|
|
1820
|
+
}
|
|
1821
|
+
// 2) Relocate genuine user drift (ours vs floor baseline) into settings.local,
|
|
1822
|
+
// and apply user array removals to the Project baseline.
|
|
1823
|
+
let driftKeys = 0;
|
|
1824
|
+
let relocationFailed = false;
|
|
1825
|
+
if (oursIsValid) {
|
|
1826
|
+
// realBase = settings.json at the last-sync floor (only trustworthy in
|
|
1827
|
+
// history_floor mode). Removals require it; additions fall back to theirs.
|
|
1828
|
+
let realBase;
|
|
1829
|
+
if (baselineMode === "history_floor" && historyFloor) {
|
|
1830
|
+
const show = run("git", ["show", `${historyFloor}:.claude/settings.json`], {
|
|
1831
|
+
cwd: srcDir,
|
|
1832
|
+
env,
|
|
1833
|
+
});
|
|
1834
|
+
if (show.status === 0)
|
|
1835
|
+
realBase = jsonParseOrNull(show.stdout);
|
|
1836
|
+
}
|
|
1837
|
+
// Removals: only with a REAL floor base (never theirs — release-only entries
|
|
1838
|
+
// would look like removals).
|
|
1839
|
+
if (isPlainObject(realBase)) {
|
|
1840
|
+
applyArrayRemovals(theirs, ours, realBase);
|
|
1841
|
+
}
|
|
1842
|
+
// Additions drift: base = realBase when available, else theirs (conservative
|
|
1843
|
+
// head_compare fallback — never loses a customization). See design note.
|
|
1844
|
+
const additionsBase = isPlainObject(realBase) ? realBase : theirs;
|
|
1845
|
+
const drift = settingsDrift(ours, additionsBase);
|
|
1846
|
+
if (isPlainObject(drift) && Object.keys(drift).length > 0) {
|
|
1847
|
+
const existingLocal = jsonParseOrNull((() => {
|
|
1848
|
+
try {
|
|
1849
|
+
return isFileFollow(localPath) ? fs.readFileSync(localPath, "utf-8") : null;
|
|
1850
|
+
}
|
|
1851
|
+
catch {
|
|
1852
|
+
return null;
|
|
1853
|
+
}
|
|
1854
|
+
})());
|
|
1855
|
+
const localObj = isPlainObject(existingLocal) ? existingLocal : {};
|
|
1856
|
+
deepMergeSettings(localObj, drift);
|
|
1857
|
+
driftKeys = Object.keys(drift).length;
|
|
1858
|
+
try {
|
|
1859
|
+
fs.mkdirSync(path.dirname(localPath), { recursive: true });
|
|
1860
|
+
fs.writeFileSync(localPath, JSON.stringify(localObj, null, 2) + "\n");
|
|
1861
|
+
}
|
|
1862
|
+
catch {
|
|
1863
|
+
relocationFailed = true;
|
|
1864
|
+
err(" WARN: could not write .claude/settings.local.json during settings reconcile\n");
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
// If drift relocation into settings.local.json failed, DO NOT overwrite
|
|
1869
|
+
// settings.json with the release baseline — that would drop the drift with no
|
|
1870
|
+
// on-disk copy under --no-backup. Restore the pre-rescue file so nothing is
|
|
1871
|
+
// lost; the update simply isn't applied to settings this run (the user can
|
|
1872
|
+
// retry once settings.local.json is writable).
|
|
1873
|
+
if (relocationFailed) {
|
|
1874
|
+
if (preRescueSettingsJson !== null) {
|
|
1875
|
+
try {
|
|
1876
|
+
fs.writeFileSync(settingsPath, preRescueSettingsJson);
|
|
1877
|
+
}
|
|
1878
|
+
catch {
|
|
1879
|
+
err(" WARN: could not restore pre-rescue .claude/settings.json after a failed drift relocation\n");
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
err(" WARN: kept your original .claude/settings.json (settings update not applied — settings.local.json was unwritable)\n");
|
|
1883
|
+
return;
|
|
1884
|
+
}
|
|
1885
|
+
// Write the re-composed release baseline back to settings.json.
|
|
1886
|
+
try {
|
|
1887
|
+
fs.writeFileSync(settingsPath, JSON.stringify(theirs, null, 2) + "\n");
|
|
1888
|
+
}
|
|
1889
|
+
catch {
|
|
1890
|
+
err(" WARN: could not write re-composed .claude/settings.json during reconcile\n");
|
|
1891
|
+
return;
|
|
1892
|
+
}
|
|
1893
|
+
if (driftKeys > 0) {
|
|
1894
|
+
out(`==> Reconciled .claude/settings.json to release baseline (env.PATH re-composed; ` +
|
|
1895
|
+
`relocated ${driftKeys} drift key(s) -> .claude/settings.local.json)\n`);
|
|
1896
|
+
}
|
|
1897
|
+
else {
|
|
1898
|
+
out("==> Reconciled .claude/settings.json to release baseline (env.PATH re-composed; no local drift to relocate)\n");
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1512
1901
|
/**
|
|
1513
1902
|
* Defense-in-depth for executable scaffold scripts. `rescue` rebuilds the tree
|
|
1514
1903
|
* with `git clone` + `rsync -a`, faithfully propagating each file's git tree
|