@rosenfan/matrix 0.1.0
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 +49 -0
- package/.claude/skills/matrix/agents/openai.yaml +4 -0
- package/.claude/skills/matrix/scripts/matrix_claude.py +166 -0
- package/.claude/skills/matrix/scripts/matrix_state.py +114 -0
- package/.claude/skills/matrix-archive/SKILL.md +26 -0
- package/.claude/skills/matrix-archive/agents/openai.yaml +4 -0
- package/.claude/skills/matrix-build/SKILL.md +55 -0
- package/.claude/skills/matrix-build/agents/openai.yaml +4 -0
- package/.claude/skills/matrix-claude/SKILL.md +96 -0
- package/.claude/skills/matrix-claude/agents/openai.yaml +4 -0
- package/.claude/skills/matrix-design/SKILL.md +68 -0
- package/.claude/skills/matrix-design/agents/openai.yaml +4 -0
- package/.claude/skills/matrix-hotfix/SKILL.md +8 -0
- package/.claude/skills/matrix-hotfix/agents/openai.yaml +4 -0
- package/.claude/skills/matrix-open/SKILL.md +33 -0
- package/.claude/skills/matrix-open/agents/openai.yaml +4 -0
- package/.claude/skills/matrix-status/SKILL.md +14 -0
- package/.claude/skills/matrix-status/agents/openai.yaml +4 -0
- package/.claude/skills/matrix-tweak/SKILL.md +8 -0
- package/.claude/skills/matrix-tweak/agents/openai.yaml +4 -0
- package/.claude/skills/matrix-verify/SKILL.md +82 -0
- package/.claude/skills/matrix-verify/agents/openai.yaml +4 -0
- package/LICENSE +21 -0
- package/README.md +273 -0
- package/README.zh-CN.md +271 -0
- package/assets/skills-zh-CN/matrix/SKILL.md +28 -0
- package/assets/skills-zh-CN/matrix-archive/SKILL.md +12 -0
- package/assets/skills-zh-CN/matrix-build/SKILL.md +13 -0
- package/assets/skills-zh-CN/matrix-claude/SKILL.md +30 -0
- package/assets/skills-zh-CN/matrix-design/SKILL.md +13 -0
- package/assets/skills-zh-CN/matrix-hotfix/SKILL.md +8 -0
- package/assets/skills-zh-CN/matrix-open/SKILL.md +13 -0
- package/assets/skills-zh-CN/matrix-status/SKILL.md +14 -0
- package/assets/skills-zh-CN/matrix-tweak/SKILL.md +8 -0
- package/assets/skills-zh-CN/matrix-verify/SKILL.md +13 -0
- package/bin/matrix.js +15 -0
- package/package.json +56 -0
- package/src/catalog.js +87 -0
- package/src/cli.js +117 -0
- package/src/distribution.js +192 -0
- package/src/installer.js +45 -0
- package/src/matt.js +34 -0
- package/src/messages.js +33 -0
- package/src/ui.js +89 -0
- package/src/workflow.js +95 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: matrix
|
|
3
|
+
description: 启动、恢复并治理持久化的 Matrix 开发工作流。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Matrix
|
|
7
|
+
|
|
8
|
+
Matrix 的事实来源是当前项目的 `.matrix/`,不得从聊天记录推断阶段。
|
|
9
|
+
|
|
10
|
+
先执行:
|
|
11
|
+
|
|
12
|
+
```powershell
|
|
13
|
+
matrix workflow inspect
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
只有当系统明确报告 `matrix` 命令不存在时,才解析当前 Matrix Skill 目录并执行:
|
|
17
|
+
|
|
18
|
+
```powershell
|
|
19
|
+
node <matrix-skill-directory>/scripts/matrix-runtime.mjs inspect
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
若没有活动变更,为开发请求创建 kebab-case 变更 ID:
|
|
23
|
+
|
|
24
|
+
```powershell
|
|
25
|
+
matrix workflow init <change-id> --workflow <full|hotfix|tweak> --title "<title>"
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
阶段只能由 `matrix workflow transition` 推进;架构、范围或归档前必须征求用户确认。
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: matrix-build
|
|
3
|
+
description: 用测试优先的纵向切片实现冻结的 Matrix 计划。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Matrix Build
|
|
7
|
+
|
|
8
|
+
只实现冻结计划中的范围。使用 `implement` 与预先确认的 `tdd` 测试边界;持续记录 `artifacts/verification.md` 的 `## Build evidence`。如发现范围或设计变化,返回设计阶段。
|
|
9
|
+
|
|
10
|
+
```powershell
|
|
11
|
+
matrix workflow guard build
|
|
12
|
+
matrix workflow transition verify
|
|
13
|
+
```
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: matrix-claude
|
|
3
|
+
description: 将冻结的 Matrix 设计导出为受边界约束的 Claude Code 任务包,不改变 Matrix 工作流状态。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Matrix Claude Handoff
|
|
7
|
+
|
|
8
|
+
这是可选的旁路操作,而不是 Matrix phase。它不得执行 `transition`、`archive`,也不得写入 `matrix.yaml`、`run-state.json` 或 `events.jsonl`。
|
|
9
|
+
|
|
10
|
+
仅当设计阶段守卫已通过,并且用户明确选择由 Claude Code 实现时使用。先检查:
|
|
11
|
+
|
|
12
|
+
```powershell
|
|
13
|
+
matrix workflow guard design
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
如果主命令确实不在 PATH 中,才使用已安装根 Matrix Skill 中的 bundled runtime;主命令已启动但返回业务错误时不得回退。
|
|
17
|
+
|
|
18
|
+
导出通用任务包:
|
|
19
|
+
|
|
20
|
+
```powershell
|
|
21
|
+
matrix workflow export --task-id <ID> --target generic
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
导出 FnSec 任务包时,只有在 `docs/tasks/active.md` 包含 `NO_ACTIVE_OBJECTIVE` 时才可使用 `--apply-fnsec-board`;现有任务绝不覆盖:
|
|
25
|
+
|
|
26
|
+
```powershell
|
|
27
|
+
matrix workflow export --task-id <ID> --target fnsec --apply-fnsec-board
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
任务包会包含冻结的 proposal、design、plan,以及 Claude Code 回执所需的改动、验证、边界确认和 `NEEDS_DECISION` 项目。
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: matrix-design
|
|
3
|
+
description: 在实现前设计并冻结活动 Matrix 变更。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Matrix Design
|
|
7
|
+
|
|
8
|
+
阅读 proposal,写入 `design.md`(`## Decisions`、`## Boundaries`、`## Test seams`、`## Risks`)与 `plan.md`(`## Steps`、`## Validation`、`## Stop conditions`)。重大范围或架构决策须先由用户确认。
|
|
9
|
+
|
|
10
|
+
```powershell
|
|
11
|
+
matrix workflow guard design
|
|
12
|
+
matrix workflow transition build
|
|
13
|
+
```
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: matrix-open
|
|
3
|
+
description: 将开发请求澄清为有证据的 Matrix 提案。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Matrix Open
|
|
7
|
+
|
|
8
|
+
读取 `matrix workflow inspect` 的活动变更。使用 `grill-with-docs` 澄清完整工作流请求;写入 `artifacts/proposal.md`,必须包含 `## Goal`、`## Scope`、`## Non-goals`、`## Acceptance`、`## Risks`。
|
|
9
|
+
|
|
10
|
+
```powershell
|
|
11
|
+
matrix workflow guard open
|
|
12
|
+
matrix workflow transition design
|
|
13
|
+
```
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: matrix-verify
|
|
3
|
+
description: 依据计划验证 Matrix 变更并审查差异。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Matrix Verify
|
|
7
|
+
|
|
8
|
+
运行计划的验证命令,在 `verification.md` 写入 `## Test evidence`。使用 `code-review` 完成标准与规格双轴审查,并在 `## Review evidence` 记录结论。失败时回到产生问题的阶段。
|
|
9
|
+
|
|
10
|
+
```powershell
|
|
11
|
+
matrix workflow guard verify
|
|
12
|
+
matrix workflow transition archive
|
|
13
|
+
```
|
package/bin/matrix.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { main } from "../src/cli.js";
|
|
4
|
+
|
|
5
|
+
main(process.argv.slice(2)).then((exitCode) => {
|
|
6
|
+
if (Number.isInteger(exitCode) && exitCode !== 0) process.exitCode = exitCode;
|
|
7
|
+
}).catch((error) => {
|
|
8
|
+
if (error.code === "CANCELLED") {
|
|
9
|
+
console.log("\nCancelled.");
|
|
10
|
+
process.exitCode = 0;
|
|
11
|
+
} else {
|
|
12
|
+
console.error(`\nMatrix error: ${error.message}`);
|
|
13
|
+
process.exitCode = 1;
|
|
14
|
+
}
|
|
15
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rosenfan/matrix",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Install and manage the Matrix workflow for Matt Pocock's agent skills.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"matrix": "bin/matrix.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"bin/",
|
|
12
|
+
"src/",
|
|
13
|
+
"assets/skills-zh-CN",
|
|
14
|
+
".claude/skills/matrix",
|
|
15
|
+
".claude/skills/matrix-open",
|
|
16
|
+
".claude/skills/matrix-design",
|
|
17
|
+
".claude/skills/matrix-build",
|
|
18
|
+
".claude/skills/matrix-verify",
|
|
19
|
+
".claude/skills/matrix-archive",
|
|
20
|
+
".claude/skills/matrix-hotfix",
|
|
21
|
+
".claude/skills/matrix-tweak",
|
|
22
|
+
".claude/skills/matrix-status",
|
|
23
|
+
".claude/skills/matrix-claude",
|
|
24
|
+
"!.claude/skills/**/__pycache__",
|
|
25
|
+
"!.claude/skills/**/scripts/test_*.py",
|
|
26
|
+
"README.md",
|
|
27
|
+
"README.zh-CN.md",
|
|
28
|
+
"LICENSE"
|
|
29
|
+
],
|
|
30
|
+
"scripts": {
|
|
31
|
+
"test": "node --test",
|
|
32
|
+
"pack:check": "npm pack --dry-run",
|
|
33
|
+
"prepublishOnly": "npm run test && npm run pack:check"
|
|
34
|
+
},
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=18"
|
|
37
|
+
},
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "git+https://github.com/Rosenfan/Matrix.git"
|
|
41
|
+
},
|
|
42
|
+
"bugs": {
|
|
43
|
+
"url": "https://github.com/Rosenfan/Matrix/issues"
|
|
44
|
+
},
|
|
45
|
+
"homepage": "https://github.com/Rosenfan/Matrix#readme",
|
|
46
|
+
"publishConfig": {
|
|
47
|
+
"access": "public"
|
|
48
|
+
},
|
|
49
|
+
"keywords": [
|
|
50
|
+
"claude-code",
|
|
51
|
+
"agent-skills",
|
|
52
|
+
"workflow",
|
|
53
|
+
"tdd",
|
|
54
|
+
"ai"
|
|
55
|
+
]
|
|
56
|
+
}
|
package/src/catalog.js
ADDED
|
@@ -0,0 +1,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
|
+
"grill-with-docs", "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
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { PLATFORMS } from "./catalog.js";
|
|
5
|
+
import { createDistribution } from "./distribution.js";
|
|
6
|
+
import { text } from "./messages.js";
|
|
7
|
+
import { createUi } from "./ui.js";
|
|
8
|
+
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;
|
|
29
|
+
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
|
+
|
|
74
|
+
async function init(options) {
|
|
75
|
+
const ui = createUi({ color: options.color && !options.json });
|
|
76
|
+
if (!options.json) await ui.banner();
|
|
77
|
+
const intent = await intentFrom(options, ui);
|
|
78
|
+
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"));
|
|
91
|
+
return 0;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function doctor(options) {
|
|
95
|
+
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;
|
|
110
|
+
}
|
|
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.`);
|
|
117
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
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 {
|
|
6
|
+
MATRIX_SKILLS, MATT_SKILLS, PLATFORM_IDS, PLATFORMS, hashDirectory,
|
|
7
|
+
installationRoot, manifestPath, releaseCatalog, scopeBase, sourceSkillsRoot, workflowRuntimeSource
|
|
8
|
+
} from "./catalog.js";
|
|
9
|
+
import { createMattAdapter } from "./matt.js";
|
|
10
|
+
import { text } from "./messages.js";
|
|
11
|
+
|
|
12
|
+
const VERSION = 1;
|
|
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
|
+
const transactionPath = (intent) => path.join(installationRoot(intent.scope, intent.projectRoot, intent.home), "transaction.json");
|
|
18
|
+
const writeJson = (file, value) => {
|
|
19
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
20
|
+
const temporary = `${file}.${process.pid}.${Date.now()}.tmp`;
|
|
21
|
+
fs.writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`);
|
|
22
|
+
fs.renameSync(temporary, file);
|
|
23
|
+
};
|
|
24
|
+
const readJson = (file) => { try { return JSON.parse(fs.readFileSync(file, "utf8")); } catch { return null; } };
|
|
25
|
+
const removeEntry = (value) => { if (pathExists(value)) fs.rmSync(value, { recursive: true, force: true }); };
|
|
26
|
+
const removeStaging = (stagingRoot) => {
|
|
27
|
+
removeEntry(stagingRoot);
|
|
28
|
+
const parent = path.dirname(stagingRoot);
|
|
29
|
+
if (exists(parent) && fs.readdirSync(parent).length === 0) fs.rmdirSync(parent);
|
|
30
|
+
};
|
|
31
|
+
const compareVersions = (left, right) => {
|
|
32
|
+
const parse = (value) => String(value ?? "0.0.0").split("-")[0].split(".").map((part) => Number(part) || 0);
|
|
33
|
+
const a = parse(left); const b = parse(right);
|
|
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;
|
|
36
|
+
};
|
|
37
|
+
const skillPresent = (root, skill) => exists(path.join(root, skill, "SKILL.md"));
|
|
38
|
+
const isBrokenLink = (value) => { try { return fs.lstatSync(value).isSymbolicLink() && !exists(value); } catch { return false; } };
|
|
39
|
+
|
|
40
|
+
function readManifest(intent) { return readJson(manifestPath(intent.scope, intent.projectRoot, intent.home)); }
|
|
41
|
+
|
|
42
|
+
function detectPlatforms(projectRoot, scope, home) {
|
|
43
|
+
const bases = [scopeBase(scope, projectRoot, home)];
|
|
44
|
+
if (scope === "project") bases.push(home);
|
|
45
|
+
return PLATFORM_IDS.filter((id) => bases.some((base) => exists(PLATFORMS[id].skillRoot(base))));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
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" };
|
|
53
|
+
if (!["en", "zh-CN"].includes(raw.language ?? "en")) return { error: "INVALID_INTENT", detail: "language" };
|
|
54
|
+
if (platforms.some((id) => !PLATFORM_IDS.includes(id))) return { error: "UNVERIFIED_PLATFORM" };
|
|
55
|
+
return { projectRoot: path.resolve(raw.projectRoot ?? process.cwd()), home: raw.home ?? os.homedir(), scope, platforms,
|
|
56
|
+
language: raw.language ?? "en", mode: raw.mode ?? "copy", matrixPolicy: raw.matrixPolicy ?? "safe",
|
|
57
|
+
matt: raw.matt ?? "none", dryRun: Boolean(raw.dryRun) };
|
|
58
|
+
}
|
|
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);
|
|
78
|
+
const mattPresent = MATT_SKILLS.filter((skill) => skillPresent(root, skill));
|
|
79
|
+
const globalMatt = intent.scope === "project" ? MATT_SKILLS.filter((skill) => skillPresent(globalRoot, skill)) : [];
|
|
80
|
+
const availableMatt = new Set([...mattPresent, ...globalMatt]); const missing = MATT_SKILLS.filter((skill) => !availableMatt.has(skill));
|
|
81
|
+
const inherited = globalMatt.filter((skill) => !mattPresent.includes(skill)).length;
|
|
82
|
+
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";
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function recoverPendingTransaction(intent) {
|
|
95
|
+
const file = transactionPath(intent); const journal = readJson(file);
|
|
96
|
+
if (!journal) return { ok: true, recovered: false };
|
|
97
|
+
try {
|
|
98
|
+
if (journal.state === "committed") {
|
|
99
|
+
removeStaging(journal.stagingRoot);
|
|
100
|
+
for (const operation of journal.operations ?? []) removeEntry(operation.backup);
|
|
101
|
+
removeEntry(journal.manifest?.backup); removeEntry(file);
|
|
102
|
+
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
|
+
if (journal.manifest?.backup && pathExists(journal.manifest.backup)) {
|
|
109
|
+
removeEntry(journal.manifest.path); fs.mkdirSync(path.dirname(journal.manifest.path), { recursive: true }); fs.renameSync(journal.manifest.backup, journal.manifest.path);
|
|
110
|
+
} else if (journal.manifest?.written && !journal.manifest?.existed) removeEntry(journal.manifest.path);
|
|
111
|
+
removeStaging(journal.stagingRoot); removeEntry(file);
|
|
112
|
+
return { ok: true, recovered: true, receipt: `Recovered interrupted Matrix transaction ${journal.id}.` };
|
|
113
|
+
} catch (error) { return { ok: false, code: "RECOVERY_REQUIRED", recovery: `Keep ${file} and restore the paths recorded there before retrying.`, error: error.message }; }
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function createDistribution({ failAt, mattAdapter = createMattAdapter() } = {}) {
|
|
117
|
+
const evaluate = (rawIntent) => {
|
|
118
|
+
const intent = normalizeIntent(rawIntent); const language = intent.language ?? rawIntent.language ?? "en";
|
|
119
|
+
if (intent.error) return { ok: false, code: intent.error, diagnostics: [{ code: intent.error, message: text(language, "noPlatform") }] };
|
|
120
|
+
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
|
+
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); const observations = intent.platforms.map((id) => observePlatform(intent, catalog, manifest, id));
|
|
123
|
+
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"); const plan = { version: VERSION, intent, catalogDigest: catalog.digest, manifestPath: manifestPath(intent.scope, intent.projectRoot, intent.home), actions, observations, preconditionDigest: digest(observations) };
|
|
125
|
+
plan.seal = digest({ intent, catalogDigest: plan.catalogDigest, actions, preconditionDigest: plan.preconditionDigest });
|
|
126
|
+
return { ok: blocked.length === 0, code: blocked.length ? "USER_MODIFIED_BLOCKED" : "OK", intent, observations, actions, plan, diagnostics: blocked.map((item) => ({ code: "USER_MODIFIED_BLOCKED", platform: item.platform, message: `${item.platform}: existing Matrix assets are not proven managed.` })), summary: `${text(intent.language, "preview")}: ${actions.map((item) => `${item.platform}: ${item.matrix}`).join(", ")}` };
|
|
127
|
+
};
|
|
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." });
|
|
141
|
+
diagnostics.push({ code: `MATT_${observation.matt.state.toUpperCase()}`, platform: observation.id, message: `${observation.name}: Matt Pocock skills are ${observation.matt.state}.` });
|
|
142
|
+
if (observation.matt.inherited) diagnostics.push({ code: "MATT_INHERITED", platform: observation.id, message: `${observation.name}: ${observation.matt.inherited} ${text(evaluation.intent.language, "mattInherited")}` });
|
|
143
|
+
}
|
|
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 };
|
|
153
|
+
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 = { id, state: "in-progress", stagingRoot, manifest: { path: plan.manifestPath, existed: pathExists(plan.manifestPath), backup: null, written: false }, operations: [] };
|
|
155
|
+
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" });
|
|
156
|
+
const persist = () => writeJson(journalFile, journal);
|
|
157
|
+
try {
|
|
158
|
+
persist(); const catalog = releaseCatalog(plan.intent.language); const stagedPlatforms = new Map();
|
|
159
|
+
for (const item of refreshed.observations) {
|
|
160
|
+
if (plan.actions.find((entry) => entry.platform === item.id).matrix === "keep") continue;
|
|
161
|
+
const platformStage = path.join(stagingRoot, item.id);
|
|
162
|
+
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")); 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}`); }
|
|
163
|
+
stagedPlatforms.set(item.id, platformStage);
|
|
164
|
+
}
|
|
165
|
+
failAt?.("after-stage"); let operationIndex = 0;
|
|
166
|
+
for (const item of refreshed.observations) {
|
|
167
|
+
if (plan.actions.find((entry) => entry.platform === item.id).matrix === "keep") continue;
|
|
168
|
+
fs.mkdirSync(item.root, { recursive: true });
|
|
169
|
+
for (const skill of MATRIX_SKILLS) {
|
|
170
|
+
const operation = journal.operations[operationIndex++]; const target = operation.target; operation.status = "backing-up";
|
|
171
|
+
if (pathExists(target)) operation.backup = path.join(root, "backups", id, item.id, skill); persist();
|
|
172
|
+
if (operation.backup) { fs.mkdirSync(path.dirname(operation.backup), { recursive: true }); fs.renameSync(target, operation.backup); }
|
|
173
|
+
operation.status = "backed-up"; persist(); failAt?.("after-backup", { platform: item.id, skill });
|
|
174
|
+
const staged = path.join(stagedPlatforms.get(item.id), skill);
|
|
175
|
+
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);
|
|
176
|
+
operation.status = "committed"; persist(); failAt?.("after-commit", { platform: item.id, skill });
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
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); }
|
|
180
|
+
const records = Object.fromEntries(refreshed.observations.map((item) => [item.id, { root: item.root, skills: Object.fromEntries(MATRIX_SKILLS.map((skill) => [skill, { hash: catalog.skills[skill] }])) }]));
|
|
181
|
+
writeJson(plan.manifestPath, { version: VERSION, catalogVersion: catalog.version, language: plan.intent.language, mode: plan.intent.mode, catalogDigest: catalog.digest, platforms: records }); journal.manifest.written = true; persist(); failAt?.("after-manifest"); journal.state = "committed"; persist();
|
|
182
|
+
removeStaging(stagingRoot); removeEntry(journal.manifest.backup); removeEntry(journalFile);
|
|
183
|
+
const mattActions = plan.actions.filter((item) => item.matt.kind === "install-missing"); const mattResults = [];
|
|
184
|
+
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 postcondition = evaluate({ ...plan.intent, platforms: plan.intent.platforms }); const incomplete = mattActions.length ? postcondition.observations.filter((item) => item.matt.state !== "complete") : [];
|
|
186
|
+
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) return { ok: true, code: "PARTIAL", committed: journal.operations.map((item) => item.target), backups, recovery: "Matrix is installed. Run matrix init --with-mattpocock after resolving the Matt skills installation.", matt: mattResults, recoveryReceipt: recovery.receipt };
|
|
188
|
+
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
|
+
}
|