macca-method 2.1.2 → 3.0.0-rc.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/macca-lock.json +2 -1
- package/.agents/macca-managed-skills.txt +1 -0
- package/.agents/skills/_shared/references/additional-skills.md +17 -1
- package/.agents/skills/_shared/references/brainstorm-session.md +61 -21
- package/.agents/skills/_shared/references/config-mutation.md +58 -18
- package/.agents/skills/_shared/references/fix-mode.md +21 -7
- package/.agents/skills/_shared/references/human-loop.md +2 -0
- package/.agents/skills/_shared/references/implementation-principles.md +4 -0
- package/.agents/skills/_shared/references/interaction-contract.md +33 -0
- package/.agents/skills/_shared/references/invocation-policy.md +14 -3
- package/.agents/skills/_shared/references/language-config.md +24 -4
- package/.agents/skills/_shared/references/output-ownership.md +12 -5
- package/.agents/skills/_shared/references/planning-principles.md +35 -0
- package/.agents/skills/_shared/references/scope-delta.md +21 -0
- package/.agents/skills/_shared/references/scope-rules.md +1 -1
- package/.agents/skills/_shared/references/skill-catalog.md +30 -13
- package/.agents/skills/_shared/references/workspace-safety.md +11 -0
- package/.agents/skills/_shared/scripts/config-file.js +132 -0
- package/.agents/skills/_shared/scripts/config-validator.js +164 -0
- package/.agents/skills/_shared/scripts/read-preferences.js +128 -0
- package/.agents/skills/add-feature/SKILL.md +24 -18
- package/.agents/skills/brainstorm-api/SKILL.md +24 -23
- package/.agents/skills/brainstorm-api/assets/api.template.md +11 -23
- package/.agents/skills/brainstorm-architecture/SKILL.md +41 -48
- package/.agents/skills/brainstorm-architecture/assets/architecture.template.md +24 -16
- package/.agents/skills/brainstorm-prd/SKILL.md +26 -28
- package/.agents/skills/brainstorm-prd/assets/PRD.template.md +10 -9
- package/.agents/skills/brainstorm-rules/SKILL.md +42 -46
- package/.agents/skills/brainstorm-rules/assets/rules.template.md +8 -8
- package/.agents/skills/brainstorm-schema/SKILL.md +17 -17
- package/.agents/skills/brainstorm-schema/assets/schema.template.md +9 -8
- package/.agents/skills/brainstorm-styleguide/SKILL.md +27 -26
- package/.agents/skills/brainstorm-styleguide/assets/StyleGuide.template.md +27 -51
- package/.agents/skills/brainstorm-task/SKILL.md +53 -37
- package/.agents/skills/brainstorm-task/assets/Task.template.md +11 -12
- package/.agents/skills/bug-fix/SKILL.md +78 -175
- package/.agents/skills/bug-fix/assets/bug-log.template.md +61 -0
- package/.agents/skills/code-review/SKILL.md +14 -6
- package/.agents/skills/code-review/references/review-checklist.md +24 -44
- package/.agents/skills/developer/SKILL.md +23 -25
- package/.agents/skills/developer/references/close-phase.md +11 -6
- package/.agents/skills/developer/references/execute-task.md +15 -8
- package/.agents/skills/developer/references/onboarding.md +16 -19
- package/.agents/skills/help/SKILL.md +49 -49
- package/.agents/skills/meet/SKILL.md +37 -85
- package/.agents/skills/quick-dev/SKILL.md +40 -75
- package/.agents/skills/release-readiness/SKILL.md +1 -1
- package/.agents/skills/setup-macca-method/SKILL.md +79 -0
- package/.agents/skills/spec-audit/SKILL.md +21 -17
- package/.agents/skills/spec-compliance/SKILL.md +36 -18
- package/.agents/skills/spec-init/SKILL.md +53 -22
- package/CHANGELOG.md +31 -0
- package/README.md +92 -1195
- package/bin/macca-method.js +833 -286
- package/docs/configuration.md +174 -0
- package/docs/troubleshooting.md +180 -0
- package/docs/workflows.md +175 -0
- package/package.json +48 -4
- package/scripts/lib/npm-command.js +76 -0
- package/scripts/test-cli-setup.js +869 -0
- package/scripts/test-config.js +320 -0
- package/scripts/test-install.js +63 -28
- package/scripts/test-installer-safety.js +545 -0
- package/scripts/test-npm-command.js +143 -0
- package/scripts/test-package-safety.js +252 -0
- package/scripts/test-preferences.js +475 -0
- package/scripts/test-upgrade-legacy.js +11 -21
- package/scripts/validate-docs.js +166 -0
- package/scripts/validate-package.js +285 -0
- package/scripts/validate-skill-behavior.js +914 -12
- package/.agents/skills/antislop-copywriting/SKILL.md +0 -372
- package/.agents/skills/skill-creator/LICENSE.txt +0 -202
- package/.agents/skills/skill-creator/SKILL.md +0 -485
- package/.agents/skills/skill-creator/agents/analyzer.md +0 -274
- package/.agents/skills/skill-creator/agents/comparator.md +0 -202
- package/.agents/skills/skill-creator/agents/grader.md +0 -223
- package/.agents/skills/skill-creator/assets/eval_review.html +0 -146
- package/.agents/skills/skill-creator/eval-viewer/generate_review.py +0 -471
- package/.agents/skills/skill-creator/eval-viewer/viewer.html +0 -1325
- package/.agents/skills/skill-creator/references/schemas.md +0 -441
- package/.agents/skills/skill-creator/scripts/__init__.py +0 -0
- package/.agents/skills/skill-creator/scripts/aggregate_benchmark.py +0 -401
- package/.agents/skills/skill-creator/scripts/generate_report.py +0 -326
- package/.agents/skills/skill-creator/scripts/improve_description.py +0 -247
- package/.agents/skills/skill-creator/scripts/package_skill.py +0 -136
- package/.agents/skills/skill-creator/scripts/quick_validate.py +0 -103
- package/.agents/skills/skill-creator/scripts/run_eval.py +0 -310
- package/.agents/skills/skill-creator/scripts/run_loop.py +0 -328
- package/.agents/skills/skill-creator/scripts/utils.py +0 -47
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
"use strict";
|
|
4
|
+
|
|
5
|
+
const assert = require("node:assert/strict");
|
|
6
|
+
const fs = require("node:fs");
|
|
7
|
+
const path = require("node:path");
|
|
8
|
+
const vm = require("node:vm");
|
|
9
|
+
const { spawnSync } = require("node:child_process");
|
|
10
|
+
const validatorPath = path.resolve(
|
|
11
|
+
__dirname, "../.agents/skills/_shared/scripts/config-validator.js",
|
|
12
|
+
);
|
|
13
|
+
const { validateConfig, assertValidConfig } = require(validatorPath);
|
|
14
|
+
const source = fs.readFileSync(validatorPath, "utf8");
|
|
15
|
+
const loaderSource = fs.readFileSync(path.join(path.dirname(validatorPath), "config-file.js"), "utf8");
|
|
16
|
+
let passed = 0;
|
|
17
|
+
|
|
18
|
+
function test(name, action) {
|
|
19
|
+
action();
|
|
20
|
+
passed += 1;
|
|
21
|
+
process.stdout.write(`OK: ${name}\n`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function fields(value) {
|
|
25
|
+
return validateConfig(value).map((error) => error.field);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// CLI fixtures stay in memory: even the test suite does not write config files.
|
|
29
|
+
// A read-only fs stub also fails if the CLI attempts any other filesystem API.
|
|
30
|
+
function runCli(content, { args = ["SECRET-config.json"], readError, imported = false } = {}) {
|
|
31
|
+
const output = { stdout: "", stderr: "", exitCode: 0, reads: 0 };
|
|
32
|
+
const file = path.resolve(args[0] || "unused");
|
|
33
|
+
const bytes = Buffer.from(content);
|
|
34
|
+
let offset = 0;
|
|
35
|
+
const stat = (directory = false) => ({
|
|
36
|
+
dev: 1, ino: 2, size: bytes.length, mtimeMs: 10, ctimeMs: 10,
|
|
37
|
+
isFile: () => !directory, isDirectory: () => directory, isSymbolicLink: () => false,
|
|
38
|
+
});
|
|
39
|
+
const loader = { exports: {} };
|
|
40
|
+
vm.runInNewContext(loaderSource, {
|
|
41
|
+
module: loader, Buffer, process: { platform: process.platform },
|
|
42
|
+
require(name) {
|
|
43
|
+
if (name === "node:path") return path;
|
|
44
|
+
assert.equal(name, "node:fs");
|
|
45
|
+
return Object.freeze({
|
|
46
|
+
constants: fs.constants,
|
|
47
|
+
lstatSync(target) {
|
|
48
|
+
if (target === file && readError) throw readError;
|
|
49
|
+
return stat(target !== file);
|
|
50
|
+
},
|
|
51
|
+
openSync(target) { assert.equal(target, file); return 123; },
|
|
52
|
+
fstatSync(fd) { assert.equal(fd, 123); return stat(); },
|
|
53
|
+
readSync(fd, buffer, start, length) {
|
|
54
|
+
assert.equal(fd, 123);
|
|
55
|
+
output.reads += 1;
|
|
56
|
+
const count = Math.min(length, bytes.length - offset);
|
|
57
|
+
bytes.copy(buffer, start, offset, offset + count);
|
|
58
|
+
offset += count;
|
|
59
|
+
return count;
|
|
60
|
+
},
|
|
61
|
+
closeSync(fd) { assert.equal(fd, 123); },
|
|
62
|
+
});
|
|
63
|
+
},
|
|
64
|
+
}, { timeout: 5000 });
|
|
65
|
+
const module = { exports: {} };
|
|
66
|
+
const requireStub = (name) => {
|
|
67
|
+
assert.equal(name, "./config-file.js");
|
|
68
|
+
return loader.exports;
|
|
69
|
+
};
|
|
70
|
+
requireStub.main = imported ? {} : module;
|
|
71
|
+
const processStub = {
|
|
72
|
+
argv: ["node", validatorPath, ...args],
|
|
73
|
+
stdout: { write: (text) => { output.stdout += text; } },
|
|
74
|
+
stderr: { write: (text) => { output.stderr += text; } },
|
|
75
|
+
get exitCode() { return output.exitCode; },
|
|
76
|
+
set exitCode(value) { output.exitCode = value; },
|
|
77
|
+
};
|
|
78
|
+
vm.runInNewContext(source, {
|
|
79
|
+
module, require: requireStub, process: processStub,
|
|
80
|
+
}, { filename: validatorPath, timeout: 5000 });
|
|
81
|
+
return output;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
test("optional fields and complete current config", () => {
|
|
85
|
+
assert.deepEqual(validateConfig({}), []);
|
|
86
|
+
assert.deepEqual(validateConfig({
|
|
87
|
+
name: "", project: "",
|
|
88
|
+
languagePreferences: {
|
|
89
|
+
communication: { raw: "English", normalized: "english" },
|
|
90
|
+
documents: { raw: "Bahasa Indonesia", normalized: "indonesian" },
|
|
91
|
+
},
|
|
92
|
+
developerPreferences: { workMode: "direct", scope: "fullstack" },
|
|
93
|
+
brainstormPreferences: {
|
|
94
|
+
discussionMode: "one-by-one", recommendations: true, discoveryDepth: "standard",
|
|
95
|
+
},
|
|
96
|
+
codeReviewPreferences: { fixMode: "report-first" },
|
|
97
|
+
additionalSkills: [{
|
|
98
|
+
name: "react", purpose: "UI", paths: { copilot: "a", opencode: "b", codex: "c" },
|
|
99
|
+
}],
|
|
100
|
+
availableMCPs: ["context7", "supabase"],
|
|
101
|
+
}), []);
|
|
102
|
+
assert.equal(assertValidConfig({}, "valid.json"), undefined);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("all documented enums remain accepted", () => {
|
|
106
|
+
const enums = {
|
|
107
|
+
developerPreferences: { workMode: ["direct", "plan-first"], scope: ["frontend", "backend", "fullstack"] },
|
|
108
|
+
brainstormPreferences: {
|
|
109
|
+
discussionMode: ["one-by-one", "three-at-a-time", "all-at-once"],
|
|
110
|
+
discoveryDepth: ["quick", "standard", "critical"], recommendations: [true, false],
|
|
111
|
+
},
|
|
112
|
+
codeReviewPreferences: { fixMode: ["report-first", "fix-then-report"] },
|
|
113
|
+
};
|
|
114
|
+
for (const [section, choices] of Object.entries(enums)) {
|
|
115
|
+
for (const [field, values] of Object.entries(choices)) {
|
|
116
|
+
for (const value of values) assert.deepEqual(validateConfig({ [section]: { [field]: value } }), []);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("language aliases and fallback strings are preserved verbatim", () => {
|
|
122
|
+
for (const language of [
|
|
123
|
+
"indonesian", "id", "indo", "indonesia", "bahasa indonesia",
|
|
124
|
+
"english", "en", "eng", "inggris", "bahasa inggris", " EN ", "", "custom language",
|
|
125
|
+
]) {
|
|
126
|
+
const config = { languagePreferences: {
|
|
127
|
+
communication: { raw: language, normalized: language }, documents: {},
|
|
128
|
+
} };
|
|
129
|
+
const before = JSON.stringify(config);
|
|
130
|
+
assert.deepEqual(validateConfig(config), []);
|
|
131
|
+
assert.equal(JSON.stringify(config), before);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("legacy paths and explicit MCP denial are supported", () => {
|
|
136
|
+
const skill = { name: "legacy", purpose: "", paths: { futureHost: "" } };
|
|
137
|
+
for (const key of ["path", "githubPath", "opencodePath", "claudePath", "cursorPath",
|
|
138
|
+
"windsurfPath", "geminiPath", "kiloPath", "kimiPath", "codexPath"]) {
|
|
139
|
+
skill[key] = "legacy/SKILL.md";
|
|
140
|
+
assert.deepEqual(fields({ additionalSkills: [{ name: "legacy", [key]: 12 }] }), [
|
|
141
|
+
`additionalSkills[0].${key}`,
|
|
142
|
+
]);
|
|
143
|
+
}
|
|
144
|
+
assert.deepEqual(validateConfig({ additionalSkills: [skill], availableMCPs: "none" }), []);
|
|
145
|
+
assert.deepEqual(validateConfig({ additionalSkills: [], availableMCPs: [] }), []);
|
|
146
|
+
assert.deepEqual(validateConfig({ availableMCPs: ["none"] }), []);
|
|
147
|
+
assert.deepEqual(fields({ additionalSkills: ["legacy"] }), ["additionalSkills[0]"]);
|
|
148
|
+
assert.deepEqual(fields({ availableMCPs: "context7" }), ["availableMCPs"]);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test("root and nested objects reject null, arrays, and primitives", () => {
|
|
152
|
+
for (const bad of [null, [], "x", 7, true, undefined]) {
|
|
153
|
+
assert.deepEqual(fields(bad), ["$"]);
|
|
154
|
+
for (const key of ["languagePreferences", "developerPreferences", "brainstormPreferences", "codeReviewPreferences"]) {
|
|
155
|
+
assert.deepEqual(fields({ [key]: bad }), [key]);
|
|
156
|
+
}
|
|
157
|
+
for (const key of ["communication", "documents"]) {
|
|
158
|
+
assert.deepEqual(fields({ languagePreferences: { [key]: bad } }), [`languagePreferences.${key}`]);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("nested errors accumulate with exact field and array positions", () => {
|
|
164
|
+
assert.deepEqual(fields({
|
|
165
|
+
name: null, project: [],
|
|
166
|
+
languagePreferences: { communication: { raw: [], normalized: false }, documents: null },
|
|
167
|
+
developerPreferences: { workMode: "unsafe", scope: null },
|
|
168
|
+
brainstormPreferences: { discussionMode: [], recommendations: "true", discoveryDepth: "unsafe" },
|
|
169
|
+
codeReviewPreferences: { fixMode: "unsafe" },
|
|
170
|
+
additionalSkills: [null, {}, { name: " ", purpose: 7, paths: null }, { name: "x", paths: { codex: [] } }],
|
|
171
|
+
availableMCPs: ["context7", null, " ", 4],
|
|
172
|
+
}), [
|
|
173
|
+
"name", "project", "languagePreferences.communication.raw",
|
|
174
|
+
"languagePreferences.communication.normalized", "languagePreferences.documents",
|
|
175
|
+
"developerPreferences.workMode", "developerPreferences.scope",
|
|
176
|
+
"brainstormPreferences.discussionMode", "brainstormPreferences.recommendations",
|
|
177
|
+
"brainstormPreferences.discoveryDepth", "codeReviewPreferences.fixMode",
|
|
178
|
+
"additionalSkills[0]", "additionalSkills[1].name", "additionalSkills[2].name",
|
|
179
|
+
"additionalSkills[2].purpose", "additionalSkills[2].paths", "additionalSkills[3].paths[0]",
|
|
180
|
+
"availableMCPs[1]", "availableMCPs[2]", "availableMCPs[3]",
|
|
181
|
+
]);
|
|
182
|
+
for (const bad of [null, {}, "", false]) {
|
|
183
|
+
assert.deepEqual(fields({ additionalSkills: bad }), ["additionalSkills"]);
|
|
184
|
+
assert.deepEqual(fields({ availableMCPs: bad }), ["availableMCPs"]);
|
|
185
|
+
}
|
|
186
|
+
assert.deepEqual(fields({ availableMCPs: new Array(1) }), ["availableMCPs[0]"]);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test("unknown fields at every schema level are untouched and never evaluated", () => {
|
|
190
|
+
const extension = { get secret() { throw new Error("extension evaluated"); } };
|
|
191
|
+
const language = { raw: "custom", normalized: "custom" };
|
|
192
|
+
const skill = { name: "x", paths: { newHost: "x" } };
|
|
193
|
+
const config = {
|
|
194
|
+
languagePreferences: { communication: language, documents: {} },
|
|
195
|
+
developerPreferences: {}, brainstormPreferences: {}, codeReviewPreferences: {},
|
|
196
|
+
additionalSkills: [skill], availableMCPs: [],
|
|
197
|
+
};
|
|
198
|
+
const levels = [config, config.languagePreferences, language, config.languagePreferences.documents,
|
|
199
|
+
config.developerPreferences, config.brainstormPreferences, config.codeReviewPreferences, skill];
|
|
200
|
+
for (const level of levels) {
|
|
201
|
+
level.extension = extension;
|
|
202
|
+
Object.defineProperty(level, "unknownAccessor", {
|
|
203
|
+
enumerable: true, get() { throw new Error("unknown accessor evaluated"); },
|
|
204
|
+
});
|
|
205
|
+
Object.freeze(level);
|
|
206
|
+
}
|
|
207
|
+
assert.deepEqual(validateConfig(config), []);
|
|
208
|
+
assertValidConfig(config);
|
|
209
|
+
for (const level of levels) assert.equal(level.extension, extension);
|
|
210
|
+
const special = JSON.parse('{"__proto__":{"polluted":true},"constructor":null,"prototype":[]}');
|
|
211
|
+
const before = JSON.stringify(special);
|
|
212
|
+
assert.deepEqual(validateConfig(special), []);
|
|
213
|
+
assert.equal(JSON.stringify(special), before);
|
|
214
|
+
assert.equal(Object.prototype.polluted, undefined);
|
|
215
|
+
assert.deepEqual(fields(Object.assign(Object.create({ name: 42 }), { project: "" })), []);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test("known accessors are rejected without execution", () => {
|
|
219
|
+
const config = { get name() { throw new Error("must not execute"); } };
|
|
220
|
+
assert.deepEqual(fields(config), ["name"]);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test("assertion errors expose code and fields but never hostile values or map keys", () => {
|
|
224
|
+
const secret = "SECRET-credential-123\n\u001b[31m";
|
|
225
|
+
const config = {
|
|
226
|
+
name: { secret }, developerPreferences: { workMode: secret },
|
|
227
|
+
additionalSkills: [{ name: "x", paths: { [secret]: { secret } } }],
|
|
228
|
+
};
|
|
229
|
+
const before = JSON.stringify(config);
|
|
230
|
+
const diagnostics = validateConfig(config);
|
|
231
|
+
assert.equal(JSON.stringify(config), before);
|
|
232
|
+
for (const error of diagnostics) assert.deepEqual(Object.keys(error), ["field", "message"]);
|
|
233
|
+
assert(!JSON.stringify(diagnostics).includes("SECRET"));
|
|
234
|
+
assert.throws(() => assertValidConfig(config, "project/.agents/developer-config.json"), (error) => {
|
|
235
|
+
assert.equal(error.code, "MACCA_CONFIG_INVALID");
|
|
236
|
+
assert.equal(error.field, "name");
|
|
237
|
+
assert.deepEqual(error.fields, ["name", "developerPreferences.workMode", "additionalSkills[0].paths[0]"]);
|
|
238
|
+
assert.match(error.message, /project\/\.agents\/developer-config\.json/);
|
|
239
|
+
assert(!`${error.stack}${JSON.stringify(error)}`.includes("SECRET"));
|
|
240
|
+
return true;
|
|
241
|
+
});
|
|
242
|
+
assert.throws(() => assertValidConfig(null, "config\n\u001b.json"), (error) => {
|
|
243
|
+
assert(error.message.includes('"config\\n\\u001b.json"'));
|
|
244
|
+
return true;
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test("requiring the module has no CLI side effects and exports only the agreed API", () => {
|
|
249
|
+
assert.deepEqual(Object.keys(require(validatorPath)).sort(), ["assertValidConfig", "validateConfig"]);
|
|
250
|
+
assert.deepEqual(runCli("not JSON", { imported: true }), { stdout: "", stderr: "", exitCode: 0, reads: 0 });
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test("CLI success is human-readable and read-only", () => {
|
|
254
|
+
assert.deepEqual(runCli('{"name":"SECRET","extension":{"token":"PRIVATE"}}'), {
|
|
255
|
+
stdout: 'Valid config.\n', stderr: "", exitCode: 0, reads: 2,
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test("CLI schema and JSON diagnostics redact configuration and parser excerpts", () => {
|
|
260
|
+
for (const content of [
|
|
261
|
+
'{"developerPreferences":{"workMode":"SECRET"}}',
|
|
262
|
+
'{"additionalSkills":[{"name":"x","paths":{"SECRET":false}}]}',
|
|
263
|
+
'{"name":"SECRET", broken JSON}',
|
|
264
|
+
"SECRET invalid JSON", "null", "[]",
|
|
265
|
+
]) {
|
|
266
|
+
const result = runCli(content);
|
|
267
|
+
assert.equal(result.exitCode, 1);
|
|
268
|
+
assert.equal(result.stdout, "");
|
|
269
|
+
assert.match(result.stderr, /Invalid config:/);
|
|
270
|
+
assert(!result.stderr.includes("SECRET"));
|
|
271
|
+
}
|
|
272
|
+
assert.match(runCli('{"availableMCPs":[false]}').stderr, /availableMCPs\[0\]/);
|
|
273
|
+
assert.match(runCli("malformed").stderr, /\$: must contain valid JSON/);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test("CLI missing/unreadable files and usage return exit 1 with no raw exceptions", () => {
|
|
277
|
+
for (const code of ["ENOENT", "EACCES", "EISDIR"]) {
|
|
278
|
+
const readError = Object.assign(new Error("SECRET filesystem excerpt"), { code });
|
|
279
|
+
const result = runCli("", { readError });
|
|
280
|
+
assert.equal(result.exitCode, 1);
|
|
281
|
+
assert.equal(result.stdout, "");
|
|
282
|
+
assert.match(result.stderr, code === "ENOENT" ? /Missing config file/ : /Cannot inspect config file/);
|
|
283
|
+
assert(!result.stderr.includes("SECRET"));
|
|
284
|
+
}
|
|
285
|
+
for (const args of [[], ["one.json", "SECRET"]]) {
|
|
286
|
+
const result = runCli("", { args });
|
|
287
|
+
assert.equal(result.exitCode, 1);
|
|
288
|
+
assert.equal(result.reads, 0);
|
|
289
|
+
assert.match(result.stderr, /^Usage:/);
|
|
290
|
+
assert(!result.stderr.includes("SECRET"));
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
test("real Node CLI exits correctly without creating fixture files", () => {
|
|
295
|
+
const usage = spawnSync(process.execPath, [validatorPath], { encoding: "utf8", timeout: 10000 });
|
|
296
|
+
assert.ifError(usage.error);
|
|
297
|
+
assert.equal(usage.status, 1);
|
|
298
|
+
assert.match(usage.stderr, /^Usage:/);
|
|
299
|
+
// package.json is also a valid extension-rich config object (name is a string).
|
|
300
|
+
const packagePath = fs.realpathSync(path.resolve(__dirname, "../package.json"));
|
|
301
|
+
for (const [file, status] of [[packagePath, 0], [fs.realpathSync(validatorPath), 1]]) {
|
|
302
|
+
const before = fs.readFileSync(file);
|
|
303
|
+
const result = spawnSync(process.execPath, [validatorPath, file], {
|
|
304
|
+
encoding: "utf8", timeout: 10000,
|
|
305
|
+
});
|
|
306
|
+
assert.ifError(result.error);
|
|
307
|
+
assert.equal(result.status, status, result.stderr);
|
|
308
|
+
assert.deepEqual(fs.readFileSync(file), before);
|
|
309
|
+
assert.match(status === 0 ? result.stdout : result.stderr, status === 0 ? /^Valid config/ : /must contain valid JSON/);
|
|
310
|
+
}
|
|
311
|
+
const missing = path.join(fs.realpathSync(__dirname), "__macca_config_missing_fixture__.json");
|
|
312
|
+
assert.equal(fs.existsSync(missing), false);
|
|
313
|
+
const result = spawnSync(process.execPath, [validatorPath, missing], { encoding: "utf8", timeout: 10000 });
|
|
314
|
+
assert.ifError(result.error);
|
|
315
|
+
assert.equal(result.status, 1);
|
|
316
|
+
assert.match(result.stderr, /^Missing config file/);
|
|
317
|
+
assert.equal(fs.existsSync(missing), false);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
process.stdout.write(`\n${passed} config validator tests passed.\n`);
|
package/scripts/test-install.js
CHANGED
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
"use strict";
|
|
4
4
|
|
|
5
5
|
const fs = require("node:fs");
|
|
6
|
+
const assert = require("node:assert/strict");
|
|
6
7
|
const crypto = require("node:crypto");
|
|
7
8
|
const os = require("node:os");
|
|
8
9
|
const path = require("node:path");
|
|
9
10
|
const { execFileSync } = require("node:child_process");
|
|
11
|
+
const { resolveNpmCommand } = require("./lib/npm-command.js");
|
|
10
12
|
|
|
11
13
|
const rootDir = path.resolve(__dirname, "..");
|
|
12
14
|
const mode = process.argv.includes("--published") ? "published" : "local";
|
|
@@ -19,12 +21,11 @@ const symlinkDir = path.join(tmpDir, "symlink-project");
|
|
|
19
21
|
const driftDir = path.join(tmpDir, "drift-project");
|
|
20
22
|
let packageSpec = "macca-method";
|
|
21
23
|
|
|
22
|
-
function
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
return base === "npm" || base === "npx" ? `${base}.cmd` : base;
|
|
24
|
+
function publishedPackageSpec() {
|
|
25
|
+
const version = String(process.env.MACCA_PUBLISHED_VERSION || "")
|
|
26
|
+
.trim()
|
|
27
|
+
.replace(/^v/, "");
|
|
28
|
+
return version ? `macca-method@${version}` : "macca-method@latest";
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
function resolveTempRoot() {
|
|
@@ -37,22 +38,13 @@ function resolveTempRoot() {
|
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
function runNpm(args, options = {}) {
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
return capture(process.execPath, [npmExecPath, ...args], options);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
return capture(commandName("npm"), args, options);
|
|
41
|
+
const npm = resolveNpmCommand(args);
|
|
42
|
+
return capture(npm.command, npm.args, options);
|
|
46
43
|
}
|
|
47
44
|
|
|
48
45
|
function runNpmWithInheritedStdio(args, options = {}) {
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
run(process.execPath, [npmExecPath, ...args], options);
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
run(commandName("npm"), args, options);
|
|
46
|
+
const npm = resolveNpmCommand(args);
|
|
47
|
+
run(npm.command, npm.args, options);
|
|
56
48
|
}
|
|
57
49
|
|
|
58
50
|
function run(command, args, options = {}) {
|
|
@@ -60,6 +52,7 @@ function run(command, args, options = {}) {
|
|
|
60
52
|
cwd: rootDir,
|
|
61
53
|
stdio: "inherit",
|
|
62
54
|
...options,
|
|
55
|
+
shell: false,
|
|
63
56
|
});
|
|
64
57
|
}
|
|
65
58
|
|
|
@@ -68,6 +61,7 @@ function capture(command, args, options = {}) {
|
|
|
68
61
|
cwd: rootDir,
|
|
69
62
|
encoding: "utf8",
|
|
70
63
|
...options,
|
|
64
|
+
shell: false,
|
|
71
65
|
});
|
|
72
66
|
}
|
|
73
67
|
|
|
@@ -77,7 +71,7 @@ function runCli(args) {
|
|
|
77
71
|
"exec",
|
|
78
72
|
"--yes",
|
|
79
73
|
"--package",
|
|
80
|
-
|
|
74
|
+
publishedPackageSpec(),
|
|
81
75
|
"--",
|
|
82
76
|
"macca-method",
|
|
83
77
|
...args,
|
|
@@ -104,7 +98,7 @@ function expectCliFailure(args, expectedText) {
|
|
|
104
98
|
"exec",
|
|
105
99
|
"--yes",
|
|
106
100
|
"--package",
|
|
107
|
-
|
|
101
|
+
publishedPackageSpec(),
|
|
108
102
|
"--",
|
|
109
103
|
"macca-method",
|
|
110
104
|
...args,
|
|
@@ -143,6 +137,14 @@ function assertPathExists(targetPath) {
|
|
|
143
137
|
}
|
|
144
138
|
}
|
|
145
139
|
|
|
140
|
+
function assertInstalledSkills(skillsRoot) {
|
|
141
|
+
const expected = JSON.parse(fs.readFileSync(path.join(rootDir, ".agents", "macca-lock.json"), "utf8")).skills;
|
|
142
|
+
for (const skill of expected.filter((name) => name !== "_shared")) {
|
|
143
|
+
assertPathExists(path.join(skillsRoot, skill, "SKILL.md"));
|
|
144
|
+
}
|
|
145
|
+
assertPathExists(path.join(skillsRoot, "_shared", "scripts", "config-validator.js"));
|
|
146
|
+
}
|
|
147
|
+
|
|
146
148
|
function readNonEmptyLines(filePath) {
|
|
147
149
|
return fs
|
|
148
150
|
.readFileSync(filePath, "utf8")
|
|
@@ -183,7 +185,7 @@ function ensureExpectedTools(filePath) {
|
|
|
183
185
|
}
|
|
184
186
|
|
|
185
187
|
function resolveLocalPackage() {
|
|
186
|
-
const packOutput = runNpm(["pack", "--json", "--pack-destination", tmpDir]);
|
|
188
|
+
const packOutput = runNpm(["pack", "--json", "--ignore-scripts", "--pack-destination", tmpDir]);
|
|
187
189
|
const packageList = JSON.parse(packOutput);
|
|
188
190
|
|
|
189
191
|
if (
|
|
@@ -195,6 +197,7 @@ function resolveLocalPackage() {
|
|
|
195
197
|
}
|
|
196
198
|
|
|
197
199
|
packageSpec = path.join(tmpDir, packageList[0].filename);
|
|
200
|
+
run(process.execPath, [path.join(rootDir, "scripts", "validate-package.js"), packageSpec]);
|
|
198
201
|
}
|
|
199
202
|
|
|
200
203
|
function main() {
|
|
@@ -269,16 +272,26 @@ function main() {
|
|
|
269
272
|
const outerReal = path.join(tmpDir, "outer-real-project");
|
|
270
273
|
const outerLink = path.join(tmpDir, "outer-link-project");
|
|
271
274
|
fs.mkdirSync(outerReal, { recursive: true });
|
|
275
|
+
let junctionCreated = false;
|
|
272
276
|
try {
|
|
273
|
-
|
|
277
|
+
fs.symlinkSync(outerReal, outerLink, "junction");
|
|
278
|
+
junctionCreated = true;
|
|
279
|
+
} catch (error) {
|
|
280
|
+
if (!["EPERM", "EACCES"].includes(error.code)) throw error;
|
|
281
|
+
process.stdout.write(
|
|
282
|
+
" Skipping Windows junction ancestry test (junction creation not permitted)\n",
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
if (junctionCreated) {
|
|
274
286
|
expectCliFailure(
|
|
275
287
|
["install", "--yes", "--tool", "codex", "--directory", outerLink],
|
|
276
288
|
"Refusing symlinked target project ancestor",
|
|
277
289
|
);
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
290
|
+
if (fs.readdirSync(outerReal).length !== 0) {
|
|
291
|
+
throw new Error(
|
|
292
|
+
"Target directory junction test wrote into the real directory",
|
|
293
|
+
);
|
|
294
|
+
}
|
|
282
295
|
}
|
|
283
296
|
}
|
|
284
297
|
|
|
@@ -308,6 +321,14 @@ function main() {
|
|
|
308
321
|
"github-copilot",
|
|
309
322
|
"--directory",
|
|
310
323
|
projectDir,
|
|
324
|
+
"--name",
|
|
325
|
+
"Initial User",
|
|
326
|
+
"--project",
|
|
327
|
+
"Initial Project",
|
|
328
|
+
"--communication-language",
|
|
329
|
+
"Unsupported Language",
|
|
330
|
+
"--document-language",
|
|
331
|
+
"Unsupported Language",
|
|
311
332
|
]);
|
|
312
333
|
|
|
313
334
|
const configPath = path.join(
|
|
@@ -316,6 +337,12 @@ function main() {
|
|
|
316
337
|
"developer-config.json",
|
|
317
338
|
);
|
|
318
339
|
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
340
|
+
if (
|
|
341
|
+
config.languagePreferences.communication.normalized !== "indonesian" ||
|
|
342
|
+
config.languagePreferences.documents.normalized !== "indonesian"
|
|
343
|
+
) {
|
|
344
|
+
throw new Error("Unknown language did not use the Indonesian fallback");
|
|
345
|
+
}
|
|
319
346
|
config.name = "Preserved Name";
|
|
320
347
|
config.project = "Preserved Project";
|
|
321
348
|
config.languagePreferences.communication = {
|
|
@@ -341,7 +368,7 @@ function main() {
|
|
|
341
368
|
projectDir,
|
|
342
369
|
]);
|
|
343
370
|
|
|
344
|
-
run(
|
|
371
|
+
run(process.execPath, [
|
|
345
372
|
path.join(rootDir, "scripts", "run-skill-validator.js"),
|
|
346
373
|
path.join(
|
|
347
374
|
projectDir,
|
|
@@ -359,6 +386,11 @@ function main() {
|
|
|
359
386
|
"macca-managed-skills.txt",
|
|
360
387
|
);
|
|
361
388
|
const originalManaged = fs.readFileSync(managedPath, "utf8");
|
|
389
|
+
assert.deepEqual(
|
|
390
|
+
readNonEmptyLines(managedPath).sort(),
|
|
391
|
+
readNonEmptyLines(path.join(rootDir, ".agents", "macca-managed-skills.txt")).sort(),
|
|
392
|
+
"Installed manifest must include every official managed skill",
|
|
393
|
+
);
|
|
362
394
|
fs.writeFileSync(managedPath, `${originalManaged}../../outside\n`, "utf8");
|
|
363
395
|
expectCliFailure(
|
|
364
396
|
["upgrade", "--directory", projectDir],
|
|
@@ -509,6 +541,7 @@ function main() {
|
|
|
509
541
|
["upgrade", "--directory", driftDir],
|
|
510
542
|
"locally modified managed skill",
|
|
511
543
|
);
|
|
544
|
+
runCli(["upgrade", "--directory", driftDir, "--force"]);
|
|
512
545
|
|
|
513
546
|
assertPathExists(path.join(projectDir, ".agents", "developer-config.json"));
|
|
514
547
|
assertPathExists(
|
|
@@ -536,6 +569,7 @@ function main() {
|
|
|
536
569
|
path.join(projectDir, ".opencode", "skills"),
|
|
537
570
|
]) {
|
|
538
571
|
assertPathExists(path.join(skillsRoot, "meet", "SKILL.md"));
|
|
572
|
+
assertInstalledSkills(skillsRoot);
|
|
539
573
|
}
|
|
540
574
|
assertPathExists(customSkill);
|
|
541
575
|
assertPathExists(path.join(projectDir, ".agents", "macca-lock.json"));
|
|
@@ -576,6 +610,7 @@ function main() {
|
|
|
576
610
|
path.join(allToolsDir, ".kilo", "skills", "meet", "SKILL.md"),
|
|
577
611
|
]) {
|
|
578
612
|
assertPathExists(expectedPath);
|
|
613
|
+
assertInstalledSkills(path.dirname(path.dirname(expectedPath)));
|
|
579
614
|
}
|
|
580
615
|
|
|
581
616
|
if (mode === "local") {
|