@mutmutco/hub 4.0.2 → 4.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +296 -38
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -27,6 +27,8 @@ __export(index_exports, {
|
|
|
27
27
|
hermesArm: () => hermesArm,
|
|
28
28
|
hermesHome: () => hermesHome,
|
|
29
29
|
hubStatus: () => hubStatus,
|
|
30
|
+
launchdPlist: () => launchdPlist,
|
|
31
|
+
launcherSh: () => launcherSh,
|
|
30
32
|
launcherVbs: () => launcherVbs,
|
|
31
33
|
localStartBoundary: () => localStartBoundary,
|
|
32
34
|
main: () => main,
|
|
@@ -37,6 +39,8 @@ __export(index_exports, {
|
|
|
37
39
|
retryHint: () => retryHint,
|
|
38
40
|
schedulerStatus: () => schedulerStatus,
|
|
39
41
|
startSpinner: () => startSpinner,
|
|
42
|
+
systemdServiceUnit: () => systemdServiceUnit,
|
|
43
|
+
systemdTimerUnit: () => systemdTimerUnit,
|
|
40
44
|
taskXml: () => taskXml,
|
|
41
45
|
updateHeader: () => updateHeader,
|
|
42
46
|
wrapWords: () => wrapWords
|
|
@@ -150,6 +154,22 @@ function journalHighWater(path, surface) {
|
|
|
150
154
|
}
|
|
151
155
|
return high;
|
|
152
156
|
}
|
|
157
|
+
function journalSurfaceRepeatedFail(path, surface, currentRun) {
|
|
158
|
+
let lastVerdict = null;
|
|
159
|
+
try {
|
|
160
|
+
for (const line of (0, import_node_fs.readFileSync)(path, "utf8").split("\n")) {
|
|
161
|
+
if (!line.trim()) continue;
|
|
162
|
+
try {
|
|
163
|
+
const event = JSON.parse(line);
|
|
164
|
+
if (event.dryRun || event.kind !== "arm" || event.surface !== surface || event.run === currentRun) continue;
|
|
165
|
+
lastVerdict = event.verdict;
|
|
166
|
+
} catch {
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
} catch {
|
|
170
|
+
}
|
|
171
|
+
return lastVerdict === "fail";
|
|
172
|
+
}
|
|
153
173
|
function parseSemver(version) {
|
|
154
174
|
const match = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/.exec(version.trim());
|
|
155
175
|
if (!match) return null;
|
|
@@ -200,6 +220,11 @@ function runNpm(args, env = process.env, options = {}) {
|
|
|
200
220
|
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 });
|
|
201
221
|
return { status: result.status, stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
|
|
202
222
|
}
|
|
223
|
+
function globalRoot(env = process.env) {
|
|
224
|
+
const root = runNpm(["root", "-g"], env);
|
|
225
|
+
if (root.status !== 0 || !root.stdout.trim()) return null;
|
|
226
|
+
return root.stdout.trim();
|
|
227
|
+
}
|
|
203
228
|
function npmView(spec, field, env = process.env) {
|
|
204
229
|
const args = ["view", spec, "--json"];
|
|
205
230
|
if (field) args.splice(2, 0, field);
|
|
@@ -362,9 +387,8 @@ var import_node_fs5 = require("node:fs");
|
|
|
362
387
|
var import_node_child_process3 = require("node:child_process");
|
|
363
388
|
var import_node_path4 = require("node:path");
|
|
364
389
|
function globalCliDist(env) {
|
|
365
|
-
const
|
|
366
|
-
|
|
367
|
-
return (0, import_node_path4.join)(prefix.stdout.trim(), "node_modules", "@mutmutco", "cli", "dist", "index.cjs");
|
|
390
|
+
const root = globalRoot(env);
|
|
391
|
+
return root ? (0, import_node_path4.join)(root, "@mutmutco", "cli", "dist", "index.cjs") : null;
|
|
368
392
|
}
|
|
369
393
|
function probeCliVersion(distPath) {
|
|
370
394
|
if (!fileExists(distPath)) return null;
|
|
@@ -421,9 +445,10 @@ function cliArm(options) {
|
|
|
421
445
|
if (globalInstall.status !== 0) {
|
|
422
446
|
return { surface: "cli", from: installed, to: target, verdict: "defer", detail: `global install failed: ${(globalInstall.stderr || globalInstall.stdout).split("\n").filter(Boolean).slice(-3).join(" | ")}` };
|
|
423
447
|
}
|
|
424
|
-
const
|
|
448
|
+
const switchedDist = globalCliDist(env);
|
|
449
|
+
const switched = probeCliVersion(switchedDist ?? "");
|
|
425
450
|
if (switched !== target) {
|
|
426
|
-
return { surface: "cli", from: installed, to: switched ?? installed ?? "unknown", verdict: "fail", detail: `post-switch probe returned ${switched ?? "dead"}, expected ${target}` };
|
|
451
|
+
return { surface: "cli", from: installed, to: switched ?? installed ?? "unknown", verdict: "fail", detail: `post-switch probe returned ${switched ?? "dead"} at ${switchedDist ?? "an unresolvable path"}, expected ${target}; the global install already ran \u2014 nothing was rolled back` };
|
|
427
452
|
}
|
|
428
453
|
return { surface: "cli", from: installed, to: target, verdict: "ok", detail: `converged ${installed ?? "absent"} -> ${target} (staged, verified, switched)` };
|
|
429
454
|
}
|
|
@@ -433,9 +458,8 @@ var import_node_fs6 = require("node:fs");
|
|
|
433
458
|
var import_node_child_process4 = require("node:child_process");
|
|
434
459
|
var import_node_path5 = require("node:path");
|
|
435
460
|
function globalDist(env, packageName, distRel) {
|
|
436
|
-
const
|
|
437
|
-
|
|
438
|
-
return (0, import_node_path5.join)(prefix.stdout.trim(), "node_modules", ...packageName.split("/"), ...distRel.split("/"));
|
|
461
|
+
const root = globalRoot(env);
|
|
462
|
+
return root ? (0, import_node_path5.join)(root, ...packageName.split("/"), ...distRel.split("/")) : null;
|
|
439
463
|
}
|
|
440
464
|
function probeVersion(distPath) {
|
|
441
465
|
if (!fileExists(distPath)) return null;
|
|
@@ -501,9 +525,10 @@ function npmGlobalArm(options) {
|
|
|
501
525
|
if (globalInstall.status !== 0) {
|
|
502
526
|
return result(installed, target, "defer", `global install failed: ${tail(globalInstall.stderr || globalInstall.stdout)}`);
|
|
503
527
|
}
|
|
504
|
-
const
|
|
528
|
+
const switchedDist = globalDist(env, packageName, distRel);
|
|
529
|
+
const switched = probeVersion(switchedDist ?? "");
|
|
505
530
|
if (switched !== target) {
|
|
506
|
-
return result(installed, switched ?? installed ?? "unknown", "fail", `post-switch probe returned ${switched ?? "dead"}, expected ${target}`);
|
|
531
|
+
return result(installed, switched ?? installed ?? "unknown", "fail", `post-switch probe returned ${switched ?? "dead"} at ${switchedDist ?? "an unresolvable path"}, expected ${target}; the global install already ran \u2014 nothing was rolled back`);
|
|
507
532
|
}
|
|
508
533
|
return result(installed, target, "ok", `converged ${installed ?? "absent"} -> ${target} (staged, verified, switched)`);
|
|
509
534
|
}
|
|
@@ -1787,6 +1812,7 @@ function reconcile(options) {
|
|
|
1787
1812
|
};
|
|
1788
1813
|
const summary = { run, target: null, gate: null, atomicity: null, cli: null, updater: null, plugins: [], reap: [], surfaces: [], journalOk: true, exit: 0 };
|
|
1789
1814
|
const recordArm = (arm) => {
|
|
1815
|
+
if (arm.verdict === "fail") arm.repeatFailure = journalSurfaceRepeatedFail(paths.journalPath, arm.surface, run);
|
|
1790
1816
|
journal({ kind: "arm", surface: arm.surface, from: arm.from, to: arm.to, verdict: arm.verdict, detail: arm.detail, compat: arm.compat });
|
|
1791
1817
|
options.onArm?.(arm);
|
|
1792
1818
|
if (arm.verdict === "defer") summary.exit = Math.max(summary.exit, 2);
|
|
@@ -1890,16 +1916,37 @@ function reconcile(options) {
|
|
|
1890
1916
|
// src/scheduler.ts
|
|
1891
1917
|
var import_node_child_process9 = require("node:child_process");
|
|
1892
1918
|
var import_node_fs14 = require("node:fs");
|
|
1919
|
+
var import_node_os5 = require("node:os");
|
|
1893
1920
|
var import_node_path12 = require("node:path");
|
|
1894
1921
|
var TASK_NAME = "MMI Fleet Updater";
|
|
1895
1922
|
var LAUNCHER_FILE = "reconcile-hidden.vbs";
|
|
1896
1923
|
var RUN_BOUND = "PT15M";
|
|
1897
1924
|
var REPEAT_INTERVAL = "PT1H";
|
|
1898
1925
|
var CADENCE = `hourly from registration, StartWhenAvailable, single-instance, ${RUN_BOUND} bound`;
|
|
1926
|
+
var POSIX_LAUNCHER_FILE = "reconcile.sh";
|
|
1927
|
+
var STARTINTERVAL_SECONDS = 3600;
|
|
1928
|
+
var RUN_BOUND_SECONDS = 900;
|
|
1929
|
+
var POSIX_CADENCE = "hourly from registration, catch-up-if-missed, single-instance";
|
|
1930
|
+
var LAUNCHD_LABEL = "com.mutmutco.mmi-hub.updater";
|
|
1931
|
+
var LAUNCHD_PLIST_NAME = `${LAUNCHD_LABEL}.plist`;
|
|
1932
|
+
var SYSTEMD_UNIT = "mmi-hub-updater";
|
|
1933
|
+
var SYSTEMD_SERVICE_NAME = `${SYSTEMD_UNIT}.service`;
|
|
1934
|
+
var SYSTEMD_TIMER_NAME = `${SYSTEMD_UNIT}.timer`;
|
|
1899
1935
|
function schtasks(args) {
|
|
1900
1936
|
const result = (0, import_node_child_process9.spawnSync)("schtasks.exe", args, { encoding: "utf8", windowsHide: true });
|
|
1901
1937
|
return { status: result.status, stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
|
|
1902
1938
|
}
|
|
1939
|
+
function launchctl(args) {
|
|
1940
|
+
const result = (0, import_node_child_process9.spawnSync)("launchctl", args, { encoding: "utf8", windowsHide: true });
|
|
1941
|
+
return { status: result.status, stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
|
|
1942
|
+
}
|
|
1943
|
+
function systemctlUser(args) {
|
|
1944
|
+
const result = (0, import_node_child_process9.spawnSync)("systemctl", ["--user", ...args], { encoding: "utf8", windowsHide: true });
|
|
1945
|
+
return { status: result.status, stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
|
|
1946
|
+
}
|
|
1947
|
+
function uid() {
|
|
1948
|
+
return typeof process.getuid === "function" ? process.getuid() : 0;
|
|
1949
|
+
}
|
|
1903
1950
|
function xmlEscape(value) {
|
|
1904
1951
|
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1905
1952
|
}
|
|
@@ -1918,6 +1965,14 @@ code = shell.Run("""${vbsQuote(nodeExe)}"" ""${vbsQuote(hubDist)}"" update", 0,
|
|
|
1918
1965
|
WScript.Quit code
|
|
1919
1966
|
`;
|
|
1920
1967
|
}
|
|
1968
|
+
function launcherSh(nodeExe, hubDist) {
|
|
1969
|
+
const shQuote = (value) => `'${value.replaceAll("'", `'\\''`)}'`;
|
|
1970
|
+
return `#!/bin/sh
|
|
1971
|
+
# MMI Hub maintenance launcher \u2014 generated by \`mmi-hub autoupdate on\`.
|
|
1972
|
+
# The exit code is propagated and reported by \`mmi-hub status\`.
|
|
1973
|
+
exec ${shQuote(nodeExe)} ${shQuote(hubDist)} update
|
|
1974
|
+
`;
|
|
1975
|
+
}
|
|
1921
1976
|
function taskXml(launcherPath, workDir, userId, startBoundary) {
|
|
1922
1977
|
return `<?xml version="1.0" encoding="UTF-16"?>
|
|
1923
1978
|
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
|
|
@@ -1960,7 +2015,53 @@ function taskXml(launcherPath, workDir, userId, startBoundary) {
|
|
|
1960
2015
|
</Task>
|
|
1961
2016
|
`;
|
|
1962
2017
|
}
|
|
1963
|
-
function
|
|
2018
|
+
function launchdPlist(launcherPath, workDir) {
|
|
2019
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
2020
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
2021
|
+
<plist version="1.0">
|
|
2022
|
+
<dict>
|
|
2023
|
+
<key>Label</key>
|
|
2024
|
+
<string>${LAUNCHD_LABEL}</string>
|
|
2025
|
+
<key>ProgramArguments</key>
|
|
2026
|
+
<array>
|
|
2027
|
+
<string>/bin/sh</string>
|
|
2028
|
+
<string>${xmlEscape(launcherPath)}</string>
|
|
2029
|
+
</array>
|
|
2030
|
+
<key>WorkingDirectory</key>
|
|
2031
|
+
<string>${xmlEscape(workDir)}</string>
|
|
2032
|
+
<key>StartInterval</key>
|
|
2033
|
+
<integer>${STARTINTERVAL_SECONDS}</integer>
|
|
2034
|
+
<key>RunAtLoad</key>
|
|
2035
|
+
<true/>
|
|
2036
|
+
</dict>
|
|
2037
|
+
</plist>
|
|
2038
|
+
`;
|
|
2039
|
+
}
|
|
2040
|
+
function systemdServiceUnit(launcherPath, workDir) {
|
|
2041
|
+
return `[Unit]
|
|
2042
|
+
Description=MMI Hub maintenance \u2014 converge this machine's MMI surfaces to the newest gated release
|
|
2043
|
+
|
|
2044
|
+
[Service]
|
|
2045
|
+
Type=oneshot
|
|
2046
|
+
WorkingDirectory=${workDir}
|
|
2047
|
+
ExecStart=/bin/sh ${launcherPath}
|
|
2048
|
+
TimeoutStartSec=${RUN_BOUND_SECONDS}
|
|
2049
|
+
`;
|
|
2050
|
+
}
|
|
2051
|
+
function systemdTimerUnit() {
|
|
2052
|
+
return `[Unit]
|
|
2053
|
+
Description=MMI Hub maintenance hourly reconcile timer
|
|
2054
|
+
|
|
2055
|
+
[Timer]
|
|
2056
|
+
OnUnitActiveSec=${STARTINTERVAL_SECONDS}
|
|
2057
|
+
Persistent=true
|
|
2058
|
+
AccuracySec=1min
|
|
2059
|
+
|
|
2060
|
+
[Install]
|
|
2061
|
+
WantedBy=timers.target
|
|
2062
|
+
`;
|
|
2063
|
+
}
|
|
2064
|
+
function readFileOrNull(path) {
|
|
1964
2065
|
try {
|
|
1965
2066
|
return (0, import_node_fs14.readFileSync)(path, "utf8");
|
|
1966
2067
|
} catch {
|
|
@@ -1983,6 +2084,7 @@ function elevatedRelaunch(mode, env) {
|
|
|
1983
2084
|
}
|
|
1984
2085
|
var NEVER_RUN_RESULT = 267011;
|
|
1985
2086
|
var NEVER_RUN_YEAR = 1999;
|
|
2087
|
+
var EMPTY_RUN_INFO = { lastRunTime: null, lastTaskResult: null, nextRunTime: null };
|
|
1986
2088
|
function taskRunInfo() {
|
|
1987
2089
|
const result = (0, import_node_child_process9.spawnSync)(
|
|
1988
2090
|
"powershell.exe",
|
|
@@ -2004,24 +2106,20 @@ function taskRunInfo() {
|
|
|
2004
2106
|
function neverRan(info) {
|
|
2005
2107
|
return info.lastTaskResult === NEVER_RUN_RESULT || (info.lastRunTime?.includes(String(NEVER_RUN_YEAR)) ?? false);
|
|
2006
2108
|
}
|
|
2007
|
-
function
|
|
2008
|
-
const empty = { lastRunTime: null, lastTaskResult: null, nextRunTime: null };
|
|
2009
|
-
if (process.platform !== "win32") {
|
|
2010
|
-
return { ok: false, supported: false, enabled: false, ...empty, detail: "automatic scheduling is available on Windows Task Scheduler only" };
|
|
2011
|
-
}
|
|
2109
|
+
function windowsSchedulerStatus(env) {
|
|
2012
2110
|
const query = schtasks(["/query", "/tn", TASK_NAME, "/xml"]);
|
|
2013
2111
|
if (query.status !== 0) {
|
|
2014
|
-
return { ok: false, supported: true, enabled: false, ...
|
|
2112
|
+
return { ok: false, supported: true, enabled: false, ...EMPTY_RUN_INFO, detail: `"${TASK_NAME}" is not registered \u2014 run \`mmi-hub autoupdate on\`` };
|
|
2015
2113
|
}
|
|
2016
2114
|
const hubDist = globalDist(env, "@mutmutco/hub", "dist/index.cjs");
|
|
2017
2115
|
const launcherPath = (0, import_node_path12.join)(statePaths(env).root, LAUNCHER_FILE);
|
|
2018
2116
|
const info = taskRunInfo();
|
|
2019
|
-
const runInfo = info ??
|
|
2117
|
+
const runInfo = info ?? EMPTY_RUN_INFO;
|
|
2020
2118
|
const fail = (detail) => ({ ok: false, supported: true, enabled: true, ...runInfo, detail });
|
|
2021
2119
|
if (!query.stdout.includes(launcherPath)) {
|
|
2022
2120
|
return fail(`"${TASK_NAME}" action does not run the hidden launcher ${launcherPath} \u2014 run \`mmi-hub autoupdate on\``);
|
|
2023
2121
|
}
|
|
2024
|
-
const launcher =
|
|
2122
|
+
const launcher = readFileOrNull(launcherPath);
|
|
2025
2123
|
if (hubDist && !launcher?.includes(hubDist)) {
|
|
2026
2124
|
return fail(`"${TASK_NAME}" launcher does not point at the current @mutmutco/hub dist ${hubDist} \u2014 run \`mmi-hub autoupdate on\``);
|
|
2027
2125
|
}
|
|
@@ -2038,9 +2136,74 @@ function schedulerStatus(env = process.env) {
|
|
|
2038
2136
|
const ran = !info ? "run history unreadable" : neverRan(info) ? "never run yet" : `last run ${info.lastRunTime} \u2192 exit ${info.lastTaskResult ?? "unknown"}`;
|
|
2039
2137
|
return { ok: true, supported: true, enabled: true, ...runInfo, detail: `"${TASK_NAME}" registered ${CADENCE}; ${points}; ${ran}; next ${runInfo.nextRunTime ?? "unscheduled"}` };
|
|
2040
2138
|
}
|
|
2041
|
-
function
|
|
2042
|
-
|
|
2043
|
-
const
|
|
2139
|
+
function darwinSchedulerStatus(env) {
|
|
2140
|
+
const plistPath = (0, import_node_path12.join)((0, import_node_os5.homedir)(), "Library", "LaunchAgents", LAUNCHD_PLIST_NAME);
|
|
2141
|
+
const plist = readFileOrNull(plistPath);
|
|
2142
|
+
if (!plist) {
|
|
2143
|
+
return { ok: false, supported: true, enabled: false, ...EMPTY_RUN_INFO, detail: `"${LAUNCHD_LABEL}" is not registered \u2014 run \`mmi-hub autoupdate on\`` };
|
|
2144
|
+
}
|
|
2145
|
+
const hubDist = globalDist(env, "@mutmutco/hub", "dist/index.cjs");
|
|
2146
|
+
const launcherPath = (0, import_node_path12.join)(statePaths(env).root, POSIX_LAUNCHER_FILE);
|
|
2147
|
+
const fail = (detail) => ({ ok: false, supported: true, enabled: true, ...EMPTY_RUN_INFO, detail });
|
|
2148
|
+
if (!plist.includes(launcherPath)) {
|
|
2149
|
+
return fail(`"${LAUNCHD_LABEL}" does not run the reconcile launcher ${launcherPath} \u2014 run \`mmi-hub autoupdate on\``);
|
|
2150
|
+
}
|
|
2151
|
+
const launcher = readFileOrNull(launcherPath);
|
|
2152
|
+
if (hubDist && !launcher?.includes(hubDist)) {
|
|
2153
|
+
return fail(`"${LAUNCHD_LABEL}" launcher does not point at the current @mutmutco/hub dist ${hubDist} \u2014 run \`mmi-hub autoupdate on\``);
|
|
2154
|
+
}
|
|
2155
|
+
if (!plist.includes(`<integer>${STARTINTERVAL_SECONDS}</integer>`)) {
|
|
2156
|
+
return fail(`"${LAUNCHD_LABEL}" has no ${STARTINTERVAL_SECONDS}s StartInterval \u2014 run \`mmi-hub autoupdate on\``);
|
|
2157
|
+
}
|
|
2158
|
+
const loaded = launchctl(["print", `gui/${uid()}/${LAUNCHD_LABEL}`]);
|
|
2159
|
+
if (loaded.status !== 0) {
|
|
2160
|
+
return fail(`"${LAUNCHD_LABEL}" is registered but not loaded \u2014 run \`mmi-hub autoupdate on\``);
|
|
2161
|
+
}
|
|
2162
|
+
const points = `points at ${hubDist ?? "the registered Hub dist"}`;
|
|
2163
|
+
return { ok: true, supported: true, enabled: true, ...EMPTY_RUN_INFO, detail: `"${LAUNCHD_LABEL}" registered ${POSIX_CADENCE}; ${points}` };
|
|
2164
|
+
}
|
|
2165
|
+
function linuxSchedulerStatus(env) {
|
|
2166
|
+
const unitDir = (0, import_node_path12.join)((0, import_node_os5.homedir)(), ".config", "systemd", "user");
|
|
2167
|
+
const timerPath = (0, import_node_path12.join)(unitDir, SYSTEMD_TIMER_NAME);
|
|
2168
|
+
const timer = readFileOrNull(timerPath);
|
|
2169
|
+
const service = readFileOrNull((0, import_node_path12.join)(unitDir, SYSTEMD_SERVICE_NAME));
|
|
2170
|
+
if (!timer || !service) {
|
|
2171
|
+
return { ok: false, supported: true, enabled: false, ...EMPTY_RUN_INFO, detail: `"${SYSTEMD_TIMER_NAME}" is not registered \u2014 run \`mmi-hub autoupdate on\`` };
|
|
2172
|
+
}
|
|
2173
|
+
const hubDist = globalDist(env, "@mutmutco/hub", "dist/index.cjs");
|
|
2174
|
+
const launcherPath = (0, import_node_path12.join)(statePaths(env).root, POSIX_LAUNCHER_FILE);
|
|
2175
|
+
const fail = (detail) => ({ ok: false, supported: true, enabled: true, ...EMPTY_RUN_INFO, detail });
|
|
2176
|
+
if (!service.includes(launcherPath)) {
|
|
2177
|
+
return fail(`"${SYSTEMD_SERVICE_NAME}" does not run the reconcile launcher ${launcherPath} \u2014 run \`mmi-hub autoupdate on\``);
|
|
2178
|
+
}
|
|
2179
|
+
const launcher = readFileOrNull(launcherPath);
|
|
2180
|
+
if (hubDist && !launcher?.includes(hubDist)) {
|
|
2181
|
+
return fail(`"${SYSTEMD_SERVICE_NAME}" launcher does not point at the current @mutmutco/hub dist ${hubDist} \u2014 run \`mmi-hub autoupdate on\``);
|
|
2182
|
+
}
|
|
2183
|
+
if (!timer.includes(`OnUnitActiveSec=${STARTINTERVAL_SECONDS}`)) {
|
|
2184
|
+
return fail(`"${SYSTEMD_TIMER_NAME}" has no ${STARTINTERVAL_SECONDS}s OnUnitActiveSec \u2014 run \`mmi-hub autoupdate on\``);
|
|
2185
|
+
}
|
|
2186
|
+
const enabled = systemctlUser(["is-enabled", SYSTEMD_TIMER_NAME]);
|
|
2187
|
+
if (enabled.status !== 0) {
|
|
2188
|
+
return fail(`"${SYSTEMD_TIMER_NAME}" is registered but not enabled \u2014 run \`mmi-hub autoupdate on\``);
|
|
2189
|
+
}
|
|
2190
|
+
const points = `points at ${hubDist ?? "the registered Hub dist"}`;
|
|
2191
|
+
return { ok: true, supported: true, enabled: true, ...EMPTY_RUN_INFO, detail: `"${SYSTEMD_TIMER_NAME}" registered ${POSIX_CADENCE}, ${RUN_BOUND_SECONDS}s bound; ${points}` };
|
|
2192
|
+
}
|
|
2193
|
+
function schedulerStatus(env = process.env) {
|
|
2194
|
+
if (process.platform === "win32") return windowsSchedulerStatus(env);
|
|
2195
|
+
if (process.platform === "darwin") return darwinSchedulerStatus(env);
|
|
2196
|
+
if (process.platform === "linux") return linuxSchedulerStatus(env);
|
|
2197
|
+
return {
|
|
2198
|
+
ok: false,
|
|
2199
|
+
supported: false,
|
|
2200
|
+
enabled: false,
|
|
2201
|
+
...EMPTY_RUN_INFO,
|
|
2202
|
+
detail: "automatic scheduling is available on Windows Task Scheduler, macOS launchd, and Linux systemd user timers only"
|
|
2203
|
+
};
|
|
2204
|
+
}
|
|
2205
|
+
function installWindowsTask(env) {
|
|
2206
|
+
const current = windowsSchedulerStatus(env);
|
|
2044
2207
|
if (current.ok) return { ok: true, detail: `already enabled \u2014 ${current.detail}` };
|
|
2045
2208
|
const hubDist = globalDist(env, "@mutmutco/hub", "dist/index.cjs");
|
|
2046
2209
|
if (!hubDist) return { ok: false, detail: "cannot resolve the global @mutmutco/hub dist \u2014 run `npm install -g @mutmutco/hub` first" };
|
|
@@ -2057,18 +2220,68 @@ function installTask(env = process.env) {
|
|
|
2057
2220
|
if (!denied || !elevatedRelaunch("on", env)) {
|
|
2058
2221
|
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()}` };
|
|
2059
2222
|
}
|
|
2060
|
-
const verify =
|
|
2223
|
+
const verify = windowsSchedulerStatus(env);
|
|
2061
2224
|
return verify.ok ? { ok: true, detail: `enabled "${TASK_NAME}" (elevated) \u2014 ${CADENCE}` } : { ok: false, detail: `elevated registration did not take: ${verify.detail}` };
|
|
2062
2225
|
}
|
|
2063
2226
|
return { ok: true, detail: `enabled "${TASK_NAME}" \u2014 ${CADENCE} \u2192 ${hubDist}` };
|
|
2064
2227
|
}
|
|
2065
|
-
function
|
|
2066
|
-
|
|
2228
|
+
function installLaunchd(env) {
|
|
2229
|
+
const current = darwinSchedulerStatus(env);
|
|
2230
|
+
if (current.ok) return { ok: true, detail: `already enabled \u2014 ${current.detail}` };
|
|
2231
|
+
const hubDist = globalDist(env, "@mutmutco/hub", "dist/index.cjs");
|
|
2232
|
+
if (!hubDist) return { ok: false, detail: "cannot resolve the global @mutmutco/hub dist \u2014 run `npm install -g @mutmutco/hub` first" };
|
|
2233
|
+
const paths = ensureState(env);
|
|
2234
|
+
const launcherPath = (0, import_node_path12.join)(paths.root, POSIX_LAUNCHER_FILE);
|
|
2235
|
+
(0, import_node_fs14.writeFileSync)(launcherPath, launcherSh(process.execPath, hubDist), "utf8");
|
|
2236
|
+
(0, import_node_fs14.chmodSync)(launcherPath, 493);
|
|
2237
|
+
const agentsDir = (0, import_node_path12.join)((0, import_node_os5.homedir)(), "Library", "LaunchAgents");
|
|
2238
|
+
(0, import_node_fs14.mkdirSync)(agentsDir, { recursive: true });
|
|
2239
|
+
const plistPath = (0, import_node_path12.join)(agentsDir, LAUNCHD_PLIST_NAME);
|
|
2240
|
+
(0, import_node_fs14.writeFileSync)(plistPath, launchdPlist(launcherPath, paths.root), "utf8");
|
|
2241
|
+
launchctl(["bootout", `gui/${uid()}/${LAUNCHD_LABEL}`]);
|
|
2242
|
+
const load = launchctl(["bootstrap", `gui/${uid()}`, plistPath]);
|
|
2243
|
+
if (load.status !== 0) {
|
|
2244
|
+
return { ok: false, detail: `launchctl bootstrap failed: ${(load.stderr || load.stdout).trim() || "unknown error"}` };
|
|
2245
|
+
}
|
|
2246
|
+
const verify = darwinSchedulerStatus(env);
|
|
2247
|
+
return verify.ok ? { ok: true, detail: `enabled "${LAUNCHD_LABEL}" \u2014 ${POSIX_CADENCE} \u2192 ${hubDist}` } : { ok: false, detail: `registration did not take: ${verify.detail}` };
|
|
2248
|
+
}
|
|
2249
|
+
function installSystemdTimer(env) {
|
|
2250
|
+
const current = linuxSchedulerStatus(env);
|
|
2251
|
+
if (current.ok) return { ok: true, detail: `already enabled \u2014 ${current.detail}` };
|
|
2252
|
+
const hubDist = globalDist(env, "@mutmutco/hub", "dist/index.cjs");
|
|
2253
|
+
if (!hubDist) return { ok: false, detail: "cannot resolve the global @mutmutco/hub dist \u2014 run `npm install -g @mutmutco/hub` first" };
|
|
2254
|
+
const paths = ensureState(env);
|
|
2255
|
+
const launcherPath = (0, import_node_path12.join)(paths.root, POSIX_LAUNCHER_FILE);
|
|
2256
|
+
(0, import_node_fs14.writeFileSync)(launcherPath, launcherSh(process.execPath, hubDist), "utf8");
|
|
2257
|
+
(0, import_node_fs14.chmodSync)(launcherPath, 493);
|
|
2258
|
+
const unitDir = (0, import_node_path12.join)((0, import_node_os5.homedir)(), ".config", "systemd", "user");
|
|
2259
|
+
(0, import_node_fs14.mkdirSync)(unitDir, { recursive: true });
|
|
2260
|
+
(0, import_node_fs14.writeFileSync)((0, import_node_path12.join)(unitDir, SYSTEMD_SERVICE_NAME), systemdServiceUnit(launcherPath, paths.root), "utf8");
|
|
2261
|
+
(0, import_node_fs14.writeFileSync)((0, import_node_path12.join)(unitDir, SYSTEMD_TIMER_NAME), systemdTimerUnit(), "utf8");
|
|
2262
|
+
const reload = systemctlUser(["daemon-reload"]);
|
|
2263
|
+
if (reload.status !== 0) {
|
|
2264
|
+
return { ok: false, detail: `systemctl --user daemon-reload failed: ${(reload.stderr || reload.stdout).trim() || "unknown error \u2014 is a user systemd instance running?"}` };
|
|
2265
|
+
}
|
|
2266
|
+
const enable = systemctlUser(["enable", "--now", SYSTEMD_TIMER_NAME]);
|
|
2267
|
+
if (enable.status !== 0) {
|
|
2268
|
+
return { ok: false, detail: `systemctl --user enable --now ${SYSTEMD_TIMER_NAME} failed: ${(enable.stderr || enable.stdout).trim() || "unknown error"}` };
|
|
2269
|
+
}
|
|
2270
|
+
const verify = linuxSchedulerStatus(env);
|
|
2271
|
+
return verify.ok ? { ok: true, detail: `enabled "${SYSTEMD_TIMER_NAME}" \u2014 ${POSIX_CADENCE} \u2192 ${hubDist}` } : { ok: false, detail: `registration did not take: ${verify.detail}` };
|
|
2272
|
+
}
|
|
2273
|
+
function installTask(env = process.env) {
|
|
2274
|
+
if (process.platform === "win32") return installWindowsTask(env);
|
|
2275
|
+
if (process.platform === "darwin") return installLaunchd(env);
|
|
2276
|
+
if (process.platform === "linux") return installSystemdTimer(env);
|
|
2277
|
+
return { ok: false, detail: "scheduler registration is available on Windows Task Scheduler, macOS launchd, and Linux systemd user timers only" };
|
|
2278
|
+
}
|
|
2279
|
+
function uninstallWindowsTask(env) {
|
|
2067
2280
|
const del = schtasks(["/delete", "/tn", TASK_NAME, "/f"]);
|
|
2068
2281
|
if (del.status !== 0) {
|
|
2069
2282
|
const query = schtasks(["/query", "/tn", TASK_NAME]);
|
|
2070
2283
|
if (query.status !== 0 && !/access is denied/i.test(del.stderr + del.stdout)) {
|
|
2071
|
-
dropLauncher(env);
|
|
2284
|
+
dropLauncher(env, LAUNCHER_FILE);
|
|
2072
2285
|
return { ok: true, detail: `automatic updates already off \u2014 "${TASK_NAME}" is not registered; installed tooling was kept` };
|
|
2073
2286
|
}
|
|
2074
2287
|
const denied = /access is denied/i.test(del.stderr + del.stdout);
|
|
@@ -2079,12 +2292,47 @@ function uninstallTask(env = process.env) {
|
|
|
2079
2292
|
return { ok: false, detail: `elevated deletion did not take: "${TASK_NAME}" is still registered` };
|
|
2080
2293
|
}
|
|
2081
2294
|
}
|
|
2082
|
-
dropLauncher(env);
|
|
2295
|
+
dropLauncher(env, LAUNCHER_FILE);
|
|
2083
2296
|
return { ok: true, detail: `automatic updates off \u2014 removed "${TASK_NAME}"; installed tooling was kept` };
|
|
2084
2297
|
}
|
|
2085
|
-
function
|
|
2298
|
+
function uninstallLaunchd(env) {
|
|
2299
|
+
const plistPath = (0, import_node_path12.join)((0, import_node_os5.homedir)(), "Library", "LaunchAgents", LAUNCHD_PLIST_NAME);
|
|
2300
|
+
const boot = launchctl(["bootout", `gui/${uid()}/${LAUNCHD_LABEL}`]);
|
|
2301
|
+
if (boot.status !== 0 && (0, import_node_fs14.existsSync)(plistPath)) {
|
|
2302
|
+
return { ok: false, detail: `launchctl bootout failed: ${(boot.stderr || boot.stdout).trim() || "unknown error"}` };
|
|
2303
|
+
}
|
|
2086
2304
|
try {
|
|
2087
|
-
(0, import_node_fs14.rmSync)(
|
|
2305
|
+
(0, import_node_fs14.rmSync)(plistPath, { force: true });
|
|
2306
|
+
} catch {
|
|
2307
|
+
}
|
|
2308
|
+
dropLauncher(env, POSIX_LAUNCHER_FILE);
|
|
2309
|
+
return { ok: true, detail: `automatic updates off \u2014 removed "${LAUNCHD_LABEL}"; installed tooling was kept` };
|
|
2310
|
+
}
|
|
2311
|
+
function uninstallSystemdTimer(env) {
|
|
2312
|
+
const unitDir = (0, import_node_path12.join)((0, import_node_os5.homedir)(), ".config", "systemd", "user");
|
|
2313
|
+
const timerPath = (0, import_node_path12.join)(unitDir, SYSTEMD_TIMER_NAME);
|
|
2314
|
+
const disable = systemctlUser(["disable", "--now", SYSTEMD_TIMER_NAME]);
|
|
2315
|
+
if (disable.status !== 0 && (0, import_node_fs14.existsSync)(timerPath)) {
|
|
2316
|
+
return { ok: false, detail: `systemctl --user disable --now ${SYSTEMD_TIMER_NAME} failed: ${(disable.stderr || disable.stdout).trim() || "unknown error"}` };
|
|
2317
|
+
}
|
|
2318
|
+
try {
|
|
2319
|
+
(0, import_node_fs14.rmSync)(timerPath, { force: true });
|
|
2320
|
+
(0, import_node_fs14.rmSync)((0, import_node_path12.join)(unitDir, SYSTEMD_SERVICE_NAME), { force: true });
|
|
2321
|
+
} catch {
|
|
2322
|
+
}
|
|
2323
|
+
systemctlUser(["daemon-reload"]);
|
|
2324
|
+
dropLauncher(env, POSIX_LAUNCHER_FILE);
|
|
2325
|
+
return { ok: true, detail: `automatic updates off \u2014 removed "${SYSTEMD_TIMER_NAME}"; installed tooling was kept` };
|
|
2326
|
+
}
|
|
2327
|
+
function uninstallTask(env = process.env) {
|
|
2328
|
+
if (process.platform === "win32") return uninstallWindowsTask(env);
|
|
2329
|
+
if (process.platform === "darwin") return uninstallLaunchd(env);
|
|
2330
|
+
if (process.platform === "linux") return uninstallSystemdTimer(env);
|
|
2331
|
+
return { ok: false, detail: "scheduler registration is available on Windows Task Scheduler, macOS launchd, and Linux systemd user timers only" };
|
|
2332
|
+
}
|
|
2333
|
+
function dropLauncher(env, file) {
|
|
2334
|
+
try {
|
|
2335
|
+
(0, import_node_fs14.rmSync)((0, import_node_path12.join)(statePaths(env).root, file), { force: true });
|
|
2088
2336
|
} catch {
|
|
2089
2337
|
}
|
|
2090
2338
|
}
|
|
@@ -2149,7 +2397,7 @@ function hubStatus(hubVersion, env = process.env, invokedPath = process.argv[1])
|
|
|
2149
2397
|
const failures = [];
|
|
2150
2398
|
if (error) failures.push(`${error} \u2014 inspect permissions for ${paths.journalPath}`);
|
|
2151
2399
|
if (!events.length && !error) failures.push("no maintenance journal yet \u2014 run `mmi-hub install`");
|
|
2152
|
-
if (!schedule.ok) failures.push(schedule.detail);
|
|
2400
|
+
if (!schedule.ok && schedule.supported) failures.push(schedule.detail);
|
|
2153
2401
|
for (const [surface, row] of Object.entries(installed)) {
|
|
2154
2402
|
if (!row.installed) failures.push(`${surface}: installed version is unreadable at ${row.location ?? "an unknown location"} \u2014 run \`mmi-hub update\``);
|
|
2155
2403
|
if (row.state === "behind") failures.push(`${surface}: installed ${row.installed} is behind expected ${row.expected} \u2014 run \`mmi-hub update\``);
|
|
@@ -2157,7 +2405,8 @@ function hubStatus(hubVersion, env = process.env, invokedPath = process.argv[1])
|
|
|
2157
2405
|
for (const event of latestRunEvents) {
|
|
2158
2406
|
if ((event.kind === "arm" || event.kind === "reconcile") && (event.verdict === "fail" || event.verdict === "defer")) {
|
|
2159
2407
|
const surface = event.surface === "updater" ? "hub" : event.surface ?? event.kind;
|
|
2160
|
-
|
|
2408
|
+
const repeat = event.kind === "arm" && event.verdict === "fail" && event.surface && journalSurfaceRepeatedFail(paths.journalPath, event.surface, latestEvent.run);
|
|
2409
|
+
failures.push(repeat ? `last run ${surface}: ${event.detail ?? event.verdict} \u2014 already failed the previous run too; \`mmi-hub update\` will not clear it, inspect the journal` : `last run ${surface}: ${event.detail ?? event.verdict} \u2014 run \`mmi-hub update\``);
|
|
2161
2410
|
}
|
|
2162
2411
|
}
|
|
2163
2412
|
return {
|
|
@@ -2218,9 +2467,10 @@ var RETRY_HINTS = [
|
|
|
2218
2467
|
[/elevat|access is denied/, "run from elevated terminal"]
|
|
2219
2468
|
];
|
|
2220
2469
|
var FAIL_CONSEQUENCE = [
|
|
2221
|
-
[/
|
|
2222
|
-
[/
|
|
2470
|
+
[/rollback failed|did not restore/, "rollback incomplete \u2014 run doctor now"],
|
|
2471
|
+
[/post-switch probe|post-update evidence|post-install evidence|post-switch marker|expected .*\/true/, "not verified \u2014 switch already applied, nothing was rolled back"]
|
|
2223
2472
|
];
|
|
2473
|
+
var RESTORE_EVIDENCE = /\brestored\b|\brolled back to\b/i;
|
|
2224
2474
|
function matchFirst(patterns, detail) {
|
|
2225
2475
|
const haystack = (detail ?? "").toLowerCase();
|
|
2226
2476
|
for (const [pattern, text] of patterns) if (pattern.test(haystack)) return text;
|
|
@@ -2229,13 +2479,17 @@ function matchFirst(patterns, detail) {
|
|
|
2229
2479
|
function retryHint(detail) {
|
|
2230
2480
|
return matchFirst(RETRY_HINTS, detail) ?? "retries next hourly run";
|
|
2231
2481
|
}
|
|
2232
|
-
function failConsequence(
|
|
2482
|
+
function failConsequence(row) {
|
|
2483
|
+
const detail = row.detail ?? "";
|
|
2484
|
+
if (RESTORE_EVIDENCE.test(detail)) {
|
|
2485
|
+
return `not verified \u2014 restored previous version ${row.from ?? "unknown"}, safe to use`;
|
|
2486
|
+
}
|
|
2233
2487
|
return matchFirst(FAIL_CONSEQUENCE, detail) ?? "update failed \u2014 previous version kept";
|
|
2234
2488
|
}
|
|
2235
2489
|
function classify(arm) {
|
|
2236
2490
|
const kind = arm.verdict === "ok" ? arm.from !== null && arm.from === arm.to ? "current" : "updated" : arm.verdict === "skip" ? "keep" : arm.verdict === "defer" ? "retry" : "failed";
|
|
2237
2491
|
const note = kind === "updated" ? ACTIVATION_NOTE[arm.surface] ?? null : kind === "keep" ? "above target" : kind === "retry" ? retryHint(arm.detail) : null;
|
|
2238
|
-
return { name: arm.surface === "updater" ? "hub" : arm.surface, from: arm.from, to: arm.to, kind, note, detail: arm.detail };
|
|
2492
|
+
return { name: arm.surface === "updater" ? "hub" : arm.surface, from: arm.from, to: arm.to, kind, note, detail: arm.detail, repeatFailure: arm.repeatFailure === true };
|
|
2239
2493
|
}
|
|
2240
2494
|
function statusText(kind, dryRun) {
|
|
2241
2495
|
if (dryRun && kind === "updated") return "would update";
|
|
@@ -2309,7 +2563,7 @@ function collectRows(summary) {
|
|
|
2309
2563
|
for (const arm of summary.plugins) rows.push(classify(arm));
|
|
2310
2564
|
for (const surface of summary.surfaces) {
|
|
2311
2565
|
if (surface.present && surface.action === "arm-pending") {
|
|
2312
|
-
rows.push({ name: surface.id, from: null, to: summary.target ?? "?", kind: "pending", note: "arm not landed", detail: "" });
|
|
2566
|
+
rows.push({ name: surface.id, from: null, to: summary.target ?? "?", kind: "pending", note: "arm not landed", detail: "", repeatFailure: false });
|
|
2313
2567
|
}
|
|
2314
2568
|
}
|
|
2315
2569
|
if (summary.updater) rows.push(classify(summary.updater));
|
|
@@ -2360,8 +2614,8 @@ function reportFooterLines(summary, options = {}) {
|
|
|
2360
2614
|
out.push("");
|
|
2361
2615
|
for (const row of rows.filter((row2) => row2.kind === "failed")) {
|
|
2362
2616
|
const mark = tty ? paint("failed", "\u2717 ") : "";
|
|
2363
|
-
out.push(...noteLines(" " + mark + row.name, failConsequence(row
|
|
2364
|
-
out.push(...noteLines(" fix: ",
|
|
2617
|
+
out.push(...noteLines(" " + mark + row.name, failConsequence(row), width));
|
|
2618
|
+
out.push(...noteLines(" fix: ", row.repeatFailure ? "doctor + update already tried and did not clear this \u2014 see log, do not repeat" : "mmi-cli doctor, then mmi-hub update", width));
|
|
2365
2619
|
out.push(...noteLines(" log: ", journalPath, width));
|
|
2366
2620
|
}
|
|
2367
2621
|
}
|
|
@@ -2596,6 +2850,8 @@ if (typeof require !== "undefined" && require.main === module) {
|
|
|
2596
2850
|
hermesArm,
|
|
2597
2851
|
hermesHome,
|
|
2598
2852
|
hubStatus,
|
|
2853
|
+
launchdPlist,
|
|
2854
|
+
launcherSh,
|
|
2599
2855
|
launcherVbs,
|
|
2600
2856
|
localStartBoundary,
|
|
2601
2857
|
main,
|
|
@@ -2606,6 +2862,8 @@ if (typeof require !== "undefined" && require.main === module) {
|
|
|
2606
2862
|
retryHint,
|
|
2607
2863
|
schedulerStatus,
|
|
2608
2864
|
startSpinner,
|
|
2865
|
+
systemdServiceUnit,
|
|
2866
|
+
systemdTimerUnit,
|
|
2609
2867
|
taskXml,
|
|
2610
2868
|
updateHeader,
|
|
2611
2869
|
wrapWords
|
package/package.json
CHANGED