@rosenfan/matrix 0.1.1 → 0.1.3

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 (38) hide show
  1. package/.claude/skills/matrix/SKILL.md +60 -17
  2. package/.claude/skills/matrix/agents/openai.yaml +4 -4
  3. package/.claude/skills/matrix/scripts/matrix_claude.py +3 -158
  4. package/.claude/skills/matrix/scripts/matrix_state.py +53 -107
  5. package/.claude/skills/matrix-archive/SKILL.md +23 -13
  6. package/.claude/skills/matrix-build/SKILL.md +24 -35
  7. package/.claude/skills/matrix-claude/SKILL.md +26 -24
  8. package/.claude/skills/matrix-design/SKILL.md +29 -26
  9. package/.claude/skills/matrix-hotfix/SKILL.md +8 -8
  10. package/.claude/skills/matrix-open/SKILL.md +25 -18
  11. package/.claude/skills/matrix-status/SKILL.md +3 -2
  12. package/.claude/skills/matrix-tweak/SKILL.md +8 -8
  13. package/.claude/skills/matrix-verify/SKILL.md +21 -30
  14. package/LICENSE +21 -21
  15. package/README.md +294 -255
  16. package/README.zh-CN.md +293 -255
  17. package/assets/skills-zh-CN/matrix/SKILL.md +80 -25
  18. package/assets/skills-zh-CN/matrix-archive/SKILL.md +33 -9
  19. package/assets/skills-zh-CN/matrix-build/SKILL.md +36 -10
  20. package/assets/skills-zh-CN/matrix-claude/SKILL.md +31 -28
  21. package/assets/skills-zh-CN/matrix-design/SKILL.md +19 -10
  22. package/assets/skills-zh-CN/matrix-hotfix/SKILL.md +8 -8
  23. package/assets/skills-zh-CN/matrix-open/SKILL.md +22 -7
  24. package/assets/skills-zh-CN/matrix-status/SKILL.md +15 -14
  25. package/assets/skills-zh-CN/matrix-tweak/SKILL.md +8 -8
  26. package/assets/skills-zh-CN/matrix-verify/SKILL.md +26 -10
  27. package/bin/matrix.js +15 -15
  28. package/package.json +56 -56
  29. package/src/catalog.js +92 -87
  30. package/src/cli.js +159 -101
  31. package/src/distribution.js +281 -186
  32. package/src/installer.js +45 -45
  33. package/src/matt.js +45 -45
  34. package/src/messages.js +33 -33
  35. package/src/self-update.js +81 -0
  36. package/src/ui.js +89 -89
  37. package/src/workflow-transaction.js +622 -0
  38. package/src/workflow.js +787 -82
