@rosenfan/matrix 0.1.2 → 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 (33) hide show
  1. package/.claude/skills/matrix/SKILL.md +58 -56
  2. package/.claude/skills/matrix/scripts/matrix_claude.py +11 -11
  3. package/.claude/skills/matrix/scripts/matrix_state.py +60 -60
  4. package/.claude/skills/matrix-archive/SKILL.md +27 -24
  5. package/.claude/skills/matrix-build/SKILL.md +26 -24
  6. package/.claude/skills/matrix-claude/SKILL.md +11 -9
  7. package/.claude/skills/matrix-design/SKILL.md +31 -27
  8. package/.claude/skills/matrix-hotfix/SKILL.md +1 -1
  9. package/.claude/skills/matrix-open/SKILL.md +29 -19
  10. package/.claude/skills/matrix-status/SKILL.md +5 -5
  11. package/.claude/skills/matrix-tweak/SKILL.md +1 -1
  12. package/.claude/skills/matrix-verify/SKILL.md +23 -21
  13. package/README.md +123 -97
  14. package/README.zh-CN.md +122 -96
  15. package/assets/skills-zh-CN/matrix/SKILL.md +78 -75
  16. package/assets/skills-zh-CN/matrix-archive/SKILL.md +31 -28
  17. package/assets/skills-zh-CN/matrix-build/SKILL.md +37 -35
  18. package/assets/skills-zh-CN/matrix-claude/SKILL.md +31 -29
  19. package/assets/skills-zh-CN/matrix-design/SKILL.md +10 -6
  20. package/assets/skills-zh-CN/matrix-hotfix/SKILL.md +1 -1
  21. package/assets/skills-zh-CN/matrix-open/SKILL.md +19 -9
  22. package/assets/skills-zh-CN/matrix-status/SKILL.md +5 -5
  23. package/assets/skills-zh-CN/matrix-tweak/SKILL.md +1 -1
  24. package/assets/skills-zh-CN/matrix-verify/SKILL.md +17 -15
  25. package/package.json +2 -2
  26. package/src/catalog.js +16 -16
  27. package/src/cli.js +81 -28
  28. package/src/distribution.js +154 -123
  29. package/src/messages.js +4 -4
  30. package/src/self-update.js +81 -0
  31. package/src/ui.js +1 -1
  32. package/src/workflow-transaction.js +622 -622
  33. package/src/workflow.js +652 -420
@@ -2,38 +2,47 @@ import crypto from "node:crypto";
2
2
  import fs from "node:fs";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
- import {
6
- MATRIX_SKILLS, MATT_SKILLS, UNMANAGED_MATT_SKILLS, PLATFORM_IDS, PLATFORMS, hashDirectory,
7
- installationRoot, manifestPath, releaseCatalog, scopeBase, sourceSkillsRoot, workflowRuntimeSource, workflowTransactionSource
8
- } from "./catalog.js";
5
+ import {
6
+ MATRIX_SKILLS, MATT_SKILLS, UNMANAGED_MATT_SKILLS, PLATFORM_IDS, PLATFORMS, hashDirectory,
7
+ installationRoot, manifestPath, releaseCatalog, scopeBase, sourceSkillsRoot, workflowRuntimeSource, workflowTransactionSource
8
+ } from "./catalog.js";
9
9
  import { createMattAdapter } from "./matt.js";
10
10
  import { text } from "./messages.js";
11
11
 
12
- const VERSION = 2;
12
+ const VERSION = 2;
13
13
  const digest = (value) => crypto.createHash("sha256").update(JSON.stringify(value)).digest("hex");
14
14
  const exists = (value) => fs.existsSync(value);
15
15
  const pathExists = (value) => { try { fs.lstatSync(value); return true; } catch { return false; } };
16
16
  const nowStamp = () => new Date().toISOString().replace(/[:.]/g, "-");
