@mutmutco/hub 3.137.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.
Files changed (3) hide show
  1. package/README.md +29 -0
  2. package/dist/index.cjs +2123 -0
  3. package/package.json +33 -0
package/dist/index.cjs ADDED
@@ -0,0 +1,2123 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+
21
+ // src/index.ts
22
+ var index_exports = {};
23
+ __export(index_exports, {
24
+ hubStatus: () => hubStatus,
25
+ launcherVbs: () => launcherVbs,
26
+ localStartBoundary: () => localStartBoundary,
27
+ main: () => main,
28
+ ownVersion: () => ownVersion,
29
+ schedulerStatus: () => schedulerStatus,
30
+ taskXml: () => taskXml
31
+ });
32
+ module.exports = __toCommonJS(index_exports);
33
+ var import_node_fs15 = require("node:fs");
34
+ var import_node_path12 = require("node:path");
35
+
36
+ // src/state.ts
37
+ var import_node_fs = require("node:fs");
38
+ var import_node_os = require("node:os");
39
+ var import_node_path = require("node:path");
40
+ function stateRoot(env = process.env) {
41
+ return env.MMI_UPDATER_HOME || (0, import_node_path.join)((0, import_node_os.homedir)(), ".mmi", "updater");
42
+ }
43
+ function statePaths(env = process.env) {
44
+ const root = stateRoot(env);
45
+ const staging = (0, import_node_path.join)(root, "staging");
46
+ const gitCache = (0, import_node_path.join)(root, "git-cache", "mmi-hub.git");
47
+ return { root, staging, gitCache, journalPath: (0, import_node_path.join)(root, "journal.jsonl"), leasePath: (0, import_node_path.join)(root, "lease.lock") };
48
+ }
49
+ function ensureState(env = process.env) {
50
+ const paths = statePaths(env);
51
+ (0, import_node_fs.mkdirSync)(paths.staging, { recursive: true });
52
+ (0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(paths.gitCache), { recursive: true });
53
+ return paths;
54
+ }
55
+ var UpdaterBusy = class extends Error {
56
+ };
57
+ var LEASE_WAIT_MS = 3e4;
58
+ var LEASE_STALE_MS = 10 * 6e4;
59
+ function tryAcquire(path) {
60
+ const token = `${Date.now()}-${process.pid}-${Math.random().toString(36).slice(2, 10)}`;
61
+ try {
62
+ const fd = (0, import_node_fs.openSync)(path, "wx");
63
+ (0, import_node_fs.writeFileSync)(fd, JSON.stringify({ token, pid: process.pid, ts: Date.now() }) + "\n");
64
+ (0, import_node_fs.closeSync)(fd);
65
+ return {
66
+ token,
67
+ release() {
68
+ try {
69
+ const current2 = readLease(path);
70
+ if (current2 && current2.token === token) (0, import_node_fs.unlinkSync)(path);
71
+ } catch {
72
+ }
73
+ }
74
+ };
75
+ } catch (error) {
76
+ if (error?.code !== "EEXIST") throw error;
77
+ }
78
+ const current = readLease(path);
79
+ if (current && Date.now() - current.ts > LEASE_STALE_MS) {
80
+ try {
81
+ (0, import_node_fs.renameSync)(path, `${path}.stale-${Date.now()}`);
82
+ return "stolen-stale";
83
+ } catch {
84
+ }
85
+ }
86
+ return "taken";
87
+ }
88
+ function readLease(path) {
89
+ try {
90
+ return JSON.parse((0, import_node_fs.readFileSync)(path, "utf8"));
91
+ } catch {
92
+ return null;
93
+ }
94
+ }
95
+ function withLease(path, fn) {
96
+ const deadline = Date.now() + LEASE_WAIT_MS;
97
+ for (; ; ) {
98
+ const attempt = tryAcquire(path);
99
+ if (typeof attempt === "object") {
100
+ try {
101
+ return fn();
102
+ } finally {
103
+ attempt.release();
104
+ }
105
+ }
106
+ if (Date.now() >= deadline) {
107
+ throw new UpdaterBusy(`another updater holds ${path} (waited ${LEASE_WAIT_MS}ms) \u2014 deferring`);
108
+ }
109
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 500);
110
+ }
111
+ }
112
+ function appendJournal(path, event) {
113
+ try {
114
+ (0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(path), { recursive: true });
115
+ const fd = (0, import_node_fs.openSync)(path, "a");
116
+ (0, import_node_fs.writeFileSync)(fd, JSON.stringify(event) + "\n");
117
+ (0, import_node_fs.closeSync)(fd);
118
+ return true;
119
+ } catch {
120
+ return false;
121
+ }
122
+ }
123
+ function journalHighWater(path, surface) {
124
+ let high = null;
125
+ try {
126
+ for (const line of (0, import_node_fs.readFileSync)(path, "utf8").split("\n")) {
127
+ if (!line.trim()) continue;
128
+ try {
129
+ const event = JSON.parse(line);
130
+ if (event.dryRun) continue;
131
+ if (event.surface === surface && event.verdict === "ok" && event.to && (!high || compareSemver(event.to, high) > 0)) {
132
+ high = event.to;
133
+ }
134
+ } catch {
135
+ }
136
+ }
137
+ } catch {
138
+ }
139
+ return high;
140
+ }
141
+ function parseSemver(version) {
142
+ const match = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/.exec(version.trim());
143
+ if (!match) return null;
144
+ return { major: +match[1], minor: +match[2], patch: +match[3], pre: match[4] ? match[4].split(".") : [] };
145
+ }
146
+ function compareSemver(a, b) {
147
+ const va = parseSemver(a);
148
+ const vb = parseSemver(b);
149
+ if (!va || !vb) return 0;
150
+ if (va.major !== vb.major) return va.major - vb.major;
151
+ if (va.minor !== vb.minor) return va.minor - vb.minor;
152
+ if (va.patch !== vb.patch) return va.patch - vb.patch;
153
+ if (!va.pre.length && !vb.pre.length) return 0;
154
+ if (!va.pre.length) return 1;
155
+ if (!vb.pre.length) return -1;
156
+ return va.pre.join(".") < vb.pre.join(".") ? -1 : va.pre.join(".") > vb.pre.join(".") ? 1 : 0;
157
+ }
158
+ function fileExists(path) {
159
+ return (0, import_node_fs.existsSync)(path);
160
+ }
161
+
162
+ // src/npm.ts
163
+ var import_node_child_process = require("node:child_process");
164
+ var import_node_fs2 = require("node:fs");
165
+ var import_node_path2 = require("node:path");
166
+ var cachedNpm = null;
167
+ function resolveNpm(env = process.env) {
168
+ if (env.MMI_UPDATER_NPM) return { kind: "command", command: env.MMI_UPDATER_NPM };
169
+ if (cachedNpm) return cachedNpm;
170
+ if (process.platform === "win32") {
171
+ const where = (0, import_node_child_process.spawnSync)("where.exe", ["npm.cmd"], { encoding: "utf8", windowsHide: true });
172
+ const first = where.stdout.split("\n").map((l) => l.trim()).find(Boolean);
173
+ if (where.status === 0 && first) {
174
+ const script = (0, import_node_path2.join)((0, import_node_path2.dirname)(first), "node_modules", "npm", "bin", "npm-cli.js");
175
+ if ((0, import_node_fs2.existsSync)(script)) {
176
+ cachedNpm = { kind: "node-script", script };
177
+ return cachedNpm;
178
+ }
179
+ cachedNpm = { kind: "command", command: first };
180
+ return cachedNpm;
181
+ }
182
+ }
183
+ cachedNpm = { kind: "command", command: "npm" };
184
+ return cachedNpm;
185
+ }
186
+ function runNpm(args, env = process.env, options = {}) {
187
+ const npm = resolveNpm(env);
188
+ const result = npm.kind === "node-script" ? (0, import_node_child_process.spawnSync)(process.execPath, [npm.script, ...args], { encoding: "utf8", env, cwd: options.cwd, windowsHide: true }) : (0, import_node_child_process.spawnSync)(npm.command, args, { encoding: "utf8", env, cwd: options.cwd, shell: process.platform === "win32", windowsHide: true });
189
+ return { status: result.status, stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
190
+ }
191
+ function npmView(spec, field, env = process.env) {
192
+ const args = ["view", spec, "--json"];
193
+ if (field) args.splice(2, 0, field);
194
+ const result = runNpm(args, env);
195
+ if (result.status !== 0 || !result.stdout.trim()) return null;
196
+ try {
197
+ return JSON.parse(result.stdout);
198
+ } catch {
199
+ return null;
200
+ }
201
+ }
202
+ function npmVersions(packageName, env = process.env) {
203
+ const versions = npmView(packageName, "versions", env);
204
+ return Array.isArray(versions) ? versions.filter((v) => typeof v === "string") : [];
205
+ }
206
+
207
+ // src/gate.ts
208
+ var import_node_child_process2 = require("node:child_process");
209
+ var import_node_fs3 = require("node:fs");
210
+ function parseBom(text) {
211
+ const bom = JSON.parse(text);
212
+ if (typeof bom?.version !== "string" || typeof bom?.sourceCommit !== "string" || !Array.isArray(bom?.artifacts)) {
213
+ throw new Error("distribution-bom.json at tag is malformed (version/sourceCommit/artifacts)");
214
+ }
215
+ return bom;
216
+ }
217
+ function npmPackRows(bom) {
218
+ return bom.artifacts.filter((row) => row.identity?.kind === "npm-pack" && row.identity.packagePath);
219
+ }
220
+ var GateEnvError = class extends Error {
221
+ };
222
+ function git(cachePath, args) {
223
+ const result = (0, import_node_child_process2.spawnSync)("git", ["-C", cachePath, ...args], { encoding: "utf8", windowsHide: true });
224
+ return { status: result.status, stdout: (result.stdout ?? "").trim(), stderr: result.stderr ?? "" };
225
+ }
226
+ function ensureBareCache(cachePath, repoUrl) {
227
+ const probe = (0, import_node_child_process2.spawnSync)("git", ["--version"], { encoding: "utf8", windowsHide: true });
228
+ if (probe.status !== 0) {
229
+ throw new GateEnvError("git is not available \u2014 the release gate needs a git client");
230
+ }
231
+ if (!(0, import_node_fs3.existsSync)(cachePath)) {
232
+ const init = (0, import_node_child_process2.spawnSync)("git", ["init", "--bare", cachePath], { encoding: "utf8", windowsHide: true });
233
+ if (init.status !== 0) throw new GateEnvError(`cannot create bare cache at ${cachePath}: ${init.stderr}`);
234
+ }
235
+ }
236
+ function fetchIntoCache(cachePath, repoUrl, refspec) {
237
+ const result = git(cachePath, ["fetch", "--no-tags", repoUrl, refspec]);
238
+ return result.status === 0;
239
+ }
240
+ function resolveTagCommit(cachePath, tag) {
241
+ const result = git(cachePath, ["rev-parse", `refs/tags/${tag}^{commit}`]);
242
+ return result.status === 0 ? result.stdout : null;
243
+ }
244
+ function isAncestor(cachePath, ancestor, descendant) {
245
+ return git(cachePath, ["merge-base", "--is-ancestor", ancestor, descendant]).status === 0;
246
+ }
247
+ function isAncestorOfMain(cachePath, commit) {
248
+ return isAncestor(cachePath, commit, "refs/heads/main");
249
+ }
250
+ function showFile(cachePath, commit, path) {
251
+ const result = git(cachePath, ["show", `${commit}:${path}`]);
252
+ return result.status === 0 ? result.stdout : null;
253
+ }
254
+ function gateCandidate(cachePath, repoUrl, candidate) {
255
+ const tag = `v${candidate}`;
256
+ if (!fetchIntoCache(cachePath, repoUrl, `refs/tags/${tag}:refs/tags/${tag}`)) {
257
+ return { ok: false, reason: `tag ${tag} not found` };
258
+ }
259
+ const commit = resolveTagCommit(cachePath, tag);
260
+ if (!commit) return { ok: false, reason: `tag ${tag} resolves to no commit` };
261
+ if (!isAncestorOfMain(cachePath, commit)) {
262
+ return { ok: false, reason: `tag ${tag} commit is not reachable from main` };
263
+ }
264
+ const bomText = showFile(cachePath, commit, "distribution-bom.json");
265
+ if (!bomText) return { ok: false, reason: `distribution-bom.json absent at ${tag}` };
266
+ let bom;
267
+ try {
268
+ bom = parseBom(bomText);
269
+ } catch (error) {
270
+ return { ok: false, reason: `BOM at ${tag} unparseable: ${error?.message ?? error}` };
271
+ }
272
+ if (bom.version !== candidate) {
273
+ return { ok: false, reason: `BOM at ${tag} stamps ${bom.version}, not ${candidate}` };
274
+ }
275
+ if (!isAncestor(cachePath, bom.sourceCommit, commit)) {
276
+ return { ok: false, reason: `BOM at ${tag} stamps sourceCommit ${bom.sourceCommit.slice(0, 12)} which the tag commit does not contain` };
277
+ }
278
+ return { ok: true, tagCommit: commit, bom };
279
+ }
280
+ function checkAtomicity(cachePath, tagCommit, bom, candidate, npm) {
281
+ const rows = [];
282
+ for (const row of npmPackRows(bom)) {
283
+ if (row.version !== candidate) {
284
+ rows.push({ id: row.id, ok: false, detail: `BOM row at ${row.version}, candidate ${candidate}` });
285
+ continue;
286
+ }
287
+ const manifestText = showFile(cachePath, tagCommit, `${row.identity.packagePath}/package.json`);
288
+ let name;
289
+ let manifestVersion;
290
+ try {
291
+ const manifest2 = JSON.parse(manifestText ?? "");
292
+ name = manifest2.name;
293
+ manifestVersion = manifest2.version;
294
+ } catch {
295
+ }
296
+ if (!name || manifestVersion !== candidate) {
297
+ rows.push({ id: row.id, name, ok: false, detail: `package.json at ${row.identity.packagePath} is not stamped ${candidate}` });
298
+ continue;
299
+ }
300
+ const integrity = npm.distIntegrity(`${name}@${candidate}`);
301
+ if (!integrity) {
302
+ rows.push({ id: row.id, name, ok: false, detail: `${name}@${candidate} not on npm` });
303
+ continue;
304
+ }
305
+ if (integrity !== row.identity.value) {
306
+ rows.push({ id: row.id, name, ok: false, detail: `${name}@${candidate} dist.integrity differs from BOM identity` });
307
+ continue;
308
+ }
309
+ rows.push({ id: row.id, name, ok: true, detail: `${name}@${candidate} published, integrity matches` });
310
+ }
311
+ return { ok: rows.length > 0 && rows.every((row) => row.ok), rows };
312
+ }
313
+
314
+ // src/surfaces.ts
315
+ var import_node_fs4 = require("node:fs");
316
+ var import_node_os2 = require("node:os");
317
+ var import_node_path3 = require("node:path");
318
+ function kimiHome(env = process.env) {
319
+ return env.KIMI_CODE_HOME?.trim() || (0, import_node_path3.join)((0, import_node_os2.homedir)(), ".kimi-code");
320
+ }
321
+ function jervcodeAgentDir(env = process.env) {
322
+ return env.MMI_UPDATER_JERVCODE_AGENT_DIR?.trim() || env.JERVCODE_CODING_AGENT_DIR?.trim() || env.PI_CODING_AGENT_DIR?.trim() || (0, import_node_path3.join)((0, import_node_os2.homedir)(), ".jerv", "agent");
323
+ }
324
+ function enumerateSurfaces(env = process.env) {
325
+ const home = (0, import_node_os2.homedir)();
326
+ const codexHome = env.CODEX_HOME || (0, import_node_path3.join)(home, ".codex");
327
+ return [
328
+ { id: "claude", marker: (0, import_node_path3.join)(home, ".claude", "plugins", "installed_plugins.json") },
329
+ { id: "codex", marker: (0, import_node_path3.join)(codexHome, "plugins", "installed_plugins.json") },
330
+ { id: "cursor", marker: (0, import_node_path3.join)(home, ".cursor", "plugins", "local", "mmi") },
331
+ { id: "kilo", marker: (0, import_node_path3.join)(home, ".config", "kilo") },
332
+ // The host, not the payload (#4951): this marker used to be plugins/managed/mmi — the MMI plugin
333
+ // itself — so a machine running Kimi WITHOUT the plugin enumerated as "no Kimi" and the arm
334
+ // could never make the first install. The plugin's absence is a converge action, not a skip.
335
+ { id: "kimi", marker: kimiHome(env) },
336
+ { id: "jervcode", marker: (0, import_node_path3.join)(jervcodeAgentDir(env), "settings.json") }
337
+ ].map((probe) => ({ ...probe, present: (0, import_node_fs4.existsSync)(probe.marker) }));
338
+ }
339
+
340
+ // src/arm-cli.ts
341
+ var import_node_fs5 = require("node:fs");
342
+ var import_node_child_process3 = require("node:child_process");
343
+ var import_node_path4 = require("node:path");
344
+ function globalCliDist(env) {
345
+ const prefix = runNpm(["prefix", "-g"], env);
346
+ if (prefix.status !== 0 || !prefix.stdout.trim()) return null;
347
+ return (0, import_node_path4.join)(prefix.stdout.trim(), "node_modules", "@mutmutco", "cli", "dist", "index.cjs");
348
+ }
349
+ function probeCliVersion(distPath) {
350
+ if (!fileExists(distPath)) return null;
351
+ const probe = (0, import_node_child_process3.spawnSync)(process.execPath, [distPath, "--version"], { encoding: "utf8", windowsHide: true });
352
+ const version = (probe.stdout || "").trim();
353
+ return probe.status === 0 && /^\d+\.\d+\.\d+/.test(version) ? version : null;
354
+ }
355
+ function probeCliInstallation(env = process.env) {
356
+ const location = globalCliDist(env);
357
+ const version = location ? probeCliVersion(location) : null;
358
+ return { version, location, detail: version ? `absolute binary reported ${version}` : "global @mutmutco/cli binary was not readable" };
359
+ }
360
+ function cliArm(options) {
361
+ const env = options.env ?? process.env;
362
+ const { target, dryRun, paths } = options;
363
+ const dist = globalCliDist(env);
364
+ const installed = dist ? probeCliVersion(dist) : null;
365
+ if (installed && compareSemver(installed, target) > 0) {
366
+ return { surface: "cli", from: installed, to: installed, verdict: "skip", detail: `installed ${installed} is above candidate ${target} (monotonic)` };
367
+ }
368
+ const highWater = journalHighWater(paths.journalPath, "cli");
369
+ if (highWater && compareSemver(highWater, target) > 0) {
370
+ return { surface: "cli", from: installed, to: highWater, verdict: "skip", detail: `journal high-water ${highWater} is above candidate ${target} (monotonic)` };
371
+ }
372
+ if (installed === target) {
373
+ return { surface: "cli", from: installed, to: target, verdict: "ok", detail: `already at target ${target}` };
374
+ }
375
+ if (dryRun) {
376
+ return { surface: "cli", from: installed, to: target, verdict: "ok", detail: `dry-run: would converge ${installed ?? "absent"} -> ${target}` };
377
+ }
378
+ const stageRoot = (0, import_node_path4.join)(paths.staging, "cli", target);
379
+ const stageDist = (0, import_node_path4.join)(stageRoot, "node_modules", "@mutmutco", "cli", "dist", "index.cjs");
380
+ if (!fileExists(stageDist)) {
381
+ (0, import_node_fs5.mkdirSync)(stageRoot, { recursive: true });
382
+ (0, import_node_fs5.writeFileSync)((0, import_node_path4.join)(stageRoot, "package.json"), JSON.stringify({ name: "mmi-updater-stage", private: true }) + "\n");
383
+ const install = runNpm(["install", "--prefix", stageRoot, "--no-audit", "--no-fund", "--no-package-lock", `@mutmutco/cli@${target}`], env);
384
+ if (install.status !== 0) {
385
+ return { surface: "cli", from: installed, to: target, verdict: "defer", detail: `staging install failed: ${(install.stderr || install.stdout).split("\n").filter(Boolean).slice(-3).join(" | ")}` };
386
+ }
387
+ }
388
+ let stagedManifestVersion;
389
+ try {
390
+ stagedManifestVersion = JSON.parse((0, import_node_fs5.readFileSync)((0, import_node_path4.join)(stageRoot, "node_modules", "@mutmutco", "cli", "package.json"), "utf8")).version;
391
+ } catch {
392
+ }
393
+ if (stagedManifestVersion !== target) {
394
+ return { surface: "cli", from: installed, to: target, verdict: "defer", detail: `staged manifest is ${stagedManifestVersion ?? "unreadable"}, expected ${target}` };
395
+ }
396
+ const stagedVersion = probeCliVersion(stageDist);
397
+ if (stagedVersion !== target) {
398
+ return { surface: "cli", from: installed, to: target, verdict: "defer", detail: `staged binary probed ${stagedVersion ?? "dead"}, expected ${target}` };
399
+ }
400
+ const globalInstall = runNpm(["install", "-g", "--no-audit", "--no-fund", `@mutmutco/cli@${target}`], env);
401
+ if (globalInstall.status !== 0) {
402
+ return { surface: "cli", from: installed, to: target, verdict: "defer", detail: `global install failed: ${(globalInstall.stderr || globalInstall.stdout).split("\n").filter(Boolean).slice(-3).join(" | ")}` };
403
+ }
404
+ const switched = probeCliVersion(globalCliDist(env) ?? "");
405
+ if (switched !== target) {
406
+ return { surface: "cli", from: installed, to: switched ?? installed ?? "unknown", verdict: "fail", detail: `post-switch probe returned ${switched ?? "dead"}, expected ${target}` };
407
+ }
408
+ return { surface: "cli", from: installed, to: target, verdict: "ok", detail: `converged ${installed ?? "absent"} -> ${target} (staged, verified, switched)` };
409
+ }
410
+
411
+ // src/arm-npm-global.ts
412
+ var import_node_fs6 = require("node:fs");
413
+ var import_node_child_process4 = require("node:child_process");
414
+ var import_node_path5 = require("node:path");
415
+ function globalDist(env, packageName, distRel) {
416
+ const prefix = runNpm(["prefix", "-g"], env);
417
+ if (prefix.status !== 0 || !prefix.stdout.trim()) return null;
418
+ return (0, import_node_path5.join)(prefix.stdout.trim(), "node_modules", ...packageName.split("/"), ...distRel.split("/"));
419
+ }
420
+ function probeVersion(distPath) {
421
+ if (!fileExists(distPath)) return null;
422
+ const probe = (0, import_node_child_process4.spawnSync)(process.execPath, [distPath, "--version"], { encoding: "utf8", windowsHide: true });
423
+ const version = (probe.stdout || "").trim();
424
+ return probe.status === 0 && /^\d+\.\d+\.\d+/.test(version) ? version : null;
425
+ }
426
+ function tail(output) {
427
+ return output.split("\n").filter(Boolean).slice(-3).join(" | ");
428
+ }
429
+ function stagePackage(options) {
430
+ const { surface, packageName, target, witness, paths, env } = options;
431
+ const stageRoot = (0, import_node_path5.join)(paths.staging, surface, target);
432
+ const packageRoot = (0, import_node_path5.join)(stageRoot, "node_modules", ...packageName.split("/"));
433
+ if (!fileExists((0, import_node_path5.join)(packageRoot, ...witness.split("/")))) {
434
+ (0, import_node_fs6.mkdirSync)(stageRoot, { recursive: true });
435
+ (0, import_node_fs6.writeFileSync)((0, import_node_path5.join)(stageRoot, "package.json"), JSON.stringify({ name: "mmi-updater-stage", private: true }) + "\n");
436
+ const install = runNpm(["install", "--prefix", stageRoot, "--no-audit", "--no-fund", "--no-package-lock", `${packageName}@${target}`], env);
437
+ if (install.status !== 0) {
438
+ return { defer: `staging install failed: ${tail(install.stderr || install.stdout)}` };
439
+ }
440
+ }
441
+ let stagedManifestVersion;
442
+ try {
443
+ stagedManifestVersion = JSON.parse((0, import_node_fs6.readFileSync)((0, import_node_path5.join)(packageRoot, "package.json"), "utf8")).version;
444
+ } catch {
445
+ }
446
+ if (stagedManifestVersion !== target) {
447
+ return { defer: `staged manifest is ${stagedManifestVersion ?? "unreadable"}, expected ${target}` };
448
+ }
449
+ return { packageRoot };
450
+ }
451
+ function npmGlobalArm(options) {
452
+ const env = options.env ?? process.env;
453
+ const { surface, packageName, distRel, target, dryRun, paths } = options;
454
+ const spec = `${packageName}@${target}`;
455
+ const result = (from, to, verdict, detail) => ({ surface, from, to, verdict, detail });
456
+ const dist = globalDist(env, packageName, distRel);
457
+ const installed = dist ? probeVersion(dist) : null;
458
+ if (installed && compareSemver(installed, target) > 0) {
459
+ return result(installed, installed, "skip", `installed ${installed} is above candidate ${target} (monotonic)`);
460
+ }
461
+ const highWater = journalHighWater(paths.journalPath, surface);
462
+ if (highWater && compareSemver(highWater, target) > 0) {
463
+ return result(installed, highWater, "skip", `journal high-water ${highWater} is above candidate ${target} (monotonic)`);
464
+ }
465
+ if (installed === target) {
466
+ return result(installed, target, "ok", `already at target ${target}`);
467
+ }
468
+ if (dryRun) {
469
+ return result(installed, target, "ok", `dry-run: would converge ${installed ?? "absent"} -> ${target}`);
470
+ }
471
+ const staged = stagePackage({ surface, packageName, target, witness: distRel, paths, env });
472
+ if ("defer" in staged) {
473
+ return result(installed, target, "defer", staged.defer);
474
+ }
475
+ const stageDist = (0, import_node_path5.join)(staged.packageRoot, ...distRel.split("/"));
476
+ const stagedVersion = probeVersion(stageDist);
477
+ if (stagedVersion !== target) {
478
+ return result(installed, target, "defer", `staged binary probed ${stagedVersion ?? "dead"}, expected ${target}`);
479
+ }
480
+ const globalInstall = runNpm(["install", "-g", "--no-audit", "--no-fund", spec], env);
481
+ if (globalInstall.status !== 0) {
482
+ return result(installed, target, "defer", `global install failed: ${tail(globalInstall.stderr || globalInstall.stdout)}`);
483
+ }
484
+ const switched = probeVersion(globalDist(env, packageName, distRel) ?? "");
485
+ if (switched !== target) {
486
+ return result(installed, switched ?? installed ?? "unknown", "fail", `post-switch probe returned ${switched ?? "dead"}, expected ${target}`);
487
+ }
488
+ return result(installed, target, "ok", `converged ${installed ?? "absent"} -> ${target} (staged, verified, switched)`);
489
+ }
490
+
491
+ // src/arm-self.ts
492
+ function selfArm(options) {
493
+ return npmGlobalArm({
494
+ surface: "updater",
495
+ packageName: "@mutmutco/hub",
496
+ distRel: "dist/index.cjs",
497
+ ...options
498
+ });
499
+ }
500
+
501
+ // src/arm-host-plugins.ts
502
+ var import_node_child_process5 = require("node:child_process");
503
+ var import_node_fs7 = require("node:fs");
504
+ var import_node_os3 = require("node:os");
505
+ var import_node_path6 = require("node:path");
506
+ function cmdArg(value) {
507
+ return /^[A-Za-z0-9_@./:\\=+-]+$/.test(value) ? value : `"${value.replaceAll('"', '""')}"`;
508
+ }
509
+ var runHostCommand = (command, args, env) => {
510
+ const result = process.platform === "win32" ? (0, import_node_child_process5.spawnSync)("cmd.exe", ["/d", "/s", "/c", [command, ...args].map(cmdArg).join(" ")], {
511
+ encoding: "utf8",
512
+ env,
513
+ input: "",
514
+ timeout: 12e4,
515
+ windowsHide: true
516
+ }) : (0, import_node_child_process5.spawnSync)(command, args, {
517
+ encoding: "utf8",
518
+ env,
519
+ input: "",
520
+ timeout: 12e4,
521
+ windowsHide: true
522
+ });
523
+ return { status: result.status, stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
524
+ };
525
+ function failure(result) {
526
+ return (result.stderr || result.stdout).split("\n").filter(Boolean).slice(-3).join(" | ") || `exit ${result.status ?? "signal"}`;
527
+ }
528
+ function parseJson(result) {
529
+ if (result.status !== 0) return null;
530
+ try {
531
+ return JSON.parse(result.stdout);
532
+ } catch {
533
+ return null;
534
+ }
535
+ }
536
+ function claudeMmi(runner, env) {
537
+ const result = runner("claude", ["plugin", "list", "--json"], env);
538
+ const parsed = parseJson(result);
539
+ if (!Array.isArray(parsed)) return { row: null, error: `claude plugin list --json failed: ${failure(result)}` };
540
+ return { row: parsed.find((row) => row.id === "mmi@mutmutco") ?? null };
541
+ }
542
+ function claudeMarketplacesPath(env) {
543
+ return env.MMI_UPDATER_CLAUDE_MARKETPLACES || (0, import_node_path6.join)((0, import_node_os3.homedir)(), ".claude", "plugins", "known_marketplaces.json");
544
+ }
545
+ var CLAUDE_PROCESS_SEGMENT = /^claude(-code|-cli)?(\.(exe|cmd|bat|ps1|js|mjs|cjs|py))?$/;
546
+ function claudeProcessSegment(line) {
547
+ for (const token of line.replace(/["']/g, " ").split(/\s+/)) {
548
+ for (const segment of token.split(/[/\\]/)) {
549
+ if (CLAUDE_PROCESS_SEGMENT.test(segment)) return segment;
550
+ }
551
+ }
552
+ return null;
553
+ }
554
+ function claudeHostRunning(env) {
555
+ const marker = env.CLAUDE_CODE_SESSION_ID?.trim() ? "CLAUDE_CODE_SESSION_ID" : env.CLAUDECODE?.trim() ? "CLAUDECODE" : env.CLAUDE_PLUGIN_ROOT?.trim() ? "CLAUDE_PLUGIN_ROOT" : null;
556
+ if (marker) return { running: true, evidence: `${marker} is set \u2014 this process is itself inside a Claude hook` };
557
+ const result = process.platform === "win32" ? (0, import_node_child_process5.spawnSync)("powershell.exe", ["-NoProfile", "-Command", "Get-CimInstance Win32_Process | ForEach-Object { $_.Name + ' ' + $_.CommandLine }"], { encoding: "utf8", windowsHide: true, timeout: 15e3 }) : (0, import_node_child_process5.spawnSync)("ps", ["-eo", "args="], { encoding: "utf8", windowsHide: true, timeout: 15e3 });
558
+ if (result.status !== 0 || !(result.stdout ?? "").trim()) return { running: true, evidence: "the process list is unreadable \u2014 unsafe to write" };
559
+ for (const line of result.stdout.split(/\r?\n/)) {
560
+ const segment = claudeProcessSegment(line);
561
+ if (segment) return { running: true, evidence: `a live process names claude (${segment})` };
562
+ }
563
+ return { running: false, evidence: "no claude-named process in the process list" };
564
+ }
565
+ function ensureClaudeSingleWriter(env, dryRun) {
566
+ const path = claudeMarketplacesPath(env);
567
+ let body;
568
+ try {
569
+ body = JSON.parse((0, import_node_fs7.readFileSync)(path, "utf8"));
570
+ } catch {
571
+ return { ok: false, detail: `cannot read ${path}` };
572
+ }
573
+ const registration = body.mutmutco;
574
+ if (!registration || typeof registration !== "object") return { ok: false, detail: "mutmutco marketplace is not registered" };
575
+ if (registration.autoUpdate !== true) return { ok: true };
576
+ if (dryRun) return { ok: true, detail: "would disable Claude background autoUpdate (updater is the single writer)" };
577
+ const quiescence = claudeHostRunning(env);
578
+ if (quiescence.running) {
579
+ return { ok: false, detail: `Claude background autoUpdate is on, but Claude Code is running and would overwrite the registration \u2014 defer until the host is absent: ${quiescence.evidence}` };
580
+ }
581
+ registration.autoUpdate = false;
582
+ const tmp = `${path}.tmp-${process.pid}`;
583
+ try {
584
+ (0, import_node_fs7.writeFileSync)(tmp, JSON.stringify(body, null, 2) + "\n", "utf8");
585
+ (0, import_node_fs7.renameSync)(tmp, path);
586
+ const reread = JSON.parse((0, import_node_fs7.readFileSync)(path, "utf8"));
587
+ return reread?.mutmutco?.autoUpdate === false ? { ok: true, detail: "disabled Claude background autoUpdate (updater is the single writer)" } : { ok: false, detail: "Claude background autoUpdate write did not persist" };
588
+ } catch (error) {
589
+ return { ok: false, detail: `cannot disable Claude background autoUpdate: ${error?.message ?? error}` };
590
+ }
591
+ }
592
+ function claudeArm(options) {
593
+ const env = options.env ?? process.env;
594
+ const runner = options.runner ?? runHostCommand;
595
+ const before = claudeMmi(runner, env);
596
+ if (before.error) return { surface: "claude", from: null, to: options.target, verdict: "defer", detail: before.error };
597
+ const from = before.row?.version ?? null;
598
+ const singleWriter = ensureClaudeSingleWriter(env, options.dryRun);
599
+ if (!singleWriter.ok) return { surface: "claude", from, to: options.target, verdict: "defer", detail: singleWriter.detail ?? "cannot establish updater as the single writer" };
600
+ if (options.dryRun) {
601
+ const writer = singleWriter.detail ? `; ${singleWriter.detail}` : "";
602
+ return { surface: "claude", from, to: options.target, verdict: "ok", detail: `dry-run: would refresh mutmutco and converge mmi@mutmutco ${from ?? "absent"} -> ${options.target}${writer}` };
603
+ }
604
+ const marketplace = runner("claude", ["plugin", "marketplace", "update", "mutmutco"], env);
605
+ if (marketplace.status !== 0) {
606
+ return { surface: "claude", from, to: options.target, verdict: "defer", detail: `marketplace update failed: ${failure(marketplace)}` };
607
+ }
608
+ let observed = claudeMmi(runner, env);
609
+ if (observed.error) return { surface: "claude", from, to: options.target, verdict: "defer", detail: observed.error };
610
+ if (observed.row?.version === options.target && observed.row.enabled === true) {
611
+ const writer = singleWriter.detail ? `${singleWriter.detail}; ` : "";
612
+ return { surface: "claude", from, to: options.target, verdict: "ok", detail: `${writer}catalog refreshed; installed+enabled at target ${options.target}` };
613
+ }
614
+ const scope = observed.row?.scope || before.row?.scope || "user";
615
+ const command = observed.row ? ["plugin", "update", "mmi@mutmutco", "--scope", scope] : ["plugin", "install", "mmi@mutmutco", "--scope", scope];
616
+ const converge = runner("claude", command, env);
617
+ if (converge.status !== 0) {
618
+ return { surface: "claude", from, to: options.target, verdict: "defer", detail: `${observed.row ? "plugin update" : "plugin install"} failed: ${failure(converge)}` };
619
+ }
620
+ observed = claudeMmi(runner, env);
621
+ if (observed.error) return { surface: "claude", from, to: options.target, verdict: "defer", detail: observed.error };
622
+ if (observed.row && observed.row.version === options.target && observed.row.enabled !== true) {
623
+ const enable = runner("claude", ["plugin", "enable", "mmi@mutmutco", "--scope", observed.row.scope || scope], env);
624
+ if (enable.status !== 0) {
625
+ return { surface: "claude", from, to: options.target, verdict: "defer", detail: `plugin enable failed: ${failure(enable)}` };
626
+ }
627
+ observed = claudeMmi(runner, env);
628
+ }
629
+ if (observed.row?.version !== options.target || observed.row.enabled !== true) {
630
+ return { surface: "claude", from, to: observed.row?.version ?? "unknown", verdict: "fail", detail: `post-update evidence is version ${observed.row?.version ?? "missing"}, enabled=${String(observed.row?.enabled)}, expected ${options.target}/true` };
631
+ }
632
+ return { surface: "claude", from, to: options.target, verdict: "ok", detail: `catalog refreshed; converged and verified ${from ?? "absent"} -> ${options.target}` };
633
+ }
634
+ function codexMmi(runner, env) {
635
+ const result = runner("codex", ["plugin", "list", "--json"], env);
636
+ const parsed = parseJson(result);
637
+ if (!parsed || !Array.isArray(parsed.installed)) return { row: null, error: `codex plugin list --json failed: ${failure(result)}` };
638
+ return { row: parsed.installed.find((row) => row.pluginId === "mmi@mutmutco") ?? null };
639
+ }
640
+ function codexArm(options) {
641
+ const env = options.env ?? process.env;
642
+ const runner = options.runner ?? runHostCommand;
643
+ const before = codexMmi(runner, env);
644
+ if (before.error) return { surface: "codex", from: null, to: options.target, verdict: "defer", detail: before.error };
645
+ const from = before.row?.version ?? null;
646
+ if (options.dryRun) {
647
+ return { surface: "codex", from, to: options.target, verdict: "ok", detail: `dry-run: would upgrade mutmutco and converge mmi@mutmutco ${from ?? "absent"} -> ${options.target}` };
648
+ }
649
+ const upgrade = runner("codex", ["plugin", "marketplace", "upgrade", "mutmutco", "--json"], env);
650
+ const upgradeJson = parseJson(upgrade);
651
+ if (upgrade.status !== 0 || !upgradeJson || (upgradeJson.errors?.length ?? 0) > 0) {
652
+ return { surface: "codex", from, to: options.target, verdict: "defer", detail: `marketplace upgrade failed: ${failure(upgrade)}` };
653
+ }
654
+ let observed = codexMmi(runner, env);
655
+ if (observed.error) return { surface: "codex", from, to: options.target, verdict: "defer", detail: observed.error };
656
+ if (!observed.row) {
657
+ const install = runner("codex", ["plugin", "add", "mmi@mutmutco", "--json"], env);
658
+ if (install.status !== 0) {
659
+ return { surface: "codex", from, to: options.target, verdict: "defer", detail: `plugin add failed: ${failure(install)}` };
660
+ }
661
+ observed = codexMmi(runner, env);
662
+ }
663
+ if (observed.row?.version !== options.target || observed.row.installed !== true || observed.row.enabled !== true) {
664
+ return { surface: "codex", from, to: observed.row?.version ?? "unknown", verdict: "fail", detail: `post-upgrade evidence is version ${observed.row?.version ?? "missing"}, installed=${String(observed.row?.installed)}, enabled=${String(observed.row?.enabled)}, expected ${options.target}/true/true` };
665
+ }
666
+ return { surface: "codex", from, to: options.target, verdict: "ok", detail: `marketplace upgraded; payload refresh and native evidence verified at ${options.target}` };
667
+ }
668
+ var KILO_PACKAGE = "@mutmutco/kilo-plugin";
669
+ function kiloConfigPath(env) {
670
+ return env.MMI_UPDATER_KILO_CONFIG || (0, import_node_path6.join)((0, import_node_os3.homedir)(), ".config", "kilo", "opencode.json");
671
+ }
672
+ function readKiloConfig(env) {
673
+ try {
674
+ const config = JSON.parse((0, import_node_fs7.readFileSync)(kiloConfigPath(env), "utf8"));
675
+ const specs = Array.isArray(config.plugin) && config.plugin.every((entry) => typeof entry === "string") ? config.plugin : [];
676
+ return { config, specs };
677
+ } catch {
678
+ return null;
679
+ }
680
+ }
681
+ function isLegacyMmiKiloPath(spec) {
682
+ const normalized = spec.replaceAll("\\", "/");
683
+ return /\/(?:MMI-Hub|@jervaise\/jerv-cli)\/\.kilo-plugin\/?$/i.test(normalized);
684
+ }
685
+ function normalizeKiloConfig(env, exact) {
686
+ const current = readKiloConfig(env);
687
+ if (!current) return `cannot read ${kiloConfigPath(env)}`;
688
+ const next = current.specs.filter((spec) => !isLegacyMmiKiloPath(spec));
689
+ if (!next.includes(exact)) next.push(exact);
690
+ if (JSON.stringify(next) === JSON.stringify(current.specs)) return null;
691
+ const path = kiloConfigPath(env);
692
+ const tmp = `${path}.tmp-${process.pid}`;
693
+ try {
694
+ (0, import_node_fs7.writeFileSync)(tmp, JSON.stringify({ ...current.config, plugin: next }, null, 2) + "\n", "utf8");
695
+ (0, import_node_fs7.renameSync)(tmp, path);
696
+ return null;
697
+ } catch (error) {
698
+ return `cannot retire legacy Kilo registrations: ${error?.message ?? error}`;
699
+ }
700
+ }
701
+ function kiloVersion(specs) {
702
+ const exact = specs?.find((entry) => entry.startsWith(`${KILO_PACKAGE}@`));
703
+ return exact?.slice(`${KILO_PACKAGE}@`.length) || null;
704
+ }
705
+ function probeNativeHostInstallation(surface, env = process.env, runner = runHostCommand) {
706
+ if (surface === "claude") {
707
+ const observed = claudeMmi(runner, env);
708
+ return {
709
+ version: observed.row?.version ?? null,
710
+ location: "claude plugin list --json",
711
+ detail: observed.error ?? `native host reports enabled=${String(observed.row?.enabled)}`
712
+ };
713
+ }
714
+ if (surface === "codex") {
715
+ const observed = codexMmi(runner, env);
716
+ return {
717
+ version: observed.row?.version ?? null,
718
+ location: "codex plugin list --json",
719
+ detail: observed.error ?? `native host reports installed=${String(observed.row?.installed)}, enabled=${String(observed.row?.enabled)}`
720
+ };
721
+ }
722
+ const config = readKiloConfig(env);
723
+ return {
724
+ version: kiloVersion(config?.specs ?? null),
725
+ location: kiloConfigPath(env),
726
+ detail: config ? "exact package pin read from Kilo config" : "Kilo config was unreadable"
727
+ };
728
+ }
729
+ function kiloArm(options) {
730
+ const env = options.env ?? process.env;
731
+ const runner = options.runner ?? runHostCommand;
732
+ const exact = `${KILO_PACKAGE}@${options.target}`;
733
+ const before = readKiloConfig(env);
734
+ if (!before) return { surface: "kilo", from: null, to: options.target, verdict: "defer", detail: `cannot read ${kiloConfigPath(env)}` };
735
+ const from = kiloVersion(before.specs);
736
+ const legacyCount = before.specs.filter(isLegacyMmiKiloPath).length;
737
+ if (options.dryRun) {
738
+ const action = before.specs.includes(exact) ? "keep exact pin" : `configure ${exact} globally`;
739
+ return { surface: "kilo", from, to: options.target, verdict: "ok", detail: `dry-run: would ${action}${legacyCount ? ` and retire ${legacyCount} legacy MMI path${legacyCount === 1 ? "" : "s"}` : ""} (payload provisions next session)` };
740
+ }
741
+ if (!before.specs.includes(exact)) {
742
+ const install = runner("kilo", ["plugin", exact, "--global", "--force"], env);
743
+ if (install.status !== 0) {
744
+ return { surface: "kilo", from, to: options.target, verdict: "defer", detail: `kilo plugin failed: ${failure(install)}` };
745
+ }
746
+ }
747
+ const normalizeError = normalizeKiloConfig(env, exact);
748
+ if (normalizeError) return { surface: "kilo", from, to: options.target, verdict: "defer", detail: normalizeError };
749
+ const after = readKiloConfig(env);
750
+ const legacyAfter = after?.specs.filter(isLegacyMmiKiloPath) ?? [];
751
+ if (!after?.specs.includes(exact) || legacyAfter.length) {
752
+ return { surface: "kilo", from, to: kiloVersion(after?.specs ?? null) ?? "unknown", verdict: "fail", detail: `post-install config must contain ${exact} and no legacy MMI paths (legacy=${legacyAfter.length})` };
753
+ }
754
+ return { surface: "kilo", from, to: options.target, verdict: "ok", detail: `global config verified at ${exact}; ${legacyCount ? `retired ${legacyCount} legacy MMI path${legacyCount === 1 ? "" : "s"}; ` : ""}payload provisions at next session start` };
755
+ }
756
+
757
+ // src/arm-kimi.ts
758
+ var import_node_child_process6 = require("node:child_process");
759
+ var import_node_fs9 = require("node:fs");
760
+ var import_node_path7 = require("node:path");
761
+
762
+ // src/compat.ts
763
+ var import_node_fs8 = require("node:fs");
764
+ function readManifestCompat(manifestPath) {
765
+ try {
766
+ const parsed = JSON.parse((0, import_node_fs8.readFileSync)(manifestPath, "utf8"));
767
+ return typeof parsed.mmiCompat === "string" ? parsed.mmiCompat : void 0;
768
+ } catch {
769
+ return void 0;
770
+ }
771
+ }
772
+
773
+ // src/arm-kimi.ts
774
+ var KIMI_PACKAGE = "@mutmutco/kimi-plugin";
775
+ var TREE_MARKERS = [".kimi-plugin/plugin.json", "skills/mmi/SKILL.md", "scripts/hook-run.mjs"];
776
+ function treeHealthy(root) {
777
+ return TREE_MARKERS.every((rel) => fileExists((0, import_node_path7.join)(root, ...rel.split("/"))));
778
+ }
779
+ function markerVersion(root) {
780
+ try {
781
+ const version = JSON.parse((0, import_node_fs9.readFileSync)((0, import_node_path7.join)(root, ".kimi-plugin", "plugin.json"), "utf8")).version;
782
+ return typeof version === "string" ? version : null;
783
+ } catch {
784
+ return null;
785
+ }
786
+ }
787
+ function packageVersion(root) {
788
+ try {
789
+ const version = JSON.parse((0, import_node_fs9.readFileSync)((0, import_node_path7.join)(root, "package.json"), "utf8")).version;
790
+ return typeof version === "string" ? version : null;
791
+ } catch {
792
+ return null;
793
+ }
794
+ }
795
+ function probeKimiInstallation(env = process.env) {
796
+ const location = (0, import_node_path7.join)(kimiHome(env), "plugins", "managed", "mmi");
797
+ const version = markerVersion(location);
798
+ return { version, location, detail: version ? "installed Kimi plugin manifest" : "installed Kimi plugin manifest was unreadable" };
799
+ }
800
+ function tail2(output) {
801
+ return output.split("\n").filter(Boolean).slice(-3).join(" | ");
802
+ }
803
+ function scratchRoot(env) {
804
+ return (0, import_node_path7.join)(kimiHome(env), ".mmi-updater");
805
+ }
806
+ function pruneScratchDir(root, keep) {
807
+ try {
808
+ for (const name of (0, import_node_fs9.readdirSync)(root)) {
809
+ if (name === keep) continue;
810
+ try {
811
+ (0, import_node_fs9.rmSync)((0, import_node_path7.join)(root, name), { recursive: true, force: true });
812
+ } catch {
813
+ }
814
+ }
815
+ } catch {
816
+ }
817
+ }
818
+ var KIMI_PROCESS_SEGMENT = /^kimi(-cli|-code)?(\.(exe|cmd|bat|ps1|js|mjs|cjs|py))?$/i;
819
+ function kimiProcessToken(line) {
820
+ for (const token of line.replace(/["']/g, " ").split(/\s+/)) {
821
+ if (token && token.split(/[/\\]/).some((segment) => KIMI_PROCESS_SEGMENT.test(segment))) return token;
822
+ }
823
+ return null;
824
+ }
825
+ function kimiHostRunning(env) {
826
+ if (env.KIMI_PLUGIN_ROOT?.trim()) return { running: true, evidence: "KIMI_PLUGIN_ROOT is set \u2014 this process is itself inside a Kimi hook" };
827
+ const result = process.platform === "win32" ? (0, import_node_child_process6.spawnSync)("powershell.exe", ["-NoProfile", "-Command", "Get-CimInstance Win32_Process | ForEach-Object { $_.Name + ' ' + $_.CommandLine }"], { encoding: "utf8", windowsHide: true, timeout: 15e3 }) : (0, import_node_child_process6.spawnSync)("ps", ["-eo", "args="], { encoding: "utf8", windowsHide: true, timeout: 15e3 });
828
+ if (result.status !== 0 || !(result.stdout ?? "").trim()) return { running: true, evidence: "the process list is unreadable \u2014 unsafe to write" };
829
+ for (const line of result.stdout.split(/\r?\n/)) {
830
+ const token = kimiProcessToken(line);
831
+ if (token) return { running: true, evidence: `a live process names kimi (${token})` };
832
+ }
833
+ return { running: false, evidence: "no kimi-named process in the process list" };
834
+ }
835
+ function restoreQuarantine(quarantine, live) {
836
+ try {
837
+ (0, import_node_fs9.renameSync)(quarantine, live);
838
+ } catch (error) {
839
+ return { restored: false, detail: `${error?.message ?? error}` };
840
+ }
841
+ if (!(0, import_node_fs9.existsSync)(live)) return { restored: false, detail: `${live} is still absent after the restore rename` };
842
+ return { restored: true, detail: `marker ${markerVersion(live) ?? "unreadable"}` };
843
+ }
844
+ function kimiArm(options) {
845
+ const env = options.env ?? process.env;
846
+ const { target, dryRun, paths } = options;
847
+ const result = (from, to, verdict, detail) => ({
848
+ surface: "kimi",
849
+ from,
850
+ to,
851
+ verdict,
852
+ detail,
853
+ // C11: the constraint of the tree LEFT at `live` — read at result time, so ok carries the new
854
+ // manifest's declaration and defer/fail carry the previous tree's (#4973 reconsult ruling).
855
+ compat: readManifestCompat((0, import_node_path7.join)(live, ".kimi-plugin", "plugin.json"))
856
+ });
857
+ const managedParent = (0, import_node_path7.join)(kimiHome(env), "plugins", "managed");
858
+ const live = (0, import_node_path7.join)(managedParent, "mmi");
859
+ const installed = markerVersion(live);
860
+ if (installed && compareSemver(installed, target) > 0) {
861
+ return result(installed, installed, "skip", `installed ${installed} is above candidate ${target} (monotonic)`);
862
+ }
863
+ const highWater = journalHighWater(paths.journalPath, "kimi");
864
+ if (highWater && compareSemver(highWater, target) > 0) {
865
+ return result(installed, highWater, "skip", `journal high-water ${highWater} is above candidate ${target} (monotonic)`);
866
+ }
867
+ if (installed === target && treeHealthy(live)) {
868
+ return result(installed, target, "ok", `already at target ${target}`);
869
+ }
870
+ if (dryRun) {
871
+ return result(installed, target, "ok", `dry-run: would converge ${installed ?? "absent"} -> ${target} (tarball generation switch while Kimi is absent)`);
872
+ }
873
+ const stageRoot = (0, import_node_path7.join)(paths.staging, "kimi", target);
874
+ const staged = (0, import_node_path7.join)(stageRoot, "node_modules", ...KIMI_PACKAGE.split("/"));
875
+ if (!treeHealthy(staged)) {
876
+ (0, import_node_fs9.mkdirSync)(stageRoot, { recursive: true });
877
+ (0, import_node_fs9.writeFileSync)((0, import_node_path7.join)(stageRoot, "package.json"), JSON.stringify({ name: "mmi-updater-stage", private: true }) + "\n");
878
+ const install = runNpm(["install", "--prefix", stageRoot, "--no-audit", "--no-fund", "--no-package-lock", `${KIMI_PACKAGE}@${target}`], env);
879
+ if (install.status !== 0) {
880
+ return result(installed, target, "defer", `staging install failed: ${tail2(install.stderr || install.stdout)}`);
881
+ }
882
+ }
883
+ const stagedPackage = packageVersion(staged);
884
+ if (stagedPackage !== target) {
885
+ return result(installed, target, "defer", `staged package manifest is ${stagedPackage ?? "unreadable"}, expected ${target}`);
886
+ }
887
+ const stagedMarker = markerVersion(staged);
888
+ if (stagedMarker !== target) {
889
+ return result(installed, target, "defer", `staged plugin manifest is ${stagedMarker ?? "unreadable"}, expected ${target}`);
890
+ }
891
+ if (!treeHealthy(staged)) {
892
+ return result(installed, target, "defer", `staged tree is missing one of ${TREE_MARKERS.join(", ")}`);
893
+ }
894
+ const quiescence = kimiHostRunning(env);
895
+ if (quiescence.running) {
896
+ return result(installed, target, "defer", `Kimi may be running \u2014 the managed tree is switched only while the host is absent: ${quiescence.evidence}`);
897
+ }
898
+ const scratch = scratchRoot(env);
899
+ const incomingRoot = (0, import_node_path7.join)(scratch, "incoming");
900
+ const incoming = (0, import_node_path7.join)(incomingRoot, target);
901
+ const quarantineRoot = (0, import_node_path7.join)(scratch, "quarantine");
902
+ const rejectedRoot = (0, import_node_path7.join)(scratch, "rejected");
903
+ try {
904
+ (0, import_node_fs9.mkdirSync)(managedParent, { recursive: true });
905
+ (0, import_node_fs9.mkdirSync)(quarantineRoot, { recursive: true });
906
+ (0, import_node_fs9.rmSync)(incomingRoot, { recursive: true, force: true });
907
+ (0, import_node_fs9.cpSync)(staged, incoming, { recursive: true });
908
+ } catch (error) {
909
+ return result(installed, target, "defer", `cannot place the incoming generation under ${scratch}: ${error?.message ?? error}`);
910
+ }
911
+ if (markerVersion(incoming) !== target || !treeHealthy(incoming)) {
912
+ return result(installed, target, "defer", `incoming generation verifies as ${markerVersion(incoming) ?? "unreadable"}, expected ${target}`);
913
+ }
914
+ const hadLive = (0, import_node_fs9.existsSync)(live);
915
+ const generation = `${installed ?? "unknown"}-${Date.now()}`;
916
+ const quarantine = (0, import_node_path7.join)(quarantineRoot, generation);
917
+ if (hadLive) {
918
+ try {
919
+ (0, import_node_fs9.renameSync)(live, quarantine);
920
+ } catch (error) {
921
+ return result(installed, target, "defer", `cannot quarantine the live generation: ${error?.message ?? error}`);
922
+ }
923
+ }
924
+ try {
925
+ (0, import_node_fs9.renameSync)(incoming, live);
926
+ } catch (error) {
927
+ const failed = `cannot switch the new generation in: ${error?.message ?? error}`;
928
+ if (!hadLive) {
929
+ return result(installed, target, "defer", `${failed} \u2014 no generation was displaced; ${live} is still absent`);
930
+ }
931
+ const restore = restoreQuarantine(quarantine, live);
932
+ return restore.restored ? result(installed, installed ?? "unknown", "defer", `${failed} \u2014 the previous generation is back in place (${restore.detail})`) : result(installed, "none", "fail", `${failed}; ROLLBACK FAILED (${restore.detail}) \u2014 ${live} now has NO plugin and Kimi enforces nothing until this is repaired; the known-good generation is intact at ${quarantine}`);
933
+ }
934
+ const switched = markerVersion(live);
935
+ if (switched !== target || !treeHealthy(live)) {
936
+ const observed = `post-switch marker is ${switched ?? "unreadable"}, expected ${target}`;
937
+ const rejectedName = `${target}-${Date.now()}`;
938
+ try {
939
+ (0, import_node_fs9.mkdirSync)(rejectedRoot, { recursive: true });
940
+ (0, import_node_fs9.renameSync)(live, (0, import_node_path7.join)(rejectedRoot, rejectedName));
941
+ } catch (error) {
942
+ return result(installed, switched ?? "unknown", "fail", `${observed} \u2014 the unverified tree could NOT be moved aside (${error?.message ?? error}) and is STILL LIVE at ${live}${hadLive ? `; the previous generation is intact at ${quarantine}` : ""}`);
943
+ }
944
+ pruneScratchDir(rejectedRoot, rejectedName);
945
+ if (!hadLive) {
946
+ return result(installed, "none", "fail", `${observed} \u2014 the unverified tree is parked at ${(0, import_node_path7.join)(rejectedRoot, rejectedName)}; ${live} is absent (there was no previous generation to restore)`);
947
+ }
948
+ const restore = restoreQuarantine(quarantine, live);
949
+ return restore.restored ? result(installed, installed ?? "unknown", "fail", `${observed} \u2014 the unverified tree is parked at ${(0, import_node_path7.join)(rejectedRoot, rejectedName)}; rolled back to ${installed ?? "the previous generation"} and read back (${restore.detail})`) : result(installed, "none", "fail", `${observed}; ROLLBACK FAILED (${restore.detail}) \u2014 ${live} now has NO plugin and Kimi enforces nothing until this is repaired; the known-good generation is intact at ${quarantine}`);
950
+ }
951
+ pruneScratchDir(quarantineRoot, hadLive ? generation : null);
952
+ pruneScratchDir(rejectedRoot, null);
953
+ try {
954
+ (0, import_node_fs9.rmSync)(incomingRoot, { recursive: true, force: true });
955
+ } catch {
956
+ }
957
+ return result(installed, target, "ok", `converged ${installed ?? "absent"} -> ${target} (staged, verified, switched${hadLive ? `; previous generation quarantined at ${quarantine}` : ""})`);
958
+ }
959
+
960
+ // src/arm-cursor.ts
961
+ var import_node_fs10 = require("node:fs");
962
+ var import_node_child_process7 = require("node:child_process");
963
+ var import_node_os4 = require("node:os");
964
+ var import_node_path8 = require("node:path");
965
+ var PACKAGE = "@mutmutco/cursor-plugin";
966
+ var MANIFEST = ".cursor-plugin/plugin.json";
967
+ var KEEP_GENERATIONS = 2;
968
+ var STRANDED_TMP_AGE_MS = 24 * 60 * 6e4;
969
+ var TREE_FILES = [MANIFEST, "skills/mmi/SKILL.md", "hooks/cursor-hooks.json", "scripts/hook-run.mjs", "scripts/hook-policy.mjs"];
970
+ function cursorPluginsRoot() {
971
+ return (0, import_node_path8.join)((0, import_node_os4.homedir)(), ".cursor", "plugins");
972
+ }
973
+ function treeHealthy2(root) {
974
+ return TREE_FILES.every((rel) => (0, import_node_fs10.existsSync)((0, import_node_path8.join)(root, ...rel.split("/"))));
975
+ }
976
+ function readJson(root, rel) {
977
+ try {
978
+ return JSON.parse((0, import_node_fs10.readFileSync)((0, import_node_path8.join)(root, ...rel.split("/")), "utf8"));
979
+ } catch {
980
+ return null;
981
+ }
982
+ }
983
+ function manifest(root) {
984
+ return readJson(root, MANIFEST);
985
+ }
986
+ function probeCursorInstallation() {
987
+ const location = (0, import_node_path8.join)(cursorPluginsRoot(), "local", "mmi");
988
+ const version = manifest(location)?.version ?? null;
989
+ return { version, location, detail: version ? "installed Cursor plugin manifest" : "installed Cursor plugin manifest was unreadable" };
990
+ }
991
+ function mmiOwned(root) {
992
+ return manifest(root)?.name === "mmi" || readJson(root, "package.json")?.name === PACKAGE;
993
+ }
994
+ function safeSegment(value) {
995
+ return value.replace(/[^0-9A-Za-z.+-]/g, "_").slice(0, 40) || "unknown";
996
+ }
997
+ function message(error) {
998
+ return String(error?.message ?? error).trim().replace(/\s+/g, " ").slice(0, 200);
999
+ }
1000
+ function discard(path) {
1001
+ try {
1002
+ (0, import_node_fs10.rmSync)(path, { recursive: true, force: true });
1003
+ } catch {
1004
+ }
1005
+ }
1006
+ function cursorExecutable(line) {
1007
+ const trimmed = line.trim();
1008
+ if (!trimmed) return null;
1009
+ const quoted = /^"([^"]+)"|^'([^']+)'/.exec(trimmed);
1010
+ const executable = quoted ? quoted[1] ?? quoted[2] : trimmed.split(/\s+/)[0];
1011
+ const base = executable.split(/[/\\]/).pop() ?? "";
1012
+ const host = /^cursor(\.exe)?$/i.test(base) || /[/\\]Cursor\.app[/\\]/i.test(executable);
1013
+ return host ? executable.slice(0, 160) : null;
1014
+ }
1015
+ function cursorHostEvidence(env) {
1016
+ for (const marker of ["CURSOR_TRACE_ID", "CURSOR_SESSION_ID", "CURSOR_EXTENSION_HOST_ROLE"]) {
1017
+ if (env[marker]?.trim()) return `env marker ${marker}`;
1018
+ }
1019
+ if (env.CURSOR_AGENT === "1") return "env marker CURSOR_AGENT=1";
1020
+ const result = process.platform === "win32" ? (0, import_node_child_process7.spawnSync)("powershell.exe", ["-NoProfile", "-Command", "Get-CimInstance Win32_Process | ForEach-Object { $_.CommandLine }"], { encoding: "utf8", windowsHide: true, timeout: 15e3 }) : (0, import_node_child_process7.spawnSync)("ps", ["-eo", "args="], { encoding: "utf8", windowsHide: true, timeout: 15e3 });
1021
+ if (result.status !== 0 || !(result.stdout ?? "").trim()) return "the process table is unreadable (unsafe to write)";
1022
+ for (const line of result.stdout.split(/\r?\n/)) {
1023
+ const executable = cursorExecutable(line);
1024
+ if (executable) return `process ${executable}`;
1025
+ }
1026
+ return null;
1027
+ }
1028
+ function writeRollbackPointer(root, entry) {
1029
+ const path = (0, import_node_path8.join)(root, "quarantine", "rollback.json");
1030
+ const tmp = `${path}.tmp-${process.pid}`;
1031
+ try {
1032
+ (0, import_node_fs10.writeFileSync)(tmp, JSON.stringify({ surface: "cursor", ts: (/* @__PURE__ */ new Date()).toISOString(), ...entry }, null, 2) + "\n", "utf8");
1033
+ (0, import_node_fs10.renameSync)(tmp, path);
1034
+ } catch {
1035
+ discard(tmp);
1036
+ }
1037
+ }
1038
+ function mtimeMs(path) {
1039
+ try {
1040
+ return (0, import_node_fs10.statSync)(path).mtimeMs;
1041
+ } catch {
1042
+ return 0;
1043
+ }
1044
+ }
1045
+ function pruneQuarantine(root, keep) {
1046
+ const dir = (0, import_node_path8.join)(root, "quarantine");
1047
+ let generations;
1048
+ try {
1049
+ generations = (0, import_node_fs10.readdirSync)(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory() && entry.name.startsWith("mmi-")).map((entry) => (0, import_node_path8.join)(dir, entry.name)).sort((a, b) => mtimeMs(b) - mtimeMs(a));
1050
+ } catch {
1051
+ return 0;
1052
+ }
1053
+ let stranded = 0;
1054
+ try {
1055
+ for (const entry of (0, import_node_fs10.readdirSync)(dir, { withFileTypes: true })) {
1056
+ if (!entry.isFile() || !/^rollback\.json\.tmp-\d+$/.test(entry.name)) continue;
1057
+ const path = (0, import_node_path8.join)(dir, entry.name);
1058
+ if (Date.now() - mtimeMs(path) < STRANDED_TMP_AGE_MS) continue;
1059
+ discard(path);
1060
+ stranded += 1;
1061
+ }
1062
+ } catch {
1063
+ }
1064
+ const retained = new Set([...new Set([keep, ...generations].filter((path) => !!path))].slice(0, KEEP_GENERATIONS - 1));
1065
+ let pruned = 0;
1066
+ for (const path of generations) {
1067
+ if (retained.has(path)) continue;
1068
+ discard(path);
1069
+ pruned += 1;
1070
+ }
1071
+ return pruned + stranded;
1072
+ }
1073
+ function restorePrevious(live, quarantined, broken) {
1074
+ try {
1075
+ (0, import_node_fs10.renameSync)(live, broken);
1076
+ } catch {
1077
+ return "kept-broken";
1078
+ }
1079
+ try {
1080
+ (0, import_node_fs10.renameSync)(quarantined, live);
1081
+ return "restored";
1082
+ } catch {
1083
+ try {
1084
+ (0, import_node_fs10.renameSync)(broken, live);
1085
+ return "kept-broken";
1086
+ } catch {
1087
+ return "lost";
1088
+ }
1089
+ }
1090
+ }
1091
+ function cursorArm(options) {
1092
+ const env = options.env ?? process.env;
1093
+ const { target, dryRun, paths } = options;
1094
+ const root = cursorPluginsRoot();
1095
+ const live = (0, import_node_path8.join)(root, "local", "mmi");
1096
+ const result = (from, to, verdict, detail) => ({
1097
+ surface: "cursor",
1098
+ from,
1099
+ to,
1100
+ verdict,
1101
+ detail,
1102
+ // C11: the constraint of the generation LEFT at `live` — read at result time, so a failed
1103
+ // switch reports the restored generation's declaration, not the rejected one's (#4973).
1104
+ compat: readManifestCompat((0, import_node_path8.join)(live, MANIFEST))
1105
+ });
1106
+ const current = manifest(live);
1107
+ const installed = current?.version ?? null;
1108
+ if (installed && compareSemver(installed, target) > 0) {
1109
+ return result(installed, installed, "skip", `installed ${installed} is above candidate ${target} (monotonic)`);
1110
+ }
1111
+ const highWater = journalHighWater(paths.journalPath, "cursor");
1112
+ if (highWater && compareSemver(highWater, target) > 0) {
1113
+ return result(installed, highWater, "skip", `journal high-water ${highWater} is above candidate ${target} (monotonic)`);
1114
+ }
1115
+ if (installed === target && treeHealthy2(live)) {
1116
+ return result(installed, target, "ok", `already at target ${target}`);
1117
+ }
1118
+ if ((0, import_node_fs10.existsSync)(live) && !mmiOwned(live)) {
1119
+ return result(installed, target, "defer", `${live} carries no MMI ownership marker (plugin manifest name, or a ${PACKAGE} package.json) \u2014 refusing to displace an unmanaged directory (mmi-cli plugin heal adopts it)`);
1120
+ }
1121
+ if (dryRun) {
1122
+ return result(installed, target, "ok", `dry-run: would stage ${PACKAGE}@${target} and switch ${installed ?? "absent"} -> ${target} while Cursor is absent`);
1123
+ }
1124
+ const staged = stagePackage({ surface: "cursor", packageName: PACKAGE, target, witness: MANIFEST, paths, env });
1125
+ if ("defer" in staged) {
1126
+ return result(installed, target, "defer", staged.defer);
1127
+ }
1128
+ const stagedMarker = manifest(staged.packageRoot)?.version;
1129
+ if (stagedMarker !== target || !treeHealthy2(staged.packageRoot)) {
1130
+ return result(installed, target, "defer", `staged tree marker is ${stagedMarker ?? "unreadable"} and the tree is ${treeHealthy2(staged.packageRoot) ? "complete" : "incomplete"}, expected ${target} and complete`);
1131
+ }
1132
+ const busy = cursorHostEvidence(env);
1133
+ if (busy) {
1134
+ return result(installed, target, "defer", `Cursor is running (${busy}) \u2014 the plugin tree is only swapped while the host is absent`);
1135
+ }
1136
+ const stamp = Date.now();
1137
+ const incoming = (0, import_node_path8.join)(root, "staging", `mmi-${safeSegment(target)}-${stamp}`);
1138
+ const quarantined = (0, import_node_path8.join)(root, "quarantine", `mmi-${safeSegment(installed ?? "unknown")}-${stamp}`);
1139
+ for (const dir of ["local", "staging", "quarantine"]) (0, import_node_fs10.mkdirSync)((0, import_node_path8.join)(root, dir), { recursive: true });
1140
+ try {
1141
+ discard(incoming);
1142
+ (0, import_node_fs10.cpSync)(staged.packageRoot, incoming, { recursive: true });
1143
+ } catch (error) {
1144
+ discard(incoming);
1145
+ return result(installed, target, "defer", `copy into ${root} failed: ${message(error)}`);
1146
+ }
1147
+ if (manifest(incoming)?.version !== target || !treeHealthy2(incoming)) {
1148
+ discard(incoming);
1149
+ return result(installed, target, "defer", "the copy on the Cursor volume did not reproduce the verified tree");
1150
+ }
1151
+ const started = cursorHostEvidence(env);
1152
+ if (started) {
1153
+ discard(incoming);
1154
+ return result(installed, target, "defer", `Cursor started during staging (${started}) \u2014 the plugin tree is only swapped while the host is absent`);
1155
+ }
1156
+ let displaced = false;
1157
+ try {
1158
+ if ((0, import_node_fs10.existsSync)(live)) {
1159
+ (0, import_node_fs10.renameSync)(live, quarantined);
1160
+ displaced = true;
1161
+ }
1162
+ (0, import_node_fs10.renameSync)(incoming, live);
1163
+ } catch (error) {
1164
+ if (displaced && !(0, import_node_fs10.existsSync)(live)) {
1165
+ try {
1166
+ (0, import_node_fs10.renameSync)(quarantined, live);
1167
+ } catch {
1168
+ }
1169
+ }
1170
+ discard(incoming);
1171
+ return result(installed, target, "defer", `generation switch failed, previous generation retained: ${message(error)}`);
1172
+ }
1173
+ const evidence = manifest(live)?.version;
1174
+ if (evidence !== target || !treeHealthy2(live)) {
1175
+ const broken = `${(0, import_node_path8.join)(root, "quarantine", `mmi-${safeSegment(evidence ?? "broken")}-${stamp}`)}-failed`;
1176
+ const outcome = displaced ? restorePrevious(live, quarantined, broken) : "none";
1177
+ const detail = outcome === "restored" ? `restored the previous generation ${installed ?? "unknown"}, unverified tree kept at ${broken}` : outcome === "lost" ? `restore failed and ${live} is missing \u2014 the previous generation is at ${quarantined}` : displaced ? `restore failed, the unverified tree is still live and the previous generation is at ${quarantined}` : "nothing was displaced \u2014 there is no previous generation to restore";
1178
+ writeRollbackPointer(root, outcome === "restored" ? { installed: installed ?? "unknown", previous: null, generation: null } : { installed: evidence ?? "unknown", previous: installed, generation: displaced ? quarantined : null });
1179
+ return result(installed, outcome === "restored" ? installed ?? "unknown" : evidence ?? "unknown", "fail", `post-switch marker reads ${evidence ?? "missing"}, expected ${target}; ${detail}`);
1180
+ }
1181
+ writeRollbackPointer(root, { installed: target, previous: installed, generation: displaced ? quarantined : null });
1182
+ const pruned = pruneQuarantine(root, displaced ? quarantined : null);
1183
+ const previous = displaced ? `previous generation quarantined at ${quarantined}` : "no previous generation to quarantine";
1184
+ return result(installed, target, "ok", `switched ${installed ?? "absent"} -> ${target}; ${previous}${pruned ? `; pruned ${pruned} older generation${pruned === 1 ? "" : "s"}` : ""}`);
1185
+ }
1186
+
1187
+ // src/arm-jervcode.ts
1188
+ var import_node_fs11 = require("node:fs");
1189
+ var import_node_path9 = require("node:path");
1190
+ var PI_PACKAGE = "@mutmutco/pi-plugin";
1191
+ function hostEnv(env) {
1192
+ const dir = jervcodeAgentDir(env);
1193
+ return { ...env, JERVCODE_CODING_AGENT_DIR: dir, PI_CODING_AGENT_DIR: dir };
1194
+ }
1195
+ function hostCommands(_env) {
1196
+ return ["pi", "jervcode"];
1197
+ }
1198
+ function readPackageSpecs(env) {
1199
+ try {
1200
+ const settings = JSON.parse((0, import_node_fs11.readFileSync)((0, import_node_path9.join)(jervcodeAgentDir(env), "settings.json"), "utf8"));
1201
+ return Array.isArray(settings.packages) ? settings.packages.filter((entry) => typeof entry === "string") : [];
1202
+ } catch {
1203
+ return null;
1204
+ }
1205
+ }
1206
+ function installedVersion(env) {
1207
+ try {
1208
+ const manifest2 = JSON.parse((0, import_node_fs11.readFileSync)((0, import_node_path9.join)(jervcodeAgentDir(env), "npm", "node_modules", ...PI_PACKAGE.split("/"), "package.json"), "utf8"));
1209
+ return typeof manifest2.version === "string" ? manifest2.version : null;
1210
+ } catch {
1211
+ return null;
1212
+ }
1213
+ }
1214
+ function probeJervcodeInstallation(env = process.env) {
1215
+ const location = (0, import_node_path9.join)(jervcodeAgentDir(env), "npm", "node_modules", ...PI_PACKAGE.split("/"));
1216
+ const version = installedVersion(env);
1217
+ return { version, location, detail: version ? "materialized pi package manifest" : "materialized pi package manifest was unreadable" };
1218
+ }
1219
+ function isLegacyMmiPiPath(spec) {
1220
+ return /[/\\]mutmutco[/\\]mmi[/\\]\d+\.\d+\.\d+[/\\]\.pi-plugin[/\\]?$/i.test(spec);
1221
+ }
1222
+ function failure2(result) {
1223
+ return (result.stderr || result.stdout).split("\n").filter(Boolean).slice(-3).join(" | ") || `exit ${result.status ?? "signal"}`;
1224
+ }
1225
+ function jervcodeArm(options) {
1226
+ const env = options.env ?? process.env;
1227
+ const runner = options.runner ?? runHostCommand;
1228
+ const exact = `npm:${PI_PACKAGE}@${options.target}`;
1229
+ const result = (from2, to, verdict, detail) => ({
1230
+ surface: "jervcode",
1231
+ from: from2,
1232
+ to,
1233
+ verdict,
1234
+ detail,
1235
+ // C11: the constraint of the package pi LEFT materialised — read at result time, so defer/fail
1236
+ // carry the previous payload's declaration (#4973 reconsult ruling).
1237
+ compat: readManifestCompat((0, import_node_path9.join)(jervcodeAgentDir(env), "npm", "node_modules", ...PI_PACKAGE.split("/"), "package.json"))
1238
+ });
1239
+ const before = readPackageSpecs(env);
1240
+ if (!before) return result(null, options.target, "defer", `cannot read ${(0, import_node_path9.join)(jervcodeAgentDir(env), "settings.json")}`);
1241
+ const from = installedVersion(env);
1242
+ const legacy = before.filter(isLegacyMmiPiPath);
1243
+ const highWater = journalHighWater(options.paths.journalPath, "jervcode");
1244
+ const floor = from && compareSemver(from, options.target) > 0 ? `installed ${from} is above candidate ${options.target} (monotonic)` : highWater && compareSemver(highWater, options.target) > 0 ? `journal high-water ${highWater} is above candidate ${options.target} (monotonic)` : null;
1245
+ if (options.dryRun) {
1246
+ const action = floor ? `hold ${from ?? "the install"} and skip the install (${floor})` : before.includes(exact) && from === options.target ? "keep exact pin" : `install ${exact}`;
1247
+ return { surface: "jervcode", from, to: options.target, verdict: "ok", detail: `dry-run: would ${action}${legacy.length ? ` and retire ${legacy.length} legacy claude-cache path registration${legacy.length === 1 ? "" : "s"}` : ""}`, compat: readManifestCompat((0, import_node_path9.join)(jervcodeAgentDir(env), "npm", "node_modules", ...PI_PACKAGE.split("/"), "package.json")) };
1248
+ }
1249
+ const hosted = hostEnv(env);
1250
+ const commands = hostCommands(env);
1251
+ const command = commands.find((bin) => runner(bin, ["--version"], hosted).status === 0);
1252
+ if (!command) return { surface: "jervcode", from, to: options.target, verdict: "defer", detail: "neither `jervcode` nor `pi` answered --version \u2014 the host CLI is not on PATH", compat: readManifestCompat((0, import_node_path9.join)(jervcodeAgentDir(env), "npm", "node_modules", ...PI_PACKAGE.split("/"), "package.json")) };
1253
+ if (!floor && (!before.includes(exact) || from !== options.target)) {
1254
+ const install = runner(command, ["install", exact], hosted);
1255
+ if (install.status !== 0) {
1256
+ return { surface: "jervcode", from, to: options.target, verdict: "defer", detail: `${command} install ${exact} failed: ${failure2(install)}`, compat: readManifestCompat((0, import_node_path9.join)(jervcodeAgentDir(env), "npm", "node_modules", ...PI_PACKAGE.split("/"), "package.json")) };
1257
+ }
1258
+ }
1259
+ for (const stale of legacy) {
1260
+ const removed = runner(command, ["remove", stale], hosted);
1261
+ if (removed.status !== 0) {
1262
+ return { surface: "jervcode", from, to: options.target, verdict: "defer", detail: `${command} remove of the legacy registration failed: ${failure2(removed)}`, compat: readManifestCompat((0, import_node_path9.join)(jervcodeAgentDir(env), "npm", "node_modules", ...PI_PACKAGE.split("/"), "package.json")) };
1263
+ }
1264
+ }
1265
+ const after = readPackageSpecs(env);
1266
+ const converged = installedVersion(env);
1267
+ const legacyAfter = after?.filter(isLegacyMmiPiPath) ?? [];
1268
+ const retired = legacy.length ? `; retired ${legacy.length} legacy claude-cache path registration${legacy.length === 1 ? "" : "s"}` : "";
1269
+ if (floor && !legacyAfter.length) {
1270
+ return { surface: "jervcode", from, to: converged ?? highWater ?? options.target, verdict: "skip", detail: `${floor}${retired}`, compat: readManifestCompat((0, import_node_path9.join)(jervcodeAgentDir(env), "npm", "node_modules", ...PI_PACKAGE.split("/"), "package.json")) };
1271
+ }
1272
+ if (!after?.includes(exact) || converged !== options.target || legacyAfter.length) {
1273
+ return {
1274
+ surface: "jervcode",
1275
+ from,
1276
+ to: converged ?? "unknown",
1277
+ verdict: "fail",
1278
+ detail: `post-install evidence is spec=${after?.includes(exact) ? exact : "missing"}, materialised ${converged ?? "nothing"}, legacy=${legacyAfter.length}; expected ${exact} materialised at ${options.target} with no legacy paths`
1279
+ };
1280
+ }
1281
+ return {
1282
+ surface: "jervcode",
1283
+ from,
1284
+ to: options.target,
1285
+ verdict: "ok",
1286
+ detail: `${exact} pinned and materialised at ${options.target}${retired}; loads at the next seat launch`
1287
+ };
1288
+ }
1289
+
1290
+ // src/reap.ts
1291
+ var import_node_child_process8 = require("node:child_process");
1292
+ var import_node_fs12 = require("node:fs");
1293
+ var import_node_path10 = require("node:path");
1294
+ var KEEP_GENERATIONS2 = 2;
1295
+ var SCRATCH_AGE_MS = 24 * 60 * 6e4;
1296
+ var ORPHAN_MIN_AGE_MS = 10 * 6e4;
1297
+ var SNAPSHOT_TIMEOUT_MS = 2e4;
1298
+ var MAX_UNWIND_ROUNDS = 4;
1299
+ function listDirs(path) {
1300
+ try {
1301
+ return (0, import_node_fs12.readdirSync)(path, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
1302
+ } catch {
1303
+ return [];
1304
+ }
1305
+ }
1306
+ function generationSets(root) {
1307
+ const children = listDirs(root);
1308
+ if (children.some((name) => parseSemver(name))) return [root];
1309
+ return children.map((name) => (0, import_node_path10.join)(root, name)).filter((path) => listDirs(path).some((name) => parseSemver(name)));
1310
+ }
1311
+ function pruneGenerations(root, category, dryRun) {
1312
+ let reaped = 0;
1313
+ const failures = [];
1314
+ for (const set of generationSets(root)) {
1315
+ const versions = listDirs(set).filter((name) => parseSemver(name)).sort((a, b) => compareSemver(b, a));
1316
+ for (const version of versions.slice(KEEP_GENERATIONS2)) {
1317
+ if (dryRun) {
1318
+ reaped += 1;
1319
+ continue;
1320
+ }
1321
+ try {
1322
+ (0, import_node_fs12.rmSync)((0, import_node_path10.join)(set, version), { recursive: true, force: true });
1323
+ reaped += 1;
1324
+ } catch (error) {
1325
+ failures.push(`${version}: ${error?.code ?? error?.message ?? error}`);
1326
+ }
1327
+ }
1328
+ }
1329
+ if (!reaped && !failures.length) return { category, reaped: 0, verdict: "skip", detail: `${root}: within keep-${KEEP_GENERATIONS2}` };
1330
+ const verb = dryRun ? "dry-run: would prune" : "pruned";
1331
+ const stuck = failures.length ? `; ${failures.length} locked (${failures.join(", ")})` : "";
1332
+ return { category, reaped, verdict: failures.length ? "defer" : "ok", detail: `${verb} ${reaped} generation(s) below keep-${KEEP_GENERATIONS2} under ${root}${stuck}` };
1333
+ }
1334
+ var LEASE_STEAL_SCRATCH = /^lease\.lock\.stale-\d+$/;
1335
+ function atomicWriteScratch(path) {
1336
+ const literal = (0, import_node_path10.basename)(path).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1337
+ return { dir: (0, import_node_path10.dirname)(path), pattern: new RegExp(`^${literal}\\.tmp-\\d+$`) };
1338
+ }
1339
+ function reapScratch(context, dryRun) {
1340
+ const category = "scratch";
1341
+ const scanned = /* @__PURE__ */ new Map();
1342
+ const scan = (dir, pattern) => void scanned.set(dir, [...scanned.get(dir) ?? [], pattern]);
1343
+ scan(context.root, LEASE_STEAL_SCRATCH);
1344
+ for (const path of context.atomicWritePaths) {
1345
+ const { dir, pattern } = atomicWriteScratch(path);
1346
+ scan(dir, pattern);
1347
+ }
1348
+ const now = Date.now();
1349
+ let reaped = 0;
1350
+ const failures = [];
1351
+ const unreadable = [];
1352
+ for (const [dir, patterns] of scanned) {
1353
+ let entries;
1354
+ try {
1355
+ entries = (0, import_node_fs12.readdirSync)(dir, { withFileTypes: true });
1356
+ } catch {
1357
+ unreadable.push(dir);
1358
+ continue;
1359
+ }
1360
+ for (const entry of entries) {
1361
+ if (!patterns.some((pattern) => pattern.test(entry.name))) continue;
1362
+ if (!entry.isFile()) continue;
1363
+ const path = (0, import_node_path10.join)(dir, entry.name);
1364
+ let ageMs;
1365
+ try {
1366
+ ageMs = now - (0, import_node_fs12.statSync)(path).mtimeMs;
1367
+ } catch {
1368
+ continue;
1369
+ }
1370
+ if (ageMs < SCRATCH_AGE_MS) continue;
1371
+ if (dryRun) {
1372
+ reaped += 1;
1373
+ continue;
1374
+ }
1375
+ try {
1376
+ (0, import_node_fs12.rmSync)(path, { force: true });
1377
+ reaped += 1;
1378
+ } catch (error) {
1379
+ failures.push(`${entry.name}: ${error?.code ?? error?.message ?? error}`);
1380
+ }
1381
+ }
1382
+ }
1383
+ const swept = [...scanned.keys()].filter((dir) => !unreadable.includes(dir));
1384
+ if (!reaped && !failures.length) return { category, reaped: 0, verdict: "skip", detail: `no scratch older than ${SCRATCH_AGE_MS / 36e5}h under ${swept.join(", ") || "any readable scratch directory"}` };
1385
+ const verb = dryRun ? "dry-run: would remove" : "removed";
1386
+ const stuck = failures.length ? `; ${failures.length} locked (${failures.join(", ")})` : "";
1387
+ return { category, reaped, verdict: failures.length ? "defer" : "ok", detail: `${verb} ${reaped} aged scratch entr(ies) under ${swept.join(", ")}${stuck}` };
1388
+ }
1389
+ var HOOK_IMAGES = /* @__PURE__ */ new Set(["mmi-hook.exe", "node.exe", "cmd.exe", "powershell.exe", "pwsh.exe"]);
1390
+ var HOOK_ARTIFACT = /(?:^|[\\/])(?:mmi-hook(?:-console)?(?:\.exe|\.cmd|\.bat)?|hook-run\.mjs)$/i;
1391
+ var NODE_INTERPRETER = /(?:^|[\\/])node(?:\.exe)?$/i;
1392
+ var NODE_VALUE_FLAGS = /* @__PURE__ */ new Set(["-e", "--eval", "-p", "--print", "-r", "--require", "--import", "--loader", "--experimental-loader", "-C", "--conditions"]);
1393
+ var CMD_SHELL = /(?:^|[\\/])cmd(?:\.exe)?$/i;
1394
+ var CMD_RUN_FLAGS = /* @__PURE__ */ new Set(["/c", "/k"]);
1395
+ var PS_SHELL = /(?:^|[\\/])(?:powershell|pwsh)(?:\.exe)?$/i;
1396
+ var PS_COMMAND_FLAGS = /* @__PURE__ */ new Set(["-command", "-c"]);
1397
+ var SEPARATORS = ["&", "|"];
1398
+ function tokenize(commandLine) {
1399
+ const tokens = [];
1400
+ let current = "";
1401
+ let quoted = false;
1402
+ let slashes = 0;
1403
+ for (const char of commandLine) {
1404
+ if (char === "\\") {
1405
+ slashes += 1;
1406
+ continue;
1407
+ }
1408
+ if (char === '"') {
1409
+ current += "\\".repeat(slashes >> 1);
1410
+ if (slashes % 2) current += '"';
1411
+ else quoted = !quoted;
1412
+ slashes = 0;
1413
+ continue;
1414
+ }
1415
+ current += "\\".repeat(slashes);
1416
+ slashes = 0;
1417
+ if (!quoted && /\s/.test(char)) {
1418
+ if (current) tokens.push(current);
1419
+ current = "";
1420
+ continue;
1421
+ }
1422
+ current += char;
1423
+ }
1424
+ current += "\\".repeat(slashes);
1425
+ if (current) tokens.push(current);
1426
+ return tokens;
1427
+ }
1428
+ function invocation(commandLine) {
1429
+ let tokens = tokenize(commandLine);
1430
+ if (tokens.length && CMD_SHELL.test(tokens[0])) {
1431
+ const flag = tokens.findIndex((token) => CMD_RUN_FLAGS.has(token.toLowerCase()));
1432
+ if (flag < 0) return [];
1433
+ const rest = tokens.slice(flag + 1);
1434
+ tokens = rest.length === 1 ? tokenize(rest[0]) : rest;
1435
+ } else if (tokens.length && PS_SHELL.test(tokens[0])) {
1436
+ const flag = tokens.findIndex((token) => PS_COMMAND_FLAGS.has(token.toLowerCase()));
1437
+ if (flag < 0) return [];
1438
+ const rest = tokens.slice(flag + 1);
1439
+ tokens = rest.length === 1 ? tokenize(rest[0]) : rest;
1440
+ if (tokens[0] === "&") tokens = tokens.slice(1);
1441
+ }
1442
+ const cut = tokens.findIndex((token) => SEPARATORS.some((separator) => token.includes(separator)));
1443
+ return cut < 0 ? tokens : tokens.slice(0, cut);
1444
+ }
1445
+ function hookProgram(tokens) {
1446
+ if (!tokens.length) return null;
1447
+ if (HOOK_ARTIFACT.test(tokens[0])) return tokens[0];
1448
+ if (!NODE_INTERPRETER.test(tokens[0])) return null;
1449
+ let at = 1;
1450
+ while (at < tokens.length && tokens[at].startsWith("-")) {
1451
+ const flag = tokens[at].toLowerCase();
1452
+ at += !flag.includes("=") && NODE_VALUE_FLAGS.has(flag) ? 2 : 1;
1453
+ }
1454
+ return at < tokens.length && HOOK_ARTIFACT.test(tokens[at]) ? tokens[at] : null;
1455
+ }
1456
+ function flagValue(tokens, flag) {
1457
+ const at = tokens.indexOf(flag);
1458
+ return at >= 0 && at + 1 < tokens.length ? tokens[at + 1] : null;
1459
+ }
1460
+ function selectHookOrphans(rows, livePids, options) {
1461
+ const kills = [];
1462
+ for (const row of rows) {
1463
+ if (row.pid === options.selfPid) continue;
1464
+ if (!HOOK_IMAGES.has(row.name.toLowerCase())) continue;
1465
+ const tokens = invocation(row.commandLine);
1466
+ const program = hookProgram(tokens);
1467
+ if (!program) continue;
1468
+ const surface = flagValue(tokens, "--surface");
1469
+ const gate = flagValue(tokens, "--gate");
1470
+ if (!surface || !gate) continue;
1471
+ if (livePids.has(row.ppid)) continue;
1472
+ if (row.startedMs === null) continue;
1473
+ const ageMs = options.now - row.startedMs;
1474
+ if (ageMs < options.minAgeMs) continue;
1475
+ kills.push({
1476
+ pid: row.pid,
1477
+ name: row.name,
1478
+ reason: `runs ${program} --surface ${surface} --gate ${gate}; parent ${row.ppid} absent from the snapshot; alive ${Math.round(ageMs / 6e4)}m`,
1479
+ identity: row
1480
+ });
1481
+ }
1482
+ return kills;
1483
+ }
1484
+ function stillTheProvenProcess(rows, kill) {
1485
+ const live = rows.find((row) => row.pid === kill.pid);
1486
+ if (!live) return false;
1487
+ const same = live.name === kill.identity.name && live.commandLine === kill.identity.commandLine && live.startedMs === kill.identity.startedMs && live.ppid === kill.identity.ppid;
1488
+ return same && !rows.some((row) => row.pid === kill.identity.ppid);
1489
+ }
1490
+ var SNAPSHOT_COMMAND = "Get-CimInstance Win32_Process | Select-Object ProcessId,ParentProcessId,Name,CommandLine,@{n='Started';e={if ($_.CreationDate) { $_.CreationDate.ToUniversalTime().ToString('o') }}} | ConvertTo-Json -Compress";
1491
+ function processSnapshot() {
1492
+ const result = (0, import_node_child_process8.spawnSync)("powershell.exe", ["-NoProfile", "-Command", SNAPSHOT_COMMAND], { encoding: "utf8", windowsHide: true, timeout: SNAPSHOT_TIMEOUT_MS });
1493
+ if (result.status !== 0 || !(result.stdout ?? "").trim()) return null;
1494
+ try {
1495
+ const parsed = JSON.parse(result.stdout);
1496
+ const rows = Array.isArray(parsed) ? parsed : [parsed];
1497
+ return rows.filter((row) => typeof row?.ProcessId === "number").map((row) => ({
1498
+ pid: row.ProcessId,
1499
+ ppid: typeof row.ParentProcessId === "number" ? row.ParentProcessId : -1,
1500
+ name: typeof row.Name === "string" ? row.Name : "",
1501
+ commandLine: typeof row.CommandLine === "string" ? row.CommandLine : "",
1502
+ startedMs: typeof row.Started === "string" ? Number.isNaN(Date.parse(row.Started)) ? null : Date.parse(row.Started) : null
1503
+ }));
1504
+ } catch {
1505
+ return null;
1506
+ }
1507
+ }
1508
+ function reapHookOrphans(dryRun) {
1509
+ const category = "hook-orphans";
1510
+ if (process.platform !== "win32") {
1511
+ return { category, reaped: 0, verdict: "skip", detail: "dead-parent hook reap is Windows-only (#4943 class) \u2014 skipped" };
1512
+ }
1513
+ const snapshot = processSnapshot();
1514
+ if (!snapshot) {
1515
+ return { category, reaped: 0, verdict: "skip", detail: "process list unreadable \u2014 nothing is provably an orphan, so nothing was killed" };
1516
+ }
1517
+ const select = (rows) => selectHookOrphans(rows, new Set(rows.map((row) => row.pid)), { now: Date.now(), selfPid: process.pid, minAgeMs: ORPHAN_MIN_AGE_MS });
1518
+ const first = select(snapshot);
1519
+ if (!first.length) return { category, reaped: 0, verdict: "skip", detail: `no dead-parent MMI hook descendants among ${snapshot.length} processes` };
1520
+ if (dryRun) {
1521
+ const named2 = first.map((kill) => `${kill.name}#${kill.pid} (${kill.reason})`).join("; ");
1522
+ return { category, reaped: first.length, verdict: "ok", detail: `dry-run: would kill ${first.length} dead-parent hook orphan(s) in the first unwind round: ${named2}` };
1523
+ }
1524
+ const reaped = [];
1525
+ const survived = [];
1526
+ const declined = [];
1527
+ const attempted = /* @__PURE__ */ new Set();
1528
+ let pending = first;
1529
+ let rounds = 0;
1530
+ let halted = "";
1531
+ while (pending.length && rounds < MAX_UNWIND_ROUNDS) {
1532
+ rounds += 1;
1533
+ const confirm = processSnapshot();
1534
+ if (!confirm) {
1535
+ halted = "a re-validation snapshot was unreadable, so the remaining rows were not killed";
1536
+ break;
1537
+ }
1538
+ const proven = pending.filter((kill) => stillTheProvenProcess(confirm, kill));
1539
+ declined.push(...pending.filter((kill) => !proven.includes(kill)));
1540
+ for (const kill of pending) attempted.add(kill.pid);
1541
+ pending = [];
1542
+ if (!proven.length) break;
1543
+ for (const orphan of proven) {
1544
+ try {
1545
+ process.kill(orphan.pid);
1546
+ } catch {
1547
+ }
1548
+ }
1549
+ const after = processSnapshot();
1550
+ if (!after) {
1551
+ halted = `${proven.length} kill(s) were issued but the verification snapshot was unreadable \u2014 they are not claimed reaped`;
1552
+ break;
1553
+ }
1554
+ const alive = new Set(after.map((row) => row.pid));
1555
+ for (const orphan of proven) (alive.has(orphan.pid) ? survived : reaped).push(orphan);
1556
+ pending = select(after).filter((kill) => !attempted.has(kill.pid));
1557
+ }
1558
+ const notes = [];
1559
+ if (survived.length) notes.push(`${survived.length} survived the kill (${survived.map((kill) => kill.pid).join(", ")})`);
1560
+ if (declined.length) notes.push(`${declined.length} declined at re-validation \u2014 gone on its own, or no longer the row that was proven (${declined.map((kill) => kill.pid).join(", ")})`);
1561
+ if (pending.length) {
1562
+ const why = rounds >= MAX_UNWIND_ROUNDS ? `the ${MAX_UNWIND_ROUNDS}-round unwind budget ran out` : "this tick stopped short";
1563
+ notes.push(`${pending.length} hook orphan(s) still standing because ${why} (${pending.map((kill) => `${kill.name}#${kill.pid}`).join(", ")}) \u2014 next tick continues`);
1564
+ }
1565
+ if (halted) notes.push(halted);
1566
+ const named = reaped.map((kill) => `${kill.name}#${kill.pid} (${kill.reason})`).join("; ");
1567
+ if (!notes.length) {
1568
+ return { category, reaped: reaped.length, verdict: "ok", detail: `reaped ${reaped.length} dead-parent hook orphan(s) over ${rounds} unwind round(s), verified gone: ${named}` };
1569
+ }
1570
+ return {
1571
+ category,
1572
+ reaped: reaped.length,
1573
+ verdict: "defer",
1574
+ detail: `reaped ${reaped.length} dead-parent hook orphan(s) over ${rounds} unwind round(s)${named ? ` (${named})` : ""}; ${notes.join("; ")}`
1575
+ };
1576
+ }
1577
+ function reap(options) {
1578
+ const { paths, dryRun } = options;
1579
+ const env = options.env ?? process.env;
1580
+ const guarded = (category, run) => {
1581
+ try {
1582
+ return run();
1583
+ } catch (error) {
1584
+ return { category, reaped: 0, verdict: "fail", detail: `reap failed: ${error?.message ?? error}` };
1585
+ }
1586
+ };
1587
+ const atomicWritePaths = [claudeMarketplacesPath(env), kiloConfigPath(env)];
1588
+ return [
1589
+ guarded("staging-prune", () => pruneGenerations(paths.staging, "staging-prune", dryRun)),
1590
+ guarded("scratch", () => reapScratch({ root: paths.root, atomicWritePaths }, dryRun)),
1591
+ guarded("hook-orphans", () => reapHookOrphans(dryRun))
1592
+ ];
1593
+ }
1594
+
1595
+ // src/reconcile.ts
1596
+ var REPO_URL_DEFAULT = "https://github.com/mutmutco/MMI-Hub.git";
1597
+ var CANDIDATE_BOUND_DEFAULT = 12;
1598
+ function reconcile(options) {
1599
+ const env = options.env ?? process.env;
1600
+ const say = (line) => options.narrate?.(line);
1601
+ const run = `${(/* @__PURE__ */ new Date()).toISOString()}-${process.pid}`;
1602
+ const paths = ensureState(env);
1603
+ const repoUrl = env.MMI_UPDATER_REPO || REPO_URL_DEFAULT;
1604
+ const bound = Number(env.MMI_UPDATER_CANDIDATES || CANDIDATE_BOUND_DEFAULT) || CANDIDATE_BOUND_DEFAULT;
1605
+ const journal = (event) => {
1606
+ const stamped = options.dryRun ? { ...event, dryRun: true } : event;
1607
+ if (!appendJournal(paths.journalPath, { ts: (/* @__PURE__ */ new Date()).toISOString(), run, ...stamped })) summary.journalOk = false;
1608
+ };
1609
+ const summary = { run, target: null, gate: null, atomicity: null, cli: null, updater: null, plugins: [], reap: [], surfaces: [], journalOk: true, exit: 0 };
1610
+ const recordArm = (arm) => {
1611
+ journal({ kind: "arm", surface: arm.surface, from: arm.from, to: arm.to, verdict: arm.verdict, detail: arm.detail, compat: arm.compat });
1612
+ if (arm.verdict === "defer") summary.exit = Math.max(summary.exit, 2);
1613
+ if (arm.verdict === "fail") summary.exit = Math.max(summary.exit, 3);
1614
+ };
1615
+ try {
1616
+ say("acquiring single-writer lease");
1617
+ return withLease(paths.leasePath, () => {
1618
+ summary.reap = reap({ paths, dryRun: options.dryRun, env });
1619
+ for (const row of summary.reap) journal({ kind: "reap", surface: row.category, verdict: row.verdict, count: row.reaped, detail: row.detail });
1620
+ for (const row of summary.reap.filter((entry) => entry.verdict !== "skip")) say(`reap ${row.category}: ${row.verdict}${row.detail ? " \u2014 " + row.detail : ""}`);
1621
+ say("polling npm for @mutmutco/cli versions");
1622
+ const versions = npmVersions("@mutmutco/cli", env).sort((a, b) => compareSemver(b, a));
1623
+ if (!versions.length) {
1624
+ journal({ kind: "poll", verdict: "fail", detail: "@mutmutco/cli has no npm versions \u2014 cannot discover a candidate" });
1625
+ summary.exit = 1;
1626
+ summary.detail = "npm poll returned no versions for @mutmutco/cli";
1627
+ return summary;
1628
+ }
1629
+ say("fetching release BOM gate cache");
1630
+ ensureBareCache(paths.gitCache, repoUrl);
1631
+ fetchIntoCache(paths.gitCache, repoUrl, "refs/heads/main:refs/heads/main");
1632
+ for (const candidate of versions.slice(0, bound)) {
1633
+ say(`gating candidate v${candidate}`);
1634
+ const gate = gateCandidate(paths.gitCache, repoUrl, candidate);
1635
+ if (!gate.ok) {
1636
+ say(`candidate v${candidate} rejected (${gate.reason ?? "gate"})`);
1637
+ journal({ kind: "candidate-reject", to: candidate, verdict: "skip", detail: gate.reason ?? "gate rejected" });
1638
+ continue;
1639
+ }
1640
+ const atomicity = checkAtomicity(paths.gitCache, gate.tagCommit, gate.bom, candidate, {
1641
+ distIntegrity: (spec) => {
1642
+ const integrity = npmView(spec, "dist.integrity", env);
1643
+ return typeof integrity === "string" ? integrity : null;
1644
+ }
1645
+ });
1646
+ if (!atomicity.ok) {
1647
+ journal({ kind: "candidate-reject", to: candidate, verdict: "skip", detail: `atomicity: ${atomicity.rows.filter((row) => !row.ok).map((row) => row.detail).join("; ")}` });
1648
+ continue;
1649
+ }
1650
+ summary.target = candidate;
1651
+ summary.gate = `tag v${candidate} reachable from main, BOM stamp coherent`;
1652
+ summary.atomicity = atomicity;
1653
+ journal({ kind: "target", to: candidate, verdict: "ok", detail: summary.gate });
1654
+ break;
1655
+ }
1656
+ if (!summary.target) {
1657
+ say("no green target \u2014 deferred");
1658
+ journal({ kind: "target", verdict: "defer", detail: `no green candidate in the newest ${Math.min(bound, versions.length)} versions \u2014 registry not ready for this fleet stamp yet` });
1659
+ for (const probe of enumerateSurfaces(env)) {
1660
+ summary.surfaces.push({ id: probe.id, present: probe.present, action: probe.present ? "deferred-no-target" : "absent-skip" });
1661
+ journal({ kind: "surface", surface: probe.id, present: probe.present, verdict: "skip", detail: probe.present ? "present; no green target yet" : `absent (${probe.marker}) \u2014 skipped` });
1662
+ }
1663
+ return summary;
1664
+ }
1665
+ say(`target v${summary.target} (${summary.gate})`);
1666
+ say(`cli: converging to v${summary.target}`);
1667
+ const cli = cliArm({ target: summary.target, dryRun: options.dryRun, paths, env });
1668
+ summary.cli = cli;
1669
+ recordArm(cli);
1670
+ say(`cli: ${cli.verdict}${cli.detail ? " \u2014 " + cli.detail : ""}`);
1671
+ for (const probe of enumerateSurfaces(env)) {
1672
+ if (!probe.present) {
1673
+ summary.surfaces.push({ id: probe.id, present: false, action: "absent-skip" });
1674
+ journal({ kind: "surface", surface: probe.id, present: false, verdict: "skip", detail: `absent (${probe.marker}) \u2014 skipped` });
1675
+ continue;
1676
+ }
1677
+ say(`${probe.id}: converging to v${summary.target}`);
1678
+ const arm = probe.id === "claude" ? claudeArm({ target: summary.target, dryRun: options.dryRun, env }) : probe.id === "codex" ? codexArm({ target: summary.target, dryRun: options.dryRun, env }) : probe.id === "kilo" ? kiloArm({ target: summary.target, dryRun: options.dryRun, env }) : probe.id === "kimi" ? kimiArm({ target: summary.target, dryRun: options.dryRun, paths, env }) : probe.id === "cursor" ? cursorArm({ target: summary.target, dryRun: options.dryRun, paths, env }) : probe.id === "jervcode" ? jervcodeArm({ target: summary.target, dryRun: options.dryRun, paths, env }) : null;
1679
+ if (arm) {
1680
+ summary.plugins.push(arm);
1681
+ summary.surfaces.push({ id: probe.id, present: true, action: `arm-${arm.verdict}` });
1682
+ recordArm(arm);
1683
+ say(`${probe.id}: ${arm.verdict}${arm.detail ? " \u2014 " + arm.detail : ""}`);
1684
+ } else {
1685
+ summary.surfaces.push({ id: probe.id, present: true, action: "arm-pending" });
1686
+ journal({ kind: "surface", surface: probe.id, present: true, verdict: "skip", detail: `present, its converge arm has not landed yet (target ${summary.target})` });
1687
+ }
1688
+ }
1689
+ say("updater: self-bootstrap");
1690
+ const self = selfArm({ target: summary.target, dryRun: options.dryRun, paths, env });
1691
+ summary.updater = self;
1692
+ recordArm(self);
1693
+ say(`updater: ${self.verdict}${self.detail ? " \u2014 " + self.detail : ""}`);
1694
+ return summary;
1695
+ });
1696
+ } catch (error) {
1697
+ if (error instanceof UpdaterBusy || error instanceof GateEnvError) {
1698
+ journal({ kind: "reconcile", verdict: "defer", detail: error.message });
1699
+ summary.exit = 1;
1700
+ summary.detail = error.message;
1701
+ return summary;
1702
+ }
1703
+ throw error;
1704
+ } finally {
1705
+ if (!summary.journalOk && summary.exit < 4) summary.exit = 4;
1706
+ }
1707
+ }
1708
+
1709
+ // src/scheduler.ts
1710
+ var import_node_child_process9 = require("node:child_process");
1711
+ var import_node_fs13 = require("node:fs");
1712
+ var import_node_path11 = require("node:path");
1713
+ var TASK_NAME = "MMI Fleet Updater";
1714
+ var LAUNCHER_FILE = "reconcile-hidden.vbs";
1715
+ var RUN_BOUND = "PT15M";
1716
+ var REPEAT_INTERVAL = "PT1H";
1717
+ var CADENCE = `hourly from registration, StartWhenAvailable, single-instance, ${RUN_BOUND} bound`;
1718
+ function schtasks(args) {
1719
+ const result = (0, import_node_child_process9.spawnSync)("schtasks.exe", args, { encoding: "utf8", windowsHide: true });
1720
+ return { status: result.status, stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
1721
+ }
1722
+ function xmlEscape(value) {
1723
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1724
+ }
1725
+ function localStartBoundary(now) {
1726
+ const pad = (value) => String(value).padStart(2, "0");
1727
+ return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}T${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`;
1728
+ }
1729
+ function launcherVbs(nodeExe, hubDist) {
1730
+ const vbsQuote = (value) => value.replaceAll('"', '""');
1731
+ return `' MMI Hub maintenance launcher \u2014 generated by \`mmi-hub autoupdate on\`.
1732
+ ' wscript //B starts node with the window hidden so the hourly tick never flashes a console.
1733
+ ' The exit code is propagated and reported by \`mmi-hub status\`.
1734
+ Dim shell, code
1735
+ Set shell = CreateObject("WScript.Shell")
1736
+ code = shell.Run("""${vbsQuote(nodeExe)}"" ""${vbsQuote(hubDist)}"" update", 0, True)
1737
+ WScript.Quit code
1738
+ `;
1739
+ }
1740
+ function taskXml(launcherPath, workDir, userId, startBoundary) {
1741
+ return `<?xml version="1.0" encoding="UTF-16"?>
1742
+ <Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
1743
+ <RegistrationInfo>
1744
+ <Description>MMI Hub maintenance \u2014 converge this machine's MMI surfaces to the newest gated release</Description>
1745
+ </RegistrationInfo>
1746
+ <Triggers>
1747
+ <TimeTrigger>
1748
+ <StartBoundary>${xmlEscape(startBoundary)}</StartBoundary>
1749
+ <Enabled>true</Enabled>
1750
+ <Repetition>
1751
+ <Interval>${REPEAT_INTERVAL}</Interval>
1752
+ <StopAtDurationEnd>false</StopAtDurationEnd>
1753
+ </Repetition>
1754
+ </TimeTrigger>
1755
+ </Triggers>
1756
+ <Principals>
1757
+ <Principal id="Author">
1758
+ <UserId>${xmlEscape(userId)}</UserId>
1759
+ <LogonType>InteractiveToken</LogonType>
1760
+ <RunLevel>LeastPrivilege</RunLevel>
1761
+ </Principal>
1762
+ </Principals>
1763
+ <Settings>
1764
+ <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
1765
+ <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
1766
+ <StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
1767
+ <AllowHardTerminate>true</AllowHardTerminate>
1768
+ <StartWhenAvailable>true</StartWhenAvailable>
1769
+ <ExecutionTimeLimit>${RUN_BOUND}</ExecutionTimeLimit>
1770
+ <Enabled>true</Enabled>
1771
+ </Settings>
1772
+ <Actions Context="Author">
1773
+ <Exec>
1774
+ <Command>wscript.exe</Command>
1775
+ <Arguments>//B //Nologo "${xmlEscape(launcherPath)}"</Arguments>
1776
+ <WorkingDirectory>${xmlEscape(workDir)}</WorkingDirectory>
1777
+ </Exec>
1778
+ </Actions>
1779
+ </Task>
1780
+ `;
1781
+ }
1782
+ function readLauncher(path) {
1783
+ try {
1784
+ return (0, import_node_fs13.readFileSync)(path, "utf8");
1785
+ } catch {
1786
+ return null;
1787
+ }
1788
+ }
1789
+ function psQuote(value) {
1790
+ return `'${value.replaceAll("'", "''")}'`;
1791
+ }
1792
+ function elevatedRelaunch(mode, env) {
1793
+ if (env.MMI_UPDATER_ELEVATED_RETRY === "1") return false;
1794
+ const self = process.argv[1];
1795
+ const command = self ? `"${process.execPath}" "${self}" autoupdate ${mode}` : `mmi-hub autoupdate ${mode}`;
1796
+ const relaunch = (0, import_node_child_process9.spawnSync)(
1797
+ "powershell.exe",
1798
+ ["-NoProfile", "-Command", `Start-Process -Verb RunAs -Wait -WindowStyle Hidden -FilePath 'cmd.exe' -ArgumentList '/c',${psQuote(`set MMI_UPDATER_ELEVATED_RETRY=1&& ${command}`)}`],
1799
+ { encoding: "utf8", windowsHide: true }
1800
+ );
1801
+ return relaunch.status === 0;
1802
+ }
1803
+ var NEVER_RUN_RESULT = 267011;
1804
+ var NEVER_RUN_YEAR = 1999;
1805
+ function taskRunInfo() {
1806
+ const result = (0, import_node_child_process9.spawnSync)(
1807
+ "powershell.exe",
1808
+ ["-NoProfile", "-Command", `$stamp = { param($t) if ($t) { $t.ToString('yyyy-MM-dd HH:mm:ss') } }; Get-ScheduledTaskInfo -TaskName '${TASK_NAME}' | Select-Object @{n='LastRunTime';e={& $stamp $_.LastRunTime}},LastTaskResult,@{n='NextRunTime';e={& $stamp $_.NextRunTime}} | ConvertTo-Json -Compress`],
1809
+ { encoding: "utf8", windowsHide: true, timeout: 3e4 }
1810
+ );
1811
+ if (result.status !== 0) return null;
1812
+ try {
1813
+ const parsed = JSON.parse(result.stdout);
1814
+ return {
1815
+ lastRunTime: typeof parsed.LastRunTime === "string" ? parsed.LastRunTime : null,
1816
+ lastTaskResult: typeof parsed.LastTaskResult === "number" ? parsed.LastTaskResult : null,
1817
+ nextRunTime: typeof parsed.NextRunTime === "string" ? parsed.NextRunTime : null
1818
+ };
1819
+ } catch {
1820
+ return null;
1821
+ }
1822
+ }
1823
+ function neverRan(info) {
1824
+ return info.lastTaskResult === NEVER_RUN_RESULT || (info.lastRunTime?.includes(String(NEVER_RUN_YEAR)) ?? false);
1825
+ }
1826
+ function schedulerStatus(env = process.env) {
1827
+ const empty = { lastRunTime: null, lastTaskResult: null, nextRunTime: null };
1828
+ if (process.platform !== "win32") {
1829
+ return { ok: false, supported: false, enabled: false, ...empty, detail: "automatic scheduling is available on Windows Task Scheduler only" };
1830
+ }
1831
+ const query = schtasks(["/query", "/tn", TASK_NAME, "/xml"]);
1832
+ if (query.status !== 0) {
1833
+ return { ok: false, supported: true, enabled: false, ...empty, detail: `"${TASK_NAME}" is not registered \u2014 run \`mmi-hub autoupdate on\`` };
1834
+ }
1835
+ const hubDist = globalDist(env, "@mutmutco/hub", "dist/index.cjs");
1836
+ const launcherPath = (0, import_node_path11.join)(statePaths(env).root, LAUNCHER_FILE);
1837
+ const info = taskRunInfo();
1838
+ const runInfo = info ?? empty;
1839
+ const fail = (detail) => ({ ok: false, supported: true, enabled: true, ...runInfo, detail });
1840
+ if (!query.stdout.includes(launcherPath)) {
1841
+ return fail(`"${TASK_NAME}" action does not run the hidden launcher ${launcherPath} \u2014 run \`mmi-hub autoupdate on\``);
1842
+ }
1843
+ const launcher = readLauncher(launcherPath);
1844
+ if (hubDist && !launcher?.includes(hubDist)) {
1845
+ return fail(`"${TASK_NAME}" launcher does not point at the current @mutmutco/hub dist ${hubDist} \u2014 run \`mmi-hub autoupdate on\``);
1846
+ }
1847
+ if (query.stdout.includes("<LogonTrigger>")) {
1848
+ return fail(`"${TASK_NAME}" still carries the dormant logon-only trigger \u2014 run \`mmi-hub autoupdate on\``);
1849
+ }
1850
+ if (!query.stdout.includes("<TimeTrigger>") || !query.stdout.includes(`<Interval>${REPEAT_INTERVAL}</Interval>`)) {
1851
+ return fail(`"${TASK_NAME}" has no ${REPEAT_INTERVAL}-repeating TimeTrigger \u2014 run \`mmi-hub autoupdate on\``);
1852
+ }
1853
+ if (info && neverRan(info) && !info.nextRunTime) {
1854
+ return fail(`"${TASK_NAME}" has never run and has no next run time \u2014 run \`mmi-hub autoupdate on\``);
1855
+ }
1856
+ const points = `points at ${hubDist ?? "the registered Hub dist"}`;
1857
+ const ran = !info ? "run history unreadable" : neverRan(info) ? "never run yet" : `last run ${info.lastRunTime} \u2192 exit ${info.lastTaskResult ?? "unknown"}`;
1858
+ return { ok: true, supported: true, enabled: true, ...runInfo, detail: `"${TASK_NAME}" registered ${CADENCE}; ${points}; ${ran}; next ${runInfo.nextRunTime ?? "unscheduled"}` };
1859
+ }
1860
+ function installTask(env = process.env) {
1861
+ if (process.platform !== "win32") return { ok: false, detail: "scheduler registration is Windows Task Scheduler only" };
1862
+ const current = schedulerStatus(env);
1863
+ if (current.ok) return { ok: true, detail: `already enabled \u2014 ${current.detail}` };
1864
+ const hubDist = globalDist(env, "@mutmutco/hub", "dist/index.cjs");
1865
+ if (!hubDist) return { ok: false, detail: "cannot resolve the global @mutmutco/hub dist \u2014 run `npm install -g @mutmutco/hub` first" };
1866
+ const paths = ensureState(env);
1867
+ const userId = `${env.USERDOMAIN || env.COMPUTERNAME || ""}\\${env.USERNAME || ""}`.replace(/^\\/, "");
1868
+ if (!env.USERNAME) return { ok: false, detail: "cannot resolve the registering user (USERNAME unset) \u2014 the task principal must be explicit" };
1869
+ const launcherPath = (0, import_node_path11.join)(paths.root, LAUNCHER_FILE);
1870
+ (0, import_node_fs13.writeFileSync)(launcherPath, launcherVbs(process.execPath, hubDist), "utf8");
1871
+ const xmlPath = (0, import_node_path11.join)(paths.root, "task.xml");
1872
+ (0, import_node_fs13.writeFileSync)(xmlPath, "\uFEFF" + taskXml(launcherPath, paths.root, userId, localStartBoundary(/* @__PURE__ */ new Date())), "utf16le");
1873
+ const create = schtasks(["/create", "/tn", TASK_NAME, "/xml", xmlPath, "/f"]);
1874
+ if (create.status !== 0) {
1875
+ const denied = /access is denied/i.test(create.stderr + create.stdout);
1876
+ if (!denied || !elevatedRelaunch("on", env)) {
1877
+ return { ok: false, detail: denied ? "task creation needs elevation and UAC consent was not granted \u2014 run `mmi-hub autoupdate on` from an elevated terminal" : `schtasks /create failed: ${(create.stderr || create.stdout).trim()}` };
1878
+ }
1879
+ const verify = schedulerStatus(env);
1880
+ return verify.ok ? { ok: true, detail: `enabled "${TASK_NAME}" (elevated) \u2014 ${CADENCE}` } : { ok: false, detail: `elevated registration did not take: ${verify.detail}` };
1881
+ }
1882
+ return { ok: true, detail: `enabled "${TASK_NAME}" \u2014 ${CADENCE} \u2192 ${hubDist}` };
1883
+ }
1884
+ function uninstallTask(env = process.env) {
1885
+ if (process.platform !== "win32") return { ok: false, detail: "scheduler registration is Windows Task Scheduler only" };
1886
+ const del = schtasks(["/delete", "/tn", TASK_NAME, "/f"]);
1887
+ if (del.status !== 0) {
1888
+ const query = schtasks(["/query", "/tn", TASK_NAME]);
1889
+ if (query.status !== 0 && !/access is denied/i.test(del.stderr + del.stdout)) {
1890
+ dropLauncher(env);
1891
+ return { ok: true, detail: `automatic updates already off \u2014 "${TASK_NAME}" is not registered; installed tooling was kept` };
1892
+ }
1893
+ const denied = /access is denied/i.test(del.stderr + del.stdout);
1894
+ if (!denied || !elevatedRelaunch("off", env)) {
1895
+ return { ok: false, detail: denied ? "task deletion needs elevation and UAC consent was not granted \u2014 run `mmi-hub autoupdate off` from an elevated terminal" : `schtasks /delete failed: ${(del.stderr || del.stdout).trim()}` };
1896
+ }
1897
+ if (schtasks(["/query", "/tn", TASK_NAME]).status === 0) {
1898
+ return { ok: false, detail: `elevated deletion did not take: "${TASK_NAME}" is still registered` };
1899
+ }
1900
+ }
1901
+ dropLauncher(env);
1902
+ return { ok: true, detail: `automatic updates off \u2014 removed "${TASK_NAME}"; installed tooling was kept` };
1903
+ }
1904
+ function dropLauncher(env) {
1905
+ try {
1906
+ (0, import_node_fs13.rmSync)((0, import_node_path11.join)(statePaths(env).root, LAUNCHER_FILE), { force: true });
1907
+ } catch {
1908
+ }
1909
+ }
1910
+
1911
+ // src/status.ts
1912
+ var import_node_fs14 = require("node:fs");
1913
+ function readJournal(path) {
1914
+ try {
1915
+ const events = (0, import_node_fs14.readFileSync)(path, "utf8").split("\n").filter((line) => line.trim()).map((line) => {
1916
+ try {
1917
+ return JSON.parse(line);
1918
+ } catch {
1919
+ return null;
1920
+ }
1921
+ }).filter((event) => event !== null);
1922
+ return { events, error: null };
1923
+ } catch (error) {
1924
+ return error?.code === "ENOENT" ? { events: [], error: null } : { events: [], error: `journal unreadable: ${error?.message ?? error}` };
1925
+ }
1926
+ }
1927
+ function versionState(installed, expected) {
1928
+ if (!installed || !expected) return "unknown";
1929
+ const relation = compareSemver(installed, expected);
1930
+ return relation === 0 ? "current" : relation > 0 ? "ahead" : "behind";
1931
+ }
1932
+ function hubStatus(hubVersion, env = process.env, invokedPath = process.argv[1]) {
1933
+ const paths = statePaths(env);
1934
+ const { events, error } = readJournal(paths.journalPath);
1935
+ const realEvents = events.filter((event) => !event.dryRun);
1936
+ const expectedVersion = [...realEvents].reverse().find((event) => event.kind === "target" && event.verdict === "ok" && event.to)?.to ?? null;
1937
+ const latestEvent = realEvents.at(-1) ?? null;
1938
+ const latestRunEvents = latestEvent ? realEvents.filter((event) => event.run === latestEvent.run) : [];
1939
+ const actual = /* @__PURE__ */ new Map();
1940
+ const hubDist = globalDist(env, "@mutmutco/hub", "dist/index.cjs");
1941
+ const globalHubVersion = hubDist ? probeVersion(hubDist) : null;
1942
+ actual.set("hub", globalHubVersion ? { version: globalHubVersion, location: hubDist, detail: "absolute global Hub binary" } : { version: hubVersion, location: invokedPath ?? null, detail: "currently executing Hub bundle (global package location unavailable)" });
1943
+ actual.set("cli", probeCliInstallation(env));
1944
+ for (const surface of enumerateSurfaces(env).filter((probe) => probe.present)) {
1945
+ if (surface.id === "claude" || surface.id === "codex" || surface.id === "kilo") {
1946
+ actual.set(surface.id, probeNativeHostInstallation(surface.id, env));
1947
+ } else if (surface.id === "kimi") {
1948
+ actual.set(surface.id, probeKimiInstallation(env));
1949
+ } else if (surface.id === "cursor") {
1950
+ actual.set(surface.id, probeCursorInstallation());
1951
+ } else if (surface.id === "jervcode") {
1952
+ actual.set(surface.id, probeJervcodeInstallation(env));
1953
+ }
1954
+ }
1955
+ const installed = {};
1956
+ for (const [surface, probe] of actual) {
1957
+ installed[surface] = {
1958
+ installed: probe.version,
1959
+ expected: expectedVersion,
1960
+ location: probe.location,
1961
+ state: versionState(probe.version, expectedVersion),
1962
+ detail: probe.detail
1963
+ };
1964
+ }
1965
+ const schedule = schedulerStatus(env);
1966
+ const failures = [];
1967
+ if (error) failures.push(`${error} \u2014 inspect permissions for ${paths.journalPath}`);
1968
+ if (!events.length && !error) failures.push("no maintenance journal yet \u2014 run `mmi-hub install`");
1969
+ if (!schedule.ok) failures.push(schedule.detail);
1970
+ for (const [surface, row] of Object.entries(installed)) {
1971
+ if (!row.installed) failures.push(`${surface}: installed version is unreadable at ${row.location ?? "an unknown location"} \u2014 run \`mmi-hub update\``);
1972
+ if (row.state === "behind") failures.push(`${surface}: installed ${row.installed} is behind expected ${row.expected} \u2014 run \`mmi-hub update\``);
1973
+ }
1974
+ for (const event of latestRunEvents) {
1975
+ if ((event.kind === "arm" || event.kind === "reconcile") && (event.verdict === "fail" || event.verdict === "defer")) {
1976
+ const surface = event.surface === "updater" ? "hub" : event.surface ?? event.kind;
1977
+ failures.push(`last run ${surface}: ${event.detail ?? event.verdict} \u2014 run \`mmi-hub update\``);
1978
+ }
1979
+ }
1980
+ return {
1981
+ hubVersion,
1982
+ expectedVersion,
1983
+ installed,
1984
+ schedule,
1985
+ lastRun: latestEvent ? { id: latestEvent.run, at: latestEvent.ts } : null,
1986
+ journalPath: paths.journalPath,
1987
+ failures: [...new Set(failures)]
1988
+ };
1989
+ }
1990
+ function formatHubStatus(status) {
1991
+ const lines = [
1992
+ `mmi-hub ${status.hubVersion} status`,
1993
+ ` expected: ${status.expectedVersion ?? "unknown (no gated target recorded)"}`,
1994
+ " installed:"
1995
+ ];
1996
+ for (const [surface, row] of Object.entries(status.installed)) {
1997
+ lines.push(` ${surface}: ${row.installed ?? "unknown"} (expected ${row.expected ?? "unknown"}; ${row.state})`);
1998
+ lines.push(` ${row.location ?? "location unavailable"} \u2014 ${row.detail}`);
1999
+ }
2000
+ lines.push(` autoupdate: ${status.schedule.enabled ? "on" : "off"} \u2014 ${status.schedule.detail}`);
2001
+ lines.push(` last run: ${status.lastRun ? `${status.lastRun.at} (${status.lastRun.id})` : "never recorded"}`);
2002
+ if (status.failures.length) {
2003
+ lines.push(" actionable failures:");
2004
+ for (const failure3 of status.failures) lines.push(` - ${failure3}`);
2005
+ } else {
2006
+ lines.push(" actionable failures: none");
2007
+ }
2008
+ return lines.join("\n");
2009
+ }
2010
+
2011
+ // src/index.ts
2012
+ function ownVersion() {
2013
+ try {
2014
+ return JSON.parse((0, import_node_fs15.readFileSync)((0, import_node_path12.join)(__dirname, "..", "package.json"), "utf8")).version;
2015
+ } catch {
2016
+ return "0.0.0";
2017
+ }
2018
+ }
2019
+ var HELP = `mmi-hub ${ownVersion()} \u2014 install and maintain MMI tooling
2020
+
2021
+ status [--json] read installed/expected versions, hourly schedule, last run,
2022
+ and actionable failures (default; never heals)
2023
+ install [--json] converge the CLI and every present host surface now, then enable
2024
+ hourly automatic updates
2025
+ update [--dry-run] [--json] converge immediately to the newest gated release
2026
+ autoupdate on|off idempotently enable or disable hourly convergence; off keeps tools
2027
+ --version print the Hub maintenance package version
2028
+
2029
+ State remains under $MMI_UPDATER_HOME (default ~/.mmi/updater) for the compatibility window. The
2030
+ Windows task name also remains "MMI Fleet Updater", so existing installations migrate in place.
2031
+ Status performs no npm install, host repair, scheduler write, state mkdir, or repository cleanup.`;
2032
+ function printReconcile(summary, json) {
2033
+ if (json) {
2034
+ process.stdout.write(JSON.stringify(summary, null, 2) + "\n");
2035
+ return;
2036
+ }
2037
+ const target = summary.target ?? "none (deferred)";
2038
+ process.stdout.write(`mmi-hub: target ${target}
2039
+ `);
2040
+ if (summary.cli) process.stdout.write(` cli: ${summary.cli.verdict} \u2014 ${summary.cli.detail}
2041
+ `);
2042
+ for (const arm of summary.plugins) process.stdout.write(` ${arm.surface}: ${arm.verdict} \u2014 ${arm.detail}
2043
+ `);
2044
+ if (summary.updater) process.stdout.write(` hub: ${summary.updater.verdict} \u2014 ${summary.updater.detail}
2045
+ `);
2046
+ for (const row of summary.reap.filter((entry) => entry.verdict !== "skip")) {
2047
+ process.stdout.write(` maintenance ${row.category}: ${row.verdict} \u2014 ${row.detail}
2048
+ `);
2049
+ }
2050
+ const armed = new Set(summary.plugins.map((arm) => arm.surface));
2051
+ for (const surface of summary.surfaces.filter((surface2) => !armed.has(surface2.id))) {
2052
+ process.stdout.write(` ${surface.id}: ${surface.present ? "present" : "absent"} (${surface.action})
2053
+ `);
2054
+ }
2055
+ }
2056
+ function runUpdate(args) {
2057
+ const narrate = (line) => {
2058
+ process.stderr.write(`mmi-hub: ${line}
2059
+ `);
2060
+ };
2061
+ return reconcile({ dryRun: args.includes("--dry-run"), narrate });
2062
+ }
2063
+ function main(args = process.argv.slice(2)) {
2064
+ if (args.includes("--version")) {
2065
+ process.stdout.write(ownVersion() + "\n");
2066
+ return 0;
2067
+ }
2068
+ if (args.includes("--help") || args.includes("-h")) {
2069
+ process.stdout.write(HELP + "\n");
2070
+ return 0;
2071
+ }
2072
+ const command = args.find((arg) => !arg.startsWith("--")) ?? "status";
2073
+ const json = args.includes("--json");
2074
+ if (command === "status") {
2075
+ const status = hubStatus(ownVersion());
2076
+ process.stdout.write(json ? JSON.stringify(status, null, 2) + "\n" : formatHubStatus(status) + "\n");
2077
+ return status.failures.length ? 1 : 0;
2078
+ }
2079
+ if (command === "update") {
2080
+ const summary = runUpdate(args);
2081
+ printReconcile(summary, json);
2082
+ return summary.exit;
2083
+ }
2084
+ if (command === "install") {
2085
+ const summary = runUpdate(args);
2086
+ const schedule = installTask();
2087
+ if (json) {
2088
+ process.stdout.write(JSON.stringify({ convergence: summary, autoupdate: schedule }, null, 2) + "\n");
2089
+ } else {
2090
+ printReconcile(summary, false);
2091
+ process.stdout.write(`mmi-hub install: ${schedule.detail}
2092
+ `);
2093
+ }
2094
+ return Math.max(summary.exit, schedule.ok ? 0 : 1);
2095
+ }
2096
+ if (command === "autoupdate") {
2097
+ const commandIndex = args.indexOf(command);
2098
+ const mode = args[commandIndex + 1];
2099
+ if (mode !== "on" && mode !== "off") {
2100
+ process.stderr.write("mmi-hub autoupdate requires on or off\n");
2101
+ return 1;
2102
+ }
2103
+ const result = mode === "on" ? installTask() : uninstallTask();
2104
+ process.stdout.write(json ? JSON.stringify({ mode, ...result }, null, 2) + "\n" : `mmi-hub autoupdate ${mode}: ${result.detail}
2105
+ `);
2106
+ return result.ok ? 0 : 1;
2107
+ }
2108
+ process.stdout.write(HELP + "\n");
2109
+ return 1;
2110
+ }
2111
+ if (typeof require !== "undefined" && require.main === module) {
2112
+ process.exit(main());
2113
+ }
2114
+ // Annotate the CommonJS export names for ESM import in node:
2115
+ 0 && (module.exports = {
2116
+ hubStatus,
2117
+ launcherVbs,
2118
+ localStartBoundary,
2119
+ main,
2120
+ ownVersion,
2121
+ schedulerStatus,
2122
+ taskXml
2123
+ });