package/src/catalog.js CHANGED
@@ -1,87 +1,92 @@
1
- import crypto from "node:crypto";
2
- import fs from "node:fs";
3
- import os from "node:os";
4
- import path from "node:path";
5
- import { fileURLToPath } from "node:url";
6
-
7
- export const LANGUAGE_IDS = ["en", "zh-CN"];
8
- export const PLATFORM_IDS = ["claude-code", "codex"];
9
- export const MATRIX_SKILLS = [
10
- "matrix", "matrix-open", "matrix-design", "matrix-build", "matrix-verify",
11
- "matrix-archive", "matrix-hotfix", "matrix-tweak", "matrix-status", "matrix-claude"
12
- ];
13
- export const MATT_SKILLS = [
14
- "grill-with-docs", "grilling", "domain-modeling", "research", "wayfinder", "prototype",
15
- "codebase-design", "implement", "tdd", "code-review", "diagnosing-bugs",
16
- "resolving-merge-conflicts", "improve-codebase-architecture"
17
- ];
18
-
19
- const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
20
- const packageManifest = JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "utf8"));
21
-
22
- export const PLATFORMS = {
23
- "claude-code": {
24
- id: "claude-code", name: "Claude Code", skillsCliAgent: "claude-code",
25
- skillRoot(base) { return path.join(base, ".claude", "skills"); },
26
- legacyRoots() { return []; }
27
- },
28
- codex: {
29
- id: "codex", name: "Codex", skillsCliAgent: "codex",
30
- skillRoot(base) { return path.join(base, ".agents", "skills"); },
31
- legacyRoots(base) { return [path.join(base, ".codex", "skills")]; }
32
- }
33
- };
34
-
35
- export function sourceSkillsRoot(language = "en") {
36
- const localized = language === "zh-CN"
37
- ? path.join(packageRoot, "assets", "skills-zh-CN")
38
- : path.join(packageRoot, ".claude", "skills");
39
- if (!fs.existsSync(localized)) throw new Error(`Matrix package is incomplete: ${language} assets are missing.`);
40
- return localized;
41
- }
42
-
43
- export function scopeBase(scope, projectRoot, home = os.homedir()) {
44
- return scope === "global" ? home : projectRoot;
45
- }
46
-
47
- export function installationRoot(scope, projectRoot, home) {
48
- return path.join(scopeBase(scope, projectRoot, home), ".matrix");
49
- }
50
-
51
- export function manifestPath(scope, projectRoot, home) {
52
- return path.join(installationRoot(scope, projectRoot, home), "installation.json");
53
- }
54
-
55
- export function hashDirectory(directory, extraFiles = []) {
56
- const hash = crypto.createHash("sha256");
57
- const files = new Map();
58
- const visit = (current, relative = "") => {
59
- for (const entry of fs.readdirSync(current, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
60
- const child = path.join(current, entry.name);
61
- const childRelative = path.join(relative, entry.name).replaceAll("\\", "/");
62
- if (entry.isDirectory()) visit(child, childRelative);
63
- else if (entry.isFile()) {
64
- files.set(childRelative, fs.readFileSync(child));
65
- }
66
- }
67
- };
68
- visit(directory);
69
- for (const extra of extraFiles) files.set(extra.path, Buffer.from(extra.contents));
70
- for (const [relative, contents] of [...files.entries()].sort(([left], [right]) => left.localeCompare(right))) {
71
- hash.update(`${relative}\0`);
72
- hash.update(contents);
73
- }
74
- return hash.digest("hex");
75
- }
76
-
77
- export function workflowRuntimeSource() { return path.join(packageRoot, "src", "workflow.js"); }
78
-
79
- export function releaseCatalog(language = "en") {
80
- if (!LANGUAGE_IDS.includes(language)) throw new Error(`Unsupported language: ${language}`);
81
- const root = sourceSkillsRoot(language);
82
- const missing = MATRIX_SKILLS.filter((skill) => !fs.existsSync(path.join(root, skill, "SKILL.md")));
83
- if (missing.length) throw new Error(`Matrix package is incomplete. Missing: ${missing.join(", ")}`);
84
- const runtime = fs.readFileSync(workflowRuntimeSource());
85
- const skills = Object.fromEntries(MATRIX_SKILLS.map((skill) => [skill, hashDirectory(path.join(root, skill), [{ path: "scripts/matrix-runtime.mjs", contents: runtime }])]));
86
- return { language, root, version: packageManifest.version, skills, digest: crypto.createHash("sha256").update(JSON.stringify(skills)).digest("hex") };
87
- }
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ export const LANGUAGE_IDS = ["en", "zh-CN"];
8
+ export const PLATFORM_IDS = ["claude-code", "codex"];
9
+ export const MATRIX_SKILLS = [
10
+ "matrix", "matrix-open", "matrix-design", "matrix-build", "matrix-verify",
11
+ "matrix-archive", "matrix-hotfix", "matrix-tweak", "matrix-status", "matrix-claude"
12
+ ];
13
+ export const MATT_SKILLS = [
14
+ "grilling", "domain-modeling", "research", "wayfinder", "prototype",
15
+ "codebase-design", "tdd", "diagnosing-bugs", "resolving-merge-conflicts", "code-review"
16
+ ];
17
+ export const UNMANAGED_MATT_SKILLS = ["grill-with-docs", "implement", "improve-codebase-architecture"];
18
+
19
+ const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
20
+ const packageManifest = JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "utf8"));
21
+
22
+ export const PLATFORMS = {
23
+ "claude-code": {
24
+ id: "claude-code", name: "Claude Code", skillsCliAgent: "claude-code",
25
+ skillRoot(base) { return path.join(base, ".claude", "skills"); },
26
+ legacyRoots() { return []; }
27
+ },
28
+ codex: {
29
+ id: "codex", name: "Codex", skillsCliAgent: "codex",
30
+ skillRoot(base) { return path.join(base, ".agents", "skills"); },
31
+ legacyRoots(base) { return [path.join(base, ".codex", "skills")]; }
32
+ }
33
+ };
34
+
35
+ export function sourceSkillsRoot(language = "en") {
36
+ const localized = language === "zh-CN"
37
+ ? path.join(packageRoot, "assets", "skills-zh-CN")
38
+ : path.join(packageRoot, ".claude", "skills");
39
+ if (!fs.existsSync(localized)) throw new Error(`Matrix package is incomplete: ${language} assets are missing.`);
40
+ return localized;
41
+ }
42
+
43
+ export function scopeBase(scope, projectRoot, home = os.homedir()) {
44
+ return scope === "global" ? home : projectRoot;
45
+ }
46
+
47
+ export function installationRoot(scope, projectRoot, home) {
48
+ return path.join(scopeBase(scope, projectRoot, home), ".matrix");
49
+ }
50
+
51
+ export function manifestPath(scope, projectRoot, home) {
52
+ return path.join(installationRoot(scope, projectRoot, home), "installation.json");
53
+ }
54
+
55
+ export function hashDirectory(directory, extraFiles = []) {
56
+ const hash = crypto.createHash("sha256");
57
+ const files = new Map();
58
+ const visit = (current, relative = "") => {
59
+ for (const entry of fs.readdirSync(current, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
60
+ const child = path.join(current, entry.name);
61
+ const childRelative = path.join(relative, entry.name).replaceAll("\\", "/");
62
+ if (entry.isDirectory()) visit(child, childRelative);
63
+ else if (entry.isFile()) {
64
+ files.set(childRelative, fs.readFileSync(child));
65
+ }
66
+ }
67
+ };
68
+ visit(directory);
69
+ for (const extra of extraFiles) files.set(extra.path, Buffer.from(extra.contents));
70
+ for (const [relative, contents] of [...files.entries()].sort(([left], [right]) => left.localeCompare(right))) {
71
+ hash.update(`${relative}\0`);
72
+ hash.update(contents);
73
+ }
74
+ return hash.digest("hex");
75
+ }
76
+
77
+ export function workflowRuntimeSource() { return path.join(packageRoot, "src", "workflow.js"); }
78
+ export function workflowTransactionSource() { return path.join(packageRoot, "src", "workflow-transaction.js"); }
79
+
80
+ export function releaseCatalog(language = "en") {
81
+ if (!LANGUAGE_IDS.includes(language)) throw new Error(`Unsupported language: ${language}`);
82
+ const root = sourceSkillsRoot(language);
83
+ const missing = MATRIX_SKILLS.filter((skill) => !fs.existsSync(path.join(root, skill, "SKILL.md")));
84
+ if (missing.length) throw new Error(`Matrix package is incomplete. Missing: ${missing.join(", ")}`);
85
+ const runtime = fs.readFileSync(workflowRuntimeSource());
86
+ const transaction = fs.readFileSync(workflowTransactionSource());
87
+ const skills = Object.fromEntries(MATRIX_SKILLS.map((skill) => [skill, hashDirectory(path.join(root, skill), [
88
+ { path: "scripts/matrix-runtime.mjs", contents: runtime },
89
+ { path: "scripts/workflow-transaction.js", contents: transaction }
90
+ ])]));
91
+ return { language, root, version: packageManifest.version, skills, digest: crypto.createHash("sha256").update(JSON.stringify(skills)).digest("hex") };
92
+ }
package/src/cli.js CHANGED
@@ -1,117 +1,175 @@
1
- import fs from "node:fs";
1
+ import fs from "node:fs";
2
2
  import path from "node:path";
3
- import { fileURLToPath } from "node:url";
4
- import { PLATFORMS } from "./catalog.js";
5
- import { createDistribution } from "./distribution.js";
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";
6
7
  import { text } from "./messages.js";
7
8
  import { createUi } from "./ui.js";
8
9
  import { invoke } from "./workflow.js";
9
-
10
- const packagePath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "package.json");
11
- const version = JSON.parse(fs.readFileSync(packagePath, "utf8")).version;
12
- const HELP = `Matrix ${version}\n\nUsage: matrix init [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 --yes, -y --dry-run --json --no-color`;
13
-
14
- function parse(argv) {
15
- const result = { command: null, directory: null, scope: null, language: null, platforms: [], mode: "copy", matt: null, matrixPolicy: "safe", yes: false, dryRun: false, json: false, color: true };
16
- const values = [...argv];
17
- if (values[0] && !values[0].startsWith("-")) result.command = values.shift();
18
- while (values.length) {
19
- const token = values.shift();
20
- if (!token.startsWith("-") && !result.directory) result.directory = token;
21
- else if (token === "--scope") result.scope = values.shift();
22
- else if (token === "--language") result.language = values.shift();
23
- else if (token === "--platform") result.platforms.push(values.shift());
24
- else if (token === "--mode") result.mode = values.shift();
25
- else if (token === "--with-mattpocock") result.matt = "missing";
26
- else if (token === "--without-mattpocock") result.matt = "none";
27
- else if (token === "--force") result.matrixPolicy = "replace";
28
- else if (["--yes", "-y"].includes(token)) result.yes = true;
10
+ import { compareVersions, globalBinPath, latestVersion, selfUpdate } from "./self-update.js";
11
+
12
+ const packagePath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "package.json");
13
+ const version = JSON.parse(fs.readFileSync(packagePath, "utf8")).version;
14
+ 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 --default-orchestration <prim|arch>\n --skip-self-update --force --yes, -y --dry-run --json --no-color`;
15
+
16
+ function parse(argv) {
17
+ const result = { command: null, directory: null, scope: null, language: null, platforms: [], mode: "copy", matt: null, defaultOrchestration: null, matrixPolicy: "safe", yes: false, dryRun: false, json: false, color: true, skipSelfUpdate: false, reexec: false };
18
+ const values = [...argv];
19
+ if (values[0] && !values[0].startsWith("-")) result.command = values.shift();
20
+ while (values.length) {
21
+ const token = values.shift();
22
+ if (!token.startsWith("-") && !result.directory) result.directory = token;
23
+ else if (token === "--scope") result.scope = values.shift();
24
+ else if (token === "--language") result.language = values.shift();
25
+ else if (token === "--platform") result.platforms.push(values.shift());
26
+ else if (token === "--mode") result.mode = values.shift();
27
+ else if (token === "--with-mattpocock") result.matt = "missing";
28
+ else if (token === "--without-mattpocock") result.matt = "none";
29
+ else if (token === "--default-orchestration") result.defaultOrchestration = values.shift();
30
+ else if (token === "--force") result.matrixPolicy = "replace";
31
+ else if (["--yes", "-y"].includes(token)) result.yes = true;
29
32
  else if (token === "--dry-run") result.dryRun = true;
30
- else if (token === "--json") result.json = true;
31
- else if (token === "--no-color") result.color = false;
32
- else if (["--help", "-h"].includes(token)) result.help = true;
33
- else if (["--version", "-v"].includes(token)) result.version = true;
34
- else throw new Error(`Unknown option: ${token}`);
35
- }
36
- return result;
37
- }
38
-
39
- function projectRoot(directory) {
40
- const value = path.resolve(directory ?? process.cwd());
41
- if (!fs.existsSync(value) || !fs.statSync(value).isDirectory()) throw new Error(`Not a directory: ${value}`);
42
- return value;
43
- }
44
-
45
- function labelsFor(language) { return { yes: text(language, "yes"), no: text(language, "no") }; }
46
-
47
- function localizedState(language, state) {
48
- return text(language, ({ missing: "stateMissing", matching: "stateMatching", complete: "stateComplete", "user-modified": "stateUserModified", partial: "statePartial", damaged: "statePartial", newer: "stateMatching", "outdated-unchanged": "stateOutdated" })[state] ?? state);
49
- }
50
-
51
- function localizedAction(language, action) {
52
- return text(language, ({ install: "actionInstall", keep: "actionKeep", "backup-replace": "actionBackupReplace", repair: "actionRepair", blocked: "actionBlocked" })[action] ?? action);
53
- }
54
-
55
- async function intentFrom(options, ui) {
56
- const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY && !options.json);
57
- const language = options.language ?? (options.yes ? "en" : await ui.select("Language / 语言", [{ label: "English", value: "en" }, { label: "中文", value: "zh-CN" }]));
58
- 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" }]));
59
- let platforms = options.platforms;
60
- if (!platforms.length && interactive && !options.yes) platforms = await ui.selectMany(text(language, "platformsQuestion"), [{ label: "Claude Code", value: "claude-code" }, { label: "Codex", value: "codex" }]);
61
- const matt = options.matt ?? (interactive && !options.yes ? (await ui.confirm(text(language, "mattQuestion"), true, labelsFor(language)) ? "missing" : "none") : "missing");
62
- return { projectRoot: projectRoot(options.directory), language, scope, platforms, mode: options.mode, matt, matrixPolicy: options.matrixPolicy, dryRun: options.dryRun };
63
- }
64
-
65
- function render(ui, evaluation) {
66
- const language = evaluation.intent?.language ?? "en";
67
- const actions = (evaluation.actions ?? []).map((item) => `${item.platform}: ${localizedAction(language, item.matrix)}`).join(", ");
68
- ui.info(actions ? `${text(language, "preview")}: ${actions}` : evaluation.code);
69
- for (const item of evaluation.observations ?? []) ui.muted(`${item.name}: ${text(language, "matrix")} ${localizedState(language, item.matrix)};${text(language, "matt")} ${localizedState(language, item.matt.state)}${item.matt.inherited ? ` (${text(language, "global")})` : ""}`);
70
- for (const item of evaluation.diagnostics ?? []) ui.warn(item.code === "USER_MODIFIED_BLOCKED" ? text(language, "replaceQuestion") : item.message ?? item.code);
71
- for (const item of evaluation.diagnosis ?? []) (item.code === "MATT_INHERITED" ? ui.muted : ui.warn)(item.message ?? item.code);
72
- }
73
-
33
+ else if (token === "--skip-self-update") result.skipSelfUpdate = true;
34
+ else if (token === "--_reexec") result.reexec = true;
35
+ else if (token === "--json") result.json = true;
36
+ else if (token === "--no-color") result.color = false;
37
+ else if (["--help", "-h"].includes(token)) result.help = true;
38
+ else if (["--version", "-v"].includes(token)) result.version = true;
39
+ else throw new Error(`Unknown option: ${token}`);
40
+ }
41
+ return result;
42
+ }
43
+
44
+ function projectRoot(directory) {
45
+ const value = path.resolve(directory ?? process.cwd());
46
+ if (!fs.existsSync(value) || !fs.statSync(value).isDirectory()) throw new Error(`Not a directory: ${value}`);
47
+ return value;
48
+ }
49
+
50
+ function labelsFor(language) { return { yes: text(language, "yes"), no: text(language, "no") }; }
51
+
52
+ function localizedState(language, state) {
53
+ return text(language, ({ missing: "stateMissing", matching: "stateMatching", complete: "stateComplete", "user-modified": "stateUserModified", partial: "statePartial", damaged: "statePartial", newer: "stateMatching", "outdated-unchanged": "stateOutdated" })[state] ?? state);
54
+ }
55
+
56
+ function localizedAction(language, action) {
57
+ return text(language, ({ install: "actionInstall", keep: "actionKeep", "backup-replace": "actionBackupReplace", repair: "actionRepair", blocked: "actionBlocked" })[action] ?? action);
58
+ }
59
+
60
+ async function intentFrom(options, ui) {
61
+ const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY && !options.json);
62
+ const language = options.language ?? (options.yes ? "en" : await ui.select("Language / 语言", [{ label: "English", value: "en" }, { label: "中文", value: "zh-CN" }]));
63
+ 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" }]));
64
+ let platforms = options.platforms;
65
+ if (!platforms.length && interactive && !options.yes) platforms = await ui.selectMany(text(language, "platformsQuestion"), [{ label: "Claude Code", value: "claude-code" }, { label: "Codex", value: "codex" }]);
66
+ const matt = options.matt ?? (interactive && !options.yes ? (await ui.confirm(text(language, "mattQuestion"), true, labelsFor(language)) ? "missing" : "none") : "missing");
67
+ const defaultOrchestration = options.defaultOrchestration ?? (interactive && !options.yes && matt === "missing"
68
+ ? await ui.select(text(language, "orchestrationQuestion"), [{ label: "Arch", value: "arch" }, { label: "Prim", value: "prim" }])
69
+ : null);
70
+ return { projectRoot: projectRoot(options.directory), language, scope, platforms, mode: options.mode, matt, defaultOrchestration, nonInteractive: options.yes || !interactive, matrixPolicy: options.matrixPolicy, dryRun: options.dryRun };
71
+ }
72
+
73
+ function render(ui, evaluation) {
74
+ const language = evaluation.intent?.language ?? "en";
75
+ const actions = (evaluation.actions ?? []).map((item) => `${item.platform}: ${localizedAction(language, item.matrix)}`).join(", ");
76
+ ui.info(actions ? `${text(language, "preview")}: ${actions}` : evaluation.code);
77
+ if (evaluation.intent?.defaultOrchestration) ui.muted(`Default orchestration: ${evaluation.intent.defaultOrchestration}`);
78
+ for (const item of evaluation.observations ?? []) ui.muted(`${item.name}: ${text(language, "matrix")} ${localizedState(language, item.matrix)};${text(language, "matt")} ${localizedState(language, item.matt.state)}${item.matt.inherited ? ` (${text(language, "global")})` : ""}`);
79
+ for (const item of evaluation.diagnostics ?? []) ui.warn(item.code === "USER_MODIFIED_BLOCKED" ? text(language, "replaceQuestion") : item.message ?? item.code);
80
+ for (const item of evaluation.diagnosis ?? []) (item.code === "MATT_INHERITED" ? ui.muted : ui.warn)(item.message ?? item.code);
81
+ }
82
+
74
83
  async function init(options) {
75
84
  const ui = createUi({ color: options.color && !options.json });
76
85
  if (!options.json) await ui.banner();
77
86
  const intent = await intentFrom(options, ui);
87
+ return refresh(intent, options, ui);
88
+ }
89
+
90
+ async function refresh(intent, options, ui) {
78
91
  const distribution = createDistribution();
79
- let evaluation = distribution.evaluate(intent);
80
- const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY && !options.json);
81
- if (!evaluation.ok && evaluation.code === "USER_MODIFIED_BLOCKED" && interactive && !options.yes && await ui.confirm(text(intent.language, "replaceQuestion"), false, labelsFor(intent.language))) evaluation = distribution.evaluate({ ...intent, matrixPolicy: "replace" });
82
- if (options.json) { console.log(JSON.stringify(evaluation)); return evaluation.ok ? 0 : 2; }
83
- render(ui, evaluation);
84
- if (!evaluation.ok) return 2;
85
- if (!options.yes && interactive && !options.dryRun && !await ui.confirm(text(intent.language, "confirm"), true, labelsFor(intent.language))) return 0;
86
- const result = distribution.commit(evaluation.plan);
87
- if (!result.ok) { ui.warn(result.error ?? result.code); return 2; }
88
- if (result.code === "PARTIAL") { ui.warn(result.recovery); return 1; }
89
- ui.success(options.dryRun ? text(intent.language, "dryRunComplete") : text(intent.language, "ready"));
90
- ui.muted(evaluation.intent.platforms.map((id) => `${PLATFORMS[id].name}: $matrix <request>`).join("\n"));
92
+ let evaluation = distribution.evaluate(intent);
93
+ const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY && !options.json);
94
+ if (!evaluation.ok && evaluation.code === "USER_MODIFIED_BLOCKED" && interactive && !options.yes && await ui.confirm(text(intent.language, "replaceQuestion"), false, labelsFor(intent.language))) evaluation = distribution.evaluate({ ...intent, matrixPolicy: "replace" });
95
+ if (options.json) { console.log(JSON.stringify(evaluation)); return evaluation.ok ? 0 : 2; }
96
+ render(ui, evaluation);
97
+ if (!evaluation.ok) return 2;
98
+ if (!options.yes && interactive && !options.dryRun && !await ui.confirm(text(intent.language, "confirm"), true, labelsFor(intent.language))) return 0;
99
+ const result = distribution.commit(evaluation.plan);
100
+ if (!result.ok) { ui.warn(result.error ?? result.code); return 2; }
101
+ if (result.code === "PARTIAL") { ui.warn(result.recovery); return 1; }
102
+ ui.success(options.dryRun ? text(intent.language, "dryRunComplete") : text(intent.language, "ready"));
103
+ ui.muted(evaluation.intent.platforms.map((id) => `${PLATFORMS[id].name}: $matrix <request>`).join("\n"));
91
104
  return 0;
92
105
  }
93
106
 
94
- async function doctor(options) {
107
+ async function update(options) {
95
108
  const ui = createUi({ color: options.color && !options.json });
96
- const intent = await intentFrom({ ...options, yes: true }, ui);
97
- const evaluation = createDistribution().diagnose(intent);
98
- if (options.json) console.log(JSON.stringify(evaluation));
99
- else { await ui.banner(); ui.info(text(intent.language, "doctor")); render(ui, evaluation); }
100
- return evaluation.ok ? 0 : 2;
101
- }
102
-
103
- export async function main(argv) {
104
- if (argv[0] === "workflow") {
105
- const json = argv.includes("--json");
106
- const result = invoke(argv.slice(1).filter((value) => value !== "--json"));
107
- if (json) console.log(JSON.stringify(result));
108
- else console.log(result.ok ? JSON.stringify(result, null, 2) : `MATRIX ERROR [${result.code}]: ${result.message}`);
109
- return result.ok ? 0 : 2;
109
+ const root = projectRoot(options.directory);
110
+ const installed = installedIntent({ projectRoot: root });
111
+ if (!installed.ok) {
112
+ const result = { ok: false, code: installed.code, message: "No trusted Matrix installation manifest was found. Run matrix init first." };
113
+ if (options.json) console.log(JSON.stringify(result)); else ui.warn(result.message);
114
+ return 2;
110
115
  }
111
- const options = parse(argv);
112
- if (options.version) { console.log(version); return 0; }
113
- if (options.help || !options.command) { console.log(HELP); return 0; }
114
- if (options.command === "init") return init(options);
115
- if (options.command === "doctor") return doctor(options);
116
- throw new Error(`Unknown command: ${options.command}. Run matrix --help.`);
116
+ const forbidden = options.language || options.scope || options.platforms.length || options.matt || options.defaultOrchestration || options.mode !== "copy";
117
+ if (forbidden) throw new Error("matrix update inherits installation settings. Use matrix init to change language, scope, platform, mode, Matt skills, or orchestration.");
118
+ const intent = { ...installed.intent, dryRun: options.dryRun };
119
+ let npm = { status: "skipped", current: version, target: version, reason: options.skipSelfUpdate ? "disabled by --skip-self-update" : null };
120
+ if (!options.skipSelfUpdate && !options.reexec) {
121
+ try {
122
+ const target = await latestVersion(); const comparison = compareVersions(target, version);
123
+ if (comparison === null) throw new Error("unable to compare registry version");
124
+ npm = comparison > 0 ? { status: "available", current: version, target } : { status: "skipped", current: version, target, reason: comparison === 0 ? "already latest" : "registry version is older" };
125
+ } catch (error) {
126
+ const result = { ok: false, code: "SELF_UPDATE_CHECK_FAILED", npm: { status: "failed", current: version, reason: error.message }, recovery_command: "matrix update --skip-self-update" };
127
+ if (options.json) console.log(JSON.stringify(result)); else ui.warn(`${result.code}: ${error.message}\nRun ${result.recovery_command} to refresh assets without npm.`);
128
+ return 2;
129
+ }
130
+ }
131
+ if (options.json && options.dryRun) { console.log(JSON.stringify({ ok: true, code: "DRY_RUN", npm, intent })); return 0; }
132
+ if (!options.json) {
133
+ await ui.banner(); ui.info(`Matrix update: CLI ${npm.current}${npm.status === "available" ? ` → ${npm.target}` : ""}`);
134
+ ui.muted(`Assets: ${intent.scope}, ${intent.language}, ${intent.platforms.join(", ")}`);
135
+ }
136
+ const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY && !options.json);
137
+ if (npm.status === "available") {
138
+ 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;
139
+ if (options.dryRun) { if (options.json) console.log(JSON.stringify({ ok: true, code: "DRY_RUN", npm, intent })); else ui.success(text(intent.language, "dryRunComplete")); return 0; }
140
+ const updated = selfUpdate(version, npm.target);
141
+ 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; }
142
+ const args = [globalBinPath(), "update", root, "--skip-self-update", "--_reexec", "--yes"];
143
+ if (options.matrixPolicy === "replace") args.push("--force"); if (options.json) args.push("--json"); if (!options.color) args.push("--no-color");
144
+ const rerun = spawnSync(process.execPath, args, { stdio: "inherit" });
145
+ return Number.isInteger(rerun.status) ? rerun.status : 2;
146
+ }
147
+ if (options.json) return refresh(intent, { ...options, yes: true }, ui);
148
+ return refresh(intent, options, ui);
117
149
  }
150
+
151
+ async function doctor(options) {
152
+ const ui = createUi({ color: options.color && !options.json });
153
+ const intent = await intentFrom({ ...options, yes: true }, ui);
154
+ const evaluation = createDistribution().diagnose(intent);
155
+ if (options.json) console.log(JSON.stringify(evaluation));
156
+ else { await ui.banner(); ui.info(text(intent.language, "doctor")); render(ui, evaluation); }
157
+ return evaluation.ok ? 0 : 2;
158
+ }
159
+
160
+ export async function main(argv) {
161
+ if (argv[0] === "workflow") {
162
+ const json = argv.includes("--json");
163
+ const result = invoke(argv.slice(1).filter((value) => value !== "--json"));
164
+ if (json) console.log(JSON.stringify(result));
165
+ else console.log(result.ok ? JSON.stringify(result, null, 2) : `MATRIX ERROR [${result.code}]: ${result.message}`);
166
+ return result.ok ? 0 : 2;
167
+ }
168
+ const options = parse(argv);
169
+ if (options.version) { console.log(version); return 0; }
170
+ if (options.help || !options.command) { console.log(HELP); return 0; }
171
+ if (options.command === "init") return init(options);
172
+ if (options.command === "update") return update(options);
173
+ if (options.command === "doctor") return doctor(options);
174
+ throw new Error(`Unknown command: ${options.command}. Run matrix --help.`);
175
+ }