diff --git a/package.json b/package.json index 57357af0e..171f82425 100644 --- a/package.json +++ b/package.json @@ -124,6 +124,7 @@ "scripts/memory.js", "scripts/memory-mcp.mjs", "scripts/gemini-adapt-agents.js", + "scripts/github-coordination.js", "scripts/harness-adapter-compliance.js", "scripts/harness-audit.js", "scripts/eval-harness.js", diff --git a/tests/scripts/npm-publish-surface.test.js b/tests/scripts/npm-publish-surface.test.js index 02ca28943..ae0179bca 100644 --- a/tests/scripts/npm-publish-surface.test.js +++ b/tests/scripts/npm-publish-surface.test.js @@ -36,6 +36,25 @@ function isCoveredByAncestor(target, roots) { return false } +// `node scripts/.js`, tolerating an optional "$SKILL_DIR/" style prefix. +const SCRIPT_INVOCATION_RE = /node\s+(?:\$\{?SKILL_DIR\}?\/)?scripts\/([A-Za-z0-9._/-]+\.js)/g + +function collectCommandScriptReferences(repoRoot) { + const commandsDir = path.join(repoRoot, "commands") + if (!fs.existsSync(commandsDir)) { + return [] + } + + const references = [] + for (const file of fs.readdirSync(commandsDir).filter((name) => name.endsWith(".md"))) { + const content = fs.readFileSync(path.join(commandsDir, file), "utf8") + for (const match of content.matchAll(SCRIPT_INVOCATION_RE)) { + references.push({ script: `scripts/${match[1]}`, command: file }) + } + } + return references +} + function buildExpectedPublishPaths(repoRoot) { const modules = JSON.parse( fs.readFileSync(path.join(repoRoot, "manifests", "install-modules.json"), "utf8") @@ -80,6 +99,7 @@ function buildExpectedPublishPaths(repoRoot) { "scripts/harness-adapter-compliance.js", "scripts/session-inspect.js", "scripts/setup.js", + "scripts/github-coordination.js", "scripts/uninstall.js", "scripts/welcome.js", "scripts/gemini-adapt-agents.js", @@ -153,6 +173,20 @@ function main() { ["package.json files align to the module graph and explicit runtime allowlist", () => { assert.deepStrictEqual(actualPublishPaths, expectedPublishPaths) }], + ["every scripts/*.js a published command invokes is on the publish surface", () => { + // commands/ ships in full, so a command that tells an agent to run a script + // that npm never packages breaks only after install. See issue #3270. + const publishRoots = new Set(actualPublishPaths) + const unpublished = collectCommandScriptReferences(repoRoot) + .filter(({ script }) => !publishRoots.has(script) && !isCoveredByAncestor(script, publishRoots)) + .map(({ script, command }) => `${script} (invoked by commands/${command})`) + + assert.deepStrictEqual( + [...new Set(unpublished)].sort(), + [], + `published commands invoke unpublished scripts: ${[...new Set(unpublished)].join(", ")}` + ) + }], ["npm pack --ignore-scripts publishes the reduced runtime surface (prepack not tested)", () => { const cache = fs.mkdtempSync(path.join(os.tmpdir(), "ecc-pack-surface-")) let result