@rosenfan/matrix 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/matrix/SKILL.md +62 -21
- package/.claude/skills/matrix/agents/openai.yaml +4 -4
- package/.claude/skills/matrix/scripts/matrix_claude.py +11 -166
- package/.claude/skills/matrix/scripts/matrix_state.py +60 -114
- package/.claude/skills/matrix-archive/SKILL.md +33 -26
- package/.claude/skills/matrix-build/SKILL.md +26 -39
- package/.claude/skills/matrix-claude/SKILL.md +22 -22
- package/.claude/skills/matrix-design/SKILL.md +28 -29
- package/.claude/skills/matrix-hotfix/SKILL.md +8 -8
- package/.claude/skills/matrix-open/SKILL.md +17 -20
- package/.claude/skills/matrix-status/SKILL.md +4 -3
- package/.claude/skills/matrix-tweak/SKILL.md +8 -8
- package/.claude/skills/matrix-verify/SKILL.md +18 -29
- package/LICENSE +21 -21
- package/README.md +255 -242
- package/README.zh-CN.md +254 -242
- package/assets/skills-zh-CN/matrix/SKILL.md +57 -5
- package/assets/skills-zh-CN/matrix-archive/SKILL.md +23 -2
- package/assets/skills-zh-CN/matrix-build/SKILL.md +26 -2
- package/assets/skills-zh-CN/matrix-claude/SKILL.md +12 -11
- package/assets/skills-zh-CN/matrix-design/SKILL.md +16 -11
- package/assets/skills-zh-CN/matrix-hotfix/SKILL.md +8 -8
- package/assets/skills-zh-CN/matrix-open/SKILL.md +16 -11
- package/assets/skills-zh-CN/matrix-status/SKILL.md +12 -11
- package/assets/skills-zh-CN/matrix-tweak/SKILL.md +8 -8
- package/assets/skills-zh-CN/matrix-verify/SKILL.md +22 -8
- package/bin/matrix.js +15 -15
- package/package.json +56 -56
- package/src/catalog.js +85 -80
- package/src/cli.js +116 -111
- package/src/distribution.js +216 -152
- package/src/installer.js +45 -45
- package/src/matt.js +45 -34
- package/src/messages.js +33 -33
- package/src/ui.js +89 -89
- package/src/workflow-transaction.js +622 -0
- package/src/workflow.js +547 -74
package/src/distribution.js
CHANGED
|
@@ -1,192 +1,256 @@
|
|
|
1
|
-
import crypto from "node:crypto";
|
|
2
|
-
import fs from "node:fs";
|
|
3
|
-
import os from "node:os";
|
|
4
|
-
import path from "node:path";
|
|
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
5
|
import {
|
|
6
|
-
MATRIX_SKILLS, MATT_SKILLS, PLATFORM_IDS, PLATFORMS, hashDirectory,
|
|
7
|
-
installationRoot, manifestPath, releaseCatalog, scopeBase, sourceSkillsRoot, workflowRuntimeSource
|
|
6
|
+
MATRIX_SKILLS, MATT_SKILLS, UNMANAGED_MATT_SKILLS, PLATFORM_IDS, PLATFORMS, hashDirectory,
|
|
7
|
+
installationRoot, manifestPath, releaseCatalog, scopeBase, sourceSkillsRoot, workflowRuntimeSource, workflowTransactionSource
|
|
8
8
|
} from "./catalog.js";
|
|
9
|
-
import { createMattAdapter } from "./matt.js";
|
|
10
|
-
import { text } from "./messages.js";
|
|
11
|
-
|
|
12
|
-
const VERSION =
|
|
13
|
-
const digest = (value) => crypto.createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
|
14
|
-
const exists = (value) => fs.existsSync(value);
|
|
15
|
-
const pathExists = (value) => { try { fs.lstatSync(value); return true; } catch { return false; } };
|
|
16
|
-
const nowStamp = () => new Date().toISOString().replace(/[:.]/g, "-");
|
|
9
|
+
import { createMattAdapter } from "./matt.js";
|
|
10
|
+
import { text } from "./messages.js";
|
|
11
|
+
|
|
12
|
+
const VERSION = 2;
|
|
13
|
+
const digest = (value) => crypto.createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
|
14
|
+
const exists = (value) => fs.existsSync(value);
|
|
15
|
+
const pathExists = (value) => { try { fs.lstatSync(value); return true; } catch { return false; } };
|
|
16
|
+
const nowStamp = () => new Date().toISOString().replace(/[:.]/g, "-");
|
|
17
17
|
const transactionPath = (intent) => path.join(installationRoot(intent.scope, intent.projectRoot, intent.home), "transaction.json");
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
fs.
|
|
23
|
-
|
|
18
|
+
const configPath = (intent) => path.join(intent.projectRoot, ".matrix", "config.yaml");
|
|
19
|
+
const writeJson = (file, value) => {
|
|
20
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
21
|
+
const temporary = `${file}.${process.pid}.${Date.now()}.tmp`;
|
|
22
|
+
fs.writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`);
|
|
23
|
+
fs.renameSync(temporary, file);
|
|
24
|
+
};
|
|
24
25
|
const readJson = (file) => { try { return JSON.parse(fs.readFileSync(file, "utf8")); } catch { return null; } };
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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 {}; }
|
|
30
33
|
};
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
for (let index = 0; index < Math.max(a.length, b.length); index += 1) if ((a[index] ?? 0) !== (b[index] ?? 0)) return (a[index] ?? 0) > (b[index] ?? 0) ? 1 : -1;
|
|
35
|
-
return 0;
|
|
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`;
|
|
36
37
|
};
|
|
37
|
-
const
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
38
|
+
const removeEntry = (value) => { if (pathExists(value)) fs.rmSync(value, { recursive: true, force: true }); };
|
|
39
|
+
const removeStaging = (stagingRoot) => {
|
|
40
|
+
removeEntry(stagingRoot);
|
|
41
|
+
const parent = path.dirname(stagingRoot);
|
|
42
|
+
if (exists(parent) && fs.readdirSync(parent).length === 0) fs.rmdirSync(parent);
|
|
43
|
+
};
|
|
44
|
+
const compareVersions = (left, right) => {
|
|
45
|
+
const parse = (value) => String(value ?? "0.0.0").split("-")[0].split(".").map((part) => Number(part) || 0);
|
|
46
|
+
const a = parse(left); const b = parse(right);
|
|
47
|
+
for (let index = 0; index < Math.max(a.length, b.length); index += 1) if ((a[index] ?? 0) !== (b[index] ?? 0)) return (a[index] ?? 0) > (b[index] ?? 0) ? 1 : -1;
|
|
48
|
+
return 0;
|
|
49
|
+
};
|
|
50
|
+
const skillPresent = (root, skill) => exists(path.join(root, skill, "SKILL.md"));
|
|
51
|
+
const isBrokenLink = (value) => { try { return fs.lstatSync(value).isSymbolicLink() && !exists(value); } catch { return false; } };
|
|
52
|
+
|
|
53
|
+
function readManifest(intent) { return readJson(manifestPath(intent.scope, intent.projectRoot, intent.home)); }
|
|
54
|
+
|
|
55
|
+
function detectPlatforms(projectRoot, scope, home) {
|
|
56
|
+
const bases = [scopeBase(scope, projectRoot, home)];
|
|
57
|
+
if (scope === "project") bases.push(home);
|
|
58
|
+
return PLATFORM_IDS.filter((id) => bases.some((base) => exists(PLATFORMS[id].skillRoot(base))));
|
|
59
|
+
}
|
|
60
|
+
|
|
48
61
|
function normalizeIntent(raw) {
|
|
49
|
-
const scope = raw.scope ?? "project";
|
|
50
|
-
const platforms = [...new Set(raw.platforms ?? [])];
|
|
51
|
-
if (!["project", "global"].includes(scope)) return { error: "INVALID_INTENT", detail: "scope" };
|
|
52
|
-
if (!["copy", "symlink"].includes(raw.mode ?? "copy")) return { error: "INVALID_INTENT", detail: "mode" };
|
|
62
|
+
const scope = raw.scope ?? "project";
|
|
63
|
+
const platforms = [...new Set(raw.platforms ?? [])];
|
|
64
|
+
if (!["project", "global"].includes(scope)) return { error: "INVALID_INTENT", detail: "scope" };
|
|
65
|
+
if (!["copy", "symlink"].includes(raw.mode ?? "copy")) return { error: "INVALID_INTENT", detail: "mode" };
|
|
53
66
|
if (!["en", "zh-CN"].includes(raw.language ?? "en")) return { error: "INVALID_INTENT", detail: "language" };
|
|
54
|
-
if (
|
|
55
|
-
|
|
67
|
+
if (raw.defaultOrchestration != null && !["prim", "arch"].includes(raw.defaultOrchestration)) return { error: "INVALID_INTENT", detail: "default-orchestration" };
|
|
68
|
+
if (platforms.some((id) => !PLATFORM_IDS.includes(id))) return { error: "UNVERIFIED_PLATFORM" };
|
|
69
|
+
return { projectRoot: path.resolve(raw.projectRoot ?? process.cwd()), home: raw.home ?? os.homedir(), scope, platforms,
|
|
56
70
|
language: raw.language ?? "en", mode: raw.mode ?? "copy", matrixPolicy: raw.matrixPolicy ?? "safe",
|
|
57
|
-
matt: raw.matt ?? "none",
|
|
71
|
+
matt: raw.matt ?? "none", defaultOrchestration: raw.defaultOrchestration ?? null,
|
|
72
|
+
nonInteractive: Boolean(raw.nonInteractive), dryRun: Boolean(raw.dryRun) };
|
|
58
73
|
}
|
|
59
|
-
|
|
60
|
-
function observePlatform(intent, catalog, manifest, platformId) {
|
|
61
|
-
const platform = PLATFORMS[platformId]; const base = scopeBase(intent.scope, intent.projectRoot, intent.home);
|
|
62
|
-
const root = platform.skillRoot(base); const records = manifest?.platforms?.[platformId]?.skills ?? {};
|
|
63
|
-
const skills = MATRIX_SKILLS.map((skill) => {
|
|
64
|
-
const target = path.join(root, skill);
|
|
65
|
-
if (!pathExists(target)) return { skill, target, state: "missing" };
|
|
66
|
-
if (isBrokenLink(target) || !exists(path.join(target, "SKILL.md"))) return { skill, target, state: "damaged" };
|
|
67
|
-
const actual = hashDirectory(target); const recorded = records[skill]?.hash; const expected = catalog.skills[skill];
|
|
68
|
-
if (!recorded) return { skill, target, state: "user-modified", actual };
|
|
69
|
-
if (actual === expected) return { skill, target, state: "matching", actual };
|
|
70
|
-
if (actual === recorded) return { skill, target, state: compareVersions(manifest?.catalogVersion, catalog.version) > 0 ? "newer" : "outdated-unchanged", actual };
|
|
71
|
-
return { skill, target, state: "user-modified", actual };
|
|
72
|
-
});
|
|
73
|
-
const states = new Set(skills.map((item) => item.state));
|
|
74
|
-
const matrix = states.size === 1 && states.has("matching") ? "matching" : states.has("user-modified") ? "user-modified"
|
|
75
|
-
: states.has("missing") && states.size === 1 ? "missing" : states.has("newer") ? "newer"
|
|
76
|
-
: states.has("damaged") ? "damaged" : states.has("outdated-unchanged") ? "outdated-unchanged" : "partial";
|
|
77
|
-
const globalRoot = platform.skillRoot(intent.home);
|
|
74
|
+
|
|
75
|
+
function observePlatform(intent, catalog, manifest, platformId) {
|
|
76
|
+
const platform = PLATFORMS[platformId]; const base = scopeBase(intent.scope, intent.projectRoot, intent.home);
|
|
77
|
+
const root = platform.skillRoot(base); const records = manifest?.platforms?.[platformId]?.skills ?? {};
|
|
78
|
+
const skills = MATRIX_SKILLS.map((skill) => {
|
|
79
|
+
const target = path.join(root, skill);
|
|
80
|
+
if (!pathExists(target)) return { skill, target, state: "missing" };
|
|
81
|
+
if (isBrokenLink(target) || !exists(path.join(target, "SKILL.md"))) return { skill, target, state: "damaged" };
|
|
82
|
+
const actual = hashDirectory(target); const recorded = records[skill]?.hash; const expected = catalog.skills[skill];
|
|
83
|
+
if (!recorded) return { skill, target, state: "user-modified", actual };
|
|
84
|
+
if (actual === expected) return { skill, target, state: "matching", actual };
|
|
85
|
+
if (actual === recorded) return { skill, target, state: compareVersions(manifest?.catalogVersion, catalog.version) > 0 ? "newer" : "outdated-unchanged", actual };
|
|
86
|
+
return { skill, target, state: "user-modified", actual };
|
|
87
|
+
});
|
|
88
|
+
const states = new Set(skills.map((item) => item.state));
|
|
89
|
+
const matrix = states.size === 1 && states.has("matching") ? "matching" : states.has("user-modified") ? "user-modified"
|
|
90
|
+
: states.has("missing") && states.size === 1 ? "missing" : states.has("newer") ? "newer"
|
|
91
|
+
: states.has("damaged") ? "damaged" : states.has("outdated-unchanged") ? "outdated-unchanged" : "partial";
|
|
92
|
+
const globalRoot = platform.skillRoot(intent.home);
|
|
78
93
|
const mattPresent = MATT_SKILLS.filter((skill) => skillPresent(root, skill));
|
|
79
94
|
const globalMatt = intent.scope === "project" ? MATT_SKILLS.filter((skill) => skillPresent(globalRoot, skill)) : [];
|
|
80
95
|
const availableMatt = new Set([...mattPresent, ...globalMatt]); const missing = MATT_SKILLS.filter((skill) => !availableMatt.has(skill));
|
|
81
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)));
|
|
82
102
|
return { id: platformId, name: platform.name, root, skills, matrix, matt: { present: mattPresent.length, missing, total: MATT_SKILLS.length, inherited,
|
|
83
|
-
state: availableMatt.size === MATT_SKILLS.length ? "complete" : availableMatt.size ? "partial" : "missing" } };
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function actionFor(observation, policy) {
|
|
87
|
-
if (observation.matrix === "matching" || observation.matrix === "newer") return "keep";
|
|
88
|
-
if (observation.matrix === "missing") return "install";
|
|
89
|
-
if (observation.matrix === "outdated-unchanged") return "backup-replace";
|
|
90
|
-
if (["damaged", "user-modified"].includes(observation.matrix)) return policy === "replace" ? "backup-replace" : "blocked";
|
|
91
|
-
return "repair";
|
|
103
|
+
state: availableMatt.size === MATT_SKILLS.length ? "complete" : availableMatt.size ? "partial" : "missing", skills: managedSkills, unmanaged } };
|
|
92
104
|
}
|
|
93
|
-
|
|
105
|
+
|
|
106
|
+
function actionFor(observation, policy) {
|
|
107
|
+
if (observation.matrix === "matching" || observation.matrix === "newer") return "keep";
|
|
108
|
+
if (observation.matrix === "missing") return "install";
|
|
109
|
+
if (observation.matrix === "outdated-unchanged") return "backup-replace";
|
|
110
|
+
if (["damaged", "user-modified"].includes(observation.matrix)) return policy === "replace" ? "backup-replace" : "blocked";
|
|
111
|
+
return "repair";
|
|
112
|
+
}
|
|
113
|
+
|
|
94
114
|
export function recoverPendingTransaction(intent) {
|
|
95
115
|
const file = transactionPath(intent); const journal = readJson(file);
|
|
96
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
|
+
};
|
|
97
125
|
try {
|
|
98
126
|
if (journal.state === "committed") {
|
|
99
127
|
removeStaging(journal.stagingRoot);
|
|
100
128
|
for (const operation of journal.operations ?? []) removeEntry(operation.backup);
|
|
101
|
-
removeEntry(journal.manifest?.backup); removeEntry(file);
|
|
129
|
+
removeEntry(journal.manifest?.backup); removeEntry(journal.config?.backup); removeEntry(file);
|
|
102
130
|
return { ok: true, recovered: true, receipt: `Finalized completed Matrix transaction ${journal.id}.` };
|
|
103
|
-
}
|
|
104
|
-
for (const operation of [...(journal.operations ?? [])].reverse()) {
|
|
105
|
-
if (operation.status !== "pending") removeEntry(operation.target);
|
|
106
|
-
if (operation.backup && pathExists(operation.backup)) { fs.mkdirSync(path.dirname(operation.target), { recursive: true }); fs.renameSync(operation.backup, operation.target); }
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
export function createDistribution({ failAt, mattAdapter = createMattAdapter() } = {}) {
|
|
131
|
+
}
|
|
132
|
+
for (const operation of [...(journal.operations ?? [])].reverse()) {
|
|
133
|
+
if (operation.status !== "pending") removeEntry(operation.target);
|
|
134
|
+
if (operation.backup && pathExists(operation.backup)) { fs.mkdirSync(path.dirname(operation.target), { recursive: true }); fs.renameSync(operation.backup, operation.target); }
|
|
135
|
+
}
|
|
136
|
+
restoreMetadata(journal.config);
|
|
137
|
+
restoreMetadata(journal.manifest);
|
|
138
|
+
removeStaging(journal.stagingRoot); removeEntry(file);
|
|
139
|
+
return { ok: true, recovered: true, receipt: `Recovered interrupted Matrix transaction ${journal.id}.` };
|
|
140
|
+
} catch (error) { return { ok: false, code: "RECOVERY_REQUIRED", recovery: `Keep ${file} and restore the paths recorded there before retrying.`, error: error.message }; }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function createDistribution({ failAt, mattAdapter = createMattAdapter() } = {}) {
|
|
117
144
|
const evaluate = (rawIntent) => {
|
|
118
145
|
const intent = normalizeIntent(rawIntent); const language = intent.language ?? rawIntent.language ?? "en";
|
|
119
146
|
if (intent.error) return { ok: false, code: intent.error, diagnostics: [{ code: intent.error, message: text(language, "noPlatform") }] };
|
|
120
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; }
|
|
121
148
|
let catalog; try { catalog = releaseCatalog(intent.language); } catch (error) { return { ok: false, code: "PACKAGE_INCOMPLETE", intent, diagnostics: [{ code: "PACKAGE_INCOMPLETE", message: error.message }] }; }
|
|
122
|
-
const manifest = readManifest(intent);
|
|
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");
|
|
123
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 } }));
|
|
124
|
-
const blocked = actions.filter((item) => item.matrix === "blocked");
|
|
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) };
|
|
125
161
|
plan.seal = digest({ intent, catalogDigest: plan.catalogDigest, actions, preconditionDigest: plan.preconditionDigest });
|
|
126
|
-
|
|
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(", ")}` };
|
|
127
167
|
};
|
|
128
|
-
|
|
129
|
-
return { evaluate,
|
|
130
|
-
diagnose(rawIntent) {
|
|
131
|
-
const evaluation = evaluate(rawIntent); if (!evaluation.intent) return evaluation;
|
|
132
|
-
const diagnostics = []; const pending = readJson(transactionPath(evaluation.intent)); const manifest = readManifest(evaluation.intent);
|
|
133
|
-
if (pending) diagnostics.push({ code: "RECOVERY_REQUIRED", message: text(evaluation.intent.language, "transactionPending") });
|
|
134
|
-
if (!manifest) diagnostics.push({ code: "MANIFEST_MISSING", message: text(evaluation.intent.language, "manifestMissing") });
|
|
135
|
-
for (const observation of evaluation.observations ?? []) {
|
|
136
|
-
diagnostics.push({ code: `MATRIX_${observation.matrix.toUpperCase().replaceAll("-", "_")}`, platform: observation.id, message: `${observation.name}: Matrix is ${observation.matrix}.` });
|
|
137
|
-
const matrixRoot = path.join(observation.root, "matrix");
|
|
138
|
-
if (pathExists(matrixRoot) && (!exists(matrixRoot) || !exists(path.join(matrixRoot, "scripts", "matrix-runtime.mjs")))) diagnostics.push({ code: "RUNTIME_MISSING", platform: observation.id, message: `${observation.name}: ${text(evaluation.intent.language, "runtimeMissing")}` });
|
|
139
|
-
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}` });
|
|
140
|
-
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." });
|
|
168
|
+
|
|
169
|
+
return { evaluate,
|
|
170
|
+
diagnose(rawIntent) {
|
|
171
|
+
const evaluation = evaluate(rawIntent); if (!evaluation.intent) return evaluation;
|
|
172
|
+
const diagnostics = []; const pending = readJson(transactionPath(evaluation.intent)); const manifest = readManifest(evaluation.intent);
|
|
173
|
+
if (pending) diagnostics.push({ code: "RECOVERY_REQUIRED", message: text(evaluation.intent.language, "transactionPending") });
|
|
174
|
+
if (!manifest) diagnostics.push({ code: "MANIFEST_MISSING", message: text(evaluation.intent.language, "manifestMissing") });
|
|
175
|
+
for (const observation of evaluation.observations ?? []) {
|
|
176
|
+
diagnostics.push({ code: `MATRIX_${observation.matrix.toUpperCase().replaceAll("-", "_")}`, platform: observation.id, message: `${observation.name}: Matrix is ${observation.matrix}.` });
|
|
177
|
+
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")}` });
|
|
179
|
+
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
|
+
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." });
|
|
141
181
|
diagnostics.push({ code: `MATT_${observation.matt.state.toUpperCase()}`, platform: observation.id, message: `${observation.name}: Matt Pocock skills are ${observation.matt.state}.` });
|
|
142
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(", ")}.` });
|
|
143
184
|
}
|
|
144
|
-
return { ...evaluation, diagnosis: diagnostics };
|
|
145
|
-
},
|
|
146
|
-
commit(plan) {
|
|
147
|
-
if (!plan?.seal || plan.seal !== digest({ intent: plan.intent, catalogDigest: plan.catalogDigest, actions: plan.actions, preconditionDigest: plan.preconditionDigest })) return { ok: false, code: "INVALID_INTENT" };
|
|
148
|
-
const recovery = recoverPendingTransaction(plan.intent); if (!recovery.ok) return recovery;
|
|
149
|
-
const refreshed = evaluate({ ...plan.intent, platforms: plan.intent.platforms });
|
|
150
|
-
if (!refreshed.plan || refreshed.plan.preconditionDigest !== plan.preconditionDigest || refreshed.plan.catalogDigest !== plan.catalogDigest) return { ok: false, code: "PLAN_STALE" };
|
|
151
|
-
if (!refreshed.ok) return { ok: false, code: refreshed.code, diagnostics: refreshed.diagnostics };
|
|
152
|
-
if (plan.intent.dryRun) return { ok: true, code: "DRY_RUN", actions: plan.actions };
|
|
185
|
+
return { ...evaluation, diagnosis: diagnostics };
|
|
186
|
+
},
|
|
187
|
+
commit(plan) {
|
|
188
|
+
if (!plan?.seal || plan.seal !== digest({ intent: plan.intent, catalogDigest: plan.catalogDigest, actions: plan.actions, preconditionDigest: plan.preconditionDigest })) return { ok: false, code: "INVALID_INTENT" };
|
|
189
|
+
const recovery = recoverPendingTransaction(plan.intent); if (!recovery.ok) return recovery;
|
|
190
|
+
const refreshed = evaluate({ ...plan.intent, platforms: plan.intent.platforms });
|
|
191
|
+
if (!refreshed.plan || refreshed.plan.preconditionDigest !== plan.preconditionDigest || refreshed.plan.catalogDigest !== plan.catalogDigest) return { ok: false, code: "PLAN_STALE" };
|
|
192
|
+
if (!refreshed.ok) return { ok: false, code: refreshed.code, diagnostics: refreshed.diagnostics };
|
|
193
|
+
if (plan.intent.dryRun) return { ok: true, code: "DRY_RUN", actions: plan.actions };
|
|
153
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);
|
|
154
|
-
const journal = {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
operation.
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
+
};
|
|
201
|
+
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
|
+
const persist = () => writeJson(journalFile, journal);
|
|
203
|
+
try {
|
|
204
|
+
persist(); const catalog = releaseCatalog(plan.intent.language); const stagedPlatforms = new Map();
|
|
205
|
+
for (const item of refreshed.observations) {
|
|
206
|
+
if (plan.actions.find((entry) => entry.platform === item.id).matrix === "keep") continue;
|
|
207
|
+
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}`); }
|
|
209
|
+
stagedPlatforms.set(item.id, platformStage);
|
|
210
|
+
}
|
|
211
|
+
failAt?.("after-stage"); let operationIndex = 0;
|
|
212
|
+
for (const item of refreshed.observations) {
|
|
213
|
+
if (plan.actions.find((entry) => entry.platform === item.id).matrix === "keep") continue;
|
|
214
|
+
fs.mkdirSync(item.root, { recursive: true });
|
|
215
|
+
for (const skill of MATRIX_SKILLS) {
|
|
216
|
+
const operation = journal.operations[operationIndex++]; const target = operation.target; operation.status = "backing-up";
|
|
217
|
+
if (pathExists(target)) operation.backup = path.join(root, "backups", id, item.id, skill); persist();
|
|
218
|
+
if (operation.backup) { fs.mkdirSync(path.dirname(operation.backup), { recursive: true }); fs.renameSync(target, operation.backup); }
|
|
219
|
+
operation.status = "backed-up"; persist(); failAt?.("after-backup", { platform: item.id, skill });
|
|
220
|
+
const staged = path.join(stagedPlatforms.get(item.id), skill);
|
|
221
|
+
if (plan.intent.mode === "symlink") { const store = path.join(root, "skills", catalog.digest, skill); if (!pathExists(store)) { fs.mkdirSync(path.dirname(store), { recursive: true }); fs.renameSync(staged, store); } fs.symlinkSync(store, target, process.platform === "win32" ? "junction" : "dir"); } else fs.renameSync(staged, target);
|
|
222
|
+
operation.status = "committed"; persist(); failAt?.("after-commit", { platform: item.id, skill });
|
|
223
|
+
}
|
|
224
|
+
}
|
|
183
225
|
const mattActions = plan.actions.filter((item) => item.matt.kind === "install-missing"); const mattResults = [];
|
|
184
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 }); }
|
|
185
|
-
const
|
|
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") : [];
|
|
186
230
|
const backups = journal.operations.filter((item) => item.backup).map((item) => ({ target: item.target, backup: item.backup }));
|
|
187
|
-
if (mattResults.some((item) => !item.ok) || incomplete.length)
|
|
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);
|
|
188
252
|
return { ok: true, code: "OK", committed: journal.operations.map((item) => item.target), backups, matt: mattResults, recoveryReceipt: recovery.receipt };
|
|
189
|
-
} 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 }; }
|
|
190
|
-
}
|
|
191
|
-
};
|
|
192
|
-
}
|
|
253
|
+
} 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
|
+
}
|
|
255
|
+
};
|
|
256
|
+
}
|
package/src/installer.js
CHANGED
|
@@ -1,45 +1,45 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { createDistribution } from "./distribution.js";
|
|
4
|
-
import { MATRIX_SKILLS, MATT_SKILLS, PLATFORMS, sourceSkillsRoot } from "./catalog.js";
|
|
5
|
-
import { createMattAdapter } from "./matt.js";
|
|
6
|
-
|
|
7
|
-
export { MATRIX_SKILLS, MATT_SKILLS, sourceSkillsRoot };
|
|
8
|
-
|
|
9
|
-
export function destinationFor({ scope, projectRoot, platform = "claude-code", home }) {
|
|
10
|
-
const base = scope === "global" ? (home ?? process.env.USERPROFILE ?? process.env.HOME) : projectRoot;
|
|
11
|
-
return PLATFORMS[platform].skillRoot(base);
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function inspectInstallation(destination) {
|
|
15
|
-
const inspect = (names) => names.filter((skill) => {
|
|
16
|
-
try { return Boolean(destination) && requireExists(destination, skill); } catch { return false; }
|
|
17
|
-
});
|
|
18
|
-
const installed = inspect(MATRIX_SKILLS);
|
|
19
|
-
const mattInstalled = inspect(MATT_SKILLS);
|
|
20
|
-
return { installed, missing: MATRIX_SKILLS.filter((skill) => !installed.includes(skill)), mattInstalled, mattMissing: MATT_SKILLS.filter((skill) => !mattInstalled.includes(skill)) };
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function requireExists(destination, skill) {
|
|
24
|
-
// Avoid exposing a second installation policy through this legacy helper.
|
|
25
|
-
return fs.existsSync(path.join(destination, skill, "SKILL.md"));
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function installMatrix({ destination, mode = "copy", conflict = "error", dryRun = false }) {
|
|
29
|
-
const platform = destination?.includes(".agents") ? "codex" : "claude-code";
|
|
30
|
-
const projectRoot = path.resolve(destination, "..", "..");
|
|
31
|
-
const distribution = createDistribution();
|
|
32
|
-
const evaluation = distribution.evaluate({ projectRoot, scope: "project", platforms: [platform], mode, matrixPolicy: conflict === "replace" ? "replace" : "safe", dryRun });
|
|
33
|
-
if (!evaluation.ok) throw new Error(evaluation.diagnostics?.[0]?.message ?? evaluation.code);
|
|
34
|
-
const result = distribution.commit(evaluation.plan);
|
|
35
|
-
if (!result.ok) throw new Error(result.error ?? result.code);
|
|
36
|
-
return { installed: result.committed ?? MATRIX_SKILLS, skipped: [], existing: [], backupRoot: result.backups?.[0] ? result.backups[0].backup : null };
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function installMattSkills({ projectRoot, platforms = ["claude-code"], skills = MATT_SKILLS, global = false, timeout = 300_000 }) {
|
|
40
|
-
const result = createMattAdapter().installMissing({ projectRoot, platforms, skills, global, timeout });
|
|
41
|
-
if (!result.ok) throw new Error(`${result.code}: ${result.message}`);
|
|
42
|
-
return result;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export function findPython() { return null; }
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { createDistribution } from "./distribution.js";
|
|
4
|
+
import { MATRIX_SKILLS, MATT_SKILLS, PLATFORMS, sourceSkillsRoot } from "./catalog.js";
|
|
5
|
+
import { createMattAdapter } from "./matt.js";
|
|
6
|
+
|
|
7
|
+
export { MATRIX_SKILLS, MATT_SKILLS, sourceSkillsRoot };
|
|
8
|
+
|
|
9
|
+
export function destinationFor({ scope, projectRoot, platform = "claude-code", home }) {
|
|
10
|
+
const base = scope === "global" ? (home ?? process.env.USERPROFILE ?? process.env.HOME) : projectRoot;
|
|
11
|
+
return PLATFORMS[platform].skillRoot(base);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function inspectInstallation(destination) {
|
|
15
|
+
const inspect = (names) => names.filter((skill) => {
|
|
16
|
+
try { return Boolean(destination) && requireExists(destination, skill); } catch { return false; }
|
|
17
|
+
});
|
|
18
|
+
const installed = inspect(MATRIX_SKILLS);
|
|
19
|
+
const mattInstalled = inspect(MATT_SKILLS);
|
|
20
|
+
return { installed, missing: MATRIX_SKILLS.filter((skill) => !installed.includes(skill)), mattInstalled, mattMissing: MATT_SKILLS.filter((skill) => !mattInstalled.includes(skill)) };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function requireExists(destination, skill) {
|
|
24
|
+
// Avoid exposing a second installation policy through this legacy helper.
|
|
25
|
+
return fs.existsSync(path.join(destination, skill, "SKILL.md"));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function installMatrix({ destination, mode = "copy", conflict = "error", dryRun = false }) {
|
|
29
|
+
const platform = destination?.includes(".agents") ? "codex" : "claude-code";
|
|
30
|
+
const projectRoot = path.resolve(destination, "..", "..");
|
|
31
|
+
const distribution = createDistribution();
|
|
32
|
+
const evaluation = distribution.evaluate({ projectRoot, scope: "project", platforms: [platform], mode, matrixPolicy: conflict === "replace" ? "replace" : "safe", dryRun });
|
|
33
|
+
if (!evaluation.ok) throw new Error(evaluation.diagnostics?.[0]?.message ?? evaluation.code);
|
|
34
|
+
const result = distribution.commit(evaluation.plan);
|
|
35
|
+
if (!result.ok) throw new Error(result.error ?? result.code);
|
|
36
|
+
return { installed: result.committed ?? MATRIX_SKILLS, skipped: [], existing: [], backupRoot: result.backups?.[0] ? result.backups[0].backup : null };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function installMattSkills({ projectRoot, platforms = ["claude-code"], skills = MATT_SKILLS, global = false, timeout = 300_000 }) {
|
|
40
|
+
const result = createMattAdapter().installMissing({ projectRoot, platforms, skills, global, timeout });
|
|
41
|
+
if (!result.ok) throw new Error(`${result.code}: ${result.message}`);
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function findPython() { return null; }
|