17
- const transactionPath = (intent) => path.join(installationRoot(intent.scope, intent.projectRoot, intent.home), "transaction.json");
18
- const configPath = (intent) => path.join(intent.projectRoot, ".matrix", "config.yaml");
17
+ const transactionPath = (intent) => path.join(installationRoot(intent.scope, intent.projectRoot, intent.home), "transaction.json");
18
+ const configPath = (intent) => path.join(intent.projectRoot, ".matrix", "config.yaml");
19
19
  const writeJson = (file, value) => {
20
20
  fs.mkdirSync(path.dirname(file), { recursive: true });
21
21
  const temporary = `${file}.${process.pid}.${Date.now()}.tmp`;
22
22
  fs.writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`);
23
23
  fs.renameSync(temporary, file);
24
24
  };
25
- const readJson = (file) => { try { return JSON.parse(fs.readFileSync(file, "utf8")); } catch { return null; } };
26
- const readConfig = (file) => {
27
- try {
28
- return Object.fromEntries(fs.readFileSync(file, "utf8").split(/\r?\n/).filter((line) => line.includes(":") && !line.startsWith(" ")).map((line) => {
29
- const [key, ...value] = line.split(":");
30
- return [key.trim(), value.join(":").trim()];
31
- }));
32
- } catch { return {}; }
33
- };
34
- const configContent = (existing, defaultOrchestration, scope) => {
35
- const autoTransition = existing.auto_transition === "false" ? "false" : "true";
36
- return `schema: matrix/config/v1\nauto_transition: ${autoTransition}\ndefault_orchestration: ${defaultOrchestration}\ninstallation_scope: ${scope}\n`;
25
+ const readJson = (file) => { try { return JSON.parse(fs.readFileSync(file, "utf8")); } catch { return null; } };
26
+ const readConfig = (file) => {
27
+ try {
28
+ return Object.fromEntries(fs.readFileSync(file, "utf8").split(/\r?\n/).filter((line) => line.includes(":") && !line.startsWith(" ")).map((line) => {
29
+ const [key, ...value] = line.split(":");
30
+ return [key.trim(), value.join(":").trim()];
31
+ }));
32
+ } catch { return {}; }
33
+ };
34
+ const configContent = (existing, defaultOrchestration, scope, language) => {
35
+ const values = {
36
+ ...existing,
37
+ schema: "matrix/config/v1",
38
+ auto_transition: existing.auto_transition === "false" ? "false" : "true",
39
+ default_orchestration: defaultOrchestration,
40
+ installation_scope: scope,
41
+ language
42
+ };
43
+ const preferred = ["schema", "auto_transition", "default_orchestration", "installation_scope", "language"];
44
+ const keys = [...preferred, ...Object.keys(values).filter((key) => !preferred.includes(key)).sort()];
45
+ return `${keys.map((key) => `${key}: ${values[key]}`).join("\n")}\n`;
37
46
  };
38
47
  const removeEntry = (value) => { if (pathExists(value)) fs.rmSync(value, { recursive: true, force: true }); };
39
48
  const removeStaging = (stagingRoot) => {
@@ -50,7 +59,29 @@ const compareVersions = (left, right) => {
50
59
  const skillPresent = (root, skill) => exists(path.join(root, skill, "SKILL.md"));
51
60
  const isBrokenLink = (value) => { try { return fs.lstatSync(value).isSymbolicLink() && !exists(value); } catch { return false; } };
52
61
 
53
- function readManifest(intent) { return readJson(manifestPath(intent.scope, intent.projectRoot, intent.home)); }
62
+ function readManifest(intent) { return readJson(manifestPath(intent.scope, intent.projectRoot, intent.home)); }
63
+
64
+ export function installedIntent({ projectRoot = process.cwd(), home = os.homedir() } = {}) {
65
+ const resolvedProjectRoot = path.resolve(projectRoot);
66
+ const config = readConfig(path.join(resolvedProjectRoot, ".matrix", "config.yaml"));
67
+ const scope = config.installation_scope === "global" ? "global" : "project";
68
+ const manifest = readJson(manifestPath(scope, resolvedProjectRoot, home));
69
+ const platforms = Object.keys(manifest?.platforms ?? {}).filter((id) => PLATFORM_IDS.includes(id));
70
+ if (!manifest || !platforms.length) return { ok: false, code: "MANIFEST_MISSING", scope, config };
71
+ const language = ["en", "zh-CN"].includes(manifest.language) ? manifest.language : (["en", "zh-CN"].includes(config.language) ? config.language : "en");
72
+ const defaultOrchestration = ["prim", "arch"].includes(manifest.orchestration?.default) ? manifest.orchestration.default : "prim";
73
+ const matt = manifest.orchestration?.available?.includes("arch") ? "missing" : "none";
74
+ return {
75
+ ok: true,
76
+ intent: {
77
+ projectRoot: resolvedProjectRoot, home, scope, platforms, language,
78
+ mode: ["copy", "symlink"].includes(manifest.mode) ? manifest.mode : "copy",
79
+ matt, defaultOrchestration, nonInteractive: true, matrixPolicy: "safe", dryRun: false
80
+ },
81
+ manifest,
82
+ config
83
+ };
84
+ }
54
85
 
55
86
  function detectPlatforms(projectRoot, scope, home) {
56
87
  const bases = [scopeBase(scope, projectRoot, home)];
@@ -58,19 +89,19 @@ function detectPlatforms(projectRoot, scope, home) {
58
89
  return PLATFORM_IDS.filter((id) => bases.some((base) => exists(PLATFORMS[id].skillRoot(base))));
59
90
  }
60
91
 
61
- function normalizeIntent(raw) {
92
+ function normalizeIntent(raw) {
62
93
  const scope = raw.scope ?? "project";
63
94
  const platforms = [...new Set(raw.platforms ?? [])];
64
95
  if (!["project", "global"].includes(scope)) return { error: "INVALID_INTENT", detail: "scope" };
65
96
  if (!["copy", "symlink"].includes(raw.mode ?? "copy")) return { error: "INVALID_INTENT", detail: "mode" };
66
- if (!["en", "zh-CN"].includes(raw.language ?? "en")) return { error: "INVALID_INTENT", detail: "language" };
67
- if (raw.defaultOrchestration != null && !["prim", "arch"].includes(raw.defaultOrchestration)) return { error: "INVALID_INTENT", detail: "default-orchestration" };
97
+ if (!["en", "zh-CN"].includes(raw.language ?? "en")) return { error: "INVALID_INTENT", detail: "language" };
98
+ if (raw.defaultOrchestration != null && !["prim", "arch"].includes(raw.defaultOrchestration)) return { error: "INVALID_INTENT", detail: "default-orchestration" };
68
99
  if (platforms.some((id) => !PLATFORM_IDS.includes(id))) return { error: "UNVERIFIED_PLATFORM" };
69
100
  return { projectRoot: path.resolve(raw.projectRoot ?? process.cwd()), home: raw.home ?? os.homedir(), scope, platforms,
70
- language: raw.language ?? "en", mode: raw.mode ?? "copy", matrixPolicy: raw.matrixPolicy ?? "safe",
71
- matt: raw.matt ?? "none", defaultOrchestration: raw.defaultOrchestration ?? null,
72
- nonInteractive: Boolean(raw.nonInteractive), dryRun: Boolean(raw.dryRun) };
73
- }
101
+ language: raw.language ?? "en", mode: raw.mode ?? "copy", matrixPolicy: raw.matrixPolicy ?? "safe",
102
+ matt: raw.matt ?? "none", defaultOrchestration: raw.defaultOrchestration ?? null,
103
+ nonInteractive: Boolean(raw.nonInteractive), dryRun: Boolean(raw.dryRun) };
104
+ }
74
105
 
75
106
  function observePlatform(intent, catalog, manifest, platformId) {
76
107
  const platform = PLATFORMS[platformId]; const base = scopeBase(intent.scope, intent.projectRoot, intent.home);
@@ -90,18 +121,18 @@ function observePlatform(intent, catalog, manifest, platformId) {
90
121
  : states.has("missing") && states.size === 1 ? "missing" : states.has("newer") ? "newer"
91
122
  : states.has("damaged") ? "damaged" : states.has("outdated-unchanged") ? "outdated-unchanged" : "partial";
92
123
  const globalRoot = platform.skillRoot(intent.home);
93
- const mattPresent = MATT_SKILLS.filter((skill) => skillPresent(root, skill));
94
- const globalMatt = intent.scope === "project" ? MATT_SKILLS.filter((skill) => skillPresent(globalRoot, skill)) : [];
95
- const availableMatt = new Set([...mattPresent, ...globalMatt]); const missing = MATT_SKILLS.filter((skill) => !availableMatt.has(skill));
96
- const inherited = globalMatt.filter((skill) => !mattPresent.includes(skill)).length;
97
- const managedSkills = Object.fromEntries([...availableMatt].map((skill) => {
98
- const target = mattPresent.includes(skill) ? path.join(root, skill) : path.join(globalRoot, skill);
99
- return [skill, { root: target, hash: hashDirectory(target) }];
100
- }));
101
- const unmanaged = UNMANAGED_MATT_SKILLS.filter((skill) => skillPresent(root, skill) || (intent.scope === "project" && skillPresent(globalRoot, skill)));
102
- return { id: platformId, name: platform.name, root, skills, matrix, matt: { present: mattPresent.length, missing, total: MATT_SKILLS.length, inherited,
103
- state: availableMatt.size === MATT_SKILLS.length ? "complete" : availableMatt.size ? "partial" : "missing", skills: managedSkills, unmanaged } };
104
- }
124
+ const mattPresent = MATT_SKILLS.filter((skill) => skillPresent(root, skill));
125
+ const globalMatt = intent.scope === "project" ? MATT_SKILLS.filter((skill) => skillPresent(globalRoot, skill)) : [];
126
+ const availableMatt = new Set([...mattPresent, ...globalMatt]); const missing = MATT_SKILLS.filter((skill) => !availableMatt.has(skill));
127
+ const inherited = globalMatt.filter((skill) => !mattPresent.includes(skill)).length;
128
+ const managedSkills = Object.fromEntries([...availableMatt].map((skill) => {
129
+ const target = mattPresent.includes(skill) ? path.join(root, skill) : path.join(globalRoot, skill);
130
+ return [skill, { root: target, hash: hashDirectory(target) }];
131
+ }));
132
+ const unmanaged = UNMANAGED_MATT_SKILLS.filter((skill) => skillPresent(root, skill) || (intent.scope === "project" && skillPresent(globalRoot, skill)));
133
+ return { id: platformId, name: platform.name, root, skills, matrix, matt: { present: mattPresent.length, missing, total: MATT_SKILLS.length, inherited,
134
+ state: availableMatt.size === MATT_SKILLS.length ? "complete" : availableMatt.size ? "partial" : "missing", skills: managedSkills, unmanaged } };
135
+ }
105
136
 
106
137
  function actionFor(observation, policy) {
107
138
  if (observation.matrix === "matching" || observation.matrix === "newer") return "keep";
@@ -111,60 +142,60 @@ function actionFor(observation, policy) {
111
142
  return "repair";
112
143
  }
113
144
 
114
- export function recoverPendingTransaction(intent) {
115
- const file = transactionPath(intent); const journal = readJson(file);
116
- if (!journal) return { ok: true, recovered: false };
117
- const restoreMetadata = (entry) => {
118
- if (!entry) return;
119
- if (entry.backup && pathExists(entry.backup)) {
120
- removeEntry(entry.path);
121
- fs.mkdirSync(path.dirname(entry.path), { recursive: true });
122
- fs.renameSync(entry.backup, entry.path);
123
- } else if (entry.written && !entry.existed) removeEntry(entry.path);
124
- };
125
- try {
126
- if (journal.state === "committed") {
127
- removeStaging(journal.stagingRoot);
128
- for (const operation of journal.operations ?? []) removeEntry(operation.backup);
129
- removeEntry(journal.manifest?.backup); removeEntry(journal.config?.backup); removeEntry(file);
130
- return { ok: true, recovered: true, receipt: `Finalized completed Matrix transaction ${journal.id}.` };
145
+ export function recoverPendingTransaction(intent) {
146
+ const file = transactionPath(intent); const journal = readJson(file);
147
+ if (!journal) return { ok: true, recovered: false };
148
+ const restoreMetadata = (entry) => {
149
+ if (!entry) return;
150
+ if (entry.backup && pathExists(entry.backup)) {
151
+ removeEntry(entry.path);
152
+ fs.mkdirSync(path.dirname(entry.path), { recursive: true });
153
+ fs.renameSync(entry.backup, entry.path);
154
+ } else if (entry.written && !entry.existed) removeEntry(entry.path);
155
+ };
156
+ try {
157
+ if (journal.state === "committed") {
158
+ removeStaging(journal.stagingRoot);
159
+ for (const operation of journal.operations ?? []) removeEntry(operation.backup);
160
+ removeEntry(journal.manifest?.backup); removeEntry(journal.config?.backup); removeEntry(file);
161
+ return { ok: true, recovered: true, receipt: `Finalized completed Matrix transaction ${journal.id}.` };
131
162
  }
132
163
  for (const operation of [...(journal.operations ?? [])].reverse()) {
133
164
  if (operation.status !== "pending") removeEntry(operation.target);
134
165
  if (operation.backup && pathExists(operation.backup)) { fs.mkdirSync(path.dirname(operation.target), { recursive: true }); fs.renameSync(operation.backup, operation.target); }
135
166
  }
136
- restoreMetadata(journal.config);
137
- restoreMetadata(journal.manifest);
167
+ restoreMetadata(journal.config);
168
+ restoreMetadata(journal.manifest);
138
169
  removeStaging(journal.stagingRoot); removeEntry(file);
139
170
  return { ok: true, recovered: true, receipt: `Recovered interrupted Matrix transaction ${journal.id}.` };
140
171
  } catch (error) { return { ok: false, code: "RECOVERY_REQUIRED", recovery: `Keep ${file} and restore the paths recorded there before retrying.`, error: error.message }; }
141
172
  }
142
173
 
143
174
  export function createDistribution({ failAt, mattAdapter = createMattAdapter() } = {}) {
144
- const evaluate = (rawIntent) => {
145
- const intent = normalizeIntent(rawIntent); const language = intent.language ?? rawIntent.language ?? "en";
146
- if (intent.error) return { ok: false, code: intent.error, diagnostics: [{ code: intent.error, message: text(language, "noPlatform") }] };
147
- if (!intent.platforms.length) { const detected = detectPlatforms(intent.projectRoot, intent.scope, intent.home); if (!detected.length) return { ok: false, code: "INPUT_REQUIRED", diagnostics: [{ code: "INPUT_REQUIRED", message: text(language, "noPlatform") }] }; intent.platforms = detected; }
148
- let catalog; try { catalog = releaseCatalog(intent.language); } catch (error) { return { ok: false, code: "PACKAGE_INCOMPLETE", intent, diagnostics: [{ code: "PACKAGE_INCOMPLETE", message: error.message }] }; }
149
- const manifest = readManifest(intent);
150
- intent.platforms = [...new Set([...Object.keys(manifest?.platforms ?? {}), ...intent.platforms])];
151
- const observations = intent.platforms.map((id) => observePlatform(intent, catalog, manifest, id));
152
- const existingConfig = readConfig(configPath(intent));
153
- const existingDefault = ["prim", "arch"].includes(existingConfig.default_orchestration) ? existingConfig.default_orchestration
154
- : ["prim", "arch"].includes(manifest?.orchestration?.default) ? manifest.orchestration.default : null;
155
- intent.defaultOrchestration = intent.defaultOrchestration ?? existingDefault ?? (intent.matt === "missing" && intent.nonInteractive ? "arch" : "prim");
156
- const actions = observations.map((item) => ({ platform: item.id, matrix: actionFor(item, intent.matrixPolicy), matt: item.matt.state === "complete" || intent.matt === "none" ? { kind: "keep", skills: [] } : { kind: "install-missing", skills: item.matt.missing } }));
157
- const blocked = actions.filter((item) => item.matrix === "blocked");
158
- const archMustRemainAvailable = manifest?.orchestration?.available?.includes("arch") || intent.defaultOrchestration === "arch";
159
- const incompleteWithoutInstall = archMustRemainAvailable && actions.some((item, index) => observations[index].matt.state !== "complete" && item.matt.kind !== "install-missing");
160
- const plan = { version: VERSION, intent, catalogDigest: catalog.digest, manifestPath: manifestPath(intent.scope, intent.projectRoot, intent.home), configPath: configPath(intent), actions, observations, preconditionDigest: digest(observations) };
161
- plan.seal = digest({ intent, catalogDigest: plan.catalogDigest, actions, preconditionDigest: plan.preconditionDigest });
162
- const code = blocked.length ? "USER_MODIFIED_BLOCKED" : incompleteWithoutInstall ? "ARCH_INSTALLATION_INCOMPLETE" : "OK";
163
- return { ok: code === "OK", code, intent, observations, actions, plan, diagnostics: [
164
- ...blocked.map((item) => ({ code: "USER_MODIFIED_BLOCKED", platform: item.platform, message: `${item.platform}: existing Matrix assets are not proven managed.` })),
165
- ...(incompleteWithoutInstall ? [{ code: "ARCH_INSTALLATION_INCOMPLETE", message: "Arch requires the complete managed cohort on every configured platform. Retry with --with-mattpocock." }] : [])
166
- ], summary: `${text(intent.language, "preview")}: ${actions.map((item) => `${item.platform}: ${item.matrix}`).join(", ")}` };
167
- };
175
+ const evaluate = (rawIntent) => {
176
+ const intent = normalizeIntent(rawIntent); const language = intent.language ?? rawIntent.language ?? "en";
177
+ if (intent.error) return { ok: false, code: intent.error, diagnostics: [{ code: intent.error, message: text(language, "noPlatform") }] };
178
+ if (!intent.platforms.length) { const detected = detectPlatforms(intent.projectRoot, intent.scope, intent.home); if (!detected.length) return { ok: false, code: "INPUT_REQUIRED", diagnostics: [{ code: "INPUT_REQUIRED", message: text(language, "noPlatform") }] }; intent.platforms = detected; }
179
+ let catalog; try { catalog = releaseCatalog(intent.language); } catch (error) { return { ok: false, code: "PACKAGE_INCOMPLETE", intent, diagnostics: [{ code: "PACKAGE_INCOMPLETE", message: error.message }] }; }
180
+ const manifest = readManifest(intent);
181
+ intent.platforms = [...new Set([...Object.keys(manifest?.platforms ?? {}), ...intent.platforms])];
182
+ const observations = intent.platforms.map((id) => observePlatform(intent, catalog, manifest, id));
183
+ const existingConfig = readConfig(configPath(intent));
184
+ const existingDefault = ["prim", "arch"].includes(existingConfig.default_orchestration) ? existingConfig.default_orchestration
185
+ : ["prim", "arch"].includes(manifest?.orchestration?.default) ? manifest.orchestration.default : null;
186
+ intent.defaultOrchestration = intent.defaultOrchestration ?? existingDefault ?? (intent.matt === "missing" && intent.nonInteractive ? "arch" : "prim");
187
+ const actions = observations.map((item) => ({ platform: item.id, matrix: actionFor(item, intent.matrixPolicy), matt: item.matt.state === "complete" || intent.matt === "none" ? { kind: "keep", skills: [] } : { kind: "install-missing", skills: item.matt.missing } }));
188
+ const blocked = actions.filter((item) => item.matrix === "blocked");
189
+ const archMustRemainAvailable = manifest?.orchestration?.available?.includes("arch") || intent.defaultOrchestration === "arch";
190
+ const incompleteWithoutInstall = archMustRemainAvailable && actions.some((item, index) => observations[index].matt.state !== "complete" && item.matt.kind !== "install-missing");
191
+ const plan = { version: VERSION, intent, catalogDigest: catalog.digest, manifestPath: manifestPath(intent.scope, intent.projectRoot, intent.home), configPath: configPath(intent), actions, observations, preconditionDigest: digest(observations) };
192
+ plan.seal = digest({ intent, catalogDigest: plan.catalogDigest, actions, preconditionDigest: plan.preconditionDigest });
193
+ const code = blocked.length ? "USER_MODIFIED_BLOCKED" : incompleteWithoutInstall ? "ARCH_INSTALLATION_INCOMPLETE" : "OK";
194
+ return { ok: code === "OK", code, intent, observations, actions, plan, diagnostics: [
195
+ ...blocked.map((item) => ({ code: "USER_MODIFIED_BLOCKED", platform: item.platform, message: `${item.platform}: existing Matrix assets are not proven managed.` })),
196
+ ...(incompleteWithoutInstall ? [{ code: "ARCH_INSTALLATION_INCOMPLETE", message: "Arch requires the complete managed cohort on every configured platform. Retry with --with-mattpocock." }] : [])
197
+ ], summary: `${text(intent.language, "preview")}: ${actions.map((item) => `${item.platform}: ${item.matrix}`).join(", ")}` };
198
+ };
168
199
 
169
200
  return { evaluate,
170
201
  diagnose(rawIntent) {
@@ -175,13 +206,13 @@ export function createDistribution({ failAt, mattAdapter = createMattAdapter() }
175
206
  for (const observation of evaluation.observations ?? []) {
176
207
  diagnostics.push({ code: `MATRIX_${observation.matrix.toUpperCase().replaceAll("-", "_")}`, platform: observation.id, message: `${observation.name}: Matrix is ${observation.matrix}.` });
177
208
  const matrixRoot = path.join(observation.root, "matrix");
178
- if (pathExists(matrixRoot) && (!exists(matrixRoot) || !exists(path.join(matrixRoot, "scripts", "matrix-runtime.mjs")) || !exists(path.join(matrixRoot, "scripts", "workflow-transaction.js")))) diagnostics.push({ code: "RUNTIME_MISSING", platform: observation.id, message: `${observation.name}: ${text(evaluation.intent.language, "runtimeMissing")}` });
209
+ if (pathExists(matrixRoot) && (!exists(matrixRoot) || !exists(path.join(matrixRoot, "scripts", "matrix-runtime.mjs")) || !exists(path.join(matrixRoot, "scripts", "workflow-transaction.js")))) diagnostics.push({ code: "RUNTIME_MISSING", platform: observation.id, message: `${observation.name}: ${text(evaluation.intent.language, "runtimeMissing")}` });
179
210
  for (const skill of observation.skills.filter((entry) => isBrokenLink(entry.target))) diagnostics.push({ code: "BROKEN_LINK", platform: observation.id, path: skill.target, message: `${observation.name}: ${text(evaluation.intent.language, "brokenLink")} ${skill.target}` });
180
211
  if (observation.id === "codex" && PLATFORMS.codex.legacyRoots(scopeBase(evaluation.intent.scope, evaluation.intent.projectRoot, evaluation.intent.home)).some(exists)) diagnostics.push({ code: "CODEX_LEGACY_DETECTED", platform: observation.id, message: "Codex legacy .codex/skills was detected; Matrix will not write there." });
181
- diagnostics.push({ code: `MATT_${observation.matt.state.toUpperCase()}`, platform: observation.id, message: `${observation.name}: Matt Pocock skills are ${observation.matt.state}.` });
182
- if (observation.matt.inherited) diagnostics.push({ code: "MATT_INHERITED", platform: observation.id, message: `${observation.name}: ${observation.matt.inherited} ${text(evaluation.intent.language, "mattInherited")}` });
183
- if (observation.matt.unmanaged.length) diagnostics.push({ code: "ARCH_UNMANAGED_EXTRA", platform: observation.id, skills: observation.matt.unmanaged, message: `${observation.name}: unmanaged extra Skills are preserved and will not be invoked: ${observation.matt.unmanaged.join(", ")}.` });
184
- }
212
+ diagnostics.push({ code: `MATT_${observation.matt.state.toUpperCase()}`, platform: observation.id, message: `${observation.name}: Matt Pocock skills are ${observation.matt.state}.` });
213
+ if (observation.matt.inherited) diagnostics.push({ code: "MATT_INHERITED", platform: observation.id, message: `${observation.name}: ${observation.matt.inherited} ${text(evaluation.intent.language, "mattInherited")}` });
214
+ if (observation.matt.unmanaged.length) diagnostics.push({ code: "ARCH_UNMANAGED_EXTRA", platform: observation.id, skills: observation.matt.unmanaged, message: `${observation.name}: unmanaged extra Skills are preserved and will not be invoked: ${observation.matt.unmanaged.join(", ")}.` });
215
+ }
185
216
  return { ...evaluation, diagnosis: diagnostics };
186
217
  },
187
218
  commit(plan) {
@@ -191,13 +222,13 @@ export function createDistribution({ failAt, mattAdapter = createMattAdapter() }
191
222
  if (!refreshed.plan || refreshed.plan.preconditionDigest !== plan.preconditionDigest || refreshed.plan.catalogDigest !== plan.catalogDigest) return { ok: false, code: "PLAN_STALE" };
192
223
  if (!refreshed.ok) return { ok: false, code: refreshed.code, diagnostics: refreshed.diagnostics };
193
224
  if (plan.intent.dryRun) return { ok: true, code: "DRY_RUN", actions: plan.actions };
194
- const root = installationRoot(plan.intent.scope, plan.intent.projectRoot, plan.intent.home); const id = nowStamp(); const stagingRoot = path.join(root, "staging", id); const journalFile = transactionPath(plan.intent);
195
- const journal = {
196
- id, state: "in-progress", stagingRoot,
197
- manifest: { path: plan.manifestPath, existed: pathExists(plan.manifestPath), backup: null, written: false },
198
- config: { path: plan.configPath, existed: pathExists(plan.configPath), backup: null, written: false },
199
- operations: []
200
- };
225
+ const root = installationRoot(plan.intent.scope, plan.intent.projectRoot, plan.intent.home); const id = nowStamp(); const stagingRoot = path.join(root, "staging", id); const journalFile = transactionPath(plan.intent);
226
+ const journal = {
227
+ id, state: "in-progress", stagingRoot,
228
+ manifest: { path: plan.manifestPath, existed: pathExists(plan.manifestPath), backup: null, written: false },
229
+ config: { path: plan.configPath, existed: pathExists(plan.configPath), backup: null, written: false },
230
+ operations: []
231
+ };
201
232
  for (const item of refreshed.observations) if (plan.actions.find((entry) => entry.platform === item.id).matrix !== "keep") for (const skill of MATRIX_SKILLS) journal.operations.push({ target: path.join(item.root, skill), backup: null, status: "pending" });
202
233
  const persist = () => writeJson(journalFile, journal);
203
234
  try {
@@ -205,7 +236,7 @@ export function createDistribution({ failAt, mattAdapter = createMattAdapter() }
205
236
  for (const item of refreshed.observations) {
206
237
  if (plan.actions.find((entry) => entry.platform === item.id).matrix === "keep") continue;
207
238
  const platformStage = path.join(stagingRoot, item.id);
208
- for (const skill of MATRIX_SKILLS) { const staged = path.join(platformStage, skill); fs.mkdirSync(path.dirname(staged), { recursive: true }); fs.cpSync(path.join(catalog.root, skill), staged, { recursive: true }); fs.mkdirSync(path.join(staged, "scripts"), { recursive: true }); fs.copyFileSync(workflowRuntimeSource(), path.join(staged, "scripts", "matrix-runtime.mjs")); fs.copyFileSync(workflowTransactionSource(), path.join(staged, "scripts", "workflow-transaction.js")); if (skill === "matrix") for (const filename of ["matrix_state.py", "matrix_claude.py"]) fs.copyFileSync(path.join(sourceSkillsRoot("en"), "matrix", "scripts", filename), path.join(staged, "scripts", filename)); if (!exists(path.join(staged, "SKILL.md"))) throw new Error(`Stage validation failed: ${item.id}/${skill}`); }
239
+ for (const skill of MATRIX_SKILLS) { const staged = path.join(platformStage, skill); fs.mkdirSync(path.dirname(staged), { recursive: true }); fs.cpSync(path.join(catalog.root, skill), staged, { recursive: true }); fs.mkdirSync(path.join(staged, "scripts"), { recursive: true }); fs.copyFileSync(workflowRuntimeSource(), path.join(staged, "scripts", "matrix-runtime.mjs")); fs.copyFileSync(workflowTransactionSource(), path.join(staged, "scripts", "workflow-transaction.js")); if (skill === "matrix") for (const filename of ["matrix_state.py", "matrix_claude.py"]) fs.copyFileSync(path.join(sourceSkillsRoot("en"), "matrix", "scripts", filename), path.join(staged, "scripts", filename)); if (!exists(path.join(staged, "SKILL.md"))) throw new Error(`Stage validation failed: ${item.id}/${skill}`); }
209
240
  stagedPlatforms.set(item.id, platformStage);
210
241
  }
211
242
  failAt?.("after-stage"); let operationIndex = 0;
@@ -222,34 +253,34 @@ export function createDistribution({ failAt, mattAdapter = createMattAdapter() }
222
253
  operation.status = "committed"; persist(); failAt?.("after-commit", { platform: item.id, skill });
223
254
  }
224
255
  }
225
- const mattActions = plan.actions.filter((item) => item.matt.kind === "install-missing"); const mattResults = [];
226
- for (const action of mattActions) { const result = mattAdapter.installMissing({ projectRoot: plan.intent.projectRoot, platforms: [action.platform], skills: action.matt.skills, global: plan.intent.scope === "global" }); mattResults.push({ platform: action.platform, ...result }); }
227
- const postObservations = plan.intent.platforms.map((platform) => observePlatform(plan.intent, catalog, readManifest(plan.intent), platform));
228
- const archRequired = plan.intent.defaultOrchestration === "arch" || readManifest(plan.intent)?.orchestration?.available?.includes("arch");
229
- const incomplete = archRequired || mattActions.length ? postObservations.filter((item) => item.matt.state !== "complete") : [];
230
- const backups = journal.operations.filter((item) => item.backup).map((item) => ({ target: item.target, backup: item.backup }));
231
- if (mattResults.some((item) => !item.ok) || incomplete.length) {
232
- const restored = recoverPendingTransaction(plan.intent);
233
- return { ok: true, code: "PARTIAL", committed: [], backups: [], recovery: "The platform was not added. Repair the Arch Skill installation and rerun matrix init --with-mattpocock.", matt: mattResults, recoveryReceipt: restored.receipt ?? recovery.receipt };
234
- }
235
- if (journal.manifest.existed) { journal.manifest.backup = path.join(root, "backups", id, "installation.json"); persist(); fs.mkdirSync(path.dirname(journal.manifest.backup), { recursive: true }); fs.renameSync(plan.manifestPath, journal.manifest.backup); }
236
- if (journal.config.existed) { journal.config.backup = path.join(root, "backups", id, "config.yaml"); persist(); fs.mkdirSync(path.dirname(journal.config.backup), { recursive: true }); fs.renameSync(plan.configPath, journal.config.backup); }
237
- const records = Object.fromEntries(postObservations.map((item) => [item.id, {
238
- root: item.root,
239
- skills: Object.fromEntries(MATRIX_SKILLS.map((skill) => [skill, { hash: hashDirectory(path.join(item.root, skill)) }])),
240
- matt: { state: item.matt.state, skills: item.matt.skills }
241
- }]));
242
- const available = postObservations.every((item) => item.matt.state === "complete") ? ["prim", "arch"] : ["prim"];
243
- const previousConfig = readConfig(journal.config.backup ?? plan.configPath);
244
- fs.mkdirSync(path.dirname(plan.configPath), { recursive: true });
245
- fs.writeFileSync(plan.configPath, configContent(previousConfig, plan.intent.defaultOrchestration, plan.intent.scope)); journal.config.written = true; persist();
246
- writeJson(plan.manifestPath, {
247
- version: VERSION, catalogVersion: catalog.version, language: plan.intent.language, mode: plan.intent.mode, scope: plan.intent.scope,
248
- catalogDigest: catalog.digest, orchestration: { default: plan.intent.defaultOrchestration, available }, platforms: records
249
- });
250
- journal.manifest.written = true; persist(); failAt?.("after-manifest"); journal.state = "committed"; persist();
251
- removeStaging(stagingRoot); removeEntry(journal.manifest.backup); removeEntry(journal.config.backup); removeEntry(journalFile);
252
- return { ok: true, code: "OK", committed: journal.operations.map((item) => item.target), backups, matt: mattResults, recoveryReceipt: recovery.receipt };
256
+ const mattActions = plan.actions.filter((item) => item.matt.kind === "install-missing"); const mattResults = [];
257
+ for (const action of mattActions) { const result = mattAdapter.installMissing({ projectRoot: plan.intent.projectRoot, platforms: [action.platform], skills: action.matt.skills, global: plan.intent.scope === "global" }); mattResults.push({ platform: action.platform, ...result }); }
258
+ const postObservations = plan.intent.platforms.map((platform) => observePlatform(plan.intent, catalog, readManifest(plan.intent), platform));
259
+ const archRequired = plan.intent.defaultOrchestration === "arch" || readManifest(plan.intent)?.orchestration?.available?.includes("arch");
260
+ const incomplete = archRequired || mattActions.length ? postObservations.filter((item) => item.matt.state !== "complete") : [];
261
+ const backups = journal.operations.filter((item) => item.backup).map((item) => ({ target: item.target, backup: item.backup }));
262
+ if (mattResults.some((item) => !item.ok) || incomplete.length) {
263
+ const restored = recoverPendingTransaction(plan.intent);
264
+ return { ok: true, code: "PARTIAL", committed: [], backups: [], recovery: "The platform was not added. Repair the Arch Skill installation and rerun matrix init --with-mattpocock.", matt: mattResults, recoveryReceipt: restored.receipt ?? recovery.receipt };
265
+ }
266
+ if (journal.manifest.existed) { journal.manifest.backup = path.join(root, "backups", id, "installation.json"); persist(); fs.mkdirSync(path.dirname(journal.manifest.backup), { recursive: true }); fs.renameSync(plan.manifestPath, journal.manifest.backup); }
267
+ if (journal.config.existed) { journal.config.backup = path.join(root, "backups", id, "config.yaml"); persist(); fs.mkdirSync(path.dirname(journal.config.backup), { recursive: true }); fs.renameSync(plan.configPath, journal.config.backup); }
268
+ const records = Object.fromEntries(postObservations.map((item) => [item.id, {
269
+ root: item.root,
270
+ skills: Object.fromEntries(MATRIX_SKILLS.map((skill) => [skill, { hash: hashDirectory(path.join(item.root, skill)) }])),
271
+ matt: { state: item.matt.state, skills: item.matt.skills }
272
+ }]));
273
+ const available = postObservations.every((item) => item.matt.state === "complete") ? ["prim", "arch"] : ["prim"];
274
+ const previousConfig = readConfig(journal.config.backup ?? plan.configPath);
275
+ fs.mkdirSync(path.dirname(plan.configPath), { recursive: true });
276
+ fs.writeFileSync(plan.configPath, configContent(previousConfig, plan.intent.defaultOrchestration, plan.intent.scope, plan.intent.language)); journal.config.written = true; persist();
277
+ writeJson(plan.manifestPath, {
278
+ version: VERSION, catalogVersion: catalog.version, language: plan.intent.language, mode: plan.intent.mode, scope: plan.intent.scope,
279
+ catalogDigest: catalog.digest, orchestration: { default: plan.intent.defaultOrchestration, available }, platforms: records
280
+ });
281
+ journal.manifest.written = true; persist(); failAt?.("after-manifest"); journal.state = "committed"; persist();
282
+ removeStaging(stagingRoot); removeEntry(journal.manifest.backup); removeEntry(journal.config.backup); removeEntry(journalFile);
283
+ return { ok: true, code: "OK", committed: journal.operations.map((item) => item.target), backups, matt: mattResults, recoveryReceipt: recovery.receipt };
253
284
  } catch (error) { const restored = recoverPendingTransaction(plan.intent); return { ok: false, code: "MATRIX_COMMIT_FAILED", error: error.message, recovery: restored.ok ? "Matrix targets and installation manifest were restored." : restored.recovery }; }
254
285
  }
255
286
  };
package/src/messages.js CHANGED
@@ -1,13 +1,13 @@
1
1
  export const messages = {
2
2
  en: {
3
- title: "Evidence-driven workflow with Prim and Arch orchestration",
3
+ title: "Evidence-driven workflow with Prim and Arch orchestration",
4
4
  recommended: "Recommended setup", apply: "Apply", customize: "Customize", cancel: "Cancel",
5
5
  project: "Project (current directory)", global: "Global (all projects)",
6
6
  copy: "Copy (recommended)", symlink: "Symlink (advanced)",
7
7
  matrix: "Matrix", matt: "Matt Pocock skills", installed: "installed", ready: "Matrix is ready.",
8
8
  noPlatform: "No verified platform was detected. Select Claude Code, Codex, or both.",
9
9
  preview: "Planned changes", confirm: "Apply these changes?", doctor: "Installation diagnosis",
10
- languageQuestion: "Language", scopeQuestion: "Installation scope", platformsQuestion: "Platforms", orchestrationQuestion: "Default orchestration",
10
+ languageQuestion: "Language", scopeQuestion: "Installation scope", platformsQuestion: "Platforms", orchestrationQuestion: "Default orchestration",
11
11
  mattQuestion: "Install missing Matt Pocock skills?", replaceQuestion: "Existing Matrix files are not proven managed. Back up and replace?",
12
12
  yes: "Yes", no: "No", stateMissing: "missing", stateMatching: "up to date", stateComplete: "complete", stateUserModified: "user-modified", statePartial: "partial", stateOutdated: "outdated",
13
13
  actionInstall: "install", actionKeep: "keep", actionBackupReplace: "back up and replace", actionRepair: "repair", actionBlocked: "requires approval",
@@ -15,12 +15,12 @@ export const messages = {
15
15
  },
16
16
  "zh-CN": {
17
17
  manifestMissing: "当前安装范围没有 Matrix 安装清单。", transactionPending: "发现中断的 Matrix 安装事务。请再次运行 matrix init 后再应用新计划。", runtimeMissing: "缺少随附的 Matrix runtime。", brokenLink: "检测到断开的 Matrix 链接。", mattInherited: "Matt skills 由全局安装提供。",
18
- languageQuestion: "语言", scopeQuestion: "安装范围", platformsQuestion: "要配置的平台", orchestrationQuestion: "默认编排模式",
18
+ languageQuestion: "语言", scopeQuestion: "安装范围", platformsQuestion: "要配置的平台", orchestrationQuestion: "默认编排模式",
19
19
  mattQuestion: "是否安装缺失的 Matt Pocock skills?", replaceQuestion: "现有 Matrix 文件无法确认由 Matrix 管理。是否备份并替换?",
20
20
  yes: "是", no: "否", stateMissing: "未安装", stateMatching: "已是最新", stateComplete: "完整", stateUserModified: "已被用户修改", statePartial: "不完整", stateOutdated: "可更新",
21
21
  actionInstall: "安装", actionKeep: "保留", actionBackupReplace: "备份并替换", actionRepair: "修复", actionBlocked: "需要确认",
22
22
  dryRunComplete: "预览完成。",
23
- title: "提供 Prim 与 Arch 编排的证据驱动工作流",
23
+ title: "提供 Prim 与 Arch 编排的证据驱动工作流",
24
24
  recommended: "推荐配置", apply: "开始安装", customize: "自定义", cancel: "取消",
25
25
  project: "项目(当前目录)", global: "全局(所有项目)",
26
26
  copy: "复制(推荐)", symlink: "符号链接(高级)",
@@ -0,0 +1,81 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { spawnSync } from "node:child_process";
5
+
6
+ export const PACKAGE_NAME = "@rosenfan/matrix";
7
+ const REGISTRY = "https://registry.npmjs.org/@rosenfan%2fmatrix/latest";
8
+
9
+ function parse(version) {
10
+ const match = String(version ?? "").match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/);
11
+ return match ? { numeric: match.slice(1, 4).map(Number), prerelease: match[4] ?? null } : null;
12
+ }
13
+
14
+ export function compareVersions(left, right) {
15
+ const a = parse(left); const b = parse(right);
16
+ if (!a || !b) return null;
17
+ for (let index = 0; index < 3; index += 1) if (a.numeric[index] !== b.numeric[index]) return a.numeric[index] - b.numeric[index];
18
+ if (a.prerelease === b.prerelease) return 0;
19
+ if (a.prerelease === null) return 1;
20
+ if (b.prerelease === null) return -1;
21
+ return a.prerelease.localeCompare(b.prerelease, undefined, { numeric: true });
22
+ }
23
+
24
+ export async function latestVersion(fetchImpl = fetch) {
25
+ const response = await fetchImpl(REGISTRY, { headers: { accept: "application/json" } });
26
+ if (!response.ok) throw new Error(`registry returned ${response.status}`);
27
+ const body = await response.json();
28
+ if (!parse(body.version)) throw new Error("registry returned an invalid version");
29
+ return body.version;
30
+ }
31
+
32
+ function run(command, args, options = {}) {
33
+ const result = spawnSync(command, args, { encoding: "utf8", ...options });
34
+ return { ok: !result.error && result.status === 0, status: result.status, error: result.error?.message, stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
35
+ }
36
+
37
+ export function validateCandidate(packageRoot, expectedVersion) {
38
+ const manifestFile = path.join(packageRoot, "package.json");
39
+ try {
40
+ const manifest = JSON.parse(fs.readFileSync(manifestFile, "utf8"));
41
+ if (manifest.name !== PACKAGE_NAME || manifest.version !== expectedVersion) return { ok: false, reason: "candidate package identity mismatch" };
42
+ for (const directory of [".claude/skills", "assets/skills-zh-CN", "src", "bin"]) if (!fs.existsSync(path.join(packageRoot, directory))) return { ok: false, reason: `candidate missing ${directory}` };
43
+ for (const languageRoot of [path.join(packageRoot, ".claude", "skills"), path.join(packageRoot, "assets", "skills-zh-CN")]) if (!fs.existsSync(path.join(languageRoot, "matrix", "SKILL.md"))) return { ok: false, reason: "candidate Matrix assets are incomplete" };
44
+ for (const args of [["--version"], ["--help"], ["workflow", "doctor", "--json"]]) {
45
+ const result = run(process.execPath, [path.join(packageRoot, "bin", "matrix.js"), ...args], { cwd: os.tmpdir() });
46
+ if (!result.ok) return { ok: false, reason: `candidate command failed: matrix ${args.join(" ")}` };
47
+ }
48
+ return { ok: true };
49
+ } catch (error) { return { ok: false, reason: error.message }; }
50
+ }
51
+
52
+ export function npmGlobalRoot() {
53
+ const result = run("npm", ["root", "-g"]);
54
+ if (!result.ok) throw new Error(result.error ?? result.stderr.trim() ?? "npm root -g failed");
55
+ return result.stdout.trim();
56
+ }
57
+
58
+ export function validateRegistryCandidate(version) {
59
+ const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "matrix-update-"));
60
+ try {
61
+ const install = run("npm", ["install", "--prefix", temporary, "--ignore-scripts", "--no-save", "--package-lock=false", `${PACKAGE_NAME}@${version}`], { cwd: temporary });
62
+ if (!install.ok) return { ok: false, reason: install.error ?? install.stderr.trim() ?? "candidate installation failed" };
63
+ return validateCandidate(path.join(temporary, "node_modules", "@rosenfan", "matrix"), version);
64
+ } finally { fs.rmSync(temporary, { recursive: true, force: true }); }
65
+ }
66
+
67
+ export function installGlobal(version) {
68
+ const result = run("npm", ["install", "--global", `${PACKAGE_NAME}@${version}`]);
69
+ return result.ok ? { ok: true } : { ok: false, reason: result.error ?? result.stderr.trim() ?? "npm install failed" };
70
+ }
71
+
72
+ export function selfUpdate(currentVersion, targetVersion) {
73
+ const candidate = validateRegistryCandidate(targetVersion);
74
+ if (!candidate.ok) return candidate;
75
+ const install = installGlobal(targetVersion);
76
+ if (install.ok) return install;
77
+ const rollback = installGlobal(currentVersion);
78
+ return { ok: false, reason: `${install.reason}; ${rollback.ok ? `restored ${currentVersion}` : `rollback failed: ${rollback.reason}`}` };
79
+ }
80
+
81
+ export function globalBinPath() { return path.join(npmGlobalRoot(), "@rosenfan", "matrix", "bin", "matrix.js"); }
package/src/ui.js CHANGED
@@ -53,7 +53,7 @@ export function createUi({ color = stdout.isTTY, animate = true } = {}) {
53
53
  stdout.write(`\u001b[${bannerLines}A`);
54
54
  }
55
55
  console.log(paint("cyan", logo));
56
- console.log(paint("yellow", "Evidence-driven workflow with Prim and Arch orchestration"));
56
+ console.log(paint("yellow", "Evidence-driven workflow with Prim and Arch orchestration"));
57
57
  console.log();
58
58
  },
59
59
  info(message) { console.log(`${paint("blue", "> ")} ${message}`); },