mason-context 0.16.0 → 0.16.2
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 +13 -0
- package/README.md +3 -0
- package/dist/mason-auto.js +63 -23
- package/dist/mason-auto.js.map +1 -1
- package/dist/mason-mcp.js +315 -123
- package/dist/mason-mcp.js.map +1 -1
- package/dist/mason.js +118 -73
- package/dist/mason.js.map +1 -1
- package/package.json +3 -1
package/dist/mason.js
CHANGED
|
@@ -2521,7 +2521,7 @@ var init_evidence = __esm({
|
|
|
2521
2521
|
init_dead_command();
|
|
2522
2522
|
init_storage();
|
|
2523
2523
|
exec7 = promisify8(execFile8);
|
|
2524
|
-
engineVersion = true ? "0.16.
|
|
2524
|
+
engineVersion = true ? "0.16.2" : "development";
|
|
2525
2525
|
hash = (value) => createHash3("sha256").update(JSON.stringify(value)).digest("hex");
|
|
2526
2526
|
content = readAuditInput;
|
|
2527
2527
|
internal = (file) => file === ".mason" || file === ".mason/reports" || file.startsWith(".mason/reports/");
|
|
@@ -2628,6 +2628,25 @@ var init_store = __esm({
|
|
|
2628
2628
|
|
|
2629
2629
|
// src/automation/runtime.ts
|
|
2630
2630
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
2631
|
+
async function observeReadOnlyTool(dir, event) {
|
|
2632
|
+
const ws = await workspace(dir);
|
|
2633
|
+
await withLock(ws.root, ws.directory, async () => {
|
|
2634
|
+
const statePath = ws.directory + "/state.json";
|
|
2635
|
+
const raw = await readStoreJson(ws.root, statePath);
|
|
2636
|
+
if (raw === null) return;
|
|
2637
|
+
const state = parseState(raw);
|
|
2638
|
+
if (state.root !== ws.root || state.gitDir !== ws.gitDir || state.branch !== ws.branch) {
|
|
2639
|
+
throw new Error("Automation state belongs to another branch or worktree; original evidence was retained.");
|
|
2640
|
+
}
|
|
2641
|
+
const session = event.host && event.sessionId ? state.sessions[hash([event.host, event.sessionId])] : null;
|
|
2642
|
+
if (!session) return;
|
|
2643
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2644
|
+
session.lastUsed = now;
|
|
2645
|
+
session.events[event.event] = { at: now, count: (session.events[event.event]?.count ?? 0) + 1 };
|
|
2646
|
+
await writeStoreJson(ws.root, statePath, state);
|
|
2647
|
+
});
|
|
2648
|
+
return { root: ws.root, directory: ws.directory };
|
|
2649
|
+
}
|
|
2631
2650
|
function summarize(report) {
|
|
2632
2651
|
const open = report.findings.filter((f) => f.status !== "resolved");
|
|
2633
2652
|
return [
|
|
@@ -2905,6 +2924,7 @@ __export(launcher_exports, {
|
|
|
2905
2924
|
});
|
|
2906
2925
|
import { execFile as execFile9 } from "child_process";
|
|
2907
2926
|
import { promisify as promisify9 } from "util";
|
|
2927
|
+
import fs13 from "fs/promises";
|
|
2908
2928
|
function globalCommand(args2, platform = process.platform) {
|
|
2909
2929
|
return platform === "win32" ? { command: "cmd.exe", args: ["/d", "/s", "/c", "mason", ...args2] } : { command: "mason", args: args2 };
|
|
2910
2930
|
}
|
|
@@ -2923,13 +2943,17 @@ async function installedCommand() {
|
|
|
2923
2943
|
return { available: false, version: null, message: "An up-to-date mason command is not available on this process's PATH. Install or upgrade Mason, then reopen your terminal or desktop assistant and rerun mason setup." };
|
|
2924
2944
|
}
|
|
2925
2945
|
}
|
|
2926
|
-
async function prepareLaunch(host) {
|
|
2927
|
-
const
|
|
2928
|
-
const entry = (await loadSetup(
|
|
2929
|
-
if (!entry)
|
|
2930
|
-
|
|
2946
|
+
async function prepareLaunch(host, options = {}) {
|
|
2947
|
+
const root = await fs13.realpath((await git2(process.cwd(), "rev-parse", "--show-toplevel")).trim());
|
|
2948
|
+
const entry = (await loadSetup(root))?.hosts[host];
|
|
2949
|
+
if (!entry) {
|
|
2950
|
+
if (options.allowInactive) return false;
|
|
2951
|
+
throw new Error(`Mason is not configured locally for ${host}. Run mason setup --host ${host} in this checkout.`);
|
|
2952
|
+
}
|
|
2953
|
+
process.env.MASON_SETUP_ROOT = root;
|
|
2931
2954
|
process.env.MASON_SETUP_HOST = host;
|
|
2932
2955
|
process.env.MASON_SETUP_REVISION = entry.revision;
|
|
2956
|
+
return true;
|
|
2933
2957
|
}
|
|
2934
2958
|
var executingVersion, mcpCommand, hookCommand;
|
|
2935
2959
|
var init_launcher = __esm({
|
|
@@ -2937,7 +2961,7 @@ var init_launcher = __esm({
|
|
|
2937
2961
|
"use strict";
|
|
2938
2962
|
init_evidence();
|
|
2939
2963
|
init_model();
|
|
2940
|
-
executingVersion = () => true ? "0.16.
|
|
2964
|
+
executingVersion = () => true ? "0.16.2" : "development";
|
|
2941
2965
|
mcpCommand = (host) => globalCommand(["--setup-host", host, "mcp"]);
|
|
2942
2966
|
hookCommand = (host) => `mason --setup-host ${host} auto`;
|
|
2943
2967
|
}
|
|
@@ -2950,7 +2974,7 @@ __export(observations_exports, {
|
|
|
2950
2974
|
observeActivation: () => observeActivation,
|
|
2951
2975
|
readObservation: () => readObservation
|
|
2952
2976
|
});
|
|
2953
|
-
import
|
|
2977
|
+
import fs14 from "fs/promises";
|
|
2954
2978
|
import { z as z8 } from "zod";
|
|
2955
2979
|
function observationPath(directory, host, revision, version = executingVersion()) {
|
|
2956
2980
|
return `${directory}/activation/${host}-${revision}-${hash(version).slice(0, 12)}.json`;
|
|
@@ -2966,9 +2990,9 @@ async function observeActivation(dir, event, options = {}) {
|
|
|
2966
2990
|
const host = process.env.MASON_SETUP_HOST, revision = process.env.MASON_SETUP_REVISION;
|
|
2967
2991
|
if (host !== "codex" && host !== "claude" || !revision || !process.env.MASON_SETUP_ROOT) return null;
|
|
2968
2992
|
try {
|
|
2969
|
-
const capturedDirectory = options.reportPath?.match(/^(\.mason\/reports\/automation\/[a-f0-9]{24})\/checks\//)?.[1];
|
|
2970
|
-
const ws = capturedDirectory ? { root: await
|
|
2971
|
-
if (ws.root !== await
|
|
2993
|
+
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];
|
|
2994
|
+
const ws = capturedDirectory ? { root: await fs14.realpath(dir), directory: capturedDirectory } : await workspace(dir);
|
|
2995
|
+
if (ws.root !== await fs14.realpath(process.env.MASON_SETUP_ROOT)) return null;
|
|
2972
2996
|
const setup = await loadSetup(ws.root);
|
|
2973
2997
|
if (setup?.hosts[host]?.revision !== revision) return "Mason setup changed; restart the assistant to observe the current integration.";
|
|
2974
2998
|
const directory = ws.directory + "/activation";
|
|
@@ -3039,13 +3063,13 @@ import { z as z9 } from "zod";
|
|
|
3039
3063
|
function normalizeHook(host, raw) {
|
|
3040
3064
|
hostSchema.parse(host);
|
|
3041
3065
|
const input = inputSchema.parse(raw);
|
|
3042
|
-
const readOnly = host === "claude" ? /^(Read|Glob|Grep|WebSearch|WebFetch)$/ : /^(read_file|list_dir|grep_files)
|
|
3043
|
-
return { cwd: input.cwd, name: input.hook_event_name, event: {
|
|
3066
|
+
const readOnly = (host === "claude" ? /^(Read|Glob|Grep|WebSearch|WebFetch)$/ : /^(read_file|list_dir|grep_files|Read|Glob|Grep)$/).test(input.tool_name ?? "");
|
|
3067
|
+
return { cwd: input.cwd, name: input.hook_event_name, readOnly, event: {
|
|
3044
3068
|
event: lifecycle[input.hook_event_name],
|
|
3045
3069
|
host,
|
|
3046
3070
|
sessionId: input.session_id,
|
|
3047
3071
|
toolId: input.tool_use_id,
|
|
3048
|
-
mutating: !!input.tool_name && !readOnly
|
|
3072
|
+
mutating: !!input.tool_name && !readOnly,
|
|
3049
3073
|
stopHookActive: input.stop_hook_active || input.permission_mode === "plan"
|
|
3050
3074
|
} };
|
|
3051
3075
|
}
|
|
@@ -3053,8 +3077,20 @@ async function runAutomationHook(host, stdin) {
|
|
|
3053
3077
|
let name = "";
|
|
3054
3078
|
try {
|
|
3055
3079
|
if (Buffer.byteLength(stdin) > 1024 * 1024) throw new Error("Hook input exceeds 1 MiB.");
|
|
3056
|
-
|
|
3080
|
+
let raw;
|
|
3081
|
+
try {
|
|
3082
|
+
raw = JSON.parse(stdin);
|
|
3083
|
+
} catch {
|
|
3084
|
+
throw new Error("Hook input is not valid JSON.");
|
|
3085
|
+
}
|
|
3086
|
+
const input = normalizeHook(host, raw);
|
|
3057
3087
|
name = input.name;
|
|
3088
|
+
if (["before_tool", "after_tool"].includes(input.event.event) && input.readOnly) {
|
|
3089
|
+
const observed = await observeReadOnlyTool(input.cwd, input.event);
|
|
3090
|
+
const { observeActivation: observeActivation3 } = await Promise.resolve().then(() => (init_observations(), observations_exports));
|
|
3091
|
+
const warning2 = await observeActivation3(observed.root, input.event.event, { sessionId: input.event.sessionId, directory: observed.directory });
|
|
3092
|
+
return warning2 ? { systemMessage: warning2 } : null;
|
|
3093
|
+
}
|
|
3058
3094
|
const result = await automate(input.cwd, input.event);
|
|
3059
3095
|
const { observeActivation: observeActivation2 } = await Promise.resolve().then(() => (init_observations(), observations_exports));
|
|
3060
3096
|
const warning = await observeActivation2(result.report.root, input.event.event, {
|
|
@@ -3073,8 +3109,15 @@ async function runAutomationHook(host, stdin) {
|
|
|
3073
3109
|
return { systemMessage: message2, ...["SessionStart", "UserPromptSubmit", "PreToolUse", "PostToolUse"].includes(name) ? { hookSpecificOutput: { hookEventName: name, additionalContext: message2 } } : {} };
|
|
3074
3110
|
}
|
|
3075
3111
|
}
|
|
3112
|
+
function managedHookCommand(host, platform = process.platform) {
|
|
3113
|
+
const command2 = `mason --setup-host ${host} auto hook --host ${host}`;
|
|
3114
|
+
return platform === "win32" ? `cmd.exe /d /s /c "where mason >nul 2>nul & if errorlevel 1 (exit /b 0) else (${command2})"` : `if command -v mason >/dev/null 2>&1; then ${command2}; fi`;
|
|
3115
|
+
}
|
|
3116
|
+
function knownManagedHookCommands(host) {
|
|
3117
|
+
return [managedHookCommand(host, "linux"), managedHookCommand(host, "win32"), `mason --setup-host ${host} auto hook --host ${host}`];
|
|
3118
|
+
}
|
|
3076
3119
|
function hookConfig(host, command2 = "npx --no-install --package mason-context mason-auto") {
|
|
3077
|
-
const handler = { type: "command", command: command2 + " hook --host " + host, timeout: 30 };
|
|
3120
|
+
const handler = { type: "command", command: command2 === `mason --setup-host ${host} auto` ? managedHookCommand(host) : command2 + " hook --host " + host, timeout: 30 };
|
|
3078
3121
|
return { hooks: Object.fromEntries(HOOK_EVENTS.map((name) => [
|
|
3079
3122
|
name,
|
|
3080
3123
|
[{ ...["PreToolUse", "PostToolUse"].includes(name) ? { matcher: ".*" } : {}, hooks: [{ ...handler }] }]
|
|
@@ -3115,7 +3158,7 @@ __export(files_exports, {
|
|
|
3115
3158
|
managedBlock: () => managedBlock,
|
|
3116
3159
|
readText: () => readText
|
|
3117
3160
|
});
|
|
3118
|
-
import
|
|
3161
|
+
import fs15 from "fs/promises";
|
|
3119
3162
|
import path18 from "path";
|
|
3120
3163
|
import { randomUUID as randomUUID5 } from "crypto";
|
|
3121
3164
|
async function readText(root, file) {
|
|
@@ -3145,14 +3188,14 @@ async function applyEdit(root, edit) {
|
|
|
3145
3188
|
if (edit.after === null) {
|
|
3146
3189
|
const file2 = await storePath(root, edit.path);
|
|
3147
3190
|
if (await readText(root, edit.path) !== edit.before) throw new Error("Input changed during removal: " + edit.path);
|
|
3148
|
-
await
|
|
3191
|
+
await fs15.unlink(file2);
|
|
3149
3192
|
return true;
|
|
3150
3193
|
}
|
|
3151
3194
|
const file = await storePath(root, edit.path, true);
|
|
3152
3195
|
const temporary = path18.join(path18.dirname(file), ".mason-setup-" + randomUUID5() + ".tmp");
|
|
3153
3196
|
try {
|
|
3154
|
-
const mode = await
|
|
3155
|
-
const handle = await
|
|
3197
|
+
const mode = await fs15.stat(file).then((s) => s.mode & 511, () => 384);
|
|
3198
|
+
const handle = await fs15.open(temporary, "wx", mode);
|
|
3156
3199
|
try {
|
|
3157
3200
|
await handle.writeFile(edit.after, "utf8");
|
|
3158
3201
|
await handle.sync();
|
|
@@ -3160,10 +3203,10 @@ async function applyEdit(root, edit) {
|
|
|
3160
3203
|
await handle.close();
|
|
3161
3204
|
}
|
|
3162
3205
|
if (await readText(root, edit.path) !== edit.before) throw new Error("Integration input changed during update: " + edit.path);
|
|
3163
|
-
await
|
|
3206
|
+
await fs15.rename(temporary, file);
|
|
3164
3207
|
return true;
|
|
3165
3208
|
} finally {
|
|
3166
|
-
await
|
|
3209
|
+
await fs15.rm(temporary, { force: true });
|
|
3167
3210
|
}
|
|
3168
3211
|
}
|
|
3169
3212
|
var init_files2 = __esm({
|
|
@@ -3265,9 +3308,9 @@ var init_init = __esm({
|
|
|
3265
3308
|
|
|
3266
3309
|
Mason provides recorded decisions and file impact over MCP. A concept map is optional.
|
|
3267
3310
|
|
|
3268
|
-
- Task, bug,
|
|
3311
|
+
- Task, bug, change request, or investigation of why the project works this way \u2192 \`get_context\` with the task text and known files: matching decisions, related tests, impact, and any available map entries.
|
|
3269
3312
|
- Before editing a file \u2192 \`get_impact\` to check references, tests, and historical change partners.
|
|
3270
|
-
-
|
|
3313
|
+
- Investigation or correction reveals a reusable project lesson \u2192 \`save_decision\` before finishing. Capture only the lasting constraint and reason, with anchors and known sources, owner, and recorder. This creates a proposal; never infer approval. Preserve uncertain causes and unknown attribution. Route project lessons here; keep personal preferences in agent memory. Skip code summaries, current checkout status, temporary task permissions, session trivia, and secrets. No qualifying lesson means no record.
|
|
3271
3314
|
- Consult trust metadata before relying on entries: unknown or changed freshness requires inspection, and failed verification means the description must be corrected. Check approval too: proposals are suggestions, legacy records are unreviewed, and accepted decisions are recorded constraints subject to freshness checks. An accepted revision remains operative while a pending proposal is reviewed; keep both versions and their freshness distinct.
|
|
3272
3315
|
- Asked to review or re-verify a decision \u2192 \`review_decision\` first to inspect content, sources, history, and code changes. Record acceptance, reaffirmation, or retirement only when authorized by the user or a cited team review, with the actual reviewer and reason. Never infer approval from unchanged code. Review and commit the local record through the normal project workflow.
|
|
3273
3316
|
- For an architectural overview, use \`get_snapshot\` if a map is available. If \`map.status\` is missing or invalid, use available decisions and source evidence; do not start building a map unless requested.
|
|
@@ -3519,7 +3562,7 @@ async function planAutomationInstall(root, host, command2) {
|
|
|
3519
3562
|
const previous = record.hosts[host]?.command;
|
|
3520
3563
|
const ownershipText = await readText(root, OWNERSHIP_PATH);
|
|
3521
3564
|
const pending = ownershipText === null ? [] : ownershipSchema.parse(JSON.parse(ownershipText)).files[file]?.hookCommands ?? [];
|
|
3522
|
-
const owned = /* @__PURE__ */ new Set([previous, ...pending, newCommand]);
|
|
3565
|
+
const owned = /* @__PURE__ */ new Set([previous, ...pending, newCommand, ...knownManagedHookCommands(host)]);
|
|
3523
3566
|
const hooks = existing.hooks ?? {};
|
|
3524
3567
|
for (const event of HOOK_EVENTS) {
|
|
3525
3568
|
hooks[event] = (hooks[event] ?? []).map((group) => ({
|
|
@@ -3623,6 +3666,7 @@ async function planTeardown(root, selected) {
|
|
|
3623
3666
|
const commands = (host) => new Set([
|
|
3624
3667
|
automation?.hosts[host]?.command,
|
|
3625
3668
|
...ownership.files[configPath(host)]?.hookCommands ?? [],
|
|
3669
|
+
...knownManagedHookCommands(host),
|
|
3626
3670
|
hookConfig(host, hookCommand(host)).hooks.SessionStart[0].hooks[0].command,
|
|
3627
3671
|
hookConfig(host).hooks.SessionStart[0].hooks[0].command
|
|
3628
3672
|
].filter((command2) => !!command2));
|
|
@@ -4352,7 +4396,7 @@ var init_evidence2 = __esm({
|
|
|
4352
4396
|
});
|
|
4353
4397
|
|
|
4354
4398
|
// src/review/review.ts
|
|
4355
|
-
import
|
|
4399
|
+
import fs16 from "fs/promises";
|
|
4356
4400
|
import path20 from "path";
|
|
4357
4401
|
import { execFile as execFile12 } from "child_process";
|
|
4358
4402
|
import { promisify as promisify12 } from "util";
|
|
@@ -4432,7 +4476,7 @@ async function computeReview(rootDir, base, options = {}) {
|
|
|
4432
4476
|
analyzed,
|
|
4433
4477
|
async (relPath) => {
|
|
4434
4478
|
try {
|
|
4435
|
-
await
|
|
4479
|
+
await fs16.access(path20.join(resolvedRoot, relPath));
|
|
4436
4480
|
return true;
|
|
4437
4481
|
} catch {
|
|
4438
4482
|
return false;
|
|
@@ -4586,7 +4630,7 @@ async function setupStatus(dir) {
|
|
|
4586
4630
|
version: 1,
|
|
4587
4631
|
status: pending.length ? "incomplete" : "not-configured",
|
|
4588
4632
|
hosts: {},
|
|
4589
|
-
next: pending.length ? "Setup did not finish. Rerun mason-auto setup --host " + pending[0] + " to resume using the retained original evidence." : "Run mason
|
|
4633
|
+
next: pending.length ? "Setup did not finish. Rerun mason-auto setup --host " + pending[0] + " to resume using the retained original evidence." : "Project hooks are inactive until local setup. Run mason setup --host codex or --host claude."
|
|
4590
4634
|
};
|
|
4591
4635
|
}
|
|
4592
4636
|
const hosts2 = {};
|
|
@@ -4795,12 +4839,12 @@ var init_setup = __esm({
|
|
|
4795
4839
|
});
|
|
4796
4840
|
|
|
4797
4841
|
// src/distribution/bundle.ts
|
|
4798
|
-
import
|
|
4842
|
+
import fs17 from "fs/promises";
|
|
4799
4843
|
import path21 from "path";
|
|
4800
4844
|
import { createHash as createHash4 } from "crypto";
|
|
4801
4845
|
import { z as z18 } from "zod";
|
|
4802
4846
|
async function readBundle(root) {
|
|
4803
|
-
const bytes = await
|
|
4847
|
+
const bytes = await fs17.readFile(await storePath(root, "bundle.json"));
|
|
4804
4848
|
const manifest = bundleSchema.parse(JSON.parse(bytes.toString("utf8")));
|
|
4805
4849
|
const windows = manifest.target.startsWith("win32-");
|
|
4806
4850
|
for (const file of Object.keys(manifest.files)) {
|
|
@@ -4819,7 +4863,7 @@ async function verifyBundle(root, expectedHash) {
|
|
|
4819
4863
|
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.");
|
|
4820
4864
|
const files = Object.keys(bundle.manifest.files);
|
|
4821
4865
|
for (let i = 0; i < files.length; i += 8) await Promise.all(files.slice(i, i + 8).map(async (file) => {
|
|
4822
|
-
if (sha256(await
|
|
4866
|
+
if (sha256(await fs17.readFile(await storePath(root, file))) !== bundle.manifest.files[file]) throw new Error("Mason bundle checksum mismatch: " + file);
|
|
4823
4867
|
}));
|
|
4824
4868
|
return bundle;
|
|
4825
4869
|
}
|
|
@@ -4842,7 +4886,7 @@ var init_bundle = __esm({
|
|
|
4842
4886
|
});
|
|
4843
4887
|
|
|
4844
4888
|
// src/distribution/path.ts
|
|
4845
|
-
import
|
|
4889
|
+
import fs18 from "fs/promises";
|
|
4846
4890
|
import os3 from "os";
|
|
4847
4891
|
import path22 from "path";
|
|
4848
4892
|
import { execFile as execFile13 } from "child_process";
|
|
@@ -4881,7 +4925,7 @@ ${alreadyOnPath ? message2 : `Add ${bin} to your PATH, then run: mason setup --h
|
|
|
4881
4925
|
else {
|
|
4882
4926
|
const candidates = [".bash_profile", ".bash_login", ".profile"].map((name) => path22.join(userHome, name));
|
|
4883
4927
|
let login = candidates[2];
|
|
4884
|
-
for (const file of candidates) if (await
|
|
4928
|
+
for (const file of candidates) if (await fs18.lstat(file).then(() => true, (error) => {
|
|
4885
4929
|
if (error.code === "ENOENT") return false;
|
|
4886
4930
|
throw error;
|
|
4887
4931
|
})) {
|
|
@@ -4892,12 +4936,12 @@ ${alreadyOnPath ? message2 : `Add ${bin} to your PATH, then run: mason setup --h
|
|
|
4892
4936
|
}
|
|
4893
4937
|
const marker = `# >>> mason PATH ${createHash5("sha256").update(bin).digest("hex").slice(0, 12)} >>>`;
|
|
4894
4938
|
for (const profile of profiles) {
|
|
4895
|
-
const stat = await
|
|
4939
|
+
const stat = await fs18.lstat(profile).catch((error) => {
|
|
4896
4940
|
if (error.code === "ENOENT") return null;
|
|
4897
4941
|
throw error;
|
|
4898
4942
|
});
|
|
4899
|
-
const file = stat?.isSymbolicLink() ? await
|
|
4900
|
-
const original = await
|
|
4943
|
+
const file = stat?.isSymbolicLink() ? await fs18.realpath(profile) : path22.resolve(profile);
|
|
4944
|
+
const original = await fs18.readFile(file).catch((error) => {
|
|
4901
4945
|
if (error.code === "ENOENT" && !stat) return null;
|
|
4902
4946
|
throw error;
|
|
4903
4947
|
});
|
|
@@ -4912,8 +4956,8 @@ ${alreadyOnPath ? message2 : `Add ${bin} to your PATH, then run: mason setup --h
|
|
|
4912
4956
|
changes.push({ kind: "profile", file, block, created: original === null });
|
|
4913
4957
|
await save(changes);
|
|
4914
4958
|
}
|
|
4915
|
-
await
|
|
4916
|
-
const handle = await
|
|
4959
|
+
await fs18.mkdir(path22.dirname(file), { recursive: true });
|
|
4960
|
+
const handle = await fs18.open(file, original === null ? "ax+" : "a+", 384);
|
|
4917
4961
|
try {
|
|
4918
4962
|
if (!(await handle.readFile()).equals(original ?? Buffer.alloc(0))) throw new Error(`Shell profile changed during installation: ${profile}`);
|
|
4919
4963
|
await handle.writeFile(block);
|
|
@@ -4937,13 +4981,13 @@ async function removePath(changes) {
|
|
|
4937
4981
|
await windowsPath("remove", change.entry, current, change.created);
|
|
4938
4982
|
continue;
|
|
4939
4983
|
}
|
|
4940
|
-
const stat = await
|
|
4984
|
+
const stat = await fs18.lstat(change.file).catch((error) => {
|
|
4941
4985
|
if (error.code === "ENOENT") return null;
|
|
4942
4986
|
throw error;
|
|
4943
4987
|
});
|
|
4944
4988
|
if (!stat) continue;
|
|
4945
4989
|
if (!stat.isFile()) throw new Error("profile type changed");
|
|
4946
|
-
const bytes = await
|
|
4990
|
+
const bytes = await fs18.readFile(change.file), text2 = bytes.toString("utf8");
|
|
4947
4991
|
if (!Buffer.from(text2).equals(bytes)) throw new Error("profile encoding changed");
|
|
4948
4992
|
const index2 = text2.indexOf(change.block);
|
|
4949
4993
|
if (index2 < 0 || text2.indexOf(change.block, index2 + change.block.length) >= 0) throw new Error("PATH block was edited or removed");
|
|
@@ -4953,7 +4997,7 @@ async function removePath(changes) {
|
|
|
4953
4997
|
const temporary = change.file + ".mason-" + randomUUID7();
|
|
4954
4998
|
try {
|
|
4955
4999
|
if (stat.nlink > 1) throw new Error("profile has multiple hard links");
|
|
4956
|
-
const handle = await
|
|
5000
|
+
const handle = await fs18.open(temporary, "wx", stat.mode & 511);
|
|
4957
5001
|
try {
|
|
4958
5002
|
await handle.writeFile(next);
|
|
4959
5003
|
await handle.chmod(stat.mode & 511);
|
|
@@ -4962,11 +5006,11 @@ async function removePath(changes) {
|
|
|
4962
5006
|
} finally {
|
|
4963
5007
|
await handle.close();
|
|
4964
5008
|
}
|
|
4965
|
-
const current = await
|
|
4966
|
-
if (!current.isFile() || current.ino !== stat.ino || !(await
|
|
4967
|
-
await
|
|
5009
|
+
const current = await fs18.lstat(change.file);
|
|
5010
|
+
if (!current.isFile() || current.ino !== stat.ino || !(await fs18.readFile(change.file)).equals(bytes)) throw new Error("profile changed during uninstall");
|
|
5011
|
+
await fs18.rename(temporary, change.file);
|
|
4968
5012
|
} finally {
|
|
4969
|
-
await
|
|
5013
|
+
await fs18.rm(temporary, { force: true });
|
|
4970
5014
|
}
|
|
4971
5015
|
} catch (error) {
|
|
4972
5016
|
warnings.push(`Retained PATH setting ${change.kind === "profile" ? change.file : change.entry}: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -5031,7 +5075,7 @@ __export(install_exports2, {
|
|
|
5031
5075
|
uninstallStandalone: () => uninstallStandalone,
|
|
5032
5076
|
upgradeStandalone: () => upgradeStandalone
|
|
5033
5077
|
});
|
|
5034
|
-
import
|
|
5078
|
+
import fs19 from "fs/promises";
|
|
5035
5079
|
import os4 from "os";
|
|
5036
5080
|
import path23 from "path";
|
|
5037
5081
|
import { fileURLToPath } from "url";
|
|
@@ -5041,31 +5085,31 @@ import { z as z20 } from "zod";
|
|
|
5041
5085
|
async function replace(file, content2, mode = 384) {
|
|
5042
5086
|
const temp = file + ".tmp-" + randomUUID8();
|
|
5043
5087
|
try {
|
|
5044
|
-
await
|
|
5045
|
-
await
|
|
5088
|
+
await fs19.writeFile(temp, content2, { mode, flag: "wx" });
|
|
5089
|
+
await fs19.rename(temp, file);
|
|
5046
5090
|
} finally {
|
|
5047
|
-
await
|
|
5091
|
+
await fs19.rm(temp, { force: true });
|
|
5048
5092
|
}
|
|
5049
5093
|
}
|
|
5050
5094
|
async function installStandalone(source) {
|
|
5051
5095
|
const bundle = await verifyBundle(source);
|
|
5052
5096
|
const selectedHome = path23.resolve(defaultHome());
|
|
5053
|
-
await
|
|
5054
|
-
const home = await
|
|
5097
|
+
await fs19.mkdir(selectedHome, { recursive: true });
|
|
5098
|
+
const home = await fs19.realpath(selectedHome);
|
|
5055
5099
|
const selectedBin = path23.resolve(process.env.MASON_BIN_DIR ?? (process.platform === "win32" ? path23.join(home, "bin") : path23.join(os4.homedir(), ".local/bin")));
|
|
5056
5100
|
return withLock(home, ".install-lock", async () => {
|
|
5057
5101
|
const raw = await readStoreJson(home, "install.json");
|
|
5058
5102
|
const previous = raw === null ? null : recordSchema2.parse(raw);
|
|
5059
|
-
if (!previous) for (const file of await
|
|
5103
|
+
if (!previous) for (const file of await fs19.readdir(home)) {
|
|
5060
5104
|
if (file === ".install-lock") continue;
|
|
5061
5105
|
const directory = path23.join(home, file);
|
|
5062
|
-
const stat = await
|
|
5063
|
-
if (!["versions", "bin"].includes(file) || !stat.isDirectory() || (await
|
|
5106
|
+
const stat = await fs19.lstat(directory);
|
|
5107
|
+
if (!["versions", "bin"].includes(file) || !stat.isDirectory() || (await fs19.readdir(directory)).length) {
|
|
5064
5108
|
throw new Error("Mason installation directory is not empty and has no ownership receipt.");
|
|
5065
5109
|
}
|
|
5066
5110
|
}
|
|
5067
|
-
await
|
|
5068
|
-
const bin = await
|
|
5111
|
+
await fs19.mkdir(selectedBin, { recursive: true });
|
|
5112
|
+
const bin = await fs19.realpath(selectedBin);
|
|
5069
5113
|
if (previous && (previous.home !== home || previous.bin !== bin)) throw new Error("Installation paths differ from the existing Mason receipt.");
|
|
5070
5114
|
const id = bundle.manifestHash.slice(0, 24);
|
|
5071
5115
|
const destination = await storePath(home, "versions/" + id);
|
|
@@ -5092,7 +5136,7 @@ exec ${shQuote(path23.join(destination, "node"))} ${shQuote(path23.join(destinat
|
|
|
5092
5136
|
` };
|
|
5093
5137
|
for (const [name, content2] of Object.entries(launchers)) {
|
|
5094
5138
|
const file = await storePath(bin, name);
|
|
5095
|
-
const old = await
|
|
5139
|
+
const old = await fs19.readFile(file, "utf8").catch((error) => {
|
|
5096
5140
|
if (error.code === "ENOENT") return null;
|
|
5097
5141
|
throw error;
|
|
5098
5142
|
});
|
|
@@ -5101,7 +5145,7 @@ exec ${shQuote(path23.join(destination, "node"))} ${shQuote(path23.join(destinat
|
|
|
5101
5145
|
const retired = Object.entries({ ...previous?.previousLaunchers, ...previous?.launchers }).filter(([name]) => !(name in launchers));
|
|
5102
5146
|
const checkRetired = async (name, expected) => {
|
|
5103
5147
|
if (!launcherNames.includes(name)) throw new Error("Invalid owned launcher name.");
|
|
5104
|
-
const actual = await
|
|
5148
|
+
const actual = await fs19.readFile(await storePath(bin, name), "utf8").catch((error) => {
|
|
5105
5149
|
if (error.code === "ENOENT") return null;
|
|
5106
5150
|
throw error;
|
|
5107
5151
|
});
|
|
@@ -5111,28 +5155,28 @@ exec ${shQuote(path23.join(destination, "node"))} ${shQuote(path23.join(destinat
|
|
|
5111
5155
|
const receipt = { format: 1, home, bin, current: id, version: bundle.manifest.version, launchers, pathChanges: previous?.pathChanges ?? [] };
|
|
5112
5156
|
await writeStoreJson(home, "install.json", { ...receipt, previousLaunchers: previous?.previousLaunchers ?? previous?.launchers });
|
|
5113
5157
|
const stage = await storePath(home, "versions/.install-" + randomUUID8(), true);
|
|
5114
|
-
await
|
|
5158
|
+
await fs19.mkdir(stage);
|
|
5115
5159
|
try {
|
|
5116
5160
|
for (const file of ["bundle.json", ...Object.keys(bundle.manifest.files)]) {
|
|
5117
5161
|
const from = await storePath(source, file), to = await storePath(stage, file, true);
|
|
5118
|
-
await
|
|
5119
|
-
await
|
|
5162
|
+
await fs19.copyFile(from, to);
|
|
5163
|
+
await fs19.chmod(to, (await fs19.stat(from)).mode & 511);
|
|
5120
5164
|
}
|
|
5121
5165
|
await verifyBundle(stage, bundle.manifestHash);
|
|
5122
|
-
const exists2 = await
|
|
5166
|
+
const exists2 = await fs19.lstat(destination).then(() => true, (error) => {
|
|
5123
5167
|
if (error.code === "ENOENT") return false;
|
|
5124
5168
|
throw error;
|
|
5125
5169
|
});
|
|
5126
5170
|
if (exists2) await verifyBundle(destination, bundle.manifestHash);
|
|
5127
|
-
else await
|
|
5171
|
+
else await fs19.rename(stage, destination);
|
|
5128
5172
|
for (const [name, content2] of Object.entries(launchers)) await replace(await storePath(bin, name, true), content2, 493);
|
|
5129
5173
|
for (const [name, expected] of retired) {
|
|
5130
5174
|
await checkRetired(name, expected);
|
|
5131
|
-
await
|
|
5175
|
+
await fs19.rm(await storePath(bin, name), { force: true });
|
|
5132
5176
|
}
|
|
5133
5177
|
await writeStoreJson(home, "install.json", receipt);
|
|
5134
5178
|
} finally {
|
|
5135
|
-
await
|
|
5179
|
+
await fs19.rm(stage, { recursive: true, force: true });
|
|
5136
5180
|
}
|
|
5137
5181
|
const pathResult = await configurePath(bin, receipt.pathChanges, async (changes) => {
|
|
5138
5182
|
receipt.pathChanges = changes;
|
|
@@ -5147,7 +5191,7 @@ async function currentInstallation() {
|
|
|
5147
5191
|
const raw = await readStoreJson(home, "install.json");
|
|
5148
5192
|
if (!raw) throw new Error("Run this command from a standalone installation. npm users can upgrade with npm.");
|
|
5149
5193
|
const record = recordSchema2.parse(raw);
|
|
5150
|
-
if (record.home !== await
|
|
5194
|
+
if (record.home !== await fs19.realpath(home) || path23.dirname(bundle) !== path23.join(home, "versions")) throw new Error("Invalid standalone installation receipt.");
|
|
5151
5195
|
return { home, bundle, record };
|
|
5152
5196
|
}
|
|
5153
5197
|
async function upgradeStandalone(version) {
|
|
@@ -5176,7 +5220,7 @@ async function uninstallStandalone() {
|
|
|
5176
5220
|
for (const [name, expected] of Object.entries(record.launchers)) {
|
|
5177
5221
|
if (!launcherNames.includes(name)) throw new Error("Invalid owned launcher name.");
|
|
5178
5222
|
const file = await storePath(record.bin, name);
|
|
5179
|
-
const actual = await
|
|
5223
|
+
const actual = await fs19.readFile(file, "utf8").catch((error) => {
|
|
5180
5224
|
if (error.code === "ENOENT") return null;
|
|
5181
5225
|
throw error;
|
|
5182
5226
|
});
|
|
@@ -5184,9 +5228,9 @@ async function uninstallStandalone() {
|
|
|
5184
5228
|
}
|
|
5185
5229
|
for (const warning of await removePath(record.pathChanges ?? [])) console.error(warning);
|
|
5186
5230
|
if (process.platform === "win32") await replace(await storePath(home, ".uninstall-request.txt", true), token);
|
|
5187
|
-
for (const name of Object.keys(record.launchers)) await
|
|
5231
|
+
for (const name of Object.keys(record.launchers)) await fs19.rm(await storePath(record.bin, name), { force: true });
|
|
5188
5232
|
});
|
|
5189
|
-
if (process.platform !== "win32") await
|
|
5233
|
+
if (process.platform !== "win32") await fs19.rm(home, { recursive: true, force: true });
|
|
5190
5234
|
return home;
|
|
5191
5235
|
}
|
|
5192
5236
|
var recordSchema2, shQuote, psQuote, WINDOWS_BATCH_LAUNCHER, launcherNames, defaultHome;
|
|
@@ -5341,7 +5385,7 @@ var inputArgs = process.argv.slice(2);
|
|
|
5341
5385
|
var managedLaunch = inputArgs[0] === "--setup-host";
|
|
5342
5386
|
var setupHost = managedLaunch ? inputArgs.splice(0, 2)[1] : void 0;
|
|
5343
5387
|
var [command, ...args] = inputArgs;
|
|
5344
|
-
var usage = `Mason ${"0.16.
|
|
5388
|
+
var usage = `Mason ${"0.16.2"}
|
|
5345
5389
|
|
|
5346
5390
|
Usage: mason <command> [options]
|
|
5347
5391
|
|
|
@@ -5362,11 +5406,12 @@ try {
|
|
|
5362
5406
|
if (managedLaunch) {
|
|
5363
5407
|
if (!setupHost || !["codex", "claude"].includes(setupHost) || !["mcp", "auto"].includes(command)) throw new Error("Invalid setup invocation.");
|
|
5364
5408
|
const { prepareLaunch: prepareLaunch2 } = await Promise.resolve().then(() => (init_launcher(), launcher_exports));
|
|
5365
|
-
await prepareLaunch2(setupHost);
|
|
5409
|
+
const active = await prepareLaunch2(setupHost, { allowInactive: command === "auto" && isHookCommand(args) });
|
|
5410
|
+
if (!active) process.exit(0);
|
|
5366
5411
|
}
|
|
5367
|
-
if (command === "internal-integration-version") console.log(JSON.stringify({ protocol: 1, version: "0.16.
|
|
5412
|
+
if (command === "internal-integration-version") console.log(JSON.stringify({ protocol: 1, version: "0.16.2" }));
|
|
5368
5413
|
else if (!command || ["--help", "-h", "help"].includes(command)) console.log(usage);
|
|
5369
|
-
else if (["--version", "-v"].includes(command)) console.log("0.16.
|
|
5414
|
+
else if (["--version", "-v"].includes(command)) console.log("0.16.2");
|
|
5370
5415
|
else if (command === "internal-install") {
|
|
5371
5416
|
if (args.length) throw new Error("internal-install takes no arguments.");
|
|
5372
5417
|
const { installStandalone: installStandalone2 } = await Promise.resolve().then(() => (init_install2(), install_exports2));
|