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 <command> <issue> --repo <owner/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
This commit is contained in:
Yi-111-a
2026-09-30 21:09:38 +08:00
parent c70874fae9
commit b09a26aaad
2 changed files with 35 additions and 0 deletions
+1
View File
@@ -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",
+34
View File
@@ -36,6 +36,25 @@ function isCoveredByAncestor(target, roots) {
return false
}
// `node scripts/<name>.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