mason-context 0.14.0 → 0.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.14.1 — 2026-09-07
4
+
5
+ - Configure PATH automatically during standalone installation for Bash, Zsh, POSIX sh, and Windows. Supported installations finish with a simple instruction to open a new terminal and run setup.
6
+ - Preserve existing shell settings, profile symlinks, Windows PATH entries and registry value types. Track owned additions across repeat installs and upgrades; uninstall removes unchanged additions and retains user edits.
7
+ - Avoid a same-name PowerShell script shadowing the Windows command under restricted script policy. Migrate the old owned helper without changing user or machine execution policy.
8
+ - Support `MASON_PROFILE` for custom shell profiles and `MASON_NO_MODIFY_PATH=1` for opt-out. Unsupported or unwritable settings receive explicit manual instructions.
9
+ - Test fresh-terminal discovery, duplicate prevention, and PATH cleanup in native package checks. Bound retries for transient Windows test-directory locks and report smoke success only after teardown completes.
10
+
11
+ Run the [installer](docs/distribution.md) again to upgrade from 0.14.0 and configure PATH. Existing project integrations remain pinned until you rerun `mason setup --host codex` or `--host claude` in each project.
12
+
3
13
  ## 0.14.0 — 2026-09-07
4
14
 
5
15
  Install Mason without installing Node or npm. Standalone downloads bundle the runtime for macOS, Linux, and Windows.
package/README.md CHANGED
@@ -26,9 +26,9 @@ Windows PowerShell:
26
26
  irm https://github.com/adrianczuczka/mason/releases/latest/download/install.ps1 | iex
27
27
  ```
28
28
 
29
- No Node or npm required. Git is required. Follow the installer's PATH instructions. [Platform details](docs/distribution.md) · [npm installation](docs/setup.md#unified-project-setup)
29
+ No Node or npm required. Git is required. The installer configures PATH for Bash, Zsh, and Windows. [Platform details](docs/distribution.md) · [npm installation](docs/setup.md#unified-project-setup)
30
30
 
31
- Run this in your Git repository:
31
+ Open a new terminal, then run this in your Git repository:
32
32
 
33
33
  ```bash
34
34
  mason setup --host codex
