mason-context 0.18.0 → 0.19.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.
package/dist/mason.js CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ if (!import.meta.url.endsWith("/mason-runtime.js")) await import("./mason-runtime.js").then(m => m.registerRuntime());
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
4
5
  var __esm = (fn, res) => function __init() {
@@ -1987,6 +1988,10 @@ var init_release_metadata = __esm({
1987
1988
  });
1988
1989
 
1989
1990
  // src/audit/checks/deps-changed.ts
1991
+ function hasDependencyContent(content2) {
1992
+ const text2 = content2.replace(/<!-- mason:start -->[\s\S]*?<!-- mason:end -->/g, "").replace(/<!--[\s\S]*?-->/g, "");
1993
+ return /\b(?:dependenc(?:y|ies)|librar(?:y|ies)|frameworks?|tech(?:nology)? stack|prerequisites|requirements)\b/i.test(text2) || /\b(?:package\.json|build\.gradle(?:\.kts)?|settings\.gradle(?:\.kts)?|libs\.versions\.toml|Cargo\.toml|go\.mod|pyproject\.toml|requirements\.txt|Gemfile|composer\.json)\b/i.test(text2) || /\b(?:node(?:\.js)?|npm|pnpm|yarn|bun|deno|python|ruby|rust|go|java|jdk|kotlin|gradle|swift|php|react|vue|angular|compose|ktor|room)\s*(?:version\s*)?[`*:=>~^v\s-]*\d+(?:\.\d+)*\b/i.test(text2) || /\b(?:npm|pnpm|yarn|pip3?|cargo|gem|composer)\s+(?:install|add|require)\b/i.test(text2);
1994
+ }
1990
1995
  function manifestPathspecs(doc) {
1991
1996
  const scope = documentScope(doc);
1992
1997
  if (scope === ".") return MANIFEST_PATHSPECS;
@@ -2013,6 +2018,19 @@ async function checkDepsChanged(ctx) {
2013
2018
  reason: `${doc.path} has uncommitted edits \u2013 suppressed while in flight`
2014
2019
  });
2015
2020
  }
2021
+ let content2 = doc.content;
2022
+ if (doc.dirty) {
2023
+ try {
2024
+ content2 = (await inspectionGit(
2025
+ ["show", `${doc.lastCommit.hash}:${doc.path}`],
2026
+ { cwd: ctx.root, maxBuffer: 10 * 1024 * 1024, timeout: 1e4 }
2027
+ )).stdout;
2028
+ } catch {
2029
+ result.skipped.push({ check: "deps-changed", doc: doc.path, reason: `${doc.path}: committed dependency content is unavailable` });
2030
+ continue;
2031
+ }
2032
+ }
2033
+ if (!hasDependencyContent(content2)) continue;
2016
2034
  const range = await commitsTouchingSince(
2017
2035
  ctx.root,
2018
2036
  doc.lastCommit.hash,
@@ -2058,6 +2076,7 @@ var init_deps_changed = __esm({
2058
2076
  init_docs();
2059
2077
  init_release_metadata();
2060
2078
  init_git();
2079
+ init_inspection();
2061
2080
  init_checks();
2062
2081
  MANIFEST_COMMITS_CAP = 10;
2063
2082
  MANIFEST_PATHSPECS = [
@@ -2953,7 +2972,7 @@ async function verifyOriginal(rootDir, baselinePath, options, session) {
2953
2972
  return {
2954
2973
  ...base,
2955
2974
  status: "review-required",
2956
- reason: "An audit cannot establish that this advisory was reviewed. Retain its original evidence and report a separate assessment; editing or committing the doc is not approval."
2975
+ reason: (now ? "The current check still reports this advisory. " : finding.type === "deps-changed" ? "The current check no longer reports this condition; the historical advisory remains awaiting assessment. " : "The current check no longer reports the original finding; retained evidence still requires assessment. ") + "An audit cannot establish that this advisory was reviewed. Retain its original evidence and report a separate assessment; editing or committing the doc is not approval."
2957
2976
  };
2958
2977
  }
2959
2978
  return { ...base, status: "resolved", reason: "The original check ran and no longer reports this claim. Inspect the edit for semantic correctness." };
@@ -3131,7 +3150,7 @@ async function collectInputs(root) {
3131
3150
  git3(root, "status", "--porcelain=v1", "-z", "--untracked-files=all", "--", ".", ":(exclude).mason/reports"),
3132
3151
  git3(root, "ls-files", "--stage", "-z", "--", ".", ":(exclude).mason/reports")
3133
3152
  ]) : ["", ""];
3134
- const common = [5, engineVersion, head, shallow, replacements, docContents];
3153
+ const common = [6, engineVersion, head, shallow, replacements, docContents];
3135
3154
  const keys = {
3136
3155
  "deleted-reference": hash([common, claims, docStatus]),
3137
3156
  "new-module": hash([common, modules]),
@@ -3191,7 +3210,7 @@ var init_evidence = __esm({
3191
3210
  init_dead_command();
3192
3211
  init_storage();
3193
3212
  init_advisory_review();
3194
- engineVersion = true ? "0.18.0" : "development";
3213
+ engineVersion = true ? "0.19.0" : "development";
3195
3214
  hash = (value) => createHash3("sha256").update(JSON.stringify(value)).digest("hex");
3196
3215
  content = readAuditInput;
3197
3216
  cacheSchema = z8.object({ version: z8.literal(1), entries: z8.record(z8.object({ key: z8.string(), result: checkResultSchema })), digest: z8.string() });
@@ -3405,13 +3424,17 @@ async function observeReadOnlyTool(dir, event) {
3405
3424
  }
3406
3425
  function summarize(report) {
3407
3426
  const open = report.findings.filter((f) => f.status !== "resolved");
3427
+ const onlyReview = report.status === "incomplete" && !report.diagnostics.length && !report.counts.unresolved && !report.counts.unverified && report.counts["review-required"] > 0;
3408
3428
  return [
3409
- `Mason: ${report.status}; ${report.counts.unresolved} unresolved, ${report.counts["review-required"]} need review, ${report.counts.unverified} unverified.`,
3410
- ...open.slice(0, 4).map((f) => `[${f.status}] ${cleanText(f.original.anchor.doc)}: ${cleanText(f.original.message)}`),
3429
+ onlyReview ? `Mason: ${report.counts["review-required"]} advisor${report.counts["review-required"] === 1 ? "y" : "ies"} awaiting review; no unresolved issues.` : `Mason: ${report.status}; ${report.counts.unresolved} unresolved, ${report.counts["review-required"]} need review, ${report.counts.unverified} unverified.`,
3430
+ ...open.slice(0, 4).map((f) => {
3431
+ const detail = f.current ? f.current.message : f.status === "review-required" ? `${f.original.type}: current check no longer reports ${f.original.type === "deps-changed" ? "this condition" : "the original finding"}; historical evidence awaits assessment.` : `Original evidence: ${f.original.message}. ${f.reason}`;
3432
+ return `[${f.status}] ${cleanText(f.original.anchor.doc)}: ${cleanText(detail)}`;
3433
+ }),
3411
3434
  ...open.length > 4 ? [`${open.length - 4} more findings in the report.`] : [],
3412
3435
  ...report.diagnostics.slice(0, 2).map(cleanText),
3413
3436
  `Evidence: ${report.reportPath}. Resume/check with mason_automation(action: "check") or mason-auto check.`,
3414
- "Keep original evidence. Address findings relevant to the authorized task; report unrelated findings and unresolved advisories without approving them."
3437
+ "Keep original evidence. Address findings relevant to the authorized task; report unrelated findings with a brief scope reason and suggested follow-up. Use review_advisory for authorized assessments; otherwise leave advisories open without approving them."
3415
3438
  ].join("\n");
3416
3439
  }
3417
3440
  async function loadState(ws) {
@@ -3635,7 +3658,7 @@ async function automate(dir, event) {
3635
3658
  if (event.event === "after_tool" && event.mutating) {
3636
3659
  session.mutationObserved = true;
3637
3660
  if (!event.toolId || !session.pending[event.toolId]) {
3638
- const gap = "A tool completed without an observed matching pre-tool capture; pre-edit coverage is unknown.";
3661
+ const gap = "A tool completed without an observed matching pre-tool capture in this checkout; pre-edit coverage is unknown. The tool may have changed checkouts; captures are kept separately for each worktree.";
3639
3662
  if (!session.coverageGaps.includes(gap)) session.coverageGaps.push(gap);
3640
3663
  }
3641
3664
  if (event.toolId) delete session.pending[event.toolId];
@@ -3778,11 +3801,39 @@ var init_hook_root = __esm({
3778
3801
 
3779
3802
  // src/setup/model.ts
3780
3803
  import { z as z11 } from "zod";
3804
+ import fs16 from "fs/promises";
3805
+ import path23 from "path";
3781
3806
  async function loadSetup(root) {
3782
3807
  const raw = await readStoreJson(root, SETUP_PATH);
3783
3808
  if (raw === null) return null;
3784
3809
  return setupSchema.parse(raw);
3785
3810
  }
3811
+ async function effectiveSetup(root) {
3812
+ const local = await loadSetup(root);
3813
+ if (local) return { root, setup: local };
3814
+ const common = await fs16.realpath(path23.resolve(root, (await git3(root, "rev-parse", "--git-common-dir")).trim()));
3815
+ const own = await fs16.realpath((await git3(root, "rev-parse", "--absolute-git-dir")).trim());
3816
+ if (common === own) return { root, setup: null };
3817
+ const primary = (await git3(root, "worktree", "list", "--porcelain", "-z")).split("\0\0")[0].split("\0");
3818
+ const candidate = primary.find((field) => field.startsWith("worktree "))?.slice(9);
3819
+ if (!candidate || primary.includes("bare")) return { root, setup: null };
3820
+ let inheritedRoot;
3821
+ try {
3822
+ inheritedRoot = await fs16.realpath((await git3(candidate, "rev-parse", "--show-toplevel")).trim());
3823
+ } catch (error) {
3824
+ if (/must be run in a work tree/.test(String(error.stderr))) return { root, setup: null };
3825
+ throw error;
3826
+ }
3827
+ const inheritedCommon = await fs16.realpath(path23.resolve(
3828
+ inheritedRoot,
3829
+ (await git3(inheritedRoot, "rev-parse", "--git-common-dir")).trim()
3830
+ ));
3831
+ if (inheritedCommon !== common) throw new Error("Linked worktree setup does not belong to the same Git repository.");
3832
+ const setup = await loadSetup(inheritedRoot);
3833
+ const inactive = await readStoreJson(root, INACTIVE_HOSTS_PATH);
3834
+ if (inactive && setup) for (const host of inactiveHostsSchema.parse(inactive).hosts) delete setup.hosts[host];
3835
+ return { root: inheritedRoot, setup };
3836
+ }
3786
3837
  async function loadSetupReceipt(root, directory, host) {
3787
3838
  const raw = await readStoreJson(root, directory + "/setup-" + host + ".json");
3788
3839
  if (raw === null) return null;
@@ -3790,10 +3841,11 @@ async function loadSetupReceipt(root, directory, host) {
3790
3841
  if (receipt.root !== root || receipt.host !== host) throw new Error("Setup receipt belongs to another installation.");
3791
3842
  return receipt;
3792
3843
  }
3793
- var setupHostSchema, setupSchema, SETUP_PATH, receiptSchema;
3844
+ var setupHostSchema, setupSchema, SETUP_PATH, INACTIVE_HOSTS_PATH, inactiveHostsSchema, receiptSchema;
3794
3845
  var init_model = __esm({
3795
3846
  "src/setup/model.ts"() {
3796
3847
  "use strict";
3848
+ init_evidence();
3797
3849
  init_storage();
3798
3850
  setupHostSchema = z11.object({
3799
3851
  configuredVersion: z11.string(),
@@ -3807,6 +3859,8 @@ var init_model = __esm({
3807
3859
  claude: setupHostSchema.optional()
3808
3860
  }) });
3809
3861
  SETUP_PATH = ".mason/local/setup.json";
3862
+ INACTIVE_HOSTS_PATH = ".mason/local/inactive-hosts.json";
3863
+ inactiveHostsSchema = z11.object({ version: z11.literal(1), hosts: z11.array(z11.enum(["claude", "codex"])) });
3810
3864
  receiptSchema = z11.object({
3811
3865
  version: z11.literal(1),
3812
3866
  host: z11.enum(["codex", "claude"]),
@@ -3833,7 +3887,7 @@ __export(launcher_exports, {
3833
3887
  });
3834
3888
  import { execFile as execFile2 } from "child_process";
3835
3889
  import { promisify as promisify2 } from "util";
3836
- import fs16 from "fs/promises";
3890
+ import fs17 from "fs/promises";
3837
3891
  function globalCommand(args2, platform = process.platform) {
3838
3892
  return platform === "win32" ? { command: "cmd.exe", args: ["/d", "/s", "/c", "mason", ...args2] } : { command: "mason", args: args2 };
3839
3893
  }
@@ -3853,8 +3907,8 @@ async function installedCommand() {
3853
3907
  }
3854
3908
  }
3855
3909
  async function prepareLaunch(host, options = {}) {
3856
- const root = await fs16.realpath((await git3(options.dir ?? process.cwd(), "rev-parse", "--show-toplevel")).trim());
3857
- const entry = (await loadSetup(root))?.hosts[host];
3910
+ const root = await fs17.realpath((await git3(options.dir ?? process.cwd(), "rev-parse", "--show-toplevel")).trim());
3911
+ const entry = (await effectiveSetup(root)).setup?.hosts[host];
3858
3912
  process.env.MASON_SETUP_ROOT = root;
3859
3913
  process.env.MASON_SETUP_HOST = host;
3860
3914
  delete process.env.MASON_SETUP_REVISION;
@@ -3876,7 +3930,7 @@ var init_launcher = __esm({
3876
3930
  "use strict";
3877
3931
  init_evidence();
3878
3932
  init_model();
3879
- executingVersion = () => true ? "0.18.0" : "development";
3933
+ executingVersion = () => true ? "0.19.0" : "development";
3880
3934
  mcpCommand = (host) => globalCommand(["--setup-host", host, "mcp"]);
3881
3935
  hookCommand = (host) => `mason --setup-host ${host} auto`;
3882
3936
  }
@@ -3889,7 +3943,7 @@ __export(observations_exports, {
3889
3943
  observeActivation: () => observeActivation,
3890
3944
  readObservation: () => readObservation
3891
3945
  });
3892
- import fs17 from "fs/promises";
3946
+ import fs18 from "fs/promises";
3893
3947
  import { z as z12 } from "zod";
3894
3948
  function observationPath(directory, host, revision, version = executingVersion()) {
3895
3949
  return `${directory}/activation/${host}-${revision}-${hash(version).slice(0, 12)}.json`;
@@ -3906,15 +3960,15 @@ async function observeActivation(dir, event, options = {}) {
3906
3960
  if (host !== "codex" && host !== "claude" || !process.env.MASON_SETUP_ROOT) return null;
3907
3961
  try {
3908
3962
  if (!revision) {
3909
- if ((await loadSetup(process.env.MASON_SETUP_ROOT))?.hosts[host]) {
3963
+ if ((await effectiveSetup(process.env.MASON_SETUP_ROOT)).setup?.hosts[host]) {
3910
3964
  return "Mason setup is now present. Restart the assistant to connect this MCP session to the configured integration; activation has not been established by this session.";
3911
3965
  }
3912
3966
  return inactiveSetupMessage();
3913
3967
  }
3914
3968
  const capturedDirectory = options.directory?.match(/^\.mason\/reports\/automation\/[a-f0-9]{24}$/)?.[0] ?? options.reportPath?.match(/^(\.mason\/reports\/automation\/[a-f0-9]{24})\/checks\//)?.[1];
3915
- const ws = capturedDirectory ? { root: await fs17.realpath(dir), directory: capturedDirectory } : await workspace(dir);
3916
- if (ws.root !== await fs17.realpath(process.env.MASON_SETUP_ROOT)) return null;
3917
- const setup = await loadSetup(ws.root);
3969
+ const ws = capturedDirectory ? { root: await fs18.realpath(dir), directory: capturedDirectory } : await workspace(dir);
3970
+ if (ws.root !== await fs18.realpath(process.env.MASON_SETUP_ROOT)) return null;
3971
+ const { setup } = await effectiveSetup(ws.root);
3918
3972
  if (setup?.hosts[host]?.revision !== revision) return "Mason setup changed; restart the assistant to observe the current integration.";
3919
3973
  const directory = ws.directory + "/activation";
3920
3974
  await withLock(ws.root, directory, async () => {
@@ -4091,8 +4145,8 @@ __export(files_exports, {
4091
4145
  managedBlock: () => managedBlock,
4092
4146
  readText: () => readText
4093
4147
  });
4094
- import fs18 from "fs/promises";
4095
- import path23 from "path";
4148
+ import fs19 from "fs/promises";
4149
+ import path24 from "path";
4096
4150
  import { randomUUID as randomUUID5 } from "crypto";
4097
4151
  async function readText(root, file) {
4098
4152
  try {
@@ -4121,14 +4175,14 @@ async function applyEdit(root, edit) {
4121
4175
  if (edit.after === null) {
4122
4176
  const file2 = await storePath(root, edit.path);
4123
4177
  if (await readText(root, edit.path) !== edit.before) throw new Error("Input changed during removal: " + edit.path);
4124
- await fs18.unlink(file2);
4178
+ await fs19.unlink(file2);
4125
4179
  return true;
4126
4180
  }
4127
4181
  const file = await storePath(root, edit.path, true);
4128
- const temporary = path23.join(path23.dirname(file), ".mason-setup-" + randomUUID5() + ".tmp");
4182
+ const temporary = path24.join(path24.dirname(file), ".mason-setup-" + randomUUID5() + ".tmp");
4129
4183
  try {
4130
- const mode = await fs18.stat(file).then((s) => s.mode & 511, () => 384);
4131
- const handle = await fs18.open(temporary, "wx", mode);
4184
+ const mode = await fs19.stat(file).then((s) => s.mode & 511, () => 384);
4185
+ const handle = await fs19.open(temporary, "wx", mode);
4132
4186
  try {
4133
4187
  await handle.writeFile(edit.after, "utf8");
4134
4188
  await handle.sync();
@@ -4136,10 +4190,10 @@ async function applyEdit(root, edit) {
4136
4190
  await handle.close();
4137
4191
  }
4138
4192
  if (await readText(root, edit.path) !== edit.before) throw new Error("Integration input changed during update: " + edit.path);
4139
- await fs18.rename(temporary, file);
4193
+ await fs19.rename(temporary, file);
4140
4194
  return true;
4141
4195
  } finally {
4142
- await fs18.rm(temporary, { force: true });
4196
+ await fs19.rm(temporary, { force: true });
4143
4197
  }
4144
4198
  }
4145
4199
  var init_files2 = __esm({
@@ -4547,6 +4601,8 @@ __export(teardown_exports, {
4547
4601
  teardownProject: () => teardownProject
4548
4602
  });
4549
4603
  import { isDeepStrictEqual as isDeepStrictEqual2 } from "util";
4604
+ import fs20 from "fs/promises";
4605
+ import path25 from "path";
4550
4606
  import { parse as parse3, stringify as stringify2 } from "smol-toml";
4551
4607
  import fg6 from "fast-glob";
4552
4608
  import { z as z18 } from "zod";
@@ -4568,6 +4624,11 @@ async function planTeardown(root, selected) {
4568
4624
  const ownership = ownershipBefore === null ? { version: 1, files: {} } : ownershipSchema.parse(JSON.parse(ownershipBefore));
4569
4625
  const setupBefore = await read(SETUP_PATH);
4570
4626
  const setup = setupBefore === null ? null : setupSchema.parse(JSON.parse(setupBefore));
4627
+ const linked = (await workspace(root)).gitDir !== await fs20.realpath(
4628
+ path25.resolve(root, (await git3(root, "rev-parse", "--git-common-dir")).trim())
4629
+ );
4630
+ const inactiveBefore = linked && !setup ? await read(INACTIVE_HOSTS_PATH) : null;
4631
+ const inactive = inactiveBefore === null ? { version: 1, hosts: [] } : inactiveHostsSchema.parse(JSON.parse(inactiveBefore));
4571
4632
  const automationBefore = await read(AUTOMATION_PATH);
4572
4633
  const automation = automationBefore === null ? null : recordSchema2.parse(JSON.parse(automationBefore));
4573
4634
  const commands2 = (host) => new Set([
@@ -4711,7 +4772,16 @@ async function planTeardown(root, selected) {
4711
4772
  delete ownership.files[configPath(host)];
4712
4773
  }
4713
4774
  if (!remainingHost) for (const file of DOC_CANDIDATES) delete ownership.files[file];
4714
- if (setupBefore !== null) cleanup.push({ path: SETUP_PATH, before: setupBefore, after: Object.keys(setup.hosts).length ? json(setup) : null });
4775
+ if (setupBefore !== null) cleanup.push({
4776
+ path: SETUP_PATH,
4777
+ before: setupBefore,
4778
+ after: linked || Object.keys(setup.hosts).length ? json(setup) : null
4779
+ });
4780
+ else if (linked) cleanup.push({
4781
+ path: INACTIVE_HOSTS_PATH,
4782
+ before: inactiveBefore,
4783
+ after: json({ version: 1, hosts: [.../* @__PURE__ */ new Set([...inactive.hosts, ...selected])].sort() })
4784
+ });
4715
4785
  if (automationBefore !== null) cleanup.push({ path: AUTOMATION_PATH, before: automationBefore, after: Object.keys(automation.hosts).length ? json(automation) : null });
4716
4786
  if (ownershipBefore !== null) cleanup.push({ path: OWNERSHIP_PATH, before: ownershipBefore, after: Object.keys(ownership.files).length ? json(ownership) : null });
4717
4787
  edits.push(...cleanup);
@@ -5152,10 +5222,10 @@ var init_check = __esm({
5152
5222
  });
5153
5223
 
5154
5224
  // src/review/evidence.ts
5155
- import path24 from "path";
5225
+ import path26 from "path";
5156
5226
  import { z as z22 } from "zod";
5157
5227
  function relativeArtifact(root, file) {
5158
- const relative = path24.isAbsolute(file) ? path24.relative(root, file) : file;
5228
+ const relative = path26.isAbsolute(file) ? path26.relative(root, file) : file;
5159
5229
  const normalized = normalizeRepoPath(relative);
5160
5230
  if (!normalized) throw new Error(`Evidence artifact must be inside the repository: ${file}`);
5161
5231
  return normalized;
@@ -5252,7 +5322,7 @@ async function collectReviewEvidence(root, manifests, changedFiles, decisions, d
5252
5322
  const report = await readStoreJson(root, file);
5253
5323
  if (report === null) throw new Error(`Report artifact is missing: ${file}`);
5254
5324
  const sourceRoot = check.sourceRoot ?? root;
5255
- if (!path24.posix.isAbsolute(sourceRoot) && !/^[A-Za-z]:[\\/]/.test(sourceRoot)) throw new Error("sourceRoot must identify the absolute checkout root on the check runner.");
5325
+ if (!path26.posix.isAbsolute(sourceRoot) && !/^[A-Za-z]:[\\/]/.test(sourceRoot)) throw new Error("sourceRoot must identify the absolute checkout root on the check runner.");
5256
5326
  const parsed = check.report.format === "vitest-json" ? parseVitest(report, sourceRoot) : check.report.format === "mason-check-json" ? parseCheck(report, sourceRoot) : parseSarif(report, sourceRoot);
5257
5327
  result.outcome = parsed.outcome;
5258
5328
  result.counts = parsed.counts;
@@ -5373,8 +5443,8 @@ var init_evidence2 = __esm({
5373
5443
  });
5374
5444
 
5375
5445
  // src/review/review.ts
5376
- import fs19 from "fs/promises";
5377
- import path25 from "path";
5446
+ import fs21 from "fs/promises";
5447
+ import path27 from "path";
5378
5448
  import { execFile as execFile4 } from "child_process";
5379
5449
  import { promisify as promisify4 } from "util";
5380
5450
  async function resolveMergeBase(resolvedRoot, base, head) {
@@ -5403,7 +5473,7 @@ function anchorsTouched(record, changedFiles) {
5403
5473
  return matchingPaths(record.files, changedFiles);
5404
5474
  }
5405
5475
  async function computeReview(rootDir, base, options = {}) {
5406
- const resolvedRoot = path25.resolve(rootDir);
5476
+ const resolvedRoot = path27.resolve(rootDir);
5407
5477
  const head = await getCurrentGitHash(resolvedRoot);
5408
5478
  if (head === "unknown") return null;
5409
5479
  const mergeBase = await resolveMergeBase(resolvedRoot, base, head);
@@ -5453,7 +5523,7 @@ async function computeReview(rootDir, base, options = {}) {
5453
5523
  analyzed,
5454
5524
  async (relPath) => {
5455
5525
  try {
5456
- await fs19.access(path25.join(resolvedRoot, relPath));
5526
+ await fs21.access(path27.join(resolvedRoot, relPath));
5457
5527
  return true;
5458
5528
  } catch {
5459
5529
  return false;
@@ -5596,8 +5666,9 @@ __export(status_exports, {
5596
5666
  import { isDeepStrictEqual as isDeepStrictEqual3 } from "util";
5597
5667
  async function setupStatus(dir) {
5598
5668
  const ws = await workspace(dir);
5599
- const setup = await loadSetup(ws.root);
5600
- if (!setup) {
5669
+ const activation = await effectiveSetup(ws.root);
5670
+ const setup = activation.setup;
5671
+ if (!setup || !Object.keys(setup.hosts).length) {
5601
5672
  const partial = await Promise.all(["codex", "claude"].map(async (host) => ({
5602
5673
  host,
5603
5674
  receipt: await loadSetupReceipt(ws.root, ws.directory, host)
@@ -5631,7 +5702,7 @@ async function setupStatus(dir) {
5631
5702
  ]));
5632
5703
  const hooks = isDeepStrictEqual3(compatibleHooks, expectedHooks) && !config.disabled;
5633
5704
  const local = await loadSetupReceipt(ws.root, ws.directory, host);
5634
- const configured = local?.status === "configured" && local.root === ws.root && local.revision === entry.revision;
5705
+ const configured = activation.root !== ws.root || local?.status === "configured" && local.root === ws.root && local.revision === entry.revision;
5635
5706
  const observation = await readObservation(ws.root, ws.directory, host, entry.revision, installed.version ?? "unavailable");
5636
5707
  const sessions = Object.values(observation?.sessions ?? {}).sort((a, b) => b.at.localeCompare(a.at));
5637
5708
  const complete = sessions.find((session) => events.every((event) => session.events.includes(event)));
@@ -5663,6 +5734,7 @@ async function setupStatus(dir) {
5663
5734
  status: statuses.length && statuses.every((status) => status === "active") ? "active" : statuses.includes("attention") ? "attention" : "pending",
5664
5735
  root: ws.root,
5665
5736
  hosts: hosts2,
5737
+ activationRoot: activation.root,
5666
5738
  decisionRecords: decisions.records.length,
5667
5739
  diagnostics: decisions.diagnostics,
5668
5740
  scope: "Activation receipts record observed use after setup, not complete interception, correct repairs, or measured usefulness. Host trust and higher-priority settings may prevent execution. Receipts are local to this worktree and branch."
@@ -5670,6 +5742,9 @@ async function setupStatus(dir) {
5670
5742
  }
5671
5743
  function summarizeActivation(status) {
5672
5744
  const lines = ["Mason setup: " + status.status + "."];
5745
+ if (status.activationRoot && status.activationRoot !== status.root) {
5746
+ lines.push("Activation inherited from " + status.activationRoot + "; evidence belongs to this worktree.");
5747
+ }
5673
5748
  for (const [host, state] of Object.entries(status.hosts)) {
5674
5749
  lines.push(
5675
5750
  `${host}: ${state.status}`,
@@ -5831,66 +5906,19 @@ var init_setup = __esm({
5831
5906
  }
5832
5907
  });
5833
5908
 
5834
- // src/distribution/bundle.ts
5835
- import fs20 from "fs/promises";
5836
- import path26 from "path";
5837
- import { createHash as createHash4 } from "crypto";
5838
- import { z as z23 } from "zod";
5839
- async function readBundle(root) {
5840
- const bytes = await fs20.readFile(await storePath(root, "bundle.json"));
5841
- const manifest = bundleSchema.parse(JSON.parse(bytes.toString("utf8")));
5842
- const windows = manifest.target.startsWith("win32-");
5843
- for (const file of Object.keys(manifest.files)) {
5844
- if (file.includes("\\") || file.split("/").some((p) => !p || p === "." || p === "..") || path26.isAbsolute(file)) {
5845
- throw new Error("Unsafe path in Mason bundle: " + file);
5846
- }
5847
- }
5848
- for (const required of [windows ? "node.exe" : "node", "app/package.json", "app/dist/mason.js", "app/dist/mason-auto.js", "app/dist/mason-mcp.js"]) {
5849
- if (!manifest.files[required]) throw new Error("Incomplete Mason bundle: " + required);
5850
- }
5851
- return { manifest, manifestHash: sha256(bytes) };
5852
- }
5853
- async function verifyBundle(root, expectedHash) {
5854
- const bundle = await readBundle(root);
5855
- if (expectedHash && bundle.manifestHash !== expectedHash) throw new Error("Mason bundle manifest changed.");
5856
- if (bundle.manifest.target !== `${process.platform}-${process.arch}`) throw new Error("Mason bundle is for another platform. Install this platform's bundle and rerun setup.");
5857
- const files = Object.keys(bundle.manifest.files);
5858
- for (let i = 0; i < files.length; i += 8) await Promise.all(files.slice(i, i + 8).map(async (file) => {
5859
- if (sha256(await fs20.readFile(await storePath(root, file))) !== bundle.manifest.files[file]) throw new Error("Mason bundle checksum mismatch: " + file);
5860
- }));
5861
- return bundle;
5862
- }
5863
- var targets, digest2, bundleSchema, sha256;
5864
- var init_bundle = __esm({
5865
- "src/distribution/bundle.ts"() {
5866
- "use strict";
5867
- init_storage();
5868
- targets = ["darwin-arm64", "darwin-x64", "linux-arm64", "linux-x64", "win32-arm64", "win32-x64"];
5869
- digest2 = z23.string().regex(/^[a-f0-9]{64}$/);
5870
- bundleSchema = z23.object({
5871
- format: z23.literal(1),
5872
- version: z23.string().regex(/^\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?$/),
5873
- target: z23.enum(targets),
5874
- nodeVersion: z23.string(),
5875
- files: z23.record(digest2)
5876
- }).strict();
5877
- sha256 = (bytes) => createHash4("sha256").update(bytes).digest("hex");
5878
- }
5879
- });
5880
-
5881
5909
  // src/distribution/path.ts
5882
- import fs21 from "fs/promises";
5910
+ import fs22 from "fs/promises";
5883
5911
  import os3 from "os";
5884
- import path27 from "path";
5912
+ import path28 from "path";
5885
5913
  import { execFile as execFile5 } from "child_process";
5886
5914
  import { promisify as promisify5 } from "util";
5887
- import { createHash as createHash5, randomUUID as randomUUID7 } from "crypto";
5888
- import { z as z24 } from "zod";
5915
+ import { createHash as createHash4, randomUUID as randomUUID7 } from "crypto";
5916
+ import { z as z23 } from "zod";
5889
5917
  async function configurePath(bin, previous, save, options = {}) {
5890
5918
  const env = options.env ?? process.env, platform = options.platform ?? process.platform;
5891
5919
  const changes = [...previous];
5892
5920
  const currentPath = platform === "win32" ? Object.entries(env).find(([key]) => key.toLowerCase() === "path")?.[1] : env.PATH;
5893
- const alreadyOnPath = platform === "win32" ? hasWindowsEntry(currentPath ?? null, bin, env) : (currentPath ?? "").split(":").some((entry) => path27.posix.isAbsolute(entry) && path27.posix.resolve(entry) === path27.posix.resolve(bin));
5921
+ const alreadyOnPath = platform === "win32" ? hasWindowsEntry(currentPath ?? null, bin, env) : (currentPath ?? "").split(":").some((entry) => path28.posix.isAbsolute(entry) && path28.posix.resolve(entry) === path28.posix.resolve(bin));
5894
5922
  const setupHint = "To connect a project, run one of these inside its directory:\n Codex: mason setup --host codex\n Claude Code: mason setup --host claude";
5895
5923
  const message2 = (alreadyOnPath ? "" : "Open a new terminal to use mason.\n") + setupHint;
5896
5924
  const manual = (reason2) => ({ status: "manual", changes, message: `${reason2}
@@ -5910,33 +5938,33 @@ ${alreadyOnPath ? "" : `Add ${bin} to your PATH.
5910
5938
  }
5911
5939
  return { status: "configured", changes, message: message2 };
5912
5940
  }
5913
- const shell = path27.basename(env.SHELL ?? os3.userInfo().shell ?? "");
5941
+ const shell = path28.basename(env.SHELL ?? os3.userInfo().shell ?? "");
5914
5942
  const userHome = options.userHome ?? os3.homedir();
5915
5943
  let profiles2;
5916
5944
  if (!["bash", "zsh", "sh"].includes(shell)) return manual(`Automatic PATH setup is unavailable for ${shell || "an unknown shell"}.`);
5917
- if (env.MASON_PROFILE) profiles2 = [path27.resolve(env.MASON_PROFILE)];
5918
- else if (shell === "zsh") profiles2 = [path27.join(env.ZDOTDIR || userHome, ".zshrc")];
5919
- else if (shell === "sh") profiles2 = [path27.join(userHome, ".profile")];
5945
+ if (env.MASON_PROFILE) profiles2 = [path28.resolve(env.MASON_PROFILE)];
5946
+ else if (shell === "zsh") profiles2 = [path28.join(env.ZDOTDIR || userHome, ".zshrc")];
5947
+ else if (shell === "sh") profiles2 = [path28.join(userHome, ".profile")];
5920
5948
  else {
5921
- const candidates = [".bash_profile", ".bash_login", ".profile"].map((name) => path27.join(userHome, name));
5949
+ const candidates = [".bash_profile", ".bash_login", ".profile"].map((name) => path28.join(userHome, name));
5922
5950
  let login = candidates[2];
5923
- for (const file of candidates) if (await fs21.lstat(file).then(() => true, (error) => {
5951
+ for (const file of candidates) if (await fs22.lstat(file).then(() => true, (error) => {
5924
5952
  if (error.code === "ENOENT") return false;
5925
5953
  throw error;
5926
5954
  })) {
5927
5955
  login = file;
5928
5956
  break;
5929
5957
  }
5930
- profiles2 = [path27.join(userHome, ".bashrc"), login];
5958
+ profiles2 = [path28.join(userHome, ".bashrc"), login];
5931
5959
  }
5932
- const marker = `# >>> mason PATH ${createHash5("sha256").update(bin).digest("hex").slice(0, 12)} >>>`;
5960
+ const marker = `# >>> mason PATH ${createHash4("sha256").update(bin).digest("hex").slice(0, 12)} >>>`;
5933
5961
  for (const profile of profiles2) {
5934
- const stat = await fs21.lstat(profile).catch((error) => {
5962
+ const stat = await fs22.lstat(profile).catch((error) => {
5935
5963
  if (error.code === "ENOENT") return null;
5936
5964
  throw error;
5937
5965
  });
5938
- const file = stat?.isSymbolicLink() ? await fs21.realpath(profile) : path27.resolve(profile);
5939
- const original = await fs21.readFile(file).catch((error) => {
5966
+ const file = stat?.isSymbolicLink() ? await fs22.realpath(profile) : path28.resolve(profile);
5967
+ const original = await fs22.readFile(file).catch((error) => {
5940
5968
  if (error.code === "ENOENT" && !stat) return null;
5941
5969
  throw error;
5942
5970
  });
@@ -5951,8 +5979,8 @@ ${alreadyOnPath ? "" : `Add ${bin} to your PATH.
5951
5979
  changes.push({ kind: "profile", file, block, created: original === null });
5952
5980
  await save(changes);
5953
5981
  }
5954
- await fs21.mkdir(path27.dirname(file), { recursive: true });
5955
- const handle = await fs21.open(file, original === null ? "ax+" : "a+", 384);
5982
+ await fs22.mkdir(path28.dirname(file), { recursive: true });
5983
+ const handle = await fs22.open(file, original === null ? "ax+" : "a+", 384);
5956
5984
  try {
5957
5985
  if (!(await handle.readFile()).equals(original ?? Buffer.alloc(0))) throw new Error(`Shell profile changed during installation: ${profile}`);
5958
5986
  await handle.writeFile(block);
@@ -5976,13 +6004,13 @@ async function removePath(changes) {
5976
6004
  await windowsPath("remove", change.entry, current, change.created);
5977
6005
  continue;
5978
6006
  }
5979
- const stat = await fs21.lstat(change.file).catch((error) => {
6007
+ const stat = await fs22.lstat(change.file).catch((error) => {
5980
6008
  if (error.code === "ENOENT") return null;
5981
6009
  throw error;
5982
6010
  });
5983
6011
  if (!stat) continue;
5984
6012
  if (!stat.isFile()) throw new Error("profile type changed");
5985
- const bytes = await fs21.readFile(change.file), text2 = bytes.toString("utf8");
6013
+ const bytes = await fs22.readFile(change.file), text2 = bytes.toString("utf8");
5986
6014
  if (!Buffer.from(text2).equals(bytes)) throw new Error("profile encoding changed");
5987
6015
  const index2 = text2.indexOf(change.block);
5988
6016
  if (index2 < 0 || text2.indexOf(change.block, index2 + change.block.length) >= 0) throw new Error("PATH block was edited or removed");
@@ -5992,7 +6020,7 @@ async function removePath(changes) {
5992
6020
  const temporary = change.file + ".mason-" + randomUUID7();
5993
6021
  try {
5994
6022
  if (stat.nlink > 1) throw new Error("profile has multiple hard links");
5995
- const handle = await fs21.open(temporary, "wx", stat.mode & 511);
6023
+ const handle = await fs22.open(temporary, "wx", stat.mode & 511);
5996
6024
  try {
5997
6025
  await handle.writeFile(next);
5998
6026
  await handle.chmod(stat.mode & 511);
@@ -6001,11 +6029,11 @@ async function removePath(changes) {
6001
6029
  } finally {
6002
6030
  await handle.close();
6003
6031
  }
6004
- const current = await fs21.lstat(change.file);
6005
- if (!current.isFile() || current.ino !== stat.ino || !(await fs21.readFile(change.file)).equals(bytes)) throw new Error("profile changed during uninstall");
6006
- await fs21.rename(temporary, change.file);
6032
+ const current = await fs22.lstat(change.file);
6033
+ if (!current.isFile() || current.ino !== stat.ino || !(await fs22.readFile(change.file)).equals(bytes)) throw new Error("profile changed during uninstall");
6034
+ await fs22.rename(temporary, change.file);
6007
6035
  } finally {
6008
- await fs21.rm(temporary, { force: true });
6036
+ await fs22.rm(temporary, { force: true });
6009
6037
  }
6010
6038
  } catch (error) {
6011
6039
  warnings.push(`Retained PATH setting ${change.kind === "profile" ? change.file : change.entry}: ${error instanceof Error ? error.message : String(error)}`);
@@ -6047,12 +6075,12 @@ var init_path = __esm({
6047
6075
  "src/distribution/path.ts"() {
6048
6076
  "use strict";
6049
6077
  execute2 = promisify5(execFile5);
6050
- pathChangeSchema = z24.discriminatedUnion("kind", [
6051
- z24.object({ kind: z24.literal("profile"), file: z24.string(), block: z24.string(), created: z24.boolean() }),
6052
- z24.object({ kind: z24.literal("windows"), entry: z24.string(), created: z24.boolean() })
6078
+ pathChangeSchema = z23.discriminatedUnion("kind", [
6079
+ z23.object({ kind: z23.literal("profile"), file: z23.string(), block: z23.string(), created: z23.boolean() }),
6080
+ z23.object({ kind: z23.literal("windows"), entry: z23.string(), created: z23.boolean() })
6053
6081
  ]);
6054
6082
  quote = (s) => "'" + s.replace(/'/g, "'\\''") + "'";
6055
- normalizeWindowsEntry = (value) => path27.win32.normalize(value.trim().replace(/^"(.*)"$/, "$1")).replace(/[\\/]+$/, "").toLowerCase();
6083
+ normalizeWindowsEntry = (value) => path28.win32.normalize(value.trim().replace(/^"(.*)"$/, "$1")).replace(/[\\/]+$/, "").toLowerCase();
6056
6084
  hasWindowsEntry = (value, entry, env = process.env) => {
6057
6085
  const variables = new Map(Object.entries(env).map(([key, value2]) => [key.toLowerCase(), value2]));
6058
6086
  const expand = (part) => part.replace(/%([^%]+)%/g, (match, key) => variables.get(key.toLowerCase()) ?? match);
@@ -6061,65 +6089,346 @@ var init_path = __esm({
6061
6089
  }
6062
6090
  });
6063
6091
 
6092
+ // src/distribution/state.ts
6093
+ var state_exports = {};
6094
+ __export(state_exports, {
6095
+ automaticUpdatesAllowed: () => automaticUpdatesAllowed,
6096
+ currentInstallation: () => currentInstallation,
6097
+ defaultHome: () => defaultHome,
6098
+ newerStableVersion: () => newerStableVersion,
6099
+ readInstallation: () => readInstallation,
6100
+ recordSchema: () => recordSchema3,
6101
+ releaseVersion: () => releaseVersion,
6102
+ standaloneLocation: () => standaloneLocation,
6103
+ updatePolicySchema: () => updatePolicySchema
6104
+ });
6105
+ import os4 from "os";
6106
+ import path29 from "path";
6107
+ import { fileURLToPath } from "url";
6108
+ import fs23 from "fs/promises";
6109
+ import { z as z24 } from "zod";
6110
+ async function readInstallation(home) {
6111
+ const raw = await readStoreJson(home, "install.json");
6112
+ if (!raw) throw new Error("Run this command from a standalone installation. npm users can upgrade with npm.");
6113
+ const record = recordSchema3.parse(raw);
6114
+ if (record.home !== await fs23.realpath(home)) throw new Error("Invalid standalone installation receipt.");
6115
+ return record;
6116
+ }
6117
+ function standaloneLocation() {
6118
+ const bundle = path29.resolve(path29.dirname(fileURLToPath(import.meta.url)), "../..");
6119
+ const home = path29.dirname(path29.dirname(bundle));
6120
+ if (path29.dirname(bundle) !== path29.join(home, "versions") || !/^[a-f0-9]{24}$/.test(path29.basename(bundle))) {
6121
+ return null;
6122
+ }
6123
+ return { home, bundle };
6124
+ }
6125
+ async function currentInstallation() {
6126
+ const location2 = standaloneLocation();
6127
+ if (!location2) throw new Error("Run this command from a standalone installation. npm users can upgrade with npm.");
6128
+ const { home, bundle } = location2;
6129
+ const record = await readInstallation(home);
6130
+ return { home, bundle, record };
6131
+ }
6132
+ function automaticUpdatesAllowed(record, env = process.env) {
6133
+ return record.updates?.enabled === true && !record.updates.pinnedVersion && !record.updates.mirror && !env.CI && env.MASON_NO_AUTO_UPDATE !== "1" && !env.MASON_RELEASE_BASE && !env.MASON_VERSION;
6134
+ }
6135
+ function newerStableVersion(candidate, installed) {
6136
+ if (!/^\d+\.\d+\.\d+$/.test(candidate)) return false;
6137
+ const a = candidate.split(".").map(BigInt), b = installed.split("-")[0].split(".").map(BigInt);
6138
+ for (let i = 0; i < 3; i++) {
6139
+ if (a[i] !== b[i]) return a[i] > b[i];
6140
+ }
6141
+ return installed.includes("-");
6142
+ }
6143
+ var releaseVersion, bundleId, updatePolicySchema, recordSchema3, defaultHome;
6144
+ var init_state = __esm({
6145
+ "src/distribution/state.ts"() {
6146
+ "use strict";
6147
+ init_storage();
6148
+ init_path();
6149
+ releaseVersion = z24.string().regex(/^\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?$/);
6150
+ bundleId = z24.string().regex(/^[a-f0-9]{24}$/);
6151
+ updatePolicySchema = z24.object({
6152
+ enabled: z24.boolean(),
6153
+ revision: z24.string(),
6154
+ pinnedVersion: releaseVersion.optional(),
6155
+ mirror: z24.string().optional()
6156
+ });
6157
+ recordSchema3 = z24.object({
6158
+ format: z24.literal(1),
6159
+ home: z24.string(),
6160
+ bin: z24.string(),
6161
+ current: bundleId,
6162
+ version: releaseVersion,
6163
+ launchers: z24.record(z24.string()),
6164
+ previousLaunchers: z24.record(z24.string()).optional(),
6165
+ pathChanges: z24.array(pathChangeSchema).optional(),
6166
+ updates: updatePolicySchema.optional(),
6167
+ previous: z24.object({ id: bundleId, version: releaseVersion }).optional(),
6168
+ pending: z24.object({ id: bundleId, version: releaseVersion, policyRevision: z24.string() }).optional()
6169
+ });
6170
+ defaultHome = () => process.env.MASON_HOME ?? (process.platform === "win32" ? path29.join(process.env.LOCALAPPDATA ?? os4.homedir(), "Mason") : path29.join(os4.homedir(), ".local/share/mason"));
6171
+ }
6172
+ });
6173
+
6174
+ // src/distribution/bundle.ts
6175
+ import fs24 from "fs/promises";
6176
+ import path30 from "path";
6177
+ import { createHash as createHash5 } from "crypto";
6178
+ import { z as z25 } from "zod";
6179
+ async function readBundle(root) {
6180
+ const bytes = await fs24.readFile(await storePath(root, "bundle.json"));
6181
+ const manifest = bundleSchema.parse(JSON.parse(bytes.toString("utf8")));
6182
+ const windows = manifest.target.startsWith("win32-");
6183
+ for (const file of Object.keys(manifest.files)) {
6184
+ if (file.includes("\\") || file.split("/").some((p) => !p || p === "." || p === "..") || path30.isAbsolute(file)) {
6185
+ throw new Error("Unsafe path in Mason bundle: " + file);
6186
+ }
6187
+ }
6188
+ for (const required of [windows ? "node.exe" : "node", "app/package.json", "app/dist/mason.js", "app/dist/mason-auto.js", "app/dist/mason-mcp.js"]) {
6189
+ if (!manifest.files[required]) throw new Error("Incomplete Mason bundle: " + required);
6190
+ }
6191
+ return { manifest, manifestHash: sha256(bytes) };
6192
+ }
6193
+ async function verifyBundle(root, expectedHash) {
6194
+ const bundle = await readBundle(root);
6195
+ if (expectedHash && bundle.manifestHash !== expectedHash) throw new Error("Mason bundle manifest changed.");
6196
+ if (bundle.manifest.target !== `${process.platform}-${process.arch}`) throw new Error("Mason bundle is for another platform. Install this platform's bundle and rerun setup.");
6197
+ const files = Object.keys(bundle.manifest.files);
6198
+ for (let i = 0; i < files.length; i += 8) await Promise.all(files.slice(i, i + 8).map(async (file) => {
6199
+ if (sha256(await fs24.readFile(await storePath(root, file))) !== bundle.manifest.files[file]) throw new Error("Mason bundle checksum mismatch: " + file);
6200
+ }));
6201
+ return bundle;
6202
+ }
6203
+ var targets, digest2, bundleSchema, sha256;
6204
+ var init_bundle = __esm({
6205
+ "src/distribution/bundle.ts"() {
6206
+ "use strict";
6207
+ init_storage();
6208
+ targets = ["darwin-arm64", "darwin-x64", "linux-arm64", "linux-x64", "win32-arm64", "win32-x64"];
6209
+ digest2 = z25.string().regex(/^[a-f0-9]{64}$/);
6210
+ bundleSchema = z25.object({
6211
+ format: z25.literal(1),
6212
+ version: z25.string().regex(/^\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?$/),
6213
+ target: z25.enum(targets),
6214
+ nodeVersion: z25.string(),
6215
+ files: z25.record(digest2)
6216
+ }).strict();
6217
+ sha256 = (bytes) => createHash5("sha256").update(bytes).digest("hex");
6218
+ }
6219
+ });
6220
+
6221
+ // src/distribution/runtime.ts
6222
+ import fs25 from "fs/promises";
6223
+ import { unlinkSync } from "fs";
6224
+ import os5 from "os";
6225
+ import path31 from "path";
6226
+ import { randomUUID as randomUUID8 } from "crypto";
6227
+ import { z as z26 } from "zod";
6228
+ async function pruneUnusedVersions(home) {
6229
+ const versions = await storePath(home, "versions");
6230
+ const candidates = [];
6231
+ const initial = await readInstallation(home);
6232
+ const initialKeep = /* @__PURE__ */ new Set([initial.current, initial.previous?.id, initial.pending?.id]);
6233
+ for (const name of await fs25.readdir(versions)) {
6234
+ const retired = /^\.retired-([a-f0-9]{24})-[a-f0-9-]+$/.exec(name);
6235
+ const id = retired?.[1] ?? name;
6236
+ if (!/^[a-f0-9]{24}$/.test(id) || initialKeep.has(id)) continue;
6237
+ try {
6238
+ const directory = await storePath(home, `versions/${name}`);
6239
+ await fs25.access(await storePath(directory, "app/dist/mason-runtime.js"));
6240
+ const bundle = await verifyBundle(directory);
6241
+ if (bundle.manifestHash.slice(0, 24) === id && bundle.manifest.files["app/dist/mason-runtime.js"])
6242
+ candidates.push({ id, directory, retired: !!retired });
6243
+ } catch {
6244
+ }
6245
+ }
6246
+ if (!candidates.length) return;
6247
+ const garbage = [];
6248
+ await withLock(home, ".install-lock", async () => {
6249
+ const record = await readInstallation(home);
6250
+ const keep = /* @__PURE__ */ new Set([record.current, record.previous?.id, record.pending?.id]);
6251
+ const directory = await storePath(home, "runtimes");
6252
+ const leases = await fs25.readdir(directory).catch((error) => {
6253
+ if (error.code === "ENOENT") return [];
6254
+ throw error;
6255
+ });
6256
+ for (const name of leases) {
6257
+ if (!/^[a-f0-9-]+\.json$/.test(name)) return;
6258
+ try {
6259
+ const raw = await readStoreJson(home, `runtimes/${name}`);
6260
+ if (raw === null) continue;
6261
+ const lease = leaseSchema.parse(raw);
6262
+ if (lease.host !== os5.hostname()) return;
6263
+ try {
6264
+ process.kill(lease.pid, 0);
6265
+ keep.add(lease.bundle);
6266
+ } catch (error) {
6267
+ if (error.code !== "ESRCH") return;
6268
+ await fs25.rm(await storePath(home, `runtimes/${name}`), { force: true });
6269
+ }
6270
+ } catch {
6271
+ return;
6272
+ }
6273
+ }
6274
+ for (const candidate of candidates) {
6275
+ if (keep.has(candidate.id)) continue;
6276
+ try {
6277
+ const retired = candidate.retired ? candidate.directory : await storePath(home, `versions/.retired-${candidate.id}-${randomUUID8()}`);
6278
+ if (!candidate.retired) await fs25.rename(candidate.directory, retired);
6279
+ garbage.push(retired);
6280
+ } catch {
6281
+ }
6282
+ }
6283
+ });
6284
+ for (const directory of garbage) await fs25.rm(directory, { recursive: true }).catch(() => {
6285
+ });
6286
+ }
6287
+ var leaseSchema;
6288
+ var init_runtime2 = __esm({
6289
+ "src/distribution/runtime.ts"() {
6290
+ "use strict";
6291
+ init_store();
6292
+ init_storage();
6293
+ init_state();
6294
+ init_bundle();
6295
+ leaseSchema = z26.object({ pid: z26.number().int().positive(), host: z26.string(), bundle: z26.string().regex(/^[a-f0-9]{24}$/) });
6296
+ }
6297
+ });
6298
+
6299
+ // src/distribution/sessions.ts
6300
+ import fs26 from "fs/promises";
6301
+ import os6 from "os";
6302
+ import { randomUUID as randomUUID9 } from "crypto";
6303
+ import { z as z27 } from "zod";
6304
+ async function hasRunningMcp(home) {
6305
+ const directory = await storePath(home, "sessions");
6306
+ const files = await fs26.readdir(directory).catch((error) => {
6307
+ if (error.code === "ENOENT") return [];
6308
+ throw error;
6309
+ });
6310
+ for (const name of files) {
6311
+ if (!/^[a-f0-9-]+\.json$/.test(name)) return true;
6312
+ const relative = "sessions/" + name;
6313
+ try {
6314
+ const raw = await readStoreJson(home, relative);
6315
+ if (raw === null) continue;
6316
+ const lease = leaseSchema2.parse(raw);
6317
+ if (lease.host !== os6.hostname()) return true;
6318
+ try {
6319
+ process.kill(lease.pid, 0);
6320
+ return true;
6321
+ } catch (error) {
6322
+ if (error.code !== "ESRCH") return true;
6323
+ }
6324
+ await fs26.rm(await storePath(home, relative), { force: true });
6325
+ } catch {
6326
+ return true;
6327
+ }
6328
+ }
6329
+ return false;
6330
+ }
6331
+ async function registerMcp(home) {
6332
+ const name = `sessions/${randomUUID9()}.json`;
6333
+ await writeStoreJson(home, name, { pid: process.pid, host: os6.hostname() });
6334
+ return async () => {
6335
+ await fs26.rm(await storePath(home, name), { force: true }).catch(() => {
6336
+ });
6337
+ };
6338
+ }
6339
+ var leaseSchema2;
6340
+ var init_sessions = __esm({
6341
+ "src/distribution/sessions.ts"() {
6342
+ "use strict";
6343
+ init_storage();
6344
+ leaseSchema2 = z27.object({ pid: z27.number().int().positive(), host: z27.string() });
6345
+ }
6346
+ });
6347
+
6064
6348
  // src/distribution/install.ts
6065
6349
  var install_exports2 = {};
6066
6350
  __export(install_exports2, {
6067
6351
  WINDOWS_BATCH_LAUNCHER: () => WINDOWS_BATCH_LAUNCHER,
6068
6352
  defaultHome: () => defaultHome,
6069
6353
  installStandalone: () => installStandalone,
6354
+ prepareStandaloneMcp: () => prepareStandaloneMcp,
6355
+ rollbackStandalone: () => rollbackStandalone,
6070
6356
  uninstallStandalone: () => uninstallStandalone,
6071
6357
  upgradeStandalone: () => upgradeStandalone
6072
6358
  });
6073
- import fs22 from "fs/promises";
6074
- import os4 from "os";
6075
- import path28 from "path";
6076
- import { fileURLToPath } from "url";
6077
- import { spawn } from "child_process";
6078
- import { randomUUID as randomUUID8 } from "crypto";
6079
- import { z as z25 } from "zod";
6359
+ import fs27 from "fs/promises";
6360
+ import os7 from "os";
6361
+ import path32 from "path";
6362
+ import { execFile as execFile6, spawn } from "child_process";
6363
+ import { promisify as promisify6 } from "util";
6364
+ import { randomUUID as randomUUID10 } from "crypto";
6080
6365
  async function replace(file, content2, mode = 384) {
6081
- const temp = file + ".tmp-" + randomUUID8();
6366
+ const temp = file + ".tmp-" + randomUUID10();
6082
6367
  try {
6083
- await fs22.writeFile(temp, content2, { mode, flag: "wx" });
6084
- await fs22.rename(temp, file);
6368
+ await fs27.writeFile(temp, content2, { mode, flag: "wx" });
6369
+ await fs27.rename(temp, file);
6085
6370
  } finally {
6086
- await fs22.rm(temp, { force: true });
6371
+ await fs27.rm(temp, { force: true });
6087
6372
  }
6088
6373
  }
6089
- async function installStandalone(source, progress2) {
6374
+ async function installStandalone(source, progress2, options = {}) {
6090
6375
  progress2?.step("Checking installation files and location");
6091
6376
  const bundle = await verifyBundle(source);
6092
- const selectedHome = path28.resolve(defaultHome());
6093
- await fs22.mkdir(selectedHome, { recursive: true });
6094
- const home = await fs22.realpath(selectedHome);
6095
- const selectedBin = path28.resolve(process.env.MASON_BIN_DIR ?? (process.platform === "win32" ? path28.join(home, "bin") : path28.join(os4.homedir(), ".local/bin")));
6096
- return withLock(home, ".install-lock", async () => {
6097
- const raw = await readStoreJson(home, "install.json");
6098
- const previous = raw === null ? null : recordSchema3.parse(raw);
6099
- if (!previous) for (const file of await fs22.readdir(home)) {
6100
- if (file === ".install-lock") continue;
6101
- const directory = path28.join(home, file);
6102
- const stat = await fs22.lstat(directory);
6103
- if (!["versions", "bin"].includes(file) || !stat.isDirectory() || (await fs22.readdir(directory)).length) {
6104
- throw new Error("Mason installation directory is not empty and has no ownership receipt.");
6105
- }
6106
- }
6107
- await fs22.mkdir(selectedBin, { recursive: true });
6108
- const bin = await fs22.realpath(selectedBin);
6109
- if (previous && (previous.home !== home || previous.bin !== bin)) throw new Error("Installation paths differ from the existing Mason receipt.");
6110
- const id = bundle.manifestHash.slice(0, 24);
6111
- const destination = await storePath(home, "versions/" + id);
6112
- const launchers = process.platform === "win32" ? {
6113
- "mason.cmd": WINDOWS_BATCH_LAUNCHER,
6114
- "mason-launcher.ps1": `$ErrorActionPreference = 'Stop'
6377
+ const selectedHome = path32.resolve(defaultHome());
6378
+ if (!options.stageOnly) await fs27.mkdir(selectedHome, { recursive: true });
6379
+ const home = await fs27.realpath(selectedHome);
6380
+ if (options.stageOnly) {
6381
+ assertUpdatePolicy(await readInstallation(home), bundle.manifest.version, options.policyRevision);
6382
+ const destination = await storePath(home, "versions/" + bundle.manifestHash.slice(0, 24));
6383
+ await copyBundle(home, source, destination, bundle);
6384
+ const { stdout } = await promisify6(execFile6)(
6385
+ path32.join(destination, process.platform === "win32" ? "node.exe" : "node"),
6386
+ [path32.join(destination, "app/dist/mason.js"), "--version"],
6387
+ { timeout: 3e4, windowsHide: true, env: { ...process.env, MASON_NO_AUTO_UPDATE: "1" } }
6388
+ );
6389
+ if (stdout.trim() !== bundle.manifest.version) throw new Error("Downloaded Mason failed its version health check.");
6390
+ }
6391
+ return withLock(home, ".install-lock", () => installLocked2(home, source, bundle, progress2, options));
6392
+ }
6393
+ function assertUpdatePolicy(record, version, revision) {
6394
+ if (!record || !automaticUpdatesAllowed(record, { ...process.env, MASON_VERSION: "" }) || record.updates?.revision !== revision || !newerStableVersion(version, record.version)) {
6395
+ throw new Error("Update policy or installed version changed; download was not activated.");
6396
+ }
6397
+ }
6398
+ async function installLocked2(home, source, bundle, progress2, options, alreadyStaged = false) {
6399
+ const raw = await readStoreJson(home, "install.json");
6400
+ const previous = raw === null ? null : recordSchema3.parse(raw);
6401
+ if (options.stageOnly) assertUpdatePolicy(previous, bundle.manifest.version, options.policyRevision);
6402
+ if (!previous) for (const file of await fs27.readdir(home)) {
6403
+ if (file === ".install-lock") continue;
6404
+ const directory = path32.join(home, file);
6405
+ const stat = await fs27.lstat(directory);
6406
+ if (!["versions", "bin"].includes(file) || !stat.isDirectory() || (await fs27.readdir(directory)).length) {
6407
+ throw new Error("Mason installation directory is not empty and has no ownership receipt.");
6408
+ }
6409
+ }
6410
+ const selectedBin = path32.resolve(process.env.MASON_BIN_DIR ?? previous?.bin ?? (process.platform === "win32" ? path32.join(home, "bin") : path32.join(os7.homedir(), ".local/bin")));
6411
+ await fs27.mkdir(selectedBin, { recursive: true });
6412
+ const bin = await fs27.realpath(selectedBin);
6413
+ if (previous && (previous.home !== home || previous.bin !== bin)) throw new Error("Installation paths differ from the existing Mason receipt.");
6414
+ const id = bundle.manifestHash.slice(0, 24);
6415
+ const destination = await storePath(home, "versions/" + id);
6416
+ if (options.stageOnly) {
6417
+ const receipt2 = { ...previous, pending: { id, version: bundle.manifest.version, policyRevision: options.policyRevision } };
6418
+ await writeStoreJson(home, "install.json", receipt2);
6419
+ return { home, bin, version: bundle.manifest.version, previousVersion: previous.version, path: { message: "Update staged for a future MCP launch." } };
6420
+ }
6421
+ const launchers = process.platform === "win32" ? {
6422
+ "mason.cmd": WINDOWS_BATCH_LAUNCHER,
6423
+ "mason-launcher.ps1": `$ErrorActionPreference = 'Stop'
6115
6424
  $previousToken = $env:MASON_UNINSTALL_TOKEN
6116
6425
  $token = [Guid]::NewGuid().ToString()
6117
6426
  $env:MASON_UNINSTALL_TOKEN = $token
6118
6427
  try {
6119
- & ${psQuote(path28.join(destination, "node.exe"))} ${psQuote(path28.join(destination, "app/dist/mason.js"))} @args
6428
+ & ${psQuote(path32.join(destination, "node.exe"))} ${psQuote(path32.join(destination, "app/dist/mason.js"))} @args
6120
6429
  $code = $LASTEXITCODE
6121
6430
  if ($code -eq 0 -and $args.Count -eq 1 -and $args[0] -eq 'uninstall') {
6122
- if ([IO.File]::ReadAllText(${psQuote(path28.join(home, ".uninstall-request.txt"))}) -cne $token) { throw 'Mason did not authorize installation cleanup.' }
6431
+ if ([IO.File]::ReadAllText(${psQuote(path32.join(home, ".uninstall-request.txt"))}) -cne $token) { throw 'Mason did not authorize installation cleanup.' }
6123
6432
  Remove-Item -LiteralPath ${psQuote(home)} -Recurse -Force
6124
6433
  Write-Output ${psQuote("Removed standalone installation: " + home)}
6125
6434
  }
@@ -6127,70 +6436,146 @@ try {
6127
6436
  } catch { [Console]::Error.WriteLine($_.Exception.Message); exit 2 }
6128
6437
  finally { $env:MASON_UNINSTALL_TOKEN = $previousToken }
6129
6438
  `
6130
- } : { mason: `#!/bin/sh
6131
- exec ${shQuote(path28.join(destination, "node"))} ${shQuote(path28.join(destination, "app/dist/mason.js"))} "$@"
6439
+ } : { mason: `#!/bin/sh
6440
+ exec ${shQuote(path32.join(destination, "node"))} ${shQuote(path32.join(destination, "app/dist/mason.js"))} "$@"
6132
6441
  ` };
6133
- for (const [name, content2] of Object.entries(launchers)) {
6134
- const file = await storePath(bin, name);
6135
- const old = await fs22.readFile(file, "utf8").catch((error) => {
6136
- if (error.code === "ENOENT") return null;
6137
- throw error;
6138
- });
6139
- if (old !== null && old !== previous?.launchers[name] && old !== previous?.previousLaunchers?.[name] && old !== content2) throw new Error("Refusing to replace an unrelated or edited launcher: " + file);
6442
+ for (const [name, content2] of Object.entries(launchers)) {
6443
+ const file = await storePath(bin, name);
6444
+ const old = await fs27.readFile(file, "utf8").catch((error) => {
6445
+ if (error.code === "ENOENT") return null;
6446
+ throw error;
6447
+ });
6448
+ if (old !== null && old !== previous?.launchers[name] && old !== previous?.previousLaunchers?.[name] && old !== content2) throw new Error("Refusing to replace an unrelated or edited launcher: " + file);
6449
+ }
6450
+ const retired = Object.entries({ ...previous?.previousLaunchers, ...previous?.launchers }).filter(([name]) => !(name in launchers));
6451
+ const checkRetired = async (name, expected) => {
6452
+ if (!launcherNames.includes(name)) throw new Error("Invalid owned launcher name.");
6453
+ const actual = await fs27.readFile(await storePath(bin, name), "utf8").catch((error) => {
6454
+ if (error.code === "ENOENT") return null;
6455
+ throw error;
6456
+ });
6457
+ if (actual !== null && actual !== expected) throw new Error("Retired launcher was edited; retained: " + name);
6458
+ };
6459
+ for (const [name, expected] of retired) await checkRetired(name, expected);
6460
+ progress2?.step(`Installing Mason ${bundle.manifest.version}`);
6461
+ const updates = options.preservePolicy && previous?.updates ? { ...previous.updates } : {
6462
+ ...previous?.updates ?? { enabled: !previous && !process.env.CI && process.env.MASON_NO_AUTO_UPDATE !== "1" },
6463
+ revision: randomUUID10(),
6464
+ pinnedVersion: process.env.MASON_VERSION?.replace(/^v/, "") || void 0,
6465
+ mirror: process.env.MASON_RELEASE_BASE || previous?.updates?.mirror
6466
+ };
6467
+ if (options.rollback) {
6468
+ updates.pinnedVersion = bundle.manifest.version;
6469
+ updates.revision = randomUUID10();
6470
+ }
6471
+ const receipt = {
6472
+ format: 1,
6473
+ home,
6474
+ bin,
6475
+ current: id,
6476
+ version: bundle.manifest.version,
6477
+ launchers,
6478
+ pathChanges: previous?.pathChanges ?? [],
6479
+ updates,
6480
+ previous: previous && previous.current !== id ? { id: previous.current, version: previous.version } : previous?.previous
6481
+ };
6482
+ await writeStoreJson(home, "install.json", previous ? { ...previous, previousLaunchers: launchers } : receipt);
6483
+ try {
6484
+ if (!alreadyStaged) await copyBundle(home, source, destination, bundle);
6485
+ for (const [name, content2] of Object.entries(launchers)) await replace(await storePath(bin, name, true), content2, 493);
6486
+ for (const [name, expected] of retired) {
6487
+ await checkRetired(name, expected);
6488
+ await fs27.rm(await storePath(bin, name), { force: true });
6140
6489
  }
6141
- const retired = Object.entries({ ...previous?.previousLaunchers, ...previous?.launchers }).filter(([name]) => !(name in launchers));
6142
- const checkRetired = async (name, expected) => {
6143
- if (!launcherNames.includes(name)) throw new Error("Invalid owned launcher name.");
6144
- const actual = await fs22.readFile(await storePath(bin, name), "utf8").catch((error) => {
6145
- if (error.code === "ENOENT") return null;
6146
- throw error;
6147
- });
6148
- if (actual !== null && actual !== expected) throw new Error("Retired launcher was edited; retained: " + name);
6149
- };
6150
- for (const [name, expected] of retired) await checkRetired(name, expected);
6151
- progress2?.step(`Installing Mason ${bundle.manifest.version}`);
6152
- const receipt = { format: 1, home, bin, current: id, version: bundle.manifest.version, launchers, pathChanges: previous?.pathChanges ?? [] };
6153
- await writeStoreJson(home, "install.json", { ...receipt, previousLaunchers: previous?.previousLaunchers ?? previous?.launchers });
6154
- const stage = await storePath(home, "versions/.install-" + randomUUID8(), true);
6155
- await fs22.mkdir(stage);
6490
+ await writeStoreJson(home, "install.json", receipt);
6491
+ } catch (error) {
6492
+ if (previous) {
6493
+ for (const [name, content2] of Object.entries(previous.launchers)) await replace(await storePath(bin, name, true), content2, 493);
6494
+ await writeStoreJson(home, "install.json", previous);
6495
+ }
6496
+ throw error;
6497
+ }
6498
+ if (options.preservePolicy) return { home, bin, version: receipt.version, previousVersion: previous?.version, path: { message: "Installation selected." } };
6499
+ progress2?.step("Checking terminal command availability");
6500
+ const pathResult = await configurePath(bin, receipt.pathChanges ?? [], async (changes) => {
6501
+ receipt.pathChanges = changes;
6502
+ await writeStoreJson(home, "install.json", receipt);
6503
+ });
6504
+ return { home, bin, version: bundle.manifest.version, previousVersion: previous?.version, path: pathResult };
6505
+ }
6506
+ async function copyBundle(home, source, destination, bundle) {
6507
+ const exists = await fs27.lstat(destination).then(() => true, (error) => {
6508
+ if (error.code === "ENOENT") return false;
6509
+ throw error;
6510
+ });
6511
+ if (exists) {
6512
+ await verifyBundle(destination, bundle.manifestHash);
6513
+ return;
6514
+ }
6515
+ const stage = await storePath(home, "versions/.install-" + randomUUID10(), true);
6516
+ await fs27.mkdir(stage);
6517
+ try {
6518
+ for (const file of ["bundle.json", ...Object.keys(bundle.manifest.files)]) {
6519
+ const from = await storePath(source, file), to = await storePath(stage, file, true);
6520
+ await fs27.copyFile(from, to);
6521
+ await fs27.chmod(to, (await fs27.stat(from)).mode & 511);
6522
+ }
6523
+ await verifyBundle(stage, bundle.manifestHash);
6156
6524
  try {
6157
- for (const file of ["bundle.json", ...Object.keys(bundle.manifest.files)]) {
6158
- const from = await storePath(source, file), to = await storePath(stage, file, true);
6159
- await fs22.copyFile(from, to);
6160
- await fs22.chmod(to, (await fs22.stat(from)).mode & 511);
6161
- }
6162
- await verifyBundle(stage, bundle.manifestHash);
6163
- const exists = await fs22.lstat(destination).then(() => true, (error) => {
6164
- if (error.code === "ENOENT") return false;
6525
+ await fs27.rename(stage, destination);
6526
+ } catch (error) {
6527
+ try {
6528
+ await verifyBundle(destination, bundle.manifestHash);
6529
+ } catch {
6165
6530
  throw error;
6166
- });
6167
- if (exists) await verifyBundle(destination, bundle.manifestHash);
6168
- else await fs22.rename(stage, destination);
6169
- for (const [name, content2] of Object.entries(launchers)) await replace(await storePath(bin, name, true), content2, 493);
6170
- for (const [name, expected] of retired) {
6171
- await checkRetired(name, expected);
6172
- await fs22.rm(await storePath(bin, name), { force: true });
6173
6531
  }
6174
- await writeStoreJson(home, "install.json", receipt);
6175
- } finally {
6176
- await fs22.rm(stage, { recursive: true, force: true });
6177
6532
  }
6178
- progress2?.step("Checking terminal command availability");
6179
- const pathResult = await configurePath(bin, receipt.pathChanges, async (changes) => {
6180
- receipt.pathChanges = changes;
6181
- await writeStoreJson(home, "install.json", receipt);
6182
- });
6183
- return { home, bin, version: bundle.manifest.version, previousVersion: previous?.version, path: pathResult };
6533
+ } finally {
6534
+ await fs27.rm(stage, { recursive: true, force: true });
6535
+ }
6536
+ }
6537
+ async function prepareStandaloneMcp(home) {
6538
+ const before = await readInstallation(home), candidate = before.pending;
6539
+ let verified, verificationError;
6540
+ if (candidate && automaticUpdatesAllowed(before) && !await hasRunningMcp(home)) {
6541
+ try {
6542
+ verified = await verifyBundle(await storePath(home, "versions/" + candidate.id));
6543
+ if (verified.manifestHash.slice(0, 24) !== candidate.id || verified.manifest.version !== candidate.version) throw new Error("Staged update changed.");
6544
+ } catch (error) {
6545
+ verificationError = error;
6546
+ }
6547
+ }
6548
+ return withLock(home, ".install-lock", async () => {
6549
+ let record = await readInstallation(home);
6550
+ const pending = record.pending;
6551
+ if (pending && candidate?.id === pending.id && (verified || verificationError) && automaticUpdatesAllowed(record) && pending.policyRevision === record.updates?.revision && newerStableVersion(pending.version, record.version) && !await hasRunningMcp(home)) {
6552
+ try {
6553
+ if (verificationError) throw verificationError;
6554
+ const source = await storePath(home, "versions/" + pending.id);
6555
+ await installLocked2(home, source, verified, void 0, { preservePolicy: true }, true);
6556
+ record = await readInstallation(home);
6557
+ } catch (error) {
6558
+ record = await readInstallation(home);
6559
+ delete record.pending;
6560
+ await writeStoreJson(home, "install.json", record);
6561
+ await writeStoreJson(home, "update-status.json", { lastAttempt: Date.now(), error: String(error) });
6562
+ }
6563
+ }
6564
+ const release = await registerMcp(home);
6565
+ return { bundle: await storePath(home, "versions/" + record.current), release };
6184
6566
  });
6185
6567
  }
6186
- async function currentInstallation() {
6187
- const app = path28.dirname(path28.dirname(fileURLToPath(import.meta.url)));
6188
- const bundle = path28.dirname(app), home = path28.dirname(path28.dirname(bundle));
6189
- const raw = await readStoreJson(home, "install.json");
6190
- if (!raw) throw new Error("Run this command from a standalone installation. npm users can upgrade with npm.");
6191
- const record = recordSchema3.parse(raw);
6192
- if (record.home !== await fs22.realpath(home) || path28.dirname(bundle) !== path28.join(home, "versions")) throw new Error("Invalid standalone installation receipt.");
6193
- return { home, bundle, record };
6568
+ async function rollbackStandalone() {
6569
+ const { home } = await currentInstallation();
6570
+ return withLock(home, ".install-lock", async () => {
6571
+ const record = await readInstallation(home);
6572
+ if (!record.previous) throw new Error("No previous installation is available.");
6573
+ if (await hasRunningMcp(home)) throw new Error("Close running Mason MCP sessions before rolling back.");
6574
+ const source = await storePath(home, "versions/" + record.previous.id);
6575
+ const bundle = await verifyBundle(source);
6576
+ if (bundle.manifestHash.slice(0, 24) !== record.previous.id || bundle.manifest.version !== record.previous.version) throw new Error("Previous installation changed.");
6577
+ return installLocked2(home, source, bundle, void 0, { preservePolicy: true, rollback: true });
6578
+ });
6194
6579
  }
6195
6580
  async function upgradeStandalone(version, progress2) {
6196
6581
  if (version && !/^v?\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?$/.test(version)) throw new Error("Expected a release version such as 0.14.0.");
@@ -6198,13 +6583,21 @@ async function upgradeStandalone(version, progress2) {
6198
6583
  const { home, bundle, record } = await currentInstallation();
6199
6584
  await verifyBundle(bundle);
6200
6585
  const windows = process.platform === "win32";
6201
- const script = path28.join(bundle, windows ? "install.ps1" : "install.sh");
6586
+ const script = path32.join(bundle, windows ? "install.ps1" : "install.sh");
6202
6587
  progress2?.stop();
6203
6588
  return new Promise((resolve, reject) => {
6204
6589
  const child = spawn(
6205
6590
  windows ? "powershell.exe" : "sh",
6206
6591
  windows ? ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", script] : [script],
6207
- { stdio: "inherit", windowsHide: true, env: { ...process.env, MASON_HOME: home, MASON_BIN_DIR: record.bin, MASON_VERSION: version ?? "" } }
6592
+ { stdio: "inherit", windowsHide: true, env: {
6593
+ ...process.env,
6594
+ MASON_HOME: home,
6595
+ MASON_BIN_DIR: record.bin,
6596
+ MASON_RELEASE_BASE: process.env.MASON_RELEASE_BASE || record.updates?.mirror || "",
6597
+ MASON_VERSION: version ?? "",
6598
+ MASON_UPDATE_POLICY_REVISION: "",
6599
+ MASON_EXPECTED_SHA256: ""
6600
+ } }
6208
6601
  );
6209
6602
  child.on("error", reject);
6210
6603
  child.on("exit", (code) => resolve(code ?? 2));
@@ -6220,7 +6613,7 @@ async function uninstallStandalone() {
6220
6613
  for (const [name, expected] of Object.entries(record.launchers)) {
6221
6614
  if (!launcherNames.includes(name)) throw new Error("Invalid owned launcher name.");
6222
6615
  const file = await storePath(record.bin, name);
6223
- const actual = await fs22.readFile(file, "utf8").catch((error) => {
6616
+ const actual = await fs27.readFile(file, "utf8").catch((error) => {
6224
6617
  if (error.code === "ENOENT") return null;
6225
6618
  throw error;
6226
6619
  });
@@ -6228,12 +6621,12 @@ async function uninstallStandalone() {
6228
6621
  }
6229
6622
  for (const warning of await removePath(record.pathChanges ?? [])) console.error(warning);
6230
6623
  if (process.platform === "win32") await replace(await storePath(home, ".uninstall-request.txt", true), token);
6231
- for (const name of Object.keys(record.launchers)) await fs22.rm(await storePath(record.bin, name), { force: true });
6624
+ for (const name of Object.keys(record.launchers)) await fs27.rm(await storePath(record.bin, name), { force: true });
6232
6625
  });
6233
- if (process.platform !== "win32") await fs22.rm(home, { recursive: true, force: true });
6626
+ if (process.platform !== "win32") await fs27.rm(home, { recursive: true, force: true });
6234
6627
  return home;
6235
6628
  }
6236
- var recordSchema3, shQuote, psQuote, WINDOWS_BATCH_LAUNCHER, launcherNames, defaultHome;
6629
+ var shQuote, psQuote, WINDOWS_BATCH_LAUNCHER, launcherNames;
6237
6630
  var init_install2 = __esm({
6238
6631
  "src/distribution/install.ts"() {
6239
6632
  "use strict";
@@ -6241,21 +6634,254 @@ var init_install2 = __esm({
6241
6634
  init_storage();
6242
6635
  init_store();
6243
6636
  init_path();
6244
- recordSchema3 = z25.object({
6245
- format: z25.literal(1),
6246
- home: z25.string(),
6247
- bin: z25.string(),
6248
- current: z25.string().regex(/^[a-f0-9]{24}$/),
6249
- version: z25.string(),
6250
- launchers: z25.record(z25.string()),
6251
- previousLaunchers: z25.record(z25.string()).optional(),
6252
- pathChanges: z25.array(pathChangeSchema).optional()
6253
- });
6637
+ init_state();
6638
+ init_sessions();
6639
+ init_state();
6254
6640
  shQuote = (s) => "'" + s.replace(/'/g, "'\\''") + "'";
6255
6641
  psQuote = (s) => "'" + s.replace(/'/g, "''") + "'";
6256
6642
  WINDOWS_BATCH_LAUNCHER = '@echo off\r\ngoto #_mason_handoff_# 2>NUL || title %COMSPEC% & powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "%~dp0mason-launcher.ps1" %*\r\n';
6257
6643
  launcherNames = ["mason", "mason.cmd", "mason.ps1", "mason-launcher.ps1"];
6258
- defaultHome = () => process.env.MASON_HOME ?? (process.platform === "win32" ? path28.join(process.env.LOCALAPPDATA ?? os4.homedir(), "Mason") : path28.join(os4.homedir(), ".local/share/mason"));
6644
+ }
6645
+ });
6646
+
6647
+ // src/distribution/updates.ts
6648
+ var updates_exports = {};
6649
+ __export(updates_exports, {
6650
+ CHECK_INTERVAL_MS: () => CHECK_INTERVAL_MS,
6651
+ RELEASE_AGE_MS: () => RELEASE_AGE_MS,
6652
+ fetchVerifiedRelease: () => fetchVerifiedRelease,
6653
+ runAutomaticUpdate: () => runAutomaticUpdate,
6654
+ scheduleAutomaticUpdate: () => scheduleAutomaticUpdate,
6655
+ startMcpWithUpdates: () => startMcpWithUpdates,
6656
+ updateManifestSchema: () => updateManifestSchema,
6657
+ updateSettings: () => updateSettings,
6658
+ updateStatus: () => updateStatus
6659
+ });
6660
+ import fs28 from "fs/promises";
6661
+ import path33 from "path";
6662
+ import { spawn as spawn2 } from "child_process";
6663
+ import { randomUUID as randomUUID11 } from "crypto";
6664
+ import { z as z28 } from "zod";
6665
+ async function boundedDownload(url) {
6666
+ const response = await fetch(url, { signal: AbortSignal.timeout(15e3), redirect: "follow" });
6667
+ if (!response.ok || !response.body) throw new Error(`Update metadata request failed (HTTP ${response.status}).`);
6668
+ if (Number(response.headers.get("content-length")) > 1024 * 1024) throw new Error("Update metadata is too large.");
6669
+ const reader = response.body.getReader(), chunks = [];
6670
+ let size = 0;
6671
+ try {
6672
+ for (; ; ) {
6673
+ const { value, done } = await reader.read();
6674
+ if (done) break;
6675
+ size += value.length;
6676
+ if (size > 1024 * 1024) throw new Error("Update metadata is too large.");
6677
+ chunks.push(value);
6678
+ }
6679
+ } finally {
6680
+ await reader.cancel();
6681
+ }
6682
+ return Buffer.concat(chunks);
6683
+ }
6684
+ async function fetchVerifiedRelease(home, now = Date.now()) {
6685
+ const releases = releaseListSchema.parse(JSON.parse((await boundedDownload("https://api.github.com/repos/adrianczuczka/mason/releases?per_page=30")).toString("utf8")));
6686
+ const installed = (await readInstallation(home)).version;
6687
+ let selected;
6688
+ for (const release of releases) {
6689
+ const version = release.tag_name.replace(/^v/, "");
6690
+ if (release.tag_name !== "v" + version || release.draft || release.prerelease || !release.published_at || !(now - Date.parse(release.published_at) >= RELEASE_AGE_MS) || !newerStableVersion(version, installed)) continue;
6691
+ if (!selected || newerStableVersion(version, selected)) selected = version;
6692
+ }
6693
+ if (!selected) return null;
6694
+ const bytes = await boundedDownload(`${releaseBase}/download/v${selected}/update.json`);
6695
+ const manifest = updateManifestSchema.parse(JSON.parse(bytes.toString("utf8")));
6696
+ if (manifest.version !== selected) throw new Error("Signed manifest does not match the selected release.");
6697
+ const signature = JSON.parse((await boundedDownload(`${releaseBase}/download/v${manifest.version}/update.sigstore.json`)).toString("utf8"));
6698
+ if (!signature?.messageSignature || signature.dsseEnvelope) throw new Error("Update metadata requires a detached message signature.");
6699
+ const { verify } = await import("sigstore");
6700
+ await verify(signature, bytes, {
6701
+ certificateIdentityURI: `https://github.com/adrianczuczka/mason/.github/workflows/publish.yml@refs/tags/v${manifest.version}`,
6702
+ certificateIssuer: "https://token.actions.githubusercontent.com",
6703
+ tufCachePath: await storePath(home, "sigstore-cache"),
6704
+ timeout: 1e4,
6705
+ retry: 0,
6706
+ ctLogThreshold: 1,
6707
+ tlogThreshold: 1
6708
+ });
6709
+ return manifest;
6710
+ }
6711
+ async function runInstaller(home, version, digest3, revision) {
6712
+ const record = await readInstallation(home);
6713
+ const bundle = await storePath(home, "versions/" + record.current);
6714
+ await verifyBundle(bundle);
6715
+ const windows = process.platform === "win32", script = path33.join(bundle, windows ? "install.ps1" : "install.sh");
6716
+ await new Promise((resolve, reject) => {
6717
+ const child = spawn2(windows ? "powershell.exe" : "sh", windows ? ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", script] : [script], {
6718
+ cwd: home,
6719
+ windowsHide: true,
6720
+ stdio: ["ignore", "ignore", "pipe"],
6721
+ timeout: 10 * 60 * 1e3,
6722
+ env: {
6723
+ ...process.env,
6724
+ MASON_HOME: home,
6725
+ MASON_BIN_DIR: record.bin,
6726
+ MASON_VERSION: version,
6727
+ MASON_RELEASE_BASE: "",
6728
+ MASON_EXPECTED_SHA256: digest3,
6729
+ MASON_UPDATE_POLICY_REVISION: revision
6730
+ }
6731
+ });
6732
+ let diagnostic = "";
6733
+ child.stderr.on("data", (bytes) => {
6734
+ diagnostic = (diagnostic + bytes).slice(-4e3);
6735
+ });
6736
+ child.on("error", reject);
6737
+ child.on("close", (code) => code === 0 ? resolve() : reject(new Error(`Update download failed (${code}): ${diagnostic.trim()}`)));
6738
+ });
6739
+ }
6740
+ async function runAutomaticUpdate(home, dependencies = { fetchRelease: fetchVerifiedRelease, install: runInstaller }, now = Date.now()) {
6741
+ await withLock(home, ".update-lock", async () => {
6742
+ const record = await readInstallation(home);
6743
+ if (!automaticUpdatesAllowed(record)) return;
6744
+ const old = statusSchema2.safeParse(await readStoreJson(home, "update-status.json"));
6745
+ if (old.success && now - old.data.lastAttempt < CHECK_INTERVAL_MS && old.data.lastAttempt <= now) return;
6746
+ const status = { lastAttempt: now };
6747
+ await writeStoreJson(home, "update-status.json", status);
6748
+ try {
6749
+ await pruneUnusedVersions(home).catch(() => {
6750
+ });
6751
+ const release = await dependencies.fetchRelease(home);
6752
+ if (!release) return;
6753
+ status.availableVersion = release.version;
6754
+ const age = now - Date.parse(release.publishedAt);
6755
+ if (age >= RELEASE_AGE_MS && newerStableVersion(release.version, record.version) && (!record.pending || newerStableVersion(release.version, record.pending.version))) {
6756
+ const asset = `mason-${process.platform}-${process.arch}.${process.platform === "win32" ? "zip" : "tar.gz"}`;
6757
+ const digest3 = release.assets[asset];
6758
+ if (!digest3) throw new Error("Signed release has no archive for this platform.");
6759
+ const fresh = await readInstallation(home);
6760
+ if (automaticUpdatesAllowed(fresh) && fresh.updates?.revision === record.updates.revision && newerStableVersion(release.version, fresh.version)) {
6761
+ await dependencies.install(home, release.version, digest3, record.updates.revision);
6762
+ }
6763
+ }
6764
+ } catch (error) {
6765
+ status.error = error instanceof Error ? error.message : String(error);
6766
+ }
6767
+ if (await fs28.access(path33.join(home, "install.json")).then(() => true, () => false)) await writeStoreJson(home, "update-status.json", status);
6768
+ }, 0);
6769
+ }
6770
+ async function scheduleAutomaticUpdate() {
6771
+ try {
6772
+ const { home, record } = await currentInstallation();
6773
+ if (!automaticUpdatesAllowed(record)) return;
6774
+ const status = statusSchema2.safeParse(await readStoreJson(home, "update-status.json"));
6775
+ const now = Date.now();
6776
+ if (status.success && status.data.lastAttempt <= now && now - status.data.lastAttempt < CHECK_INTERVAL_MS) return;
6777
+ const bundle = await storePath(home, "versions/" + record.current);
6778
+ const child = spawn2(
6779
+ path33.join(bundle, process.platform === "win32" ? "node.exe" : "node"),
6780
+ [path33.join(bundle, "app/dist/mason.js"), "internal-update"],
6781
+ { cwd: home, env: process.env, stdio: "ignore", detached: true, windowsHide: true }
6782
+ );
6783
+ child.on("error", () => {
6784
+ });
6785
+ child.unref();
6786
+ } catch {
6787
+ }
6788
+ }
6789
+ async function updateSettings(home, action) {
6790
+ return withLock(home, ".install-lock", async () => {
6791
+ const record = await readInstallation(home);
6792
+ const updates = { ...record.updates ?? { enabled: false }, revision: randomUUID11() };
6793
+ if (action === "enable") updates.enabled = true;
6794
+ if (action === "disable") updates.enabled = false;
6795
+ if (action === "pin") updates.pinnedVersion = record.version;
6796
+ if (action === "unpin") delete updates.pinnedVersion;
6797
+ record.updates = updates;
6798
+ delete record.pending;
6799
+ await writeStoreJson(home, "install.json", record);
6800
+ return record;
6801
+ });
6802
+ }
6803
+ async function updateStatus(home) {
6804
+ const record = await readInstallation(home);
6805
+ const lastCheck = statusSchema2.parse(await readStoreJson(home, "update-status.json") ?? { lastAttempt: 0 });
6806
+ return {
6807
+ version: record.version,
6808
+ enabled: record.updates?.enabled ?? false,
6809
+ effective: automaticUpdatesAllowed(record),
6810
+ pinnedVersion: record.updates?.pinnedVersion ?? null,
6811
+ mirror: record.updates?.mirror ?? null,
6812
+ pendingVersion: record.pending?.version ?? null,
6813
+ lastCheck: lastCheck.lastAttempt ? lastCheck : null
6814
+ };
6815
+ }
6816
+ async function startMcpWithUpdates(start) {
6817
+ const current = standaloneLocation();
6818
+ if (!current) {
6819
+ await start();
6820
+ return;
6821
+ }
6822
+ const session = await prepareStandaloneMcp(current.home);
6823
+ if (session.bundle !== current.bundle) {
6824
+ const child = spawn2(
6825
+ path33.join(session.bundle, process.platform === "win32" ? "node.exe" : "node"),
6826
+ [path33.join(session.bundle, "app/dist/mason-mcp.js"), ...process.argv.slice(2)],
6827
+ { stdio: "inherit", windowsHide: true }
6828
+ );
6829
+ const interrupt = () => {
6830
+ child.kill("SIGINT");
6831
+ }, terminate = () => {
6832
+ child.kill("SIGTERM");
6833
+ };
6834
+ process.on("SIGINT", interrupt);
6835
+ process.on("SIGTERM", terminate);
6836
+ try {
6837
+ process.exitCode = await new Promise((resolve, reject) => {
6838
+ child.on("error", reject);
6839
+ child.on("exit", (code) => resolve(code ?? 1));
6840
+ });
6841
+ } finally {
6842
+ process.off("SIGINT", interrupt);
6843
+ process.off("SIGTERM", terminate);
6844
+ await session.release();
6845
+ }
6846
+ return;
6847
+ }
6848
+ const timer = setInterval(() => {
6849
+ void scheduleAutomaticUpdate();
6850
+ }, 60 * 60 * 1e3);
6851
+ timer.unref();
6852
+ const stop = () => {
6853
+ clearInterval(timer);
6854
+ void session?.release();
6855
+ };
6856
+ try {
6857
+ await start(stop);
6858
+ void scheduleAutomaticUpdate();
6859
+ } catch (error) {
6860
+ stop();
6861
+ throw error;
6862
+ }
6863
+ }
6864
+ var CHECK_INTERVAL_MS, RELEASE_AGE_MS, releaseBase, releaseListSchema, updateManifestSchema, statusSchema2;
6865
+ var init_updates = __esm({
6866
+ "src/distribution/updates.ts"() {
6867
+ "use strict";
6868
+ init_store();
6869
+ init_storage();
6870
+ init_state();
6871
+ init_bundle();
6872
+ init_runtime2();
6873
+ init_install2();
6874
+ CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
6875
+ RELEASE_AGE_MS = 24 * 60 * 60 * 1e3;
6876
+ releaseBase = "https://github.com/adrianczuczka/mason/releases";
6877
+ releaseListSchema = z28.array(z28.object({ tag_name: z28.string(), draft: z28.boolean(), prerelease: z28.boolean(), published_at: z28.string().nullable() }));
6878
+ updateManifestSchema = z28.object({
6879
+ format: z28.literal(1),
6880
+ version: z28.string().regex(/^\d+\.\d+\.\d+$/),
6881
+ publishedAt: z28.string().datetime(),
6882
+ assets: z28.record(z28.string().regex(/^[a-f0-9]{64}$/))
6883
+ }).strict();
6884
+ statusSchema2 = z28.object({ lastAttempt: z28.number(), availableVersion: z28.string().optional(), error: z28.string().optional() });
6259
6885
  }
6260
6886
  });
6261
6887
 
@@ -6452,7 +7078,7 @@ var inputArgs = process.argv.slice(2);
6452
7078
  var managedLaunch = inputArgs[0] === "--setup-host";
6453
7079
  var setupHost = managedLaunch ? inputArgs.splice(0, 2)[1] : void 0;
6454
7080
  var [command, ...args] = inputArgs;
6455
- var usage = `Mason ${"0.18.0"}
7081
+ var usage = `Mason ${"0.19.0"}
6456
7082
 
6457
7083
  Usage: mason <command> [options]
6458
7084
 
@@ -6465,6 +7091,9 @@ Usage: mason <command> [options]
6465
7091
  drift Check an optional architecture map
6466
7092
  mcp Run the MCP server (stdio)
6467
7093
  upgrade [version] Upgrade the standalone user installation
7094
+ updates [status|enable|disable|pin|unpin]
7095
+ Manage automatic standalone updates
7096
+ rollback Restore and pin the previous standalone version
6468
7097
  uninstall Remove that installation; retain project data
6469
7098
 
6470
7099
  Use mason <command> --help for options. npm users can keep using mason-auto,
@@ -6478,19 +7107,67 @@ try {
6478
7107
  await prepareLaunch2(setupHost, { allowInactive: command === "mcp" });
6479
7108
  }
6480
7109
  }
6481
- if (command === "internal-integration-version") console.log(JSON.stringify({ protocol: 1, version: "0.18.0" }));
7110
+ if (command === "internal-integration-version") console.log(JSON.stringify({ protocol: 1, version: "0.19.0" }));
6482
7111
  else if (!command || ["--help", "-h", "help"].includes(command)) console.log(usage);
6483
- else if (["--version", "-v"].includes(command)) console.log("0.18.0");
6484
- else if (command === "internal-install") {
7112
+ else if (["--version", "-v"].includes(command)) console.log("0.19.0");
7113
+ else if (command === "internal-update") {
7114
+ if (args.length) throw new Error("internal-update takes no arguments.");
7115
+ const { currentInstallation: currentInstallation2 } = await Promise.resolve().then(() => (init_state(), state_exports));
7116
+ const { runAutomaticUpdate: runAutomaticUpdate2 } = await Promise.resolve().then(() => (init_updates(), updates_exports));
7117
+ await runAutomaticUpdate2((await currentInstallation2()).home);
7118
+ } else if (command === "internal-install") {
6485
7119
  if (args.length) throw new Error("internal-install takes no arguments.");
6486
7120
  const { installStandalone: installStandalone2 } = await Promise.resolve().then(() => (init_install2(), install_exports2));
6487
7121
  const bundleRoot = fileURLToPath2(new URL("../..", import.meta.url));
6488
7122
  progress = createProgress();
6489
- const installed = await installStandalone2(bundleRoot, progress);
7123
+ const policyRevision = process.env.MASON_UPDATE_POLICY_REVISION;
7124
+ if (policyRevision && !/^[a-f0-9]{64}$/.test(process.env.MASON_EXPECTED_SHA256 ?? "")) throw new Error("Automatic staging requires a verified release checksum.");
7125
+ const installed = await installStandalone2(bundleRoot, progress, { stageOnly: !!policyRevision, policyRevision });
6490
7126
  progress.stop();
6491
7127
  const outcome = installed.previousVersion && installed.previousVersion !== installed.version ? `Updated Mason ${installed.previousVersion} \u2192 ${installed.version}` : `Installed Mason ${installed.version}`;
6492
- console.log(`${outcome} in ${installed.home}.
6493
- ${installed.path.message}`);
7128
+ if (policyRevision) console.log(`Staged Mason ${installed.version}. It will activate at a future MCP launch after existing servers exit.`);
7129
+ else {
7130
+ const { updateStatus: updateStatus2 } = await Promise.resolve().then(() => (init_updates(), updates_exports));
7131
+ const status = await updateStatus2(installed.home);
7132
+ const policy = status.pinnedVersion ? `pinned to ${status.pinnedVersion}` : status.mirror ? "managed by your mirror" : status.enabled ? "enabled" : "disabled";
7133
+ console.log(`${outcome} in ${installed.home}.
7134
+ ${installed.path.message}
7135
+ Automatic updates: ${policy}. Manage with mason updates.`);
7136
+ }
7137
+ } else if (command === "updates") {
7138
+ if (args.includes("--help")) console.log("Usage: mason updates [status|enable|disable|pin|unpin] [--json]. Pin holds the installed version; mason upgrade <version> installs and pins a specific release. Environment and mirror restrictions still apply.");
7139
+ else {
7140
+ const action = args.filter((arg) => arg !== "--json")[0] ?? "status";
7141
+ if (args.filter((arg) => arg !== "--json").length > 1 || !["status", "enable", "disable", "pin", "unpin"].includes(action)) throw new Error("Usage: mason updates [status|enable|disable|pin|unpin] [--json]");
7142
+ const { currentInstallation: currentInstallation2 } = await Promise.resolve().then(() => (init_state(), state_exports));
7143
+ const { updateSettings: updateSettings2, updateStatus: updateStatus2, scheduleAutomaticUpdate: scheduleAutomaticUpdate2 } = await Promise.resolve().then(() => (init_updates(), updates_exports));
7144
+ const { home } = await currentInstallation2();
7145
+ if (action !== "status") await updateSettings2(home, action);
7146
+ const status = await updateStatus2(home);
7147
+ if (args.includes("--json")) console.log(JSON.stringify(status));
7148
+ else {
7149
+ console.log(`Mason ${status.version}
7150
+ Automatic updates: ${status.effective ? "enabled" : "inactive"} (preference: ${status.enabled ? "enabled" : "disabled"}).`);
7151
+ if (status.pinnedVersion) console.log(`Pinned to ${status.pinnedVersion}. Run mason updates unpin to follow stable releases.`);
7152
+ if (status.mirror) console.log("Company mirror installation: automatic public updates are disabled.");
7153
+ if (!status.effective && status.enabled && !status.pinnedVersion && !status.mirror) console.log("Suppressed by CI, MASON_NO_AUTO_UPDATE, MASON_VERSION, or MASON_RELEASE_BASE.");
7154
+ if (status.pendingVersion) console.log(`Downloaded ${status.pendingVersion}; activates on a future MCP launch after existing servers exit.`);
7155
+ if (status.lastCheck) {
7156
+ console.log(`Last check: ${new Date(status.lastCheck.lastAttempt).toISOString()}`);
7157
+ if (status.lastCheck.availableVersion) console.log(`Available stable release: ${status.lastCheck.availableVersion}`);
7158
+ if (status.lastCheck.error) console.log(`Last update problem: ${status.lastCheck.error}`);
7159
+ }
7160
+ }
7161
+ if (action === "enable" || action === "unpin") void scheduleAutomaticUpdate2();
7162
+ }
7163
+ } else if (command === "rollback") {
7164
+ if (args.includes("--help")) console.log("Usage: mason rollback. Close running Mason MCP servers, then restore and pin the previous installed version.");
7165
+ else {
7166
+ if (args.length) throw new Error("rollback takes no arguments.");
7167
+ const { rollbackStandalone: rollbackStandalone2 } = await Promise.resolve().then(() => (init_install2(), install_exports2));
7168
+ const restored = await rollbackStandalone2();
7169
+ console.log(`Restored Mason ${restored.version} and pinned it. Run mason updates unpin to resume automatic updates.`);
7170
+ }
6494
7171
  } else if (command === "upgrade" || command === "uninstall") {
6495
7172
  if (args.includes("--help")) console.log(command === "upgrade" ? "Usage: mason upgrade [version]. Configured projects use the upgraded command on their next launch. Restart running assistants." : "Usage: mason uninstall. Removes the standalone user installation; project configuration and knowledge are retained. Run mason teardown in each project first to disconnect its integrations.");
6496
7173
  else {
@@ -6515,6 +7192,10 @@ ${installed.path.message}`);
6515
7192
  void 0,
6516
7193
  { managedHost: managedLaunch ? setupHost : void 0 }
6517
7194
  );
7195
+ if (command === "setup" && process.exitCode === 0 && !args.includes("--help")) {
7196
+ const { scheduleAutomaticUpdate: scheduleAutomaticUpdate2 } = await Promise.resolve().then(() => (init_updates(), updates_exports));
7197
+ void scheduleAutomaticUpdate2();
7198
+ }
6518
7199
  } else if (["audit", "review", "drift", "hook", "mcp"].includes(command)) {
6519
7200
  const binary = new URL(`./mason-${command}.js`, import.meta.url);
6520
7201
  process.argv = [process.execPath, fileURLToPath2(binary), ...args];