Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Expand Down
21 changes: 18 additions & 3 deletions packaging/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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-<os>-<arch>`)
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
Expand Down
42 changes: 39 additions & 3 deletions packaging/npm/build.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 =
Expand DownExpand Up@@ -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 });

Expand All@@ -91,15 +106,36 @@ 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,
2
) + '\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}`);
}
Expand Down
17 changes: 15 additions & 2 deletions packaging/npm/publish.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,6 +20,9 @@ set -euo pipefail

DIST="${1:?usage: publish.sh <dist/npm dir>}"
ENTRY="modelslab-cli"
# Platform packages are scoped, so they land one directory deeper than the entry
# package: dist/npm/@modelslab/cli-<os>-<arch>.
PLATFORM_GLOB="${DIST}/@*/*"
MAX_ATTEMPTS=5

already_published() {
Expand DownExpand Up@@ -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"
12 changes: 6 additions & 6 deletions packaging/npm/shim.cjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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() {
Expand Down
Loading