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,143 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const assert = require("node:assert/strict");
|
|
5
|
+
const fs = require("node:fs");
|
|
6
|
+
const path = require("node:path");
|
|
7
|
+
const vm = require("node:vm");
|
|
8
|
+
|
|
9
|
+
const source = fs.readFileSync(path.join(__dirname, "lib", "npm-command.js"), "utf8");
|
|
10
|
+
let checks = 0;
|
|
11
|
+
|
|
12
|
+
function fixture({ platform = "win32", env = {}, execPath = "C:\\Node JS\\node.exe",
|
|
13
|
+
files = [], links = {}, directories = [], where = [], whereError = false } = {}) {
|
|
14
|
+
const calls = [];
|
|
15
|
+
const module = { exports: {} };
|
|
16
|
+
const filesystem = {
|
|
17
|
+
realpathSync(candidate) {
|
|
18
|
+
if (Object.hasOwn(links, candidate)) return links[candidate];
|
|
19
|
+
if (files.includes(candidate) || directories.includes(candidate)) return candidate;
|
|
20
|
+
throw new Error("ENOENT");
|
|
21
|
+
},
|
|
22
|
+
statSync(candidate) {
|
|
23
|
+
if (!files.includes(candidate) && !directories.includes(candidate)) throw new Error("ENOENT");
|
|
24
|
+
return { isFile: () => files.includes(candidate) };
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
vm.runInNewContext(source, {
|
|
28
|
+
module, process: { platform, env, execPath },
|
|
29
|
+
require(name) {
|
|
30
|
+
if (name === "node:path") return platform === "win32" ? path.win32 : path.posix;
|
|
31
|
+
if (name === "node:fs") return filesystem;
|
|
32
|
+
assert.equal(name, "node:child_process");
|
|
33
|
+
return { execFileSync(command, args, options) {
|
|
34
|
+
calls.push({ command, args: Array.from(args), options });
|
|
35
|
+
assert.equal(command, "where.exe");
|
|
36
|
+
assert.deepEqual(Array.from(args), ["npm"]);
|
|
37
|
+
assert.equal(options.shell, false);
|
|
38
|
+
if (whereError) throw new Error("where unavailable");
|
|
39
|
+
return where.join("\r\n");
|
|
40
|
+
} };
|
|
41
|
+
},
|
|
42
|
+
}, { filename: "npm-command.js" });
|
|
43
|
+
return {
|
|
44
|
+
calls,
|
|
45
|
+
resolve(args = ["--version"]) {
|
|
46
|
+
const result = module.exports.resolveNpmCommand(args);
|
|
47
|
+
return { command: result.command, args: Array.from(result.args) };
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function test(name, action) {
|
|
53
|
+
action();
|
|
54
|
+
checks += 1;
|
|
55
|
+
process.stdout.write(`OK: ${name}\n`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
test("valid npm_execpath takes priority and preserves spaces and shell metacharacters", () => {
|
|
59
|
+
const cli = "C:\\Tools & Packages\\npm\\bin\\npm-cli.js";
|
|
60
|
+
const instance = fixture({ env: { npm_execpath: cli }, files: [cli] });
|
|
61
|
+
const args = Object.freeze(["pack", "C:\\project space\\a & b", "$(literal)", '"quoted"']);
|
|
62
|
+
assert.deepEqual(instance.resolve(args), { command: "C:\\Node JS\\node.exe", args: [cli, ...args] });
|
|
63
|
+
assert.equal(instance.calls.length, 0);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("invalid, missing, relative and other-manager npm_execpath fall back to Node-adjacent npm", () => {
|
|
67
|
+
const cli = "C:\\Node JS\\node_modules\\npm\\bin\\npm-cli.js";
|
|
68
|
+
for (const npmPath of [undefined, "C:\\absent\\npm-cli.js", "relative/npm-cli.js",
|
|
69
|
+
"C:\\tools\\npm.cmd", "C:\\tools\\pnpm.cjs", "C:\\directory\\npm-cli.js"]) {
|
|
70
|
+
const instance = fixture({
|
|
71
|
+
env: { npm_execpath: npmPath }, files: [cli, "C:\\tools\\npm.cmd", "C:\\tools\\pnpm.cjs"],
|
|
72
|
+
directories: ["C:\\directory\\npm-cli.js"],
|
|
73
|
+
});
|
|
74
|
+
assert.deepEqual(instance.resolve(), { command: "C:\\Node JS\\node.exe", args: [cli, "--version"] });
|
|
75
|
+
assert.equal(instance.calls.length, 0);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("npm_execpath symlink resolves to npm CLI before execution", () => {
|
|
80
|
+
const cli = "/opt/node/npm/bin/npm-cli.js";
|
|
81
|
+
const instance = fixture({ platform: "linux", execPath: "/usr/bin/node",
|
|
82
|
+
env: { npm_execpath: "/usr/bin/npm" }, files: [cli], links: { "/usr/bin/npm": cli } });
|
|
83
|
+
assert.deepEqual(instance.resolve(), { command: "/usr/bin/node", args: [cli, "--version"] });
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("Node executable realpath discovers version-manager installation", () => {
|
|
87
|
+
const cli = "C:\\versions\\22\\node_modules\\npm\\bin\\npm-cli.js";
|
|
88
|
+
const instance = fixture({ files: [cli, "C:\\versions\\22\\node.exe"],
|
|
89
|
+
links: { "C:\\Node JS\\node.exe": "C:\\versions\\22\\node.exe" } });
|
|
90
|
+
assert.equal(instance.resolve().args[0], cli);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("where npm discovers .cmd sibling payload without executing the shim", () => {
|
|
94
|
+
const cli = "C:\\User Tools\\node_modules\\npm\\bin\\npm-cli.js";
|
|
95
|
+
const instance = fixture({ files: [cli, "C:\\User Tools\\npm.cmd"],
|
|
96
|
+
where: ["C:\\missing\\npm.cmd", "C:\\User Tools\\npm.cmd"] });
|
|
97
|
+
assert.deepEqual(instance.resolve(), { command: "C:\\Node JS\\node.exe", args: [cli, "--version"] });
|
|
98
|
+
assert.equal(instance.calls.length, 1);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("where shim parents and real targets are inspected", () => {
|
|
102
|
+
const cli = "C:\\npm home\\node_modules\\npm\\bin\\npm-cli.js";
|
|
103
|
+
const instance = fixture({ files: [cli, "C:\\npm home\\bin\\npm.cmd"],
|
|
104
|
+
links: { "C:\\shims\\npm.cmd": "C:\\npm home\\bin\\npm.cmd" },
|
|
105
|
+
where: ["C:\\shims\\npm.cmd"] });
|
|
106
|
+
assert.equal(instance.resolve().args[0], cli);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("where realpath may directly identify npm CLI", () => {
|
|
110
|
+
const cli = "C:\\npm\\bin\\npm-cli.js";
|
|
111
|
+
const instance = fixture({ files: [cli], links: { "C:\\bin\\npm": cli }, where: ["C:\\bin\\npm"] });
|
|
112
|
+
assert.equal(instance.resolve().args[0], cli);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("POSIX lib/node_modules layout is supported", () => {
|
|
116
|
+
const cli = "/opt/node/lib/node_modules/npm/bin/npm-cli.js";
|
|
117
|
+
for (const platform of ["linux", "darwin"]) {
|
|
118
|
+
const instance = fixture({ platform, execPath: "/opt/node/bin/node", files: [cli] });
|
|
119
|
+
assert.deepEqual(instance.resolve(), { command: "/opt/node/bin/node", args: [cli, "--version"] });
|
|
120
|
+
assert.equal(instance.calls.length, 0);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("POSIX binary fallback keeps literal arguments without a shell", () => {
|
|
125
|
+
const instance = fixture({ platform: "linux", execPath: "/usr/bin/node" });
|
|
126
|
+
const args = ["pack", "/tmp/a b;$(literal)"];
|
|
127
|
+
const result = instance.resolve(args);
|
|
128
|
+
assert.deepEqual(result, { command: "npm", args });
|
|
129
|
+
assert.notEqual(result.args, args);
|
|
130
|
+
assert.equal(instance.calls.length, 0);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("Windows missing CLI fails descriptively, including failed where and bare shims", () => {
|
|
134
|
+
for (const options of [{}, { whereError: true },
|
|
135
|
+
{ files: ["C:\\shim\\npm.cmd"], where: ["C:\\shim\\npm.cmd"] },
|
|
136
|
+
{ where: ["npm.cmd", "relative\\npm-cli.js"] }]) {
|
|
137
|
+
const instance = fixture(options);
|
|
138
|
+
assert.throws(() => instance.resolve(), /Cannot locate npm's JavaScript CLI.*Install Node\.js with npm/);
|
|
139
|
+
assert.equal(instance.calls.length, 1);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
process.stdout.write(`OK: ${checks} npm command resolver tests passed\n`);
|
|
@@ -0,0 +1,252 @@
|
|
|
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 os = require("node:os");
|
|
8
|
+
const path = require("node:path");
|
|
9
|
+
const { execFileSync, spawnSync } = require("node:child_process");
|
|
10
|
+
const { gunzipSync, gzipSync } = require("node:zlib");
|
|
11
|
+
const { resolveNpmCommand } = require("./lib/npm-command.js");
|
|
12
|
+
const { documentationFiles, markdownLinks, headingAnchors, validateDocs } = require("./validate-docs.js");
|
|
13
|
+
|
|
14
|
+
const root = path.resolve(__dirname, "..");
|
|
15
|
+
const validator = path.join(root, "scripts", "validate-package.js");
|
|
16
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "macca-package-safety-"));
|
|
17
|
+
const fixture = path.join(tmp, "fixture");
|
|
18
|
+
let checks = 0;
|
|
19
|
+
|
|
20
|
+
function npm(args, cwd) {
|
|
21
|
+
const npm = resolveNpmCommand(args);
|
|
22
|
+
return execFileSync(
|
|
23
|
+
npm.command, npm.args,
|
|
24
|
+
{ cwd, encoding: "utf8", shell: false, stdio: ["ignore", "pipe", "pipe"] },
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function pack(directory) {
|
|
29
|
+
const [result] = JSON.parse(npm([
|
|
30
|
+
"pack", "--json", "--ignore-scripts", "--pack-destination", tmp,
|
|
31
|
+
], directory));
|
|
32
|
+
return { ...result, tarball: path.join(tmp, result.filename) };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function validate(script, args, expected = []) {
|
|
36
|
+
const result = spawnSync(process.execPath, [script, ...args], {
|
|
37
|
+
cwd: tmp,
|
|
38
|
+
encoding: "utf8",
|
|
39
|
+
});
|
|
40
|
+
assert.ifError(result.error);
|
|
41
|
+
const output = `${result.stdout}${result.stderr}`;
|
|
42
|
+
assert.equal(result.status, expected.length ? 1 : 0, output);
|
|
43
|
+
for (const text of expected) {
|
|
44
|
+
assert.ok(output.includes(text), `Missing diagnostic ${text}: ${output}`);
|
|
45
|
+
}
|
|
46
|
+
checks += 1;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
assert.deepEqual(markdownLinks([
|
|
51
|
+
"`[example](missing.md)` and ``[example](also-missing.md)``",
|
|
52
|
+
"~~~md", "[example](fenced.md)", "~~~",
|
|
53
|
+
" [guide](docs/workflows.md#baseline)",
|
|
54
|
+
"[reference][guide]", "[guide]: docs/configuration.md",
|
|
55
|
+
].join("\n")).map((link) => link.destination), [
|
|
56
|
+
"image.webp", "docs/workflows.md#baseline", "docs/configuration.md",
|
|
57
|
+
]);
|
|
58
|
+
assert.deepEqual([...headingAnchors([
|
|
59
|
+
"# Setup: `work_mode` & checks!", "# Repeat", "# Repeat", "# Repeat-1", "# Repeat",
|
|
60
|
+
"```md", "# Not a heading", "```", "Café guide", "----------",
|
|
61
|
+
].join("\n"))], ["setup-work_mode--checks", "repeat", "repeat-1", "repeat-1-1", "repeat-2", "café-guide"]);
|
|
62
|
+
checks += 1;
|
|
63
|
+
|
|
64
|
+
const docsFixture = path.join(tmp, "docs-fixture");
|
|
65
|
+
fs.mkdirSync(path.join(docsFixture, "docs"), { recursive: true });
|
|
66
|
+
for (const file of documentationFiles) fs.writeFileSync(path.join(docsFixture, file), "# Guide\n");
|
|
67
|
+
const docsReadme = path.join(docsFixture, "README.md");
|
|
68
|
+
const packagedDocs = new Set(documentationFiles);
|
|
69
|
+
fs.writeFileSync(docsReadme, "# Guide\n[workflow](docs/workflows.md#guide)\n`[not a link](missing.md)`\n");
|
|
70
|
+
assert.deepEqual(validateDocs(docsFixture, packagedDocs), []);
|
|
71
|
+
checks += 1;
|
|
72
|
+
fs.writeFileSync(docsReadme, "[bad anchor](docs/workflows.md#missing)\n\n");
|
|
73
|
+
const brokenLinks = validateDocs(docsFixture, packagedDocs);
|
|
74
|
+
assert.ok(brokenLinks.some((issue) => issue.includes("missing heading anchor")));
|
|
75
|
+
assert.ok(brokenLinks.some((issue) => issue.includes("missing local link target")));
|
|
76
|
+
checks += 1;
|
|
77
|
+
fs.writeFileSync(docsReadme, "[unpublished guide](docs/workflows.md#guide)\n");
|
|
78
|
+
assert.ok(validateDocs(docsFixture, new Set(["README.md"])).some((issue) => issue.includes("not packaged")));
|
|
79
|
+
checks += 1;
|
|
80
|
+
|
|
81
|
+
const original = pack(root);
|
|
82
|
+
validate(validator, [original.tarball]);
|
|
83
|
+
|
|
84
|
+
// Copy only the validated publication payload, never arbitrary workspace state.
|
|
85
|
+
for (const { path: relative } of original.files) {
|
|
86
|
+
const target = path.join(fixture, relative);
|
|
87
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
88
|
+
fs.copyFileSync(path.join(root, relative), target);
|
|
89
|
+
}
|
|
90
|
+
const packagePath = path.join(fixture, "package.json");
|
|
91
|
+
const metadata = JSON.parse(fs.readFileSync(packagePath, "utf8"));
|
|
92
|
+
// Directory inclusion and npm's automatic CHANGELOG inclusion must not mask
|
|
93
|
+
// a missing explicit publication declaration.
|
|
94
|
+
const explicitFiles = [
|
|
95
|
+
"CHANGELOG.md", ".agents/skills/_shared/scripts/read-preferences.js", "scripts/test-preferences.js",
|
|
96
|
+
];
|
|
97
|
+
for (const file of explicitFiles) {
|
|
98
|
+
fs.writeFileSync(packagePath, JSON.stringify({ ...metadata, files: metadata.files.filter((entry) => entry !== file) }));
|
|
99
|
+
validate(validator, [pack(fixture).tarball], [`package files must explicitly include ${file}`]);
|
|
100
|
+
}
|
|
101
|
+
fs.writeFileSync(packagePath, JSON.stringify(metadata));
|
|
102
|
+
const omittedDoc = "docs/workflows.md";
|
|
103
|
+
const docPath = path.join(fixture, omittedDoc);
|
|
104
|
+
const originalDoc = fs.readFileSync(docPath);
|
|
105
|
+
fs.rmSync(docPath);
|
|
106
|
+
const withoutDoc = pack(fixture);
|
|
107
|
+
assert.ok(!withoutDoc.files.some((file) => file.path === omittedDoc));
|
|
108
|
+
validate(validator, [withoutDoc.tarball], [`published package is missing ${omittedDoc}`]);
|
|
109
|
+
fs.writeFileSync(docPath, originalDoc);
|
|
110
|
+
const lockPath = path.join(fixture, ".agents", "macca-lock.json");
|
|
111
|
+
const manifestPath = path.join(fixture, ".agents", "macca-managed-skills.txt");
|
|
112
|
+
const originalLock = fs.readFileSync(lockPath, "utf8");
|
|
113
|
+
const originalManifest = fs.readFileSync(manifestPath, "utf8");
|
|
114
|
+
const staleLock = JSON.parse(originalLock);
|
|
115
|
+
staleLock.version = "0.0.0-stale-lock";
|
|
116
|
+
fs.writeFileSync(lockPath, JSON.stringify(staleLock));
|
|
117
|
+
validate(validator, [pack(fixture).tarball], ["package/lock version mismatch"]);
|
|
118
|
+
fs.writeFileSync(lockPath, originalLock);
|
|
119
|
+
fs.writeFileSync(manifestPath, originalManifest.replace(/^developer\r?\n/m, ""));
|
|
120
|
+
validate(validator, [pack(fixture).tarball], [
|
|
121
|
+
"macca-lock.json skills do not match macca-managed-skills.txt",
|
|
122
|
+
"managed skills do not match the official package contract",
|
|
123
|
+
]);
|
|
124
|
+
staleLock.version = metadata.version;
|
|
125
|
+
staleLock.skills = staleLock.skills.filter((skill) => skill !== "developer");
|
|
126
|
+
fs.writeFileSync(lockPath, JSON.stringify(staleLock));
|
|
127
|
+
validate(validator, [pack(fixture).tarball], [
|
|
128
|
+
"managed skills do not match the official package contract",
|
|
129
|
+
]);
|
|
130
|
+
fs.writeFileSync(manifestPath, originalManifest);
|
|
131
|
+
validate(validator, [pack(fixture).tarball], [
|
|
132
|
+
"macca-lock.json skills do not match macca-managed-skills.txt",
|
|
133
|
+
]);
|
|
134
|
+
fs.writeFileSync(lockPath, originalLock);
|
|
135
|
+
|
|
136
|
+
// Exercise archive parsing without relying on an installed tar executable.
|
|
137
|
+
const cleanTarball = pack(fixture).tarball;
|
|
138
|
+
const tar = gunzipSync(fs.readFileSync(cleanTarball));
|
|
139
|
+
const firstSize = Number.parseInt(tar.subarray(124, 136).toString("ascii"), 8);
|
|
140
|
+
const firstEnd = 512 + Math.ceil(firstSize / 512) * 512;
|
|
141
|
+
const firstPath = tar.subarray(0, 100).toString("utf8").split("\0")[0];
|
|
142
|
+
const crafted = path.join(tmp, "crafted.tgz");
|
|
143
|
+
function checkArchive(bytes, expected = []) {
|
|
144
|
+
fs.writeFileSync(crafted, gzipSync(bytes));
|
|
145
|
+
validate(validator, [crafted], expected);
|
|
146
|
+
}
|
|
147
|
+
function checksum(header) {
|
|
148
|
+
header.fill(32, 148, 156);
|
|
149
|
+
const value = header.reduce((sum, byte) => sum + byte, 0);
|
|
150
|
+
header.write(`${value.toString(8).padStart(6, "0")}\0 `, 148, 8, "ascii");
|
|
151
|
+
return header;
|
|
152
|
+
}
|
|
153
|
+
function paxEntry(value, type = "x") {
|
|
154
|
+
const record = `path=${value}\n`;
|
|
155
|
+
let length = Buffer.byteLength(record) + 2;
|
|
156
|
+
while (length !== Buffer.byteLength(record) + String(length).length + 1) {
|
|
157
|
+
length = Buffer.byteLength(record) + String(length).length + 1;
|
|
158
|
+
}
|
|
159
|
+
const body = Buffer.from(`${length} ${record}`);
|
|
160
|
+
const header = Buffer.from(tar.subarray(0, 512));
|
|
161
|
+
header.fill(0, 0, 100);
|
|
162
|
+
header.write("PaxHeader");
|
|
163
|
+
header.write(`${body.length.toString(8).padStart(11, "0")}\0`, 124, 12, "ascii");
|
|
164
|
+
header[156] = type.charCodeAt(0);
|
|
165
|
+
return Buffer.concat([checksum(header), body, Buffer.alloc((512 - body.length % 512) % 512)]);
|
|
166
|
+
}
|
|
167
|
+
// A PAX path override must take precedence over the regular header name.
|
|
168
|
+
const renamed = Buffer.from(tar);
|
|
169
|
+
renamed.fill(0, 0, 100);
|
|
170
|
+
renamed.write("package/placeholder");
|
|
171
|
+
checksum(renamed.subarray(0, 512));
|
|
172
|
+
checkArchive(Buffer.concat([paxEntry(firstPath), renamed]));
|
|
173
|
+
const longPath = `package/${"long-private-path-".repeat(8)}.md`;
|
|
174
|
+
checkArchive(Buffer.concat([paxEntry(longPath), tar]), [
|
|
175
|
+
`published package includes unauthorized file ${longPath.slice(8)}`,
|
|
176
|
+
]);
|
|
177
|
+
checkArchive(Buffer.concat([paxEntry("package/../private.md"), tar]), [
|
|
178
|
+
"non-canonical package path",
|
|
179
|
+
]);
|
|
180
|
+
checkArchive(Buffer.concat([paxEntry(firstPath, "g"), tar]), ["unsupported tar entry type"]);
|
|
181
|
+
checkArchive(Buffer.concat([tar.subarray(0, firstEnd), tar]), ["duplicate package path"]);
|
|
182
|
+
const link = Buffer.from(tar);
|
|
183
|
+
link[156] = 50;
|
|
184
|
+
checksum(link.subarray(0, 512));
|
|
185
|
+
checkArchive(link, ["unsupported tar entry type"]);
|
|
186
|
+
const badChecksum = Buffer.from(tar);
|
|
187
|
+
badChecksum[0] ^= 1;
|
|
188
|
+
checkArchive(badChecksum, ["tar checksum mismatch"]);
|
|
189
|
+
checkArchive(tar.subarray(0, firstEnd - 1), ["truncated tar block"]);
|
|
190
|
+
|
|
191
|
+
metadata.scripts.prepack = 'node -e "process.exit(97)"';
|
|
192
|
+
metadata.scripts.postpack = 'node -e "process.exit(97)"';
|
|
193
|
+
const unauthorized = [
|
|
194
|
+
"docs/private.md",
|
|
195
|
+
"bin/credentials.json",
|
|
196
|
+
".agents/skills/developer/private.md",
|
|
197
|
+
".agents/skills/setup-macca-method/private.md",
|
|
198
|
+
".agents/skills/_shared/references/secrets.md",
|
|
199
|
+
".agents/skills/brainstorm-prd/assets/private.template.md",
|
|
200
|
+
".agents/skills/unofficial/SKILL.md",
|
|
201
|
+
".agents/developer-config.json",
|
|
202
|
+
"evals/results/report.json",
|
|
203
|
+
"evals/acceptance-workspace/transcript.md",
|
|
204
|
+
];
|
|
205
|
+
metadata.files.push(".agents/skills/unofficial/", ".agents/developer-config.json", "docs/private.md", "evals/");
|
|
206
|
+
fs.writeFileSync(packagePath, `${JSON.stringify(metadata, null, 2)}\n`);
|
|
207
|
+
for (const relative of unauthorized) {
|
|
208
|
+
const target = path.join(fixture, relative);
|
|
209
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
210
|
+
fs.writeFileSync(target, "Synthetic private fixture; never publish.\n");
|
|
211
|
+
}
|
|
212
|
+
const unexpectedDiagnostics = unauthorized.map(
|
|
213
|
+
(file) => `published package includes unauthorized file ${file}`,
|
|
214
|
+
);
|
|
215
|
+
// The current fixture is invalid, but the earlier tarball is still valid.
|
|
216
|
+
const fixtureValidator = path.join(fixture, "scripts", "validate-package.js");
|
|
217
|
+
validate(fixtureValidator, [original.tarball]);
|
|
218
|
+
validate(fixtureValidator, [], unexpectedDiagnostics);
|
|
219
|
+
const contaminated = pack(fixture);
|
|
220
|
+
assert.ok(contaminated.files.some((file) => file.path === "docs/private.md"),
|
|
221
|
+
"private documentation fixture must actually be packed to exercise rejection");
|
|
222
|
+
for (const relative of unauthorized) {
|
|
223
|
+
fs.rmSync(path.join(fixture, relative));
|
|
224
|
+
}
|
|
225
|
+
// Cleaning the source cannot hide files already present in the actual artifact.
|
|
226
|
+
validate(validator, [contaminated.tarball], unexpectedDiagnostics);
|
|
227
|
+
|
|
228
|
+
const missing = original.files.map((entry) => entry.path).filter((file) =>
|
|
229
|
+
file.startsWith(".agents/skills/_shared/") ||
|
|
230
|
+
file.includes("/assets/") || file.includes("/references/") ||
|
|
231
|
+
file === ".agents/legacy-payloads.json" ||
|
|
232
|
+
file === ".agents/skills/developer/SKILL.md" ||
|
|
233
|
+
file === ".agents/skills/setup-macca-method/SKILL.md" ||
|
|
234
|
+
["CHANGELOG.md", "scripts/test-preferences.js", "scripts/test-config.js", "scripts/test-cli-setup.js", "scripts/test-npm-command.js",
|
|
235
|
+
"scripts/lib/npm-command.js"].includes(file),
|
|
236
|
+
);
|
|
237
|
+
for (const relative of missing) {
|
|
238
|
+
fs.rmSync(path.join(fixture, relative));
|
|
239
|
+
}
|
|
240
|
+
validate(validator, [pack(fixture).tarball], missing.map(
|
|
241
|
+
(file) => `published package is missing ${file}`,
|
|
242
|
+
));
|
|
243
|
+
|
|
244
|
+
metadata.version = "0.0.0-package-safety-fixture";
|
|
245
|
+
fs.writeFileSync(packagePath, `${JSON.stringify(metadata, null, 2)}\n`);
|
|
246
|
+
validate(validator, [pack(fixture).tarball], [
|
|
247
|
+
"tarball name/version do not match package.json",
|
|
248
|
+
]);
|
|
249
|
+
process.stdout.write(`OK: ${checks} package safety checks passed\n`);
|
|
250
|
+
} finally {
|
|
251
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
252
|
+
}
|