@rosenfan/matrix 0.1.5 → 0.1.7

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/src/cli.js CHANGED
@@ -1,233 +1,303 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- import { spawnSync } from "node:child_process";
4
- import { fileURLToPath } from "node:url";
5
- import { PLATFORMS } from "./catalog.js";
6
- import { createDistribution, installedIntent } from "./distribution.js";
7
- import { createMattAdapter, inspectMattInstallation } from "./matt.js";
8
- import { text } from "./messages.js";
9
- import { createUi } from "./ui.js";
10
- import { invoke } from "./workflow.js";
11
- import { compareVersions, globalBinPath, latestVersion, selfUpdate } from "./self-update.js";
12
- import { resolveProjectRuntime } from "./runtime-resolver.js";
13
- import { MATT_COMPATIBILITY } from "./matt-catalog.mjs";
14
-
15
- const packagePath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "package.json");
16
- const version = JSON.parse(fs.readFileSync(packagePath, "utf8")).version;
17
- const HELP = `Matrix ${version}\n\nUsage: matrix init [directory] [options]\n matrix update [directory] [options]\n matrix doctor [directory] [options]\n\nOptions:\n --language <en|zh-CN>\n --scope <project|global>\n --platform <claude-code|codex> (repeatable)\n --mode <copy|symlink>\n --with-mattpocock | --without-mattpocock\n --force-matt\n --default-orchestration <prim|arch>\n --skip-self-update --force --yes, -y --dry-run --json --no-color`;
18
-
19
- function parse(argv) {
20
- const result = { command: null, directory: null, scope: null, language: null, platforms: [], mode: "copy", matt: null, mattPolicy: "preserve", defaultOrchestration: null, matrixPolicy: "safe", yes: false, dryRun: false, json: false, color: true, skipSelfUpdate: false, reexec: false };
21
- const values = [...argv];
22
- if (values[0] && !values[0].startsWith("-")) result.command = values.shift();
23
- while (values.length) {
24
- const token = values.shift();
25
- if (!token.startsWith("-") && !result.directory) result.directory = token;
26
- else if (token === "--scope") result.scope = values.shift();
27
- else if (token === "--language") result.language = values.shift();
28
- else if (token === "--platform") result.platforms.push(values.shift());
29
- else if (token === "--mode") result.mode = values.shift();
30
- else if (token === "--with-mattpocock") result.matt = "missing";
31
- else if (token === "--without-mattpocock") result.matt = "none";
32
- else if (token === "--force-matt") result.mattPolicy = "replace";
33
- else if (token === "--default-orchestration") result.defaultOrchestration = values.shift();
34
- else if (token === "--force") result.matrixPolicy = "replace";
35
- else if (["--yes", "-y"].includes(token)) result.yes = true;
36
- else if (token === "--dry-run") result.dryRun = true;
37
- else if (token === "--skip-self-update") result.skipSelfUpdate = true;
38
- else if (token === "--_reexec") result.reexec = true;
39
- else if (token === "--json") result.json = true;
40
- else if (token === "--no-color") result.color = false;
41
- else if (["--help", "-h"].includes(token)) result.help = true;
42
- else if (["--version", "-v"].includes(token)) result.version = true;
43
- else throw new Error(`Unknown option: ${token}`);
44
- }
45
- return result;
46
- }
47
-
48
- function projectRoot(directory) {
49
- const value = path.resolve(directory ?? process.cwd());
50
- if (!fs.existsSync(value) || !fs.statSync(value).isDirectory()) throw new Error(`Not a directory: ${value}`);
51
- return value;
52
- }
53
-
54
- function labelsFor(language) { return { yes: text(language, "yes"), no: text(language, "no") }; }
55
-
56
- function localizedState(language, state) {
57
- return text(language, ({ missing: "stateMissing", matching: "stateMatching", complete: "stateComplete", "user-modified": "stateUserModified", partial: "statePartial", damaged: "statePartial", newer: "stateMatching", "outdated-unchanged": "stateOutdated", "safe-update": "stateSafeUpdate", adoptable: "stateAdoptable", unreadable: "stateUnreadable", compatible: "stateCompatible", "update-required": "stateUpdateRequired", "unsupported-newer": "stateUnsupportedNewer", unverified: "stateUnverified", unknown: "stateUnknown", incomplete: "statePartial" })[state] ?? state);
58
- }
59
-
60
- function localizedAction(language, action) {
61
- return text(language, ({ install: "actionInstall", keep: "actionKeep", "backup-replace": "actionBackupReplace", repair: "actionRepair", blocked: "actionBlocked" })[action] ?? action);
62
- }
63
-
64
- async function intentFrom(options, ui) {
65
- const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY && !options.json);
66
- const language = options.language ?? (options.yes ? "en" : await ui.select("Language / 语言", [{ label: "English", value: "en" }, { label: "中文", value: "zh-CN" }]));
67
- const scope = options.scope ?? (options.yes ? "project" : await ui.select(text(language, "scopeQuestion"), [{ label: text(language, "project"), value: "project" }, { label: text(language, "global"), value: "global" }]));
68
- let platforms = options.platforms;
69
- if (!platforms.length && interactive && !options.yes) platforms = await ui.selectMany(text(language, "platformsQuestion"), [{ label: "Claude Code", value: "claude-code" }, { label: "Codex", value: "codex" }]);
70
- const matt = options.matt ?? (interactive && !options.yes ? (await ui.confirm(text(language, "mattQuestion"), true, labelsFor(language)) ? "missing" : "none") : "missing");
71
- const defaultOrchestration = options.defaultOrchestration ?? (interactive && !options.yes && matt === "missing"
72
- ? await ui.select(text(language, "orchestrationQuestion"), [{ label: "Arch", value: "arch" }, { label: "Prim", value: "prim" }])
73
- : null);
74
- return { projectRoot: projectRoot(options.directory), language, scope, platforms, mode: options.mode, matt, mattPolicy: options.mattPolicy, defaultOrchestration, nonInteractive: options.yes || !interactive, matrixPolicy: options.matrixPolicy, dryRun: options.dryRun };
75
- }
76
-
77
- function render(ui, evaluation) {
78
- const language = evaluation.intent?.language ?? "en";
79
- const actions = (evaluation.actions ?? []).map((item) => `${item.platform}: ${localizedAction(language, item.matrix)}`).join(", ");
80
- ui.info(actions ? `${text(language, "preview")}: ${actions}` : evaluation.code);
81
- if (evaluation.intent?.defaultOrchestration) ui.muted(`${text(language, "defaultOrchestration")}: ${evaluation.intent.defaultOrchestration}`);
82
- for (const item of evaluation.observations ?? []) {
83
- const mattStates = Object.entries(item.matt.classificationCounts ?? {}).map(([state, count]) => `${localizedState(language, state)}=${count}`).join(", ");
84
- ui.muted(`${item.name}: ${text(language, "matrix")} ${localizedState(language, item.matrix)};${text(language, "matt")} ${localizedState(language, item.matt.state)}${mattStates ? ` (${mattStates})` : ""}`);
85
- }
86
- for (const item of evaluation.diagnostics ?? []) ui.warn(item.code === "USER_MODIFIED_BLOCKED" ? text(language, "replaceQuestion") : item.message ?? item.code);
87
- for (const item of evaluation.diagnosis ?? []) (item.code === "MATT_GLOBAL_IGNORED" ? ui.muted : ui.warn)(item.message ?? item.code);
88
- }
89
-
90
- async function init(options) {
91
- const ui = createUi({ color: options.color && !options.json });
92
- if (!options.json) await ui.banner();
93
- const intent = await intentFrom(options, ui);
94
- return refresh(intent, options, ui);
95
- }
96
-
97
- async function refresh(intent, options, ui) {
98
- const distribution = createDistribution({ mattAdapter: createMattAdapter({ stdio: options.json ? "pipe" : "inherit" }) });
99
- let evaluation = distribution.evaluate(intent);
100
- const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY && !options.json);
101
- let reevaluated = false;
102
- if (!options.json) render(ui, evaluation);
103
- if (!evaluation.ok && evaluation.code === "USER_MODIFIED_BLOCKED" && interactive && !options.yes && await ui.confirm(text(intent.language, "replaceQuestion"), false, labelsFor(intent.language))) {
104
- evaluation = distribution.evaluate({ ...intent, matrixPolicy: "replace" }); reevaluated = true;
105
- }
106
- if (!evaluation.ok && evaluation.code === "MATT_USER_MODIFIED" && interactive && !options.yes
107
- && await ui.confirm(text(intent.language, "mattReplaceQuestion"), false, labelsFor(intent.language))) {
108
- evaluation = distribution.evaluate({ ...intent, mattPolicy: "replace" }); reevaluated = true;
109
- }
110
- if (options.json) {
111
- if (!evaluation.ok || options.dryRun) {
112
- const output = options.updateMode ? { ...evaluation, matt: inspectMattInstallation({ projectRoot: intent.projectRoot, home: intent.home, platforms: intent.platforms }) } : evaluation;
113
- console.log(JSON.stringify(output)); return evaluation.ok ? 0 : 2;
114
- }
115
- const result = distribution.commit(evaluation.plan);
116
- const output = { ...evaluation, ...result, intent: evaluation.intent, observations: evaluation.observations, actions: evaluation.actions,
117
- ...(options.updateMode ? { matt: inspectMattInstallation({ projectRoot: intent.projectRoot, home: intent.home, platforms: intent.platforms }) } : {}) };
118
- console.log(JSON.stringify(output));
119
- return !result.ok ? 2 : result.code === "PARTIAL" ? 1 : 0;
120
- }
121
- if (reevaluated) render(ui, evaluation);
122
- if (!evaluation.ok) return 2;
123
- if (!options.yes && interactive && !options.dryRun && !await ui.confirm(text(intent.language, "confirm"), true, labelsFor(intent.language))) return 0;
124
- const result = distribution.commit(evaluation.plan);
125
- if (!result.ok) { ui.warn(result.error ?? result.code); return 2; }
126
- if (result.code === "PARTIAL") { ui.warn(result.recovery); return 1; }
127
- ui.success(options.dryRun ? text(intent.language, "dryRunComplete") : text(intent.language, "ready"));
128
- if (!options.updateMode && intent.matt === "missing" && !options.dryRun) ui.muted(text(intent.language, "mattSetupHint").replace("{release}", MATT_COMPATIBILITY.release));
129
- if (options.updateMode) {
130
- const matt = inspectMattInstallation({ projectRoot: intent.projectRoot, home: intent.home, platforms: intent.platforms });
131
- ui.muted(`${text(intent.language, "mattCompatibilityStatus").replace("{release}", matt.supported.release).replace("{status}", localizedState(intent.language, matt.status))}${matt.action ? `; ${matt.action}` : ""}`);
132
- }
133
- ui.muted(evaluation.intent.platforms.map((id) => `${PLATFORMS[id].name}: $matrix <request>`).join("\n"));
134
- return 0;
135
- }
136
-
137
- async function update(options) {
138
- const ui = createUi({ color: options.color && !options.json });
139
- const root = projectRoot(options.directory);
140
- const installed = installedIntent({ projectRoot: root });
141
- if (!installed.ok) {
142
- const result = { ok: false, code: installed.code, message: "No trusted Matrix installation manifest was found. Run matrix init first." };
143
- if (options.json) console.log(JSON.stringify(result)); else ui.warn(result.message);
144
- return 2;
145
- }
146
- const forbidden = options.language || options.scope || options.platforms.length || options.matt || options.mattPolicy === "replace" || options.defaultOrchestration || options.mode !== "copy";
147
- if (forbidden) throw new Error("matrix update inherits installation settings. Use matrix init to change language, scope, platform, mode, Matt skills, or orchestration.");
148
- const intent = { ...installed.intent, dryRun: options.dryRun };
149
- let npm = { status: "skipped", current: version, target: version, reason: options.skipSelfUpdate ? "disabled by --skip-self-update" : null };
150
- if (!options.skipSelfUpdate && !options.reexec) {
151
- try {
152
- const target = await latestVersion(); const comparison = compareVersions(target, version);
153
- if (comparison === null) throw new Error("unable to compare registry version");
154
- npm = comparison > 0 ? { status: "available", current: version, target } : { status: "skipped", current: version, target, reason: comparison === 0 ? "already latest" : "registry version is older" };
155
- } catch (error) {
156
- const result = { ok: false, code: "SELF_UPDATE_CHECK_FAILED", npm: { status: "failed", current: version, reason: error.message }, recovery_command: "matrix update --skip-self-update" };
157
- if (options.json) console.log(JSON.stringify(result)); else ui.warn(`${result.code}: ${error.message}\nRun ${result.recovery_command} to refresh assets without npm.`);
158
- return 2;
159
- }
160
- }
161
- if (options.json && options.dryRun) { console.log(JSON.stringify({ ok: true, code: "DRY_RUN", npm, intent, matt: inspectMattInstallation({ projectRoot: root, home: intent.home, platforms: intent.platforms }) })); return 0; }
162
- if (!options.json) {
163
- await ui.banner(); ui.info(`Matrix update: CLI ${npm.current}${npm.status === "available" ? ` → ${npm.target}` : ""}`);
164
- ui.muted(`Assets: ${intent.scope}, ${intent.language}, ${intent.platforms.join(", ")}`);
165
- }
166
- const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY && !options.json);
167
- if (npm.status === "available") {
168
- if (!options.yes && interactive && !await ui.confirm(`Upgrade Matrix CLI ${npm.current} → ${npm.target} and then refresh these assets?`, true, labelsFor(intent.language))) return 0;
169
- if (options.dryRun) { if (options.json) console.log(JSON.stringify({ ok: true, code: "DRY_RUN", npm, intent, matt: inspectMattInstallation({ projectRoot: root, home: intent.home, platforms: intent.platforms }) })); else ui.success(text(intent.language, "dryRunComplete")); return 0; }
170
- const updated = selfUpdate(version, npm.target);
171
- if (!updated.ok) { const result = { ok: false, code: "SELF_UPDATE_FAILED", npm: { ...npm, status: "failed", reason: updated.reason }, recovery_command: "matrix update --skip-self-update" }; if (options.json) console.log(JSON.stringify(result)); else ui.warn(`${result.code}: ${updated.reason}`); return 2; }
172
- const args = [globalBinPath(), "update", root, "--skip-self-update", "--_reexec", "--yes"];
173
- if (options.matrixPolicy === "replace") args.push("--force"); if (options.json) args.push("--json"); if (!options.color) args.push("--no-color");
174
- const rerun = spawnSync(process.execPath, args, { stdio: "inherit" });
175
- return Number.isInteger(rerun.status) ? rerun.status : 2;
176
- }
177
- if (options.json) return refresh(intent, { ...options, yes: true, updateMode: true }, ui);
178
- return refresh(intent, { ...options, updateMode: true }, ui);
179
- }
180
-
181
- function recoveryArgument(value) {
182
- const argument = String(value);
183
- if (/^[A-Za-z0-9_./:@=-]+$/.test(argument)) return argument;
184
- if (process.platform === "win32") return `"${argument.replaceAll('"', '\\"')}"`;
185
- return `'${argument.replaceAll("'", "'\\''")}'`;
186
- }
187
-
188
- function runtimeRecovery(projectRuntime, argv) {
189
- const argumentsWithoutJson = argv.slice(1).filter((value) => value !== "--json");
190
- return { ...projectRuntime, recovery_command: [projectRuntime.recovery_command, ...argumentsWithoutJson.map(recoveryArgument)].join(" ") };
191
- }
192
-
193
- async function doctor(options) {
194
- const ui = createUi({ color: options.color && !options.json });
195
- const root = projectRoot(options.directory);
196
- const installed = installedIntent({ projectRoot: root });
197
- const intent = installed.ok
198
- ? { ...installed.intent, dryRun: true }
199
- : await intentFrom({ ...options, directory: root, yes: true }, ui);
200
- const evaluation = createDistribution().diagnose(intent);
201
- const matt = inspectMattInstallation({ projectRoot: intent.projectRoot, home: intent.home, platforms: intent.platforms });
202
- if (options.json) console.log(JSON.stringify({ ...evaluation, matt }));
203
- else { await ui.banner(); ui.info(text(intent.language, "doctor")); render(ui, evaluation); ui.muted(`${text(intent.language, "mattCompatibilityStatus").replace("{release}", matt.supported.release).replace("{status}", localizedState(intent.language, matt.status))}${matt.action ? `; ${matt.action}` : ""}`); }
204
- return evaluation.ok ? 0 : 2;
205
- }
206
-
207
- export async function main(argv) {
208
- if (argv[0] === "workflow") {
209
- const json = argv.includes("--json");
210
- const projectRuntime = resolveProjectRuntime({ projectRoot: process.cwd() });
211
- if (!projectRuntime.ok) {
212
- const failure = projectRuntime.recovery_command ? runtimeRecovery(projectRuntime, argv) : projectRuntime;
213
- if (json) console.log(JSON.stringify(failure));
214
- else console.log(`MATRIX ERROR [${failure.code}]: ${failure.message}${failure.recovery_command ? `\nRun ${failure.recovery_command}` : ""}`);
215
- return 2;
216
- }
217
- if (projectRuntime.runtime) {
218
- const delegated = spawnSync(process.execPath, [projectRuntime.runtime, ...argv.slice(1).filter((value) => value !== "--json")], { stdio: "inherit" });
219
- return Number.isInteger(delegated.status) ? delegated.status : 2;
220
- }
221
- const result = invoke(argv.slice(1).filter((value) => value !== "--json"));
222
- if (json) console.log(JSON.stringify(result));
223
- else console.log(result.ok ? JSON.stringify(result, null, 2) : `MATRIX ERROR [${result.code}]: ${result.message}`);
224
- return result.ok ? 0 : 2;
225
- }
226
- const options = parse(argv);
227
- if (options.version) { console.log(version); return 0; }
228
- if (options.help || !options.command) { console.log(HELP); return 0; }
229
- if (options.command === "init") return init(options);
230
- if (options.command === "update") return update(options);
231
- if (options.command === "doctor") return doctor(options);
232
- throw new Error(`Unknown command: ${options.command}. Run matrix --help.`);
233
- }
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { spawnSync } from "node:child_process";
4
+ import { fileURLToPath } from "node:url";
5
+ import { PLATFORMS, PLATFORM_IDS } from "./catalog.js";
6
+ import { createDistribution, installedIntent } from "./distribution.js";
7
+ import { createMattAdapter, inspectMattInstallation } from "./matt.js";
8
+ import { text } from "./messages.js";
9
+ import { createUi } from "./ui.js";
10
+ import { invoke } from "./workflow.js";
11
+ import { compareVersions, globalBinPath, latestVersion, PACKAGE_NAME, selfUpdate } from "./self-update.js";
12
+ import { resolveProjectRuntime } from "./runtime-resolver.js";
13
+ import { readProjectsIndex, registerProject, pruneProject } from "./project-index.js";
14
+ import { MATT_COMPATIBILITY } from "./matt-catalog.mjs";
15
+
16
+ const packagePath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "package.json");
17
+ const version = JSON.parse(fs.readFileSync(packagePath, "utf8")).version;
18
+ const HELP = `Matrix ${version}\n\nUsage: matrix init [directory] [options]\n matrix update [directory] [options]\n matrix doctor [directory] [options]\n\nOptions:\n --language <en|zh-CN>\n --scope <project|global>\n --platform <${PLATFORM_IDS.join("|")}> (repeatable)\n --mode <copy|symlink>\n --with-mattpocock | --without-mattpocock\n --force-matt (with --with-mattpocock; replace modified Matt Skills)\n --default-orchestration <prim|arch>\n update only: --all | --project-only (update scope across indexed projects)\n --skip-self-update --force --yes, -y --dry-run --json --no-color`;
19
+
20
+ function parse(argv) {
21
+ const result = { command: null, directory: null, scope: null, language: null, platforms: [], mode: "copy", matt: null, mattPolicy: "preserve", defaultOrchestration: null, matrixPolicy: "safe", yes: false, dryRun: false, json: false, color: true, skipSelfUpdate: false, reexec: false, all: false, projectOnly: false };
22
+ const values = [...argv];
23
+ if (values[0] && !values[0].startsWith("-")) result.command = values.shift();
24
+ while (values.length) {
25
+ const token = values.shift();
26
+ if (!token.startsWith("-") && !result.directory) result.directory = token;
27
+ else if (token === "--scope") result.scope = values.shift();
28
+ else if (token === "--language") result.language = values.shift();
29
+ else if (token === "--platform") result.platforms.push(values.shift());
30
+ else if (token === "--mode") result.mode = values.shift();
31
+ else if (token === "--with-mattpocock") result.matt = "missing";
32
+ else if (token === "--without-mattpocock") result.matt = "none";
33
+ else if (token === "--force-matt") result.mattPolicy = "replace";
34
+ else if (token === "--default-orchestration") result.defaultOrchestration = values.shift();
35
+ else if (token === "--force") result.matrixPolicy = "replace";
36
+ else if (token === "--all") result.all = true;
37
+ else if (token === "--project-only") result.projectOnly = true;
38
+ else if (["--yes", "-y"].includes(token)) result.yes = true;
39
+ else if (token === "--dry-run") result.dryRun = true;
40
+ else if (token === "--skip-self-update") result.skipSelfUpdate = true;
41
+ else if (token === "--_reexec") result.reexec = true;
42
+ else if (token === "--json") result.json = true;
43
+ else if (token === "--no-color") result.color = false;
44
+ else if (["--help", "-h"].includes(token)) result.help = true;
45
+ else if (["--version", "-v"].includes(token)) result.version = true;
46
+ else throw new Error(`Unknown option: ${token}`);
47
+ }
48
+ return result;
49
+ }
50
+
51
+ function projectRoot(directory) {
52
+ const value = path.resolve(directory ?? process.cwd());
53
+ if (!fs.existsSync(value) || !fs.statSync(value).isDirectory()) throw new Error(`Not a directory: ${value}`);
54
+ return value;
55
+ }
56
+
57
+ function labelsFor(language) { return { yes: text(language, "yes"), no: text(language, "no") }; }
58
+
59
+ function localizedState(language, state) {
60
+ return text(language, ({ missing: "stateMissing", matching: "stateMatching", complete: "stateComplete", "user-modified": "stateUserModified", partial: "statePartial", damaged: "statePartial", newer: "stateMatching", "outdated-unchanged": "stateOutdated", "safe-update": "stateSafeUpdate", adoptable: "stateAdoptable", unreadable: "stateUnreadable", compatible: "stateCompatible", "update-required": "stateUpdateRequired", "unsupported-newer": "stateUnsupportedNewer", unverified: "stateUnverified", unknown: "stateUnknown", incomplete: "statePartial" })[state] ?? state);
61
+ }
62
+
63
+ function localizedAction(language, action) {
64
+ return text(language, ({ install: "actionInstall", keep: "actionKeep", "backup-replace": "actionBackupReplace", repair: "actionRepair", blocked: "actionBlocked" })[action] ?? action);
65
+ }
66
+
67
+ async function intentFrom(options, ui) {
68
+ const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY && !options.json);
69
+ const language = options.language ?? (options.yes ? "en" : await ui.select("Language / 语言", [{ label: "English", value: "en" }, { label: "中文", value: "zh-CN" }]));
70
+ const scope = options.scope ?? (options.yes ? "project" : await ui.select(text(language, "scopeQuestion"), [{ label: text(language, "project"), value: "project" }, { label: text(language, "global"), value: "global" }]));
71
+ let platforms = options.platforms;
72
+ if (!platforms.length && interactive && !options.yes) platforms = await ui.selectMany(text(language, "platformsQuestion"), PLATFORM_IDS.map((id) => ({ label: PLATFORMS[id].name, value: id })));
73
+ const matt = options.matt ?? (interactive && !options.yes ? (await ui.confirm(text(language, "mattQuestion"), true, labelsFor(language)) ? "missing" : "none") : "missing");
74
+ const defaultOrchestration = options.defaultOrchestration ?? (interactive && !options.yes && matt === "missing"
75
+ ? await ui.select(text(language, "orchestrationQuestion"), [
76
+ { label: "Arch", value: "arch", detail: text(language, "orchestrationArchDetail") },
77
+ { label: "Prim", value: "prim", detail: text(language, "orchestrationPrimDetail") }
78
+ ])
79
+ : null);
80
+ return { projectRoot: projectRoot(options.directory), language, scope, platforms, mode: options.mode, matt, mattPolicy: options.mattPolicy, defaultOrchestration, nonInteractive: options.yes || !interactive, matrixPolicy: options.matrixPolicy, dryRun: options.dryRun };
81
+ }
82
+
83
+ function render(ui, evaluation) {
84
+ const language = evaluation.intent?.language ?? "en";
85
+ const actions = (evaluation.actions ?? []).map((item) => `${item.platform}: ${localizedAction(language, item.matrix)}`).join(", ");
86
+ ui.info(actions ? `${text(language, "preview")}: ${actions}` : evaluation.code);
87
+ if (evaluation.intent?.defaultOrchestration) ui.muted(`${text(language, "defaultOrchestration")}: ${evaluation.intent.defaultOrchestration}`);
88
+ for (const item of evaluation.observations ?? []) {
89
+ const mattStates = Object.entries(item.matt.classificationCounts ?? {}).map(([state, count]) => `${localizedState(language, state)}=${count}`).join(", ");
90
+ ui.muted(`${item.name}: ${text(language, "matrix")} ${localizedState(language, item.matrix)};${text(language, "matt")} ${localizedState(language, item.matt.state)}${mattStates ? ` (${mattStates})` : ""}`);
91
+ }
92
+ for (const item of evaluation.diagnostics ?? []) ui.warn(item.code === "USER_MODIFIED_BLOCKED" ? text(language, "replaceQuestion") : item.message ?? item.code);
93
+ for (const item of evaluation.diagnosis ?? []) (item.code === "MATT_GLOBAL_IGNORED" ? ui.muted : ui.warn)(item.message ?? item.code);
94
+ }
95
+
96
+ async function init(options) {
97
+ const ui = createUi({ color: options.color && !options.json });
98
+ if (!options.json) await ui.banner();
99
+ const intent = await intentFrom(options, ui);
100
+ return refresh(intent, options, ui);
101
+ }
102
+
103
+ async function refresh(intent, options, ui) {
104
+ const distribution = createDistribution({ mattAdapter: createMattAdapter({ stdio: options.json ? "pipe" : "inherit" }) });
105
+ let evaluation = distribution.evaluate(intent);
106
+ const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY && !options.json);
107
+ let reevaluated = false;
108
+ if (!options.json) render(ui, evaluation);
109
+ if (!evaluation.ok && evaluation.code === "USER_MODIFIED_BLOCKED" && interactive && !options.yes && await ui.confirm(text(intent.language, "replaceQuestion"), false, labelsFor(intent.language))) {
110
+ evaluation = distribution.evaluate({ ...intent, matrixPolicy: "replace" }); reevaluated = true;
111
+ }
112
+ if (!evaluation.ok && evaluation.code === "MATT_USER_MODIFIED" && interactive && !options.yes
113
+ && await ui.confirm(text(intent.language, "mattReplaceQuestion"), false, labelsFor(intent.language))) {
114
+ evaluation = distribution.evaluate({ ...intent, mattPolicy: "replace" }); reevaluated = true;
115
+ }
116
+ if (options.json) {
117
+ if (!evaluation.ok || options.dryRun) {
118
+ const output = options.updateMode ? { ...evaluation, matt: inspectMattInstallation({ projectRoot: intent.projectRoot, home: intent.home, platforms: intent.platforms, action: "matrix update --with-mattpocock" }) } : evaluation;
119
+ console.log(JSON.stringify(output)); return evaluation.ok ? 0 : 2;
120
+ }
121
+ const result = distribution.commit(evaluation.plan);
122
+ const output = { ...evaluation, ...result, intent: evaluation.intent, observations: evaluation.observations, actions: evaluation.actions,
123
+ ...(options.updateMode ? { matt: inspectMattInstallation({ projectRoot: intent.projectRoot, home: intent.home, platforms: intent.platforms, action: "matrix update --with-mattpocock" }) } : {}) };
124
+ console.log(JSON.stringify(output));
125
+ return !result.ok ? 2 : result.code === "PARTIAL" ? 1 : 0;
126
+ }
127
+ if (reevaluated) render(ui, evaluation);
128
+ if (!evaluation.ok) return 2;
129
+ if (!options.yes && interactive && !options.dryRun && !await ui.confirm(text(intent.language, "confirm"), true, labelsFor(intent.language))) return 0;
130
+ const result = distribution.commit(evaluation.plan);
131
+ if (!result.ok) { ui.warn(result.error ?? result.code); return 2; }
132
+ if (intent.scope === "project" && !options.dryRun) {
133
+ try { registerProject({ projectRoot: intent.projectRoot, scope: intent.scope, platforms: intent.platforms, language: intent.language, home: intent.home }); }
134
+ catch { /* a broken project index must never fail an update */ }
135
+ }
136
+ if (result.code === "PARTIAL") { ui.warn(result.recovery); return 1; }
137
+ ui.success(options.dryRun ? text(intent.language, "dryRunComplete") : text(intent.language, "ready"));
138
+ if (!options.updateMode && intent.matt === "missing" && !options.dryRun) ui.muted(text(intent.language, "mattSetupHint").replace("{release}", MATT_COMPATIBILITY.release));
139
+ if (options.updateMode) {
140
+ const matt = inspectMattInstallation({ projectRoot: intent.projectRoot, home: intent.home, platforms: intent.platforms, action: "matrix update --with-mattpocock" });
141
+ ui.muted(`${text(intent.language, "mattCompatibilityStatus").replace("{release}", matt.supported.release).replace("{status}", localizedState(intent.language, matt.status))}${matt.action ? `; ${matt.action}` : ""}`);
142
+ }
143
+ ui.muted(text(intent.language, "readyHint").replace("{platform}", evaluation.intent.platforms.map((id) => PLATFORMS[id].name).join(", ")));
144
+ ui.muted(evaluation.intent.platforms.map((id) => `${PLATFORMS[id].name}: $matrix <request>`).join("\n"));
145
+ return 0;
146
+ }
147
+
148
+ function updateAllProjects({ intent, options, ui, root }) {
149
+ const index = readProjectsIndex({ home: intent.home });
150
+ if (!index.ok) ui.warn("Project index is unreadable; falling back to the current project only.");
151
+ const targets = [...new Set([...index.projects.map((entry) => entry.path), root])];
152
+ const bin = path.join(path.dirname(packagePath), "bin", "matrix.js");
153
+ const forwarded = ["--skip-self-update", "--yes", "--json"];
154
+ if (options.matt === "missing") forwarded.push("--with-mattpocock");
155
+ if (options.mattPolicy === "replace" && options.matt === "missing") forwarded.push("--force-matt");
156
+ let failed = 0;
157
+ const pruned = [];
158
+ for (const target of targets) {
159
+ if (!fs.existsSync(target)) {
160
+ pruneProject({ projectRoot: target, home: intent.home });
161
+ pruned.push(target);
162
+ continue;
163
+ }
164
+ const run = spawnSync(process.execPath, [bin, "update", target, ...forwarded], { encoding: "utf8" });
165
+ let ok = run.status === 0;
166
+ let detail = run.stderr?.trim();
167
+ if (ok) {
168
+ try {
169
+ const output = JSON.parse(run.stdout.trim().split("\n").pop());
170
+ ok = output.ok !== false && output.code !== "PARTIAL";
171
+ detail = output.code;
172
+ } catch { ok = false; detail = detail || "unparseable update output"; }
173
+ }
174
+ if (!ok) failed += 1;
175
+ (ok ? ui.muted : ui.warn)(`${ok ? "OK" : "FAILED"} ${target}${detail ? ` (${detail})` : ""}`);
176
+ }
177
+ for (const item of pruned) ui.muted(`Pruned missing project from the index: ${item}`);
178
+ if (failed) { ui.warn(`${failed} project update(s) failed.`); return 2; }
179
+ ui.success("All indexed projects are up to date.");
180
+ return 0;
181
+ }
182
+
183
+ async function update(options) {
184
+ const ui = createUi({ color: options.color && !options.json });
185
+ const root = projectRoot(options.directory);
186
+ const installed = installedIntent({ projectRoot: root });
187
+ if (!installed.ok) {
188
+ const result = { ok: false, code: installed.code, message: "No trusted Matrix installation manifest was found. Run matrix init first." };
189
+ if (options.json) console.log(JSON.stringify(result)); else ui.warn(result.message);
190
+ return 2;
191
+ }
192
+ if (options.matt === "none") throw new Error("matrix update cannot remove Matt Skills. Use matrix init --without-mattpocock.");
193
+ if (options.mattPolicy === "replace" && options.matt !== "missing") throw new Error("matrix update --force-matt requires --with-mattpocock.");
194
+ const forbidden = options.language || options.scope || options.platforms.length || options.defaultOrchestration || options.mode !== "copy";
195
+ if (forbidden) throw new Error("matrix update inherits installation settings. Use matrix init to change language, scope, platform, mode, or orchestration.");
196
+ const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY && !options.json);
197
+ const intent = { ...installed.intent, dryRun: options.dryRun };
198
+ let npm = { status: "skipped", current: version, target: version, reason: options.skipSelfUpdate ? "disabled by --skip-self-update" : null };
199
+ if (!options.skipSelfUpdate && !options.reexec) {
200
+ try {
201
+ const target = await latestVersion(); const comparison = compareVersions(target, version);
202
+ if (comparison === null) throw new Error("unable to compare registry version");
203
+ npm = comparison > 0 ? { status: "available", current: version, target } : { status: "skipped", current: version, target, reason: comparison === 0 ? "already latest" : "registry version is older" };
204
+ } catch (error) {
205
+ const result = { ok: false, code: "SELF_UPDATE_CHECK_FAILED", npm: { status: "failed", current: version, reason: error.message }, recovery_command: "matrix update --skip-self-update",
206
+ recovery: `Run matrix update --skip-self-update to refresh assets without npm, or install the latest CLI manually with \`npm i -g ${PACKAGE_NAME}@latest\` and retry.` };
207
+ if (options.json) console.log(JSON.stringify(result)); else ui.warn(`${result.code}: ${error.message}\n${result.recovery}`);
208
+ return 2;
209
+ }
210
+ }
211
+ const mattStatus = inspectMattInstallation({ projectRoot: root, home: intent.home, platforms: intent.platforms, action: "matrix update --with-mattpocock" });
212
+ const mattNeedsRepair = ["incomplete", "update-required", "unverified"].includes(mattStatus.status);
213
+ const mattRepair = options.matt === "missing" || (mattNeedsRepair && interactive && !options.yes
214
+ && await ui.confirm(text(intent.language, "mattRepairQuestion").replace("{state}", localizedState(intent.language, mattStatus.status)).replace("{release}", MATT_COMPATIBILITY.release), true, labelsFor(intent.language)));
215
+ if (mattRepair) intent.matt = "missing";
216
+ if (options.json && options.dryRun) { console.log(JSON.stringify({ ok: true, code: "DRY_RUN", npm, intent, matt: mattStatus })); return 0; }
217
+ const scope = options.all ? "all" : options.projectOnly ? "project"
218
+ : interactive && !options.yes ? await ui.select(text(intent.language, "updateScopeQuestion"), [
219
+ { label: text(intent.language, "updateScopeCurrent"), value: "project" },
220
+ { label: text(intent.language, "updateScopeAll"), value: "all" }
221
+ ])
222
+ : "project";
223
+ if (!options.json) {
224
+ await ui.banner(); ui.info(`Matrix update: CLI ${npm.current}${npm.status === "available" ? ` → ${npm.target}` : ""}`);
225
+ ui.muted(`Assets: ${intent.scope}, ${intent.language}, ${intent.platforms.join(", ")}`);
226
+ }
227
+ if (npm.status === "available") {
228
+ const repairNotice = mattRepair ? ` Matt Skills will be repaired to ${MATT_COMPATIBILITY.release} as part of the update.` : "";
229
+ if (!options.yes && interactive && !await ui.confirm(`Upgrade Matrix CLI ${npm.current} → ${npm.target} and then refresh these assets?${repairNotice}`, true, labelsFor(intent.language))) return 0;
230
+ if (options.dryRun) { if (options.json) console.log(JSON.stringify({ ok: true, code: "DRY_RUN", npm, intent, matt: mattStatus })); else ui.success(text(intent.language, "dryRunComplete")); return 0; }
231
+ const updated = selfUpdate(version, npm.target);
232
+ if (!updated.ok) {
233
+ const result = { ok: false, code: "SELF_UPDATE_FAILED", npm: { ...npm, status: "failed", reason: updated.reason }, recovery_command: `npm i -g ${PACKAGE_NAME}@${npm.target} && matrix update --skip-self-update` };
234
+ if (options.json) console.log(JSON.stringify(result)); else ui.warn(`${result.code}: ${updated.reason}\nRun ${result.recovery_command}`);
235
+ return 2;
236
+ }
237
+ const args = [globalBinPath(), "update", root, "--skip-self-update", "--_reexec", "--yes"];
238
+ if (options.matrixPolicy === "replace") args.push("--force");
239
+ if (mattRepair) args.push("--with-mattpocock");
240
+ if (options.mattPolicy === "replace" && mattRepair) args.push("--force-matt");
241
+ if (scope === "all") args.push("--all");
242
+ if (options.json) args.push("--json"); if (!options.color) args.push("--no-color");
243
+ const rerun = spawnSync(process.execPath, args, { stdio: "inherit" });
244
+ return Number.isInteger(rerun.status) ? rerun.status : 2;
245
+ }
246
+ if (scope === "all") return updateAllProjects({ intent, options, ui, root });
247
+ if (options.json) return refresh(intent, { ...options, yes: true, updateMode: true }, ui);
248
+ return refresh(intent, { ...options, updateMode: true }, ui);
249
+ }
250
+
251
+ function recoveryArgument(value) {
252
+ const argument = String(value);
253
+ if (/^[A-Za-z0-9_./:@=-]+$/.test(argument)) return argument;
254
+ if (process.platform === "win32") return `"${argument.replaceAll('"', '\\"')}"`;
255
+ return `'${argument.replaceAll("'", "'\\''")}'`;
256
+ }
257
+
258
+ function runtimeRecovery(projectRuntime, argv) {
259
+ const argumentsWithoutJson = argv.slice(1).filter((value) => value !== "--json");
260
+ return { ...projectRuntime, recovery_command: [projectRuntime.recovery_command, ...argumentsWithoutJson.map(recoveryArgument)].join(" ") };
261
+ }
262
+
263
+ async function doctor(options) {
264
+ const ui = createUi({ color: options.color && !options.json });
265
+ const root = projectRoot(options.directory);
266
+ const installed = installedIntent({ projectRoot: root });
267
+ const intent = installed.ok
268
+ ? { ...installed.intent, dryRun: true }
269
+ : await intentFrom({ ...options, directory: root, yes: true }, ui);
270
+ const evaluation = createDistribution().diagnose(intent);
271
+ const matt = inspectMattInstallation({ projectRoot: intent.projectRoot, home: intent.home, platforms: intent.platforms, action: installed.ok ? "matrix update --with-mattpocock" : undefined });
272
+ if (options.json) console.log(JSON.stringify({ ...evaluation, matt }));
273
+ else { await ui.banner(); ui.info(text(intent.language, "doctor")); render(ui, evaluation); ui.muted(`${text(intent.language, "mattCompatibilityStatus").replace("{release}", matt.supported.release).replace("{status}", localizedState(intent.language, matt.status))}${matt.action ? `; ${matt.action}` : ""}`); }
274
+ return evaluation.ok ? 0 : 2;
275
+ }
276
+
277
+ export async function main(argv) {
278
+ if (argv[0] === "workflow") {
279
+ const json = argv.includes("--json");
280
+ const projectRuntime = resolveProjectRuntime({ projectRoot: process.cwd() });
281
+ if (!projectRuntime.ok) {
282
+ const failure = projectRuntime.recovery_command ? runtimeRecovery(projectRuntime, argv) : projectRuntime;
283
+ if (json) console.log(JSON.stringify(failure));
284
+ else console.log(`MATRIX ERROR [${failure.code}]: ${failure.message}${failure.recovery_command ? `\nRun ${failure.recovery_command}` : ""}`);
285
+ return 2;
286
+ }
287
+ if (projectRuntime.runtime) {
288
+ const delegated = spawnSync(process.execPath, [projectRuntime.runtime, ...argv.slice(1).filter((value) => value !== "--json")], { stdio: "inherit" });
289
+ return Number.isInteger(delegated.status) ? delegated.status : 2;
290
+ }
291
+ const result = invoke(argv.slice(1).filter((value) => value !== "--json"));
292
+ if (json) console.log(JSON.stringify(result));
293
+ else console.log(result.ok ? JSON.stringify(result, null, 2) : `MATRIX ERROR [${result.code}]: ${result.message}`);
294
+ return result.ok ? 0 : 2;
295
+ }
296
+ const options = parse(argv);
297
+ if (options.version) { console.log(version); return 0; }
298
+ if (options.help || !options.command) { console.log(HELP); return 0; }
299
+ if (options.command === "init") return init(options);
300
+ if (options.command === "update") return update(options);
301
+ if (options.command === "doctor") return doctor(options);
302
+ throw new Error(`Unknown command: ${options.command}. Run matrix --help.`);
303
+ }
@@ -200,7 +200,7 @@ export function recoverPendingTransaction(intent, { allowMattWrites = true } = {
200
200
  const file = transactionPath(intent); const journal = readJson(file);
201
201
  if (!journal) return { ok: true, recovered: false };
202
202
  if (!allowMattWrites && ((journal.operations ?? []).some((operation) => operation.kind === "matt") || journal.mattReceipt?.written || journal.mattReceipt?.backup)) {
203
- return { ok: false, code: "RECOVERY_REQUIRED", recovery: `A pending Matt init transaction must be recovered by matrix init --with-mattpocock before Matrix update can continue. Keep ${file}.` };
203
+ return { ok: false, code: "RECOVERY_REQUIRED", recovery: `A pending Matt transaction must be recovered before Matrix update can continue. Run matrix init --with-mattpocock, or matrix update --with-mattpocock on an installed project. Keep ${file}.` };
204
204
  }
205
205
  const restoreMetadata = (entry) => {
206
206
  if (!entry) return;