mason-context 0.14.2 → 0.16.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/CHANGELOG.md +12 -0
- package/README.md +1 -1
- package/dist/mason-auto.js +807 -643
- package/dist/mason-auto.js.map +1 -1
- package/dist/mason-mcp.js +2286 -2352
- package/dist/mason-mcp.js.map +1 -1
- package/dist/mason.js +1292 -1056
- package/dist/mason.js.map +1 -1
- package/package.json +1 -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.
|
|
2524
|
+
engineVersion = true ? "0.16.0" : "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/");
|
|
@@ -2813,9 +2813,9 @@ async function automationStatus(dir) {
|
|
|
2813
2813
|
if (state.root !== ws.root || state.gitDir !== ws.gitDir || state.branch !== ws.branch) throw new Error("Automation state belongs to another workspace.");
|
|
2814
2814
|
const inputs = await readInputs(ws.root);
|
|
2815
2815
|
const latest = state.latest ? await readStoreJson(ws.root, state.latest) : null;
|
|
2816
|
-
const
|
|
2816
|
+
const hosts2 = {};
|
|
2817
2817
|
for (const session of Object.values(state.sessions)) {
|
|
2818
|
-
const host =
|
|
2818
|
+
const host = hosts2[session.host] ??= { sessions: 0, observedEvents: [] };
|
|
2819
2819
|
host.sessions++;
|
|
2820
2820
|
host.observedEvents = [.../* @__PURE__ */ new Set([...host.observedEvents, ...Object.keys(session.events)])];
|
|
2821
2821
|
}
|
|
@@ -2828,7 +2828,7 @@ async function automationStatus(dir) {
|
|
|
2828
2828
|
baselinePaths: state.baselines.map((b) => b.path),
|
|
2829
2829
|
reportPath: state.latest,
|
|
2830
2830
|
verificationStatus: unfinished ? "unavailable" : latest?.status ?? "unavailable",
|
|
2831
|
-
hosts,
|
|
2831
|
+
hosts: hosts2,
|
|
2832
2832
|
execution,
|
|
2833
2833
|
note: "Observed events do not prove all tool paths are intercepted. Run check to verify the retained evidence."
|
|
2834
2834
|
};
|
|
@@ -2852,7 +2852,7 @@ var init_runtime = __esm({
|
|
|
2852
2852
|
// src/setup/model.ts
|
|
2853
2853
|
import { z as z7 } from "zod";
|
|
2854
2854
|
async function loadSetup(root) {
|
|
2855
|
-
const raw = await readStoreJson(root,
|
|
2855
|
+
const raw = await readStoreJson(root, SETUP_PATH);
|
|
2856
2856
|
if (raw === null) return null;
|
|
2857
2857
|
return setupSchema.parse(raw);
|
|
2858
2858
|
}
|
|
@@ -2863,28 +2863,23 @@ async function loadSetupReceipt(root, directory, host) {
|
|
|
2863
2863
|
if (receipt.root !== root || receipt.host !== host) throw new Error("Setup receipt belongs to another installation.");
|
|
2864
2864
|
return receipt;
|
|
2865
2865
|
}
|
|
2866
|
-
var
|
|
2866
|
+
var setupHostSchema, setupSchema, SETUP_PATH, receiptSchema;
|
|
2867
2867
|
var init_model = __esm({
|
|
2868
2868
|
"src/setup/model.ts"() {
|
|
2869
2869
|
"use strict";
|
|
2870
2870
|
init_storage();
|
|
2871
|
-
runtimeSchema = z7.object({
|
|
2872
|
-
id: z7.string().regex(/^[a-f0-9]{24}$/),
|
|
2873
|
-
version: z7.string().regex(/^\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?$/),
|
|
2874
|
-
hashes: z7.record(z7.string().regex(/^[a-f0-9]{64}$/)),
|
|
2875
|
-
bundle: z7.object({ target: z7.string(), manifestHash: z7.string().regex(/^[a-f0-9]{64}$/) }).optional()
|
|
2876
|
-
});
|
|
2877
2871
|
setupHostSchema = z7.object({
|
|
2878
|
-
|
|
2872
|
+
configuredVersion: z7.string(),
|
|
2879
2873
|
revision: z7.string().uuid(),
|
|
2880
2874
|
fingerprint: z7.string(),
|
|
2881
2875
|
mcpFingerprint: z7.string(),
|
|
2882
2876
|
instructions: z7.array(z7.string())
|
|
2883
2877
|
});
|
|
2884
|
-
setupSchema = z7.object({ version: z7.literal(
|
|
2878
|
+
setupSchema = z7.object({ version: z7.literal(2), hosts: z7.object({
|
|
2885
2879
|
codex: setupHostSchema.optional(),
|
|
2886
2880
|
claude: setupHostSchema.optional()
|
|
2887
2881
|
}) });
|
|
2882
|
+
SETUP_PATH = ".mason/local/setup.json";
|
|
2888
2883
|
receiptSchema = z7.object({
|
|
2889
2884
|
version: z7.literal(1),
|
|
2890
2885
|
host: z7.enum(["codex", "claude"]),
|
|
@@ -2898,6 +2893,56 @@ var init_model = __esm({
|
|
|
2898
2893
|
}
|
|
2899
2894
|
});
|
|
2900
2895
|
|
|
2896
|
+
// src/setup/launcher.ts
|
|
2897
|
+
var launcher_exports = {};
|
|
2898
|
+
__export(launcher_exports, {
|
|
2899
|
+
executingVersion: () => executingVersion,
|
|
2900
|
+
globalCommand: () => globalCommand,
|
|
2901
|
+
hookCommand: () => hookCommand,
|
|
2902
|
+
installedCommand: () => installedCommand,
|
|
2903
|
+
mcpCommand: () => mcpCommand,
|
|
2904
|
+
prepareLaunch: () => prepareLaunch
|
|
2905
|
+
});
|
|
2906
|
+
import { execFile as execFile9 } from "child_process";
|
|
2907
|
+
import { promisify as promisify9 } from "util";
|
|
2908
|
+
function globalCommand(args2, platform = process.platform) {
|
|
2909
|
+
return platform === "win32" ? { command: "cmd.exe", args: ["/d", "/s", "/c", "mason", ...args2] } : { command: "mason", args: args2 };
|
|
2910
|
+
}
|
|
2911
|
+
async function installedCommand() {
|
|
2912
|
+
const invocation = globalCommand(["internal-integration-version"]);
|
|
2913
|
+
try {
|
|
2914
|
+
const { stdout } = await promisify9(execFile9)(
|
|
2915
|
+
invocation.command,
|
|
2916
|
+
invocation.args,
|
|
2917
|
+
{ timeout: 5e3, maxBuffer: 8192, windowsHide: true }
|
|
2918
|
+
);
|
|
2919
|
+
const result = JSON.parse(stdout);
|
|
2920
|
+
if (result.protocol !== 1 || typeof result.version !== "string") throw new Error("Unsupported Mason integration protocol.");
|
|
2921
|
+
return { available: true, version: result.version, message: null };
|
|
2922
|
+
} catch {
|
|
2923
|
+
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
|
+
}
|
|
2925
|
+
}
|
|
2926
|
+
async function prepareLaunch(host) {
|
|
2927
|
+
const ws = await workspace(process.cwd());
|
|
2928
|
+
const entry = (await loadSetup(ws.root))?.hosts[host];
|
|
2929
|
+
if (!entry) throw new Error(`Mason is not configured locally for ${host}. Run mason setup --host ${host} in this checkout.`);
|
|
2930
|
+
process.env.MASON_SETUP_ROOT = ws.root;
|
|
2931
|
+
process.env.MASON_SETUP_HOST = host;
|
|
2932
|
+
process.env.MASON_SETUP_REVISION = entry.revision;
|
|
2933
|
+
}
|
|
2934
|
+
var executingVersion, mcpCommand, hookCommand;
|
|
2935
|
+
var init_launcher = __esm({
|
|
2936
|
+
"src/setup/launcher.ts"() {
|
|
2937
|
+
"use strict";
|
|
2938
|
+
init_evidence();
|
|
2939
|
+
init_model();
|
|
2940
|
+
executingVersion = () => true ? "0.16.0" : "development";
|
|
2941
|
+
mcpCommand = (host) => globalCommand(["--setup-host", host, "mcp"]);
|
|
2942
|
+
hookCommand = (host) => `mason --setup-host ${host} auto`;
|
|
2943
|
+
}
|
|
2944
|
+
});
|
|
2945
|
+
|
|
2901
2946
|
// src/setup/observations.ts
|
|
2902
2947
|
var observations_exports = {};
|
|
2903
2948
|
__export(observations_exports, {
|
|
@@ -2907,14 +2952,14 @@ __export(observations_exports, {
|
|
|
2907
2952
|
});
|
|
2908
2953
|
import fs13 from "fs/promises";
|
|
2909
2954
|
import { z as z8 } from "zod";
|
|
2910
|
-
function observationPath(directory, host, revision) {
|
|
2911
|
-
return `${directory}/activation/${host}-${revision}.json`;
|
|
2955
|
+
function observationPath(directory, host, revision, version = executingVersion()) {
|
|
2956
|
+
return `${directory}/activation/${host}-${revision}-${hash(version).slice(0, 12)}.json`;
|
|
2912
2957
|
}
|
|
2913
|
-
async function readObservation(root, directory, host, revision) {
|
|
2914
|
-
const raw = await readStoreJson(root, observationPath(directory, host, revision));
|
|
2958
|
+
async function readObservation(root, directory, host, revision, version = executingVersion()) {
|
|
2959
|
+
const raw = await readStoreJson(root, observationPath(directory, host, revision, version));
|
|
2915
2960
|
if (raw === null) return null;
|
|
2916
2961
|
const record = observationSchema.parse(raw);
|
|
2917
|
-
if (record.root !== root || record.host !== host || record.revision !== revision) throw new Error("Activation receipt belongs to another installation.");
|
|
2962
|
+
if (record.root !== root || record.host !== host || record.revision !== revision || record.masonVersion !== version) throw new Error("Activation receipt belongs to another installation.");
|
|
2918
2963
|
return record;
|
|
2919
2964
|
}
|
|
2920
2965
|
async function observeActivation(dir, event, options = {}) {
|
|
@@ -2931,6 +2976,7 @@ async function observeActivation(dir, event, options = {}) {
|
|
|
2931
2976
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2932
2977
|
const record = await readObservation(ws.root, ws.directory, host, revision) ?? {
|
|
2933
2978
|
version: 1,
|
|
2979
|
+
masonVersion: executingVersion(),
|
|
2934
2980
|
root: ws.root,
|
|
2935
2981
|
revision,
|
|
2936
2982
|
host,
|
|
@@ -2969,8 +3015,10 @@ var init_observations = __esm({
|
|
|
2969
3015
|
init_store();
|
|
2970
3016
|
init_storage();
|
|
2971
3017
|
init_model();
|
|
3018
|
+
init_launcher();
|
|
2972
3019
|
observationSchema = z8.object({
|
|
2973
3020
|
version: z8.literal(1),
|
|
3021
|
+
masonVersion: z8.string(),
|
|
2974
3022
|
root: z8.string(),
|
|
2975
3023
|
revision: z8.string(),
|
|
2976
3024
|
host: z8.enum(["codex", "claude"]),
|
|
@@ -3060,106 +3108,157 @@ var init_adapters = __esm({
|
|
|
3060
3108
|
}
|
|
3061
3109
|
});
|
|
3062
3110
|
|
|
3063
|
-
// src/
|
|
3064
|
-
var
|
|
3065
|
-
__export(
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
installedAutomation: () => installedAutomation,
|
|
3070
|
-
planAutomationInstall: () => planAutomationInstall
|
|
3111
|
+
// src/setup/files.ts
|
|
3112
|
+
var files_exports = {};
|
|
3113
|
+
__export(files_exports, {
|
|
3114
|
+
applyEdit: () => applyEdit,
|
|
3115
|
+
managedBlock: () => managedBlock,
|
|
3116
|
+
readText: () => readText
|
|
3071
3117
|
});
|
|
3072
|
-
import
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3118
|
+
import fs14 from "fs/promises";
|
|
3119
|
+
import path18 from "path";
|
|
3120
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
3121
|
+
async function readText(root, file) {
|
|
3122
|
+
try {
|
|
3123
|
+
const text2 = await readBoundedFile(await storePath(root, file), 2 * 1024 * 1024);
|
|
3124
|
+
if (text2 === null) throw new Error("Setup input is not a regular file or exceeds 2 MiB: " + file);
|
|
3125
|
+
return text2;
|
|
3126
|
+
} catch (error) {
|
|
3127
|
+
if (error.code === "ENOENT") return null;
|
|
3128
|
+
throw error;
|
|
3129
|
+
}
|
|
3076
3130
|
}
|
|
3077
|
-
|
|
3078
|
-
const
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
const desired = hookConfig(host, command2);
|
|
3082
|
-
const newCommand = desired.hooks.SessionStart[0].hooks[0].command;
|
|
3083
|
-
const previous = record.hosts[host]?.command;
|
|
3084
|
-
const hooks = existing.hooks ?? {};
|
|
3085
|
-
for (const event of HOOK_EVENTS) {
|
|
3086
|
-
hooks[event] = (hooks[event] ?? []).map((group) => ({
|
|
3087
|
-
...group,
|
|
3088
|
-
hooks: group.hooks.filter((handler) => !(handler.type === "command" && typeof handler.command === "string" && (handler.command === previous || handler.command === newCommand)))
|
|
3089
|
-
})).filter((group) => group.hooks.length);
|
|
3090
|
-
hooks[event].push(...desired.hooks[event]);
|
|
3131
|
+
function managedBlock(text2, start, end, body) {
|
|
3132
|
+
const starts = text2.split(start).length - 1, ends = text2.split(end).length - 1;
|
|
3133
|
+
if (starts !== ends || starts > 1 || starts === 1 && text2.indexOf(end) < text2.indexOf(start)) {
|
|
3134
|
+
throw new Error("Ambiguous Mason instruction/configuration markers; repair the marked block before setup.");
|
|
3091
3135
|
}
|
|
3092
|
-
|
|
3093
|
-
|
|
3136
|
+
const eol = text2.includes("\r\n") ? "\r\n" : "\n";
|
|
3137
|
+
const block = [start, body.replace(/\r?\n/g, eol), end].join(eol);
|
|
3138
|
+
if (starts) return text2.slice(0, text2.indexOf(start)) + block + text2.slice(text2.indexOf(end) + end.length);
|
|
3139
|
+
return text2 + (text2.length && !text2.endsWith("\n") ? eol : "") + (text2.length ? eol : "") + block + eol;
|
|
3094
3140
|
}
|
|
3095
|
-
async function
|
|
3096
|
-
const
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3141
|
+
async function applyEdit(root, edit) {
|
|
3142
|
+
const current = await readText(root, edit.path);
|
|
3143
|
+
if (current === edit.after) return false;
|
|
3144
|
+
if (current !== edit.before) throw new Error("Integration input changed during update: " + edit.path + ". Rerun the requested command to resume.");
|
|
3145
|
+
if (edit.after === null) {
|
|
3146
|
+
const file2 = await storePath(root, edit.path);
|
|
3147
|
+
if (await readText(root, edit.path) !== edit.before) throw new Error("Input changed during removal: " + edit.path);
|
|
3148
|
+
await fs14.unlink(file2);
|
|
3149
|
+
return true;
|
|
3150
|
+
}
|
|
3151
|
+
const file = await storePath(root, edit.path, true);
|
|
3152
|
+
const temporary = path18.join(path18.dirname(file), ".mason-setup-" + randomUUID5() + ".tmp");
|
|
3153
|
+
try {
|
|
3154
|
+
const mode = await fs14.stat(file).then((s) => s.mode & 511, () => 384);
|
|
3155
|
+
const handle = await fs14.open(temporary, "wx", mode);
|
|
3156
|
+
try {
|
|
3157
|
+
await handle.writeFile(edit.after, "utf8");
|
|
3158
|
+
await handle.sync();
|
|
3159
|
+
} finally {
|
|
3160
|
+
await handle.close();
|
|
3161
|
+
}
|
|
3162
|
+
if (await readText(root, edit.path) !== edit.before) throw new Error("Integration input changed during update: " + edit.path);
|
|
3163
|
+
await fs14.rename(temporary, file);
|
|
3164
|
+
return true;
|
|
3165
|
+
} finally {
|
|
3166
|
+
await fs14.rm(temporary, { force: true });
|
|
3167
|
+
}
|
|
3109
3168
|
}
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3169
|
+
var init_files2 = __esm({
|
|
3170
|
+
"src/setup/files.ts"() {
|
|
3171
|
+
"use strict";
|
|
3172
|
+
init_files();
|
|
3173
|
+
init_storage();
|
|
3174
|
+
}
|
|
3175
|
+
});
|
|
3176
|
+
|
|
3177
|
+
// src/setup/ownership.ts
|
|
3178
|
+
var ownership_exports = {};
|
|
3179
|
+
__export(ownership_exports, {
|
|
3180
|
+
BLOCKS: () => BLOCKS,
|
|
3181
|
+
OWNERSHIP_PATH: () => OWNERSHIP_PATH,
|
|
3182
|
+
blockSpan: () => blockSpan,
|
|
3183
|
+
completedHookOwnership: () => completedHookOwnership,
|
|
3184
|
+
ownershipEdit: () => ownershipEdit,
|
|
3185
|
+
ownershipSchema: () => ownershipSchema
|
|
3186
|
+
});
|
|
3187
|
+
import { z as z10 } from "zod";
|
|
3188
|
+
import { parse } from "smol-toml";
|
|
3189
|
+
function blockSpan(text2, start, end) {
|
|
3190
|
+
const starts = text2.split(start).length - 1, ends = text2.split(end).length - 1;
|
|
3191
|
+
if (!starts && !ends) return null;
|
|
3192
|
+
const from = text2.indexOf(start), to = text2.indexOf(end) + end.length;
|
|
3193
|
+
if (starts !== 1 || ends !== 1 || to <= from) throw new Error("Ambiguous Mason markers; inspect the marked block manually.");
|
|
3194
|
+
return { from, to, text: text2.slice(from, to) };
|
|
3195
|
+
}
|
|
3196
|
+
async function ownershipEdit(root, edits, hookCommands = {}) {
|
|
3197
|
+
const before = await readText(root, OWNERSHIP_PATH);
|
|
3198
|
+
const record = before === null ? { version: 1, files: {} } : ownershipSchema.parse(JSON.parse(before));
|
|
3199
|
+
for (const edit of edits) {
|
|
3200
|
+
const previous = record.files[edit.path];
|
|
3201
|
+
const entry = {
|
|
3202
|
+
created: edit.before === null || (previous?.created ?? false),
|
|
3203
|
+
initialHash: edit.before === null || previous?.initialHash === hash(edit.before) ? hash(edit.after) : previous?.initialHash ?? hash(edit.after),
|
|
3204
|
+
blocks: Object.fromEntries(BLOCKS.flatMap(([start, end]) => {
|
|
3205
|
+
const block = blockSpan(edit.after, start, end);
|
|
3206
|
+
return block ? [[start, hash(block.text)]] : [];
|
|
3207
|
+
}))
|
|
3208
|
+
};
|
|
3209
|
+
const mcp = edit.path === ".codex/config.toml" ? parse(edit.after).mcp_servers?.mason : edit.path === ".mcp.json" ? JSON.parse(edit.after).mcpServers?.mason : void 0;
|
|
3210
|
+
const hooks = edit.path === ".codex/hooks.json" || edit.path === ".claude/settings.json" ? JSON.parse(edit.after).hooks : void 0;
|
|
3211
|
+
record.files[edit.path] = {
|
|
3212
|
+
...entry,
|
|
3213
|
+
...mcp === void 0 ? {} : { mcpHash: hash(mcp) },
|
|
3214
|
+
...hookCommands[edit.path] ? { hookCommands: [.../* @__PURE__ */ new Set([...previous?.hookCommands ?? [], hookCommands[edit.path]])] } : {},
|
|
3215
|
+
...hooks === void 0 ? {} : { hooks: Object.fromEntries(Object.entries(hooks).map(([event, groups]) => [event, groups.flatMap((group) => group.hooks.map(hash))])) }
|
|
3126
3216
|
};
|
|
3127
3217
|
}
|
|
3128
|
-
return
|
|
3218
|
+
return { path: OWNERSHIP_PATH, before, after: JSON.stringify(record, null, 2) + "\n" };
|
|
3129
3219
|
}
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3220
|
+
async function completedHookOwnership(root, file, command2) {
|
|
3221
|
+
const before = await readText(root, OWNERSHIP_PATH);
|
|
3222
|
+
const record = ownershipSchema.parse(JSON.parse(before ?? "null"));
|
|
3223
|
+
record.files[file].hookCommands = [command2];
|
|
3224
|
+
return { path: OWNERSHIP_PATH, before, after: JSON.stringify(record, null, 2) + "\n" };
|
|
3225
|
+
}
|
|
3226
|
+
var OWNERSHIP_PATH, BLOCKS, fileSchema, ownershipSchema;
|
|
3227
|
+
var init_ownership = __esm({
|
|
3228
|
+
"src/setup/ownership.ts"() {
|
|
3133
3229
|
"use strict";
|
|
3134
|
-
init_storage();
|
|
3135
3230
|
init_evidence();
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3231
|
+
init_files2();
|
|
3232
|
+
OWNERSHIP_PATH = ".mason/local/integration.json";
|
|
3233
|
+
BLOCKS = [
|
|
3234
|
+
["<!-- mason:start -->", "<!-- mason:end -->"],
|
|
3235
|
+
["<!-- mason:agents:start -->", "<!-- mason:agents:end -->"],
|
|
3236
|
+
["# mason:mcp:start", "# mason:mcp:end"]
|
|
3237
|
+
];
|
|
3238
|
+
fileSchema = z10.object({
|
|
3239
|
+
created: z10.boolean(),
|
|
3240
|
+
initialHash: z10.string(),
|
|
3241
|
+
blocks: z10.record(z10.string()),
|
|
3242
|
+
mcpHash: z10.string().optional(),
|
|
3243
|
+
hookCommands: z10.array(z10.string()).optional(),
|
|
3244
|
+
hooks: z10.record(z10.array(z10.string())).optional()
|
|
3245
|
+
});
|
|
3246
|
+
ownershipSchema = z10.object({ version: z10.literal(1), files: z10.record(fileSchema) });
|
|
3142
3247
|
}
|
|
3143
3248
|
});
|
|
3144
3249
|
|
|
3145
3250
|
// src/mcp/init.ts
|
|
3146
3251
|
import { z as z11 } from "zod";
|
|
3147
|
-
async function loadProjectMarker(rootDir) {
|
|
3148
|
-
const raw = await readStoreJson(rootDir, ".mason/project.json");
|
|
3149
|
-
return raw === null ? null : markerSchema.parse(raw);
|
|
3150
|
-
}
|
|
3151
|
-
async function saveProjectMarker(rootDir, marker) {
|
|
3152
|
-
await writeStoreJson(rootDir, ".mason/project.json", markerSchema.parse(marker));
|
|
3153
|
-
}
|
|
3154
3252
|
var markerSchema, CLAUDE_MD_SECTION, ASSISTANT_SETUP, QUICKSTART_PLAYBOOK, MAP_PLAYBOOK;
|
|
3155
3253
|
var init_init = __esm({
|
|
3156
3254
|
"src/mcp/init.ts"() {
|
|
3157
3255
|
"use strict";
|
|
3158
3256
|
init_storage();
|
|
3257
|
+
init_files2();
|
|
3159
3258
|
markerSchema = z11.object({
|
|
3160
3259
|
version: z11.literal(1),
|
|
3161
3260
|
initializedAt: z11.string(),
|
|
3162
|
-
features: z11.object({ confluence: z11.boolean().optional() }).optional()
|
|
3261
|
+
features: z11.object({ confluence: z11.boolean().optional() }).passthrough().optional()
|
|
3163
3262
|
}).passthrough();
|
|
3164
3263
|
CLAUDE_MD_SECTION = `<!-- mason:start -->
|
|
3165
3264
|
## Mason project knowledge
|
|
@@ -3181,7 +3280,7 @@ Mason provides recorded decisions and file impact over MCP. A concept map is opt
|
|
|
3181
3280
|
Inspect source for what the retrieved context does not answer.
|
|
3182
3281
|
<!-- mason:end -->`;
|
|
3183
3282
|
ASSISTANT_SETUP = `UNIFIED SETUP
|
|
3184
|
-
When the user requests Mason setup, call mason_init again with mode: "setup" and host: "codex" or "claude" for the assistant being configured. This uses the same engine as mason-auto setup: retain the initial audit before instruction edits,
|
|
3283
|
+
When the user requests Mason setup, call mason_init again with mode: "setup" and host: "codex" or "claude" for the assistant being configured. This uses the same engine as mason-auto setup: retain the initial audit before instruction edits, use the installed mason command on PATH, merge project MCP configuration and hooks, update marker-delimited instructions and Git ignore rules, and record setup. A concept map is optional.
|
|
3185
3284
|
If the user only requested inspection or review, report findings without running setup. The setup result distinguishes configured from observed activation. Explain the host's native trust step and request a new session; never trust hooks on the user's behalf or claim activation from generated configuration alone. After a normal task finishes, mason_automation(action: "status") or mason-auto status reports observed context use and hook events. Existing findings remain reviewable; setup does not approve advisories or manufacture decisions.
|
|
3186
3285
|
|
|
3187
3286
|
The managed project guidance is:
|
|
@@ -3189,7 +3288,7 @@ ${CLAUDE_MD_SECTION}`;
|
|
|
3189
3288
|
QUICKSTART_PLAYBOOK = `Start with the audit and review results included in this response. No concept map is required.
|
|
3190
3289
|
|
|
3191
3290
|
1. Explain the actionable findings with their source evidence. Separate audit issues, advisories, and skipped checks. The review covers committed changes from the merge base to HEAD; workingTree paths are not included in that review. An unavailable or empty check is not proof that the project is correct. Use the CLI for full output if a summary is truncated.
|
|
3192
|
-
2. Address findings within the user's requested scope. Setup alone authorizes
|
|
3291
|
+
2. Address findings within the user's requested scope. Setup alone authorizes Mason host configuration, hooks, instructions, and ignore rules; rewriting existing project claims requires repair scope. If repair is authorized, call \`mason_repair(dir, action: "prepare")\` before editing; it saves the full original findings even when this summary is truncated. Inspect relevant source, make grounded edits, then call \`mason_repair(dir, action: "verify", baselinePath)\` with that same baseline, including after any final doc commit. Report resolved, unresolved, review-required, unverified, and new findings. Keep suppressed advisories visible even when setup has already dirtied a doc. Do not invent a decision just to populate the store.
|
|
3193
3292
|
3. When the task reveals a real lesson or constraint, call \`save_decision\` with title, body, category, anchors, and known owner/source/actor information. Missing attribution can be added later. The tool writes a local proposal; editing it preserves revision history and requires a new acceptance, while any earlier accepted revision remains operative. An unchanged save never refreshes its evidence. When decision review is requested, \`review_decision\` prepares the record and code evidence before any authorized verdict. Review and commit records through the normal workflow. Retrieve it on the next relevant task with \`get_context(dir, task, files)\`.
|
|
3194
3293
|
|
|
3195
3294
|
${ASSISTANT_SETUP}
|
|
@@ -3231,138 +3330,630 @@ If a build is interrupted, its partials remain in \`.mason/partial-snapshots/\`.
|
|
|
3231
3330
|
}
|
|
3232
3331
|
});
|
|
3233
3332
|
|
|
3234
|
-
// src/
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
try {
|
|
3239
|
-
const { stdout: shallow } = await exec8("git", ["rev-parse", "--is-shallow-repository"], { cwd: resolvedRoot });
|
|
3240
|
-
if (shallow.trim() === "true") return null;
|
|
3241
|
-
const { stdout } = await exec8(
|
|
3242
|
-
"git",
|
|
3243
|
-
["log", `-n${HISTORY_COMMITS}`, "--format=%x01", "--name-only", "-M"],
|
|
3244
|
-
{ cwd: resolvedRoot, maxBuffer: 100 * 1024 * 1024 }
|
|
3245
|
-
);
|
|
3246
|
-
const commitsByFile = /* @__PURE__ */ new Map();
|
|
3247
|
-
const blocks = stdout.split("");
|
|
3248
|
-
let index2 = 0;
|
|
3249
|
-
for (const block of blocks) {
|
|
3250
|
-
const files = block.split("\n").map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith(".mason/"));
|
|
3251
|
-
if (files.length === 0 || files.length > MAX_COMMIT_FILES) continue;
|
|
3252
|
-
for (const file of files) {
|
|
3253
|
-
let set = commitsByFile.get(file);
|
|
3254
|
-
if (!set) {
|
|
3255
|
-
set = /* @__PURE__ */ new Set();
|
|
3256
|
-
commitsByFile.set(file, set);
|
|
3257
|
-
}
|
|
3258
|
-
set.add(index2);
|
|
3259
|
-
}
|
|
3260
|
-
index2++;
|
|
3261
|
-
}
|
|
3262
|
-
return { commitsByFile, totalCommits: index2 };
|
|
3263
|
-
} catch {
|
|
3264
|
-
return null;
|
|
3265
|
-
}
|
|
3266
|
-
}
|
|
3267
|
-
async function findMissingPartners(resolvedRoot, changedFiles, existsOnDisk, allChangedFiles = changedFiles) {
|
|
3268
|
-
const matrix = await buildMatrix(resolvedRoot);
|
|
3269
|
-
if (!matrix) return null;
|
|
3270
|
-
const changedSet = new Set(allChangedFiles);
|
|
3271
|
-
const findings = [];
|
|
3272
|
-
for (const changedFile of changedFiles) {
|
|
3273
|
-
const fileCommits = matrix.commitsByFile.get(changedFile);
|
|
3274
|
-
if (!fileCommits || fileCommits.size < MIN_FILE_COMMITS) continue;
|
|
3275
|
-
for (const [partner, partnerCommits] of matrix.commitsByFile) {
|
|
3276
|
-
if (partner === changedFile || changedSet.has(partner)) continue;
|
|
3277
|
-
let shared = 0;
|
|
3278
|
-
for (const c of fileCommits) {
|
|
3279
|
-
if (partnerCommits.has(c)) shared++;
|
|
3280
|
-
}
|
|
3281
|
-
if (shared < MIN_SHARED_COMMITS) continue;
|
|
3282
|
-
const rate = shared / fileCommits.size;
|
|
3283
|
-
if (rate < MIN_COCHANGE_RATE) continue;
|
|
3284
|
-
if (!await existsOnDisk(partner)) continue;
|
|
3285
|
-
findings.push({
|
|
3286
|
-
changedFile,
|
|
3287
|
-
missingPartner: partner,
|
|
3288
|
-
sharedCommits: shared,
|
|
3289
|
-
fileCommits: fileCommits.size,
|
|
3290
|
-
rate: Math.round(rate * 100) / 100
|
|
3291
|
-
});
|
|
3292
|
-
}
|
|
3293
|
-
}
|
|
3294
|
-
findings.sort((a, b) => b.rate - a.rate || b.sharedCommits - a.sharedCommits);
|
|
3295
|
-
return findings;
|
|
3296
|
-
}
|
|
3297
|
-
var exec8, HISTORY_COMMITS, MIN_FILE_COMMITS, MIN_SHARED_COMMITS, MIN_COCHANGE_RATE, MAX_COMMIT_FILES;
|
|
3298
|
-
var init_cochange = __esm({
|
|
3299
|
-
"src/review/cochange.ts"() {
|
|
3300
|
-
"use strict";
|
|
3301
|
-
exec8 = promisify9(execFile9);
|
|
3302
|
-
HISTORY_COMMITS = 1500;
|
|
3303
|
-
MIN_FILE_COMMITS = 5;
|
|
3304
|
-
MIN_SHARED_COMMITS = 4;
|
|
3305
|
-
MIN_COCHANGE_RATE = 0.6;
|
|
3306
|
-
MAX_COMMIT_FILES = 30;
|
|
3307
|
-
}
|
|
3333
|
+
// src/utils/git.ts
|
|
3334
|
+
var git_exports = {};
|
|
3335
|
+
__export(git_exports, {
|
|
3336
|
+
isGitRepo: () => isGitRepo
|
|
3308
3337
|
});
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
function
|
|
3312
|
-
if (value.length > 4e3) return null;
|
|
3313
|
-
let file = value;
|
|
3338
|
+
import { execFile as execFile10 } from "child_process";
|
|
3339
|
+
import { promisify as promisify10 } from "util";
|
|
3340
|
+
async function isGitRepo(dir) {
|
|
3314
3341
|
try {
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
const url = new URL(file);
|
|
3318
|
-
if (url.hostname && url.hostname !== "localhost") return null;
|
|
3319
|
-
file = decodeURIComponent(url.pathname).replace(/^\/([A-Za-z]:\/)/, "$1");
|
|
3320
|
-
} else {
|
|
3321
|
-
if (/^[a-z][a-z0-9+.-]*:/i.test(file)) return null;
|
|
3322
|
-
file = decodeURIComponent(file);
|
|
3323
|
-
}
|
|
3324
|
-
}
|
|
3342
|
+
await exec8("git", ["rev-parse", "--git-dir"], { cwd: dir });
|
|
3343
|
+
return true;
|
|
3325
3344
|
} catch {
|
|
3326
|
-
return
|
|
3327
|
-
}
|
|
3328
|
-
file = file.replace(/\\/g, "/");
|
|
3329
|
-
const root = sourceRoot.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
3330
|
-
if (file.startsWith("/") || /^[A-Za-z]:/.test(file)) {
|
|
3331
|
-
if (!file.startsWith(root + "/")) return null;
|
|
3332
|
-
file = file.slice(root.length + 1);
|
|
3345
|
+
return false;
|
|
3333
3346
|
}
|
|
3334
|
-
return normalizeRepoPath(file);
|
|
3335
3347
|
}
|
|
3336
|
-
var
|
|
3337
|
-
|
|
3348
|
+
var exec8;
|
|
3349
|
+
var init_git2 = __esm({
|
|
3350
|
+
"src/utils/git.ts"() {
|
|
3338
3351
|
"use strict";
|
|
3339
|
-
|
|
3352
|
+
exec8 = promisify10(execFile10);
|
|
3340
3353
|
}
|
|
3341
3354
|
});
|
|
3342
3355
|
|
|
3343
|
-
// src/
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
}
|
|
3356
|
+
// src/setup/config.ts
|
|
3357
|
+
var config_exports = {};
|
|
3358
|
+
__export(config_exports, {
|
|
3359
|
+
ancillaryEdits: () => ancillaryEdits,
|
|
3360
|
+
hookEdits: () => hookEdits,
|
|
3361
|
+
inspectHostConfig: () => inspectHostConfig,
|
|
3362
|
+
instructionEdits: () => instructionEdits,
|
|
3363
|
+
mcpEdit: () => mcpEdit,
|
|
3364
|
+
mcpPath: () => mcpPath
|
|
3353
3365
|
});
|
|
3354
|
-
|
|
3355
|
-
|
|
3366
|
+
import { isDeepStrictEqual } from "util";
|
|
3367
|
+
import { parse as parse2, stringify } from "smol-toml";
|
|
3356
3368
|
import { z as z12 } from "zod";
|
|
3357
|
-
function
|
|
3358
|
-
const
|
|
3359
|
-
const
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3369
|
+
async function instructionEdits(root, host) {
|
|
3370
|
+
const found = await Promise.all(DOC_CANDIDATES.map(async (file) => ({ path: file, text: await readText(root, file) })));
|
|
3371
|
+
const existing = found.find((f) => f.text !== null);
|
|
3372
|
+
const primary = host === "codex" && found[0].text === null ? { path: "AGENTS.md", text: null } : existing ?? { path: "AGENTS.md", text: null };
|
|
3373
|
+
const originalGuidance = primary.text === null && existing ? "Follow the existing project conventions in " + existing.path + ".\n" : "";
|
|
3374
|
+
const edits = [{
|
|
3375
|
+
path: primary.path,
|
|
3376
|
+
before: primary.text,
|
|
3377
|
+
after: managedBlock(primary.text ?? originalGuidance, "<!-- mason:start -->", "<!-- mason:end -->", CLAUDE_MD_SECTION.split("\n").slice(1, -1).join("\n"))
|
|
3378
|
+
}];
|
|
3379
|
+
if (primary.path === "AGENTS.md") {
|
|
3380
|
+
const secondary = found.filter((f) => f.path !== "AGENTS.md" && f.text !== null);
|
|
3381
|
+
if (host === "claude" && !secondary.length) secondary.push({ path: "CLAUDE.md", text: null });
|
|
3382
|
+
for (const file of secondary) {
|
|
3383
|
+
const text2 = file.text ?? "";
|
|
3384
|
+
const legacy = text2.includes("<!-- mason:start -->") || text2.includes("<!-- mason:end -->");
|
|
3385
|
+
const imported = "@" + (file.path.startsWith(".claude/") ? "../AGENTS.md" : "AGENTS.md");
|
|
3386
|
+
if (text2.trim() === imported) continue;
|
|
3387
|
+
const pointer = "Mason project knowledge and shared project instructions:\n" + imported;
|
|
3388
|
+
edits.push({
|
|
3389
|
+
path: file.path,
|
|
3390
|
+
before: file.text,
|
|
3391
|
+
after: managedBlock(text2, legacy ? "<!-- mason:start -->" : POINTER_START, legacy ? "<!-- mason:end -->" : POINTER_END, pointer)
|
|
3392
|
+
});
|
|
3393
|
+
}
|
|
3394
|
+
}
|
|
3395
|
+
return edits;
|
|
3396
|
+
}
|
|
3397
|
+
function managedMcp(previous, host) {
|
|
3398
|
+
const options = previous === void 0 ? {} : object.parse(previous);
|
|
3399
|
+
for (const key of ["command", "args", "url", "type", "headers", "http_headers", "env_http_headers", "bearer_token_env_var"]) delete options[key];
|
|
3400
|
+
return { ...options, ...mcpCommand(host) };
|
|
3401
|
+
}
|
|
3402
|
+
async function mcpEdit(root, host) {
|
|
3403
|
+
const file = mcpPath(host), before = await readText(root, file);
|
|
3404
|
+
if (host === "claude") {
|
|
3405
|
+
const config2 = before === null ? {} : object.parse(JSON.parse(before));
|
|
3406
|
+
const servers2 = object.parse(config2.mcpServers ?? {});
|
|
3407
|
+
return { path: file, before, after: JSON.stringify({ ...config2, mcpServers: { ...servers2, mason: managedMcp(servers2.mason, host) } }, null, 2) + "\n" };
|
|
3408
|
+
}
|
|
3409
|
+
const config = parse2(before ?? "");
|
|
3410
|
+
const managed = (before ?? "").includes(TOML_START);
|
|
3411
|
+
const servers = config.mcp_servers;
|
|
3412
|
+
const desired = managedMcp(servers?.mason, host);
|
|
3413
|
+
let base = before ?? "";
|
|
3414
|
+
if (servers?.mason && !managed) {
|
|
3415
|
+
const headers = [...base.matchAll(/^\s*\[\[?.+?\]\]?[^\S\r\n]*(?:#.*)?$/gm)];
|
|
3416
|
+
const spans = headers.flatMap((header, index2) => /^\s*\[mcp_servers\.mason(?:\.[A-Za-z0-9_-]+)*\]/.test(header[0]) ? [{ start: header.index, end: headers[index2 + 1]?.index ?? base.length }] : []);
|
|
3417
|
+
if (!spans.length) throw new Error("Cannot safely migrate the existing Mason MCP TOML entry. Use an explicit [mcp_servers.mason] table and rerun setup; the file was retained.");
|
|
3418
|
+
for (const span of spans.reverse()) base = base.slice(0, span.start) + base.slice(span.end);
|
|
3419
|
+
}
|
|
3420
|
+
const after = managedBlock(base, TOML_START, TOML_END, stringify({ mcp_servers: { mason: desired } }).trimEnd());
|
|
3421
|
+
const parsed = parse2(after);
|
|
3422
|
+
const expected = { ...config, mcp_servers: { ...servers ?? {}, mason: desired } };
|
|
3423
|
+
if (!isDeepStrictEqual(parsed, expected)) throw new Error("Could not safely configure the Mason MCP entry without changing other settings.");
|
|
3424
|
+
return { path: file, before, after };
|
|
3425
|
+
}
|
|
3426
|
+
async function hookEdits(root, host) {
|
|
3427
|
+
const file = configPath(host), before = await readText(root, file);
|
|
3428
|
+
const { planAutomationInstall: planAutomationInstall2 } = await Promise.resolve().then(() => (init_install(), install_exports));
|
|
3429
|
+
const plan = await planAutomationInstall2(root, host, hookCommand(host));
|
|
3430
|
+
return [
|
|
3431
|
+
{ path: file, before, after: JSON.stringify(plan.config, null, 2) + "\n" },
|
|
3432
|
+
{ path: AUTOMATION_PATH, before: await readText(root, AUTOMATION_PATH), after: JSON.stringify(plan.record, null, 2) + "\n" }
|
|
3433
|
+
];
|
|
3434
|
+
}
|
|
3435
|
+
async function ancillaryEdits(root) {
|
|
3436
|
+
const ignore = await readText(root, ".gitignore");
|
|
3437
|
+
const { git: git3 } = await Promise.resolve().then(() => (init_evidence(), evidence_exports));
|
|
3438
|
+
const { isGitRepo: isGitRepo2 } = await Promise.resolve().then(() => (init_git2(), git_exports));
|
|
3439
|
+
let parentIgnored = false;
|
|
3440
|
+
try {
|
|
3441
|
+
if (await isGitRepo2(root)) parentIgnored = !!(await git3(root, "check-ignore", "--no-index", ".mason")).trim();
|
|
3442
|
+
} catch (error) {
|
|
3443
|
+
if (error.code !== 1) throw error;
|
|
3444
|
+
}
|
|
3445
|
+
const retainParentRule = parentIgnored || !!ignore?.replace(/\r\n/g, "\n").includes("# mason:ignore:start\n!/.mason/\n/.mason/*");
|
|
3446
|
+
const rules = [
|
|
3447
|
+
...retainParentRule ? ["!/.mason/", "/.mason/*"] : [],
|
|
3448
|
+
"!/.mason/decisions/",
|
|
3449
|
+
"!/.mason/decisions/**",
|
|
3450
|
+
"!/.mason/config.json",
|
|
3451
|
+
"!/.mason/snapshot.json",
|
|
3452
|
+
"/.mason/local/",
|
|
3453
|
+
"/.mason/reports/"
|
|
3454
|
+
].join("\n");
|
|
3455
|
+
return [{ path: ".gitignore", before: ignore, after: managedBlock(ignore ?? "", "# mason:ignore:start", "# mason:ignore:end", rules) }];
|
|
3456
|
+
}
|
|
3457
|
+
async function inspectHostConfig(root, host, plannedMcp) {
|
|
3458
|
+
const text2 = plannedMcp ?? await readText(root, mcpPath(host));
|
|
3459
|
+
const config = host === "codex" ? parse2(text2 ?? "") : object.parse(JSON.parse(text2 ?? "{}"));
|
|
3460
|
+
const servers = host === "codex" ? config.mcp_servers : config.mcpServers;
|
|
3461
|
+
const hooks = automationConfigSchema.parse(JSON.parse(await readText(root, configPath(host)) ?? "{}"));
|
|
3462
|
+
const expected = hookConfig(host, hookCommand(host));
|
|
3463
|
+
const mcp = servers?.mason === void 0 ? null : object.parse(servers.mason);
|
|
3464
|
+
return {
|
|
3465
|
+
mcp,
|
|
3466
|
+
mcpDisabled: mcp?.enabled === false,
|
|
3467
|
+
hooks: Object.fromEntries(Object.keys(expected.hooks).map((event) => [
|
|
3468
|
+
event,
|
|
3469
|
+
(hooks.hooks?.[event] ?? []).flatMap((group) => group.hooks.filter((handler) => handler.command === expected.hooks.SessionStart[0].hooks[0].command).map((handler) => ({ ...group, hooks: [handler] })) ?? [])
|
|
3470
|
+
])),
|
|
3471
|
+
disabled: hooks.disableAllHooks === true || config.features?.hooks === false || config.features?.codex_hooks === false
|
|
3472
|
+
};
|
|
3473
|
+
}
|
|
3474
|
+
var object, mcpPath, TOML_START, TOML_END, POINTER_START, POINTER_END;
|
|
3475
|
+
var init_config = __esm({
|
|
3476
|
+
"src/setup/config.ts"() {
|
|
3477
|
+
"use strict";
|
|
3478
|
+
init_init();
|
|
3479
|
+
init_docs();
|
|
3480
|
+
init_adapters();
|
|
3481
|
+
init_install();
|
|
3482
|
+
init_files2();
|
|
3483
|
+
init_launcher();
|
|
3484
|
+
object = z12.record(z12.unknown());
|
|
3485
|
+
mcpPath = (host) => host === "codex" ? ".codex/config.toml" : ".mcp.json";
|
|
3486
|
+
TOML_START = "# mason:mcp:start";
|
|
3487
|
+
TOML_END = "# mason:mcp:end";
|
|
3488
|
+
POINTER_START = "<!-- mason:agents:start -->";
|
|
3489
|
+
POINTER_END = "<!-- mason:agents:end -->";
|
|
3490
|
+
}
|
|
3491
|
+
});
|
|
3492
|
+
|
|
3493
|
+
// src/automation/install.ts
|
|
3494
|
+
var install_exports = {};
|
|
3495
|
+
__export(install_exports, {
|
|
3496
|
+
AUTOMATION_PATH: () => AUTOMATION_PATH,
|
|
3497
|
+
automationConfigSchema: () => automationConfigSchema,
|
|
3498
|
+
configPath: () => configPath,
|
|
3499
|
+
installAutomation: () => installAutomation,
|
|
3500
|
+
installedAutomation: () => installedAutomation,
|
|
3501
|
+
planAutomationInstall: () => planAutomationInstall,
|
|
3502
|
+
recordSchema: () => recordSchema
|
|
3503
|
+
});
|
|
3504
|
+
import { z as z13 } from "zod";
|
|
3505
|
+
async function installationRecord(root) {
|
|
3506
|
+
return recordSchema.parse(await readStoreJson(root, AUTOMATION_PATH) ?? { version: 1, hosts: {} });
|
|
3507
|
+
}
|
|
3508
|
+
async function installAutomation(dir, host, command2) {
|
|
3509
|
+
const ws = await workspace(dir);
|
|
3510
|
+
return withLock(ws.root, ".mason/reports/setup-lock", () => installLocked(ws.root, host, command2));
|
|
3511
|
+
}
|
|
3512
|
+
async function planAutomationInstall(root, host, command2) {
|
|
3513
|
+
const file = configPath(host);
|
|
3514
|
+
const before = await readText(root, file);
|
|
3515
|
+
const existing = automationConfigSchema.parse(JSON.parse(before ?? "{}"));
|
|
3516
|
+
const record = await installationRecord(root);
|
|
3517
|
+
const desired = hookConfig(host, command2);
|
|
3518
|
+
const newCommand = desired.hooks.SessionStart[0].hooks[0].command;
|
|
3519
|
+
const previous = record.hosts[host]?.command;
|
|
3520
|
+
const ownershipText = await readText(root, OWNERSHIP_PATH);
|
|
3521
|
+
const pending = ownershipText === null ? [] : ownershipSchema.parse(JSON.parse(ownershipText)).files[file]?.hookCommands ?? [];
|
|
3522
|
+
const owned = /* @__PURE__ */ new Set([previous, ...pending, newCommand]);
|
|
3523
|
+
const hooks = existing.hooks ?? {};
|
|
3524
|
+
for (const event of HOOK_EVENTS) {
|
|
3525
|
+
hooks[event] = (hooks[event] ?? []).map((group) => ({
|
|
3526
|
+
...group,
|
|
3527
|
+
hooks: group.hooks.filter((handler) => !(handler.type === "command" && typeof handler.command === "string" && owned.has(handler.command)))
|
|
3528
|
+
})).filter((group) => group.hooks.length);
|
|
3529
|
+
hooks[event].push(...desired.hooks[event]);
|
|
3530
|
+
}
|
|
3531
|
+
record.hosts[host] = { command: newCommand };
|
|
3532
|
+
return { file, before, config: { ...existing, hooks }, record, newCommand };
|
|
3533
|
+
}
|
|
3534
|
+
async function installLocked(root, host, command2) {
|
|
3535
|
+
const { file, before, config, record, newCommand } = await planAutomationInstall(root, host, command2);
|
|
3536
|
+
const { ancillaryEdits: ancillaryEdits2 } = await Promise.resolve().then(() => (init_config(), config_exports));
|
|
3537
|
+
const { applyEdit: applyEdit2 } = await Promise.resolve().then(() => (init_files2(), files_exports));
|
|
3538
|
+
const { ownershipEdit: ownershipEdit2, completedHookOwnership: completedHookOwnership2 } = await Promise.resolve().then(() => (init_ownership(), ownership_exports));
|
|
3539
|
+
const edit = { path: file, before, after: JSON.stringify(config, null, 2) + "\n" };
|
|
3540
|
+
const ancillary = await ancillaryEdits2(root);
|
|
3541
|
+
await applyEdit2(root, await ownershipEdit2(root, [edit], { [file]: newCommand }));
|
|
3542
|
+
for (const edit2 of ancillary) await applyEdit2(root, edit2);
|
|
3543
|
+
await applyEdit2(root, edit);
|
|
3544
|
+
await writeStoreJson(root, AUTOMATION_PATH, record);
|
|
3545
|
+
await applyEdit2(root, await completedHookOwnership2(root, file, newCommand));
|
|
3546
|
+
return {
|
|
3547
|
+
version: 1,
|
|
3548
|
+
host,
|
|
3549
|
+
configPath: file,
|
|
3550
|
+
status: "configured",
|
|
3551
|
+
command: newCommand,
|
|
3552
|
+
events: HOOK_EVENTS,
|
|
3553
|
+
next: host === "codex" ? "Review/trust these hooks using Codex /hooks and start a new session. mason-auto status reports observed events separately from configuration." : "Start a new Claude Code session. mason-auto status reports observed events separately from configuration.",
|
|
3554
|
+
note: "Install mason-context in the project before using the default command. Ignore .mason/reports/ to keep local evidence out of commits. Hooks preserve evidence and suggest scoped repairs; they do not approve edits or decisions."
|
|
3555
|
+
};
|
|
3556
|
+
}
|
|
3557
|
+
async function installedAutomation(dir) {
|
|
3558
|
+
const ws = await workspace(dir);
|
|
3559
|
+
const record = await installationRecord(ws.root);
|
|
3560
|
+
const result = {};
|
|
3561
|
+
for (const host of ["claude", "codex"]) {
|
|
3562
|
+
const expected = record.hosts[host];
|
|
3563
|
+
if (!expected) continue;
|
|
3564
|
+
const current = automationConfigSchema.parse(await readStoreJson(ws.root, configPath(host)) ?? {});
|
|
3565
|
+
const configuredEvents = HOOK_EVENTS.filter((event) => current.hooks?.[event]?.some((group) => group.hooks.some((handler) => handler.type === "command" && handler.command === expected.command)));
|
|
3566
|
+
result[host] = {
|
|
3567
|
+
configPath: configPath(host),
|
|
3568
|
+
configuredEvents,
|
|
3569
|
+
status: current.disableAllHooks === true ? "disabled" : configuredEvents.length === HOOK_EVENTS.length ? "configured" : "incomplete",
|
|
3570
|
+
runtime: "Host version, trust, policy, and tool coverage still determine execution; inspect observed events."
|
|
3571
|
+
};
|
|
3572
|
+
}
|
|
3573
|
+
return result;
|
|
3574
|
+
}
|
|
3575
|
+
var groupSchema, automationConfigSchema, recordSchema, AUTOMATION_PATH, configPath;
|
|
3576
|
+
var init_install = __esm({
|
|
3577
|
+
"src/automation/install.ts"() {
|
|
3578
|
+
"use strict";
|
|
3579
|
+
init_storage();
|
|
3580
|
+
init_evidence();
|
|
3581
|
+
init_adapters();
|
|
3582
|
+
init_store();
|
|
3583
|
+
init_files2();
|
|
3584
|
+
init_ownership();
|
|
3585
|
+
groupSchema = z13.object({ hooks: z13.array(z13.object({ type: z13.string(), command: z13.string().optional() }).passthrough()) }).passthrough();
|
|
3586
|
+
automationConfigSchema = z13.object({ hooks: z13.record(z13.array(groupSchema)).optional() }).passthrough();
|
|
3587
|
+
recordSchema = z13.object({ version: z13.literal(1), hosts: z13.record(z13.object({ command: z13.string() })) });
|
|
3588
|
+
AUTOMATION_PATH = ".mason/local/automation.json";
|
|
3589
|
+
configPath = (host) => host === "claude" ? ".claude/settings.json" : ".codex/hooks.json";
|
|
3590
|
+
}
|
|
3591
|
+
});
|
|
3592
|
+
|
|
3593
|
+
// src/setup/teardown.ts
|
|
3594
|
+
var teardown_exports = {};
|
|
3595
|
+
__export(teardown_exports, {
|
|
3596
|
+
summarizeTeardown: () => summarizeTeardown,
|
|
3597
|
+
teardownProject: () => teardownProject
|
|
3598
|
+
});
|
|
3599
|
+
import { isDeepStrictEqual as isDeepStrictEqual2 } from "util";
|
|
3600
|
+
import { parse as parse3, stringify as stringify2 } from "smol-toml";
|
|
3601
|
+
import fg5 from "fast-glob";
|
|
3602
|
+
import { z as z14 } from "zod";
|
|
3603
|
+
async function planTeardown(root, selected) {
|
|
3604
|
+
const edits = [], diagnostics = [];
|
|
3605
|
+
const inputs = /* @__PURE__ */ new Map();
|
|
3606
|
+
const read = async (file) => {
|
|
3607
|
+
if (!inputs.has(file)) inputs.set(file, await readText(root, file));
|
|
3608
|
+
return inputs.get(file);
|
|
3609
|
+
};
|
|
3610
|
+
const attempt = async (file, action) => {
|
|
3611
|
+
try {
|
|
3612
|
+
await action();
|
|
3613
|
+
} catch (error) {
|
|
3614
|
+
diagnostics.push({ path: file, message: error instanceof Error ? error.message : String(error) });
|
|
3615
|
+
}
|
|
3616
|
+
};
|
|
3617
|
+
const ownershipBefore = await read(OWNERSHIP_PATH);
|
|
3618
|
+
const ownership = ownershipBefore === null ? { version: 1, files: {} } : ownershipSchema.parse(JSON.parse(ownershipBefore));
|
|
3619
|
+
const setupBefore = await read(SETUP_PATH);
|
|
3620
|
+
const setup = setupBefore === null ? null : setupSchema.parse(JSON.parse(setupBefore));
|
|
3621
|
+
const automationBefore = await read(AUTOMATION_PATH);
|
|
3622
|
+
const automation = automationBefore === null ? null : recordSchema.parse(JSON.parse(automationBefore));
|
|
3623
|
+
const commands = (host) => new Set([
|
|
3624
|
+
automation?.hosts[host]?.command,
|
|
3625
|
+
...ownership.files[configPath(host)]?.hookCommands ?? [],
|
|
3626
|
+
hookConfig(host, hookCommand(host)).hooks.SessionStart[0].hooks[0].command,
|
|
3627
|
+
hookConfig(host).hooks.SessionStart[0].hooks[0].command
|
|
3628
|
+
].filter((command2) => !!command2));
|
|
3629
|
+
const possibleHook = (command2, host) => commands(host).has(command2) || /\bmason(?:-auto|-hook)?\b/.test(command2);
|
|
3630
|
+
const add = (file, before, after, empty) => {
|
|
3631
|
+
edits.push({ path: file, before, after: ownership.files[file]?.created && (empty || ownership.files[file].initialHash === hash(before)) ? null : after });
|
|
3632
|
+
};
|
|
3633
|
+
let remainingHost = hosts.some((host) => !selected.includes(host) && (setup?.hosts[host] || automation?.hosts[host] || ownership.files[mcpPath(host)] || ownership.files[configPath(host)]));
|
|
3634
|
+
for (const host of hosts.filter((host2) => !selected.includes(host2))) {
|
|
3635
|
+
await attempt(mcpPath(host), async () => {
|
|
3636
|
+
const text2 = await read(mcpPath(host));
|
|
3637
|
+
const config = host === "codex" ? parse3(text2 ?? "") : object2.parse(JSON.parse(text2 ?? "{}"));
|
|
3638
|
+
const servers = object2.parse((host === "codex" ? config.mcp_servers : config.mcpServers) ?? {});
|
|
3639
|
+
remainingHost ||= servers.mason !== void 0;
|
|
3640
|
+
});
|
|
3641
|
+
await attempt(configPath(host), async () => {
|
|
3642
|
+
const config = automationConfigSchema.parse(JSON.parse(await read(configPath(host)) ?? "{}"));
|
|
3643
|
+
remainingHost ||= Object.values(config.hooks ?? {}).some((groups) => groups.some((group) => group.hooks.some((handler) => handler.command && possibleHook(handler.command, host))));
|
|
3644
|
+
});
|
|
3645
|
+
}
|
|
3646
|
+
for (const host of selected) {
|
|
3647
|
+
const mcpFile = mcpPath(host);
|
|
3648
|
+
await attempt(mcpFile, async () => {
|
|
3649
|
+
const before = await read(mcpFile);
|
|
3650
|
+
if (before === null) return;
|
|
3651
|
+
const config = host === "codex" ? parse3(before) : object2.parse(JSON.parse(before));
|
|
3652
|
+
const key = host === "codex" ? "mcp_servers" : "mcpServers";
|
|
3653
|
+
const servers = object2.parse(config[key] ?? {});
|
|
3654
|
+
if (servers.mason === void 0) {
|
|
3655
|
+
if (host === "codex" && BLOCKS[2].some((marker) => before.includes(marker))) throw new Error("The marked Mason MCP entry was removed or renamed; inspect the retained block manually.");
|
|
3656
|
+
return;
|
|
3657
|
+
}
|
|
3658
|
+
const expected = ownership.files[mcpFile]?.mcpHash ?? hash(mcpCommand(host));
|
|
3659
|
+
if (hash(servers.mason) !== expected) throw new Error("The Mason MCP entry was edited or its ownership is uncertain; retained for manual cleanup.");
|
|
3660
|
+
delete servers.mason;
|
|
3661
|
+
const desired = { ...config, [key]: servers };
|
|
3662
|
+
if (!Object.keys(servers).length) delete desired[key];
|
|
3663
|
+
if (host === "claude") {
|
|
3664
|
+
add(mcpFile, before, json(desired), !Object.keys(desired).length);
|
|
3665
|
+
return;
|
|
3666
|
+
}
|
|
3667
|
+
const [start, end] = BLOCKS[2], block = blockSpan(before, start, end);
|
|
3668
|
+
if (!block) throw new Error("Mason MCP markers are missing; retained for manual cleanup.");
|
|
3669
|
+
const expectedBlock = ownership.files[mcpFile]?.blocks[start];
|
|
3670
|
+
const canonicalBlock = managedBlock("", start, end, stringify2({ mcp_servers: { mason: mcpCommand(host) } }).trimEnd()).trimEnd();
|
|
3671
|
+
if (expectedBlock ? hash(block.text) !== expectedBlock : block.text.replace(/\r\n/g, "\n") !== canonicalBlock) {
|
|
3672
|
+
throw new Error("The Mason MCP block was edited; retained for manual cleanup.");
|
|
3673
|
+
}
|
|
3674
|
+
const after = before.slice(0, block.from) + before.slice(block.to).replace(/^\r?\n/, "");
|
|
3675
|
+
const parsed = parse3(after);
|
|
3676
|
+
if (!Object.keys(servers).length && parsed[key] !== void 0) desired[key] = {};
|
|
3677
|
+
if (!isDeepStrictEqual2(parsed, desired)) throw new Error("Removing the marked block would change other TOML settings; retained for manual cleanup.");
|
|
3678
|
+
add(mcpFile, before, after, !after.trim());
|
|
3679
|
+
});
|
|
3680
|
+
const hookFile = configPath(host);
|
|
3681
|
+
await attempt(hookFile, async () => {
|
|
3682
|
+
const before = await read(hookFile);
|
|
3683
|
+
if (before === null) return;
|
|
3684
|
+
const config = automationConfigSchema.parse(JSON.parse(before));
|
|
3685
|
+
let changed = false;
|
|
3686
|
+
for (const [event, groups] of Object.entries(config.hooks ?? {})) {
|
|
3687
|
+
const known = ownership.files[hookFile]?.hooks?.[event];
|
|
3688
|
+
const handlers = groups.flatMap((group) => group.hooks);
|
|
3689
|
+
if (known && !handlers.some((handler) => handler.type === "command" && handler.command && commands(host).has(handler.command)) && handlers.some((handler) => !known.includes(hash(handler)))) {
|
|
3690
|
+
diagnostics.push({ path: hookFile, message: "A recorded Mason hook is missing beside a new or edited handler (" + event + "); inspect ownership manually." });
|
|
3691
|
+
}
|
|
3692
|
+
const kept = groups.flatMap((group) => {
|
|
3693
|
+
const handlers2 = group.hooks.filter((handler) => {
|
|
3694
|
+
if (handler.type === "command" && handler.command && commands(host).has(handler.command)) {
|
|
3695
|
+
changed = true;
|
|
3696
|
+
return false;
|
|
3697
|
+
}
|
|
3698
|
+
if (handler.command && possibleHook(handler.command, host)) diagnostics.push({
|
|
3699
|
+
path: hookFile,
|
|
3700
|
+
message: "An unrecognized Mason hook was retained for manual cleanup (" + event + ")."
|
|
3701
|
+
});
|
|
3702
|
+
return true;
|
|
3703
|
+
});
|
|
3704
|
+
return handlers2.length === group.hooks.length ? [group] : handlers2.length ? [{ ...group, hooks: handlers2 }] : [];
|
|
3705
|
+
});
|
|
3706
|
+
if (kept.length || !groups.length) config.hooks[event] = kept;
|
|
3707
|
+
else delete config.hooks[event];
|
|
3708
|
+
}
|
|
3709
|
+
if (!changed) return;
|
|
3710
|
+
if (!Object.keys(config.hooks ?? {}).length) delete config.hooks;
|
|
3711
|
+
add(hookFile, before, json(config), !Object.keys(config).length);
|
|
3712
|
+
});
|
|
3713
|
+
}
|
|
3714
|
+
const instructionEditStart = edits.length;
|
|
3715
|
+
if (!remainingHost && !diagnostics.length) for (const file of DOC_CANDIDATES) await attempt(file, async () => {
|
|
3716
|
+
const before = await read(file);
|
|
3717
|
+
if (before === null) return;
|
|
3718
|
+
let after = before;
|
|
3719
|
+
for (const [start, end] of BLOCKS.slice(0, 2)) {
|
|
3720
|
+
const block = blockSpan(after, start, end);
|
|
3721
|
+
if (!block) continue;
|
|
3722
|
+
const pointer = "Mason project knowledge and shared project instructions:\n@" + (file.startsWith(".claude/") ? "../AGENTS.md" : "AGENTS.md");
|
|
3723
|
+
const canonical = [CLAUDE_MD_SECTION, managedBlock("", start, end, pointer).trimEnd()];
|
|
3724
|
+
const expected = ownership.files[file]?.blocks[start];
|
|
3725
|
+
if (expected ? hash(block.text) !== expected : !canonical.includes(block.text.replace(/\r\n/g, "\n"))) {
|
|
3726
|
+
throw new Error("The Mason instruction block was edited or is unrecognized; retained for manual cleanup.");
|
|
3727
|
+
}
|
|
3728
|
+
after = after.slice(0, block.from) + after.slice(block.to).replace(/^\r?\n/, "");
|
|
3729
|
+
}
|
|
3730
|
+
if (after !== before) add(file, before, after, !after.trim());
|
|
3731
|
+
});
|
|
3732
|
+
if (diagnostics.length) edits.splice(instructionEditStart);
|
|
3733
|
+
if (!diagnostics.length) {
|
|
3734
|
+
const cleanup = [];
|
|
3735
|
+
const receiptFiles = await fg5(
|
|
3736
|
+
selected.map((host) => ".mason/reports/automation/*/setup-" + host + ".json"),
|
|
3737
|
+
{ cwd: root, dot: true, onlyFiles: false, followSymbolicLinks: false }
|
|
3738
|
+
);
|
|
3739
|
+
if (receiptFiles.length > 1e3) throw new Error("Too many setup receipts; inspect local setup history before teardown.");
|
|
3740
|
+
for (const file of receiptFiles.sort()) await attempt(file, async () => {
|
|
3741
|
+
const host = file.endsWith("setup-codex.json") ? "codex" : "claude";
|
|
3742
|
+
await loadSetupReceipt(root, file.slice(0, file.lastIndexOf("/")), host);
|
|
3743
|
+
cleanup.push({ path: file, before: await read(file), after: null });
|
|
3744
|
+
});
|
|
3745
|
+
if (!remainingHost) await attempt(".mason/local/project.json", async () => {
|
|
3746
|
+
const before = await read(".mason/local/project.json");
|
|
3747
|
+
if (before === null) return;
|
|
3748
|
+
z14.object({ version: z14.literal(1), initializedAt: z14.string() }).strict().parse(JSON.parse(before));
|
|
3749
|
+
cleanup.push({ path: ".mason/local/project.json", before, after: null });
|
|
3750
|
+
});
|
|
3751
|
+
if (!diagnostics.length) {
|
|
3752
|
+
for (const host of selected) {
|
|
3753
|
+
if (setup) delete setup.hosts[host];
|
|
3754
|
+
if (automation) delete automation.hosts[host];
|
|
3755
|
+
delete ownership.files[mcpPath(host)];
|
|
3756
|
+
delete ownership.files[configPath(host)];
|
|
3757
|
+
}
|
|
3758
|
+
if (!remainingHost) for (const file of DOC_CANDIDATES) delete ownership.files[file];
|
|
3759
|
+
if (setupBefore !== null) cleanup.push({ path: SETUP_PATH, before: setupBefore, after: Object.keys(setup.hosts).length ? json(setup) : null });
|
|
3760
|
+
if (automationBefore !== null) cleanup.push({ path: AUTOMATION_PATH, before: automationBefore, after: Object.keys(automation.hosts).length ? json(automation) : null });
|
|
3761
|
+
if (ownershipBefore !== null) cleanup.push({ path: OWNERSHIP_PATH, before: ownershipBefore, after: Object.keys(ownership.files).length ? json(ownership) : null });
|
|
3762
|
+
edits.push(...cleanup);
|
|
3763
|
+
}
|
|
3764
|
+
}
|
|
3765
|
+
return { edits: edits.filter((edit) => edit.before !== edit.after), diagnostics, remainingHost, inputs };
|
|
3766
|
+
}
|
|
3767
|
+
async function teardownProject(dir, options = {}) {
|
|
3768
|
+
const { root } = await workspace(dir);
|
|
3769
|
+
const selected = options.host ? [options.host] : [...hosts];
|
|
3770
|
+
const run = async (apply) => {
|
|
3771
|
+
const plan = await planTeardown(root, selected);
|
|
3772
|
+
if (apply) {
|
|
3773
|
+
for (const [file, before] of plan.inputs) if (await readText(root, file) !== before) throw new Error("Input changed during teardown: " + file + ". Rerun teardown.");
|
|
3774
|
+
for (const edit of plan.edits) await applyEdit(root, edit);
|
|
3775
|
+
}
|
|
3776
|
+
return {
|
|
3777
|
+
version: 1,
|
|
3778
|
+
action: "teardown",
|
|
3779
|
+
status: plan.diagnostics.length ? "incomplete" : "complete",
|
|
3780
|
+
root,
|
|
3781
|
+
hosts: selected,
|
|
3782
|
+
dryRun: !!options.dryRun,
|
|
3783
|
+
changes: plan.edits.map((edit) => ({ path: edit.path, action: edit.after === null ? "delete" : "update" })),
|
|
3784
|
+
diagnostics: plan.diagnostics,
|
|
3785
|
+
sharedInstructionsRetained: plan.remainingHost || !!plan.diagnostics.length,
|
|
3786
|
+
next: plan.diagnostics.length ? "Inspect the retained entries, remove or correct them, then rerun teardown." : "Restart running assistants to unload the project integration. Run mason setup --host codex or --host claude to reconnect."
|
|
3787
|
+
};
|
|
3788
|
+
};
|
|
3789
|
+
const preview = await run(false);
|
|
3790
|
+
if (options.dryRun || !preview.changes.length) return preview;
|
|
3791
|
+
return withLock(root, ".mason/reports/setup-lock", () => run(true));
|
|
3792
|
+
}
|
|
3793
|
+
function summarizeTeardown(result) {
|
|
3794
|
+
return [
|
|
3795
|
+
`Mason teardown${result.dryRun ? " preview" : ""}: ${result.status}.`,
|
|
3796
|
+
...result.changes.map((change) => ` ${result.dryRun ? "Would " : ""}${change.action}: ${change.path}`),
|
|
3797
|
+
...result.diagnostics.map((item) => ` Retained ${item.path}: ${item.message}`),
|
|
3798
|
+
...!result.changes.length ? ["No changes."] : [],
|
|
3799
|
+
"Shared knowledge, repair evidence, ignore rules and the installed Mason command are retained.",
|
|
3800
|
+
...result.sharedInstructionsRetained ? ["Shared instruction cleanup is deferred while another host or incomplete removal needs inspection."] : [],
|
|
3801
|
+
...result.dryRun ? ["Preview only; no files were changed."] : [result.next]
|
|
3802
|
+
].join("\n");
|
|
3803
|
+
}
|
|
3804
|
+
var hosts, object2, json;
|
|
3805
|
+
var init_teardown = __esm({
|
|
3806
|
+
"src/setup/teardown.ts"() {
|
|
3807
|
+
"use strict";
|
|
3808
|
+
init_evidence();
|
|
3809
|
+
init_store();
|
|
3810
|
+
init_install();
|
|
3811
|
+
init_adapters();
|
|
3812
|
+
init_docs();
|
|
3813
|
+
init_init();
|
|
3814
|
+
init_files2();
|
|
3815
|
+
init_config();
|
|
3816
|
+
init_launcher();
|
|
3817
|
+
init_model();
|
|
3818
|
+
init_ownership();
|
|
3819
|
+
hosts = ["codex", "claude"];
|
|
3820
|
+
object2 = z14.record(z14.unknown());
|
|
3821
|
+
json = (value) => JSON.stringify(value, null, 2) + "\n";
|
|
3822
|
+
}
|
|
3823
|
+
});
|
|
3824
|
+
|
|
3825
|
+
// src/review/cochange.ts
|
|
3826
|
+
import { execFile as execFile11 } from "child_process";
|
|
3827
|
+
import { promisify as promisify11 } from "util";
|
|
3828
|
+
async function buildMatrix(resolvedRoot) {
|
|
3829
|
+
try {
|
|
3830
|
+
const { stdout: shallow } = await exec9("git", ["rev-parse", "--is-shallow-repository"], { cwd: resolvedRoot });
|
|
3831
|
+
if (shallow.trim() === "true") return null;
|
|
3832
|
+
const { stdout } = await exec9(
|
|
3833
|
+
"git",
|
|
3834
|
+
["log", `-n${HISTORY_COMMITS}`, "--format=%x01", "--name-only", "-M"],
|
|
3835
|
+
{ cwd: resolvedRoot, maxBuffer: 100 * 1024 * 1024 }
|
|
3836
|
+
);
|
|
3837
|
+
const commitsByFile = /* @__PURE__ */ new Map();
|
|
3838
|
+
const blocks = stdout.split("");
|
|
3839
|
+
let index2 = 0;
|
|
3840
|
+
for (const block of blocks) {
|
|
3841
|
+
const files = block.split("\n").map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith(".mason/"));
|
|
3842
|
+
if (files.length === 0 || files.length > MAX_COMMIT_FILES) continue;
|
|
3843
|
+
for (const file of files) {
|
|
3844
|
+
let set = commitsByFile.get(file);
|
|
3845
|
+
if (!set) {
|
|
3846
|
+
set = /* @__PURE__ */ new Set();
|
|
3847
|
+
commitsByFile.set(file, set);
|
|
3848
|
+
}
|
|
3849
|
+
set.add(index2);
|
|
3850
|
+
}
|
|
3851
|
+
index2++;
|
|
3852
|
+
}
|
|
3853
|
+
return { commitsByFile, totalCommits: index2 };
|
|
3854
|
+
} catch {
|
|
3855
|
+
return null;
|
|
3856
|
+
}
|
|
3857
|
+
}
|
|
3858
|
+
async function findMissingPartners(resolvedRoot, changedFiles, existsOnDisk, allChangedFiles = changedFiles) {
|
|
3859
|
+
const matrix = await buildMatrix(resolvedRoot);
|
|
3860
|
+
if (!matrix) return null;
|
|
3861
|
+
const changedSet = new Set(allChangedFiles);
|
|
3862
|
+
const findings = [];
|
|
3863
|
+
for (const changedFile of changedFiles) {
|
|
3864
|
+
const fileCommits = matrix.commitsByFile.get(changedFile);
|
|
3865
|
+
if (!fileCommits || fileCommits.size < MIN_FILE_COMMITS) continue;
|
|
3866
|
+
for (const [partner, partnerCommits] of matrix.commitsByFile) {
|
|
3867
|
+
if (partner === changedFile || changedSet.has(partner)) continue;
|
|
3868
|
+
let shared = 0;
|
|
3869
|
+
for (const c of fileCommits) {
|
|
3870
|
+
if (partnerCommits.has(c)) shared++;
|
|
3871
|
+
}
|
|
3872
|
+
if (shared < MIN_SHARED_COMMITS) continue;
|
|
3873
|
+
const rate = shared / fileCommits.size;
|
|
3874
|
+
if (rate < MIN_COCHANGE_RATE) continue;
|
|
3875
|
+
if (!await existsOnDisk(partner)) continue;
|
|
3876
|
+
findings.push({
|
|
3877
|
+
changedFile,
|
|
3878
|
+
missingPartner: partner,
|
|
3879
|
+
sharedCommits: shared,
|
|
3880
|
+
fileCommits: fileCommits.size,
|
|
3881
|
+
rate: Math.round(rate * 100) / 100
|
|
3882
|
+
});
|
|
3883
|
+
}
|
|
3884
|
+
}
|
|
3885
|
+
findings.sort((a, b) => b.rate - a.rate || b.sharedCommits - a.sharedCommits);
|
|
3886
|
+
return findings;
|
|
3887
|
+
}
|
|
3888
|
+
var exec9, HISTORY_COMMITS, MIN_FILE_COMMITS, MIN_SHARED_COMMITS, MIN_COCHANGE_RATE, MAX_COMMIT_FILES;
|
|
3889
|
+
var init_cochange = __esm({
|
|
3890
|
+
"src/review/cochange.ts"() {
|
|
3891
|
+
"use strict";
|
|
3892
|
+
exec9 = promisify11(execFile11);
|
|
3893
|
+
HISTORY_COMMITS = 1500;
|
|
3894
|
+
MIN_FILE_COMMITS = 5;
|
|
3895
|
+
MIN_SHARED_COMMITS = 4;
|
|
3896
|
+
MIN_COCHANGE_RATE = 0.6;
|
|
3897
|
+
MAX_COMMIT_FILES = 30;
|
|
3898
|
+
}
|
|
3899
|
+
});
|
|
3900
|
+
|
|
3901
|
+
// src/review/evidence/paths.ts
|
|
3902
|
+
function evidencePath(value, sourceRoot, uri = false) {
|
|
3903
|
+
if (value.length > 4e3) return null;
|
|
3904
|
+
let file = value;
|
|
3905
|
+
try {
|
|
3906
|
+
if (uri) {
|
|
3907
|
+
if (file.startsWith("file:")) {
|
|
3908
|
+
const url = new URL(file);
|
|
3909
|
+
if (url.hostname && url.hostname !== "localhost") return null;
|
|
3910
|
+
file = decodeURIComponent(url.pathname).replace(/^\/([A-Za-z]:\/)/, "$1");
|
|
3911
|
+
} else {
|
|
3912
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(file)) return null;
|
|
3913
|
+
file = decodeURIComponent(file);
|
|
3914
|
+
}
|
|
3915
|
+
}
|
|
3916
|
+
} catch {
|
|
3917
|
+
return null;
|
|
3918
|
+
}
|
|
3919
|
+
file = file.replace(/\\/g, "/");
|
|
3920
|
+
const root = sourceRoot.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
3921
|
+
if (file.startsWith("/") || /^[A-Za-z]:/.test(file)) {
|
|
3922
|
+
if (!file.startsWith(root + "/")) return null;
|
|
3923
|
+
file = file.slice(root.length + 1);
|
|
3924
|
+
}
|
|
3925
|
+
return normalizeRepoPath(file);
|
|
3926
|
+
}
|
|
3927
|
+
var init_paths2 = __esm({
|
|
3928
|
+
"src/review/evidence/paths.ts"() {
|
|
3929
|
+
"use strict";
|
|
3930
|
+
init_paths();
|
|
3931
|
+
}
|
|
3932
|
+
});
|
|
3933
|
+
|
|
3934
|
+
// src/review/evidence/types.ts
|
|
3935
|
+
function messagePreview(message2) {
|
|
3936
|
+
return { message: message2.slice(0, 4e3), ...message2.length > 4e3 ? { truncated: true } : {} };
|
|
3937
|
+
}
|
|
3938
|
+
var MAX_FINDINGS;
|
|
3939
|
+
var init_types2 = __esm({
|
|
3940
|
+
"src/review/evidence/types.ts"() {
|
|
3941
|
+
"use strict";
|
|
3942
|
+
MAX_FINDINGS = 200;
|
|
3943
|
+
}
|
|
3944
|
+
});
|
|
3945
|
+
|
|
3946
|
+
// src/review/evidence/vitest.ts
|
|
3947
|
+
import { z as z15 } from "zod";
|
|
3948
|
+
function parseVitest(raw, sourceRoot) {
|
|
3949
|
+
const report = schema.parse(raw);
|
|
3950
|
+
const findings = [], diagnostics = [];
|
|
3951
|
+
let passed = 0, failed = 0, skipped = 0;
|
|
3952
|
+
for (const [index2, suite] of report.testResults.entries()) {
|
|
3953
|
+
const file = evidencePath(suite.name, sourceRoot);
|
|
3954
|
+
if (!file) diagnostics.push(`Test path is outside the declared checkout or invalid: ${suite.name}`);
|
|
3955
|
+
let suiteFailures = 0;
|
|
3956
|
+
for (const [testIndex, test] of suite.assertionResults.entries()) {
|
|
3366
3957
|
if (test.status === "passed") passed++;
|
|
3367
3958
|
else if (test.status === "failed") {
|
|
3368
3959
|
failed++;
|
|
@@ -3402,24 +3993,24 @@ var init_vitest = __esm({
|
|
|
3402
3993
|
"use strict";
|
|
3403
3994
|
init_paths2();
|
|
3404
3995
|
init_types2();
|
|
3405
|
-
count2 =
|
|
3406
|
-
schema =
|
|
3407
|
-
success:
|
|
3996
|
+
count2 = z15.number().int().nonnegative();
|
|
3997
|
+
schema = z15.object({
|
|
3998
|
+
success: z15.boolean(),
|
|
3408
3999
|
numTotalTests: count2,
|
|
3409
4000
|
numPassedTests: count2,
|
|
3410
4001
|
numFailedTests: count2,
|
|
3411
4002
|
numPendingTests: count2,
|
|
3412
4003
|
numTodoTests: count2,
|
|
3413
4004
|
numFailedTestSuites: count2,
|
|
3414
|
-
testResults:
|
|
3415
|
-
name:
|
|
3416
|
-
status:
|
|
3417
|
-
message:
|
|
3418
|
-
assertionResults:
|
|
3419
|
-
fullName:
|
|
3420
|
-
status:
|
|
3421
|
-
failureMessages:
|
|
3422
|
-
location:
|
|
4005
|
+
testResults: z15.array(z15.object({
|
|
4006
|
+
name: z15.string().min(1),
|
|
4007
|
+
status: z15.enum(["passed", "failed"]),
|
|
4008
|
+
message: z15.string().optional(),
|
|
4009
|
+
assertionResults: z15.array(z15.object({
|
|
4010
|
+
fullName: z15.string(),
|
|
4011
|
+
status: z15.enum(["passed", "failed", "pending", "skipped", "todo", "disabled"]),
|
|
4012
|
+
failureMessages: z15.array(z15.string()).nullable().optional(),
|
|
4013
|
+
location: z15.object({ line: count2, column: count2 }).nullable().optional()
|
|
3423
4014
|
}))
|
|
3424
4015
|
}))
|
|
3425
4016
|
});
|
|
@@ -3427,7 +4018,7 @@ var init_vitest = __esm({
|
|
|
3427
4018
|
});
|
|
3428
4019
|
|
|
3429
4020
|
// src/review/evidence/sarif.ts
|
|
3430
|
-
import { z as
|
|
4021
|
+
import { z as z16 } from "zod";
|
|
3431
4022
|
function parseSarif(raw, sourceRoot) {
|
|
3432
4023
|
const report = schema2.parse(raw), findings = [], diagnostics = [];
|
|
3433
4024
|
let active = 0, suppressed = 0, absent = 0, unresolved = 0;
|
|
@@ -3518,29 +4109,29 @@ var init_sarif = __esm({
|
|
|
3518
4109
|
"use strict";
|
|
3519
4110
|
init_paths2();
|
|
3520
4111
|
init_types2();
|
|
3521
|
-
index =
|
|
3522
|
-
artifact =
|
|
3523
|
-
message =
|
|
3524
|
-
level =
|
|
3525
|
-
location =
|
|
3526
|
-
schema2 =
|
|
3527
|
-
version:
|
|
3528
|
-
runs:
|
|
3529
|
-
tool:
|
|
3530
|
-
invocations:
|
|
3531
|
-
versionControlProvenance:
|
|
3532
|
-
originalUriBaseIds:
|
|
3533
|
-
artifacts:
|
|
3534
|
-
results:
|
|
3535
|
-
ruleId:
|
|
4112
|
+
index = z16.number().int().nonnegative();
|
|
4113
|
+
artifact = z16.object({ uri: z16.string().optional(), uriBaseId: z16.string().optional(), index: index.optional() });
|
|
4114
|
+
message = z16.object({ text: z16.string().optional(), markdown: z16.string().optional(), id: z16.string().optional(), arguments: z16.array(z16.string()).optional() });
|
|
4115
|
+
level = z16.enum(["error", "warning", "note", "none"]);
|
|
4116
|
+
location = z16.object({ physicalLocation: z16.object({ artifactLocation: artifact.optional(), region: z16.object({ startLine: index.optional(), startColumn: index.optional() }).optional() }).optional() });
|
|
4117
|
+
schema2 = z16.object({
|
|
4118
|
+
version: z16.literal("2.1.0"),
|
|
4119
|
+
runs: z16.array(z16.object({
|
|
4120
|
+
tool: z16.object({ driver: z16.object({ name: z16.string().min(1), rules: z16.array(z16.object({ id: z16.string(), defaultConfiguration: z16.object({ level: level.optional() }).optional(), messageStrings: z16.record(message).optional() })).optional(), globalMessageStrings: z16.record(message).optional() }) }),
|
|
4121
|
+
invocations: z16.array(z16.object({ executionSuccessful: z16.boolean(), commandLine: z16.string().optional(), toolExecutionNotifications: z16.array(z16.object({ level: level.optional(), message })).optional() })).optional(),
|
|
4122
|
+
versionControlProvenance: z16.array(z16.object({ revisionId: z16.string().optional() })).optional(),
|
|
4123
|
+
originalUriBaseIds: z16.record(artifact).optional(),
|
|
4124
|
+
artifacts: z16.array(z16.object({ location: artifact.optional() })).optional(),
|
|
4125
|
+
results: z16.array(z16.object({
|
|
4126
|
+
ruleId: z16.string().optional(),
|
|
3536
4127
|
ruleIndex: index.optional(),
|
|
3537
4128
|
message,
|
|
3538
4129
|
level: level.optional(),
|
|
3539
|
-
kind:
|
|
3540
|
-
baselineState:
|
|
3541
|
-
suppressions:
|
|
3542
|
-
locations:
|
|
3543
|
-
relatedLocations:
|
|
4130
|
+
kind: z16.enum(["fail", "pass", "open", "informational", "notApplicable", "review"]).optional(),
|
|
4131
|
+
baselineState: z16.enum(["new", "unchanged", "updated", "absent"]).optional(),
|
|
4132
|
+
suppressions: z16.array(z16.object({ kind: z16.enum(["inSource", "external"]), status: z16.enum(["accepted", "underReview", "rejected"]).optional() })).nullable().optional(),
|
|
4133
|
+
locations: z16.array(location).optional(),
|
|
4134
|
+
relatedLocations: z16.array(location).optional()
|
|
3544
4135
|
})).optional()
|
|
3545
4136
|
}))
|
|
3546
4137
|
});
|
|
@@ -3548,10 +4139,10 @@ var init_sarif = __esm({
|
|
|
3548
4139
|
});
|
|
3549
4140
|
|
|
3550
4141
|
// src/review/evidence.ts
|
|
3551
|
-
import
|
|
3552
|
-
import { z as
|
|
4142
|
+
import path19 from "path";
|
|
4143
|
+
import { z as z17 } from "zod";
|
|
3553
4144
|
function relativeArtifact(root, file) {
|
|
3554
|
-
const relative =
|
|
4145
|
+
const relative = path19.isAbsolute(file) ? path19.relative(root, file) : file;
|
|
3555
4146
|
const normalized = normalizeRepoPath(relative);
|
|
3556
4147
|
if (!normalized) throw new Error(`Evidence artifact must be inside the repository: ${file}`);
|
|
3557
4148
|
return normalized;
|
|
@@ -3590,7 +4181,7 @@ async function collectReviewEvidence(root, manifests, changedFiles, decisions, d
|
|
|
3590
4181
|
let manifest, raw;
|
|
3591
4182
|
try {
|
|
3592
4183
|
manifest = relativeArtifact(root, supplied);
|
|
3593
|
-
raw =
|
|
4184
|
+
raw = z17.object({ version: z17.literal(1), checks: z17.array(z17.unknown()) }).parse(await readStoreJson(root, manifest));
|
|
3594
4185
|
} catch (error) {
|
|
3595
4186
|
output.diagnostics.push(`${supplied}: ${String(error)}`);
|
|
3596
4187
|
continue;
|
|
@@ -3647,7 +4238,7 @@ async function collectReviewEvidence(root, manifests, changedFiles, decisions, d
|
|
|
3647
4238
|
const report = await readStoreJson(root, file);
|
|
3648
4239
|
if (report === null) throw new Error(`Report artifact is missing: ${file}`);
|
|
3649
4240
|
const sourceRoot = check.sourceRoot ?? root;
|
|
3650
|
-
if (!
|
|
4241
|
+
if (!path19.posix.isAbsolute(sourceRoot) && !/^[A-Za-z]:[\\/]/.test(sourceRoot)) throw new Error("sourceRoot must identify the absolute checkout root on the check runner.");
|
|
3651
4242
|
const parsed = check.report.format === "vitest-json" ? parseVitest(report, sourceRoot) : parseSarif(report, sourceRoot);
|
|
3652
4243
|
result.outcome = parsed.outcome;
|
|
3653
4244
|
result.counts = parsed.counts;
|
|
@@ -3716,693 +4307,262 @@ function summarizeEvidence(evidence) {
|
|
|
3716
4307
|
}));
|
|
3717
4308
|
return {
|
|
3718
4309
|
...check,
|
|
3719
|
-
findings,
|
|
3720
|
-
reportedCommands: check.reportedCommands ? previews(check.reportedCommands, 5) : void 0,
|
|
3721
|
-
reportedTools: check.reportedTools ? previews(check.reportedTools, 5) : void 0,
|
|
3722
|
-
diagnostics: previews(check.diagnostics, 10),
|
|
3723
|
-
truncated: check.truncated || check.findings.length > 5 || findings.some((f) => f.truncated) || check.diagnostics.length > 10 || check.diagnostics.some((d) => d.length > 2e3) || [check.reportedCommands ?? [], check.reportedTools ?? []].some((values) => values.length > 5 || values.some((v) => v.length > 2e3))
|
|
3724
|
-
};
|
|
3725
|
-
});
|
|
3726
|
-
return {
|
|
3727
|
-
...evidence,
|
|
3728
|
-
checks,
|
|
3729
|
-
diagnostics: previews(evidence.diagnostics, 20),
|
|
3730
|
-
truncated: evidence.checks.length > 10 || evidence.diagnostics.length > 20 || evidence.diagnostics.some((d) => d.length > 2e3) || checks.some((c) => c.truncated)
|
|
3731
|
-
};
|
|
3732
|
-
}
|
|
3733
|
-
var checkSchema2;
|
|
3734
|
-
var init_evidence2 = __esm({
|
|
3735
|
-
"src/review/evidence.ts"() {
|
|
3736
|
-
"use strict";
|
|
3737
|
-
init_storage();
|
|
3738
|
-
init_paths();
|
|
3739
|
-
init_snapshot();
|
|
3740
|
-
init_drift();
|
|
3741
|
-
init_test_map();
|
|
3742
|
-
init_provenance();
|
|
3743
|
-
init_vitest();
|
|
3744
|
-
init_sarif();
|
|
3745
|
-
init_types2();
|
|
3746
|
-
checkSchema2 = z14.object({
|
|
3747
|
-
id: z14.string().min(1).max(100),
|
|
3748
|
-
kind: z14.enum(["tests", "static-analysis", "security", "complexity", "duplication"]),
|
|
3749
|
-
tool: z14.string().min(1).max(200),
|
|
3750
|
-
command: z14.string().min(1).max(2e3),
|
|
3751
|
-
commit: z14.string().regex(/^(?:[a-fA-F0-9]{40}|[a-fA-F0-9]{64})$/).nullable().optional(),
|
|
3752
|
-
workingTreeClean: z14.boolean().optional(),
|
|
3753
|
-
source: z14.string().max(2e3).optional(),
|
|
3754
|
-
sourceRoot: z14.string().min(1).max(4e3).optional(),
|
|
3755
|
-
status: z14.enum(["completed", "skipped", "unavailable"]).default("completed"),
|
|
3756
|
-
reason: z14.string().min(1).max(2e3).optional(),
|
|
3757
|
-
exitCode: z14.number().int().nullable().optional(),
|
|
3758
|
-
report: z14.object({ format: z14.enum(["vitest-json", "sarif"]), path: z14.string().min(1).max(4e3) }).optional()
|
|
3759
|
-
});
|
|
3760
|
-
}
|
|
3761
|
-
});
|
|
3762
|
-
|
|
3763
|
-
// src/review/review.ts
|
|
3764
|
-
import fs14 from "fs/promises";
|
|
3765
|
-
import path19 from "path";
|
|
3766
|
-
import { execFile as execFile10 } from "child_process";
|
|
3767
|
-
import { promisify as promisify10 } from "util";
|
|
3768
|
-
async function resolveMergeBase(resolvedRoot, base, head) {
|
|
3769
|
-
try {
|
|
3770
|
-
const { stdout } = await exec9("git", ["merge-base", base, head], {
|
|
3771
|
-
cwd: resolvedRoot
|
|
3772
|
-
});
|
|
3773
|
-
return stdout.trim() || null;
|
|
3774
|
-
} catch {
|
|
3775
|
-
return null;
|
|
3776
|
-
}
|
|
3777
|
-
}
|
|
3778
|
-
async function defaultBase(resolvedRoot) {
|
|
3779
|
-
for (const ref of ["origin/HEAD", "origin/main", "origin/master", "main"]) {
|
|
3780
|
-
try {
|
|
3781
|
-
await exec9("git", ["rev-parse", "--verify", "--quiet", ref], {
|
|
3782
|
-
cwd: resolvedRoot
|
|
3783
|
-
});
|
|
3784
|
-
return ref;
|
|
3785
|
-
} catch {
|
|
3786
|
-
}
|
|
3787
|
-
}
|
|
3788
|
-
return null;
|
|
3789
|
-
}
|
|
3790
|
-
function anchorsTouched(record, changedFiles) {
|
|
3791
|
-
return matchingPaths(record.files, changedFiles);
|
|
3792
|
-
}
|
|
3793
|
-
async function computeReview(rootDir, base, options = {}) {
|
|
3794
|
-
const resolvedRoot = path19.resolve(rootDir);
|
|
3795
|
-
const head = await getCurrentGitHash(resolvedRoot);
|
|
3796
|
-
if (head === "unknown") return null;
|
|
3797
|
-
const mergeBase = await resolveMergeBase(resolvedRoot, base, head);
|
|
3798
|
-
if (!mergeBase) return null;
|
|
3799
|
-
const changes = await getChangesWithStatus(resolvedRoot, mergeBase, head);
|
|
3800
|
-
if (changes === null) return null;
|
|
3801
|
-
const changedFiles = touchedPaths(changes);
|
|
3802
|
-
const report = {
|
|
3803
|
-
version: 1,
|
|
3804
|
-
root: resolvedRoot,
|
|
3805
|
-
base,
|
|
3806
|
-
mergeBase,
|
|
3807
|
-
changedFiles,
|
|
3808
|
-
missingPartners: [],
|
|
3809
|
-
touchedDecisions: [],
|
|
3810
|
-
historyAvailable: true,
|
|
3811
|
-
truncated: false
|
|
3812
|
-
};
|
|
3813
|
-
const store = await loadDecisionStore(resolvedRoot);
|
|
3814
|
-
report.diagnostics = store.diagnostics;
|
|
3815
|
-
const decisionDrift = await computeDecisionDrift(resolvedRoot, store.records);
|
|
3816
|
-
if (options.evidence !== void 0) {
|
|
3817
|
-
report.evidence = await collectReviewEvidence(resolvedRoot, options.evidence, changedFiles, store.records, decisionDrift.freshness, head);
|
|
3818
|
-
if (store.diagnostics.length) {
|
|
3819
|
-
report.evidence.diagnostics.push("Invalid decision records make knowledge associations incomplete; consult review diagnostics.");
|
|
3820
|
-
if (report.evidence.status === "passed") report.evidence.status = "incomplete";
|
|
3821
|
-
}
|
|
3822
|
-
}
|
|
3823
|
-
const finalize = async () => {
|
|
3824
|
-
if (report.evidence && await getCurrentGitHash(resolvedRoot) !== head) {
|
|
3825
|
-
report.evidence.diagnostics.push("HEAD changed during the review; rerun to obtain consistent change and knowledge associations.");
|
|
3826
|
-
for (const check of report.evidence.checks) check.freshness = "unknown";
|
|
3827
|
-
report.evidence.summary.stale = 0;
|
|
3828
|
-
report.evidence.summary.unknown = report.evidence.checks.length;
|
|
3829
|
-
if (report.evidence.status !== "unavailable") report.evidence.status = "incomplete";
|
|
3830
|
-
}
|
|
3831
|
-
return report;
|
|
3832
|
-
};
|
|
3833
|
-
if (changedFiles.length === 0) return finalize();
|
|
3834
|
-
let analyzed = changedFiles;
|
|
3835
|
-
if (changedFiles.length > MAX_ANALYZED_FILES) {
|
|
3836
|
-
analyzed = changedFiles.slice(0, MAX_ANALYZED_FILES);
|
|
3837
|
-
report.truncated = true;
|
|
3838
|
-
}
|
|
3839
|
-
const partners = await findMissingPartners(
|
|
3840
|
-
resolvedRoot,
|
|
3841
|
-
analyzed,
|
|
3842
|
-
async (relPath) => {
|
|
3843
|
-
try {
|
|
3844
|
-
await fs14.access(path19.join(resolvedRoot, relPath));
|
|
3845
|
-
return true;
|
|
3846
|
-
} catch {
|
|
3847
|
-
return false;
|
|
3848
|
-
}
|
|
3849
|
-
},
|
|
3850
|
-
changedFiles
|
|
3851
|
-
);
|
|
3852
|
-
if (partners === null) {
|
|
3853
|
-
report.historyAvailable = false;
|
|
3854
|
-
} else {
|
|
3855
|
-
report.missingPartners = partners;
|
|
3856
|
-
}
|
|
3857
|
-
const decisions = store.records;
|
|
3858
|
-
for (const record of decisions) {
|
|
3859
|
-
if (record.status !== "active") continue;
|
|
3860
|
-
const effective = effectiveDecision(record);
|
|
3861
|
-
const touched = anchorsTouched(effective, changedFiles);
|
|
3862
|
-
const proposalTouched = effective !== record ? anchorsTouched(record, changedFiles) : [];
|
|
3863
|
-
if (touched.length > 0 || proposalTouched.length > 0) {
|
|
3864
|
-
const { pendingProposal, ...knowledge } = decisionKnowledge(record, decisionDrift.freshness?.[record.id] ?? "unknown", decisionDrift.pendingProposals?.[record.id]?.freshness ?? "unknown");
|
|
3865
|
-
report.touchedDecisions.push({
|
|
3866
|
-
...knowledge,
|
|
3867
|
-
...pendingProposal ? { pendingProposal: { ...pendingProposal, touchedFiles: proposalTouched } } : {},
|
|
3868
|
-
id: record.id,
|
|
3869
|
-
anchors: effective.files,
|
|
3870
|
-
freshness: decisionDrift.freshness?.[record.id] ?? "unknown",
|
|
3871
|
-
touchedFiles: touched
|
|
3872
|
-
});
|
|
3873
|
-
}
|
|
3874
|
-
}
|
|
3875
|
-
return finalize();
|
|
3876
|
-
}
|
|
3877
|
-
var exec9, MAX_ANALYZED_FILES;
|
|
3878
|
-
var init_review = __esm({
|
|
3879
|
-
"src/review/review.ts"() {
|
|
3880
|
-
"use strict";
|
|
3881
|
-
init_drift();
|
|
3882
|
-
init_provenance();
|
|
3883
|
-
init_decisions();
|
|
3884
|
-
init_paths();
|
|
3885
|
-
init_drift2();
|
|
3886
|
-
init_cochange();
|
|
3887
|
-
init_evidence2();
|
|
3888
|
-
init_snapshot();
|
|
3889
|
-
exec9 = promisify10(execFile10);
|
|
3890
|
-
MAX_ANALYZED_FILES = 50;
|
|
3891
|
-
}
|
|
3892
|
-
});
|
|
3893
|
-
|
|
3894
|
-
// src/mcp/onboarding.ts
|
|
3895
|
-
async function auditSummary(root) {
|
|
3896
|
-
try {
|
|
3897
|
-
const report = await computeAudit(root);
|
|
3898
|
-
if (!report) return { status: "no-context-files", reason: "No AGENTS.md, CLAUDE.md, or .claude/CLAUDE.md found to audit." };
|
|
3899
|
-
if (!report.gitAvailable) return { status: "unavailable", reason: "Audit needs readable Git history to verify documentation claims.", docs: report.docs };
|
|
3900
|
-
return {
|
|
3901
|
-
...report,
|
|
3902
|
-
status: "complete",
|
|
3903
|
-
issues: report.issues.slice(0, MAX_FINDINGS2),
|
|
3904
|
-
advisories: report.advisories.slice(0, MAX_FINDINGS2),
|
|
3905
|
-
suppressedAdvisories: (report.suppressedAdvisories ?? []).slice(0, MAX_FINDINGS2),
|
|
3906
|
-
counts: {
|
|
3907
|
-
issues: report.issues.length,
|
|
3908
|
-
advisories: report.advisories.length,
|
|
3909
|
-
suppressedAdvisories: report.suppressedAdvisories?.length ?? 0
|
|
3910
|
-
},
|
|
3911
|
-
truncated: report.issues.length > MAX_FINDINGS2 || report.advisories.length > MAX_FINDINGS2 || (report.suppressedAdvisories?.length ?? 0) > MAX_FINDINGS2
|
|
3912
|
-
};
|
|
3913
|
-
} catch (error) {
|
|
3914
|
-
return { status: "unavailable", reason: reason(error) };
|
|
3915
|
-
}
|
|
3916
|
-
}
|
|
3917
|
-
async function reviewSummary(root, requestedBase, evidence) {
|
|
3918
|
-
const workingTree = await getWorkingTree(root);
|
|
3919
|
-
const scope = "committed";
|
|
3920
|
-
try {
|
|
3921
|
-
const base = requestedBase ?? await defaultBase(root);
|
|
3922
|
-
if (!base) return { status: "unavailable", scope, workingTree, reason: "No default review base resolves. Pass base to mason_init, or run mason-review --base <ref>." };
|
|
3923
|
-
const report = await computeReview(root, base, { evidence });
|
|
3924
|
-
if (!report) return { status: "unavailable", scope, base, workingTree, reason: "The review base, merge base, or committed diff could not be read." };
|
|
3925
|
-
return {
|
|
3926
|
-
...report,
|
|
3927
|
-
scope,
|
|
3928
|
-
workingTree,
|
|
3929
|
-
...report.evidence ? { evidence: summarizeEvidence(report.evidence) } : {},
|
|
3930
|
-
status: !report.historyAvailable ? "unavailable" : report.changedFiles.length ? "complete" : "no-changes",
|
|
3931
|
-
...!report.historyAvailable ? { reason: "Co-change history could not be read; review findings are incomplete." } : {},
|
|
3932
|
-
changedFiles: report.changedFiles.slice(0, MAX_FINDINGS2),
|
|
3933
|
-
missingPartners: report.missingPartners.slice(0, MAX_FINDINGS2),
|
|
3934
|
-
touchedDecisions: report.touchedDecisions.slice(0, MAX_FINDINGS2),
|
|
3935
|
-
counts: { changedFiles: report.changedFiles.length, missingPartners: report.missingPartners.length, touchedDecisions: report.touchedDecisions.length },
|
|
3936
|
-
truncated: report.truncated || [report.changedFiles, report.missingPartners, report.touchedDecisions].some((list) => list.length > MAX_FINDINGS2),
|
|
3937
|
-
hint: "Reviews cover merge-base..HEAD. Uncommitted paths are reported separately in workingTree; they have not been reviewed."
|
|
4310
|
+
findings,
|
|
4311
|
+
reportedCommands: check.reportedCommands ? previews(check.reportedCommands, 5) : void 0,
|
|
4312
|
+
reportedTools: check.reportedTools ? previews(check.reportedTools, 5) : void 0,
|
|
4313
|
+
diagnostics: previews(check.diagnostics, 10),
|
|
4314
|
+
truncated: check.truncated || check.findings.length > 5 || findings.some((f) => f.truncated) || check.diagnostics.length > 10 || check.diagnostics.some((d) => d.length > 2e3) || [check.reportedCommands ?? [], check.reportedTools ?? []].some((values) => values.length > 5 || values.some((v) => v.length > 2e3))
|
|
3938
4315
|
};
|
|
3939
|
-
}
|
|
3940
|
-
return { status: "unavailable", scope, workingTree, reason: reason(error) };
|
|
3941
|
-
}
|
|
3942
|
-
}
|
|
3943
|
-
async function inspectOnboarding(root, base, evidence) {
|
|
3944
|
-
const [audit, review, map, decisions] = await Promise.all([
|
|
3945
|
-
auditSummary(root),
|
|
3946
|
-
reviewSummary(root, base, evidence),
|
|
3947
|
-
inspectSnapshot(root),
|
|
3948
|
-
loadDecisionStore(root)
|
|
3949
|
-
]);
|
|
4316
|
+
});
|
|
3950
4317
|
return {
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
active: decisions.records.filter((record) => record.status === "active").length,
|
|
3956
|
-
...Object.fromEntries(["accepted", "proposed", "unreviewed"].map((approval) => [approval, decisions.records.filter((record) => record.status === "active" && decisionApproval(effectiveDecision(record)) === approval).length])),
|
|
3957
|
-
pendingProposals: decisions.records.filter((record) => effectiveDecision(record) !== record).length
|
|
3958
|
-
},
|
|
3959
|
-
diagnostics: [...map.diagnostics, ...decisions.diagnostics]
|
|
4318
|
+
...evidence,
|
|
4319
|
+
checks,
|
|
4320
|
+
diagnostics: previews(evidence.diagnostics, 20),
|
|
4321
|
+
truncated: evidence.checks.length > 10 || evidence.diagnostics.length > 20 || evidence.diagnostics.some((d) => d.length > 2e3) || checks.some((c) => c.truncated)
|
|
3960
4322
|
};
|
|
3961
4323
|
}
|
|
3962
|
-
var
|
|
3963
|
-
var
|
|
3964
|
-
"src/
|
|
4324
|
+
var checkSchema2;
|
|
4325
|
+
var init_evidence2 = __esm({
|
|
4326
|
+
"src/review/evidence.ts"() {
|
|
3965
4327
|
"use strict";
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
init_drift();
|
|
4328
|
+
init_storage();
|
|
4329
|
+
init_paths();
|
|
3969
4330
|
init_snapshot();
|
|
4331
|
+
init_drift();
|
|
4332
|
+
init_test_map();
|
|
3970
4333
|
init_provenance();
|
|
3971
|
-
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
|
|
4334
|
+
init_vitest();
|
|
4335
|
+
init_sarif();
|
|
4336
|
+
init_types2();
|
|
4337
|
+
checkSchema2 = z17.object({
|
|
4338
|
+
id: z17.string().min(1).max(100),
|
|
4339
|
+
kind: z17.enum(["tests", "static-analysis", "security", "complexity", "duplication"]),
|
|
4340
|
+
tool: z17.string().min(1).max(200),
|
|
4341
|
+
command: z17.string().min(1).max(2e3),
|
|
4342
|
+
commit: z17.string().regex(/^(?:[a-fA-F0-9]{40}|[a-fA-F0-9]{64})$/).nullable().optional(),
|
|
4343
|
+
workingTreeClean: z17.boolean().optional(),
|
|
4344
|
+
source: z17.string().max(2e3).optional(),
|
|
4345
|
+
sourceRoot: z17.string().min(1).max(4e3).optional(),
|
|
4346
|
+
status: z17.enum(["completed", "skipped", "unavailable"]).default("completed"),
|
|
4347
|
+
reason: z17.string().min(1).max(2e3).optional(),
|
|
4348
|
+
exitCode: z17.number().int().nullable().optional(),
|
|
4349
|
+
report: z17.object({ format: z17.enum(["vitest-json", "sarif"]), path: z17.string().min(1).max(4e3) }).optional()
|
|
4350
|
+
});
|
|
3975
4351
|
}
|
|
3976
4352
|
});
|
|
3977
4353
|
|
|
3978
|
-
// src/
|
|
4354
|
+
// src/review/review.ts
|
|
3979
4355
|
import fs15 from "fs/promises";
|
|
3980
4356
|
import path20 from "path";
|
|
3981
|
-
import {
|
|
3982
|
-
|
|
4357
|
+
import { execFile as execFile12 } from "child_process";
|
|
4358
|
+
import { promisify as promisify12 } from "util";
|
|
4359
|
+
async function resolveMergeBase(resolvedRoot, base, head) {
|
|
3983
4360
|
try {
|
|
3984
|
-
const
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
}
|
|
3991
|
-
}
|
|
3992
|
-
function managedBlock(text2, start, end, body) {
|
|
3993
|
-
const starts = text2.split(start).length - 1, ends = text2.split(end).length - 1;
|
|
3994
|
-
if (starts !== ends || starts > 1 || starts === 1 && text2.indexOf(end) < text2.indexOf(start)) {
|
|
3995
|
-
throw new Error("Ambiguous Mason instruction/configuration markers; repair the marked block before setup.");
|
|
4361
|
+
const { stdout } = await exec10("git", ["merge-base", base, head], {
|
|
4362
|
+
cwd: resolvedRoot
|
|
4363
|
+
});
|
|
4364
|
+
return stdout.trim() || null;
|
|
4365
|
+
} catch {
|
|
4366
|
+
return null;
|
|
3996
4367
|
}
|
|
3997
|
-
const eol = text2.includes("\r\n") ? "\r\n" : "\n";
|
|
3998
|
-
const block = [start, body.replace(/\r?\n/g, eol), end].join(eol);
|
|
3999
|
-
if (starts) return text2.slice(0, text2.indexOf(start)) + block + text2.slice(text2.indexOf(end) + end.length);
|
|
4000
|
-
return text2 + (text2.length && !text2.endsWith("\n") ? eol : "") + (text2.length ? eol : "") + block + eol;
|
|
4001
4368
|
}
|
|
4002
|
-
async function
|
|
4003
|
-
const
|
|
4004
|
-
if (current === edit.after) return false;
|
|
4005
|
-
if (current !== edit.before) throw new Error("Setup input changed during installation: " + edit.path + ". Rerun setup to resume.");
|
|
4006
|
-
const file = await storePath(root, edit.path, true);
|
|
4007
|
-
const temporary = path20.join(path20.dirname(file), ".mason-setup-" + randomUUID5() + ".tmp");
|
|
4008
|
-
try {
|
|
4009
|
-
const mode = await fs15.stat(file).then((s) => s.mode & 511, () => 384);
|
|
4010
|
-
const handle = await fs15.open(temporary, "wx", mode);
|
|
4369
|
+
async function defaultBase(resolvedRoot) {
|
|
4370
|
+
for (const ref of ["origin/HEAD", "origin/main", "origin/master", "main"]) {
|
|
4011
4371
|
try {
|
|
4012
|
-
await
|
|
4013
|
-
|
|
4014
|
-
} finally {
|
|
4015
|
-
await handle.close();
|
|
4016
|
-
}
|
|
4017
|
-
if (await readText(root, edit.path) !== edit.before) throw new Error("Setup input changed during installation: " + edit.path);
|
|
4018
|
-
await fs15.rename(temporary, file);
|
|
4019
|
-
return true;
|
|
4020
|
-
} finally {
|
|
4021
|
-
await fs15.rm(temporary, { force: true });
|
|
4022
|
-
}
|
|
4023
|
-
}
|
|
4024
|
-
var init_files2 = __esm({
|
|
4025
|
-
"src/setup/files.ts"() {
|
|
4026
|
-
"use strict";
|
|
4027
|
-
init_files();
|
|
4028
|
-
init_storage();
|
|
4029
|
-
}
|
|
4030
|
-
});
|
|
4031
|
-
|
|
4032
|
-
// src/setup/launcher.ts
|
|
4033
|
-
var BOOTSTRAP, POWERSHELL_BOOTSTRAP, mcpCommand, hookCommand, SHELL_LAUNCHER, POWERSHELL_LAUNCHER, LAUNCHER;
|
|
4034
|
-
var init_launcher = __esm({
|
|
4035
|
-
"src/setup/launcher.ts"() {
|
|
4036
|
-
"use strict";
|
|
4037
|
-
BOOTSTRAP = "require(require('node:path').join(require('node:child_process').execFileSync('git',['rev-parse','--show-toplevel'],{encoding:'utf8'}).trim(),'.mason','run.cjs'))";
|
|
4038
|
-
POWERSHELL_BOOTSTRAP = "& (Join-Path (git rev-parse --show-toplevel) '.mason/run.ps1')";
|
|
4039
|
-
mcpCommand = (host, runtime) => !runtime?.bundle ? { command: "node", args: ["-e", BOOTSTRAP, "--", host, "mcp"] } : process.platform === "win32" ? { command: "powershell.exe", args: ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", POWERSHELL_BOOTSTRAP, host, "mcp"] } : { command: "sh", args: ["-c", 'exec sh "$(git rev-parse --show-toplevel)/.mason/run.sh" "$@"', "mason", host, "mcp"] };
|
|
4040
|
-
hookCommand = (host, runtime) => !runtime?.bundle ? `node -e "${BOOTSTRAP}" -- ${host} auto` : process.platform === "win32" ? `powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command "${POWERSHELL_BOOTSTRAP} ${host} auto"` : `sh "$(git rev-parse --show-toplevel)/.mason/run.sh" ${host} auto`;
|
|
4041
|
-
SHELL_LAUNCHER = `#!/bin/sh
|
|
4042
|
-
set -eu
|
|
4043
|
-
advisory=0
|
|
4044
|
-
if [ "\${2-}" = auto ] && [ "\${3-}" = hook ]; then advisory=1; fi
|
|
4045
|
-
fail() {
|
|
4046
|
-
if [ "$advisory" -eq 1 ]; then printf '%s\\n' '{"systemMessage":"Mason runtime unavailable. Rerun mason setup for this checkout; verification was not established."}'; exit 0; fi
|
|
4047
|
-
echo 'Mason runtime unavailable. Rerun mason setup for this checkout.' >&2; exit 2
|
|
4048
|
-
}
|
|
4049
|
-
root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) || fail
|
|
4050
|
-
case "\${1-}" in codex|claude) ;; *) fail ;; esac
|
|
4051
|
-
pointer="$root/.mason/runtime/$1.txt"
|
|
4052
|
-
[ -f "$pointer" ] || fail
|
|
4053
|
-
runtime=$(cat "$pointer") || fail
|
|
4054
|
-
case "$runtime" in ''|*[!0-9a-f]*) fail ;; esac
|
|
4055
|
-
[ "\${#runtime}" -eq 24 ] || fail
|
|
4056
|
-
[ -x "$root/.mason/runtime/$runtime/node" ] || fail
|
|
4057
|
-
if [ "$advisory" -eq 1 ]; then
|
|
4058
|
-
"$root/.mason/runtime/$runtime/node" "$root/.mason/run.cjs" "$@" || fail
|
|
4059
|
-
else
|
|
4060
|
-
exec "$root/.mason/runtime/$runtime/node" "$root/.mason/run.cjs" "$@"
|
|
4061
|
-
fi
|
|
4062
|
-
`;
|
|
4063
|
-
POWERSHELL_LAUNCHER = `# Managed by mason setup; no system Node installation is used.
|
|
4064
|
-
$ErrorActionPreference = 'Stop'
|
|
4065
|
-
$advisory = $args.Count -ge 3 -and $args[1] -eq 'auto' -and $args[2] -eq 'hook'
|
|
4066
|
-
try {
|
|
4067
|
-
if ($args.Count -lt 2 -or $args[0] -notin @('codex','claude')) { throw 'Invalid Mason host.' }
|
|
4068
|
-
$runtime = (Get-Content -LiteralPath (Join-Path $PSScriptRoot ('runtime/' + $args[0] + '.txt')) -Raw).Trim()
|
|
4069
|
-
if ($runtime -cnotmatch '^[a-f0-9]{24}$') { throw 'Invalid Mason runtime pointer.' }
|
|
4070
|
-
& (Join-Path $PSScriptRoot ('runtime/' + $runtime + '/node.exe')) (Join-Path $PSScriptRoot 'run.cjs') @args
|
|
4071
|
-
if ($LASTEXITCODE -ne 0) { throw 'Mason runtime failed.' }
|
|
4072
|
-
} catch {
|
|
4073
|
-
if ($advisory) { Write-Output '{"systemMessage":"Mason runtime unavailable. Rerun mason setup for this checkout; verification was not established."}'; exit 0 }
|
|
4074
|
-
[Console]::Error.WriteLine($_.Exception.Message); exit 2
|
|
4075
|
-
}
|
|
4076
|
-
`;
|
|
4077
|
-
LAUNCHER = `// Managed by mason-auto setup. Rerun setup to install this checkout's pinned runtime.
|
|
4078
|
-
const fs = require('node:fs');
|
|
4079
|
-
const path = require('node:path');
|
|
4080
|
-
const {pathToFileURL} = require('node:url');
|
|
4081
|
-
const launchArgs = process.argv.slice(process.argv[1] && path.resolve(process.argv[1]) === __filename ? 2 : 1);
|
|
4082
|
-
(async () => {
|
|
4083
|
-
const root = path.dirname(__dirname);
|
|
4084
|
-
const args = launchArgs;
|
|
4085
|
-
const [host, command, ...rest] = args;
|
|
4086
|
-
if (!['codex','claude'].includes(host) || !['auto','mcp'].includes(command)) throw new Error('Invalid Mason launcher arguments.');
|
|
4087
|
-
const setup = JSON.parse(fs.readFileSync(path.join(__dirname, 'setup.json'), 'utf8'));
|
|
4088
|
-
const entry = setup.hosts?.[host];
|
|
4089
|
-
if (setup.version !== 1 || !entry || !/^[a-f0-9]{24}$/.test(entry.runtime?.id)) throw new Error('Mason is not configured for this host. Rerun mason-auto setup.');
|
|
4090
|
-
const base = path.join(__dirname, 'runtime', entry.runtime.id);
|
|
4091
|
-
if (entry.runtime.bundle) {
|
|
4092
|
-
const node = process.platform === 'win32' ? 'node.exe' : 'node';
|
|
4093
|
-
if (fs.realpathSync(process.execPath) !== fs.realpathSync(path.join(base, node))) throw new Error('Mason runtime pointer differs from the configured version. Rerun setup.');
|
|
4094
|
-
}
|
|
4095
|
-
const binary = entry.runtime.bundle ? path.join(base, 'app', 'dist', 'mason-' + command + '.js')
|
|
4096
|
-
: path.join(base, 'node_modules', 'mason-context', 'dist', 'mason-' + command + '.js');
|
|
4097
|
-
if (!fs.existsSync(binary)) throw new Error('The pinned Mason runtime is missing in this checkout. Rerun mason-auto setup --host ' + host + '.');
|
|
4098
|
-
process.env.MASON_SETUP_ROOT = root;
|
|
4099
|
-
process.env.MASON_SETUP_HOST = host;
|
|
4100
|
-
process.env.MASON_SETUP_REVISION = entry.revision;
|
|
4101
|
-
process.argv = [process.execPath, binary, ...rest];
|
|
4102
|
-
await import(pathToFileURL(binary).href);
|
|
4103
|
-
})().catch(error => {
|
|
4104
|
-
const message = 'Mason setup unavailable: ' + error.message;
|
|
4105
|
-
if (launchArgs[1] === 'auto' && launchArgs[2] === 'hook') { console.log(JSON.stringify({systemMessage: message})); process.exitCode = 0; }
|
|
4106
|
-
else { console.error(message); process.exitCode = 2; }
|
|
4107
|
-
});
|
|
4108
|
-
`;
|
|
4109
|
-
}
|
|
4110
|
-
});
|
|
4111
|
-
|
|
4112
|
-
// src/setup/config.ts
|
|
4113
|
-
import { isDeepStrictEqual } from "util";
|
|
4114
|
-
import { parse, stringify } from "smol-toml";
|
|
4115
|
-
import { z as z15 } from "zod";
|
|
4116
|
-
async function instructionEdits(root, host) {
|
|
4117
|
-
const found = await Promise.all(DOC_CANDIDATES.map(async (file) => ({ path: file, text: await readText(root, file) })));
|
|
4118
|
-
const existing = found.find((f) => f.text !== null);
|
|
4119
|
-
const primary = host === "codex" && found[0].text === null ? { path: "AGENTS.md", text: null } : existing ?? { path: "AGENTS.md", text: null };
|
|
4120
|
-
const originalGuidance = primary.text === null && existing ? "Follow the existing project conventions in " + existing.path + ".\n" : "";
|
|
4121
|
-
const edits = [{
|
|
4122
|
-
path: primary.path,
|
|
4123
|
-
before: primary.text,
|
|
4124
|
-
after: managedBlock(primary.text ?? originalGuidance, "<!-- mason:start -->", "<!-- mason:end -->", CLAUDE_MD_SECTION.split("\n").slice(1, -1).join("\n"))
|
|
4125
|
-
}];
|
|
4126
|
-
if (primary.path === "AGENTS.md") {
|
|
4127
|
-
const secondary = found.filter((f) => f.path !== "AGENTS.md" && f.text !== null);
|
|
4128
|
-
if (host === "claude" && !secondary.length) secondary.push({ path: "CLAUDE.md", text: null });
|
|
4129
|
-
for (const file of secondary) {
|
|
4130
|
-
const text2 = file.text ?? "";
|
|
4131
|
-
const legacy = text2.includes("<!-- mason:start -->") || text2.includes("<!-- mason:end -->");
|
|
4132
|
-
const imported = "@" + (file.path.startsWith(".claude/") ? "../AGENTS.md" : "AGENTS.md");
|
|
4133
|
-
if (text2.trim() === imported) continue;
|
|
4134
|
-
const pointer = "Mason project knowledge and shared project instructions:\n" + imported;
|
|
4135
|
-
edits.push({
|
|
4136
|
-
path: file.path,
|
|
4137
|
-
before: file.text,
|
|
4138
|
-
after: managedBlock(text2, legacy ? "<!-- mason:start -->" : POINTER_START, legacy ? "<!-- mason:end -->" : POINTER_END, pointer)
|
|
4372
|
+
await exec10("git", ["rev-parse", "--verify", "--quiet", ref], {
|
|
4373
|
+
cwd: resolvedRoot
|
|
4139
4374
|
});
|
|
4375
|
+
return ref;
|
|
4376
|
+
} catch {
|
|
4140
4377
|
}
|
|
4141
4378
|
}
|
|
4142
|
-
return
|
|
4143
|
-
}
|
|
4144
|
-
function managedMcp(previous, host, runtime) {
|
|
4145
|
-
const options = previous === void 0 ? {} : object.parse(previous);
|
|
4146
|
-
for (const key of ["command", "args", "url", "type", "headers", "http_headers", "env_http_headers", "bearer_token_env_var"]) delete options[key];
|
|
4147
|
-
return { ...options, ...mcpCommand(host, runtime) };
|
|
4148
|
-
}
|
|
4149
|
-
async function mcpEdit(root, host, runtime) {
|
|
4150
|
-
const file = mcpPath(host), before = await readText(root, file);
|
|
4151
|
-
if (host === "claude") {
|
|
4152
|
-
const config2 = before === null ? {} : object.parse(JSON.parse(before));
|
|
4153
|
-
const servers2 = object.parse(config2.mcpServers ?? {});
|
|
4154
|
-
return { path: file, before, after: JSON.stringify({ ...config2, mcpServers: { ...servers2, mason: managedMcp(servers2.mason, host, runtime) } }, null, 2) + "\n" };
|
|
4155
|
-
}
|
|
4156
|
-
const config = parse(before ?? "");
|
|
4157
|
-
const managed = (before ?? "").includes(TOML_START);
|
|
4158
|
-
const servers = config.mcp_servers;
|
|
4159
|
-
const desired = managedMcp(servers?.mason, host, runtime);
|
|
4160
|
-
let base = before ?? "";
|
|
4161
|
-
if (servers?.mason && !managed) {
|
|
4162
|
-
const headers = [...base.matchAll(/^\s*\[\[?.+?\]\]?[^\S\r\n]*(?:#.*)?$/gm)];
|
|
4163
|
-
const spans = headers.flatMap((header, index2) => /^\s*\[mcp_servers\.mason(?:\.[A-Za-z0-9_-]+)*\]/.test(header[0]) ? [{ start: header.index, end: headers[index2 + 1]?.index ?? base.length }] : []);
|
|
4164
|
-
if (!spans.length) throw new Error("Cannot safely migrate the existing Mason MCP TOML entry. Use an explicit [mcp_servers.mason] table and rerun setup; the file was retained.");
|
|
4165
|
-
for (const span of spans.reverse()) base = base.slice(0, span.start) + base.slice(span.end);
|
|
4166
|
-
}
|
|
4167
|
-
const after = managedBlock(base, TOML_START, TOML_END, stringify({ mcp_servers: { mason: desired } }).trimEnd());
|
|
4168
|
-
const parsed = parse(after);
|
|
4169
|
-
const expected = { ...config, mcp_servers: { ...servers ?? {}, mason: desired } };
|
|
4170
|
-
if (!isDeepStrictEqual(parsed, expected)) throw new Error("Could not safely configure the Mason MCP entry without changing other settings.");
|
|
4171
|
-
return { path: file, before, after };
|
|
4172
|
-
}
|
|
4173
|
-
async function hookEdits(root, host, runtime) {
|
|
4174
|
-
const file = configPath(host), before = await readText(root, file);
|
|
4175
|
-
const { planAutomationInstall: planAutomationInstall2 } = await Promise.resolve().then(() => (init_install(), install_exports));
|
|
4176
|
-
const plan = await planAutomationInstall2(root, host, hookCommand(host, runtime));
|
|
4177
|
-
return [
|
|
4178
|
-
{ path: file, before, after: JSON.stringify(plan.config, null, 2) + "\n" },
|
|
4179
|
-
{ path: ".mason/automation.json", before: await readText(root, ".mason/automation.json"), after: JSON.stringify(plan.record, null, 2) + "\n" }
|
|
4180
|
-
];
|
|
4379
|
+
return null;
|
|
4181
4380
|
}
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
const { git: git3 } = await Promise.resolve().then(() => (init_evidence(), evidence_exports));
|
|
4185
|
-
let parentIgnored = false;
|
|
4186
|
-
try {
|
|
4187
|
-
parentIgnored = !!(await git3(root, "check-ignore", "--no-index", ".mason")).trim();
|
|
4188
|
-
} catch (error) {
|
|
4189
|
-
if (error.code !== 1) throw error;
|
|
4190
|
-
}
|
|
4191
|
-
const retainParentRule = parentIgnored || !!ignore?.replace(/\r\n/g, "\n").includes("# mason:ignore:start\n!/.mason/\n/.mason/*");
|
|
4192
|
-
const rules = [...retainParentRule ? ["!/.mason/", "/.mason/*"] : [], "!/.mason/decisions/", "!/.mason/decisions/**", "!/.mason/setup.json", "!/.mason/automation.json", "!/.mason/project.json", "!/.mason/run.cjs", "!/.mason/run.sh", "!/.mason/run.ps1", "/.mason/reports/", "/.mason/runtime/"].join("\n");
|
|
4193
|
-
return [
|
|
4194
|
-
{ path: ".gitignore", before: ignore, after: managedBlock(ignore ?? "", "# mason:ignore:start", "# mason:ignore:end", rules) },
|
|
4195
|
-
{ path: ".mason/run.cjs", before: await readText(root, ".mason/run.cjs"), after: LAUNCHER },
|
|
4196
|
-
{ path: ".mason/run.sh", before: await readText(root, ".mason/run.sh"), after: SHELL_LAUNCHER },
|
|
4197
|
-
{ path: ".mason/run.ps1", before: await readText(root, ".mason/run.ps1"), after: POWERSHELL_LAUNCHER }
|
|
4198
|
-
];
|
|
4381
|
+
function anchorsTouched(record, changedFiles) {
|
|
4382
|
+
return matchingPaths(record.files, changedFiles);
|
|
4199
4383
|
}
|
|
4200
|
-
async function
|
|
4201
|
-
const
|
|
4202
|
-
const
|
|
4203
|
-
|
|
4204
|
-
const
|
|
4205
|
-
|
|
4206
|
-
const
|
|
4207
|
-
return
|
|
4208
|
-
|
|
4209
|
-
|
|
4210
|
-
|
|
4211
|
-
|
|
4212
|
-
|
|
4213
|
-
|
|
4214
|
-
|
|
4384
|
+
async function computeReview(rootDir, base, options = {}) {
|
|
4385
|
+
const resolvedRoot = path20.resolve(rootDir);
|
|
4386
|
+
const head = await getCurrentGitHash(resolvedRoot);
|
|
4387
|
+
if (head === "unknown") return null;
|
|
4388
|
+
const mergeBase = await resolveMergeBase(resolvedRoot, base, head);
|
|
4389
|
+
if (!mergeBase) return null;
|
|
4390
|
+
const changes = await getChangesWithStatus(resolvedRoot, mergeBase, head);
|
|
4391
|
+
if (changes === null) return null;
|
|
4392
|
+
const changedFiles = touchedPaths(changes);
|
|
4393
|
+
const report = {
|
|
4394
|
+
version: 1,
|
|
4395
|
+
root: resolvedRoot,
|
|
4396
|
+
base,
|
|
4397
|
+
mergeBase,
|
|
4398
|
+
changedFiles,
|
|
4399
|
+
missingPartners: [],
|
|
4400
|
+
touchedDecisions: [],
|
|
4401
|
+
historyAvailable: true,
|
|
4402
|
+
truncated: false
|
|
4215
4403
|
};
|
|
4216
|
-
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
|
|
4220
|
-
|
|
4221
|
-
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
init_install();
|
|
4225
|
-
init_files2();
|
|
4226
|
-
init_launcher();
|
|
4227
|
-
object = z15.record(z15.unknown());
|
|
4228
|
-
mcpPath = (host) => host === "codex" ? ".codex/config.toml" : ".mcp.json";
|
|
4229
|
-
TOML_START = "# mason:mcp:start";
|
|
4230
|
-
TOML_END = "# mason:mcp:end";
|
|
4231
|
-
POINTER_START = "<!-- mason:agents:start -->";
|
|
4232
|
-
POINTER_END = "<!-- mason:agents:end -->";
|
|
4233
|
-
}
|
|
4234
|
-
});
|
|
4235
|
-
|
|
4236
|
-
// src/distribution/bundle.ts
|
|
4237
|
-
import fs16 from "fs/promises";
|
|
4238
|
-
import path21 from "path";
|
|
4239
|
-
import { createHash as createHash4 } from "crypto";
|
|
4240
|
-
import { z as z16 } from "zod";
|
|
4241
|
-
async function readBundle(root) {
|
|
4242
|
-
const bytes = await fs16.readFile(await storePath(root, "bundle.json"));
|
|
4243
|
-
const manifest = bundleSchema.parse(JSON.parse(bytes.toString("utf8")));
|
|
4244
|
-
const windows = manifest.target.startsWith("win32-");
|
|
4245
|
-
for (const file of Object.keys(manifest.files)) {
|
|
4246
|
-
if (file.includes("\\") || file.split("/").some((p) => !p || p === "." || p === "..") || path21.isAbsolute(file)) {
|
|
4247
|
-
throw new Error("Unsafe path in Mason bundle: " + file);
|
|
4404
|
+
const store = await loadDecisionStore(resolvedRoot);
|
|
4405
|
+
report.diagnostics = store.diagnostics;
|
|
4406
|
+
const decisionDrift = await computeDecisionDrift(resolvedRoot, store.records);
|
|
4407
|
+
if (options.evidence !== void 0) {
|
|
4408
|
+
report.evidence = await collectReviewEvidence(resolvedRoot, options.evidence, changedFiles, store.records, decisionDrift.freshness, head);
|
|
4409
|
+
if (store.diagnostics.length) {
|
|
4410
|
+
report.evidence.diagnostics.push("Invalid decision records make knowledge associations incomplete; consult review diagnostics.");
|
|
4411
|
+
if (report.evidence.status === "passed") report.evidence.status = "incomplete";
|
|
4248
4412
|
}
|
|
4249
4413
|
}
|
|
4250
|
-
|
|
4251
|
-
if (
|
|
4414
|
+
const finalize = async () => {
|
|
4415
|
+
if (report.evidence && await getCurrentGitHash(resolvedRoot) !== head) {
|
|
4416
|
+
report.evidence.diagnostics.push("HEAD changed during the review; rerun to obtain consistent change and knowledge associations.");
|
|
4417
|
+
for (const check of report.evidence.checks) check.freshness = "unknown";
|
|
4418
|
+
report.evidence.summary.stale = 0;
|
|
4419
|
+
report.evidence.summary.unknown = report.evidence.checks.length;
|
|
4420
|
+
if (report.evidence.status !== "unavailable") report.evidence.status = "incomplete";
|
|
4421
|
+
}
|
|
4422
|
+
return report;
|
|
4423
|
+
};
|
|
4424
|
+
if (changedFiles.length === 0) return finalize();
|
|
4425
|
+
let analyzed = changedFiles;
|
|
4426
|
+
if (changedFiles.length > MAX_ANALYZED_FILES) {
|
|
4427
|
+
analyzed = changedFiles.slice(0, MAX_ANALYZED_FILES);
|
|
4428
|
+
report.truncated = true;
|
|
4252
4429
|
}
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
|
|
4259
|
-
|
|
4260
|
-
|
|
4261
|
-
|
|
4262
|
-
|
|
4263
|
-
|
|
4430
|
+
const partners = await findMissingPartners(
|
|
4431
|
+
resolvedRoot,
|
|
4432
|
+
analyzed,
|
|
4433
|
+
async (relPath) => {
|
|
4434
|
+
try {
|
|
4435
|
+
await fs15.access(path20.join(resolvedRoot, relPath));
|
|
4436
|
+
return true;
|
|
4437
|
+
} catch {
|
|
4438
|
+
return false;
|
|
4439
|
+
}
|
|
4440
|
+
},
|
|
4441
|
+
changedFiles
|
|
4442
|
+
);
|
|
4443
|
+
if (partners === null) {
|
|
4444
|
+
report.historyAvailable = false;
|
|
4445
|
+
} else {
|
|
4446
|
+
report.missingPartners = partners;
|
|
4447
|
+
}
|
|
4448
|
+
const decisions = store.records;
|
|
4449
|
+
for (const record of decisions) {
|
|
4450
|
+
if (record.status !== "active") continue;
|
|
4451
|
+
const effective = effectiveDecision(record);
|
|
4452
|
+
const touched = anchorsTouched(effective, changedFiles);
|
|
4453
|
+
const proposalTouched = effective !== record ? anchorsTouched(record, changedFiles) : [];
|
|
4454
|
+
if (touched.length > 0 || proposalTouched.length > 0) {
|
|
4455
|
+
const { pendingProposal, ...knowledge } = decisionKnowledge(record, decisionDrift.freshness?.[record.id] ?? "unknown", decisionDrift.pendingProposals?.[record.id]?.freshness ?? "unknown");
|
|
4456
|
+
report.touchedDecisions.push({
|
|
4457
|
+
...knowledge,
|
|
4458
|
+
...pendingProposal ? { pendingProposal: { ...pendingProposal, touchedFiles: proposalTouched } } : {},
|
|
4459
|
+
id: record.id,
|
|
4460
|
+
anchors: effective.files,
|
|
4461
|
+
freshness: decisionDrift.freshness?.[record.id] ?? "unknown",
|
|
4462
|
+
touchedFiles: touched
|
|
4463
|
+
});
|
|
4464
|
+
}
|
|
4465
|
+
}
|
|
4466
|
+
return finalize();
|
|
4264
4467
|
}
|
|
4265
|
-
var
|
|
4266
|
-
var
|
|
4267
|
-
"src/
|
|
4468
|
+
var exec10, MAX_ANALYZED_FILES;
|
|
4469
|
+
var init_review = __esm({
|
|
4470
|
+
"src/review/review.ts"() {
|
|
4268
4471
|
"use strict";
|
|
4269
|
-
|
|
4270
|
-
|
|
4271
|
-
|
|
4272
|
-
|
|
4273
|
-
|
|
4274
|
-
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
sha256 = (bytes) => createHash4("sha256").update(bytes).digest("hex");
|
|
4472
|
+
init_drift();
|
|
4473
|
+
init_provenance();
|
|
4474
|
+
init_decisions();
|
|
4475
|
+
init_paths();
|
|
4476
|
+
init_drift2();
|
|
4477
|
+
init_cochange();
|
|
4478
|
+
init_evidence2();
|
|
4479
|
+
init_snapshot();
|
|
4480
|
+
exec10 = promisify12(execFile12);
|
|
4481
|
+
MAX_ANALYZED_FILES = 50;
|
|
4280
4482
|
}
|
|
4281
4483
|
});
|
|
4282
4484
|
|
|
4283
|
-
// src/
|
|
4284
|
-
|
|
4285
|
-
__export(runtime_exports, {
|
|
4286
|
-
installRuntime: () => installRuntime,
|
|
4287
|
-
sourceRuntime: () => sourceRuntime,
|
|
4288
|
-
verifyRuntime: () => verifyRuntime
|
|
4289
|
-
});
|
|
4290
|
-
import fs17 from "fs/promises";
|
|
4291
|
-
import path22 from "path";
|
|
4292
|
-
import { fileURLToPath } from "url";
|
|
4293
|
-
import { randomUUID as randomUUID6, createHash as createHash5 } from "crypto";
|
|
4294
|
-
import { execFile as execFile11 } from "child_process";
|
|
4295
|
-
import { promisify as promisify11 } from "util";
|
|
4296
|
-
async function packageRoot() {
|
|
4297
|
-
let directory = path22.dirname(fileURLToPath(import.meta.url));
|
|
4298
|
-
for (let i = 0; i < 8; i++) {
|
|
4299
|
-
try {
|
|
4300
|
-
const pkg = JSON.parse(await fs17.readFile(path22.join(directory, "package.json"), "utf8"));
|
|
4301
|
-
if (pkg.name === "mason-context") return directory;
|
|
4302
|
-
} catch {
|
|
4303
|
-
}
|
|
4304
|
-
directory = path22.dirname(directory);
|
|
4305
|
-
}
|
|
4306
|
-
throw new Error("Cannot locate the executing Mason distribution. Reinstall Mason and rerun setup.");
|
|
4307
|
-
}
|
|
4308
|
-
async function sourceRuntime() {
|
|
4309
|
-
const source = await packageRoot();
|
|
4310
|
-
const pkg = JSON.parse(await fs17.readFile(path22.join(source, "package.json"), "utf8"));
|
|
4311
|
-
const hashes = Object.fromEntries(await Promise.all(BINARIES.map(async (file) => [file, checksum(await fs17.readFile(path22.join(source, file)))])));
|
|
4312
|
-
const bundleRoot = path22.dirname(source);
|
|
4313
|
-
const bundleExists = await fs17.lstat(path22.join(bundleRoot, "bundle.json")).then(() => true, (error) => {
|
|
4314
|
-
if (error.code === "ENOENT") return false;
|
|
4315
|
-
throw error;
|
|
4316
|
-
});
|
|
4317
|
-
if (bundleExists) {
|
|
4318
|
-
const { manifest, manifestHash } = await verifyBundle(bundleRoot);
|
|
4319
|
-
if (manifest.version !== pkg.version) throw new Error("Mason package and bundle versions differ.");
|
|
4320
|
-
return { source, bundleRoot, runtime: runtimeSchema.parse({
|
|
4321
|
-
id: manifestHash.slice(0, 24),
|
|
4322
|
-
version: pkg.version,
|
|
4323
|
-
hashes,
|
|
4324
|
-
bundle: { target: manifest.target, manifestHash }
|
|
4325
|
-
}) };
|
|
4326
|
-
}
|
|
4327
|
-
return { source, runtime: runtimeSchema.parse({ id: hash([pkg.version, pkg.dependencies, hashes]).slice(0, 24), version: pkg.version, hashes }) };
|
|
4328
|
-
}
|
|
4329
|
-
async function verifyRuntime(root, runtime) {
|
|
4485
|
+
// src/mcp/onboarding.ts
|
|
4486
|
+
async function auditSummary(root) {
|
|
4330
4487
|
try {
|
|
4331
|
-
const
|
|
4332
|
-
if (
|
|
4333
|
-
if (
|
|
4334
|
-
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
|
|
4338
|
-
|
|
4339
|
-
|
|
4340
|
-
|
|
4341
|
-
|
|
4342
|
-
|
|
4343
|
-
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
|
|
4347
|
-
} catch {
|
|
4348
|
-
return
|
|
4488
|
+
const report = await computeAudit(root);
|
|
4489
|
+
if (!report) return { status: "no-context-files", reason: "No AGENTS.md, CLAUDE.md, or .claude/CLAUDE.md found to audit." };
|
|
4490
|
+
if (!report.gitAvailable) return { status: "unavailable", reason: "Audit needs readable Git history to verify documentation claims.", docs: report.docs };
|
|
4491
|
+
return {
|
|
4492
|
+
...report,
|
|
4493
|
+
status: "complete",
|
|
4494
|
+
issues: report.issues.slice(0, MAX_FINDINGS2),
|
|
4495
|
+
advisories: report.advisories.slice(0, MAX_FINDINGS2),
|
|
4496
|
+
suppressedAdvisories: (report.suppressedAdvisories ?? []).slice(0, MAX_FINDINGS2),
|
|
4497
|
+
counts: {
|
|
4498
|
+
issues: report.issues.length,
|
|
4499
|
+
advisories: report.advisories.length,
|
|
4500
|
+
suppressedAdvisories: report.suppressedAdvisories?.length ?? 0
|
|
4501
|
+
},
|
|
4502
|
+
truncated: report.issues.length > MAX_FINDINGS2 || report.advisories.length > MAX_FINDINGS2 || (report.suppressedAdvisories?.length ?? 0) > MAX_FINDINGS2
|
|
4503
|
+
};
|
|
4504
|
+
} catch (error) {
|
|
4505
|
+
return { status: "unavailable", reason: reason(error) };
|
|
4349
4506
|
}
|
|
4350
4507
|
}
|
|
4351
|
-
async function
|
|
4352
|
-
const
|
|
4353
|
-
|
|
4354
|
-
const relative = `.mason/runtime/${runtime.id}`;
|
|
4355
|
-
const target = await storePath(root, relative, true);
|
|
4356
|
-
const stage = await storePath(root, ".mason/runtime/.install-" + randomUUID6(), true);
|
|
4357
|
-
await fs17.mkdir(stage);
|
|
4508
|
+
async function reviewSummary(root, requestedBase, evidence) {
|
|
4509
|
+
const workingTree = await getWorkingTree(root);
|
|
4510
|
+
const scope = "committed";
|
|
4358
4511
|
try {
|
|
4359
|
-
|
|
4360
|
-
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
|
|
4364
|
-
|
|
4365
|
-
|
|
4366
|
-
|
|
4367
|
-
}
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
if (checksum(await fs17.readFile(path22.join(stage, "node_modules/mason-context", file))) !== runtime.hashes[file]) throw new Error("Installed Mason binary differs from the selected distribution.");
|
|
4380
|
-
}
|
|
4381
|
-
}
|
|
4382
|
-
await fs17.writeFile(path22.join(stage, "receipt.json"), JSON.stringify(runtime, null, 2) + "\n");
|
|
4383
|
-
try {
|
|
4384
|
-
await fs17.rename(target, target + ".previous-" + randomUUID6());
|
|
4385
|
-
} catch (error) {
|
|
4386
|
-
if (error.code !== "ENOENT") throw error;
|
|
4387
|
-
}
|
|
4388
|
-
await fs17.rename(stage, target);
|
|
4389
|
-
if (!await verifyRuntime(root, runtime)) throw new Error("The installed Mason runtime could not be verified.");
|
|
4390
|
-
return runtime;
|
|
4391
|
-
} finally {
|
|
4392
|
-
await fs17.rm(stage, { recursive: true, force: true });
|
|
4512
|
+
const base = requestedBase ?? await defaultBase(root);
|
|
4513
|
+
if (!base) return { status: "unavailable", scope, workingTree, reason: "No default review base resolves. Pass base to mason_init, or run mason-review --base <ref>." };
|
|
4514
|
+
const report = await computeReview(root, base, { evidence });
|
|
4515
|
+
if (!report) return { status: "unavailable", scope, base, workingTree, reason: "The review base, merge base, or committed diff could not be read." };
|
|
4516
|
+
return {
|
|
4517
|
+
...report,
|
|
4518
|
+
scope,
|
|
4519
|
+
workingTree,
|
|
4520
|
+
...report.evidence ? { evidence: summarizeEvidence(report.evidence) } : {},
|
|
4521
|
+
status: !report.historyAvailable ? "unavailable" : report.changedFiles.length ? "complete" : "no-changes",
|
|
4522
|
+
...!report.historyAvailable ? { reason: "Co-change history could not be read; review findings are incomplete." } : {},
|
|
4523
|
+
changedFiles: report.changedFiles.slice(0, MAX_FINDINGS2),
|
|
4524
|
+
missingPartners: report.missingPartners.slice(0, MAX_FINDINGS2),
|
|
4525
|
+
touchedDecisions: report.touchedDecisions.slice(0, MAX_FINDINGS2),
|
|
4526
|
+
counts: { changedFiles: report.changedFiles.length, missingPartners: report.missingPartners.length, touchedDecisions: report.touchedDecisions.length },
|
|
4527
|
+
truncated: report.truncated || [report.changedFiles, report.missingPartners, report.touchedDecisions].some((list) => list.length > MAX_FINDINGS2),
|
|
4528
|
+
hint: "Reviews cover merge-base..HEAD. Uncommitted paths are reported separately in workingTree; they have not been reviewed."
|
|
4529
|
+
};
|
|
4530
|
+
} catch (error) {
|
|
4531
|
+
return { status: "unavailable", scope, workingTree, reason: reason(error) };
|
|
4393
4532
|
}
|
|
4394
4533
|
}
|
|
4395
|
-
|
|
4396
|
-
|
|
4397
|
-
|
|
4534
|
+
async function inspectOnboarding(root, base, evidence) {
|
|
4535
|
+
const [audit, review, map, decisions] = await Promise.all([
|
|
4536
|
+
auditSummary(root),
|
|
4537
|
+
reviewSummary(root, base, evidence),
|
|
4538
|
+
inspectSnapshot(root),
|
|
4539
|
+
loadDecisionStore(root)
|
|
4540
|
+
]);
|
|
4541
|
+
return {
|
|
4542
|
+
audit,
|
|
4543
|
+
review,
|
|
4544
|
+
map: { status: map.status },
|
|
4545
|
+
decisions: {
|
|
4546
|
+
active: decisions.records.filter((record) => record.status === "active").length,
|
|
4547
|
+
...Object.fromEntries(["accepted", "proposed", "unreviewed"].map((approval) => [approval, decisions.records.filter((record) => record.status === "active" && decisionApproval(effectiveDecision(record)) === approval).length])),
|
|
4548
|
+
pendingProposals: decisions.records.filter((record) => effectiveDecision(record) !== record).length
|
|
4549
|
+
},
|
|
4550
|
+
diagnostics: [...map.diagnostics, ...decisions.diagnostics]
|
|
4551
|
+
};
|
|
4552
|
+
}
|
|
4553
|
+
var MAX_FINDINGS2, reason;
|
|
4554
|
+
var init_onboarding = __esm({
|
|
4555
|
+
"src/mcp/onboarding.ts"() {
|
|
4398
4556
|
"use strict";
|
|
4399
|
-
|
|
4400
|
-
|
|
4401
|
-
|
|
4402
|
-
|
|
4403
|
-
|
|
4404
|
-
|
|
4405
|
-
|
|
4557
|
+
init_audit();
|
|
4558
|
+
init_review();
|
|
4559
|
+
init_drift();
|
|
4560
|
+
init_snapshot();
|
|
4561
|
+
init_provenance();
|
|
4562
|
+
init_evidence2();
|
|
4563
|
+
init_decisions();
|
|
4564
|
+
MAX_FINDINGS2 = 20;
|
|
4565
|
+
reason = (error) => error instanceof Error ? error.message : String(error);
|
|
4406
4566
|
}
|
|
4407
4567
|
});
|
|
4408
4568
|
|
|
@@ -4412,7 +4572,7 @@ __export(status_exports, {
|
|
|
4412
4572
|
setupStatus: () => setupStatus,
|
|
4413
4573
|
summarizeActivation: () => summarizeActivation
|
|
4414
4574
|
});
|
|
4415
|
-
import { isDeepStrictEqual as
|
|
4575
|
+
import { isDeepStrictEqual as isDeepStrictEqual3 } from "util";
|
|
4416
4576
|
async function setupStatus(dir) {
|
|
4417
4577
|
const ws = await workspace(dir);
|
|
4418
4578
|
const setup = await loadSetup(ws.root);
|
|
@@ -4429,35 +4589,33 @@ async function setupStatus(dir) {
|
|
|
4429
4589
|
next: pending.length ? "Setup did not finish. Rerun mason-auto setup --host " + pending[0] + " to resume using the retained original evidence." : "Run mason-auto setup --host codex or --host claude."
|
|
4430
4590
|
};
|
|
4431
4591
|
}
|
|
4432
|
-
const
|
|
4433
|
-
const
|
|
4592
|
+
const hosts2 = {};
|
|
4593
|
+
const installed = await installedCommand();
|
|
4434
4594
|
const automation = await automationStatus(ws.root);
|
|
4435
4595
|
for (const host of ["codex", "claude"]) {
|
|
4436
4596
|
const entry = setup.hosts[host];
|
|
4437
4597
|
if (!entry) continue;
|
|
4438
|
-
const launcherCurrent = commonLauncherCurrent && (!entry.runtime.bundle || await readText(ws.root, ".mason/run.sh") === SHELL_LAUNCHER && await readText(ws.root, ".mason/run.ps1") === POWERSHELL_LAUNCHER && await readText(ws.root, `.mason/runtime/${host}.txt`) === entry.runtime.id + "\n");
|
|
4439
4598
|
const instructions = await instructionEdits(ws.root, host);
|
|
4440
4599
|
const instructionsCurrent = instructions.every((edit) => edit.before === edit.after);
|
|
4441
|
-
const
|
|
4442
|
-
const
|
|
4443
|
-
const
|
|
4444
|
-
const hooks = isDeepStrictEqual2(config.hooks, hookConfig(host, hookCommand(host, entry.runtime)).hooks) && !config.disabled;
|
|
4600
|
+
const config = await inspectHostConfig(ws.root, host, void 0);
|
|
4601
|
+
const mcp = !config.mcpDisabled && hash(config.mcp) === entry.mcpFingerprint && isDeepStrictEqual3({ command: config.mcp?.command, args: config.mcp?.args }, mcpCommand(host));
|
|
4602
|
+
const hooks = isDeepStrictEqual3(config.hooks, hookConfig(host, hookCommand(host)).hooks) && !config.disabled;
|
|
4445
4603
|
const local = await loadSetupReceipt(ws.root, ws.directory, host);
|
|
4446
4604
|
const configured = local?.status === "configured" && local.root === ws.root && local.revision === entry.revision;
|
|
4447
|
-
const observation = await readObservation(ws.root, ws.directory, host, entry.revision);
|
|
4605
|
+
const observation = await readObservation(ws.root, ws.directory, host, entry.revision, installed.version ?? "unavailable");
|
|
4448
4606
|
const sessions = Object.values(observation?.sessions ?? {}).sort((a, b) => b.at.localeCompare(a.at));
|
|
4449
4607
|
const complete = sessions.find((session) => events.every((event) => session.events.includes(event)));
|
|
4450
4608
|
const observedEvents = complete?.events ?? sessions[0]?.events ?? [];
|
|
4451
4609
|
const pending = [];
|
|
4452
|
-
if (!installed
|
|
4610
|
+
if (!installed.available) pending.push(installed.message);
|
|
4453
4611
|
if (!mcp || !hooks || !instructionsCurrent || !configured) pending.push("Rerun setup to reconcile project configuration; inspect any disabled host settings.");
|
|
4454
4612
|
if (!observation?.contextCalls) pending.push("Start a new assistant session and request task context through Mason's get_context tool.");
|
|
4455
4613
|
if (!complete) pending.push("Review/trust the host configuration, then complete a normal task to observe the full hook lifecycle.");
|
|
4456
4614
|
if (automation.status === "unavailable") pending.push("The latest automation attempt did not establish verification. Run mason-auto check and inspect its diagnostics.");
|
|
4457
|
-
const healthy = installed &&
|
|
4458
|
-
|
|
4615
|
+
const healthy = installed.available && mcp && hooks && instructionsCurrent && configured && automation.status !== "unavailable";
|
|
4616
|
+
hosts2[host] = {
|
|
4459
4617
|
status: healthy ? complete && observation?.contextCalls ? "active" : "pending" : "attention",
|
|
4460
|
-
runtime: installed
|
|
4618
|
+
runtime: installed.available ? "global (" + installed.version + ")" : "unavailable-on-path",
|
|
4461
4619
|
mcp: mcp ? "configured" : "changed",
|
|
4462
4620
|
instructions: instructionsCurrent ? "current" : "changed",
|
|
4463
4621
|
hookConfiguration: hooks ? "configured" : "disabled-or-changed",
|
|
@@ -4467,13 +4625,13 @@ async function setupStatus(dir) {
|
|
|
4467
4625
|
pending
|
|
4468
4626
|
};
|
|
4469
4627
|
}
|
|
4470
|
-
const statuses = Object.values(
|
|
4628
|
+
const statuses = Object.values(hosts2).map((host) => host.status);
|
|
4471
4629
|
const decisions = await loadDecisionStore(ws.root);
|
|
4472
4630
|
return {
|
|
4473
4631
|
version: 1,
|
|
4474
4632
|
status: statuses.length && statuses.every((status) => status === "active") ? "active" : statuses.includes("attention") ? "attention" : "pending",
|
|
4475
4633
|
root: ws.root,
|
|
4476
|
-
hosts,
|
|
4634
|
+
hosts: hosts2,
|
|
4477
4635
|
decisionRecords: decisions.records.length,
|
|
4478
4636
|
diagnostics: decisions.diagnostics,
|
|
4479
4637
|
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."
|
|
@@ -4503,10 +4661,8 @@ var init_status = __esm({
|
|
|
4503
4661
|
init_decisions();
|
|
4504
4662
|
init_adapters();
|
|
4505
4663
|
init_model();
|
|
4506
|
-
init_runtime2();
|
|
4507
4664
|
init_config();
|
|
4508
4665
|
init_launcher();
|
|
4509
|
-
init_files2();
|
|
4510
4666
|
init_observations();
|
|
4511
4667
|
}
|
|
4512
4668
|
});
|
|
@@ -4518,7 +4674,7 @@ __export(setup_exports, {
|
|
|
4518
4674
|
setupProject: () => setupProject,
|
|
4519
4675
|
summarizeSetup: () => summarizeSetup
|
|
4520
4676
|
});
|
|
4521
|
-
import { randomUUID as
|
|
4677
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
4522
4678
|
async function selectHost(root, explicit) {
|
|
4523
4679
|
if (explicit) return explicit;
|
|
4524
4680
|
if (process.env.MASON_SETUP_HOST === "codex" || process.env.MASON_SETUP_HOST === "claude") return process.env.MASON_SETUP_HOST;
|
|
@@ -4534,28 +4690,29 @@ async function setupProject(dir, options = {}) {
|
|
|
4534
4690
|
return withLock(ws.root, ".mason/reports/setup-lock", async () => {
|
|
4535
4691
|
const existing = await loadSetup(ws.root);
|
|
4536
4692
|
const previousReceipt = await loadSetupReceipt(ws.root, ws.directory, host);
|
|
4537
|
-
const
|
|
4538
|
-
|
|
4693
|
+
const installed = await installedCommand();
|
|
4694
|
+
if (!installed.available) throw new Error(installed.message);
|
|
4539
4695
|
const instructions = await instructionEdits(ws.root, host);
|
|
4540
|
-
const mcp = await mcpEdit(ws.root, host
|
|
4541
|
-
const hooks = await hookEdits(ws.root, host
|
|
4696
|
+
const mcp = await mcpEdit(ws.root, host);
|
|
4697
|
+
const hooks = await hookEdits(ws.root, host);
|
|
4542
4698
|
const ancillary = await ancillaryEdits(ws.root);
|
|
4543
4699
|
const edits = [...ancillary, ...instructions, mcp, ...hooks];
|
|
4544
|
-
const setupBefore = await readText(ws.root,
|
|
4545
|
-
const mcpFingerprint = hash((await inspectHostConfig(ws.root, host, mcp.after
|
|
4546
|
-
const desiredHooks = hookConfig(host, hookCommand(host
|
|
4700
|
+
const setupBefore = await readText(ws.root, SETUP_PATH);
|
|
4701
|
+
const mcpFingerprint = hash((await inspectHostConfig(ws.root, host, mcp.after)).mcp);
|
|
4702
|
+
const desiredHooks = hookConfig(host, hookCommand(host)).hooks;
|
|
4703
|
+
const configuredHook = desiredHooks.SessionStart[0].hooks[0].command;
|
|
4704
|
+
const ownership = await ownershipEdit(ws.root, [...instructions, mcp, hooks[0]], { [hooks[0].path]: configuredHook });
|
|
4547
4705
|
const fingerprint = hash({
|
|
4548
|
-
|
|
4549
|
-
launcher: LAUNCHER,
|
|
4706
|
+
command: "mason",
|
|
4550
4707
|
mcp: mcpFingerprint,
|
|
4551
4708
|
hooks: desiredHooks,
|
|
4552
4709
|
instructions: instructions.map((edit) => edit.path)
|
|
4553
4710
|
});
|
|
4554
4711
|
const previous = existing?.hosts[host];
|
|
4555
4712
|
const changed = edits.some((edit) => edit.before !== edit.after) || previous?.fingerprint !== fingerprint;
|
|
4556
|
-
const revision = !changed && previous ? previous.revision :
|
|
4557
|
-
const setup = existing ?? { version:
|
|
4558
|
-
setup.hosts[host] = {
|
|
4713
|
+
const revision = !changed && previous ? previous.revision : randomUUID6();
|
|
4714
|
+
const setup = existing ?? { version: 2, hosts: {} };
|
|
4715
|
+
setup.hosts[host] = { configuredVersion: installed.version, revision, fingerprint, mcpFingerprint, instructions: instructions.map((e) => e.path) };
|
|
4559
4716
|
const initial = await automate(ws.root, { event: "turn_start" });
|
|
4560
4717
|
const findings = await inspectOnboarding(ws.root, options.base, options.evidence);
|
|
4561
4718
|
const receiptPath = ws.directory + "/setup-" + host + ".json";
|
|
@@ -4570,20 +4727,13 @@ async function setupProject(dir, options = {}) {
|
|
|
4570
4727
|
root: ws.root,
|
|
4571
4728
|
revision
|
|
4572
4729
|
});
|
|
4573
|
-
await installRuntime(ws.root, selected);
|
|
4574
|
-
if (selected.runtime.bundle) {
|
|
4575
|
-
const pointer = `.mason/runtime/${host}.txt`;
|
|
4576
|
-
await applyEdit(ws.root, { path: pointer, before: await readText(ws.root, pointer), after: selected.runtime.id + "\n" });
|
|
4577
|
-
}
|
|
4578
4730
|
const changedFiles = [];
|
|
4731
|
+
if (await applyEdit(ws.root, ownership)) changedFiles.push(ownership.path);
|
|
4579
4732
|
for (const edit of edits) if (await applyEdit(ws.root, edit)) changedFiles.push(edit.path);
|
|
4580
|
-
if (await applyEdit(ws.root, { path:
|
|
4581
|
-
if (!
|
|
4582
|
-
await saveProjectMarker(ws.root, { version: 1, initializedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
4583
|
-
changedFiles.push(".mason/project.json");
|
|
4584
|
-
}
|
|
4733
|
+
if (await applyEdit(ws.root, { path: SETUP_PATH, before: setupBefore, after: JSON.stringify(setup, null, 2) + "\n" })) changedFiles.push(SETUP_PATH);
|
|
4734
|
+
if (await applyEdit(ws.root, await completedHookOwnership(ws.root, hooks[0].path, configuredHook)) && !changedFiles.includes(ownership.path)) changedFiles.push(ownership.path);
|
|
4585
4735
|
const checked = await automate(ws.root, { event: "turn_start" });
|
|
4586
|
-
const configured = await inspectHostConfig(ws.root, host, void 0
|
|
4736
|
+
const configured = await inspectHostConfig(ws.root, host, void 0);
|
|
4587
4737
|
await writeStoreJson(ws.root, receiptPath, {
|
|
4588
4738
|
version: 1,
|
|
4589
4739
|
host,
|
|
@@ -4600,7 +4750,7 @@ async function setupProject(dir, options = {}) {
|
|
|
4600
4750
|
status: "configured",
|
|
4601
4751
|
host,
|
|
4602
4752
|
root: ws.root,
|
|
4603
|
-
runtime:
|
|
4753
|
+
runtime: { kind: "global", command: "mason", version: installed.version },
|
|
4604
4754
|
changedFiles,
|
|
4605
4755
|
initialReportPath,
|
|
4606
4756
|
findings,
|
|
@@ -4614,14 +4764,15 @@ function summarizeSetup(result) {
|
|
|
4614
4764
|
const audit = result.findings.audit;
|
|
4615
4765
|
return [
|
|
4616
4766
|
`Mason configured for ${result.host}.`,
|
|
4617
|
-
"
|
|
4767
|
+
" MCP server and lifecycle hooks use mason from PATH.",
|
|
4618
4768
|
" Project instructions updated; original audit evidence retained.",
|
|
4619
|
-
` ${result.changedFiles.length}
|
|
4769
|
+
` ${result.changedFiles.length} files changed.`,
|
|
4620
4770
|
`Initial audit: ${audit.status}${"counts" in audit ? `; ${audit.counts.issues} issues, ${audit.counts.advisories + audit.counts.suppressedAdvisories} advisories` : ""}.`,
|
|
4621
4771
|
..."issues" in audit ? audit.issues.slice(0, 3).map((f) => " " + f.message) : [],
|
|
4622
4772
|
`Original evidence: ${result.initialReportPath}`,
|
|
4623
4773
|
"Activation: " + result.activation.status + ".",
|
|
4624
4774
|
result.next,
|
|
4775
|
+
"If the assistant cannot find mason, restart the desktop app so it picks up PATH.",
|
|
4625
4776
|
"After the task finishes, run mason-auto status. Configuration alone does not establish activation."
|
|
4626
4777
|
].join("\n");
|
|
4627
4778
|
}
|
|
@@ -4631,7 +4782,6 @@ var init_setup = __esm({
|
|
|
4631
4782
|
init_evidence();
|
|
4632
4783
|
init_store();
|
|
4633
4784
|
init_runtime();
|
|
4634
|
-
init_init();
|
|
4635
4785
|
init_onboarding();
|
|
4636
4786
|
init_storage();
|
|
4637
4787
|
init_files2();
|
|
@@ -4639,24 +4789,71 @@ var init_setup = __esm({
|
|
|
4639
4789
|
init_launcher();
|
|
4640
4790
|
init_adapters();
|
|
4641
4791
|
init_model();
|
|
4642
|
-
init_runtime2();
|
|
4643
4792
|
init_status();
|
|
4793
|
+
init_ownership();
|
|
4794
|
+
}
|
|
4795
|
+
});
|
|
4796
|
+
|
|
4797
|
+
// src/distribution/bundle.ts
|
|
4798
|
+
import fs16 from "fs/promises";
|
|
4799
|
+
import path21 from "path";
|
|
4800
|
+
import { createHash as createHash4 } from "crypto";
|
|
4801
|
+
import { z as z18 } from "zod";
|
|
4802
|
+
async function readBundle(root) {
|
|
4803
|
+
const bytes = await fs16.readFile(await storePath(root, "bundle.json"));
|
|
4804
|
+
const manifest = bundleSchema.parse(JSON.parse(bytes.toString("utf8")));
|
|
4805
|
+
const windows = manifest.target.startsWith("win32-");
|
|
4806
|
+
for (const file of Object.keys(manifest.files)) {
|
|
4807
|
+
if (file.includes("\\") || file.split("/").some((p) => !p || p === "." || p === "..") || path21.isAbsolute(file)) {
|
|
4808
|
+
throw new Error("Unsafe path in Mason bundle: " + file);
|
|
4809
|
+
}
|
|
4810
|
+
}
|
|
4811
|
+
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"]) {
|
|
4812
|
+
if (!manifest.files[required]) throw new Error("Incomplete Mason bundle: " + required);
|
|
4813
|
+
}
|
|
4814
|
+
return { manifest, manifestHash: sha256(bytes) };
|
|
4815
|
+
}
|
|
4816
|
+
async function verifyBundle(root, expectedHash) {
|
|
4817
|
+
const bundle = await readBundle(root);
|
|
4818
|
+
if (expectedHash && bundle.manifestHash !== expectedHash) throw new Error("Mason bundle manifest changed.");
|
|
4819
|
+
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
|
+
const files = Object.keys(bundle.manifest.files);
|
|
4821
|
+
for (let i = 0; i < files.length; i += 8) await Promise.all(files.slice(i, i + 8).map(async (file) => {
|
|
4822
|
+
if (sha256(await fs16.readFile(await storePath(root, file))) !== bundle.manifest.files[file]) throw new Error("Mason bundle checksum mismatch: " + file);
|
|
4823
|
+
}));
|
|
4824
|
+
return bundle;
|
|
4825
|
+
}
|
|
4826
|
+
var targets, digest2, bundleSchema, sha256;
|
|
4827
|
+
var init_bundle = __esm({
|
|
4828
|
+
"src/distribution/bundle.ts"() {
|
|
4829
|
+
"use strict";
|
|
4830
|
+
init_storage();
|
|
4831
|
+
targets = ["darwin-arm64", "darwin-x64", "linux-arm64", "linux-x64", "win32-arm64", "win32-x64"];
|
|
4832
|
+
digest2 = z18.string().regex(/^[a-f0-9]{64}$/);
|
|
4833
|
+
bundleSchema = z18.object({
|
|
4834
|
+
format: z18.literal(1),
|
|
4835
|
+
version: z18.string().regex(/^\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?$/),
|
|
4836
|
+
target: z18.enum(targets),
|
|
4837
|
+
nodeVersion: z18.string(),
|
|
4838
|
+
files: z18.record(digest2)
|
|
4839
|
+
}).strict();
|
|
4840
|
+
sha256 = (bytes) => createHash4("sha256").update(bytes).digest("hex");
|
|
4644
4841
|
}
|
|
4645
4842
|
});
|
|
4646
4843
|
|
|
4647
4844
|
// src/distribution/path.ts
|
|
4648
|
-
import
|
|
4845
|
+
import fs17 from "fs/promises";
|
|
4649
4846
|
import os3 from "os";
|
|
4650
|
-
import
|
|
4651
|
-
import { execFile as
|
|
4652
|
-
import { promisify as
|
|
4653
|
-
import { createHash as
|
|
4654
|
-
import { z as
|
|
4847
|
+
import path22 from "path";
|
|
4848
|
+
import { execFile as execFile13 } from "child_process";
|
|
4849
|
+
import { promisify as promisify13 } from "util";
|
|
4850
|
+
import { createHash as createHash5, randomUUID as randomUUID7 } from "crypto";
|
|
4851
|
+
import { z as z19 } from "zod";
|
|
4655
4852
|
async function configurePath(bin, previous, save, options = {}) {
|
|
4656
4853
|
const env = options.env ?? process.env, platform = options.platform ?? process.platform;
|
|
4657
4854
|
const changes = [...previous];
|
|
4658
4855
|
const currentPath = platform === "win32" ? Object.entries(env).find(([key]) => key.toLowerCase() === "path")?.[1] : env.PATH;
|
|
4659
|
-
const alreadyOnPath = platform === "win32" ? hasWindowsEntry(currentPath ?? null, bin, env) : (currentPath ?? "").split(":").some((entry) =>
|
|
4856
|
+
const alreadyOnPath = platform === "win32" ? hasWindowsEntry(currentPath ?? null, bin, env) : (currentPath ?? "").split(":").some((entry) => path22.posix.isAbsolute(entry) && path22.posix.resolve(entry) === path22.posix.resolve(bin));
|
|
4660
4857
|
const message2 = alreadyOnPath ? "Run: mason setup --host codex" : "Open a new terminal, then run: mason setup --host codex";
|
|
4661
4858
|
const manual = (reason2) => ({ status: "manual", changes, message: `${reason2}
|
|
4662
4859
|
${alreadyOnPath ? message2 : `Add ${bin} to your PATH, then run: mason setup --host codex`}` });
|
|
@@ -4674,33 +4871,33 @@ ${alreadyOnPath ? message2 : `Add ${bin} to your PATH, then run: mason setup --h
|
|
|
4674
4871
|
}
|
|
4675
4872
|
return { status: "configured", changes, message: message2 };
|
|
4676
4873
|
}
|
|
4677
|
-
const shell =
|
|
4874
|
+
const shell = path22.basename(env.SHELL ?? os3.userInfo().shell ?? "");
|
|
4678
4875
|
const userHome = options.userHome ?? os3.homedir();
|
|
4679
4876
|
let profiles;
|
|
4680
4877
|
if (!["bash", "zsh", "sh"].includes(shell)) return manual(`Automatic PATH setup is unavailable for ${shell || "an unknown shell"}.`);
|
|
4681
|
-
if (env.MASON_PROFILE) profiles = [
|
|
4682
|
-
else if (shell === "zsh") profiles = [
|
|
4683
|
-
else if (shell === "sh") profiles = [
|
|
4878
|
+
if (env.MASON_PROFILE) profiles = [path22.resolve(env.MASON_PROFILE)];
|
|
4879
|
+
else if (shell === "zsh") profiles = [path22.join(env.ZDOTDIR || userHome, ".zshrc")];
|
|
4880
|
+
else if (shell === "sh") profiles = [path22.join(userHome, ".profile")];
|
|
4684
4881
|
else {
|
|
4685
|
-
const candidates = [".bash_profile", ".bash_login", ".profile"].map((name) =>
|
|
4882
|
+
const candidates = [".bash_profile", ".bash_login", ".profile"].map((name) => path22.join(userHome, name));
|
|
4686
4883
|
let login = candidates[2];
|
|
4687
|
-
for (const file of candidates) if (await
|
|
4884
|
+
for (const file of candidates) if (await fs17.lstat(file).then(() => true, (error) => {
|
|
4688
4885
|
if (error.code === "ENOENT") return false;
|
|
4689
4886
|
throw error;
|
|
4690
4887
|
})) {
|
|
4691
4888
|
login = file;
|
|
4692
4889
|
break;
|
|
4693
4890
|
}
|
|
4694
|
-
profiles = [
|
|
4891
|
+
profiles = [path22.join(userHome, ".bashrc"), login];
|
|
4695
4892
|
}
|
|
4696
|
-
const marker = `# >>> mason PATH ${
|
|
4893
|
+
const marker = `# >>> mason PATH ${createHash5("sha256").update(bin).digest("hex").slice(0, 12)} >>>`;
|
|
4697
4894
|
for (const profile of profiles) {
|
|
4698
|
-
const stat = await
|
|
4895
|
+
const stat = await fs17.lstat(profile).catch((error) => {
|
|
4699
4896
|
if (error.code === "ENOENT") return null;
|
|
4700
4897
|
throw error;
|
|
4701
4898
|
});
|
|
4702
|
-
const file = stat?.isSymbolicLink() ? await
|
|
4703
|
-
const original = await
|
|
4899
|
+
const file = stat?.isSymbolicLink() ? await fs17.realpath(profile) : path22.resolve(profile);
|
|
4900
|
+
const original = await fs17.readFile(file).catch((error) => {
|
|
4704
4901
|
if (error.code === "ENOENT" && !stat) return null;
|
|
4705
4902
|
throw error;
|
|
4706
4903
|
});
|
|
@@ -4715,8 +4912,8 @@ ${alreadyOnPath ? message2 : `Add ${bin} to your PATH, then run: mason setup --h
|
|
|
4715
4912
|
changes.push({ kind: "profile", file, block, created: original === null });
|
|
4716
4913
|
await save(changes);
|
|
4717
4914
|
}
|
|
4718
|
-
await
|
|
4719
|
-
const handle = await
|
|
4915
|
+
await fs17.mkdir(path22.dirname(file), { recursive: true });
|
|
4916
|
+
const handle = await fs17.open(file, original === null ? "ax+" : "a+", 384);
|
|
4720
4917
|
try {
|
|
4721
4918
|
if (!(await handle.readFile()).equals(original ?? Buffer.alloc(0))) throw new Error(`Shell profile changed during installation: ${profile}`);
|
|
4722
4919
|
await handle.writeFile(block);
|
|
@@ -4740,23 +4937,23 @@ async function removePath(changes) {
|
|
|
4740
4937
|
await windowsPath("remove", change.entry, current, change.created);
|
|
4741
4938
|
continue;
|
|
4742
4939
|
}
|
|
4743
|
-
const stat = await
|
|
4940
|
+
const stat = await fs17.lstat(change.file).catch((error) => {
|
|
4744
4941
|
if (error.code === "ENOENT") return null;
|
|
4745
4942
|
throw error;
|
|
4746
4943
|
});
|
|
4747
4944
|
if (!stat) continue;
|
|
4748
4945
|
if (!stat.isFile()) throw new Error("profile type changed");
|
|
4749
|
-
const bytes = await
|
|
4946
|
+
const bytes = await fs17.readFile(change.file), text2 = bytes.toString("utf8");
|
|
4750
4947
|
if (!Buffer.from(text2).equals(bytes)) throw new Error("profile encoding changed");
|
|
4751
4948
|
const index2 = text2.indexOf(change.block);
|
|
4752
4949
|
if (index2 < 0 || text2.indexOf(change.block, index2 + change.block.length) >= 0) throw new Error("PATH block was edited or removed");
|
|
4753
4950
|
const prefix = text2.slice(0, index2), suffix = text2.slice(index2 + change.block.length);
|
|
4754
4951
|
const separator = prefix && suffix && !prefix.endsWith("\n") ? change.block.startsWith("\r\n") ? "\r\n" : "\n" : "";
|
|
4755
4952
|
const next = Buffer.from(prefix + separator + suffix);
|
|
4756
|
-
const temporary = change.file + ".mason-" +
|
|
4953
|
+
const temporary = change.file + ".mason-" + randomUUID7();
|
|
4757
4954
|
try {
|
|
4758
4955
|
if (stat.nlink > 1) throw new Error("profile has multiple hard links");
|
|
4759
|
-
const handle = await
|
|
4956
|
+
const handle = await fs17.open(temporary, "wx", stat.mode & 511);
|
|
4760
4957
|
try {
|
|
4761
4958
|
await handle.writeFile(next);
|
|
4762
4959
|
await handle.chmod(stat.mode & 511);
|
|
@@ -4765,11 +4962,11 @@ async function removePath(changes) {
|
|
|
4765
4962
|
} finally {
|
|
4766
4963
|
await handle.close();
|
|
4767
4964
|
}
|
|
4768
|
-
const current = await
|
|
4769
|
-
if (!current.isFile() || current.ino !== stat.ino || !(await
|
|
4770
|
-
await
|
|
4965
|
+
const current = await fs17.lstat(change.file);
|
|
4966
|
+
if (!current.isFile() || current.ino !== stat.ino || !(await fs17.readFile(change.file)).equals(bytes)) throw new Error("profile changed during uninstall");
|
|
4967
|
+
await fs17.rename(temporary, change.file);
|
|
4771
4968
|
} finally {
|
|
4772
|
-
await
|
|
4969
|
+
await fs17.rm(temporary, { force: true });
|
|
4773
4970
|
}
|
|
4774
4971
|
} catch (error) {
|
|
4775
4972
|
warnings.push(`Retained PATH setting ${change.kind === "profile" ? change.file : change.entry}: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -4810,13 +5007,13 @@ var execute2, pathChangeSchema, quote, normalizeWindowsEntry, hasWindowsEntry;
|
|
|
4810
5007
|
var init_path = __esm({
|
|
4811
5008
|
"src/distribution/path.ts"() {
|
|
4812
5009
|
"use strict";
|
|
4813
|
-
execute2 =
|
|
4814
|
-
pathChangeSchema =
|
|
4815
|
-
|
|
4816
|
-
|
|
5010
|
+
execute2 = promisify13(execFile13);
|
|
5011
|
+
pathChangeSchema = z19.discriminatedUnion("kind", [
|
|
5012
|
+
z19.object({ kind: z19.literal("profile"), file: z19.string(), block: z19.string(), created: z19.boolean() }),
|
|
5013
|
+
z19.object({ kind: z19.literal("windows"), entry: z19.string(), created: z19.boolean() })
|
|
4817
5014
|
]);
|
|
4818
5015
|
quote = (s) => "'" + s.replace(/'/g, "'\\''") + "'";
|
|
4819
|
-
normalizeWindowsEntry = (value) =>
|
|
5016
|
+
normalizeWindowsEntry = (value) => path22.win32.normalize(value.trim().replace(/^"(.*)"$/, "$1")).replace(/[\\/]+$/, "").toLowerCase();
|
|
4820
5017
|
hasWindowsEntry = (value, entry, env = process.env) => {
|
|
4821
5018
|
const variables = new Map(Object.entries(env).map(([key, value2]) => [key.toLowerCase(), value2]));
|
|
4822
5019
|
const expand = (part) => part.replace(/%([^%]+)%/g, (match, key) => variables.get(key.toLowerCase()) ?? match);
|
|
@@ -4834,41 +5031,41 @@ __export(install_exports2, {
|
|
|
4834
5031
|
uninstallStandalone: () => uninstallStandalone,
|
|
4835
5032
|
upgradeStandalone: () => upgradeStandalone
|
|
4836
5033
|
});
|
|
4837
|
-
import
|
|
5034
|
+
import fs18 from "fs/promises";
|
|
4838
5035
|
import os4 from "os";
|
|
4839
|
-
import
|
|
4840
|
-
import { fileURLToPath
|
|
5036
|
+
import path23 from "path";
|
|
5037
|
+
import { fileURLToPath } from "url";
|
|
4841
5038
|
import { spawn } from "child_process";
|
|
4842
|
-
import { randomUUID as
|
|
4843
|
-
import { z as
|
|
5039
|
+
import { randomUUID as randomUUID8 } from "crypto";
|
|
5040
|
+
import { z as z20 } from "zod";
|
|
4844
5041
|
async function replace(file, content2, mode = 384) {
|
|
4845
|
-
const temp = file + ".tmp-" +
|
|
5042
|
+
const temp = file + ".tmp-" + randomUUID8();
|
|
4846
5043
|
try {
|
|
4847
|
-
await
|
|
4848
|
-
await
|
|
5044
|
+
await fs18.writeFile(temp, content2, { mode, flag: "wx" });
|
|
5045
|
+
await fs18.rename(temp, file);
|
|
4849
5046
|
} finally {
|
|
4850
|
-
await
|
|
5047
|
+
await fs18.rm(temp, { force: true });
|
|
4851
5048
|
}
|
|
4852
5049
|
}
|
|
4853
5050
|
async function installStandalone(source) {
|
|
4854
5051
|
const bundle = await verifyBundle(source);
|
|
4855
|
-
const selectedHome =
|
|
4856
|
-
await
|
|
4857
|
-
const home = await
|
|
4858
|
-
const selectedBin =
|
|
5052
|
+
const selectedHome = path23.resolve(defaultHome());
|
|
5053
|
+
await fs18.mkdir(selectedHome, { recursive: true });
|
|
5054
|
+
const home = await fs18.realpath(selectedHome);
|
|
5055
|
+
const selectedBin = path23.resolve(process.env.MASON_BIN_DIR ?? (process.platform === "win32" ? path23.join(home, "bin") : path23.join(os4.homedir(), ".local/bin")));
|
|
4859
5056
|
return withLock(home, ".install-lock", async () => {
|
|
4860
5057
|
const raw = await readStoreJson(home, "install.json");
|
|
4861
5058
|
const previous = raw === null ? null : recordSchema2.parse(raw);
|
|
4862
|
-
if (!previous) for (const file of await
|
|
5059
|
+
if (!previous) for (const file of await fs18.readdir(home)) {
|
|
4863
5060
|
if (file === ".install-lock") continue;
|
|
4864
|
-
const directory =
|
|
4865
|
-
const stat = await
|
|
4866
|
-
if (!["versions", "bin"].includes(file) || !stat.isDirectory() || (await
|
|
5061
|
+
const directory = path23.join(home, file);
|
|
5062
|
+
const stat = await fs18.lstat(directory);
|
|
5063
|
+
if (!["versions", "bin"].includes(file) || !stat.isDirectory() || (await fs18.readdir(directory)).length) {
|
|
4867
5064
|
throw new Error("Mason installation directory is not empty and has no ownership receipt.");
|
|
4868
5065
|
}
|
|
4869
5066
|
}
|
|
4870
|
-
await
|
|
4871
|
-
const bin = await
|
|
5067
|
+
await fs18.mkdir(selectedBin, { recursive: true });
|
|
5068
|
+
const bin = await fs18.realpath(selectedBin);
|
|
4872
5069
|
if (previous && (previous.home !== home || previous.bin !== bin)) throw new Error("Installation paths differ from the existing Mason receipt.");
|
|
4873
5070
|
const id = bundle.manifestHash.slice(0, 24);
|
|
4874
5071
|
const destination = await storePath(home, "versions/" + id);
|
|
@@ -4879,10 +5076,10 @@ $previousToken = $env:MASON_UNINSTALL_TOKEN
|
|
|
4879
5076
|
$token = [Guid]::NewGuid().ToString()
|
|
4880
5077
|
$env:MASON_UNINSTALL_TOKEN = $token
|
|
4881
5078
|
try {
|
|
4882
|
-
& ${psQuote(
|
|
5079
|
+
& ${psQuote(path23.join(destination, "node.exe"))} ${psQuote(path23.join(destination, "app/dist/mason.js"))} @args
|
|
4883
5080
|
$code = $LASTEXITCODE
|
|
4884
5081
|
if ($code -eq 0 -and $args.Count -eq 1 -and $args[0] -eq 'uninstall') {
|
|
4885
|
-
if ([IO.File]::ReadAllText(${psQuote(
|
|
5082
|
+
if ([IO.File]::ReadAllText(${psQuote(path23.join(home, ".uninstall-request.txt"))}) -cne $token) { throw 'Mason did not authorize installation cleanup.' }
|
|
4886
5083
|
Remove-Item -LiteralPath ${psQuote(home)} -Recurse -Force
|
|
4887
5084
|
Write-Output ${psQuote("Removed standalone installation: " + home)}
|
|
4888
5085
|
}
|
|
@@ -4891,11 +5088,11 @@ try {
|
|
|
4891
5088
|
finally { $env:MASON_UNINSTALL_TOKEN = $previousToken }
|
|
4892
5089
|
`
|
|
4893
5090
|
} : { mason: `#!/bin/sh
|
|
4894
|
-
exec ${shQuote(
|
|
5091
|
+
exec ${shQuote(path23.join(destination, "node"))} ${shQuote(path23.join(destination, "app/dist/mason.js"))} "$@"
|
|
4895
5092
|
` };
|
|
4896
5093
|
for (const [name, content2] of Object.entries(launchers)) {
|
|
4897
5094
|
const file = await storePath(bin, name);
|
|
4898
|
-
const old = await
|
|
5095
|
+
const old = await fs18.readFile(file, "utf8").catch((error) => {
|
|
4899
5096
|
if (error.code === "ENOENT") return null;
|
|
4900
5097
|
throw error;
|
|
4901
5098
|
});
|
|
@@ -4904,7 +5101,7 @@ exec ${shQuote(path24.join(destination, "node"))} ${shQuote(path24.join(destinat
|
|
|
4904
5101
|
const retired = Object.entries({ ...previous?.previousLaunchers, ...previous?.launchers }).filter(([name]) => !(name in launchers));
|
|
4905
5102
|
const checkRetired = async (name, expected) => {
|
|
4906
5103
|
if (!launcherNames.includes(name)) throw new Error("Invalid owned launcher name.");
|
|
4907
|
-
const actual = await
|
|
5104
|
+
const actual = await fs18.readFile(await storePath(bin, name), "utf8").catch((error) => {
|
|
4908
5105
|
if (error.code === "ENOENT") return null;
|
|
4909
5106
|
throw error;
|
|
4910
5107
|
});
|
|
@@ -4913,29 +5110,29 @@ exec ${shQuote(path24.join(destination, "node"))} ${shQuote(path24.join(destinat
|
|
|
4913
5110
|
for (const [name, expected] of retired) await checkRetired(name, expected);
|
|
4914
5111
|
const receipt = { format: 1, home, bin, current: id, version: bundle.manifest.version, launchers, pathChanges: previous?.pathChanges ?? [] };
|
|
4915
5112
|
await writeStoreJson(home, "install.json", { ...receipt, previousLaunchers: previous?.previousLaunchers ?? previous?.launchers });
|
|
4916
|
-
const stage = await storePath(home, "versions/.install-" +
|
|
4917
|
-
await
|
|
5113
|
+
const stage = await storePath(home, "versions/.install-" + randomUUID8(), true);
|
|
5114
|
+
await fs18.mkdir(stage);
|
|
4918
5115
|
try {
|
|
4919
5116
|
for (const file of ["bundle.json", ...Object.keys(bundle.manifest.files)]) {
|
|
4920
5117
|
const from = await storePath(source, file), to = await storePath(stage, file, true);
|
|
4921
|
-
await
|
|
4922
|
-
await
|
|
5118
|
+
await fs18.copyFile(from, to);
|
|
5119
|
+
await fs18.chmod(to, (await fs18.stat(from)).mode & 511);
|
|
4923
5120
|
}
|
|
4924
5121
|
await verifyBundle(stage, bundle.manifestHash);
|
|
4925
|
-
const exists2 = await
|
|
5122
|
+
const exists2 = await fs18.lstat(destination).then(() => true, (error) => {
|
|
4926
5123
|
if (error.code === "ENOENT") return false;
|
|
4927
5124
|
throw error;
|
|
4928
5125
|
});
|
|
4929
5126
|
if (exists2) await verifyBundle(destination, bundle.manifestHash);
|
|
4930
|
-
else await
|
|
5127
|
+
else await fs18.rename(stage, destination);
|
|
4931
5128
|
for (const [name, content2] of Object.entries(launchers)) await replace(await storePath(bin, name, true), content2, 493);
|
|
4932
5129
|
for (const [name, expected] of retired) {
|
|
4933
5130
|
await checkRetired(name, expected);
|
|
4934
|
-
await
|
|
5131
|
+
await fs18.rm(await storePath(bin, name), { force: true });
|
|
4935
5132
|
}
|
|
4936
5133
|
await writeStoreJson(home, "install.json", receipt);
|
|
4937
5134
|
} finally {
|
|
4938
|
-
await
|
|
5135
|
+
await fs18.rm(stage, { recursive: true, force: true });
|
|
4939
5136
|
}
|
|
4940
5137
|
const pathResult = await configurePath(bin, receipt.pathChanges, async (changes) => {
|
|
4941
5138
|
receipt.pathChanges = changes;
|
|
@@ -4945,12 +5142,12 @@ exec ${shQuote(path24.join(destination, "node"))} ${shQuote(path24.join(destinat
|
|
|
4945
5142
|
});
|
|
4946
5143
|
}
|
|
4947
5144
|
async function currentInstallation() {
|
|
4948
|
-
const app =
|
|
4949
|
-
const bundle =
|
|
5145
|
+
const app = path23.dirname(path23.dirname(fileURLToPath(import.meta.url)));
|
|
5146
|
+
const bundle = path23.dirname(app), home = path23.dirname(path23.dirname(bundle));
|
|
4950
5147
|
const raw = await readStoreJson(home, "install.json");
|
|
4951
5148
|
if (!raw) throw new Error("Run this command from a standalone installation. npm users can upgrade with npm.");
|
|
4952
5149
|
const record = recordSchema2.parse(raw);
|
|
4953
|
-
if (record.home !== await
|
|
5150
|
+
if (record.home !== await fs18.realpath(home) || path23.dirname(bundle) !== path23.join(home, "versions")) throw new Error("Invalid standalone installation receipt.");
|
|
4954
5151
|
return { home, bundle, record };
|
|
4955
5152
|
}
|
|
4956
5153
|
async function upgradeStandalone(version) {
|
|
@@ -4958,7 +5155,7 @@ async function upgradeStandalone(version) {
|
|
|
4958
5155
|
const { home, bundle, record } = await currentInstallation();
|
|
4959
5156
|
await verifyBundle(bundle);
|
|
4960
5157
|
const windows = process.platform === "win32";
|
|
4961
|
-
const script =
|
|
5158
|
+
const script = path23.join(bundle, windows ? "install.ps1" : "install.sh");
|
|
4962
5159
|
return new Promise((resolve, reject) => {
|
|
4963
5160
|
const child = spawn(
|
|
4964
5161
|
windows ? "powershell.exe" : "sh",
|
|
@@ -4979,7 +5176,7 @@ async function uninstallStandalone() {
|
|
|
4979
5176
|
for (const [name, expected] of Object.entries(record.launchers)) {
|
|
4980
5177
|
if (!launcherNames.includes(name)) throw new Error("Invalid owned launcher name.");
|
|
4981
5178
|
const file = await storePath(record.bin, name);
|
|
4982
|
-
const actual = await
|
|
5179
|
+
const actual = await fs18.readFile(file, "utf8").catch((error) => {
|
|
4983
5180
|
if (error.code === "ENOENT") return null;
|
|
4984
5181
|
throw error;
|
|
4985
5182
|
});
|
|
@@ -4987,9 +5184,9 @@ async function uninstallStandalone() {
|
|
|
4987
5184
|
}
|
|
4988
5185
|
for (const warning of await removePath(record.pathChanges ?? [])) console.error(warning);
|
|
4989
5186
|
if (process.platform === "win32") await replace(await storePath(home, ".uninstall-request.txt", true), token);
|
|
4990
|
-
for (const name of Object.keys(record.launchers)) await
|
|
5187
|
+
for (const name of Object.keys(record.launchers)) await fs18.rm(await storePath(record.bin, name), { force: true });
|
|
4991
5188
|
});
|
|
4992
|
-
if (process.platform !== "win32") await
|
|
5189
|
+
if (process.platform !== "win32") await fs18.rm(home, { recursive: true, force: true });
|
|
4993
5190
|
return home;
|
|
4994
5191
|
}
|
|
4995
5192
|
var recordSchema2, shQuote, psQuote, WINDOWS_BATCH_LAUNCHER, launcherNames, defaultHome;
|
|
@@ -5000,26 +5197,26 @@ var init_install2 = __esm({
|
|
|
5000
5197
|
init_storage();
|
|
5001
5198
|
init_store();
|
|
5002
5199
|
init_path();
|
|
5003
|
-
recordSchema2 =
|
|
5004
|
-
format:
|
|
5005
|
-
home:
|
|
5006
|
-
bin:
|
|
5007
|
-
current:
|
|
5008
|
-
version:
|
|
5009
|
-
launchers:
|
|
5010
|
-
previousLaunchers:
|
|
5011
|
-
pathChanges:
|
|
5200
|
+
recordSchema2 = z20.object({
|
|
5201
|
+
format: z20.literal(1),
|
|
5202
|
+
home: z20.string(),
|
|
5203
|
+
bin: z20.string(),
|
|
5204
|
+
current: z20.string().regex(/^[a-f0-9]{24}$/),
|
|
5205
|
+
version: z20.string(),
|
|
5206
|
+
launchers: z20.record(z20.string()),
|
|
5207
|
+
previousLaunchers: z20.record(z20.string()).optional(),
|
|
5208
|
+
pathChanges: z20.array(pathChangeSchema).optional()
|
|
5012
5209
|
});
|
|
5013
5210
|
shQuote = (s) => "'" + s.replace(/'/g, "'\\''") + "'";
|
|
5014
5211
|
psQuote = (s) => "'" + s.replace(/'/g, "''") + "'";
|
|
5015
5212
|
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';
|
|
5016
5213
|
launcherNames = ["mason", "mason.cmd", "mason.ps1", "mason-launcher.ps1"];
|
|
5017
|
-
defaultHome = () => process.env.MASON_HOME ?? (process.platform === "win32" ?
|
|
5214
|
+
defaultHome = () => process.env.MASON_HOME ?? (process.platform === "win32" ? path23.join(process.env.LOCALAPPDATA ?? os4.homedir(), "Mason") : path23.join(os4.homedir(), ".local/share/mason"));
|
|
5018
5215
|
}
|
|
5019
5216
|
});
|
|
5020
5217
|
|
|
5021
5218
|
// bin/mason.ts
|
|
5022
|
-
import { fileURLToPath as
|
|
5219
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
5023
5220
|
|
|
5024
5221
|
// src/automation/cli.ts
|
|
5025
5222
|
init_execution();
|
|
@@ -5028,9 +5225,23 @@ init_adapters();
|
|
|
5028
5225
|
init_install();
|
|
5029
5226
|
init_store();
|
|
5030
5227
|
import { parseArgs } from "util";
|
|
5031
|
-
var
|
|
5228
|
+
var TEARDOWN_USAGE = `Usage: mason teardown [options]
|
|
5229
|
+
|
|
5230
|
+
Disconnect Mason from this Git project. Defaults to all project hosts.
|
|
5231
|
+
|
|
5232
|
+
--host codex|claude Disconnect only this host
|
|
5233
|
+
--dir <path> Project directory (defaults to cwd; subdirectories work)
|
|
5234
|
+
--dry-run Preview changes without writing
|
|
5235
|
+
--json Machine-readable changes and diagnostics
|
|
5032
5236
|
|
|
5033
|
-
|
|
5237
|
+
Keeps shared knowledge, repair evidence, ignore rules and the Mason installation.
|
|
5238
|
+
Edited or ambiguous entries are retained for manual cleanup. Restart assistants
|
|
5239
|
+
after teardown. Run mason setup --host codex or --host claude to reconnect.
|
|
5240
|
+
Exit 0: complete/no-op. Exit 2: cleanup needs attention (also in dry runs).`;
|
|
5241
|
+
var USAGE = `Usage: mason-auto <setup|teardown|install|config|status|check|hook> [options]
|
|
5242
|
+
|
|
5243
|
+
setup [--host claude|codex] Connect MCP, instructions and hooks to mason on PATH; retain the initial audit
|
|
5244
|
+
teardown [--host claude|codex] Disconnect one host or all project hosts; retain knowledge and evidence
|
|
5034
5245
|
install --host claude|codex Merge lifecycle hooks into this project's host config
|
|
5035
5246
|
config --host claude|codex Print the host config without writing
|
|
5036
5247
|
status Read configured hooks and observed runtime events
|
|
@@ -5039,9 +5250,11 @@ var USAGE = `Usage: mason-auto <setup|install|config|status|check|hook> [options
|
|
|
5039
5250
|
|
|
5040
5251
|
--dir <path> Project directory (defaults to cwd)
|
|
5041
5252
|
--command <prefix> Installed executable prefix for install/config
|
|
5253
|
+
--dry-run Preview teardown without writing
|
|
5042
5254
|
--json Machine-readable output (status also uses JSON when piped)
|
|
5043
5255
|
|
|
5044
5256
|
check exits 0 for verified checks, 1 for issues, 2 for incomplete/unavailable.
|
|
5257
|
+
teardown exits 0 for complete/no-op, 2 when cleanup needs attention (also in dry runs).
|
|
5045
5258
|
Hooks are advisory and exit 0; a failed capture is reported explicitly.
|
|
5046
5259
|
Local evidence is written under .mason/reports/. No LLM calls or source edits.`;
|
|
5047
5260
|
var parseCli = (argv) => parseArgs({ args: argv, allowPositionals: true, options: {
|
|
@@ -5049,7 +5262,8 @@ var parseCli = (argv) => parseArgs({ args: argv, allowPositionals: true, options
|
|
|
5049
5262
|
host: { type: "string" },
|
|
5050
5263
|
command: { type: "string" },
|
|
5051
5264
|
json: { type: "boolean" },
|
|
5052
|
-
help: { type: "boolean", short: "h" }
|
|
5265
|
+
help: { type: "boolean", short: "h" },
|
|
5266
|
+
"dry-run": { type: "boolean" }
|
|
5053
5267
|
} });
|
|
5054
5268
|
function isHookCommand(argv) {
|
|
5055
5269
|
try {
|
|
@@ -5067,12 +5281,20 @@ async function runAutomationCli(argv, stdin = "", io = {
|
|
|
5067
5281
|
try {
|
|
5068
5282
|
const { values, positionals } = parseCli(argv);
|
|
5069
5283
|
if (values.help || !positionals.length) {
|
|
5070
|
-
io.out(USAGE);
|
|
5284
|
+
io.out(positionals[0] === "teardown" ? TEARDOWN_USAGE : USAGE);
|
|
5071
5285
|
return 0;
|
|
5072
5286
|
}
|
|
5073
5287
|
if (positionals.length !== 1) throw new Error("Expected one command.");
|
|
5074
5288
|
[action] = positionals;
|
|
5075
5289
|
const dir = values.dir ?? process.cwd();
|
|
5290
|
+
if (values["dry-run"] !== void 0 && action !== "teardown") throw new Error("--dry-run applies only to teardown.");
|
|
5291
|
+
if (action === "teardown") {
|
|
5292
|
+
if (values.command !== void 0) throw new Error("--command applies only to install/config.");
|
|
5293
|
+
const { teardownProject: teardownProject2, summarizeTeardown: summarizeTeardown2 } = await Promise.resolve().then(() => (init_teardown(), teardown_exports));
|
|
5294
|
+
const result = await teardownProject2(dir, { host: values.host === void 0 ? void 0 : hostSchema.parse(values.host), dryRun: values["dry-run"] });
|
|
5295
|
+
io.out(values.json ? JSON.stringify(result, null, 2) : summarizeTeardown2(result));
|
|
5296
|
+
return result.status === "complete" ? 0 : 2;
|
|
5297
|
+
}
|
|
5076
5298
|
if (action === "hook") {
|
|
5077
5299
|
const output = await runAutomationHook(hostSchema.parse(values.host), stdin);
|
|
5078
5300
|
if (output) io.out(JSON.stringify(output));
|
|
@@ -5103,24 +5325,28 @@ async function runAutomationCli(argv, stdin = "", io = {
|
|
|
5103
5325
|
io.out(values.json ? JSON.stringify(report, null, 2) : summarize(report));
|
|
5104
5326
|
return report.status === "verified" ? 0 : report.status === "issues-remain" ? 1 : 2;
|
|
5105
5327
|
} catch (error) {
|
|
5106
|
-
const message2 = action === "setup" ? "Mason setup incomplete: " + automationFailure(error).message + ". Rerun the same setup command to resume; retained audit evidence is preserved." : failureMessage(error);
|
|
5328
|
+
const message2 = action === "setup" ? "Mason setup incomplete: " + automationFailure(error).message + ". Rerun the same setup command to resume; retained audit evidence is preserved." : action === "teardown" ? "Mason teardown incomplete: " + automationFailure(error).message + ". Inspect the reported input, then rerun teardown; knowledge and repair evidence are retained." : failureMessage(error);
|
|
5107
5329
|
if (action === "hook" || isHookCommand(argv)) {
|
|
5108
5330
|
io.out(JSON.stringify({ systemMessage: message2 }));
|
|
5109
5331
|
return 0;
|
|
5110
5332
|
}
|
|
5111
|
-
if (argv.includes("--json")) io.out(JSON.stringify({ version: 1, ...
|
|
5333
|
+
if (argv.includes("--json")) io.out(JSON.stringify({ version: 1, ...["setup", "teardown"].includes(action) ? { action, status: "incomplete", next: "Inspect the reported input, then rerun the same command to resume." } : { status: "unavailable" }, failure: automationFailure(error) }));
|
|
5112
5334
|
else io.err(message2);
|
|
5113
5335
|
return 2;
|
|
5114
5336
|
}
|
|
5115
5337
|
}
|
|
5116
5338
|
|
|
5117
5339
|
// bin/mason.ts
|
|
5118
|
-
var
|
|
5119
|
-
var
|
|
5340
|
+
var inputArgs = process.argv.slice(2);
|
|
5341
|
+
var managedLaunch = inputArgs[0] === "--setup-host";
|
|
5342
|
+
var setupHost = managedLaunch ? inputArgs.splice(0, 2)[1] : void 0;
|
|
5343
|
+
var [command, ...args] = inputArgs;
|
|
5344
|
+
var usage = `Mason ${"0.16.0"}
|
|
5120
5345
|
|
|
5121
5346
|
Usage: mason <command> [options]
|
|
5122
5347
|
|
|
5123
5348
|
setup --host codex|claude Connect this project to your assistant
|
|
5349
|
+
teardown [--host codex|claude] Disconnect project integrations; retain knowledge
|
|
5124
5350
|
status Show configuration, observed use and verification
|
|
5125
5351
|
check Resume and verify retained documentation findings
|
|
5126
5352
|
audit Audit project instructions
|
|
@@ -5133,26 +5359,30 @@ Usage: mason <command> [options]
|
|
|
5133
5359
|
Use mason <command> --help for options. npm users can keep using mason-auto,
|
|
5134
5360
|
mason-audit, mason-review, mason-drift, mason-hook and mason-mcp.`;
|
|
5135
5361
|
try {
|
|
5136
|
-
if (
|
|
5137
|
-
|
|
5362
|
+
if (managedLaunch) {
|
|
5363
|
+
if (!setupHost || !["codex", "claude"].includes(setupHost) || !["mcp", "auto"].includes(command)) throw new Error("Invalid setup invocation.");
|
|
5364
|
+
const { prepareLaunch: prepareLaunch2 } = await Promise.resolve().then(() => (init_launcher(), launcher_exports));
|
|
5365
|
+
await prepareLaunch2(setupHost);
|
|
5366
|
+
}
|
|
5367
|
+
if (command === "internal-integration-version") console.log(JSON.stringify({ protocol: 1, version: "0.16.0" }));
|
|
5368
|
+
else if (!command || ["--help", "-h", "help"].includes(command)) console.log(usage);
|
|
5369
|
+
else if (["--version", "-v"].includes(command)) console.log("0.16.0");
|
|
5138
5370
|
else if (command === "internal-install") {
|
|
5139
5371
|
if (args.length) throw new Error("internal-install takes no arguments.");
|
|
5140
|
-
const { sourceRuntime: sourceRuntime2 } = await Promise.resolve().then(() => (init_runtime2(), runtime_exports));
|
|
5141
|
-
const selected = await sourceRuntime2();
|
|
5142
|
-
if (!selected.bundleRoot) throw new Error("This command requires a standalone bundle.");
|
|
5143
5372
|
const { installStandalone: installStandalone2 } = await Promise.resolve().then(() => (init_install2(), install_exports2));
|
|
5144
|
-
const
|
|
5373
|
+
const bundleRoot = fileURLToPath2(new URL("../..", import.meta.url));
|
|
5374
|
+
const installed = await installStandalone2(bundleRoot);
|
|
5145
5375
|
console.log(`Installed Mason ${installed.version} in ${installed.home}.
|
|
5146
5376
|
${installed.path.message}`);
|
|
5147
5377
|
} else if (command === "upgrade" || command === "uninstall") {
|
|
5148
|
-
if (args.includes("--help")) console.log(command === "upgrade" ? "Usage: mason upgrade [version].
|
|
5378
|
+
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.");
|
|
5149
5379
|
else {
|
|
5150
5380
|
const { upgradeStandalone: upgradeStandalone2, uninstallStandalone: uninstallStandalone2 } = await Promise.resolve().then(() => (init_install2(), install_exports2));
|
|
5151
5381
|
if (args.length > (command === "upgrade" ? 1 : 0)) throw new Error("Unexpected arguments.");
|
|
5152
5382
|
if (command === "upgrade") process.exitCode = await upgradeStandalone2(args[0]);
|
|
5153
|
-
else console.log((process.platform === "win32" ? "Finishing installation cleanup after runtime exit: " : "Removed standalone installation: ") + await uninstallStandalone2() + ". Project
|
|
5383
|
+
else console.log((process.platform === "win32" ? "Finishing installation cleanup after runtime exit: " : "Removed standalone installation: ") + await uninstallStandalone2() + ". Project configuration and knowledge were retained. Reinstall Mason to use its integrations.");
|
|
5154
5384
|
}
|
|
5155
|
-
} else if (["setup", "status", "check", "auto"].includes(command)) {
|
|
5385
|
+
} else if (["setup", "teardown", "status", "check", "auto"].includes(command)) {
|
|
5156
5386
|
const forwarded = command === "auto" ? args : [command, ...args];
|
|
5157
5387
|
let input = "";
|
|
5158
5388
|
if (isHookCommand(forwarded) && !process.stdin.isTTY) for await (const chunk of process.stdin) {
|
|
@@ -5162,11 +5392,17 @@ ${installed.path.message}`);
|
|
|
5162
5392
|
process.exitCode = await runAutomationCli(forwarded, input);
|
|
5163
5393
|
} else if (["audit", "review", "drift", "hook", "mcp"].includes(command)) {
|
|
5164
5394
|
const binary = new URL(`./mason-${command}.js`, import.meta.url);
|
|
5165
|
-
process.argv = [process.execPath,
|
|
5395
|
+
process.argv = [process.execPath, fileURLToPath2(binary), ...args];
|
|
5166
5396
|
await import(binary.href);
|
|
5167
5397
|
} else throw new Error("Unknown command: " + command + ". Run mason --help.");
|
|
5168
5398
|
} catch (error) {
|
|
5169
|
-
|
|
5170
|
-
|
|
5399
|
+
const message2 = "Mason: " + (error instanceof Error ? error.message : String(error));
|
|
5400
|
+
if (command === "auto" && isHookCommand(args)) {
|
|
5401
|
+
console.log(JSON.stringify({ systemMessage: message2 + " Verification was not established." }));
|
|
5402
|
+
process.exitCode = 0;
|
|
5403
|
+
} else {
|
|
5404
|
+
console.error(message2);
|
|
5405
|
+
process.exitCode = 2;
|
|
5406
|
+
}
|
|
5171
5407
|
}
|
|
5172
5408
|
//# sourceMappingURL=mason.js.map
|