diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5790bd9..3a77d26 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -83,7 +83,7 @@ jobs: set -euo pipefail mkdir -p /tmp/npmcheck && cd /tmp/npmcheck && npm init -y >/dev/null npm install --no-audit --no-fund \ - "$GITHUB_WORKSPACE/dist/npm/modelslab-cli-linux-x64" \ + "$GITHUB_WORKSPACE/dist/npm/@modelslab/cli-linux-x64" \ "$GITHUB_WORKSPACE/dist/npm/modelslab-cli" # The real check: the shim resolves the binary and the binary runs. ./node_modules/.bin/modelslab --version diff --git a/packaging/README.md b/packaging/README.md index b163bef..86a6136 100644 --- a/packaging/README.md +++ b/packaging/README.md @@ -25,9 +25,24 @@ node packaging/npm/build.mjs v1.2.3 artifacts dist/npm ``` Produces seven packages: one entry package (`modelslab-cli`) whose only content -is a launcher shim, plus one package per platform holding just the binary and -the `os`/`cpu` fields npm filters on. The entry package declares the six as -`optionalDependencies`, so npm installs exactly the one that matches. +is a launcher shim, plus one package per platform (`@modelslab/cli--`) +holding the binary, a README and the `os`/`cpu` fields npm filters on. The entry +package declares the six as `optionalDependencies`, so npm installs exactly the +one that matches. + +**Platform packages are scoped; the entry package is not.** Unscoped +`modelslab-cli-win32-x64` was refused during the v0.1.2 release with +`403 Package name triggered spam detection` — a thin, unscoped package whose name +matches a very common platform-suffix pattern is precisely the shape that +heuristic targets. A scope proves ownership and sidesteps it, which is why every +comparable CLI is scoped (`@esbuild/win32-x64`, `@biomejs/cli-win32-x64`, +`@anthropic-ai/claude-code-win32-x64`); esbuild's unscoped `esbuild-windows-64` +has been frozen at 0.15.18 since they migrated. The entry package stays unscoped +so `npm install -g modelslab-cli` is unchanged and findable by name. + +The five unscoped platform packages published by v0.1.2 are orphaned at that +version. They are unreachable — no entry package ever referenced them — and npm +does not allow unpublishing them, so they are simply left alone. The alternative — one package with a postinstall that downloads a binary — was rejected deliberately. It needs network at install time and produces a silently diff --git a/packaging/npm/build.mjs b/packaging/npm/build.mjs index be50c1b..ce27383 100644 --- a/packaging/npm/build.mjs +++ b/packaging/npm/build.mjs @@ -21,6 +21,21 @@ import { mkdirSync, writeFileSync, copyFileSync, existsSync, chmodSync } from 'n import { join, resolve } from 'node:path'; const NAME = 'modelslab-cli'; +/* + * Platform packages are SCOPED; the entry package is not. + * + * Unscoped `modelslab-cli-win32-x64` was refused by npm with + * `403 Package name triggered spam detection` — a thin, unscoped package whose + * name matches a very common platform-suffix pattern is the exact shape that + * heuristic targets. A scope proves ownership and sidesteps it, which is why + * every comparable CLI is scoped (@esbuild/win32-x64, @biomejs/cli-win32-x64, + * @anthropic-ai/claude-code-win32-x64). esbuild's unscoped esbuild-windows-64 + * is frozen at 0.15.18 for the same reason. + * + * The entry package stays unscoped so `npm install -g modelslab-cli` is + * unchanged and still discoverable by name. + */ +const SCOPE = '@modelslab'; const BIN = 'modelslab'; const REPO = 'https://github.com/ModelsLab/modelslab-cli'; const DESCRIPTION = @@ -73,7 +88,7 @@ for (const target of TARGETS) { process.exit(1); } - const pkgName = `${NAME}-${target.os}-${target.cpu}`; + const pkgName = `${SCOPE}/cli-${target.os}-${target.cpu}`; const pkgDir = join(outDir, pkgName); mkdirSync(join(pkgDir, 'bin'), { recursive: true }); @@ -91,8 +106,9 @@ for (const target of TARGETS) { description: `${DESCRIPTION} (${target.os} ${target.cpu} binary)`, os: [target.os], cpu: [target.cpu], - // Only the binary. No lifecycle scripts, nothing to execute at install. - files: ['bin'], + // The binary and the README. No lifecycle scripts, nothing to + // execute at install. + files: ['bin', 'README.md'], preferUnplugged: true, }, null, @@ -100,6 +116,26 @@ for (const target of TARGETS) { ) + '\n' ); + writeFileSync( + join(pkgDir, 'README.md'), + [ + `# ${pkgName}`, + '', + `The ${target.os} ${target.cpu} binary for the [ModelsLab CLI](https://www.npmjs.com/package/${NAME}).`, + '', + '**Do not install this package directly.** It is an optional dependency of', + `\`${NAME}\`, which selects the right one for your platform:`, + '', + '```bash', + `npm install -g ${NAME}`, + '```', + '', + `- Source: ${REPO}`, + '- Docs: https://docs.modelslab.com', + '', + ].join('\n') + ); + platformPackages.push(pkgName); console.log(`built ${pkgName}`); } diff --git a/packaging/npm/publish.sh b/packaging/npm/publish.sh index ec18100..af2ccdf 100755 --- a/packaging/npm/publish.sh +++ b/packaging/npm/publish.sh @@ -20,6 +20,9 @@ set -euo pipefail DIST="${1:?usage: publish.sh }" ENTRY="modelslab-cli" +# Platform packages are scoped, so they land one directory deeper than the entry +# package: dist/npm/@modelslab/cli--. +PLATFORM_GLOB="${DIST}/@*/*" MAX_ATTEMPTS=5 already_published() { @@ -68,14 +71,24 @@ publish_one() { return 1 } -for dir in "${DIST}"/${ENTRY}-*; do +published_any=0 +for dir in ${PLATFORM_GLOB}; do [ -d "$dir" ] || continue publish_one "$dir" + published_any=1 # Pace the platform packages. Publishing them as fast as the API allows is - # what looks like spam in the first place. + # part of what looks like spam in the first place. sleep 10 done +# Guard against a glob that silently matched nothing: the entry package pins all +# six platform packages, so publishing it alone creates a version that resolves +# for nobody. +if [ "$published_any" -eq 0 ]; then + echo "fatal no platform packages found under ${PLATFORM_GLOB}" >&2 + exit 1 +fi + publish_one "${DIST}/${ENTRY}" echo "npm publish complete" diff --git a/packaging/npm/shim.cjs b/packaging/npm/shim.cjs index 8f4790c..7ef8152 100644 --- a/packaging/npm/shim.cjs +++ b/packaging/npm/shim.cjs @@ -13,12 +13,12 @@ const { existsSync, realpathSync } = require('node:fs'); const { join, dirname, sep } = require('node:path'); const PLATFORM_PACKAGES = { - 'darwin x64': 'modelslab-cli-darwin-x64', - 'darwin arm64': 'modelslab-cli-darwin-arm64', - 'linux x64': 'modelslab-cli-linux-x64', - 'linux arm64': 'modelslab-cli-linux-arm64', - 'win32 x64': 'modelslab-cli-win32-x64', - 'win32 arm64': 'modelslab-cli-win32-arm64', + 'darwin x64': '@modelslab/cli-darwin-x64', + 'darwin arm64': '@modelslab/cli-darwin-arm64', + 'linux x64': '@modelslab/cli-linux-x64', + 'linux arm64': '@modelslab/cli-linux-arm64', + 'win32 x64': '@modelslab/cli-win32-x64', + 'win32 arm64': '@modelslab/cli-win32-arm64', }; function resolveBinary() {