From 1bf9c03e5d6f929fbb7961fcb11a525acb721f01 Mon Sep 17 00:00:00 2001 From: hartsock Date: Wed, 15 Jul 2026 18:59:02 -0400 Subject: [PATCH 1/2] =?UTF-8?q?feat(npm):=20draft=20=E2=80=94=20newt-agent?= =?UTF-8?q?=20npm=20shim=20(newt=20+=20newt-mcp-server)=20via=20@gilamonst?= =?UTF-8?q?er?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Port of the scrybe reference shim (newt-agent#1221): deliver the newt and newt-mcp-server Rust binaries to npm users the uv/esbuild way, with a name-derived resolver so ONE generic resolver serves every @gilamonster/. npm/: - newt/, newt-mcp-server/ @gilamonster/ shims. bin/run.cjs + lib/binary.cjs are generic — each derives its binary + platform packages from its own name. - newt-agent/ unscoped umbrella -> newt - scripts/ build-platform-package.mjs (generic) + sync-versions.mjs - test/resolve.test.mjs node:test: manifest + resolver + happy-path exec (5/5) - platforms.json 3 targets matching build-binaries (darwin-arm64, linux-x64, win32-x64) - .gitignore re-include *.cjs/*.mjs/package.json (repo root ignores them) The release.yml build-npm + publish-npm-meta jobs (OIDC, tag-only) are held as a separate patch — a workflow-scoped push is needed for .github/workflows/. See PR. DRAFT. Co-Authored-By: Claude Opus 4.8 (gnuc agent) --- npm/.gitignore | 10 +++ npm/README.md | 42 ++++++++++++ npm/newt-agent/README.md | 19 ++++++ npm/newt-agent/bin/run.cjs | 6 ++ npm/newt-agent/package.json | 18 +++++ npm/newt-mcp-server/README.md | 14 ++++ npm/newt-mcp-server/bin/run.cjs | 31 +++++++++ npm/newt-mcp-server/lib/binary.cjs | 62 ++++++++++++++++++ npm/newt-mcp-server/package.json | 21 ++++++ npm/newt-mcp-server/platforms.json | 5 ++ npm/newt/README.md | 17 +++++ npm/newt/bin/run.cjs | 31 +++++++++ npm/newt/lib/binary.cjs | 62 ++++++++++++++++++ npm/newt/package.json | 21 ++++++ npm/newt/platforms.json | 5 ++ npm/package.json | 11 ++++ npm/platforms.json | 5 ++ npm/scripts/build-platform-package.mjs | 68 +++++++++++++++++++ npm/scripts/sync-versions.mjs | 46 +++++++++++++ npm/test/resolve.test.mjs | 91 ++++++++++++++++++++++++++ 20 files changed, 585 insertions(+) create mode 100644 npm/.gitignore create mode 100644 npm/README.md create mode 100644 npm/newt-agent/README.md create mode 100644 npm/newt-agent/bin/run.cjs create mode 100644 npm/newt-agent/package.json create mode 100644 npm/newt-mcp-server/README.md create mode 100644 npm/newt-mcp-server/bin/run.cjs create mode 100644 npm/newt-mcp-server/lib/binary.cjs create mode 100644 npm/newt-mcp-server/package.json create mode 100644 npm/newt-mcp-server/platforms.json create mode 100644 npm/newt/README.md create mode 100644 npm/newt/bin/run.cjs create mode 100644 npm/newt/lib/binary.cjs create mode 100644 npm/newt/package.json create mode 100644 npm/newt/platforms.json create mode 100644 npm/package.json create mode 100644 npm/platforms.json create mode 100644 npm/scripts/build-platform-package.mjs create mode 100644 npm/scripts/sync-versions.mjs create mode 100644 npm/test/resolve.test.mjs diff --git a/npm/.gitignore b/npm/.gitignore new file mode 100644 index 00000000..08bcd5a1 --- /dev/null +++ b/npm/.gitignore @@ -0,0 +1,10 @@ +# The repo root .gitignore blanket-ignores *.mjs, *.cjs, and package.json to keep +# the Rust workspace JS-free. The npm shim under npm/ legitimately needs all three, +# so re-include them here (scoped to this directory). +!*.mjs +!*.cjs +!package.json + +# ...but never commit build output or installed deps. +dist-npm/ +node_modules/ diff --git a/npm/README.md b/npm/README.md new file mode 100644 index 00000000..27d3dd94 --- /dev/null +++ b/npm/README.md @@ -0,0 +1,42 @@ +# newt-agent npm packages + +Sources for newt-agent's npm distribution — Rust binaries delivered to npm users +the way `uv`, `esbuild`, and `@biomejs/biome` do (no Rust toolchain, no `pip`), +ported from the [scrybe](https://github.com/hartsock/scrybe) reference shim +(newt-agent#1221). + +| Path | Package | Role | +|---|---|---| +| `newt-agent/` | [`newt-agent`](https://www.npmjs.com/package/newt-agent) | Unscoped umbrella — `npm i -g newt-agent` → the `newt` CLI. Depends on `@gilamonster/newt`. | +| `newt/` | `@gilamonster/newt` | `newt` CLI bin shim. Lists per-platform binaries as `optionalDependencies`; execs whichever npm resolved. | +| `newt-mcp-server/` | `@gilamonster/newt-mcp-server` | `newt-mcp-server` bin shim (same pattern). `npm i -g @gilamonster/newt-mcp-server`. | +| *(generated)* | `@gilamonster/--` | Per-platform packages carrying just the prebuilt binary + `os`/`cpu` fields. Built by the release job. | + +## Design + +- **Name-derived resolver.** `newt/lib/binary.cjs` and `bin/run.cjs` are **generic** — + each shim derives its binary name and platform-package names from its *own* + package name, so the exact same two files are shipped by every `@gilamonster/` + shim. Add a binary = one `package.json` (+ a copy of `bin/`, `lib/`, `platforms.json`). +- **No `postinstall`.** The binary arrives as a normal optional dependency — + hermetic, offline-cacheable. +- **`platforms.json`** is the single source of truth (`darwin-arm64`, `darwin-x64`, + `linux-x64`, `win32-x64`); `sync-versions.mjs` stamps exact pins at release. + +## Scope (this PR) + +Ships **`newt` + `newt-mcp-server`** — the two binaries `release.yml`'s +`build-binaries` actually produces (`-p newt-agent -p newt-mcp-server`). The +`@gilamonster/newt-mcp-data` and `@gilamonster/newt-provider-openai` names are +reserved but not shipped here — wire them in once those binaries are added to the +release build. + +## Develop + +```bash +cd npm && npm test # node --test: manifest integrity + resolver + happy-path exec +``` + +Publishing uses npm **OIDC trusted publishing** (see `.github/workflows/release.yml` +`build-npm` + `publish-npm-meta`). Each package has a trusted publisher configured +on npmjs pointing at `Gilamonster-Foundation/newt-agent` → `release.yml`. diff --git a/npm/newt-agent/README.md b/npm/newt-agent/README.md new file mode 100644 index 00000000..66e29d4b --- /dev/null +++ b/npm/newt-agent/README.md @@ -0,0 +1,19 @@ +# newt-agent + +Umbrella installer for [newt-agent](https://github.com/Gilamonster-Foundation/newt-agent) (Gilamonster). + +```bash +npm install -g newt-agent +newt --help +``` + +The friendly single-name install. Depends on +[`@gilamonster/newt`](https://www.npmjs.com/package/@gilamonster/newt), which delivers +the prebuilt `newt` Rust binary for your platform via per-platform +`optionalDependencies` (the `uv` / `esbuild` pattern — no postinstall). + +The MCP server is a separate install: `npm i -g @gilamonster/newt-mcp-server`. + +Other channels: `cargo install newt-agent` · `pip install newt-agent`. + +License: Apache-2.0 diff --git a/npm/newt-agent/bin/run.cjs b/npm/newt-agent/bin/run.cjs new file mode 100644 index 00000000..2e096f7a --- /dev/null +++ b/npm/newt-agent/bin/run.cjs @@ -0,0 +1,6 @@ +#!/usr/bin/env node +'use strict'; + +// Umbrella launcher. `newt-agent` depends on `@gilamonster/newt`; delegate to its +// launcher, which resolves and execs the platform binary (and exits). +require('@gilamonster/newt/bin/run.cjs'); diff --git a/npm/newt-agent/package.json b/npm/newt-agent/package.json new file mode 100644 index 00000000..f5adbd98 --- /dev/null +++ b/npm/newt-agent/package.json @@ -0,0 +1,18 @@ +{ + "name": "newt-agent", + "version": "0.0.0", + "description": "Umbrella installer for newt-agent (Gilamonster). Installs the `newt` CLI binary for your platform; no Rust toolchain or pip required.", + "keywords": ["newt", "newt-agent", "gilamonster", "cli", "agent", "mcp"], + "homepage": "https://github.com/Gilamonster-Foundation/newt-agent#readme", + "bugs": "https://github.com/Gilamonster-Foundation/newt-agent/issues", + "repository": { "type": "git", "url": "git+https://github.com/Gilamonster-Foundation/newt-agent.git", "directory": "npm/newt-agent" }, + "license": "Apache-2.0", + "author": "Shawn Hartsock", + "type": "commonjs", + "bin": { "newt": "bin/run.cjs" }, + "files": ["bin/", "README.md"], + "engines": { "node": ">=18" }, + "dependencies": { + "@gilamonster/newt": "0.0.0" + } +} diff --git a/npm/newt-mcp-server/README.md b/npm/newt-mcp-server/README.md new file mode 100644 index 00000000..466efdaf --- /dev/null +++ b/npm/newt-mcp-server/README.md @@ -0,0 +1,14 @@ +# @gilamonster/newt-mcp-server + +The `newt-mcp-server` binary ([newt-agent](https://github.com/Gilamonster-Foundation/newt-agent)), +delivered as a prebuilt Rust binary for your platform. + +```bash +npm install -g @gilamonster/newt-mcp-server +newt-mcp-server --help +``` + +Same per-platform `optionalDependencies` pattern as `@gilamonster/newt` (no postinstall). +Not your platform? Build from source: https://github.com/Gilamonster-Foundation/newt-agent + +License: Apache-2.0 diff --git a/npm/newt-mcp-server/bin/run.cjs b/npm/newt-mcp-server/bin/run.cjs new file mode 100644 index 00000000..a3c4de44 --- /dev/null +++ b/npm/newt-mcp-server/bin/run.cjs @@ -0,0 +1,31 @@ +#!/usr/bin/env node +'use strict'; + +// Generic launcher shared by every @gilamonster/ shim: resolve the platform +// binary and exec it, passing through argv, stdio, exit code, and signals. + +const os = require('os'); +const { spawnSync } = require('child_process'); +const { binaryPath, BINARY } = require('../lib/binary.cjs'); + +let bin; +try { + bin = binaryPath(); +} catch (err) { + process.stderr.write(`${err && err.message ? err.message : err}\n`); + process.exit(1); +} + +const result = spawnSync(bin, process.argv.slice(2), { stdio: 'inherit' }); + +if (result.error) { + process.stderr.write(`${BINARY}: failed to launch ${bin}: ${result.error.message}\n`); + process.exit(1); +} + +if (result.signal) { + const num = os.constants.signals[result.signal]; + process.exit(num ? 128 + num : 1); +} + +process.exit(result.status === null ? 1 : result.status); diff --git a/npm/newt-mcp-server/lib/binary.cjs b/npm/newt-mcp-server/lib/binary.cjs new file mode 100644 index 00000000..a98e170b --- /dev/null +++ b/npm/newt-mcp-server/lib/binary.cjs @@ -0,0 +1,62 @@ +'use strict'; + +// Generic resolver for a `@gilamonster/` shim. Everything is derived from +// this package's OWN name, so the exact same file is shipped verbatim by every +// binary shim (newt, newt-mcp-server, …): +// +// @gilamonster/newt -> binary "newt", platform pkgs @gilamonster/newt- +// @gilamonster/newt-mcp-server -> binary "newt-mcp-server", @gilamonster/newt-mcp-server- +// +// The uv / esbuild optionalDependencies pattern — no postinstall, no network. + +const fs = require('fs'); +const path = require('path'); + +const self = require('../package.json'); +const SHIM = self.name; // e.g. "@gilamonster/newt-mcp-server" +const BINARY = SHIM.split('/').pop(); // "newt-mcp-server" +const PLATFORMS = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'platforms.json'), 'utf8')); + +const REPO = 'https://github.com/Gilamonster-Foundation/newt-agent'; + +function platformKey() { + return `${process.platform}-${process.arch}`; +} + +function entryForCurrentPlatform() { + const key = platformKey(); + return PLATFORMS.find((p) => p.key === key) || null; +} + +function binaryFile(entry) { + return entry.os === 'win32' ? `${BINARY}.exe` : BINARY; +} + +function binaryPath() { + const key = platformKey(); + const entry = entryForCurrentPlatform(); + + if (!entry) { + const supported = PLATFORMS.map((p) => p.key).join(', '); + throw new Error( + `${BINARY}: no prebuilt binary for this platform (${key}).\n` + + `Supported: ${supported}.\n` + + `Install from source instead: ${REPO}` + ); + } + + const pkg = `${SHIM}-${entry.key}`; + try { + const pkgJsonPath = require.resolve(`${pkg}/package.json`); + return path.join(path.dirname(pkgJsonPath), binaryFile(entry)); + } catch (_err) { + throw new Error( + `${BINARY}: the platform package "${pkg}" is not installed.\n` + + `This usually means optionalDependencies were skipped during install.\n` + + `Try: npm install -g newt-agent --include=optional\n` + + `Or install from source: ${REPO}` + ); + } +} + +module.exports = { PLATFORMS, BINARY, SHIM, platformKey, entryForCurrentPlatform, binaryPath }; diff --git a/npm/newt-mcp-server/package.json b/npm/newt-mcp-server/package.json new file mode 100644 index 00000000..257032d7 --- /dev/null +++ b/npm/newt-mcp-server/package.json @@ -0,0 +1,21 @@ +{ + "name": "@gilamonster/newt-mcp-server", + "version": "0.0.0", + "description": "newt-mcp-server — the newt-agent MCP server binary. Delivers the prebuilt Rust binary for your platform; no Rust toolchain or pip required.", + "keywords": ["newt", "newt-agent", "gilamonster", "mcp", "mcp-server"], + "homepage": "https://github.com/Gilamonster-Foundation/newt-agent#readme", + "bugs": "https://github.com/Gilamonster-Foundation/newt-agent/issues", + "repository": { "type": "git", "url": "git+https://github.com/Gilamonster-Foundation/newt-agent.git", "directory": "npm/newt-mcp-server" }, + "license": "Apache-2.0", + "author": "Shawn Hartsock", + "type": "commonjs", + "bin": { "newt-mcp-server": "bin/run.cjs" }, + "files": ["bin/", "lib/", "platforms.json", "README.md"], + "engines": { "node": ">=18" }, + "publishConfig": { "access": "public" }, + "optionalDependencies": { + "@gilamonster/newt-mcp-server-darwin-arm64": "0.0.0", + "@gilamonster/newt-mcp-server-linux-x64": "0.0.0", + "@gilamonster/newt-mcp-server-win32-x64": "0.0.0" + } +} diff --git a/npm/newt-mcp-server/platforms.json b/npm/newt-mcp-server/platforms.json new file mode 100644 index 00000000..daa6f1fe --- /dev/null +++ b/npm/newt-mcp-server/platforms.json @@ -0,0 +1,5 @@ +[ + { "key": "darwin-arm64", "os": "darwin", "cpu": "arm64", "rustTarget": "aarch64-apple-darwin" }, + { "key": "linux-x64", "os": "linux", "cpu": "x64", "libc": "glibc", "rustTarget": "x86_64-unknown-linux-gnu" }, + { "key": "win32-x64", "os": "win32", "cpu": "x64", "rustTarget": "x86_64-pc-windows-msvc" } +] diff --git a/npm/newt/README.md b/npm/newt/README.md new file mode 100644 index 00000000..368852bd --- /dev/null +++ b/npm/newt/README.md @@ -0,0 +1,17 @@ +# @gilamonster/newt + +The `newt` CLI ([newt-agent](https://github.com/Gilamonster-Foundation/newt-agent)), +delivered as a prebuilt Rust binary for your platform — no Rust toolchain, no pip. + +```bash +npm install -g newt-agent # umbrella (recommended) +# or the scoped package directly: +npm install -g @gilamonster/newt +newt --help +``` + +Ships no binary itself: it declares per-platform `@gilamonster/newt--` +packages as `optionalDependencies`; your package manager installs the matching one +and the `newt` shim execs it. No `postinstall`. Not your platform? `cargo install newt-agent`. + +License: Apache-2.0 diff --git a/npm/newt/bin/run.cjs b/npm/newt/bin/run.cjs new file mode 100644 index 00000000..a3c4de44 --- /dev/null +++ b/npm/newt/bin/run.cjs @@ -0,0 +1,31 @@ +#!/usr/bin/env node +'use strict'; + +// Generic launcher shared by every @gilamonster/ shim: resolve the platform +// binary and exec it, passing through argv, stdio, exit code, and signals. + +const os = require('os'); +const { spawnSync } = require('child_process'); +const { binaryPath, BINARY } = require('../lib/binary.cjs'); + +let bin; +try { + bin = binaryPath(); +} catch (err) { + process.stderr.write(`${err && err.message ? err.message : err}\n`); + process.exit(1); +} + +const result = spawnSync(bin, process.argv.slice(2), { stdio: 'inherit' }); + +if (result.error) { + process.stderr.write(`${BINARY}: failed to launch ${bin}: ${result.error.message}\n`); + process.exit(1); +} + +if (result.signal) { + const num = os.constants.signals[result.signal]; + process.exit(num ? 128 + num : 1); +} + +process.exit(result.status === null ? 1 : result.status); diff --git a/npm/newt/lib/binary.cjs b/npm/newt/lib/binary.cjs new file mode 100644 index 00000000..a98e170b --- /dev/null +++ b/npm/newt/lib/binary.cjs @@ -0,0 +1,62 @@ +'use strict'; + +// Generic resolver for a `@gilamonster/` shim. Everything is derived from +// this package's OWN name, so the exact same file is shipped verbatim by every +// binary shim (newt, newt-mcp-server, …): +// +// @gilamonster/newt -> binary "newt", platform pkgs @gilamonster/newt- +// @gilamonster/newt-mcp-server -> binary "newt-mcp-server", @gilamonster/newt-mcp-server- +// +// The uv / esbuild optionalDependencies pattern — no postinstall, no network. + +const fs = require('fs'); +const path = require('path'); + +const self = require('../package.json'); +const SHIM = self.name; // e.g. "@gilamonster/newt-mcp-server" +const BINARY = SHIM.split('/').pop(); // "newt-mcp-server" +const PLATFORMS = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'platforms.json'), 'utf8')); + +const REPO = 'https://github.com/Gilamonster-Foundation/newt-agent'; + +function platformKey() { + return `${process.platform}-${process.arch}`; +} + +function entryForCurrentPlatform() { + const key = platformKey(); + return PLATFORMS.find((p) => p.key === key) || null; +} + +function binaryFile(entry) { + return entry.os === 'win32' ? `${BINARY}.exe` : BINARY; +} + +function binaryPath() { + const key = platformKey(); + const entry = entryForCurrentPlatform(); + + if (!entry) { + const supported = PLATFORMS.map((p) => p.key).join(', '); + throw new Error( + `${BINARY}: no prebuilt binary for this platform (${key}).\n` + + `Supported: ${supported}.\n` + + `Install from source instead: ${REPO}` + ); + } + + const pkg = `${SHIM}-${entry.key}`; + try { + const pkgJsonPath = require.resolve(`${pkg}/package.json`); + return path.join(path.dirname(pkgJsonPath), binaryFile(entry)); + } catch (_err) { + throw new Error( + `${BINARY}: the platform package "${pkg}" is not installed.\n` + + `This usually means optionalDependencies were skipped during install.\n` + + `Try: npm install -g newt-agent --include=optional\n` + + `Or install from source: ${REPO}` + ); + } +} + +module.exports = { PLATFORMS, BINARY, SHIM, platformKey, entryForCurrentPlatform, binaryPath }; diff --git a/npm/newt/package.json b/npm/newt/package.json new file mode 100644 index 00000000..8330ca0f --- /dev/null +++ b/npm/newt/package.json @@ -0,0 +1,21 @@ +{ + "name": "@gilamonster/newt", + "version": "0.0.0", + "description": "newt — the newt-agent CLI. Delivers the prebuilt Rust binary for your platform; no Rust toolchain or pip required.", + "keywords": ["newt", "newt-agent", "gilamonster", "cli", "agent", "mcp"], + "homepage": "https://github.com/Gilamonster-Foundation/newt-agent#readme", + "bugs": "https://github.com/Gilamonster-Foundation/newt-agent/issues", + "repository": { "type": "git", "url": "git+https://github.com/Gilamonster-Foundation/newt-agent.git", "directory": "npm/newt" }, + "license": "Apache-2.0", + "author": "Shawn Hartsock", + "type": "commonjs", + "bin": { "newt": "bin/run.cjs" }, + "files": ["bin/", "lib/", "platforms.json", "README.md"], + "engines": { "node": ">=18" }, + "publishConfig": { "access": "public" }, + "optionalDependencies": { + "@gilamonster/newt-darwin-arm64": "0.0.0", + "@gilamonster/newt-linux-x64": "0.0.0", + "@gilamonster/newt-win32-x64": "0.0.0" + } +} diff --git a/npm/newt/platforms.json b/npm/newt/platforms.json new file mode 100644 index 00000000..daa6f1fe --- /dev/null +++ b/npm/newt/platforms.json @@ -0,0 +1,5 @@ +[ + { "key": "darwin-arm64", "os": "darwin", "cpu": "arm64", "rustTarget": "aarch64-apple-darwin" }, + { "key": "linux-x64", "os": "linux", "cpu": "x64", "libc": "glibc", "rustTarget": "x86_64-unknown-linux-gnu" }, + { "key": "win32-x64", "os": "win32", "cpu": "x64", "rustTarget": "x86_64-pc-windows-msvc" } +] diff --git a/npm/package.json b/npm/package.json new file mode 100644 index 00000000..cf02c735 --- /dev/null +++ b/npm/package.json @@ -0,0 +1,11 @@ +{ + "name": "newt-agent-npm-shim", + "private": true, + "version": "0.0.0", + "description": "Build sources for newt-agent's npm packages (newt-agent umbrella + @gilamonster/ shims + per-platform binaries). Not published.", + "type": "module", + "scripts": { + "test": "node --test" + }, + "license": "Apache-2.0" +} diff --git a/npm/platforms.json b/npm/platforms.json new file mode 100644 index 00000000..daa6f1fe --- /dev/null +++ b/npm/platforms.json @@ -0,0 +1,5 @@ +[ + { "key": "darwin-arm64", "os": "darwin", "cpu": "arm64", "rustTarget": "aarch64-apple-darwin" }, + { "key": "linux-x64", "os": "linux", "cpu": "x64", "libc": "glibc", "rustTarget": "x86_64-unknown-linux-gnu" }, + { "key": "win32-x64", "os": "win32", "cpu": "x64", "rustTarget": "x86_64-pc-windows-msvc" } +] diff --git a/npm/scripts/build-platform-package.mjs b/npm/scripts/build-platform-package.mjs new file mode 100644 index 00000000..11e2e326 --- /dev/null +++ b/npm/scripts/build-platform-package.mjs @@ -0,0 +1,68 @@ +#!/usr/bin/env node +// Wrap a built binary into a publishable `@gilamonster/-` package. +// +// node scripts/build-platform-package.mjs \ +// --name @gilamonster/newt \ +// --binary target/x86_64-unknown-linux-gnu/release/newt \ +// --key linux-x64 --version 0.6.0 --out dist-npm/newt-linux-x64 +// +// Generic over the binary — `--name` is the shim package, the binary file name is +// its last path segment. Reads npm/platforms.json (single source of truth). + +import { readFileSync, writeFileSync, mkdirSync, copyFileSync, chmodSync } from 'node:fs'; +import { dirname, join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const here = dirname(fileURLToPath(import.meta.url)); +const PLATFORMS = JSON.parse(readFileSync(join(here, '..', 'platforms.json'), 'utf8')); + +function arg(name) { + const i = process.argv.indexOf(`--${name}`); + if (i === -1 || i === process.argv.length - 1) { + console.error(`build-platform-package: missing --${name}`); + process.exit(2); + } + return process.argv[i + 1]; +} + +const shim = arg('name'); // @gilamonster/newt +const binary = arg('binary'); +const key = arg('key'); +const version = arg('version'); +const out = resolve(arg('out')); + +const entry = PLATFORMS.find((p) => p.key === key); +if (!entry) { + console.error(`build-platform-package: unknown platform key "${key}"`); + process.exit(2); +} + +const binName = shim.split('/').pop(); +const fileName = entry.os === 'win32' ? `${binName}.exe` : binName; + +mkdirSync(out, { recursive: true }); + +const pkg = { + name: `${shim}-${entry.key}`, + version, + description: `Prebuilt ${binName} binary for ${entry.os}-${entry.cpu}.`, + homepage: 'https://github.com/Gilamonster-Foundation/newt-agent#readme', + repository: { type: 'git', url: 'git+https://github.com/Gilamonster-Foundation/newt-agent.git' }, + license: 'Apache-2.0', + os: [entry.os], + cpu: [entry.cpu], + ...(entry.libc ? { libc: [entry.libc] } : {}), + files: [fileName, 'README.md'], + publishConfig: { access: 'public' }, +}; + +writeFileSync(join(out, 'package.json'), JSON.stringify(pkg, null, 2) + '\n'); +copyFileSync(binary, join(out, fileName)); +if (entry.os !== 'win32') chmodSync(join(out, fileName), 0o755); +writeFileSync( + join(out, 'README.md'), + `# ${pkg.name}\n\nPrebuilt \`${binName}\` binary for ${entry.os}-${entry.cpu}. Installed automatically ` + + `as an optional dependency of [\`${shim}\`](https://www.npmjs.com/package/${shim}); do not depend on it directly.\n` +); + +console.log(`built ${pkg.name}@${version} -> ${out}`); diff --git a/npm/scripts/sync-versions.mjs b/npm/scripts/sync-versions.mjs new file mode 100644 index 00000000..93df1c02 --- /dev/null +++ b/npm/scripts/sync-versions.mjs @@ -0,0 +1,46 @@ +#!/usr/bin/env node +// Stamp a release version across the npm shim packages. Sets, for each shim: +// - version + each optionalDependencies pin +// and for the umbrella: +// - newt-agent version + its @gilamonster/newt dependency pin +// Exact pins so an umbrella always pulls the matching platform build. +// +// node scripts/sync-versions.mjs --version 0.6.0 + +import { readFileSync, writeFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const here = dirname(fileURLToPath(import.meta.url)); +const root = join(here, '..'); + +const SHIMS = ['newt', 'newt-mcp-server']; + +const i = process.argv.indexOf('--version'); +if (i === -1 || !process.argv[i + 1]) { + console.error('sync-versions: missing --version'); + process.exit(2); +} +const version = process.argv[i + 1]; + +function patch(rel, fn) { + const p = join(root, rel); + const pkg = JSON.parse(readFileSync(p, 'utf8')); + fn(pkg); + writeFileSync(p, JSON.stringify(pkg, null, 2) + '\n'); + console.log(`set ${pkg.name}@${version}`); +} + +for (const shim of SHIMS) { + patch(`${shim}/package.json`, (pkg) => { + pkg.version = version; + for (const dep of Object.keys(pkg.optionalDependencies || {})) { + pkg.optionalDependencies[dep] = version; + } + }); +} + +patch('newt-agent/package.json', (pkg) => { + pkg.version = version; + pkg.dependencies['@gilamonster/newt'] = version; +}); diff --git a/npm/test/resolve.test.mjs b/npm/test/resolve.test.mjs new file mode 100644 index 00000000..44c64eb3 --- /dev/null +++ b/npm/test/resolve.test.mjs @@ -0,0 +1,91 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readFileSync, mkdtempSync, mkdirSync, writeFileSync, cpSync, chmodSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { createRequire } from 'node:module'; +import { spawnSync } from 'node:child_process'; +import { tmpdir } from 'node:os'; + +const here = dirname(fileURLToPath(import.meta.url)); +const root = join(here, '..'); +const require = createRequire(import.meta.url); + +const SHIMS = ['newt', 'newt-mcp-server']; +const platforms = JSON.parse(readFileSync(join(root, 'platforms.json'), 'utf8')); +const umbrella = JSON.parse(readFileSync(join(root, 'newt-agent', 'package.json'), 'utf8')); + +const NODE_OS = new Set(['darwin', 'linux', 'win32', 'freebsd', 'openbsd', 'sunos', 'aix', 'android']); +const NODE_CPU = new Set(['arm64', 'x64', 'ia32', 'arm', 'ppc64', 's390x', 'riscv64', 'loong64']); + +test('platforms.json entries are well-formed and unique', () => { + const keys = new Set(); + for (const p of platforms) { + for (const f of ['key', 'os', 'cpu', 'rustTarget']) { + assert.ok(p[f], `entry missing "${f}": ${JSON.stringify(p)}`); + } + assert.ok(NODE_OS.has(p.os), `invalid node os: ${p.os}`); + assert.ok(NODE_CPU.has(p.cpu), `invalid node cpu: ${p.cpu}`); + if (p.libc) assert.ok(['glibc', 'musl'].includes(p.libc), `invalid libc: ${p.libc}`); + assert.equal(p.key, `${p.os}-${p.cpu}`, `key must equal "-": ${p.key}`); + assert.ok(!keys.has(p.key), `duplicate platform key: ${p.key}`); + keys.add(p.key); + } +}); + +test('each shim optionalDependencies exactly cover platforms.json, keyed by the shim name', () => { + for (const shim of SHIMS) { + const pkg = JSON.parse(readFileSync(join(root, shim, 'package.json'), 'utf8')); + const expected = platforms.map((p) => `${pkg.name}-${p.key}`).sort(); + const actual = Object.keys(pkg.optionalDependencies || {}).sort(); + assert.deepEqual(actual, expected, `${pkg.name} optionalDependencies must list every platform`); + // bin key must be the binary name (last segment of the scoped package name) + const binName = pkg.name.split('/').pop(); + assert.equal(pkg.bin[binName], 'bin/run.cjs', `${pkg.name} must expose bin.${binName}`); + assert.equal(pkg.publishConfig.access, 'public'); + } +}); + +test('umbrella newt-agent depends on @gilamonster/newt and exposes the newt bin', () => { + assert.ok(umbrella.dependencies['@gilamonster/newt'], 'newt-agent must depend on @gilamonster/newt'); + assert.equal(umbrella.bin.newt, 'bin/run.cjs'); +}); + +test('resolver derives the binary from its own name and throws an actionable error when absent', () => { + const { binaryPath, BINARY, platformKey } = require(join(root, 'newt', 'lib', 'binary.cjs')); + assert.equal(BINARY, 'newt', 'BINARY must derive from the shim package name'); + assert.match(platformKey(), /^[a-z0-9]+-[a-z0-9]+$/); + assert.throws( + () => binaryPath(), + (err) => { + assert.match(err.message, /newt-agent|Gilamonster-Foundation\/newt-agent|--include=optional/); + return true; + } + ); +}); + +test('happy path: the newt shim resolves and execs an installed platform package', () => { + const { entryForCurrentPlatform } = require(join(root, 'newt', 'lib', 'binary.cjs')); + const entry = entryForCurrentPlatform(); + if (!entry) return; + if (entry.os === 'win32') return; // fake .exe isn't a real PE + + const tmp = mkdtempSync(join(tmpdir(), 'newt-npm-')); + const scoped = join(tmp, 'node_modules', '@gilamonster'); + cpSync(join(root, 'newt'), join(scoped, 'newt'), { recursive: true }); + + const platDir = join(scoped, `newt-${entry.key}`); + mkdirSync(platDir, { recursive: true }); + writeFileSync( + join(platDir, 'package.json'), + JSON.stringify({ name: `@gilamonster/newt-${entry.key}`, version: '0.0.0', os: [entry.os], cpu: [entry.cpu] }) + ); + const fakeBin = join(platDir, 'newt'); + writeFileSync(fakeBin, '#!/bin/sh\necho newt-fake-ok "$@"\n'); + chmodSync(fakeBin, 0o755); + + const shim = join(scoped, 'newt', 'bin', 'run.cjs'); + const res = spawnSync(process.execPath, [shim, 'run', '--flag'], { encoding: 'utf8' }); + assert.equal(res.status, 0, res.stderr); + assert.match(res.stdout, /newt-fake-ok run --flag/, 'shim must exec the resolved binary with argv passed through'); +}); From 5ff518ca72a1b41d6d6edc2741cec5e4dc100efa Mon Sep 17 00:00:00 2001 From: hartsock Date: Wed, 15 Jul 2026 19:00:34 -0400 Subject: [PATCH 2/2] ci(release): npm build-npm + publish-npm-meta jobs (OIDC) --- .github/workflows/release.yml | 111 ++++++++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 58ad7f71..a8814e6a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -623,3 +623,114 @@ jobs: # account-scoped API token, so attestations MUST be disabled here. attestations: false skip-existing: true + + # ── npm binary shim (@gilamonster/ + newt-agent umbrella) ──────────── + # + # Delivers the `newt` + `newt-mcp-server` binaries to npm users the uv/esbuild + # way (newt-agent#1221): one tiny per-platform package carrying just the + # prebuilt binary + os/cpu, per-binary umbrellas `@gilamonster/` listing + # them as optionalDependencies, and the unscoped `newt-agent` umbrella + # (`npm i -g newt-agent` -> `newt`). No postinstall. Sources + design: npm/. + # + # Ships only what build-binaries produces (-p newt-agent -p newt-mcp-server); + # newt-mcp-data / newt-provider-openai names are reserved but not built yet. + # + # Auth: npm OIDC trusted publishing (NO stored token) — each of the 21 packages + # has a trusted publisher configured on npmjs pointing at + # Gilamonster-Foundation/newt-agent -> release.yml, and id-token: write + npm + # >= 11.5.1 mints a short-lived token. NOTE: this diverges from the PyPI/crates + # jobs above, which deliberately use account-scoped tokens; the maintainer + # chose OIDC for npm. Tag-only (an npm version can't be republished). + + build-npm: + name: npm binary (${{ matrix.key }}) + if: startsWith(github.ref, 'refs/tags/v') + permissions: + contents: read + id-token: write # npm provenance + OIDC trusted publishing + strategy: + fail-fast: false + matrix: + include: + - { key: darwin-arm64, os: macos-latest, target: aarch64-apple-darwin } + - { key: linux-x64, os: ubuntu-latest, target: x86_64-unknown-linux-gnu } + - { key: win32-x64, os: windows-latest, target: x86_64-pc-windows-msvc } + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "22" + registry-url: "https://registry.npmjs.org" + - name: Upgrade npm for OIDC trusted publishing (>= 11.5.1) + run: npm install -g npm@latest + - uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.target }} + - uses: Swatinem/rust-cache@v2 + with: + key: npm-${{ matrix.key }} + + - name: Derive version from tag + shell: bash + run: echo "VERSION=${GITHUB_REF_NAME#v}" >> "$GITHUB_ENV" + + - name: Build binaries + run: cargo build --release --locked --target ${{ matrix.target }} -p newt-agent -p newt-mcp-server + + - name: Wrap + publish platform packages + shell: bash + run: | + ext=""; [ "$RUNNER_OS" = "Windows" ] && ext=".exe" + for shim in "@gilamonster/newt" "@gilamonster/newt-mcp-server"; do + bin="${shim##*/}" + out="dist-npm/${bin}-${{ matrix.key }}" + node npm/scripts/build-platform-package.mjs \ + --name "$shim" \ + --binary "target/${{ matrix.target }}/release/${bin}${ext}" \ + --key "${{ matrix.key }}" --version "$VERSION" --out "$out" + ( cd "$out" + PKG=$(node -p "require('./package.json').name") + if npm view "$PKG@$VERSION" version >/dev/null 2>&1; then + echo "$PKG@$VERSION already published — skipping" + else + npm publish --provenance --access public + fi ) + done + + publish-npm-meta: + name: npm shims + umbrella + if: startsWith(github.ref, 'refs/tags/v') + needs: [build-npm] # platform packages must be published first + permissions: + contents: read + id-token: write + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "22" + registry-url: "https://registry.npmjs.org" + - name: Upgrade npm for OIDC trusted publishing (>= 11.5.1) + run: npm install -g npm@latest + + - name: Derive version from tag + run: echo "VERSION=${GITHUB_REF_NAME#v}" >> "$GITHUB_ENV" + + - name: Stamp versions across the shim packages + run: node npm/scripts/sync-versions.mjs --version "$VERSION" + + - name: Publish shims + umbrella + shell: bash + # Order matters: the newt-agent umbrella depends on @gilamonster/newt. + run: | + for dir in npm/newt npm/newt-mcp-server npm/newt-agent; do + ( cd "$dir" + PKG=$(node -p "require('./package.json').name") + if npm view "$PKG@$VERSION" version >/dev/null 2>&1; then + echo "$PKG@$VERSION already published — skipping" + else + npm publish --provenance --access public + fi ) + done