@@ -2398,7 +2398,7 @@ var init_evidence = __esm({
2398
2398
  init_dead_command();
2399
2399
  init_storage();
2400
2400
  exec7 = promisify7(execFile7);
2401
- engineVersion = true ? "0.14.0" : "development";
2401
+ engineVersion = true ? "0.14.1" : "development";
2402
2402
  hash = (value) => createHash3("sha256").update(JSON.stringify(value)).digest("hex");
2403
2403
  internal = (file) => file === ".mason" || file === ".mason/reports" || file.startsWith(".mason/reports/");
2404
2404
  cacheSchema = z5.object({ version: z5.literal(1), entries: z5.record(z5.object({ key: z5.string(), result: checkResultSchema })), digest: z5.string() });
package/dist/mason-mcp.js CHANGED
@@ -4163,7 +4163,7 @@ var init_evidence2 = __esm({
4163
4163
  init_dead_command();
4164
4164
  init_storage();
4165
4165
  exec14 = promisify14(execFile14);
4166
- engineVersion = true ? "0.14.0" : "development";
4166
+ engineVersion = true ? "0.14.1" : "development";
4167
4167
  hash = (value) => createHash4("sha256").update(JSON.stringify(value)).digest("hex");
4168
4168
  internal = (file) => file === ".mason" || file === ".mason/reports" || file.startsWith(".mason/reports/");
4169
4169
  cacheSchema = z11.object({ version: z11.literal(1), entries: z11.record(z11.object({ key: z11.string(), result: checkResultSchema })), digest: z11.string() });
@@ -8113,7 +8113,7 @@ function createMcpServer() {
8113
8113
  const server = new McpServer(
8114
8114
  {
8115
8115
  name: "mason",
8116
- version: "0.14.0"
8116
+ version: "0.14.1"
8117
8117
  },
8118
8118
  {
8119
8119
  instructions: 'Mason retrieves recorded decisions, file impact, and optional feature/flow maps. Use get_context with the task and known files, and get_impact before editing. Save learned rationale and constraints with save_decision; review and commit the local records through the project workflow. These tools work without initialization or a concept map. Consult trust and diagnostics: changed or unknown freshness needs source inspection, failed verification needs correction, and proposals are suggestions and legacy records are unreviewed. Accepted decisions are recorded team constraints, still subject to freshness checks. Use review_decision to prepare code evidence and record only authorized acceptance, reaffirmation, or retirement. Never invent a reviewer or treat these recorded identities as authenticated approval. mason_init returns documentation audit and committed-diff review findings with a quickstart guide. Use mode: "map" only when a full architecture map is requested. A missing map is not a setup failure; use decisions and source evidence. get_snapshot provides architecture navigation when a map is available. The mason-audit and mason-review CLIs also work without setup.'
package/dist/mason.js CHANGED
@@ -262,15 +262,15 @@ function parseExecution(raw) {
262
262
  function automationFailure(error) {
263
263
  const recorded = failureSchema.safeParse(error?.failure);
264
264
  if (recorded.success) return recorded.data;
265
- const message2 = (error instanceof Error ? error.message : String(error)).replace(/[\u0000-\u001f\u007f-\u009f]/g, " ").slice(0, 700);
265
+ const message3 = (error instanceof Error ? error.message : String(error)).replace(/[\u0000-\u001f\u007f-\u009f]/g, " ").slice(0, 700);
266
266
  const codes = /* @__PURE__ */ new Set();
267
267
  let cause = error;
268
268
  for (let i = 0; cause && i < 8; i++) {
269
269
  codes.add(String(cause.code));
270
270
  cause = cause.cause;
271
271
  }
272
- const code = codes.has("ENOSPC") || codes.has("EDQUOT") ? "storage-full" : /changed during|changed while|changed between/.test(message2) ? "inputs-changed" : /Automation is busy/.test(message2) ? "busy" : /not a git repository|unknown revision|bad revision|different history|unreachable/.test(message2) ? "history-unavailable" : error instanceof z.ZodError || error instanceof SyntaxError || /Hook input|Expected one command|Unknown automation command|--host/.test(message2) ? "invalid-input" : /store|baseline|modified|symbolic link|Symlink|automation state|state belongs/.test(message2) ? "invalid-evidence" : [...codes].some((c) => /^E[A-Z]+$/.test(c)) ? "io-error" : "internal";
273
- return { code, message: message2, retryable: ["inputs-changed", "storage-full", "busy", "io-error"].includes(code), receiptRecorded: false };
272
+ const code = codes.has("ENOSPC") || codes.has("EDQUOT") ? "storage-full" : /changed during|changed while|changed between/.test(message3) ? "inputs-changed" : /Automation is busy/.test(message3) ? "busy" : /not a git repository|unknown revision|bad revision|different history|unreachable/.test(message3) ? "history-unavailable" : error instanceof z.ZodError || error instanceof SyntaxError || /Hook input|Expected one command|Unknown automation command|--host/.test(message3) ? "invalid-input" : /store|baseline|modified|symbolic link|Symlink|automation state|state belongs/.test(message3) ? "invalid-evidence" : [...codes].some((c) => /^E[A-Z]+$/.test(c)) ? "io-error" : "internal";
273
+ return { code, message: message3, retryable: ["inputs-changed", "storage-full", "busy", "io-error"].includes(code), receiptRecorded: false };
274
274
  }
275
275
  function failureMessage(error) {
276
276
  const failure = automationFailure(error);
@@ -1736,7 +1736,7 @@ var init_provenance = __esm({
1736
1736
  sources: z3.array(decisionSourceSchema).max(20),
1737
1737
  history: z3.array(eventSchema).min(1)
1738
1738
  }).superRefine((record, ctx) => {
1739
- const invalid = (message2) => ctx.addIssue({ code: "custom", message: message2 });
1739
+ const invalid = (message3) => ctx.addIssue({ code: "custom", message: message3 });
1740
1740
  const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
1741
1741
  let previous;
1742
1742
  for (const event of record.history) {
@@ -2398,7 +2398,7 @@ var init_evidence = __esm({
2398
2398
  init_dead_command();
2399
2399
  init_storage();
2400
2400
  exec7 = promisify7(execFile7);
2401
- engineVersion = true ? "0.14.0" : "development";
2401
+ engineVersion = true ? "0.14.1" : "development";
2402
2402
  hash = (value) => createHash3("sha256").update(JSON.stringify(value)).digest("hex");
2403
2403
  internal = (file) => file === ".mason" || file === ".mason/reports" || file.startsWith(".mason/reports/");
2404
2404
  cacheSchema = z5.object({ version: z5.literal(1), entries: z5.record(z5.object({ key: z5.string(), result: checkResultSchema })), digest: z5.string() });
@@ -2897,8 +2897,8 @@ async function runAutomationHook(host, stdin) {
2897
2897
  }
2898
2898
  return { hookSpecificOutput: { hookEventName: name, additionalContext: result.message } };
2899
2899
  } catch (error) {
2900
- const message2 = failureMessage(error);
2901
- return { systemMessage: message2, ...["SessionStart", "UserPromptSubmit", "PreToolUse", "PostToolUse"].includes(name) ? { hookSpecificOutput: { hookEventName: name, additionalContext: message2 } } : {} };
2900
+ const message3 = failureMessage(error);
2901
+ return { systemMessage: message3, ...["SessionStart", "UserPromptSubmit", "PreToolUse", "PostToolUse"].includes(name) ? { hookSpecificOutput: { hookEventName: name, additionalContext: message3 } } : {} };
2902
2902
  }
2903
2903
  }
2904
2904
  function hookConfig(host, command2 = "npx --no-install --package mason-context mason-auto") {
@@ -3217,8 +3217,8 @@ var init_paths2 = __esm({
3217
3217
  });
3218
3218
 
3219
3219
  // src/review/evidence/types.ts
3220
- function messagePreview(message2) {
3221
- return { message: message2.slice(0, 4e3), ...message2.length > 4e3 ? { truncated: true } : {} };
3220
+ function messagePreview(message3) {
3221
+ return { message: message3.slice(0, 4e3), ...message3.length > 4e3 ? { truncated: true } : {} };
3222
3222
  }
3223
3223
  var MAX_FINDINGS;
3224
3224
  var init_types2 = __esm({
@@ -4363,7 +4363,7 @@ function summarizeActivation(status) {
4363
4363
  ` Runtime: ${state.runtime}; MCP: ${state.mcp}; instructions: ${state.instructions}.`,
4364
4364
  ` Hooks: ${state.hookConfiguration}; observed: ${state.observedEvents.join(", ") || "none"}.`,
4365
4365
  ` Task context requests: ${state.contextCalls}; verification: ${state.verificationStatus}.`,
4366
- ...state.pending.map((message2) => " Next: " + message2)
4366
+ ...state.pending.map((message3) => " Next: " + message3)
4367
4367
  );
4368
4368
  }
4369
4369
  if ("decisionRecords" in status) lines.push(`Decision records: ${status.decisionRecords}.`);
@@ -4520,6 +4520,185 @@ var init_setup = __esm({
4520
4520
  }
4521
4521
  });
4522
4522
 
4523
+ // src/distribution/path.ts
4524
+ import fs18 from "fs/promises";
4525
+ import os3 from "os";
4526
+ import path23 from "path";
4527
+ import { execFile as execFile11 } from "child_process";
4528
+ import { promisify as promisify11 } from "util";
4529
+ import { createHash as createHash6, randomUUID as randomUUID8 } from "crypto";
4530
+ import { z as z17 } from "zod";
4531
+ async function configurePath(bin, previous, save, options = {}) {
4532
+ const env = options.env ?? process.env, platform = options.platform ?? process.platform;
4533
+ const changes = [...previous];
4534
+ const manual = (reason2) => ({ status: "manual", changes, message: `${reason2}
4535
+ Add ${bin} to your PATH, then run: mason setup --host codex` });
4536
+ if (env.MASON_NO_MODIFY_PATH === "1") return manual("Automatic PATH setup is disabled (MASON_NO_MODIFY_PATH=1).");
4537
+ if (/[\r\n\0]/.test(bin) || bin.includes(platform === "win32" ? ";" : ":")) return manual("This installation path cannot be represented safely in PATH.");
4538
+ try {
4539
+ if (platform === "win32") {
4540
+ const current = await windowsPath("read");
4541
+ if (!hasWindowsEntry(current.value, bin)) {
4542
+ if (!changes.some((c) => c.kind === "windows" && c.entry === bin)) {
4543
+ changes.push({ kind: "windows", entry: bin, created: current.value === null });
4544
+ await save(changes);
4545
+ }
4546
+ await windowsPath("add", bin, current);
4547
+ }
4548
+ return { status: "configured", changes, message: message2 };
4549
+ }
4550
+ const shell = path23.basename(env.SHELL ?? os3.userInfo().shell ?? "");
4551
+ const userHome = options.userHome ?? os3.homedir();
4552
+ let profiles;
4553
+ if (!["bash", "zsh", "sh"].includes(shell)) return manual(`Automatic PATH setup is unavailable for ${shell || "an unknown shell"}.`);
4554
+ if (env.MASON_PROFILE) profiles = [path23.resolve(env.MASON_PROFILE)];
4555
+ else if (shell === "zsh") profiles = [path23.join(env.ZDOTDIR || userHome, ".zshrc")];
4556
+ else if (shell === "sh") profiles = [path23.join(userHome, ".profile")];
4557
+ else {
4558
+ const candidates = [".bash_profile", ".bash_login", ".profile"].map((name) => path23.join(userHome, name));
4559
+ let login = candidates[2];
4560
+ for (const file of candidates) if (await fs18.lstat(file).then(() => true, (error) => {
4561
+ if (error.code === "ENOENT") return false;
4562
+ throw error;
4563
+ })) {
4564
+ login = file;
4565
+ break;
4566
+ }
4567
+ profiles = [path23.join(userHome, ".bashrc"), login];
4568
+ }
4569
+ const marker = `# >>> mason PATH ${createHash6("sha256").update(bin).digest("hex").slice(0, 12)} >>>`;
4570
+ for (const profile of profiles) {
4571
+ const stat = await fs18.lstat(profile).catch((error) => {
4572
+ if (error.code === "ENOENT") return null;
4573
+ throw error;
4574
+ });
4575
+ const file = stat?.isSymbolicLink() ? await fs18.realpath(profile) : path23.resolve(profile);
4576
+ const original = await fs18.readFile(file).catch((error) => {
4577
+ if (error.code === "ENOENT" && !stat) return null;
4578
+ throw error;
4579
+ });
4580
+ if (original && !Buffer.from(original.toString("utf8")).equals(original)) return manual(`Shell profile is not UTF-8: ${profile}.`);
4581
+ const text2 = original?.toString("utf8") ?? "";
4582
+ const owned = changes.find((c) => c.kind === "profile" && c.file === file);
4583
+ if (owned && text2.includes(owned.block)) continue;
4584
+ if (text2.includes(marker) || owned && text2.includes("# <<< mason PATH")) return manual(`Mason's PATH block was edited; retained ${profile}.`);
4585
+ const newline = text2.includes("\r\n") ? "\r\n" : "\n";
4586
+ const block = owned?.block ?? ("\n" + marker + '\ncase ":${PATH-}:" in\n *:' + quote(bin) + ":*) ;;\n *) export PATH=" + quote(bin) + ':"${PATH-}" ;;\nesac\n# <<< mason PATH <<<\n').replaceAll("\n", newline);
4587
+ if (!owned) {
4588
+ changes.push({ kind: "profile", file, block, created: original === null });
4589
+ await save(changes);
4590
+ }
4591
+ await fs18.mkdir(path23.dirname(file), { recursive: true });
4592
+ const handle = await fs18.open(file, original === null ? "ax+" : "a+", 384);
4593
+ try {
4594
+ if (!(await handle.readFile()).equals(original ?? Buffer.alloc(0))) throw new Error(`Shell profile changed during installation: ${profile}`);
4595
+ await handle.writeFile(block);
4596
+ await handle.sync();
4597
+ } finally {
4598
+ await handle.close();
4599
+ }
4600
+ }
4601
+ return { status: "configured", changes, message: message2 };
4602
+ } catch (error) {
4603
+ return manual(`Could not configure PATH: ${error instanceof Error ? error.message : String(error)}`);
4604
+ }
4605
+ }
4606
+ async function removePath(changes) {
4607
+ const warnings = [];
4608
+ for (const change of changes) {
4609
+ try {
4610
+ if (change.kind === "windows") {
4611
+ const current = await windowsPath("read");
4612
+ if (!(current.value ?? "").split(";").includes(change.entry) && hasWindowsEntry(current.value, change.entry)) throw new Error("PATH entry was edited");
4613
+ await windowsPath("remove", change.entry, current, change.created);
4614
+ continue;
4615
+ }
4616
+ const stat = await fs18.lstat(change.file).catch((error) => {
4617
+ if (error.code === "ENOENT") return null;
4618
+ throw error;
4619
+ });
4620
+ if (!stat) continue;
4621
+ if (!stat.isFile()) throw new Error("profile type changed");
4622
+ const bytes = await fs18.readFile(change.file), text2 = bytes.toString("utf8");
4623
+ if (!Buffer.from(text2).equals(bytes)) throw new Error("profile encoding changed");
4624
+ const index2 = text2.indexOf(change.block);
4625
+ if (index2 < 0 || text2.indexOf(change.block, index2 + change.block.length) >= 0) throw new Error("PATH block was edited or removed");
4626
+ const prefix = text2.slice(0, index2), suffix = text2.slice(index2 + change.block.length);
4627
+ const separator = prefix && suffix && !prefix.endsWith("\n") ? change.block.startsWith("\r\n") ? "\r\n" : "\n" : "";
4628
+ const next = Buffer.from(prefix + separator + suffix);
4629
+ const temporary = change.file + ".mason-" + randomUUID8();
4630
+ try {
4631
+ if (stat.nlink > 1) throw new Error("profile has multiple hard links");
4632
+ const handle = await fs18.open(temporary, "wx", stat.mode & 511);
4633
+ try {
4634
+ await handle.writeFile(next);
4635
+ await handle.chmod(stat.mode & 511);
4636
+ if (process.platform !== "win32") await handle.chown(stat.uid, stat.gid);
4637
+ await handle.sync();
4638
+ } finally {
4639
+ await handle.close();
4640
+ }
4641
+ const current = await fs18.lstat(change.file);
4642
+ if (!current.isFile() || current.ino !== stat.ino || !(await fs18.readFile(change.file)).equals(bytes)) throw new Error("profile changed during uninstall");
4643
+ await fs18.rename(temporary, change.file);
4644
+ } finally {
4645
+ await fs18.rm(temporary, { force: true });
4646
+ }
4647
+ } catch (error) {
4648
+ warnings.push(`Retained PATH setting ${change.kind === "profile" ? change.file : change.entry}: ${error instanceof Error ? error.message : String(error)}`);
4649
+ }
4650
+ }
4651
+ return warnings;
4652
+ }
4653
+ async function windowsPath(action, entry = "", expected, removeEmpty = false) {
4654
+ const payload = Buffer.from(JSON.stringify({ action, entry, expected, removeEmpty })).toString("base64");
4655
+ const script = `$ErrorActionPreference = 'Stop'
4656
+ [Console]::OutputEncoding = New-Object Text.UTF8Encoding($false)
4657
+ $p = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${payload}')) | ConvertFrom-Json
4658
+ $key = [Microsoft.Win32.Registry]::CurrentUser.CreateSubKey('Environment')
4659
+ try {
4660
+ $value = $key.GetValue('Path', $null, [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames)
4661
+ $kind = if ($null -eq $value) { $null } else { $key.GetValueKind('Path').ToString() }
4662
+ if ($null -ne $kind -and $kind -notin @('String','ExpandString')) { throw 'User PATH is not a string.' }
4663
+ if ($p.action -eq 'read') { @{ value = $value; kind = $kind } | ConvertTo-Json -Compress; exit 0 }
4664
+ if ($p.expected -and ($value -cne $p.expected.value -or $kind -cne $p.expected.kind)) { throw 'User PATH changed concurrently; retry the operation.' }
4665
+ $parts = if ($null -eq $value -or $value -eq '') { @() } else { @($value.Split(';')) }
4666
+ if ($p.action -eq 'add') { $next = (@($p.entry) + $parts) -join ';' }
4667
+ else { $next = (@($parts | Where-Object { $_ -cne $p.entry })) -join ';' }
4668
+ if ($next -cne $value) {
4669
+ $writeKind = if ($kind) { [Microsoft.Win32.RegistryValueKind]$kind } else { [Microsoft.Win32.RegistryValueKind]::ExpandString }
4670
+ if ($p.action -eq 'remove' -and $p.removeEmpty -and $next -eq '') { $key.DeleteValue('Path', $false) }
4671
+ else { $key.SetValue('Path', $next, $writeKind) }
4672
+ Add-Type -TypeDefinition 'using System; using System.Runtime.InteropServices; public class MasonEnvironment { [DllImport("user32.dll", CharSet=CharSet.Unicode, SetLastError=true)] public static extern IntPtr SendMessageTimeout(IntPtr h, uint m, UIntPtr w, string l, uint f, uint t, out UIntPtr r); }'
4673
+ $result = [UIntPtr]::Zero
4674
+ [void][MasonEnvironment]::SendMessageTimeout([IntPtr]0xffff, 0x1a, [UIntPtr]::Zero, 'Environment', 2, 1000, [ref]$result)
4675
+ }
4676
+ @{ value = $next; kind = $kind } | ConvertTo-Json -Compress
4677
+ } finally { $key.Dispose() }
4678
+ `;
4679
+ const { stdout } = await execute("powershell.exe", ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-EncodedCommand", Buffer.from(script, "utf16le").toString("base64")], { windowsHide: true, timeout: 15e3, maxBuffer: 1024 * 1024 });
4680
+ return JSON.parse(stdout.replace(/^\uFEFF/, "").trim());
4681
+ }
4682
+ var execute, pathChangeSchema, quote, message2, normalizeWindowsEntry, hasWindowsEntry;
4683
+ var init_path = __esm({
4684
+ "src/distribution/path.ts"() {
4685
+ "use strict";
4686
+ execute = promisify11(execFile11);
4687
+ pathChangeSchema = z17.discriminatedUnion("kind", [
4688
+ z17.object({ kind: z17.literal("profile"), file: z17.string(), block: z17.string(), created: z17.boolean() }),
4689
+ z17.object({ kind: z17.literal("windows"), entry: z17.string(), created: z17.boolean() })
4690
+ ]);
4691
+ quote = (s) => "'" + s.replace(/'/g, "'\\''") + "'";
4692
+ message2 = "Open a new terminal, then run: mason setup --host codex";
4693
+ normalizeWindowsEntry = (value) => path23.win32.normalize(value.trim().replace(/^"(.*)"$/, "$1")).replace(/[\\/]+$/, "").toLowerCase();
4694
+ hasWindowsEntry = (value, entry, env = process.env) => {
4695
+ const variables = new Map(Object.entries(env).map(([key, value2]) => [key.toLowerCase(), value2]));
4696
+ const expand = (part) => part.replace(/%([^%]+)%/g, (match, key) => variables.get(key.toLowerCase()) ?? match);
4697
+ return (value ?? "").split(";").some((part) => normalizeWindowsEntry(expand(part)) === normalizeWindowsEntry(entry));
4698
+ };
4699
+ }
4700
+ });
4701
+
4523
4702
  // src/distribution/install.ts
4524
4703
  var install_exports2 = {};
4525
4704
  __export(install_exports2, {
@@ -4529,55 +4708,55 @@ __export(install_exports2, {
4529
4708
  uninstallStandalone: () => uninstallStandalone,
4530
4709
  upgradeStandalone: () => upgradeStandalone
4531
4710
  });
4532
- import fs18 from "fs/promises";
4533
- import os3 from "os";
4534
- import path23 from "path";
4711
+ import fs19 from "fs/promises";
4712
+ import os4 from "os";
4713
+ import path24 from "path";
4535
4714
  import { fileURLToPath as fileURLToPath2 } from "url";
4536
4715
  import { spawn } from "child_process";
4537
- import { randomUUID as randomUUID8 } from "crypto";
4538
- import { z as z17 } from "zod";
4716
+ import { randomUUID as randomUUID9 } from "crypto";
4717
+ import { z as z18 } from "zod";
4539
4718
  async function replace(file, content2, mode = 384) {
4540
- const temp = file + ".tmp-" + randomUUID8();
4719
+ const temp = file + ".tmp-" + randomUUID9();
4541
4720
  try {
4542
- await fs18.writeFile(temp, content2, { mode, flag: "wx" });
4543
- await fs18.rename(temp, file);
4721
+ await fs19.writeFile(temp, content2, { mode, flag: "wx" });
4722
+ await fs19.rename(temp, file);
4544
4723
  } finally {
4545
- await fs18.rm(temp, { force: true });
4724
+ await fs19.rm(temp, { force: true });
4546
4725
  }
4547
4726
  }
4548
4727
  async function installStandalone(source) {
4549
4728
  const bundle = await verifyBundle(source);
4550
- const selectedHome = path23.resolve(defaultHome());
4551
- await fs18.mkdir(selectedHome, { recursive: true });
4552
- const home = await fs18.realpath(selectedHome);
4553
- const selectedBin = path23.resolve(process.env.MASON_BIN_DIR ?? (process.platform === "win32" ? path23.join(home, "bin") : path23.join(os3.homedir(), ".local/bin")));
4729
+ const selectedHome = path24.resolve(defaultHome());
4730
+ await fs19.mkdir(selectedHome, { recursive: true });
4731
+ const home = await fs19.realpath(selectedHome);
4732
+ const selectedBin = path24.resolve(process.env.MASON_BIN_DIR ?? (process.platform === "win32" ? path24.join(home, "bin") : path24.join(os4.homedir(), ".local/bin")));
4554
4733
  return withLock(home, ".install-lock", async () => {
4555
4734
  const raw = await readStoreJson(home, "install.json");
4556
4735
  const previous = raw === null ? null : recordSchema2.parse(raw);
4557
- if (!previous) for (const file of await fs18.readdir(home)) {
4736
+ if (!previous) for (const file of await fs19.readdir(home)) {
4558
4737
  if (file === ".install-lock") continue;
4559
- const directory = path23.join(home, file);
4560
- const stat = await fs18.lstat(directory);
4561
- if (!["versions", "bin"].includes(file) || !stat.isDirectory() || (await fs18.readdir(directory)).length) {
4738
+ const directory = path24.join(home, file);
4739
+ const stat = await fs19.lstat(directory);
4740
+ if (!["versions", "bin"].includes(file) || !stat.isDirectory() || (await fs19.readdir(directory)).length) {
4562
4741
  throw new Error("Mason installation directory is not empty and has no ownership receipt.");
4563
4742
  }
4564
4743
  }
4565
- await fs18.mkdir(selectedBin, { recursive: true });
4566
- const bin = await fs18.realpath(selectedBin);
4744
+ await fs19.mkdir(selectedBin, { recursive: true });
4745
+ const bin = await fs19.realpath(selectedBin);
4567
4746
  if (previous && (previous.home !== home || previous.bin !== bin)) throw new Error("Installation paths differ from the existing Mason receipt.");
4568
4747
  const id = bundle.manifestHash.slice(0, 24);
4569
4748
  const destination = await storePath(home, "versions/" + id);
4570
4749
  const launchers = process.platform === "win32" ? {
4571
4750
  "mason.cmd": WINDOWS_BATCH_LAUNCHER,
4572
- "mason.ps1": `$ErrorActionPreference = 'Stop'
4751
+ "mason-launcher.ps1": `$ErrorActionPreference = 'Stop'
4573
4752
  $previousToken = $env:MASON_UNINSTALL_TOKEN
4574
4753
  $token = [Guid]::NewGuid().ToString()
4575
4754
  $env:MASON_UNINSTALL_TOKEN = $token
4576
4755
  try {
4577
- & ${psQuote(path23.join(destination, "node.exe"))} ${psQuote(path23.join(destination, "app/dist/mason.js"))} @args
4756
+ & ${psQuote(path24.join(destination, "node.exe"))} ${psQuote(path24.join(destination, "app/dist/mason.js"))} @args
4578
4757
  $code = $LASTEXITCODE
4579
4758
  if ($code -eq 0 -and $args.Count -eq 1 -and $args[0] -eq 'uninstall') {
4580
- if ([IO.File]::ReadAllText(${psQuote(path23.join(home, ".uninstall-request.txt"))}) -cne $token) { throw 'Mason did not authorize installation cleanup.' }
4759
+ if ([IO.File]::ReadAllText(${psQuote(path24.join(home, ".uninstall-request.txt"))}) -cne $token) { throw 'Mason did not authorize installation cleanup.' }
4581
4760
  Remove-Item -LiteralPath ${psQuote(home)} -Recurse -Force
4582
4761
  Write-Output ${psQuote("Removed standalone installation: " + home)}
4583
4762
  }
@@ -4586,48 +4765,66 @@ try {
4586
4765
  finally { $env:MASON_UNINSTALL_TOKEN = $previousToken }
4587
4766
  `
4588
4767
  } : { mason: `#!/bin/sh
4589
- exec ${shQuote(path23.join(destination, "node"))} ${shQuote(path23.join(destination, "app/dist/mason.js"))} "$@"
4768
+ exec ${shQuote(path24.join(destination, "node"))} ${shQuote(path24.join(destination, "app/dist/mason.js"))} "$@"
4590
4769
  ` };
4591
4770
  for (const [name, content2] of Object.entries(launchers)) {
4592
4771
  const file = await storePath(bin, name);
4593
- const old = await fs18.readFile(file, "utf8").catch((error) => {
4772
+ const old = await fs19.readFile(file, "utf8").catch((error) => {
4594
4773
  if (error.code === "ENOENT") return null;
4595
4774
  throw error;
4596
4775
  });
4597
4776
  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);
4598
4777
  }
4599
- const receipt = { format: 1, home, bin, current: id, version: bundle.manifest.version, launchers };
4778
+ const retired = Object.entries({ ...previous?.previousLaunchers, ...previous?.launchers }).filter(([name]) => !(name in launchers));
4779
+ const checkRetired = async (name, expected) => {
4780
+ if (!launcherNames.includes(name)) throw new Error("Invalid owned launcher name.");
4781
+ const actual = await fs19.readFile(await storePath(bin, name), "utf8").catch((error) => {
4782
+ if (error.code === "ENOENT") return null;
4783
+ throw error;
4784
+ });
4785
+ if (actual !== null && actual !== expected) throw new Error("Retired launcher was edited; retained: " + name);
4786
+ };
4787
+ for (const [name, expected] of retired) await checkRetired(name, expected);
4788
+ const receipt = { format: 1, home, bin, current: id, version: bundle.manifest.version, launchers, pathChanges: previous?.pathChanges ?? [] };
4600
4789
  await writeStoreJson(home, "install.json", { ...receipt, previousLaunchers: previous?.previousLaunchers ?? previous?.launchers });
4601
- const stage = await storePath(home, "versions/.install-" + randomUUID8(), true);
4602
- await fs18.mkdir(stage);
4790
+ const stage = await storePath(home, "versions/.install-" + randomUUID9(), true);
4791
+ await fs19.mkdir(stage);
4603
4792
  try {
4604
4793
  for (const file of ["bundle.json", ...Object.keys(bundle.manifest.files)]) {
4605
4794
  const from = await storePath(source, file), to = await storePath(stage, file, true);
4606
- await fs18.copyFile(from, to);
4607
- await fs18.chmod(to, (await fs18.stat(from)).mode & 511);
4795
+ await fs19.copyFile(from, to);
4796
+ await fs19.chmod(to, (await fs19.stat(from)).mode & 511);
4608
4797
  }
4609
4798
  await verifyBundle(stage, bundle.manifestHash);
4610
- const exists2 = await fs18.lstat(destination).then(() => true, (error) => {
4799
+ const exists2 = await fs19.lstat(destination).then(() => true, (error) => {
4611
4800
  if (error.code === "ENOENT") return false;
4612
4801
  throw error;
4613
4802
  });
4614
4803
  if (exists2) await verifyBundle(destination, bundle.manifestHash);
4615
- else await fs18.rename(stage, destination);
4804
+ else await fs19.rename(stage, destination);
4616
4805
  for (const [name, content2] of Object.entries(launchers)) await replace(await storePath(bin, name, true), content2, 493);
4806
+ for (const [name, expected] of retired) {
4807
+ await checkRetired(name, expected);
4808
+ await fs19.rm(await storePath(bin, name), { force: true });
4809
+ }
4617
4810
  await writeStoreJson(home, "install.json", receipt);
4618
4811
  } finally {
4619
- await fs18.rm(stage, { recursive: true, force: true });
4812
+ await fs19.rm(stage, { recursive: true, force: true });
4620
4813
  }
4621
- return { home, bin, version: bundle.manifest.version };
4814
+ const pathResult = await configurePath(bin, receipt.pathChanges, async (changes) => {
4815
+ receipt.pathChanges = changes;
4816
+ await writeStoreJson(home, "install.json", receipt);
4817
+ });
4818
+ return { home, bin, version: bundle.manifest.version, path: pathResult };
4622
4819
  });
4623
4820
  }
4624
4821
  async function currentInstallation() {
4625
- const app = path23.dirname(path23.dirname(fileURLToPath2(import.meta.url)));
4626
- const bundle = path23.dirname(app), home = path23.dirname(path23.dirname(bundle));
4822
+ const app = path24.dirname(path24.dirname(fileURLToPath2(import.meta.url)));
4823
+ const bundle = path24.dirname(app), home = path24.dirname(path24.dirname(bundle));
4627
4824
  const raw = await readStoreJson(home, "install.json");
4628
4825
  if (!raw) throw new Error("Run this command from a standalone installation. npm users can upgrade with npm.");
4629
4826
  const record = recordSchema2.parse(raw);
4630
- if (record.home !== await fs18.realpath(home) || path23.dirname(bundle) !== path23.join(home, "versions")) throw new Error("Invalid standalone installation receipt.");
4827
+ if (record.home !== await fs19.realpath(home) || path24.dirname(bundle) !== path24.join(home, "versions")) throw new Error("Invalid standalone installation receipt.");
4631
4828
  return { home, bundle, record };
4632
4829
  }
4633
4830
  async function upgradeStandalone(version) {
@@ -4635,7 +4832,7 @@ async function upgradeStandalone(version) {
4635
4832
  const { home, bundle, record } = await currentInstallation();
4636
4833
  await verifyBundle(bundle);
4637
4834
  const windows = process.platform === "win32";
4638
- const script = path23.join(bundle, windows ? "install.ps1" : "install.sh");
4835
+ const script = path24.join(bundle, windows ? "install.ps1" : "install.sh");
4639
4836
  return new Promise((resolve, reject) => {
4640
4837
  const child = spawn(
4641
4838
  windows ? "powershell.exe" : "sh",
@@ -4654,40 +4851,44 @@ async function uninstallStandalone() {
4654
4851
  }
4655
4852
  await withLock(home, ".install-lock", async () => {
4656
4853
  for (const [name, expected] of Object.entries(record.launchers)) {
4657
- if (!["mason", "mason.cmd", "mason.ps1"].includes(name)) throw new Error("Invalid owned launcher name.");
4854
+ if (!launcherNames.includes(name)) throw new Error("Invalid owned launcher name.");
4658
4855
  const file = await storePath(record.bin, name);
4659
- const actual = await fs18.readFile(file, "utf8").catch((error) => {
4856
+ const actual = await fs19.readFile(file, "utf8").catch((error) => {
4660
4857
  if (error.code === "ENOENT") return null;
4661
4858
  throw error;
4662
4859
  });
4663
4860
  if (actual !== null && actual !== expected) throw new Error("Launcher was edited; retained: " + file);
4664
4861
  }
4862
+ for (const warning of await removePath(record.pathChanges ?? [])) console.error(warning);
4665
4863
  if (process.platform === "win32") await replace(await storePath(home, ".uninstall-request.txt", true), token);
4666
- for (const name of Object.keys(record.launchers)) await fs18.rm(await storePath(record.bin, name), { force: true });
4864
+ for (const name of Object.keys(record.launchers)) await fs19.rm(await storePath(record.bin, name), { force: true });
4667
4865
  });
4668
- if (process.platform !== "win32") await fs18.rm(home, { recursive: true, force: true });
4866
+ if (process.platform !== "win32") await fs19.rm(home, { recursive: true, force: true });
4669
4867
  return home;
4670
4868
  }
4671
- var recordSchema2, shQuote, psQuote, WINDOWS_BATCH_LAUNCHER, defaultHome;
4869
+ var recordSchema2, shQuote, psQuote, WINDOWS_BATCH_LAUNCHER, launcherNames, defaultHome;
4672
4870
  var init_install2 = __esm({
4673
4871
  "src/distribution/install.ts"() {
4674
4872
  "use strict";
4675
4873
  init_bundle();
4676
4874
  init_storage();
4677
4875
  init_store();
4678
- recordSchema2 = z17.object({
4679
- format: z17.literal(1),
4680
- home: z17.string(),
4681
- bin: z17.string(),
4682
- current: z17.string().regex(/^[a-f0-9]{24}$/),
4683
- version: z17.string(),
4684
- launchers: z17.record(z17.string()),
4685
- previousLaunchers: z17.record(z17.string()).optional()
4876
+ init_path();
4877
+ recordSchema2 = z18.object({
4878
+ format: z18.literal(1),
4879
+ home: z18.string(),
4880
+ bin: z18.string(),
4881
+ current: z18.string().regex(/^[a-f0-9]{24}$/),
4882
+ version: z18.string(),
4883
+ launchers: z18.record(z18.string()),
4884
+ previousLaunchers: z18.record(z18.string()).optional(),
4885
+ pathChanges: z18.array(pathChangeSchema).optional()
4686
4886
  });
4687
4887
  shQuote = (s) => "'" + s.replace(/'/g, "'\\''") + "'";
4688
4888
  psQuote = (s) => "'" + s.replace(/'/g, "''") + "'";
4689
- WINDOWS_BATCH_LAUNCHER = '@echo off\r\ngoto #_mason_handoff_# 2>NUL || title %COMSPEC% & powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "%~dp0mason.ps1" %*\r\n';
4690
- defaultHome = () => process.env.MASON_HOME ?? (process.platform === "win32" ? path23.join(process.env.LOCALAPPDATA ?? os3.homedir(), "Mason") : path23.join(os3.homedir(), ".local/share/mason"));
4889
+ 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';
4890
+ launcherNames = ["mason", "mason.cmd", "mason.ps1", "mason-launcher.ps1"];
4891
+ defaultHome = () => process.env.MASON_HOME ?? (process.platform === "win32" ? path24.join(process.env.LOCALAPPDATA ?? os4.homedir(), "Mason") : path24.join(os4.homedir(), ".local/share/mason"));
4691
4892
  }
4692
4893
  });
4693
4894
 
@@ -4776,20 +4977,20 @@ async function runAutomationCli(argv, stdin = "", io = {
4776
4977
  io.out(values.json ? JSON.stringify(report, null, 2) : summarize(report));
4777
4978
  return report.status === "verified" ? 0 : report.status === "issues-remain" ? 1 : 2;
4778
4979
  } catch (error) {
4779
- const message2 = action === "setup" ? "Mason setup incomplete: " + automationFailure(error).message + ". Rerun the same setup command to resume; retained audit evidence is preserved." : failureMessage(error);
4980
+ const message3 = action === "setup" ? "Mason setup incomplete: " + automationFailure(error).message + ". Rerun the same setup command to resume; retained audit evidence is preserved." : failureMessage(error);
4780
4981
  if (action === "hook" || isHookCommand(argv)) {
4781
- io.out(JSON.stringify({ systemMessage: message2 }));
4982
+ io.out(JSON.stringify({ systemMessage: message3 }));
4782
4983
  return 0;
4783
4984
  }
4784
4985
  if (argv.includes("--json")) io.out(JSON.stringify({ version: 1, ...action === "setup" ? { action, status: "incomplete", next: "Rerun the same setup command to resume." } : { status: "unavailable" }, failure: automationFailure(error) }));
4785
- else io.err(message2);
4986
+ else io.err(message3);
4786
4987
  return 2;
4787
4988
  }
4788
4989
  }
4789
4990
 
4790
4991
  // bin/mason.ts
4791
4992
  var [command, ...args] = process.argv.slice(2);
4792
- var usage = `Mason ${"0.14.0"}
4993
+ var usage = `Mason ${"0.14.1"}
4793
4994
 
4794
4995
  Usage: mason <command> [options]
4795
4996
 
@@ -4807,7 +5008,7 @@ Use mason <command> --help for options. npm users can keep using mason-auto,
4807
5008
  mason-audit, mason-review, mason-drift, mason-hook and mason-mcp.`;
4808
5009
  try {
4809
5010
  if (!command || ["--help", "-h", "help"].includes(command)) console.log(usage);
4810
- else if (["--version", "-v"].includes(command)) console.log("0.14.0");
5011
+ else if (["--version", "-v"].includes(command)) console.log("0.14.1");
4811
5012
  else if (command === "internal-install") {
4812
5013
  if (args.length) throw new Error("internal-install takes no arguments.");
4813
5014
  const { sourceRuntime: sourceRuntime2 } = await Promise.resolve().then(() => (init_runtime2(), runtime_exports));
@@ -4816,8 +5017,7 @@ try {
4816
5017
  const { installStandalone: installStandalone2 } = await Promise.resolve().then(() => (init_install2(), install_exports2));
4817
5018
  const installed = await installStandalone2(selected.bundleRoot);
4818
5019
  console.log(`Installed Mason ${installed.version} in ${installed.home}.
4819
- Launcher: ${installed.bin}
4820
- Add that directory to PATH if needed, then run: mason setup --host codex`);
5020
+ ${installed.path.message}`);
4821
5021
  } else if (command === "upgrade" || command === "uninstall") {
4822
5022
  if (args.includes("--help")) console.log(command === "upgrade" ? "Usage: mason upgrade [version]. Project versions change only when you rerun setup." : "Usage: mason uninstall. Removes the standalone user installation; project runtimes, configuration and knowledge are retained.");
4823
5023
  else {