@ngockhoale/ukit 2.7.6 → 2.7.8
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/CHANGELOG.md +105 -0
- package/package.json +1 -1
- package/scripts/install/sync-installed-mirror.mjs +250 -0
- package/scripts/perf/audit-perf.mjs +287 -36
- package/scripts/perf/diff-perf-findings.mjs +136 -0
- package/scripts/perf/perf-findings.json +260 -206
- package/scripts/perf/perf-measure.md +271 -0
- package/src/context/detectProjectContext.js +5 -0
- package/src/core/codeintel/invalidation.js +4 -0
- package/src/core/fileOps.js +40 -117
- package/src/core/hookChainDoctor.js +65 -2
- package/src/core/memory/store.js +22 -1
- package/src/core/taskBudgetValidator.js +9 -6
- package/src/core/unattendedDoctor.js +8 -1
- package/src/render/buildVariables.js +10 -0
- package/templates/.claude/agents/bug-debugger.md +1 -1
- package/templates/.claude/agents/feature-implementer.md +2 -2
- package/templates/.claude/commands/ukit/handoff-create.md +1 -1
- package/templates/.claude/commands/ukit/handoff-fullstack.md +1 -1
- package/templates/.claude/commands/ukit/handoff-implement.md +1 -1
- package/templates/.claude/commands/ukit/handoff-review.md +1 -1
- package/templates/.claude/hooks/auto-prune-bash.sh +19 -0
- package/templates/.claude/hooks/context-hardcap-gate.sh +4 -1
- package/templates/.claude/hooks/handoff-model-guard.sh +22 -11
- package/templates/.claude/hooks/reinject-context.sh +22 -0
- package/templates/.claude/hooks/reset-compact-pressure.sh +29 -0
- package/templates/.claude/hooks/session-episode.sh +20 -0
- package/templates/.claude/hooks/skill-router.sh +15 -8
- package/templates/.claude/hooks/verification-guard.sh +3 -0
- package/templates/.claude/ukit/index/route-task.mjs +237 -32
- package/templates/.claude/ukit/index/task-budget-validator.mjs +6 -2
- package/templates/.claude/ukit/runtime/async-lock.mjs +144 -10
- package/templates/.claude/ukit/runtime/compact-threshold.mjs +5 -2
- package/templates/.claude/ukit/runtime/execution-ledger.mjs +217 -17
- package/templates/.claude/ukit/runtime/hook-chain-runner.mjs +156 -24
- package/templates/.claude/ukit/runtime/hook-payload-store.mjs +57 -0
- package/templates/.claude/ukit/runtime/hook-telemetry.mjs +84 -12
- package/templates/.claude/ukit/runtime/hook-telemetry.sh +50 -0
- package/templates/.claude/ukit/runtime/stop-coordinator.mjs +35 -20
- package/templates/.claude/ukit/runtime/token-utils.mjs +37 -126
- package/templates/.codex/settings.json +1 -5
- package/templates/.omp/agents/bug-debugger.md +1 -1
- package/templates/.omp/agents/feature-implementer.md +2 -2
- package/templates/.omp/hooks/pre/ukit-bridge.js +157 -26
- package/templates/docs/AI_HANDOFF/INDEX.md +1 -1
- package/templates/docs/AI_HANDOFF/RULES.md +6 -6
- package/templates/ukit/storage/config.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,111 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to UKit are documented here.
|
|
4
4
|
|
|
5
|
+
## 2.7.8 - 2026-09-22
|
|
6
|
+
|
|
7
|
+
Stall/hang fixes — cycle C43 (TASK-001..008): the indefinite-stall producers
|
|
8
|
+
root-caused in `docs/AI_REPORT/2026-09-21-NORMAL_CHAT_STALL.md` are fixed
|
|
9
|
+
across Claude Code, omp, and Codex runtimes.
|
|
10
|
+
|
|
11
|
+
- **CX-8**: route-state writes are atomic + locked — torn `skill-router-state.json`
|
|
12
|
+
writes can no longer wedge the router.
|
|
13
|
+
- **OMP-3/F-2**: omp receipt dedupe — a tool result is recorded once; duplicate
|
|
14
|
+
receipts no longer double-count execution records.
|
|
15
|
+
- **RC-4/OMP-1/F-3/F-4**: breaker-freeze family — budgeted reclaim, pstart
|
|
16
|
+
stamps, fail-open streaks; continuation sidecar merge gate stamps
|
|
17
|
+
`lastContinuationAt` strictly newer than its base and materializes counter
|
|
18
|
+
state when the main ledger file is absent.
|
|
19
|
+
- **F-5/F-5b**: `withFileLock` policy + reclaim — stale locks reclaimed via
|
|
20
|
+
pstart mismatch; recycled-pid regression tests added.
|
|
21
|
+
- **F-15**: verifier-classification — verification outcomes classified so a
|
|
22
|
+
non-verification failure no longer masquerades as a test failure.
|
|
23
|
+
- **F-RC-2**: `hook-chain-runner` exits promptly after verdict — bounded stdout
|
|
24
|
+
flush then `process.exit`; an abandoned in-proc step promise can no longer
|
|
25
|
+
pin the event loop for the full registered timeout.
|
|
26
|
+
- **F-10/OMP-2**: omp Stop parity — omp sessions get the same handoff-cursor
|
|
27
|
+
Stop-gate lane as Claude Code.
|
|
28
|
+
- **OMP-6/F-8/CX-10**: `createPayloadReference` staging runs under a deadline
|
|
29
|
+
with fail-open to inline payload (never throws, host loop not frozen); omp
|
|
30
|
+
SessionStart carries `session_id` so `reset-compact-pressure` no longer
|
|
31
|
+
wipes every session's pressure records; codex `requiredBeforeCompletion`
|
|
32
|
+
renders only commands that exist in `package.json`.
|
|
33
|
+
- Housekeeping: stale "Kilo" tool labels scrubbed from handoff docs, agent
|
|
34
|
+
report contracts, and config help text (functional support was already
|
|
35
|
+
removed; the regression tests and gateway comments documenting that removal
|
|
36
|
+
are intentionally kept).
|
|
37
|
+
|
|
38
|
+
## 2.7.7 - 2026-09-21
|
|
39
|
+
|
|
40
|
+
Audit-report remediation + hook observability — cycle C42 (TASK-001..011): all 12
|
|
41
|
+
findings from the three 2026-09-20 reports closed; the reports moved to
|
|
42
|
+
`docs/AI_REPORT/archive/`.
|
|
43
|
+
|
|
44
|
+
- B1: `hook-chain-runner` short-circuits on a permission decision under
|
|
45
|
+
`--emit-verdict`. A `hookSpecificOutput` decision on a step now owns the
|
|
46
|
+
verdict and stops the chain there, so the decision JSON is emitted as the
|
|
47
|
+
whole stdout instead of being concatenated in front of the next step's
|
|
48
|
+
output, and a genuinely-skipped fail-closed gate is no longer exempted from
|
|
49
|
+
the fail-closed verdict. C8 (same task): an invalid `:N` budget suffix
|
|
50
|
+
(`:0`, `:-1`) warns and runs the step on its default budget instead of
|
|
51
|
+
silently parsing; a non-numeric suffix stays a literal path.
|
|
52
|
+
- B2: `yarn test <files>` — the repo's dominant verification form — is now
|
|
53
|
+
recognized by the verification-minutes estimator alongside `yarn vitest
|
|
54
|
+
[run]`, so task budgets are no longer inflated by the 1-minute flat
|
|
55
|
+
fallback.
|
|
56
|
+
- B3: doctor strips `{{...}}` placeholders before `parse()`ing the omp config
|
|
57
|
+
template, so a placeholder in key position no longer makes the `yaml` lib
|
|
58
|
+
emit "Keys with collection values will be stringified" on every run.
|
|
59
|
+
- C5: new scoped `scripts/install/sync-installed-mirror.mjs` (`--check` reports
|
|
60
|
+
drift, bare run rewrites). It reuses the real install pipeline
|
|
61
|
+
(`buildInstallPlan` → `diffInstallPlan` → `applyDiffResults`) so drift is
|
|
62
|
+
decided by exactly the code `ukit install` uses, and it only ever touches
|
|
63
|
+
`.claude/**`/`.omp/**` plan targets — never `src/`, `docs/`, or user-authored
|
|
64
|
+
files. Parity coverage widened to `.claude/ukit/index/`.
|
|
65
|
+
- C7: hook-chain failure surfaces pinned by
|
|
66
|
+
`tests/hooks/hookChainFailureSurface.test.js` (output overflow on a
|
|
67
|
+
fail-closed gate, budget exhausted mid-chain, signal death); the decision to
|
|
68
|
+
drop non-verdict stderr is documented in `UKIT_INTERNALS.md`.
|
|
69
|
+
- O1: telemetry wired into the four previously unmeasured hooks
|
|
70
|
+
(`reinject-context`, `auto-prune-bash`, `reset-compact-pressure`,
|
|
71
|
+
`session-episode`) via a no-staging arm path in the `hook-telemetry` engine.
|
|
72
|
+
Four hooks now emit rows where they emitted none.
|
|
73
|
+
- O2: audit snapshots carry provenance (`generatedAt` + telemetry row/file
|
|
74
|
+
counts), and `scripts/perf/diff-perf-findings.mjs` diffs two snapshots per
|
|
75
|
+
finding id without touching live telemetry.
|
|
76
|
+
- O3: telemetry rows split stdin-stage time from execution time (`stageMs`),
|
|
77
|
+
so a slow producer is distinguishable from a slow step.
|
|
78
|
+
- O4/O6: telemetry consumers audited and the consolidated row shape pinned
|
|
79
|
+
(`tests/hooks/chainTelemetryRowShape.test.js`); doctor escalates when a
|
|
80
|
+
failure actually landed on a chain carrying a fail-closed gate or exhausted
|
|
81
|
+
its budget — purely additive to the existing detail/remedy.
|
|
82
|
+
- O5: telemetry append overhead measured; numbers recorded in
|
|
83
|
+
`scripts/perf/perf-measure.md`.
|
|
84
|
+
- Fixed `tests/consistency/ompDocsSync.test.js` case 4: the frozen-history
|
|
85
|
+
guard treated the `RULES.md` §Clear Handoff step-2 archive rotation (delete
|
|
86
|
+
the oldest cycle, fold it into `HISTORY.md`) as tampering, so the test was
|
|
87
|
+
red at HEAD and at the `v2.7.2`/`v2.7.5` tags. The guard now exempts a
|
|
88
|
+
cycle-dir deletion only when a matching `HISTORY.md` row exists and the
|
|
89
|
+
post-rotation archive is back within the 3-dir cap; rewrites, renames, and
|
|
90
|
+
unrecorded deletions still fail.
|
|
91
|
+
- Fixed `src/core/memory/store.js`: the session-archive cap appended and
|
|
92
|
+
`slice(-cap)`ed, so it evicted by insertion position instead of age — a
|
|
93
|
+
session archived late carrying an older end time outlived a newer one written
|
|
94
|
+
earlier. It now drops the oldest by end time (ties keep insertion order),
|
|
95
|
+
matching the key `archiveSessions` already uses. The regression case in
|
|
96
|
+
`tests/core/sessionArchiveBound.test.js` had a `Date.now()`-per-session
|
|
97
|
+
fixture whose outcome depended on whether the clock ticked mid-array, so it
|
|
98
|
+
passed on an unloaded box and failed under load; the fixture is now
|
|
99
|
+
deterministic and the case fails against the old code.
|
|
100
|
+
- `withFileLock` (both `templates/.claude/ukit/runtime/token-utils.mjs` and its
|
|
101
|
+
`src/core/fileOps.js` twin) is now fail-closed: a lock wait that exceeds
|
|
102
|
+
`maxWaitMs` skips the mutation instead of running it unlocked, and the drop is
|
|
103
|
+
journaled to `<file>.lock-drops.jsonl` (bounded 128-record JSONL). Both twins
|
|
104
|
+
delegate to `async-lock.mjs`, so owner stamping (pid + token + pstart),
|
|
105
|
+
recycled-pid detection, and claim+quarantine stale reclaim exist exactly once;
|
|
106
|
+
a stale lock whose recorded pid was recycled is now reclaimed instead of
|
|
107
|
+
wedging every mutation for that file. `writeJson`/`writeFileAtomic` fall back
|
|
108
|
+
to copy+unlink when `rename` fails with EXDEV (cross-mount tmp dirs).
|
|
109
|
+
|
|
5
110
|
## 2.7.6 - 2026-09-20
|
|
6
111
|
|
|
7
112
|
Permission posture work — cycle C41 (TASK-001..004): omp prompts aligned to
|
package/package.json
CHANGED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// C5 (docs/AI_REPORT/archive/2026-09-20-CORRECTNESS.md §C5) — scoped mirror sync/check.
|
|
3
|
+
//
|
|
4
|
+
// The installed `.claude/` + `.omp/` mirror is a *rendered* copy of `templates/.claude`
|
|
5
|
+
// + `templates/.omp`, and hand-copying it drifted three separate ways this cycle:
|
|
6
|
+
// a template change never re-copied, a raw copy that kept its literal `{{token}}`
|
|
7
|
+
// because nobody rendered it, and `settings.json`'s two gateway-injected env keys,
|
|
8
|
+
// which make whole-file byte identity impossible by construction. Parallel lanes
|
|
9
|
+
// overwrote each other because there was no tool — only whoever remembered.
|
|
10
|
+
//
|
|
11
|
+
// node scripts/install/sync-installed-mirror.mjs --check report drift, exit non-zero
|
|
12
|
+
// node scripts/install/sync-installed-mirror.mjs rewrite drifted targets
|
|
13
|
+
//
|
|
14
|
+
// This reuses the real install pipeline (`buildInstallPlan` → `diffInstallPlan` →
|
|
15
|
+
// `applyDiffResults` → `applyGatewayResilienceEnv`) rather than reimplementing it, so
|
|
16
|
+
// "drift" is decided by exactly the code `ukit install` uses — including the
|
|
17
|
+
// gateway-env carve-out in `src/core/diffPlan.js` and the render step in
|
|
18
|
+
// `src/render/buildTemplateVariables.js`. Render-tokenized templates are compared in
|
|
19
|
+
// rendered form; `settings.json` is compared section-wise, never byte-wise.
|
|
20
|
+
//
|
|
21
|
+
// Scope is deliberately narrow — the tool only ever considers plan targets that live
|
|
22
|
+
// under `.claude/**` or `.omp/**`, so it can never touch `src/`, `docs/`, or any
|
|
23
|
+
// user-authored file:
|
|
24
|
+
//
|
|
25
|
+
// * `mergeStrategy: 'skip'` entries (e.g. `.claude/settings.local.json`) are
|
|
26
|
+
// user-owned, not template-derived. They are excluded from drift AND from apply —
|
|
27
|
+
// a hands-off file must never read as drift, and this tool is not an installer.
|
|
28
|
+
// * Mirror-only detection is limited to the target directories of *selected,
|
|
29
|
+
// non-link* manifest items — directories UKit owns wholesale from `templates/`.
|
|
30
|
+
// A blanket walk of `.claude/` reports ~226 false positives, because pack-gated
|
|
31
|
+
// skill libraries (e.g. `.claude/skills/frontend-vue/**` on a `core`-only project)
|
|
32
|
+
// are present in the mirror but intentionally outside the current plan. Those
|
|
33
|
+
// directories are covered by `tests/consistency/templateParity.test.js` instead.
|
|
34
|
+
import fs from 'node:fs/promises';
|
|
35
|
+
import path from 'node:path';
|
|
36
|
+
import { fileURLToPath } from 'node:url';
|
|
37
|
+
|
|
38
|
+
import { buildPathConfig } from '../../src/core/paths.js';
|
|
39
|
+
import { loadManifest } from '../../src/manifest/loadManifest.js';
|
|
40
|
+
import { detectStack } from '../../src/stack/detectStack.js';
|
|
41
|
+
import { detectProjectContext } from '../../src/context/detectProjectContext.js';
|
|
42
|
+
import { detectProviders } from '../../src/context/detectProviders.js';
|
|
43
|
+
import { loadRuntimeConfig } from '../../src/core/runtimeConfig.js';
|
|
44
|
+
import { buildTemplateVariables } from '../../src/render/buildVariables.js';
|
|
45
|
+
import { buildInstallPlan } from '../../src/core/buildPlan.js';
|
|
46
|
+
import { diffInstallPlan } from '../../src/core/diffPlan.js';
|
|
47
|
+
import { applyDiffResults } from '../../src/core/applyPlan.js';
|
|
48
|
+
import { applyGatewayResilienceEnv } from '../../src/core/gatewayResilienceEnv.js';
|
|
49
|
+
|
|
50
|
+
const MANAGED_PREFIXES = ['.claude/', '.omp/'];
|
|
51
|
+
const USAGE = 'usage: sync-installed-mirror.mjs [--check]';
|
|
52
|
+
|
|
53
|
+
function isManaged(relativePath) {
|
|
54
|
+
return MANAGED_PREFIXES.some((prefix) => relativePath.startsWith(prefix));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function toRelative(projectRoot, targetPath) {
|
|
58
|
+
return path.relative(projectRoot, targetPath).split(path.sep).join('/');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function readPackageVersion(packageRoot) {
|
|
62
|
+
try {
|
|
63
|
+
const pkg = JSON.parse(await fs.readFile(path.join(packageRoot, 'package.json'), 'utf8'));
|
|
64
|
+
return typeof pkg.version === 'string' ? pkg.version : null;
|
|
65
|
+
} catch {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// The full set of paths the plan manages, used to decide whether a file found in the
|
|
71
|
+
// mirror is unmanaged. Built from every entry (not just the managed ones) so a path
|
|
72
|
+
// that is managed by an out-of-scope entry is never reported as mirror-only.
|
|
73
|
+
function buildPlanTargetSet(plan, projectRoot) {
|
|
74
|
+
return new Set(
|
|
75
|
+
plan.entries
|
|
76
|
+
.map((entry) => toRelative(projectRoot, entry.targetPath))
|
|
77
|
+
.filter(Boolean),
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function collectFilesRecursively(dir, out) {
|
|
82
|
+
let entries;
|
|
83
|
+
try {
|
|
84
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
85
|
+
} catch {
|
|
86
|
+
return out; // missing or unreadable directory — the create-drift lane covers it
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
for (const entry of entries) {
|
|
90
|
+
const fullPath = path.join(dir, entry.name);
|
|
91
|
+
// Symlinks are the adapter-link lane's business (`type: 'link'` items are excluded
|
|
92
|
+
// from mirror-only detection), and following one would walk another tool's tree.
|
|
93
|
+
if (entry.isSymbolicLink()) continue;
|
|
94
|
+
if (entry.isDirectory()) {
|
|
95
|
+
await collectFilesRecursively(fullPath, out);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (entry.isFile()) out.push(fullPath);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Directories UKit installs wholesale from a template directory. Any file inside one
|
|
105
|
+
// that the plan does not ship is mirror-only drift rather than user content.
|
|
106
|
+
async function collectManagedDirectoryRoots({ plan, templatesRoot }) {
|
|
107
|
+
const roots = [];
|
|
108
|
+
|
|
109
|
+
for (const item of plan.selectedItems) {
|
|
110
|
+
if (item.type === 'link') continue;
|
|
111
|
+
const target = String(item.targetPath ?? '');
|
|
112
|
+
if (!isManaged(target)) continue;
|
|
113
|
+
|
|
114
|
+
const source = path.join(templatesRoot, String(item.sourceTemplate ?? ''));
|
|
115
|
+
let stat;
|
|
116
|
+
try {
|
|
117
|
+
stat = await fs.stat(source);
|
|
118
|
+
} catch {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (stat.isDirectory()) roots.push(target.replace(/\/+$/, ''));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return roots;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function collectMirrorOnlyPaths({ plan, projectRoot, templatesRoot }) {
|
|
128
|
+
const planTargets = buildPlanTargetSet(plan, projectRoot);
|
|
129
|
+
const roots = await collectManagedDirectoryRoots({ plan, templatesRoot });
|
|
130
|
+
const mirrorOnly = [];
|
|
131
|
+
|
|
132
|
+
for (const root of roots) {
|
|
133
|
+
const files = await collectFilesRecursively(path.join(projectRoot, root), []);
|
|
134
|
+
for (const file of files) {
|
|
135
|
+
const relativePath = toRelative(projectRoot, file);
|
|
136
|
+
if (!planTargets.has(relativePath)) mirrorOnly.push(relativePath);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return [...new Set(mirrorOnly)].sort();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function main() {
|
|
144
|
+
const args = process.argv.slice(2);
|
|
145
|
+
const unknown = args.filter((arg) => arg !== '--check');
|
|
146
|
+
if (unknown.length > 0) {
|
|
147
|
+
console.error(`${USAGE}\nunknown flag: ${unknown[0]}`);
|
|
148
|
+
process.exit(2);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const checkOnly = args.includes('--check');
|
|
152
|
+
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
153
|
+
const projectRoot = process.cwd();
|
|
154
|
+
const pathConfig = buildPathConfig({ packageRoot, projectRoot });
|
|
155
|
+
|
|
156
|
+
const manifest = await loadManifest(pathConfig.manifestPath);
|
|
157
|
+
const stackContext = await detectStack(projectRoot);
|
|
158
|
+
const projectContext = await detectProjectContext(projectRoot);
|
|
159
|
+
const providerContext = await detectProviders(projectRoot);
|
|
160
|
+
const runtimeConfig = await loadRuntimeConfig(projectRoot);
|
|
161
|
+
|
|
162
|
+
// Same variable set as `runInstallPipeline`, so render-tokenized templates compare
|
|
163
|
+
// in the exact form `ukit install` would have written them.
|
|
164
|
+
const variables = buildTemplateVariables({
|
|
165
|
+
projectContext,
|
|
166
|
+
stackContext,
|
|
167
|
+
packageVersion: await readPackageVersion(packageRoot),
|
|
168
|
+
providerContext,
|
|
169
|
+
runtimeConfig,
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const plan = await buildInstallPlan({
|
|
173
|
+
manifest,
|
|
174
|
+
stackContext,
|
|
175
|
+
templatesRoot: pathConfig.templatesRoot,
|
|
176
|
+
variables,
|
|
177
|
+
projectRoot,
|
|
178
|
+
// Omitted: `ukit install`'s own default adapter set (claude + codex + omp), so the
|
|
179
|
+
// `.omp/**` half of the mirror is in scope.
|
|
180
|
+
selectedAdapterItemIds: undefined,
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
const managedEntries = plan.entries.filter((entry) => {
|
|
184
|
+
if (entry.mergeStrategy === 'skip') return false;
|
|
185
|
+
return isManaged(toRelative(projectRoot, entry.targetPath));
|
|
186
|
+
});
|
|
187
|
+
const managedRelativePaths = new Set(managedEntries.map((entry) => toRelative(projectRoot, entry.targetPath)));
|
|
188
|
+
|
|
189
|
+
const diffResults = await diffInstallPlan({ entries: managedEntries });
|
|
190
|
+
const drift = diffResults
|
|
191
|
+
.filter((entry) => entry.action === 'create' || entry.action === 'update')
|
|
192
|
+
.map((entry) => ({ entry, relativePath: toRelative(projectRoot, entry.targetPath) }))
|
|
193
|
+
.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
|
|
194
|
+
|
|
195
|
+
const mirrorOnly = await collectMirrorOnlyPaths({
|
|
196
|
+
plan,
|
|
197
|
+
projectRoot,
|
|
198
|
+
templatesRoot: pathConfig.templatesRoot,
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
if (checkOnly) {
|
|
202
|
+
for (const { entry, relativePath } of drift) {
|
|
203
|
+
console.log(`DRIFT ${relativePath} (${entry.action})`);
|
|
204
|
+
}
|
|
205
|
+
for (const relativePath of mirrorOnly) {
|
|
206
|
+
console.log(`MIRROR-ONLY ${relativePath}`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (drift.length === 0 && mirrorOnly.length === 0) {
|
|
210
|
+
console.log(`ukit mirror check: clean (${managedRelativePaths.size} scoped targets)`);
|
|
211
|
+
return 0;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
console.log(
|
|
215
|
+
`ukit mirror check: ${drift.length} drifted, ${mirrorOnly.length} mirror-only `
|
|
216
|
+
+ `(${managedRelativePaths.size} scoped targets)`,
|
|
217
|
+
);
|
|
218
|
+
return 1;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const { writes } = await applyDiffResults(
|
|
222
|
+
drift.map(({ entry }) => entry),
|
|
223
|
+
{ backupRoot: pathConfig.backupRoot, projectRoot },
|
|
224
|
+
);
|
|
225
|
+
for (const write of writes) {
|
|
226
|
+
console.log(`${write.action === 'create' ? 'CREATED' : 'UPDATED'} ${toRelative(projectRoot, write.targetPath)}`);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Re-running the managed-gateway step is what makes a rewritten `settings.json`
|
|
230
|
+
// whole again: the rendered template does not carry the two injected env keys, so
|
|
231
|
+
// serializing the template alone would drop them.
|
|
232
|
+
const gateway = await applyGatewayResilienceEnv({ projectRoot });
|
|
233
|
+
if (gateway.changed) {
|
|
234
|
+
console.log(`ukit mirror sync: re-applied managed gateway env keys (${[...gateway.applied, ...gateway.migrated].join(', ')})`);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
for (const relativePath of mirrorOnly) {
|
|
238
|
+
console.log(`MIRROR-ONLY ${relativePath} — not shipped by any template; left in place, remove it by hand if stale`);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
console.log(`ukit mirror sync: ${writes.length} written, ${mirrorOnly.length} mirror-only left in place`);
|
|
242
|
+
return 0;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
try {
|
|
246
|
+
process.exit(await main());
|
|
247
|
+
} catch (error) {
|
|
248
|
+
console.error(error && error.message ? error.message : error);
|
|
249
|
+
process.exit(1);
|
|
250
|
+
}
|