macca-method 1.1.0 → 2.1.1
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/.agents/legacy-payloads.json +22 -0
- package/{skills-lock.json → .agents/macca-lock.json} +3 -2
- package/.agents/macca-managed-skills.txt +2 -1
- package/.agents/skills/_shared/references/additional-skills.md +30 -0
- package/.agents/skills/_shared/references/brainstorm-session.md +42 -11
- package/.agents/skills/_shared/references/config-mutation.md +25 -0
- package/.agents/skills/_shared/references/finding-format.md +25 -0
- package/.agents/skills/_shared/references/fix-mode.md +54 -0
- package/.agents/skills/_shared/references/human-loop.md +3 -1
- package/.agents/skills/_shared/references/implementation-principles.md +19 -0
- package/.agents/skills/_shared/references/invocation-policy.md +39 -0
- package/.agents/skills/_shared/references/language-config.md +17 -0
- package/.agents/skills/_shared/references/output-ownership.md +4 -2
- package/.agents/skills/_shared/references/runtime-config.md +7 -168
- package/.agents/skills/_shared/references/skill-catalog.md +34 -0
- package/.agents/skills/_shared/scripts/validate-skills.py +106 -4
- package/.agents/skills/add-feature/SKILL.md +12 -7
- package/.agents/skills/brainstorm-api/SKILL.md +53 -196
- package/.agents/skills/brainstorm-api/assets/api.template.md +147 -0
- package/.agents/skills/brainstorm-architecture/SKILL.md +26 -129
- package/.agents/skills/brainstorm-architecture/assets/architecture.template.md +135 -0
- package/.agents/skills/brainstorm-prd/SKILL.md +23 -104
- package/.agents/skills/brainstorm-prd/assets/PRD.template.md +106 -0
- package/.agents/skills/brainstorm-rules/SKILL.md +19 -153
- package/.agents/skills/brainstorm-rules/assets/rules.template.md +127 -0
- package/.agents/skills/brainstorm-schema/SKILL.md +53 -117
- package/.agents/skills/brainstorm-schema/assets/schema.template.md +109 -0
- package/.agents/skills/brainstorm-styleguide/SKILL.md +21 -136
- package/.agents/skills/brainstorm-styleguide/assets/StyleGuide.template.md +147 -0
- package/.agents/skills/brainstorm-task/SKILL.md +24 -107
- package/.agents/skills/brainstorm-task/assets/Task.template.md +113 -0
- package/.agents/skills/bug-fix/SKILL.md +54 -56
- package/.agents/skills/code-review/SKILL.md +28 -19
- package/.agents/skills/code-review/references/review-checklist.md +24 -26
- package/.agents/skills/developer/SKILL.md +27 -39
- package/.agents/skills/developer/references/close-phase.md +25 -0
- package/.agents/skills/developer/references/execute-task.md +69 -0
- package/.agents/skills/developer/references/onboarding.md +47 -0
- package/.agents/skills/help/SKILL.md +19 -16
- package/.agents/skills/meet/SKILL.md +170 -0
- package/.agents/skills/quick-dev/SKILL.md +32 -34
- package/.agents/skills/release-readiness/SKILL.md +151 -0
- package/.agents/skills/spec-audit/SKILL.md +39 -22
- package/.agents/skills/spec-compliance/SKILL.md +43 -40
- package/.agents/skills/spec-init/SKILL.md +31 -14
- package/README.md +181 -132
- package/bin/macca-method.js +779 -85
- package/flow.webp +0 -0
- package/image-macca-method.webp +0 -0
- package/package.json +12 -5
- package/scripts/run-skill-validator.js +24 -0
- package/scripts/test-install.js +430 -0
- package/scripts/test-upgrade-legacy.js +143 -0
- package/scripts/validate-skill-behavior.js +124 -0
- package/.agents/skills/developer/references/execution-workflow.md +0 -322
- package/.agents/skills/rapat/SKILL.md +0 -172
package/flow.webp
ADDED
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "macca-method",
|
|
3
|
-
"version": "1.1
|
|
3
|
+
"version": "2.1.1",
|
|
4
4
|
"description": "CLI installer for MACCA AI spec-driven development skills.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"scripts": {
|
|
7
|
+
"validate": "node --check ./bin/macca-method.js && node --check ./scripts/test-install.js && node --check ./scripts/test-upgrade-legacy.js && node --check ./scripts/run-skill-validator.js && node --check ./scripts/validate-skill-behavior.js && node ./scripts/run-skill-validator.js && node ./scripts/validate-skill-behavior.js && npm run test:install && npm run test:upgrade:legacy && npm pack --dry-run",
|
|
7
8
|
"test:install": "node ./scripts/test-install.js",
|
|
8
|
-
"test:install:published": "node ./scripts/test-install.js --published"
|
|
9
|
+
"test:install:published": "node ./scripts/test-install.js --published",
|
|
10
|
+
"test:upgrade:legacy": "node ./scripts/test-upgrade-legacy.js"
|
|
9
11
|
},
|
|
10
12
|
"bin": {
|
|
11
13
|
"macca-method": "bin/macca-method.js"
|
|
@@ -13,9 +15,14 @@
|
|
|
13
15
|
"files": [
|
|
14
16
|
"bin/",
|
|
15
17
|
".agents/",
|
|
18
|
+
"scripts/test-install.js",
|
|
19
|
+
"scripts/test-upgrade-legacy.js",
|
|
20
|
+
"scripts/run-skill-validator.js",
|
|
21
|
+
"scripts/validate-skill-behavior.js",
|
|
22
|
+
"flow.webp",
|
|
23
|
+
"image-macca-method.webp",
|
|
16
24
|
"README.md",
|
|
17
|
-
"LICENSE"
|
|
18
|
-
"skills-lock.json"
|
|
25
|
+
"LICENSE"
|
|
19
26
|
],
|
|
20
27
|
"engines": {
|
|
21
28
|
"node": ">=18"
|
|
@@ -32,4 +39,4 @@
|
|
|
32
39
|
"url": "git+https://github.com/firdaus12p/MACCA-METHOD.git"
|
|
33
40
|
},
|
|
34
41
|
"homepage": "https://github.com/firdaus12p/MACCA-METHOD#readme"
|
|
35
|
-
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
"use strict";
|
|
4
|
+
|
|
5
|
+
const path = require("node:path");
|
|
6
|
+
const { spawnSync } = require("node:child_process");
|
|
7
|
+
|
|
8
|
+
const validatorPath = path.resolve(
|
|
9
|
+
process.argv[2] || path.join(__dirname, "..", ".agents", "skills", "_shared", "scripts", "validate-skills.py")
|
|
10
|
+
);
|
|
11
|
+
const candidates = process.platform === "win32"
|
|
12
|
+
? [["python", []], ["py", ["-3"]]]
|
|
13
|
+
: [["python3", []], ["python", []]];
|
|
14
|
+
|
|
15
|
+
for (const [command, prefixArgs] of candidates) {
|
|
16
|
+
const result = spawnSync(command, [...prefixArgs, validatorPath], { stdio: "inherit" });
|
|
17
|
+
if (result.error && result.error.code === "ENOENT") {
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
process.exit(result.status === null ? 1 : result.status);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
process.stderr.write("Python 3 is required to run the skill validator.\n");
|
|
24
|
+
process.exit(1);
|
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
"use strict";
|
|
4
|
+
|
|
5
|
+
const fs = require("node:fs");
|
|
6
|
+
const crypto = require("node:crypto");
|
|
7
|
+
const os = require("node:os");
|
|
8
|
+
const path = require("node:path");
|
|
9
|
+
const { execFileSync } = require("node:child_process");
|
|
10
|
+
|
|
11
|
+
const rootDir = path.resolve(__dirname, "..");
|
|
12
|
+
const mode = process.argv.includes("--published") ? "published" : "local";
|
|
13
|
+
const tmpDir = fs.mkdtempSync(path.join(resolveTempRoot(), "macca-test-install-"));
|
|
14
|
+
const projectDir = path.join(tmpDir, "project");
|
|
15
|
+
const collisionDir = path.join(tmpDir, "collision-project");
|
|
16
|
+
const symlinkDir = path.join(tmpDir, "symlink-project");
|
|
17
|
+
const driftDir = path.join(tmpDir, "drift-project");
|
|
18
|
+
let packageSpec = "macca-method";
|
|
19
|
+
|
|
20
|
+
function commandName(base) {
|
|
21
|
+
if (process.platform !== "win32") {
|
|
22
|
+
return base;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return base === "npm" || base === "npx" ? `${base}.cmd` : base;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function resolveTempRoot() {
|
|
29
|
+
const tempRoot = os.tmpdir();
|
|
30
|
+
try {
|
|
31
|
+
return fs.realpathSync(tempRoot);
|
|
32
|
+
} catch {
|
|
33
|
+
return tempRoot;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function runNpm(args, options = {}) {
|
|
38
|
+
const npmExecPath = process.env.npm_execpath;
|
|
39
|
+
if (npmExecPath && /\.c?js$/i.test(npmExecPath)) {
|
|
40
|
+
return capture(process.execPath, [npmExecPath, ...args], options);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return capture(commandName("npm"), args, options);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function runNpmWithInheritedStdio(args, options = {}) {
|
|
47
|
+
const npmExecPath = process.env.npm_execpath;
|
|
48
|
+
if (npmExecPath && /\.c?js$/i.test(npmExecPath)) {
|
|
49
|
+
run(process.execPath, [npmExecPath, ...args], options);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
run(commandName("npm"), args, options);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function run(command, args, options = {}) {
|
|
57
|
+
execFileSync(command, args, {
|
|
58
|
+
cwd: rootDir,
|
|
59
|
+
stdio: "inherit",
|
|
60
|
+
...options
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function capture(command, args, options = {}) {
|
|
65
|
+
return execFileSync(command, args, {
|
|
66
|
+
cwd: rootDir,
|
|
67
|
+
encoding: "utf8",
|
|
68
|
+
...options
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function runCli(args) {
|
|
73
|
+
if (mode === "published") {
|
|
74
|
+
runNpmWithInheritedStdio(["exec", "--yes", "--package", "macca-method", "--", "macca-method", ...args]);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
runNpmWithInheritedStdio(["exec", "--yes", "--package", packageSpec, "--", "macca-method", ...args]);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function expectCliFailure(args, expectedText) {
|
|
82
|
+
try {
|
|
83
|
+
if (mode === "published") {
|
|
84
|
+
runNpm(["exec", "--yes", "--package", "macca-method", "--", "macca-method", ...args], { cwd: rootDir, stdio: "pipe" });
|
|
85
|
+
} else {
|
|
86
|
+
runNpm(["exec", "--yes", "--package", packageSpec, "--", "macca-method", ...args], { cwd: rootDir, stdio: "pipe" });
|
|
87
|
+
}
|
|
88
|
+
} catch (error) {
|
|
89
|
+
const output = `${error.stdout || ""}${error.stderr || ""}${error.message || ""}`;
|
|
90
|
+
if (!output.includes(expectedText)) {
|
|
91
|
+
throw new Error(`Expected failed command to include ${expectedText}, got: ${output}`);
|
|
92
|
+
}
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
throw new Error(`Expected command to fail: ${args.join(" ")}`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function assertPathExists(targetPath) {
|
|
99
|
+
if (!fs.existsSync(targetPath)) {
|
|
100
|
+
throw new Error(`Missing expected path: ${targetPath}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function readNonEmptyLines(filePath) {
|
|
105
|
+
return fs
|
|
106
|
+
.readFileSync(filePath, "utf8")
|
|
107
|
+
.split(/\r?\n/)
|
|
108
|
+
.map((line) => line.trim())
|
|
109
|
+
.filter(Boolean);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function listFiles(directoryPath) {
|
|
113
|
+
const files = [];
|
|
114
|
+
|
|
115
|
+
function walk(currentPath) {
|
|
116
|
+
const entries = fs.readdirSync(currentPath, { withFileTypes: true });
|
|
117
|
+
for (const entry of entries) {
|
|
118
|
+
const entryPath = path.join(currentPath, entry.name);
|
|
119
|
+
if (entry.isDirectory()) {
|
|
120
|
+
walk(entryPath);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
files.push(entryPath);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
walk(directoryPath);
|
|
129
|
+
return files.sort();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function ensureExpectedTools(filePath) {
|
|
133
|
+
const tools = readNonEmptyLines(filePath);
|
|
134
|
+
if (!tools.includes("codex")) {
|
|
135
|
+
throw new Error("Expected codex in .agents/macca-tools.txt");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (!tools.includes("copilot")) {
|
|
139
|
+
throw new Error("Expected copilot in .agents/macca-tools.txt");
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function resolveLocalPackage() {
|
|
144
|
+
const packOutput = runNpm(["pack", "--json", "--pack-destination", tmpDir]);
|
|
145
|
+
const packageList = JSON.parse(packOutput);
|
|
146
|
+
|
|
147
|
+
if (!Array.isArray(packageList) || packageList.length === 0 || !packageList[0].filename) {
|
|
148
|
+
throw new Error("npm pack did not return a tarball filename");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
packageSpec = path.join(tmpDir, packageList[0].filename);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function main() {
|
|
155
|
+
process.stdout.write("\n");
|
|
156
|
+
process.stdout.write(" MACCA test-install\n");
|
|
157
|
+
process.stdout.write(` Mode: ${mode}\n`);
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
if (mode === "local") {
|
|
161
|
+
resolveLocalPackage();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
fs.mkdirSync(projectDir, { recursive: true });
|
|
165
|
+
const collisionSkill = path.join(collisionDir, ".agents", "skills", "developer", "SKILL.md");
|
|
166
|
+
fs.mkdirSync(path.dirname(collisionSkill), { recursive: true });
|
|
167
|
+
fs.writeFileSync(collisionSkill, "---\nname: developer\ndescription: User-owned collision fixture.\n---\n", "utf8");
|
|
168
|
+
expectCliFailure(
|
|
169
|
+
["install", "--yes", "--tool", "codex", "--directory", collisionDir],
|
|
170
|
+
"Refusing to overwrite or remove unowned skill directory"
|
|
171
|
+
);
|
|
172
|
+
if (!fs.readFileSync(collisionSkill, "utf8").includes("User-owned collision fixture")) {
|
|
173
|
+
throw new Error("Collision protection modified the user-owned skill");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (process.platform !== "win32") {
|
|
177
|
+
const outsideSkills = path.join(tmpDir, "outside-skills");
|
|
178
|
+
fs.mkdirSync(path.join(symlinkDir, ".agents"), { recursive: true });
|
|
179
|
+
fs.mkdirSync(outsideSkills, { recursive: true });
|
|
180
|
+
fs.symlinkSync(outsideSkills, path.join(symlinkDir, ".agents", "skills"), "dir");
|
|
181
|
+
expectCliFailure(
|
|
182
|
+
["install", "--yes", "--tool", "codex", "--directory", symlinkDir],
|
|
183
|
+
"Refusing symlinked skill destination component"
|
|
184
|
+
);
|
|
185
|
+
if (fs.readdirSync(outsideSkills).length !== 0) {
|
|
186
|
+
throw new Error("Symlink containment test wrote outside the target project");
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const outerReal = path.join(tmpDir, "outer-real-project");
|
|
190
|
+
const outerLink = path.join(tmpDir, "outer-link-project");
|
|
191
|
+
fs.mkdirSync(outerReal, { recursive: true });
|
|
192
|
+
fs.symlinkSync(outerReal, outerLink, "dir");
|
|
193
|
+
expectCliFailure(
|
|
194
|
+
["install", "--yes", "--tool", "codex", "--directory", outerLink],
|
|
195
|
+
"Refusing symlinked target project ancestor"
|
|
196
|
+
);
|
|
197
|
+
if (fs.readdirSync(outerReal).length !== 0) {
|
|
198
|
+
throw new Error("Target directory ancestry symlink test wrote into the real directory");
|
|
199
|
+
}
|
|
200
|
+
} else {
|
|
201
|
+
const outerReal = path.join(tmpDir, "outer-real-project");
|
|
202
|
+
const outerLink = path.join(tmpDir, "outer-link-project");
|
|
203
|
+
fs.mkdirSync(outerReal, { recursive: true });
|
|
204
|
+
try {
|
|
205
|
+
run(commandName("cmd"), ["/c", "mklink", "/J", outerLink, outerReal]);
|
|
206
|
+
expectCliFailure(
|
|
207
|
+
["install", "--yes", "--tool", "codex", "--directory", outerLink],
|
|
208
|
+
"Refusing symlinked target project ancestor"
|
|
209
|
+
);
|
|
210
|
+
} catch {
|
|
211
|
+
process.stdout.write(" Skipping Windows junction ancestry test (mklink unavailable or not permitted)\n");
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const customSkill = path.join(projectDir, ".agents", "skills", "custom-skill", "SKILL.md");
|
|
216
|
+
fs.mkdirSync(path.dirname(customSkill), { recursive: true });
|
|
217
|
+
fs.writeFileSync(customSkill, "---\nname: custom-skill\ndescription: User-owned test skill.\n---\n", "utf8");
|
|
218
|
+
|
|
219
|
+
process.stdout.write("\n");
|
|
220
|
+
process.stdout.write(` Installing into: ${projectDir}\n`);
|
|
221
|
+
|
|
222
|
+
runCli(["install", "--yes", "--tool", "codex", "--tool", "github-copilot", "--directory", projectDir]);
|
|
223
|
+
|
|
224
|
+
const configPath = path.join(projectDir, ".agents", "developer-config.json");
|
|
225
|
+
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
226
|
+
config.name = "Preserved Name";
|
|
227
|
+
config.project = "Preserved Project";
|
|
228
|
+
config.languagePreferences.communication = { raw: "English", normalized: "english" };
|
|
229
|
+
config.languagePreferences.documents = { raw: "English", normalized: "english" };
|
|
230
|
+
config.customField = { preserved: true };
|
|
231
|
+
fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
232
|
+
runCli(["install", "--yes", "--tool", "opencode", "--directory", projectDir]);
|
|
233
|
+
|
|
234
|
+
run(commandName("node"), [
|
|
235
|
+
path.join(rootDir, "scripts", "run-skill-validator.js"),
|
|
236
|
+
path.join(projectDir, ".opencode", "skills", "_shared", "scripts", "validate-skills.py")
|
|
237
|
+
]);
|
|
238
|
+
|
|
239
|
+
const managedPath = path.join(projectDir, ".agents", "macca-managed-skills.txt");
|
|
240
|
+
const originalManaged = fs.readFileSync(managedPath, "utf8");
|
|
241
|
+
fs.writeFileSync(managedPath, `${originalManaged}../../outside\n`, "utf8");
|
|
242
|
+
expectCliFailure(["upgrade", "--directory", projectDir], "locally modified MACCA metadata");
|
|
243
|
+
fs.writeFileSync(managedPath, originalManaged, "utf8");
|
|
244
|
+
|
|
245
|
+
const maliciousSource = path.join(projectDir, "src");
|
|
246
|
+
const maliciousTransactionId = "1-1-deadbeef";
|
|
247
|
+
const maliciousBackup = path.join(projectDir, `.src.macca-backup-${maliciousTransactionId}`);
|
|
248
|
+
const interruptedJournal = path.join(projectDir, ".agents", "macca-transaction.json");
|
|
249
|
+
fs.mkdirSync(maliciousSource, { recursive: true });
|
|
250
|
+
fs.writeFileSync(path.join(maliciousSource, "sentinel.txt"), "keep", "utf8");
|
|
251
|
+
fs.writeFileSync(interruptedJournal, `${JSON.stringify({
|
|
252
|
+
version: 1,
|
|
253
|
+
phase: "committed",
|
|
254
|
+
transactionId: maliciousTransactionId,
|
|
255
|
+
entries: [{
|
|
256
|
+
targetPath: maliciousSource,
|
|
257
|
+
stagingPath: null,
|
|
258
|
+
backupPath: maliciousBackup,
|
|
259
|
+
kind: "directory",
|
|
260
|
+
hadTarget: true
|
|
261
|
+
}]
|
|
262
|
+
}, null, 2)}\n`, "utf8");
|
|
263
|
+
expectCliFailure(["upgrade", "--directory", projectDir], "not a managed MACCA path");
|
|
264
|
+
assertPathExists(path.join(maliciousSource, "sentinel.txt"));
|
|
265
|
+
fs.rmSync(interruptedJournal, { force: true });
|
|
266
|
+
|
|
267
|
+
const forgedTransactionId = "3-3-feedface";
|
|
268
|
+
const customSkillDirectory = path.dirname(customSkill);
|
|
269
|
+
const forgedBackup = path.join(
|
|
270
|
+
path.dirname(customSkillDirectory),
|
|
271
|
+
`.${path.basename(customSkillDirectory)}.macca-backup-${forgedTransactionId}`
|
|
272
|
+
);
|
|
273
|
+
fs.writeFileSync(interruptedJournal, `${JSON.stringify({
|
|
274
|
+
version: 1,
|
|
275
|
+
phase: "committing",
|
|
276
|
+
transactionId: forgedTransactionId,
|
|
277
|
+
entries: [{
|
|
278
|
+
targetPath: customSkillDirectory,
|
|
279
|
+
stagingPath: null,
|
|
280
|
+
backupPath: forgedBackup,
|
|
281
|
+
kind: "directory",
|
|
282
|
+
hadTarget: false,
|
|
283
|
+
stagedHash: null
|
|
284
|
+
}]
|
|
285
|
+
}, null, 2)}\n`, "utf8");
|
|
286
|
+
expectCliFailure(["upgrade", "--directory", projectDir], "matching transaction evidence");
|
|
287
|
+
assertPathExists(customSkill);
|
|
288
|
+
fs.rmSync(interruptedJournal, { force: true });
|
|
289
|
+
|
|
290
|
+
const interruptedConfig = fs.readFileSync(configPath, "utf8");
|
|
291
|
+
const interruptedTransactionId = "2-2-cafebabe";
|
|
292
|
+
const interruptedBackup = path.join(
|
|
293
|
+
projectDir,
|
|
294
|
+
".agents",
|
|
295
|
+
`.developer-config.json.macca-backup-${interruptedTransactionId}`
|
|
296
|
+
);
|
|
297
|
+
const interruptedTarget = `${JSON.stringify({ interrupted: true }, null, 2)}\n`;
|
|
298
|
+
fs.renameSync(configPath, interruptedBackup);
|
|
299
|
+
fs.writeFileSync(configPath, interruptedTarget, "utf8");
|
|
300
|
+
fs.writeFileSync(interruptedJournal, `${JSON.stringify({
|
|
301
|
+
version: 1,
|
|
302
|
+
phase: "committing",
|
|
303
|
+
transactionId: interruptedTransactionId,
|
|
304
|
+
entries: [{
|
|
305
|
+
targetPath: configPath,
|
|
306
|
+
stagingPath: null,
|
|
307
|
+
backupPath: interruptedBackup,
|
|
308
|
+
kind: "file",
|
|
309
|
+
hadTarget: true,
|
|
310
|
+
stagedHash: crypto.createHash("sha256").update(interruptedTarget).digest("hex")
|
|
311
|
+
}]
|
|
312
|
+
}, null, 2)}\n`, "utf8");
|
|
313
|
+
runCli(["upgrade", "--directory", projectDir]);
|
|
314
|
+
if (fs.existsSync(interruptedJournal) || fs.existsSync(interruptedBackup)) {
|
|
315
|
+
throw new Error("Interrupted transaction recovery left journal or backup artifacts");
|
|
316
|
+
}
|
|
317
|
+
if (fs.readFileSync(configPath, "utf8") !== interruptedConfig) {
|
|
318
|
+
throw new Error("Interrupted transaction recovery did not restore developer-config.json");
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
runCli(["install", "--yes", "--tool", "opencode", "--directory", driftDir]);
|
|
322
|
+
const driftSkill = path.join(driftDir, ".opencode", "skills", "developer", "SKILL.md");
|
|
323
|
+
fs.appendFileSync(driftSkill, "\nlocal edit\n", "utf8");
|
|
324
|
+
expectCliFailure(["upgrade", "--directory", driftDir], "locally modified managed skill");
|
|
325
|
+
|
|
326
|
+
assertPathExists(path.join(projectDir, ".agents", "developer-config.json"));
|
|
327
|
+
assertPathExists(path.join(projectDir, ".agents", "macca-managed-skills.txt"));
|
|
328
|
+
assertPathExists(path.join(projectDir, ".agents", "macca-tools.txt"));
|
|
329
|
+
assertPathExists(path.join(projectDir, ".agents", "skills", "brainstorm-prd", "SKILL.md"));
|
|
330
|
+
assertPathExists(path.join(projectDir, ".github", "skills", "brainstorm-prd", "SKILL.md"));
|
|
331
|
+
assertPathExists(path.join(projectDir, ".opencode", "skills", "brainstorm-prd", "SKILL.md"));
|
|
332
|
+
for (const skillsRoot of [
|
|
333
|
+
path.join(projectDir, ".agents", "skills"),
|
|
334
|
+
path.join(projectDir, ".github", "skills"),
|
|
335
|
+
path.join(projectDir, ".opencode", "skills")
|
|
336
|
+
]) {
|
|
337
|
+
assertPathExists(path.join(skillsRoot, "meet", "SKILL.md"));
|
|
338
|
+
}
|
|
339
|
+
assertPathExists(customSkill);
|
|
340
|
+
assertPathExists(path.join(projectDir, ".agents", "macca-lock.json"));
|
|
341
|
+
|
|
342
|
+
const allToolsDir = path.join(tmpDir, "all-tools-project");
|
|
343
|
+
runCli([
|
|
344
|
+
"install",
|
|
345
|
+
"--yes",
|
|
346
|
+
"--tool", "codex",
|
|
347
|
+
"--tool", "github-copilot",
|
|
348
|
+
"--tool", "cursor",
|
|
349
|
+
"--tool", "claude-code",
|
|
350
|
+
"--tool", "windsurf",
|
|
351
|
+
"--tool", "gemini-cli",
|
|
352
|
+
"--tool", "opencode",
|
|
353
|
+
"--tool", "kilo-code",
|
|
354
|
+
"--tool", "kimi-cli",
|
|
355
|
+
"--directory", allToolsDir
|
|
356
|
+
]);
|
|
357
|
+
for (const expectedPath of [
|
|
358
|
+
path.join(allToolsDir, ".agents", "skills", "meet", "SKILL.md"),
|
|
359
|
+
path.join(allToolsDir, ".github", "skills", "meet", "SKILL.md"),
|
|
360
|
+
path.join(allToolsDir, ".cursor", "skills", "meet", "SKILL.md"),
|
|
361
|
+
path.join(allToolsDir, ".claude", "skills", "meet", "SKILL.md"),
|
|
362
|
+
path.join(allToolsDir, ".windsurf", "skills", "meet", "SKILL.md"),
|
|
363
|
+
path.join(allToolsDir, ".gemini", "skills", "meet", "SKILL.md"),
|
|
364
|
+
path.join(allToolsDir, ".opencode", "skills", "meet", "SKILL.md"),
|
|
365
|
+
path.join(allToolsDir, ".kilo", "skills", "meet", "SKILL.md")
|
|
366
|
+
]) {
|
|
367
|
+
assertPathExists(expectedPath);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (mode === "local") {
|
|
371
|
+
const downgradeDir = path.join(tmpDir, "downgrade-project");
|
|
372
|
+
runCli([
|
|
373
|
+
"install",
|
|
374
|
+
"--yes",
|
|
375
|
+
"--tool", "github-copilot",
|
|
376
|
+
"--tool", "opencode",
|
|
377
|
+
"--directory", downgradeDir,
|
|
378
|
+
"--name", "Downgrade User",
|
|
379
|
+
"--project", "Downgrade Project",
|
|
380
|
+
"--communication-language", "English",
|
|
381
|
+
"--document-language", "English"
|
|
382
|
+
]);
|
|
383
|
+
const downgradeLockPath = path.join(downgradeDir, ".agents", "macca-lock.json");
|
|
384
|
+
const downgradeLock = JSON.parse(fs.readFileSync(downgradeLockPath, "utf8"));
|
|
385
|
+
downgradeLock.version = "99.0.0";
|
|
386
|
+
fs.writeFileSync(downgradeLockPath, `${JSON.stringify(downgradeLock, null, 2)}\n`, "utf8");
|
|
387
|
+
expectCliFailure([
|
|
388
|
+
"upgrade",
|
|
389
|
+
"--directory", downgradeDir
|
|
390
|
+
], "Refusing to downgrade MACCA");
|
|
391
|
+
assertPathExists(path.join(downgradeDir, ".github", "skills", "meet", "SKILL.md"));
|
|
392
|
+
assertPathExists(path.join(downgradeDir, ".opencode", "skills", "meet", "SKILL.md"));
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const preservedConfig = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
396
|
+
if (!preservedConfig.customField || preservedConfig.customField.preserved !== true) {
|
|
397
|
+
throw new Error("Reinstall did not preserve unknown developer-config.json fields");
|
|
398
|
+
}
|
|
399
|
+
if (
|
|
400
|
+
preservedConfig.name !== "Preserved Name"
|
|
401
|
+
|| preservedConfig.project !== "Preserved Project"
|
|
402
|
+
|| preservedConfig.languagePreferences.communication.normalized !== "english"
|
|
403
|
+
|| preservedConfig.languagePreferences.documents.normalized !== "english"
|
|
404
|
+
) {
|
|
405
|
+
throw new Error("Reinstall reset known developer-config.json values");
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const tools = readNonEmptyLines(path.join(projectDir, ".agents", "macca-tools.txt"));
|
|
409
|
+
for (const expectedTool of ["codex", "copilot", "opencode"]) {
|
|
410
|
+
if (!tools.includes(expectedTool)) {
|
|
411
|
+
throw new Error(`Reinstall orphaned previously selected tool: ${expectedTool}`);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
ensureExpectedTools(path.join(projectDir, ".agents", "macca-tools.txt"));
|
|
416
|
+
|
|
417
|
+
process.stdout.write("\n");
|
|
418
|
+
process.stdout.write(" Verified files:\n");
|
|
419
|
+
for (const filePath of listFiles(projectDir)) {
|
|
420
|
+
process.stdout.write(`${filePath}\n`);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
process.stdout.write("\n");
|
|
424
|
+
process.stdout.write(" ✓ test-install passed\n");
|
|
425
|
+
} finally {
|
|
426
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
main();
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
"use strict";
|
|
4
|
+
|
|
5
|
+
const fs = require("node:fs");
|
|
6
|
+
const os = require("node:os");
|
|
7
|
+
const path = require("node:path");
|
|
8
|
+
const { execFileSync } = require("node:child_process");
|
|
9
|
+
|
|
10
|
+
const rootDir = path.resolve(__dirname, "..");
|
|
11
|
+
const temporaryRoot = fs.mkdtempSync(path.join(resolveTempRoot(), "macca-legacy-upgrade-"));
|
|
12
|
+
const projectDir = path.join(temporaryRoot, "project");
|
|
13
|
+
const obsoleteMeetingName = Buffer.from("cmFwYXQ=", "base64").toString("utf8");
|
|
14
|
+
|
|
15
|
+
function commandName(base) {
|
|
16
|
+
if (process.platform !== "win32") {
|
|
17
|
+
return base;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return base === "npm" || base === "npx" ? `${base}.cmd` : base;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function resolveTempRoot() {
|
|
24
|
+
const tempRoot = os.tmpdir();
|
|
25
|
+
try {
|
|
26
|
+
return fs.realpathSync(tempRoot);
|
|
27
|
+
} catch {
|
|
28
|
+
return tempRoot;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function captureNpm(args, options = {}) {
|
|
33
|
+
const npmExecPath = process.env.npm_execpath;
|
|
34
|
+
if (npmExecPath && /\.c?js$/i.test(npmExecPath)) {
|
|
35
|
+
return capture(process.execPath, [npmExecPath, ...args], options);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return capture(commandName("npm"), args, options);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function runNpm(args, options = {}) {
|
|
42
|
+
const npmExecPath = process.env.npm_execpath;
|
|
43
|
+
if (npmExecPath && /\.c?js$/i.test(npmExecPath)) {
|
|
44
|
+
run(process.execPath, [npmExecPath, ...args], options);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
run(commandName("npm"), args, options);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function run(command, args, options = {}) {
|
|
52
|
+
return execFileSync(command, args, {
|
|
53
|
+
cwd: rootDir,
|
|
54
|
+
stdio: "inherit",
|
|
55
|
+
...options
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function capture(command, args, options = {}) {
|
|
60
|
+
return execFileSync(command, args, {
|
|
61
|
+
cwd: rootDir,
|
|
62
|
+
encoding: "utf8",
|
|
63
|
+
...options
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function assertExists(targetPath) {
|
|
68
|
+
if (!fs.existsSync(targetPath)) throw new Error(`Missing expected path: ${targetPath}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function assertMissing(targetPath) {
|
|
72
|
+
if (fs.existsSync(targetPath)) throw new Error(`Obsolete path was not removed: ${targetPath}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
fs.mkdirSync(projectDir, { recursive: true });
|
|
77
|
+
|
|
78
|
+
const legacyPackOutput = captureNpm([
|
|
79
|
+
"pack",
|
|
80
|
+
"macca-method@1.1.0",
|
|
81
|
+
"--json",
|
|
82
|
+
"--pack-destination",
|
|
83
|
+
temporaryRoot
|
|
84
|
+
]);
|
|
85
|
+
const legacyPackageList = JSON.parse(legacyPackOutput);
|
|
86
|
+
const legacyPackageSpec = path.join(temporaryRoot, legacyPackageList[0].filename);
|
|
87
|
+
|
|
88
|
+
runNpm([
|
|
89
|
+
"exec",
|
|
90
|
+
"--yes",
|
|
91
|
+
"--package",
|
|
92
|
+
legacyPackageSpec,
|
|
93
|
+
"--",
|
|
94
|
+
"macca-method",
|
|
95
|
+
"install",
|
|
96
|
+
"--yes",
|
|
97
|
+
"--tool",
|
|
98
|
+
"opencode",
|
|
99
|
+
"--directory",
|
|
100
|
+
projectDir,
|
|
101
|
+
"--name",
|
|
102
|
+
"Legacy User",
|
|
103
|
+
"--project",
|
|
104
|
+
"Legacy Project",
|
|
105
|
+
"--communication-language",
|
|
106
|
+
"English",
|
|
107
|
+
"--document-language",
|
|
108
|
+
"English"
|
|
109
|
+
]);
|
|
110
|
+
|
|
111
|
+
const legacyRoot = path.join(projectDir, ".opencode", "skill");
|
|
112
|
+
assertExists(path.join(legacyRoot, obsoleteMeetingName, "SKILL.md"));
|
|
113
|
+
|
|
114
|
+
const packOutput = captureNpm(["pack", "--json", "--pack-destination", temporaryRoot]);
|
|
115
|
+
const packageList = JSON.parse(packOutput);
|
|
116
|
+
const packageSpec = path.join(temporaryRoot, packageList[0].filename);
|
|
117
|
+
|
|
118
|
+
runNpm([
|
|
119
|
+
"exec",
|
|
120
|
+
"--yes",
|
|
121
|
+
"--package",
|
|
122
|
+
packageSpec,
|
|
123
|
+
"--",
|
|
124
|
+
"macca-method",
|
|
125
|
+
"upgrade",
|
|
126
|
+
"--directory",
|
|
127
|
+
projectDir
|
|
128
|
+
]);
|
|
129
|
+
|
|
130
|
+
const currentRoot = path.join(projectDir, ".opencode", "skills");
|
|
131
|
+
assertExists(path.join(currentRoot, "meet", "SKILL.md"));
|
|
132
|
+
assertExists(path.join(currentRoot, "release-readiness", "SKILL.md"));
|
|
133
|
+
assertMissing(path.join(legacyRoot, obsoleteMeetingName));
|
|
134
|
+
|
|
135
|
+
const config = JSON.parse(fs.readFileSync(path.join(projectDir, ".agents", "developer-config.json"), "utf8"));
|
|
136
|
+
if (config.name !== "Legacy User" || config.project !== "Legacy Project") {
|
|
137
|
+
throw new Error("Legacy upgrade did not preserve developer config");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
process.stdout.write("OK: published 1.1.0 OpenCode installation upgraded safely\n");
|
|
141
|
+
} finally {
|
|
142
|
+
fs.rmSync(temporaryRoot, { recursive: true, force: true });
|
|
143
|
+
}
|