From b09a26aaadba33e6d2e0facaa2b1544ed858d8b6 Mon Sep 17 00:00:00 2001 From: Yi-111-a <153097222+Yi-111-a@users.noreply.github.com> Date: Wed, 30 Sep 2026 21:09:38 +0800 Subject: [PATCH] fix(package): publish scripts/github-coordination.js for the /epic-* commands `package.json` `files[]` enumerates each top-level `scripts/*.js` by hand, and `scripts/github-coordination.js` was never added. npm therefore drops it from the tarball while `scripts/lib/` still ships wholesale, so only the CLI entry disappears. All seven `/epic-*` commands ship in `commands/` and each instructs the agent to run: node scripts/github-coordination.js --repo so anyone who installs from npm -- including the documented `npm i -g ecc-universal` path -- gets seven commands that cannot run: Error: Cannot find module '.../ecc-universal/scripts/github-coordination.js' It works from a git clone, which is what masked it locally. Add the missing `files[]` entry, the matching publish-surface allowlist entry in `tests/scripts/npm-publish-surface.test.js` (CONTRIBUTING asks for both), and a guard test that fails when any published command invokes a `scripts/*.js` that the publish surface does not cover. The guard reproduces this exact bug: with `package.json` reverted it fails naming only `github-coordination.js`. Refs #3270 --- package.json | 1 + tests/scripts/npm-publish-surface.test.js | 34 +++++++++++++++++++++++ 2 files changed, 35 insertions(+) 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