@rosenfan/matrix 0.1.4 → 0.1.6

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.
Files changed (54) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +41 -40
  3. package/README.zh-CN.md +39 -38
  4. package/{.claude → assets}/skills/matrix/SKILL.md +79 -78
  5. package/{.claude → assets}/skills/matrix/agents/openai.yaml +4 -4
  6. package/{.claude → assets}/skills/matrix/scripts/matrix_claude.py +11 -11
  7. package/{.claude → assets}/skills/matrix/scripts/matrix_state.py +49 -49
  8. package/assets/skills/matrix/scripts/test_matrix_claude.py +69 -0
  9. package/assets/skills/matrix/scripts/test_matrix_state.py +69 -0
  10. package/{.claude → assets}/skills/matrix-archive/SKILL.md +36 -36
  11. package/{.claude/skills/matrix-hotfix → assets/skills/matrix-archive}/agents/openai.yaml +4 -4
  12. package/{.claude → assets}/skills/matrix-build/SKILL.md +41 -41
  13. package/{.claude/skills/matrix-archive → assets/skills/matrix-build}/agents/openai.yaml +4 -4
  14. package/{.claude → assets}/skills/matrix-design/SKILL.md +71 -65
  15. package/{.claude → assets}/skills/matrix-design/agents/openai.yaml +4 -4
  16. package/assets/skills/matrix-handoff/SKILL.md +89 -0
  17. package/{.claude/skills/matrix-claude → assets/skills/matrix-handoff}/agents/openai.yaml +4 -4
  18. package/{.claude → assets}/skills/matrix-hotfix/SKILL.md +8 -8
  19. package/{.claude/skills/matrix-build → assets/skills/matrix-hotfix}/agents/openai.yaml +4 -4
  20. package/{.claude → assets}/skills/matrix-open/SKILL.md +51 -40
  21. package/assets/skills/matrix-open/agents/openai.yaml +4 -0
  22. package/{.claude → assets}/skills/matrix-status/SKILL.md +15 -15
  23. package/{.claude → assets}/skills/matrix-status/agents/openai.yaml +4 -4
  24. package/{.claude → assets}/skills/matrix-tweak/SKILL.md +8 -8
  25. package/assets/skills/matrix-tweak/agents/openai.yaml +4 -0
  26. package/{.claude → assets}/skills/matrix-verify/SKILL.md +62 -62
  27. package/assets/skills/matrix-verify/agents/openai.yaml +4 -0
  28. package/assets/skills-zh-CN/matrix/SKILL.md +72 -71
  29. package/assets/skills-zh-CN/matrix-build/SKILL.md +4 -4
  30. package/assets/skills-zh-CN/matrix-design/SKILL.md +37 -20
  31. package/assets/skills-zh-CN/matrix-handoff/SKILL.md +79 -0
  32. package/assets/skills-zh-CN/matrix-open/SKILL.md +38 -28
  33. package/assets/skills-zh-CN/matrix-verify/SKILL.md +19 -19
  34. package/bin/matrix.js +15 -15
  35. package/package.json +4 -13
  36. package/src/catalog.js +79 -72
  37. package/src/cli.js +211 -141
  38. package/src/distribution.js +182 -179
  39. package/src/installer.js +10 -10
  40. package/src/matt-catalog.mjs +1 -1
  41. package/src/matt.js +9 -14
  42. package/src/messages.js +32 -23
  43. package/src/npm-invocation.js +23 -0
  44. package/src/project-index.js +43 -0
  45. package/src/runtime-resolver.js +76 -76
  46. package/src/self-update.js +88 -81
  47. package/src/ui.js +97 -89
  48. package/src/workflow-transaction.js +13 -13
  49. package/src/workflow.js +734 -705
  50. package/.claude/skills/matrix-claude/SKILL.md +0 -98
  51. package/.claude/skills/matrix-open/agents/openai.yaml +0 -4
  52. package/.claude/skills/matrix-tweak/agents/openai.yaml +0 -4
  53. package/.claude/skills/matrix-verify/agents/openai.yaml +0 -4
  54. package/assets/skills-zh-CN/matrix-claude/SKILL.md +0 -33
package/src/cli.js CHANGED
@@ -1,99 +1,105 @@
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";
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
5
  import { PLATFORMS } from "./catalog.js";
6
6
  import { createDistribution, installedIntent } from "./distribution.js";
7
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";
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
12
  import { resolveProjectRuntime } from "./runtime-resolver.js";
13
+ import { readProjectsIndex, registerProject, pruneProject } from "./project-index.js";
13
14
  import { MATT_COMPATIBILITY } from "./matt-catalog.mjs";
14
-
15
- const packagePath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "package.json");
15
+
16
+ const packagePath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "package.json");
16
17
  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
