@ngockhoale/ukit 2.7.5 → 2.7.7
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 +89 -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/core/hookChainDoctor.js +65 -2
- package/src/core/memory/store.js +22 -1
- package/src/core/ompConfigMerge.js +7 -2
- package/src/core/permissionDoctor.js +59 -4
- package/src/core/permissionPolicy.js +1 -1
- package/src/core/taskBudgetValidator.js +9 -6
- package/src/core/unattendedDoctor.js +8 -1
- package/templates/.claude/hooks/auto-prune-bash.sh +19 -0
- package/templates/.claude/hooks/reinject-context.sh +22 -0
- package/templates/.claude/hooks/reset-compact-pressure.sh +19 -0
- package/templates/.claude/hooks/session-episode.sh +20 -0
- package/templates/.claude/settings.json +0 -1
- package/templates/.claude/ukit/index/task-budget-validator.mjs +6 -2
- package/templates/.claude/ukit/runtime/hook-chain-runner.mjs +118 -20
- package/templates/.claude/ukit/runtime/hook-telemetry.mjs +84 -12
- package/templates/.claude/ukit/runtime/hook-telemetry.sh +50 -0
- package/templates/.omp/config.yml +9 -8
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,95 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to UKit are documented here.
|
|
4
4
|
|
|
5
|
+
## 2.7.7 - 2026-09-21
|
|
6
|
+
|
|
7
|
+
Audit-report remediation + hook observability — cycle C42 (TASK-001..011): all 12
|
|
8
|
+
findings from the three 2026-09-20 reports closed; the reports moved to
|
|
9
|
+
`docs/AI_REPORT/archive/`.
|
|
10
|
+
|
|
11
|
+
- B1: `hook-chain-runner` short-circuits on a permission decision under
|
|
12
|
+
`--emit-verdict`. A `hookSpecificOutput` decision on a step now owns the
|
|
13
|
+
verdict and stops the chain there, so the decision JSON is emitted as the
|
|
14
|
+
whole stdout instead of being concatenated in front of the next step's
|
|
15
|
+
output, and a genuinely-skipped fail-closed gate is no longer exempted from
|
|
16
|
+
the fail-closed verdict. C8 (same task): an invalid `:N` budget suffix
|
|
17
|
+
(`:0`, `:-1`) warns and runs the step on its default budget instead of
|
|
18
|
+
silently parsing; a non-numeric suffix stays a literal path.
|
|
19
|
+
- B2: `yarn test <files>` — the repo's dominant verification form — is now
|
|
20
|
+
recognized by the verification-minutes estimator alongside `yarn vitest
|
|
21
|
+
[run]`, so task budgets are no longer inflated by the 1-minute flat
|
|
22
|
+
fallback.
|
|
23
|
+
- B3: doctor strips `{{...}}` placeholders before `parse()`ing the omp config
|
|
24
|
+
template, so a placeholder in key position no longer makes the `yaml` lib
|
|
25
|
+
emit "Keys with collection values will be stringified" on every run.
|
|
26
|
+
- C5: new scoped `scripts/install/sync-installed-mirror.mjs` (`--check` reports
|
|
27
|
+
drift, bare run rewrites). It reuses the real install pipeline
|
|
28
|
+
(`buildInstallPlan` → `diffInstallPlan` → `applyDiffResults`) so drift is
|
|
29
|
+
decided by exactly the code `ukit install` uses, and it only ever touches
|
|
30
|
+
`.claude/**`/`.omp/**` plan targets — never `src/`, `docs/`, or user-authored
|
|
31
|
+
files. Parity coverage widened to `.claude/ukit/index/`.
|
|
32
|
+
- C7: hook-chain failure surfaces pinned by
|
|
33
|
+
`tests/hooks/hookChainFailureSurface.test.js` (output overflow on a
|
|
34
|
+
fail-closed gate, budget exhausted mid-chain, signal death); the decision to
|
|
35
|
+
drop non-verdict stderr is documented in `UKIT_INTERNALS.md`.
|
|
36
|
+
- O1: telemetry wired into the four previously unmeasured hooks
|
|
37
|
+
(`reinject-context`, `auto-prune-bash`, `reset-compact-pressure`,
|
|
38
|
+
`session-episode`) via a no-staging arm path in the `hook-telemetry` engine.
|
|
39
|
+
Four hooks now emit rows where they emitted none.
|
|
40
|
+
- O2: audit snapshots carry provenance (`generatedAt` + telemetry row/file
|
|
41
|
+
counts), and `scripts/perf/diff-perf-findings.mjs` diffs two snapshots per
|
|
42
|
+
finding id without touching live telemetry.
|
|
43
|
+
- O3: telemetry rows split stdin-stage time from execution time (`stageMs`),
|
|
44
|
+
so a slow producer is distinguishable from a slow step.
|
|
45
|
+
- O4/O6: telemetry consumers audited and the consolidated row shape pinned
|
|
46
|
+
(`tests/hooks/chainTelemetryRowShape.test.js`); doctor escalates when a
|
|
47
|
+
failure actually landed on a chain carrying a fail-closed gate or exhausted
|
|
48
|
+
its budget — purely additive to the existing detail/remedy.
|
|
49
|
+
- O5: telemetry append overhead measured; numbers recorded in
|
|
50
|
+
`scripts/perf/perf-measure.md`.
|
|
51
|
+
- Fixed `tests/consistency/ompDocsSync.test.js` case 4: the frozen-history
|
|
52
|
+
guard treated the `RULES.md` §Clear Handoff step-2 archive rotation (delete
|
|
53
|
+
the oldest cycle, fold it into `HISTORY.md`) as tampering, so the test was
|
|
54
|
+
red at HEAD and at the `v2.7.2`/`v2.7.5` tags. The guard now exempts a
|
|
55
|
+
cycle-dir deletion only when a matching `HISTORY.md` row exists and the
|
|
56
|
+
post-rotation archive is back within the 3-dir cap; rewrites, renames, and
|
|
57
|
+
unrecorded deletions still fail.
|
|
58
|
+
- Fixed `src/core/memory/store.js`: the session-archive cap appended and
|
|
59
|
+
`slice(-cap)`ed, so it evicted by insertion position instead of age — a
|
|
60
|
+
session archived late carrying an older end time outlived a newer one written
|
|
61
|
+
earlier. It now drops the oldest by end time (ties keep insertion order),
|
|
62
|
+
matching the key `archiveSessions` already uses. The regression case in
|
|
63
|
+
`tests/core/sessionArchiveBound.test.js` had a `Date.now()`-per-session
|
|
64
|
+
fixture whose outcome depended on whether the clock ticked mid-array, so it
|
|
65
|
+
passed on an unloaded box and failed under load; the fixture is now
|
|
66
|
+
deterministic and the case fails against the old code.
|
|
67
|
+
|
|
68
|
+
## 2.7.6 - 2026-09-20
|
|
69
|
+
|
|
70
|
+
Permission posture work — cycle C41 (TASK-001..004): omp prompts aligned to
|
|
71
|
+
Claude Code — routine commands auto-allow, gates kept only for destructive
|
|
72
|
+
operations.
|
|
73
|
+
|
|
74
|
+
- `rm -rf *` static catch-all removed from `templates/.omp/config.yml`
|
|
75
|
+
`bash.patterns`, `templates/.claude/settings.json`, and
|
|
76
|
+
`permissionPolicy.CANONICAL_DENY` (16→15). The `block-dangerous` hook remains
|
|
77
|
+
the authority for generic `rm -rf <dir>` (fail-closed), so coverage for
|
|
78
|
+
real destructive targets is unchanged while routine/safe cleanup paths no
|
|
79
|
+
longer hit the static deny on both hosts.
|
|
80
|
+
- `ompConfigMerge` now coerces preserved legacy `prompt`/`ask` bash.patterns
|
|
81
|
+
to `allow` instead of `deny` — under rendered `yolo` a broad preserved
|
|
82
|
+
pattern (worst case `*`) could globally brick bash. Preserved `deny` still
|
|
83
|
+
passes through; canonical deny + the hook still gate destructive commands.
|
|
84
|
+
- `permissionDoctor` new FAIL check `omp-approval-mode-not-pinned`: under
|
|
85
|
+
`orchestration.permissionMode: unattended`, fires when omp `approvalMode`
|
|
86
|
+
is missing or not `yolo`; plus an advisory audit of preserved bash.patterns.
|
|
87
|
+
`applicable:false` when `.omp/config.yml` is absent or mode ≠ unattended.
|
|
88
|
+
- `sensitive-data-guard` added to `CANONICAL_PROTECTED` (no precedence
|
|
89
|
+
change); new `tests/manifest/c41OmpPosture.test.js` asserts routine
|
|
90
|
+
commands are not prompted, all destructive classes stay denied, and
|
|
91
|
+
enumerates the full prompt-producing gate inventory (stale-spec-guard,
|
|
92
|
+
sensitive-data-guard, bridge, FAIL_CLOSED).
|
|
93
|
+
|
|
5
94
|
## 2.7.5 - 2026-09-20
|
|
6
95
|
|
|
7
96
|
Hook performance work — cycle C40 (TASK-001..007): `.mjs` module steps,
|
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
|
+
}
|