@meetless/mla 0.1.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/LICENSE +201 -0
- package/README.md +81 -0
- package/dist/build-info.json +9 -0
- package/dist/bundles/ask-core.js +396 -0
- package/dist/bundles/mcp.js +16592 -0
- package/dist/bundles/trace-core.js +263 -0
- package/dist/cli.js +828 -0
- package/dist/commands/activate.js +781 -0
- package/dist/commands/adoption.js +130 -0
- package/dist/commands/ask.js +290 -0
- package/dist/commands/context.js +114 -0
- package/dist/commands/debug.js +313 -0
- package/dist/commands/doctor.js +1021 -0
- package/dist/commands/enrich.js +427 -0
- package/dist/commands/evidence.js +229 -0
- package/dist/commands/flush.js +184 -0
- package/dist/commands/graph.js +104 -0
- package/dist/commands/init.js +272 -0
- package/dist/commands/internal-active-review.js +322 -0
- package/dist/commands/internal-auto-index.js +188 -0
- package/dist/commands/internal-capture-decisions.js +320 -0
- package/dist/commands/internal-evidence-correlate.js +239 -0
- package/dist/commands/internal-evidence-hooks.js +240 -0
- package/dist/commands/internal-evidence-inject.js +231 -0
- package/dist/commands/internal-finalize.js +221 -0
- package/dist/commands/internal-pretool-observe.js +225 -0
- package/dist/commands/internal-refresh.js +136 -0
- package/dist/commands/internal-session-nudge.js +120 -0
- package/dist/commands/internal-steer-sync.js +117 -0
- package/dist/commands/internal-turn-recap.js +140 -0
- package/dist/commands/kb.js +375 -0
- package/dist/commands/kb_add.js +681 -0
- package/dist/commands/kb_forget.js +283 -0
- package/dist/commands/kb_move.js +45 -0
- package/dist/commands/kb_pending.js +410 -0
- package/dist/commands/kb_personal.js +149 -0
- package/dist/commands/kb_promote.js +188 -0
- package/dist/commands/kb_purge.js +168 -0
- package/dist/commands/kb_reingest.js +335 -0
- package/dist/commands/kb_retime.js +170 -0
- package/dist/commands/kb_review.js +391 -0
- package/dist/commands/kb_revision.js +179 -0
- package/dist/commands/kb_show.js +385 -0
- package/dist/commands/label.js +226 -0
- package/dist/commands/login.js +295 -0
- package/dist/commands/logout.js +108 -0
- package/dist/commands/mcp-supervisor.js +93 -0
- package/dist/commands/mcp.js +227 -0
- package/dist/commands/queue-prune.js +98 -0
- package/dist/commands/review.js +358 -0
- package/dist/commands/rewire.js +124 -0
- package/dist/commands/rules.js +728 -0
- package/dist/commands/scan-context.js +67 -0
- package/dist/commands/session.js +347 -0
- package/dist/commands/stats.js +479 -0
- package/dist/commands/status.js +61 -0
- package/dist/commands/summary.js +250 -0
- package/dist/commands/turn.js +114 -0
- package/dist/commands/uninstall.js +222 -0
- package/dist/commands/whoami.js +102 -0
- package/dist/commands/workspace.js +130 -0
- package/dist/hooks-template/ce0-post-tool-use.sh +34 -0
- package/dist/hooks-template/ce0-session-start.sh +49 -0
- package/dist/hooks-template/ce0-stop.sh +29 -0
- package/dist/hooks-template/ce0-user-prompt-submit.sh +38 -0
- package/dist/hooks-template/common.sh +934 -0
- package/dist/hooks-template/event-batch-filter.jq +67 -0
- package/dist/hooks-template/flush.sh +503 -0
- package/dist/hooks-template/post-tool-use.sh +423 -0
- package/dist/hooks-template/pre-tool-use.sh +69 -0
- package/dist/hooks-template/session-start.sh +140 -0
- package/dist/hooks-template/stop.sh +308 -0
- package/dist/hooks-template/user-prompt-submit.sh +1162 -0
- package/dist/lib/activation.js +79 -0
- package/dist/lib/active-conflict-cache.js +141 -0
- package/dist/lib/active-memory.js +59 -0
- package/dist/lib/active-review-runner.js +26 -0
- package/dist/lib/agent-decision/index.js +25 -0
- package/dist/lib/agent-decision/keys.js +49 -0
- package/dist/lib/agent-decision/normalize-claude.js +183 -0
- package/dist/lib/agent-decision/types.js +21 -0
- package/dist/lib/agent-decision/validate.js +216 -0
- package/dist/lib/analytics/capture.js +96 -0
- package/dist/lib/analytics/command-event.js +267 -0
- package/dist/lib/analytics/consent.js +58 -0
- package/dist/lib/analytics/coverage-gap.js +96 -0
- package/dist/lib/analytics/envelope.js +236 -0
- package/dist/lib/analytics/event-id.js +86 -0
- package/dist/lib/analytics/evidence.js +150 -0
- package/dist/lib/analytics/followthrough.js +194 -0
- package/dist/lib/analytics/forwarder.js +109 -0
- package/dist/lib/analytics/logs.js +78 -0
- package/dist/lib/analytics/metrics.js +78 -0
- package/dist/lib/analytics/recorder.js +92 -0
- package/dist/lib/analytics/review-analytics.js +75 -0
- package/dist/lib/analytics/sequence.js +77 -0
- package/dist/lib/analytics/store.js +131 -0
- package/dist/lib/analytics/turn-recap.js +279 -0
- package/dist/lib/artifact_id.js +108 -0
- package/dist/lib/auth-breaker.js +161 -0
- package/dist/lib/auto-index.js +112 -0
- package/dist/lib/classifier.js +88 -0
- package/dist/lib/config.js +298 -0
- package/dist/lib/conflict-advisory.js +64 -0
- package/dist/lib/debug-bundle.js +520 -0
- package/dist/lib/enrichment/ingest.js +301 -0
- package/dist/lib/enrichment/plan.js +253 -0
- package/dist/lib/enrichment/protocol.js +359 -0
- package/dist/lib/enrichment/scout-brief.js +176 -0
- package/dist/lib/failure-telemetry.js +444 -0
- package/dist/lib/git.js +200 -0
- package/dist/lib/governance-cache.js +77 -0
- package/dist/lib/governed-path-cache.js +76 -0
- package/dist/lib/http.js +677 -0
- package/dist/lib/identity-envelope.js +23 -0
- package/dist/lib/kb-candidate.js +65 -0
- package/dist/lib/kb_acl.js +98 -0
- package/dist/lib/login.js +353 -0
- package/dist/lib/mcp-fetchers.js +130 -0
- package/dist/lib/mcp-restart.js +47 -0
- package/dist/lib/observability.js +805 -0
- package/dist/lib/open-url.js +33 -0
- package/dist/lib/orphan-guard.js +70 -0
- package/dist/lib/packaged.js +21 -0
- package/dist/lib/reconcile-sessions.js +171 -0
- package/dist/lib/redactor.js +89 -0
- package/dist/lib/relationship-candidate-query.js +27 -0
- package/dist/lib/render.js +611 -0
- package/dist/lib/rules/applicability.js +64 -0
- package/dist/lib/rules/attest-code-rule-version.js +47 -0
- package/dist/lib/rules/attest-notes-location.js +217 -0
- package/dist/lib/rules/attest-rule-version.js +69 -0
- package/dist/lib/rules/canonical-json.js +97 -0
- package/dist/lib/rules/ce0-emit.js +64 -0
- package/dist/lib/rules/ce0-evidence.js +281 -0
- package/dist/lib/rules/ce0-recall-sample.js +82 -0
- package/dist/lib/rules/ce0-rule.js +55 -0
- package/dist/lib/rules/ce0-sampling-bucket.js +15 -0
- package/dist/lib/rules/ce0-store.js +683 -0
- package/dist/lib/rules/ce0-telemetry-project.js +93 -0
- package/dist/lib/rules/ce0-telemetry.js +158 -0
- package/dist/lib/rules/code-rule-registry.js +17 -0
- package/dist/lib/rules/command-match.js +185 -0
- package/dist/lib/rules/consult-evidence-binding.js +27 -0
- package/dist/lib/rules/consultation-capture-adapter.js +193 -0
- package/dist/lib/rules/content-match.js +56 -0
- package/dist/lib/rules/deny-admission.js +99 -0
- package/dist/lib/rules/durable-observation.js +190 -0
- package/dist/lib/rules/enforce-notes-version.js +421 -0
- package/dist/lib/rules/evaluation-input-hash.js +126 -0
- package/dist/lib/rules/evaluator.js +108 -0
- package/dist/lib/rules/inert-rule-families.js +51 -0
- package/dist/lib/rules/input-authority-resolver.js +241 -0
- package/dist/lib/rules/interception-schema.js +170 -0
- package/dist/lib/rules/interception-store.js +267 -0
- package/dist/lib/rules/live-input-authority.js +66 -0
- package/dist/lib/rules/local-matcher.js +108 -0
- package/dist/lib/rules/local-observe.js +79 -0
- package/dist/lib/rules/local-rule-version-repo.js +214 -0
- package/dist/lib/rules/memory-requirement.js +109 -0
- package/dist/lib/rules/notes-observe.js +39 -0
- package/dist/lib/rules/notes-path.js +261 -0
- package/dist/lib/rules/notes-rule.js +75 -0
- package/dist/lib/rules/observe-adapter.js +114 -0
- package/dist/lib/rules/observed-rule-hash.js +119 -0
- package/dist/lib/rules/prompt-submit-adapter.js +132 -0
- package/dist/lib/rules/requirement-subject.js +240 -0
- package/dist/lib/rules/rule-activity.js +67 -0
- package/dist/lib/rules/rule-version-hash.js +151 -0
- package/dist/lib/rules/runtime-scope.js +55 -0
- package/dist/lib/rules/stop-adapter.js +116 -0
- package/dist/lib/rules/stop-response-snapshot.js +174 -0
- package/dist/lib/rules/types.js +10 -0
- package/dist/lib/rules/ulid.js +46 -0
- package/dist/lib/rules/version-evaluation.js +156 -0
- package/dist/lib/scanner/agent-memory.js +99 -0
- package/dist/lib/scanner/bootstrap-summary.js +87 -0
- package/dist/lib/scanner/cache.js +59 -0
- package/dist/lib/scanner/frontmatter.js +42 -0
- package/dist/lib/scanner/parse-directives.js +69 -0
- package/dist/lib/scanner/parse-structured.js +72 -0
- package/dist/lib/scanner/render.js +73 -0
- package/dist/lib/scanner/scan.js +132 -0
- package/dist/lib/scanner/score.js +38 -0
- package/dist/lib/scanner/scout-mission.js +126 -0
- package/dist/lib/scanner/types.js +7 -0
- package/dist/lib/session-scope.js +195 -0
- package/dist/lib/spool.js +355 -0
- package/dist/lib/staleness.js +100 -0
- package/dist/lib/steer-cache.js +87 -0
- package/dist/lib/tagged-reference.js +20 -0
- package/dist/lib/temporal.js +109 -0
- package/dist/lib/turn-recap-emit.js +67 -0
- package/dist/lib/unwire.js +253 -0
- package/dist/lib/update-check.js +469 -0
- package/dist/lib/update-notifier.js +217 -0
- package/dist/lib/upgrade-apply.js +643 -0
- package/dist/lib/wire.js +1087 -0
- package/dist/lib/workspace.js +96 -0
- package/dist/lib/zip.js +154 -0
- package/dist/pretool-entry.js +37 -0
- package/package.json +75 -0
|
@@ -0,0 +1,643 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.REEXEC_GUARD_ENV = void 0;
|
|
37
|
+
exports.stateFilePath = stateFilePath;
|
|
38
|
+
exports.readUpdateState = readUpdateState;
|
|
39
|
+
exports.writeUpdateState = writeUpdateState;
|
|
40
|
+
exports.liveBinaryPath = liveBinaryPath;
|
|
41
|
+
exports.prevBinaryPath = prevBinaryPath;
|
|
42
|
+
exports.stagedDir = stagedDir;
|
|
43
|
+
exports.stagedBinaryPath = stagedBinaryPath;
|
|
44
|
+
exports.trustedManifestKeys = trustedManifestKeys;
|
|
45
|
+
exports.resolveManifestUrl = resolveManifestUrl;
|
|
46
|
+
exports.fetchManifest = fetchManifest;
|
|
47
|
+
exports.sha256File = sha256File;
|
|
48
|
+
exports.downloadVerifyExtract = downloadVerifyExtract;
|
|
49
|
+
exports.cleanupDir = cleanupDir;
|
|
50
|
+
exports.atomicSwapBinary = atomicSwapBinary;
|
|
51
|
+
exports.rollbackBinary = rollbackBinary;
|
|
52
|
+
exports.withUpgradeLock = withUpgradeLock;
|
|
53
|
+
exports.stageBinary = stageBinary;
|
|
54
|
+
exports.clearStaged = clearStaged;
|
|
55
|
+
exports.reExecAfterUpgrade = reExecAfterUpgrade;
|
|
56
|
+
exports.maybePromoteStagedAndReExec = maybePromoteStagedAndReExec;
|
|
57
|
+
exports.parseUpgradeArgs = parseUpgradeArgs;
|
|
58
|
+
exports.runUpgrade = runUpgrade;
|
|
59
|
+
// IO layer for the self-upgrade mechanism (proposal 20260615-mla-version-
|
|
60
|
+
// detection-and-upgrade). The pure decision logic lives in update-check.ts; this
|
|
61
|
+
// file does every side effect the upgrade path needs:
|
|
62
|
+
//
|
|
63
|
+
// - cache IO (read/write the update-check.json state file)
|
|
64
|
+
// - resolve the trusted manifest key(s) and the manifest URL
|
|
65
|
+
// - fetch + Ed25519-verify the signed manifest
|
|
66
|
+
// - download an artifact, verify its sha256, extract the `mla` binary
|
|
67
|
+
// - atomically swap the live binary (keeping one mla.prev for rollback, D4)
|
|
68
|
+
// - take a single-writer lock so two upgrades never race
|
|
69
|
+
// - stage a verified binary in the background and promote it on launch (D3)
|
|
70
|
+
// - re-exec the freshly-promoted binary with a loop guard
|
|
71
|
+
// - the `mla upgrade` command handler
|
|
72
|
+
//
|
|
73
|
+
// Every entry point is defensive: an upgrade failure must NEVER brick the CLI.
|
|
74
|
+
// On any error the live binary is left untouched (or rolled back) and the caller
|
|
75
|
+
// continues running the version it already had. Self-replace happens ONLY for
|
|
76
|
+
// the curl install method; brew/npm/unknown are redirected to their package
|
|
77
|
+
// manager (renaming over a managed path corrupts that manager's metadata).
|
|
78
|
+
const child_process_1 = require("child_process");
|
|
79
|
+
const crypto = __importStar(require("crypto"));
|
|
80
|
+
const fs = __importStar(require("fs"));
|
|
81
|
+
const os = __importStar(require("os"));
|
|
82
|
+
const path = __importStar(require("path"));
|
|
83
|
+
const config_1 = require("./config");
|
|
84
|
+
const observability_1 = require("./observability");
|
|
85
|
+
const update_check_1 = require("./update-check");
|
|
86
|
+
// --- cache IO (moved here from update-notifier so the upgrade path and the nag
|
|
87
|
+
// share one reader/writer with no import cycle) -------------------------
|
|
88
|
+
function stateFilePath() {
|
|
89
|
+
return path.join(config_1.HOME, update_check_1.UPDATE_STATE_FILE);
|
|
90
|
+
}
|
|
91
|
+
function readUpdateState() {
|
|
92
|
+
try {
|
|
93
|
+
return (0, update_check_1.parseState)(fs.readFileSync(stateFilePath(), "utf8"));
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return (0, update_check_1.parseState)(null);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function writeUpdateState(state) {
|
|
100
|
+
try {
|
|
101
|
+
fs.mkdirSync(config_1.HOME, { recursive: true });
|
|
102
|
+
fs.writeFileSync(stateFilePath(), (0, update_check_1.serializeState)(state), "utf8");
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
// best-effort; a read-only HOME just means we re-check next time.
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// --- filesystem layout -------------------------------------------------------
|
|
109
|
+
// The curl install puts the binary here (install.sh: $HOME/.meetless/bin/mla),
|
|
110
|
+
// as a plain file (no symlink), so rename(2) over this path atomically swaps the
|
|
111
|
+
// inode. HOME honors MEETLESS_HOME, so a throwaway home makes eval hermetic.
|
|
112
|
+
function liveBinaryPath() {
|
|
113
|
+
return path.join(config_1.HOME, "bin", "mla");
|
|
114
|
+
}
|
|
115
|
+
// The single rollback slot (D4: keep exactly one previous binary).
|
|
116
|
+
function prevBinaryPath() {
|
|
117
|
+
return path.join(config_1.HOME, "bin", "mla.prev");
|
|
118
|
+
}
|
|
119
|
+
// Where the background stager parks a verified, ready-to-promote binary so the
|
|
120
|
+
// hot path (apply-on-launch) only does a cheap local rename, never a network call.
|
|
121
|
+
function stagedDir() {
|
|
122
|
+
return path.join(config_1.HOME, "staged");
|
|
123
|
+
}
|
|
124
|
+
function stagedBinaryPath() {
|
|
125
|
+
return path.join(stagedDir(), "mla");
|
|
126
|
+
}
|
|
127
|
+
function lockPath() {
|
|
128
|
+
return path.join(config_1.HOME, "upgrade.lock");
|
|
129
|
+
}
|
|
130
|
+
// --- trust + url resolution (dev-gated overrides) ----------------------------
|
|
131
|
+
// A baked key may be a raw SPKI PEM or a single-line base64-of-PEM (env-friendly
|
|
132
|
+
// for CI). Normalize either into PEM text; return "" if it is neither.
|
|
133
|
+
function normalizeKeyPem(raw) {
|
|
134
|
+
const v = (raw || "").trim();
|
|
135
|
+
if (!v)
|
|
136
|
+
return "";
|
|
137
|
+
if (v.includes("-----BEGIN"))
|
|
138
|
+
return v;
|
|
139
|
+
// Try base64-of-PEM. A bad decode that does not yield a PEM is rejected.
|
|
140
|
+
try {
|
|
141
|
+
const decoded = Buffer.from(v, "base64").toString("utf8");
|
|
142
|
+
if (decoded.includes("-----BEGIN"))
|
|
143
|
+
return decoded;
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
// fall through
|
|
147
|
+
}
|
|
148
|
+
return "";
|
|
149
|
+
}
|
|
150
|
+
// The trusted Ed25519 public key(s) the manifest signature is checked against.
|
|
151
|
+
// Production trusts ONLY the key baked into the binary (buildInfo.updatePublicKey).
|
|
152
|
+
// On a dev build (buildInfo.dirty) the MLA_UPDATE_PUBLIC_KEY env override is also
|
|
153
|
+
// honored, so local eval can sign with a throwaway key. This mirrors the
|
|
154
|
+
// sentryDsn dev-gating in observability.ts: never let a runtime env var weaken a
|
|
155
|
+
// release binary's trust root.
|
|
156
|
+
function trustedManifestKeys(opts) {
|
|
157
|
+
const { env, buildInfo } = opts;
|
|
158
|
+
const keys = [];
|
|
159
|
+
const baked = normalizeKeyPem(buildInfo.updatePublicKey);
|
|
160
|
+
if (baked)
|
|
161
|
+
keys.push(baked);
|
|
162
|
+
if (buildInfo.dirty) {
|
|
163
|
+
const override = normalizeKeyPem(env.MLA_UPDATE_PUBLIC_KEY);
|
|
164
|
+
if (override)
|
|
165
|
+
keys.push(override);
|
|
166
|
+
}
|
|
167
|
+
return keys;
|
|
168
|
+
}
|
|
169
|
+
// The manifest URL. Defaults to the public bucket; a dev build honors the
|
|
170
|
+
// MLA_UPDATE_MANIFEST_URL override so eval can point at a 127.0.0.1 server.
|
|
171
|
+
function resolveManifestUrl(opts) {
|
|
172
|
+
const { env, buildInfo } = opts;
|
|
173
|
+
if (buildInfo.dirty && env.MLA_UPDATE_MANIFEST_URL) {
|
|
174
|
+
return env.MLA_UPDATE_MANIFEST_URL;
|
|
175
|
+
}
|
|
176
|
+
return update_check_1.DEFAULT_MANIFEST_URL;
|
|
177
|
+
}
|
|
178
|
+
// --- manifest fetch + verify -------------------------------------------------
|
|
179
|
+
async function fetchBytes(url, timeoutMs) {
|
|
180
|
+
const controller = new AbortController();
|
|
181
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
182
|
+
try {
|
|
183
|
+
const res = await fetch(url, {
|
|
184
|
+
headers: { "User-Agent": "mla-upgrade" },
|
|
185
|
+
signal: controller.signal,
|
|
186
|
+
});
|
|
187
|
+
if (!res.ok)
|
|
188
|
+
return null;
|
|
189
|
+
return Buffer.from(await res.arrayBuffer());
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
finally {
|
|
195
|
+
clearTimeout(timer);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
// Fetch manifest.json + manifest.json.sig, verify the detached Ed25519 signature
|
|
199
|
+
// over the EXACT manifest bytes against the trust list, then parse+validate.
|
|
200
|
+
// Returns null on ANY failure (offline, http error, bad signature, malformed
|
|
201
|
+
// manifest) so the caller treats it as "no update", never acting on unverified
|
|
202
|
+
// or untrusted bytes. The signature is checked over the raw bytes BEFORE parsing.
|
|
203
|
+
async function fetchManifest(opts) {
|
|
204
|
+
const env = opts.env ?? process.env;
|
|
205
|
+
const buildInfo = opts.buildInfo ?? (0, observability_1.loadBuildInfo)();
|
|
206
|
+
const timeoutMs = opts.timeoutMs ?? 5000;
|
|
207
|
+
const url = resolveManifestUrl({ env, buildInfo });
|
|
208
|
+
const sigUrl = url.endsWith(".json") ? url.slice(0, -5) + ".json.sig" : url + ".sig";
|
|
209
|
+
const [bytes, sigBytes] = await Promise.all([
|
|
210
|
+
fetchBytes(url, timeoutMs),
|
|
211
|
+
fetchBytes(sigUrl, timeoutMs),
|
|
212
|
+
]);
|
|
213
|
+
if (!bytes || !sigBytes)
|
|
214
|
+
return null;
|
|
215
|
+
const keys = trustedManifestKeys({ env, buildInfo });
|
|
216
|
+
if (keys.length === 0)
|
|
217
|
+
return null; // no trust root: refuse rather than trust blindly
|
|
218
|
+
const signatureB64 = sigBytes.toString("utf8").trim();
|
|
219
|
+
if (!(0, update_check_1.verifyManifestSignature)(bytes, signatureB64, keys))
|
|
220
|
+
return null;
|
|
221
|
+
const manifest = (0, update_check_1.parseManifest)(bytes.toString("utf8"));
|
|
222
|
+
if (!manifest)
|
|
223
|
+
return null;
|
|
224
|
+
return { manifest, bytes };
|
|
225
|
+
}
|
|
226
|
+
// --- artifact download + verify + extract ------------------------------------
|
|
227
|
+
function sha256File(filePath) {
|
|
228
|
+
const hash = crypto.createHash("sha256");
|
|
229
|
+
hash.update(fs.readFileSync(filePath));
|
|
230
|
+
return hash.digest("hex");
|
|
231
|
+
}
|
|
232
|
+
async function downloadVerifyExtract(opts) {
|
|
233
|
+
const { artifact } = opts;
|
|
234
|
+
const timeoutMs = opts.timeoutMs ?? 60000;
|
|
235
|
+
let dir = null;
|
|
236
|
+
try {
|
|
237
|
+
dir = fs.mkdtempSync(path.join(os.tmpdir(), "mla-upgrade-"));
|
|
238
|
+
const archive = path.join(dir, "pkg.tar.gz");
|
|
239
|
+
const bytes = await fetchBytes(artifact.url, timeoutMs);
|
|
240
|
+
if (!bytes) {
|
|
241
|
+
cleanupDir(dir);
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
fs.writeFileSync(archive, bytes);
|
|
245
|
+
const got = sha256File(archive);
|
|
246
|
+
if (got !== artifact.sha256) {
|
|
247
|
+
cleanupDir(dir);
|
|
248
|
+
return null; // tampered or truncated download: refuse
|
|
249
|
+
}
|
|
250
|
+
// tar is present on macOS and Linux; -xzf handles the gzip. Extract into the
|
|
251
|
+
// temp dir and require the binary at the archive root (release-layout invariant).
|
|
252
|
+
(0, child_process_1.execFileSync)("tar", ["-xzf", archive, "-C", dir], { stdio: "ignore" });
|
|
253
|
+
const binaryPath = path.join(dir, "mla");
|
|
254
|
+
if (!fs.existsSync(binaryPath)) {
|
|
255
|
+
cleanupDir(dir);
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
fs.chmodSync(binaryPath, 0o755);
|
|
259
|
+
return { binaryPath, dir };
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
if (dir)
|
|
263
|
+
cleanupDir(dir);
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
function cleanupDir(dir) {
|
|
268
|
+
try {
|
|
269
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
270
|
+
}
|
|
271
|
+
catch {
|
|
272
|
+
// best-effort temp cleanup
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
// --- atomic swap + rollback --------------------------------------------------
|
|
276
|
+
// Atomically replace the live binary with `newBinaryPath`:
|
|
277
|
+
// 1. copy the new bytes to mla.new.<pid> IN THE SAME DIR (so rename is on one
|
|
278
|
+
// filesystem and therefore atomic), fsync, chmod 0755.
|
|
279
|
+
// 2. copy the current live binary to mla.prev (the single rollback slot, D4).
|
|
280
|
+
// 3. rename(2) mla.new.<pid> over the live path. The running process keeps its
|
|
281
|
+
// open inode, so a swap mid-run is safe; the NEXT exec gets the new bytes.
|
|
282
|
+
// Returns true on success. Throws nothing the caller cannot recover from: on a
|
|
283
|
+
// mid-swap failure the live path is either the old binary (rename not reached)
|
|
284
|
+
// or the new one (rename done); mla.prev always holds the prior bytes.
|
|
285
|
+
function atomicSwapBinary(opts) {
|
|
286
|
+
const live = opts.live ?? liveBinaryPath();
|
|
287
|
+
const prev = opts.prev ?? prevBinaryPath();
|
|
288
|
+
const dir = path.dirname(live);
|
|
289
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
290
|
+
const tmp = path.join(dir, `mla.new.${process.pid}`);
|
|
291
|
+
// 1. stage the new bytes next to the live path, durably.
|
|
292
|
+
const data = fs.readFileSync(opts.newBinaryPath);
|
|
293
|
+
const fd = fs.openSync(tmp, "w", 0o755);
|
|
294
|
+
try {
|
|
295
|
+
fs.writeSync(fd, data);
|
|
296
|
+
fs.fsyncSync(fd);
|
|
297
|
+
}
|
|
298
|
+
finally {
|
|
299
|
+
fs.closeSync(fd);
|
|
300
|
+
}
|
|
301
|
+
fs.chmodSync(tmp, 0o755);
|
|
302
|
+
// 2. snapshot the current live binary for rollback (if one exists).
|
|
303
|
+
try {
|
|
304
|
+
if (fs.existsSync(live))
|
|
305
|
+
fs.copyFileSync(live, prev);
|
|
306
|
+
}
|
|
307
|
+
catch {
|
|
308
|
+
// a missing/locked prev slot is not fatal; we still swap.
|
|
309
|
+
}
|
|
310
|
+
// 3. atomic replace.
|
|
311
|
+
fs.renameSync(tmp, live);
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
314
|
+
// Restore the previous binary from the rollback slot over the live path. Used
|
|
315
|
+
// when a freshly-swapped binary fails its post-swap smoke (so a bad release
|
|
316
|
+
// never leaves the user stranded). Returns true if a rollback was performed.
|
|
317
|
+
function rollbackBinary(opts) {
|
|
318
|
+
const live = opts?.live ?? liveBinaryPath();
|
|
319
|
+
const prev = opts?.prev ?? prevBinaryPath();
|
|
320
|
+
try {
|
|
321
|
+
if (!fs.existsSync(prev))
|
|
322
|
+
return false;
|
|
323
|
+
fs.renameSync(prev, live);
|
|
324
|
+
return true;
|
|
325
|
+
}
|
|
326
|
+
catch {
|
|
327
|
+
return false;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
// --- single-writer lock ------------------------------------------------------
|
|
331
|
+
const LOCK_STALE_MS = 5 * 60 * 1000; // a lock older than this is presumed abandoned
|
|
332
|
+
// Run `fn` while holding an exclusive upgrade lock. Uses O_EXCL create as the
|
|
333
|
+
// atomic primitive. If the lock exists but is stale (older than LOCK_STALE_MS),
|
|
334
|
+
// it is stolen once. If a live lock is held by another process, returns
|
|
335
|
+
// { ran: false } WITHOUT running fn (the other upgrade wins; we never double-swap).
|
|
336
|
+
async function withUpgradeLock(fn) {
|
|
337
|
+
const lock = lockPath();
|
|
338
|
+
fs.mkdirSync(path.dirname(lock), { recursive: true });
|
|
339
|
+
const tryAcquire = () => {
|
|
340
|
+
try {
|
|
341
|
+
return fs.openSync(lock, "wx"); // O_CREAT | O_EXCL | O_WRONLY
|
|
342
|
+
}
|
|
343
|
+
catch (e) {
|
|
344
|
+
if (e.code === "EEXIST")
|
|
345
|
+
return null;
|
|
346
|
+
throw e;
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
let fd = tryAcquire();
|
|
350
|
+
if (fd === null) {
|
|
351
|
+
// Steal a stale lock exactly once.
|
|
352
|
+
try {
|
|
353
|
+
const st = fs.statSync(lock);
|
|
354
|
+
if (Date.now() - st.mtimeMs > LOCK_STALE_MS) {
|
|
355
|
+
fs.rmSync(lock, { force: true });
|
|
356
|
+
fd = tryAcquire();
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
catch {
|
|
360
|
+
// race on stat/unlink: fall through to "held"
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
if (fd === null)
|
|
364
|
+
return { ran: false };
|
|
365
|
+
try {
|
|
366
|
+
fs.writeSync(fd, String(process.pid));
|
|
367
|
+
fs.closeSync(fd);
|
|
368
|
+
}
|
|
369
|
+
catch {
|
|
370
|
+
// closing failure is non-fatal; we still own the file and will unlink it.
|
|
371
|
+
}
|
|
372
|
+
try {
|
|
373
|
+
const value = await fn();
|
|
374
|
+
return { ran: true, value };
|
|
375
|
+
}
|
|
376
|
+
finally {
|
|
377
|
+
fs.rmSync(lock, { force: true });
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
// --- stage / promote / clear -------------------------------------------------
|
|
381
|
+
// Copy a verified binary into the staged dir and record a staged pointer in the
|
|
382
|
+
// cache. The recorded sha256 is the BINARY's sha (re-verified at promote time),
|
|
383
|
+
// not the archive sha (which was already verified during download). Replaces any
|
|
384
|
+
// existing staged binary (D4: one at a time). Returns the staged pointer.
|
|
385
|
+
function stageBinary(opts) {
|
|
386
|
+
const dir = stagedDir();
|
|
387
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
388
|
+
const dest = stagedBinaryPath();
|
|
389
|
+
fs.copyFileSync(opts.binaryPath, dest);
|
|
390
|
+
fs.chmodSync(dest, 0o755);
|
|
391
|
+
const staged = {
|
|
392
|
+
version: opts.version,
|
|
393
|
+
triple: opts.triple,
|
|
394
|
+
sha256: sha256File(dest),
|
|
395
|
+
path: dest,
|
|
396
|
+
stagedAt: opts.now,
|
|
397
|
+
};
|
|
398
|
+
const prev = readUpdateState();
|
|
399
|
+
writeUpdateState({ ...prev, staged });
|
|
400
|
+
return staged;
|
|
401
|
+
}
|
|
402
|
+
// Drop the staged binary and its cache pointer (after a promote, or when stale).
|
|
403
|
+
function clearStaged() {
|
|
404
|
+
try {
|
|
405
|
+
fs.rmSync(stagedDir(), { recursive: true, force: true });
|
|
406
|
+
}
|
|
407
|
+
catch {
|
|
408
|
+
// best-effort
|
|
409
|
+
}
|
|
410
|
+
const prev = readUpdateState();
|
|
411
|
+
if (prev.staged)
|
|
412
|
+
writeUpdateState({ ...prev, staged: null });
|
|
413
|
+
}
|
|
414
|
+
// --- re-exec -----------------------------------------------------------------
|
|
415
|
+
exports.REEXEC_GUARD_ENV = "MLA_UPGRADE_REEXECED";
|
|
416
|
+
// A defined, harmless value for PKG_EXECPATH in the re-exec'd child env. See the
|
|
417
|
+
// long comment in reExecAfterUpgrade: it must be non-empty (so pkg's patched
|
|
418
|
+
// spawnSync leaves it alone), must NOT equal the child binary's path, and must
|
|
419
|
+
// NOT be the magic "PKG_INVOKE_NODEJS" string. Any other constant forces the
|
|
420
|
+
// child's pkg bootstrap down the normal app-launch branch.
|
|
421
|
+
const PKG_REEXEC_SENTINEL = "MLA_REEXEC";
|
|
422
|
+
// Re-run the same command with the freshly-promoted binary. Sets the loop-guard
|
|
423
|
+
// env var so the child skips apply-on-launch (otherwise an apply that did not
|
|
424
|
+
// actually change the version could re-exec forever). Returns the child's exit
|
|
425
|
+
// code; the caller should process.exit with it. Used ONLY after a successful
|
|
426
|
+
// apply-on-launch swap, so `live` is the new binary.
|
|
427
|
+
function reExecAfterUpgrade(opts) {
|
|
428
|
+
const live = opts.live ?? liveBinaryPath();
|
|
429
|
+
// For a pkg binary process.argv is [binary, snapshotEntry, ...userArgs], so the
|
|
430
|
+
// real user args start at slice(2) (the same slice the main entry parses). We
|
|
431
|
+
// spawn `live` directly; pkg re-injects its own snapshot entry as argv[1] in the
|
|
432
|
+
// child, so passing slice(1) here would leak that path through as the command.
|
|
433
|
+
const args = opts.argv ?? process.argv.slice(2);
|
|
434
|
+
// pkg env interaction (proven empirically against a real pkg binary):
|
|
435
|
+
// pkg patches child_process.spawnSync inside a packaged process. When the
|
|
436
|
+
// spawn env has no PKG_EXECPATH key, the patch INJECTS PKG_EXECPATH = the
|
|
437
|
+
// PARENT's process.execPath. Apply-on-launch re-execs the SAME path (the live
|
|
438
|
+
// binary, post-swap), so the child's process.execPath equals that injected
|
|
439
|
+
// value. pkg's prelude/bootstrap then takes its "run as node script" branch
|
|
440
|
+
// and path.resolve()s our first user arg (e.g. "whoami") as a script file,
|
|
441
|
+
// crashing with "Cannot find module .../whoami". Deleting the key does NOT
|
|
442
|
+
// help (the patch re-injects on undefined). Setting it to a defined sentinel
|
|
443
|
+
// that is neither the binary path nor "PKG_INVOKE_NODEJS" makes the patch
|
|
444
|
+
// leave it alone and the child boots normally into the app. Mirrors the
|
|
445
|
+
// CLAUDECODE env-inheritance lesson: neutralize the inherited launcher var.
|
|
446
|
+
const env = {
|
|
447
|
+
...(opts.env ?? process.env),
|
|
448
|
+
[exports.REEXEC_GUARD_ENV]: "1",
|
|
449
|
+
PKG_EXECPATH: PKG_REEXEC_SENTINEL,
|
|
450
|
+
};
|
|
451
|
+
const res = (0, child_process_1.spawnSync)(live, args, { stdio: "inherit", env });
|
|
452
|
+
if (typeof res.status === "number")
|
|
453
|
+
return res.status;
|
|
454
|
+
return 1; // killed by signal or failed to spawn: non-zero
|
|
455
|
+
}
|
|
456
|
+
const NO_PROMOTE = { reExeced: false };
|
|
457
|
+
// The apply-on-launch hook, run at the very top of CLI bootstrap. If a verified
|
|
458
|
+
// staged binary is present, auto-apply is enabled, and this is a curl install,
|
|
459
|
+
// it promotes the staged binary with a cheap local swap and re-execs the command
|
|
460
|
+
// with the new binary. Returns { reExeced: true, code } when the caller should
|
|
461
|
+
// exit with `code`; otherwise { reExeced: false } and the caller continues
|
|
462
|
+
// normally on the current binary. NEVER throws: any failure falls open to the
|
|
463
|
+
// running binary (and rolls back a half-applied swap).
|
|
464
|
+
async function maybePromoteStagedAndReExec(opts) {
|
|
465
|
+
try {
|
|
466
|
+
const env = opts.env ?? process.env;
|
|
467
|
+
const buildInfo = opts.buildInfo ?? (0, observability_1.loadBuildInfo)();
|
|
468
|
+
// Loop guard: the re-exec'd child must never re-promote.
|
|
469
|
+
if (env[exports.REEXEC_GUARD_ENV])
|
|
470
|
+
return NO_PROMOTE;
|
|
471
|
+
// Never promote under the detached check child, an explicit upgrade, or the
|
|
472
|
+
// long-lived `mla mcp` server. `mcp` is carved out because it is spawned by
|
|
473
|
+
// an editor as a persistent stdio daemon: a launch-time re-exec would fork an
|
|
474
|
+
// extra process layer under the supervisor (one more thing to leak) and the
|
|
475
|
+
// server has its OWN in-band stale-dist self-heal (the worker exits with the
|
|
476
|
+
// restart sentinel and the supervisor respawns on the fresh dist, no re-exec
|
|
477
|
+
// needed). See notes/20260622-mla-mcp-process-leak-findings-and-fix.md (Tier
|
|
478
|
+
// 1 Phase 3).
|
|
479
|
+
if (opts.command === "_internal" ||
|
|
480
|
+
opts.command === "upgrade" ||
|
|
481
|
+
opts.command === "mcp") {
|
|
482
|
+
return NO_PROMOTE;
|
|
483
|
+
}
|
|
484
|
+
// Total kill switch / auto-apply opt-out.
|
|
485
|
+
if ((0, update_check_1.upgradeKillSwitch)(env))
|
|
486
|
+
return NO_PROMOTE;
|
|
487
|
+
if (!(0, update_check_1.resolveAutoApply)({ env, configAutoApply: (0, config_1.readUpdateConfig)().autoApply })) {
|
|
488
|
+
return NO_PROMOTE;
|
|
489
|
+
}
|
|
490
|
+
// Only curl installs self-replace; brew/npm/unknown are package-manager owned.
|
|
491
|
+
const method = (0, update_check_1.detectInstallMethod)({
|
|
492
|
+
execPath: process.execPath,
|
|
493
|
+
scriptPath: process.argv[1],
|
|
494
|
+
env,
|
|
495
|
+
});
|
|
496
|
+
if (method !== "curl")
|
|
497
|
+
return NO_PROMOTE;
|
|
498
|
+
const state = readUpdateState();
|
|
499
|
+
const staged = state.staged;
|
|
500
|
+
if (!staged)
|
|
501
|
+
return NO_PROMOTE;
|
|
502
|
+
// Validate the staged pointer against this machine + the running version.
|
|
503
|
+
const triple = (0, update_check_1.currentTriple)(process.platform, process.arch);
|
|
504
|
+
if (!triple || staged.triple !== triple) {
|
|
505
|
+
clearStaged();
|
|
506
|
+
return NO_PROMOTE;
|
|
507
|
+
}
|
|
508
|
+
// Re-verify the staged file's bytes before trusting them (defends against a
|
|
509
|
+
// corrupted or partially-written staged file).
|
|
510
|
+
if (!fs.existsSync(staged.path) || sha256File(staged.path) !== staged.sha256) {
|
|
511
|
+
clearStaged();
|
|
512
|
+
return NO_PROMOTE;
|
|
513
|
+
}
|
|
514
|
+
const locked = await withUpgradeLock(async () => {
|
|
515
|
+
atomicSwapBinary({ newBinaryPath: staged.path });
|
|
516
|
+
return true;
|
|
517
|
+
});
|
|
518
|
+
if (!locked.ran)
|
|
519
|
+
return NO_PROMOTE; // another process is mid-upgrade
|
|
520
|
+
clearStaged();
|
|
521
|
+
const code = reExecAfterUpgrade({ env });
|
|
522
|
+
return { reExeced: true, code };
|
|
523
|
+
}
|
|
524
|
+
catch {
|
|
525
|
+
// An apply-on-launch failure must never break the command. Try a rollback
|
|
526
|
+
// (best-effort) and fall through to running the current binary.
|
|
527
|
+
try {
|
|
528
|
+
rollbackBinary();
|
|
529
|
+
}
|
|
530
|
+
catch {
|
|
531
|
+
// ignore
|
|
532
|
+
}
|
|
533
|
+
return NO_PROMOTE;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
function parseUpgradeArgs(argv) {
|
|
537
|
+
return {
|
|
538
|
+
force: argv.includes("--force") || argv.includes("-f"),
|
|
539
|
+
check: argv.includes("--check") || argv.includes("-n"),
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
// `mla upgrade` handler. Returns the process exit code. Explicit, foreground,
|
|
543
|
+
// and chatty (unlike the silent background path). Honors the kill switch,
|
|
544
|
+
// redirects managed installs to their package manager, enforces the downgrade
|
|
545
|
+
// guard via planUpgrade, and on a real upgrade does the verified download +
|
|
546
|
+
// atomic swap under the lock. Does NOT re-exec: the user is not mid-command, so
|
|
547
|
+
// the new binary simply takes effect on the next run.
|
|
548
|
+
async function runUpgrade(opts) {
|
|
549
|
+
const env = opts.env ?? process.env;
|
|
550
|
+
const buildInfo = opts.buildInfo ?? (0, observability_1.loadBuildInfo)();
|
|
551
|
+
const log = opts.log ?? ((l) => process.stderr.write(l + "\n"));
|
|
552
|
+
const args = parseUpgradeArgs(opts.argv);
|
|
553
|
+
const current = buildInfo.version;
|
|
554
|
+
if ((0, update_check_1.upgradeKillSwitch)(env)) {
|
|
555
|
+
log("Self-upgrade is disabled (MLA_DISABLE_UPGRADE is set).");
|
|
556
|
+
return 1;
|
|
557
|
+
}
|
|
558
|
+
// Managed installs: redirect, never self-replace.
|
|
559
|
+
const method = (0, update_check_1.detectInstallMethod)({
|
|
560
|
+
execPath: process.execPath,
|
|
561
|
+
scriptPath: process.argv[1],
|
|
562
|
+
env,
|
|
563
|
+
});
|
|
564
|
+
if (method !== "curl") {
|
|
565
|
+
log(method === "unknown"
|
|
566
|
+
? "Could not determine how mla was installed."
|
|
567
|
+
: `mla was installed via ${method}.`);
|
|
568
|
+
log(`To upgrade, run: ${(0, update_check_1.upgradeCommandFor)(method)}`);
|
|
569
|
+
return 0;
|
|
570
|
+
}
|
|
571
|
+
const verified = await fetchManifest({ env, buildInfo });
|
|
572
|
+
if (!verified) {
|
|
573
|
+
log("Could not fetch or verify the release manifest. Try again later.");
|
|
574
|
+
return 1;
|
|
575
|
+
}
|
|
576
|
+
const manifest = verified.manifest;
|
|
577
|
+
const triple = (0, update_check_1.currentTriple)(process.platform, process.arch);
|
|
578
|
+
const plan = (0, update_check_1.planUpgrade)({ current, manifest, triple, force: args.force });
|
|
579
|
+
switch (plan.action) {
|
|
580
|
+
case "up-to-date":
|
|
581
|
+
log(`mla is up to date (${current}).`);
|
|
582
|
+
return 0;
|
|
583
|
+
case "no-artifact":
|
|
584
|
+
log(`No release artifact published for this platform (${triple ?? "unsupported"}).`);
|
|
585
|
+
return 1;
|
|
586
|
+
case "downgrade-blocked":
|
|
587
|
+
log(`The published version (${plan.to}) is older than the installed one (${current}). ` +
|
|
588
|
+
"Pass --force to install it anyway.");
|
|
589
|
+
return 1;
|
|
590
|
+
case "unparseable-current":
|
|
591
|
+
log(`Running a dev build (${current}); refusing to overwrite it. ` +
|
|
592
|
+
`Pass --force to install ${plan.to}.`);
|
|
593
|
+
return 1;
|
|
594
|
+
case "upgrade":
|
|
595
|
+
break;
|
|
596
|
+
}
|
|
597
|
+
if (args.check) {
|
|
598
|
+
log(`Update available: ${current} -> ${plan.to}. Run \`mla upgrade\` to install it.`);
|
|
599
|
+
return 0;
|
|
600
|
+
}
|
|
601
|
+
const artifact = (0, update_check_1.selectArtifact)(manifest, triple);
|
|
602
|
+
if (!artifact) {
|
|
603
|
+
log(`No release artifact published for this platform (${triple ?? "unsupported"}).`);
|
|
604
|
+
return 1;
|
|
605
|
+
}
|
|
606
|
+
log(`Downloading mla ${plan.to} for ${triple}...`);
|
|
607
|
+
const extracted = await downloadVerifyExtract({ artifact });
|
|
608
|
+
if (!extracted) {
|
|
609
|
+
log("Download or verification failed. Your current mla is unchanged.");
|
|
610
|
+
return 1;
|
|
611
|
+
}
|
|
612
|
+
const result = await withUpgradeLock(async () => {
|
|
613
|
+
try {
|
|
614
|
+
atomicSwapBinary({ newBinaryPath: extracted.binaryPath });
|
|
615
|
+
return true;
|
|
616
|
+
}
|
|
617
|
+
catch {
|
|
618
|
+
rollbackBinary();
|
|
619
|
+
return false;
|
|
620
|
+
}
|
|
621
|
+
});
|
|
622
|
+
cleanupDir(extracted.dir);
|
|
623
|
+
if (!result.ran) {
|
|
624
|
+
log("Another upgrade is already in progress. Try again in a moment.");
|
|
625
|
+
return 1;
|
|
626
|
+
}
|
|
627
|
+
if (!result.value) {
|
|
628
|
+
log("Failed to replace the binary; rolled back to the previous version.");
|
|
629
|
+
return 1;
|
|
630
|
+
}
|
|
631
|
+
// A manual upgrade supersedes any background-staged binary.
|
|
632
|
+
clearStaged();
|
|
633
|
+
// Stamp the cache so the nag does not immediately fire for a version we just installed.
|
|
634
|
+
const prevState = readUpdateState();
|
|
635
|
+
writeUpdateState({
|
|
636
|
+
...prevState,
|
|
637
|
+
latestVersion: manifest.version,
|
|
638
|
+
minVersion: manifest.minVersion,
|
|
639
|
+
staged: null,
|
|
640
|
+
});
|
|
641
|
+
log(`Upgraded mla ${current} -> ${plan.to}. The new version takes effect on your next command.`);
|
|
642
|
+
return 0;
|
|
643
|
+
}
|