-
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 <claude-code|codex> (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
+
19
20
  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();
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();
30
31
  else if (token === "--with-mattpocock") result.matt = "missing";
31
32
  else if (token === "--without-mattpocock") result.matt = "none";
32
33
  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
-
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
+
56
59
  function localizedState(language, state) {
57
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);
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);
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"), [{ label: "Claude Code", value: "claude-code" }, { label: "Codex", value: "codex" }]);
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);
74
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 };
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
+ }
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);
81
87
  if (evaluation.intent?.defaultOrchestration) ui.muted(`${text(language, "defaultOrchestration")}: ${evaluation.intent.defaultOrchestration}`);
82
88
  for (const item of evaluation.observations ?? []) {
83
89
  const mattStates = Object.entries(item.matt.classificationCounts ?? {}).map(([state, count]) => `${localizedState(language, state)}=${count}`).join(", ");
84
90
  ui.muted(`${item.name}: ${text(language, "matrix")} ${localizedState(language, item.matrix)};${text(language, "matt")} ${localizedState(language, item.matt.state)}${mattStates ? ` (${mattStates})` : ""}`);
85
91
  }
86
- for (const item of evaluation.diagnostics ?? []) ui.warn(item.code === "USER_MODIFIED_BLOCKED" ? text(language, "replaceQuestion") : item.message ?? item.code);
92
+ for (const item of evaluation.diagnostics ?? []) ui.warn(item.code === "USER_MODIFIED_BLOCKED" ? text(language, "replaceQuestion") : item.message ?? item.code);
87
93
  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
-
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
+
97
103
  async function refresh(intent, options, ui) {
98
104
  const distribution = createDistribution({ mattAdapter: createMattAdapter({ stdio: options.json ? "pipe" : "inherit" }) });
99
105
  let evaluation = distribution.evaluate(intent);
@@ -119,61 +125,125 @@ async function refresh(intent, options, ui) {
119
125
  return !result.ok ? 2 : result.code === "PARTIAL" ? 1 : 0;
120
126
  }
121
127
  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;
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;
124
130
  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; }
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; }
127
137
  ui.success(options.dryRun ? text(intent.language, "dryRunComplete") : text(intent.language, "ready"));
128
138
  if (!options.updateMode && intent.matt === "missing" && !options.dryRun) ui.muted(text(intent.language, "mattSetupHint").replace("{release}", MATT_COMPATIBILITY.release));
129
139
  if (options.updateMode) {
130
- const matt = inspectMattInstallation({ projectRoot: intent.projectRoot, home: intent.home, platforms: intent.platforms });
140
+ const matt = inspectMattInstallation({ projectRoot: intent.projectRoot, home: intent.home, platforms: intent.platforms, action: "matrix update --with-mattpocock" });
131
141
  ui.muted(`${text(intent.language, "mattCompatibilityStatus").replace("{release}", matt.supported.release).replace("{status}", localizedState(intent.language, matt.status))}${matt.action ? `; ${matt.action}` : ""}`);
132
142
  }
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
- }
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 });
177
247
  if (options.json) return refresh(intent, { ...options, yes: true, updateMode: true }, ui);
178
248
  return refresh(intent, { ...options, updateMode: true }, ui);
179
249
  }
@@ -189,7 +259,7 @@ function runtimeRecovery(projectRuntime, argv) {
189
259
  const argumentsWithoutJson = argv.slice(1).filter((value) => value !== "--json");
190
260
  return { ...projectRuntime, recovery_command: [projectRuntime.recovery_command, ...argumentsWithoutJson.map(recoveryArgument)].join(" ") };
191
261
  }
192
-
262
+
193
263
  async function doctor(options) {
194
264
  const ui = createUi({ color: options.color && !options.json });
195
265
  const root = projectRoot(options.directory);
@@ -198,12 +268,12 @@ async function doctor(options) {
198
268
  ? { ...installed.intent, dryRun: true }
199
269
  : await intentFrom({ ...options, directory: root, yes: true }, ui);
200
270
  const evaluation = createDistribution().diagnose(intent);
201
- const matt = inspectMattInstallation({ projectRoot: intent.projectRoot, home: intent.home, platforms: intent.platforms });
271
+ const matt = inspectMattInstallation({ projectRoot: intent.projectRoot, home: intent.home, platforms: intent.platforms, action: installed.ok ? "matrix update --with-mattpocock" : undefined });
202
272
  if (options.json) console.log(JSON.stringify({ ...evaluation, matt }));
203
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}` : ""}`); }
204
274
  return evaluation.ok ? 0 : 2;
205
275
  }
206
-
276
+
207
277
  export async function main(argv) {
208
278
  if (argv[0] === "workflow") {
209
279
  const json = argv.includes("--json");
@@ -219,15 +289,15 @@ export async function main(argv) {
219
289
  return Number.isInteger(delegated.status) ? delegated.status : 2;
220
290
  }
221
291
  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
- }
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
+ }