diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml new file mode 100644 index 00000000..f20ac7e5 --- /dev/null +++ b/.github/workflows/npm-publish.yml @@ -0,0 +1,83 @@ +name: Publish npm + +# Manual, credential-gated publish of @aoagents/ao and its per-platform +# sub-packages to npm. +# +# Safe by default: `dry_run` is true, so a normal run only validates with +# `npm publish --dry-run` and never contacts the registry's write path. A real +# publish requires BOTH dry_run=false AND an `NPM_TOKEN` repository secret +# (absent by default) — so an accidental dispatch cannot publish. Publishing is +# a deliberate human decision. + +on: + workflow_dispatch: + inputs: + dry_run: + description: "Dry run only (no real publish)" + type: boolean + default: true + +permissions: + contents: read + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version-file: backend/go.mod + cache: false + + - uses: actions/setup-node@v4 + with: + node-version: 20 + registry-url: "https://registry.npmjs.org" + + - name: Validate packaging matrix + run: node npm/scripts/check.mjs + + - name: Cross-compile daemon + assemble npm packages + run: node npm/scripts/build.mjs + env: + AO_COMMIT: ${{ github.sha }} + + - name: Pack npm tarballs + run: node npm/scripts/pack.mjs + + - name: Guard real publish + if: ${{ inputs.dry_run == false }} + run: | + if [ -z "${NPM_TOKEN}" ]; then + echo "dry_run=false but NPM_TOKEN secret is not set; refusing to publish." >&2 + exit 1 + fi + env: + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + + # Publish per-platform sub-packages FIRST, then the main package, so the + # main package's optionalDependencies resolve at install time. The main + # tarball is the one matching the package version with no platform suffix. + - name: Publish + run: | + flag="" + if [ "${DRY_RUN}" = "true" ]; then + flag="--dry-run" + echo "DRY RUN: validating publish without writing to the registry." + fi + main_name="aoagents-ao-$(node -p "require('./npm/package.json').version").tgz" + publish() { + echo "::group::npm publish $flag $1" + npm publish --access public $flag "$1" + echo "::endgroup::" + } + for t in npm/build/tarballs/*.tgz; do + [ "$(basename "$t")" = "$main_name" ] && continue + publish "$t" + done + publish "npm/build/tarballs/$main_name" + env: + DRY_RUN: ${{ inputs.dry_run }} + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..7daeefd8 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,88 @@ +name: Daemon release + +# Cross-compiles the Go daemon for every npm target, publishes the raw binaries +# to a GitHub Release, builds the per-platform @aoagents/ao npm sub-packages from +# those same binaries, and verifies them with `npm publish --dry-run`. +# +# This workflow does NOT publish to npm. Real publishing is the manual, +# credential-gated `npm-publish.yml`. +# +# Trigger: push a `daemon-v` tag that matches npm/package.json +# (e.g. daemon-v0.10.0). This mirrors the Electron app's `desktop-v*` lane so the +# two release artifacts have distinct, self-describing tags. + +on: + push: + tags: + - "daemon-v*" + workflow_dispatch: + +permissions: + contents: write + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version-file: backend/go.mod + cache: false + + - uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Validate packaging matrix + run: node npm/scripts/check.mjs + + - name: Verify tag matches package version + if: startsWith(github.ref, 'refs/tags/daemon-v') + run: | + pkg_version="$(node -p "require('./npm/package.json').version")" + tag_version="${GITHUB_REF_NAME#daemon-v}" + if [ "$pkg_version" != "$tag_version" ]; then + echo "Tag $GITHUB_REF_NAME ($tag_version) != npm/package.json version $pkg_version" >&2 + exit 1 + fi + echo "Releasing @aoagents/ao@$pkg_version" + + # Pure-Go (CGO disabled, modernc SQLite) so a single Linux runner + # cross-compiles every GOOS/GOARCH in the matrix. + - name: Cross-compile daemon + assemble npm packages + run: node npm/scripts/build.mjs + env: + AO_COMMIT: ${{ github.sha }} + + - name: Collect release binaries + checksums + run: node npm/scripts/collect-release.mjs + + - name: Pack npm tarballs + run: node npm/scripts/pack.mjs + + - name: Publish binaries to GitHub Release + if: startsWith(github.ref, 'refs/tags/daemon-v') + uses: softprops/action-gh-release@v2 + with: + files: | + npm/build/release/ao-* + npm/build/release/SHA256SUMS + npm/build/tarballs/*.tgz + + # Prove the tarballs are publishable without touching the registry. + - name: Verify npm publish (dry run) + run: | + for t in npm/build/tarballs/*.tgz; do + echo "::group::npm publish --dry-run $t" + npm publish --dry-run --access public "$t" + echo "::endgroup::" + done + + - name: Upload npm tarballs as workflow artifacts + uses: actions/upload-artifact@v4 + with: + name: npm-tarballs + path: npm/build/tarballs/*.tgz + if-no-files-found: error diff --git a/.gitignore b/.gitignore index 0a8225ec..55890048 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,9 @@ yarn-error.log* # Go .go/ bin/ +# but keep the committed npm launcher shim (build output lives in npm/build/) +!npm/bin/ +!npm/bin/** *.test *.out vendor/ diff --git a/npm/README.md b/npm/README.md new file mode 100644 index 00000000..483e5709 --- /dev/null +++ b/npm/README.md @@ -0,0 +1,78 @@ +# @aoagents/ao + +Agent Orchestrator: a local daemon that supervises parallel coding-agent +sessions, driven by the `ao` CLI. + +```bash +npm install -g @aoagents/ao +ao start # start the loopback daemon +ao status # talk to it +``` + +## How the binary gets onto your machine + +The published `@aoagents/ao` tarball ships **no** binary. The prebuilt Go daemon +(~20 MB, per platform) is delivered through per-platform +`optionalDependencies` (the esbuild/swc pattern): + +| optional dependency | `os` | `cpu` | +| --------------------------- | ------ | ----- | +| `@aoagents/ao-darwin-arm64` | darwin | arm64 | +| `@aoagents/ao-darwin-x64` | darwin | x64 | +| `@aoagents/ao-linux-x64` | linux | x64 | +| `@aoagents/ao-linux-arm64` | linux | arm64 | +| `@aoagents/ao-win32-x64` | win32 | x64 | +| `@aoagents/ao-win32-arm64` | win32 | arm64 | + +npm installs **only** the one matching your host. The `ao` command is a thin +Node shim (`bin/ao.js`) that resolves the installed sub-package, finds its +`bin/ao` (`bin/ao.exe` on Windows), and execs it, forwarding argv, stdio, exit +code, and signals. There is no `postinstall` and no install-time network call. + +If no matching sub-package is present (unsupported platform, or installed with +`--omit=optional`), the shim exits 1 with a clear message. + +> Only the Go daemon ships via npm. The Electron desktop app is fetched from +> GitHub Releases during migration and is out of scope for this package. + +## Maintainer workflow + +The matrix is defined once in [`targets.mjs`](./targets.mjs). The committed +`package.json` `optionalDependencies` must stay in lockstep with it; CI and +`scripts/check.mjs` enforce that. + +```bash +# Validate that package.json matches the matrix. +node scripts/check.mjs + +# Cross-compile + assemble every platform sub-package and stage the main package. +node scripts/build.mjs # all targets (pure-Go, CGO disabled, cross-compiles anywhere) +node scripts/build.mjs --host # only the host target (fast local smoke test) + +# Produce .tgz tarballs from whatever was staged. +node scripts/pack.mjs # -> npm/build/tarballs/*.tgz +``` + +Everything under `npm/build/` is generated and gitignored. + +### Local smoke test + +```bash +node scripts/build.mjs --host && node scripts/pack.mjs +prefix="$(mktemp -d)" +npm install -g --prefix "$prefix" \ + npm/build/tarballs/aoagents-ao-*.tgz \ + npm/build/tarballs/aoagents-ao-$(node -p "process.platform")-$(node -p "process.arch")-*.tgz +"$prefix/bin/ao" version # execs the host Go binary -> 0.10.0 +``` + +### Release & publish + +- `.github/workflows/release.yml` (on a `daemon-v*` tag, e.g. `daemon-v0.10.0`, + mirroring the Electron app's `desktop-v*` lane): cross-compiles every target, + publishes the raw binaries to the GitHub Release, builds the npm sub-packages + from those binaries, and runs `npm publish --dry-run` as a verification gate. +- `.github/workflows/npm-publish.yml` (manual `workflow_dispatch`): builds the + packages and publishes. It defaults to a **dry run**; a real publish requires + `dry_run=false` **and** an `NPM_TOKEN` secret, neither of which is wired by + default. Publishing is a deliberate human action. diff --git a/npm/bin/ao.js b/npm/bin/ao.js new file mode 100644 index 00000000..534f082b --- /dev/null +++ b/npm/bin/ao.js @@ -0,0 +1,78 @@ +#!/usr/bin/env node +"use strict"; + +// `ao` launcher shim for @aoagents/ao. +// +// The npm tarball ships no binary itself. The prebuilt Go daemon for the host +// arrives via one of the `@aoagents/ao--` optionalDependencies — npm +// installs only the one matching `os`/`cpu`. This shim resolves that package, +// locates its binary, and execs it, forwarding argv, stdio, exit code, and +// terminating signals. The CLI verbs behind it talk to the daemon over +// loopback exactly as the native binary does. + +const path = require("path"); +const { spawnSync } = require("child_process"); + +const platform = process.platform; // darwin | linux | win32 +const arch = process.arch; // arm64 | x64 +const pkgName = `@aoagents/ao-${platform}-${arch}`; +const binName = platform === "win32" ? "ao.exe" : "ao"; + +const SUPPORTED = ["darwin-arm64", "darwin-x64", "linux-x64", "linux-arm64", "win32-x64", "win32-arm64"]; + +function resolveBinary() { + // Resolve the sub-package via its package.json (always resolvable; avoids + // extension/exports edge cases of resolving the binary file directly), then + // join the known bin path next to it. + let pkgJsonPath; + try { + pkgJsonPath = require.resolve(`${pkgName}/package.json`); + } catch { + return null; + } + return path.join(path.dirname(pkgJsonPath), "bin", binName); +} + +const binary = resolveBinary(); +if (!binary) { + process.stderr.write( + `ao: no prebuilt binary found for ${platform}-${arch}.\n` + + `\n` + + `Expected the optional dependency "${pkgName}" to be installed alongside\n` + + `@aoagents/ao, but it was not found. This usually means either:\n` + + ` - your platform is not supported, or\n` + + ` - the package was installed with optional dependencies disabled\n` + + ` (e.g. "npm install --omit=optional" / "--no-optional", or a CI cache\n` + + ` that dropped optionalDependencies).\n` + + `\n` + + `Supported platforms: ${SUPPORTED.join(", ")}.\n` + + `\n` + + `Reinstall with optional dependencies enabled:\n` + + ` npm install -g @aoagents/ao\n`, + ); + process.exit(1); +} + +const result = spawnSync(binary, process.argv.slice(2), { stdio: "inherit" }); + +if (result.error) { + if (result.error.code === "ENOENT") { + process.stderr.write( + `ao: platform package "${pkgName}" is installed but its binary is missing\n` + + ` at ${binary}. Try reinstalling: npm install -g @aoagents/ao\n`, + ); + } else { + process.stderr.write(`ao: failed to launch ${binary}: ${result.error.message}\n`); + } + process.exit(1); +} + +// Re-raise a terminating signal so the parent shell observes the right cause of +// death (e.g. Ctrl-C). Otherwise propagate the child's exit code verbatim. +if (result.signal) { + process.kill(process.pid, result.signal); + // If the re-raise did not terminate us, fall through to a non-zero exit. + process.exit(1); +} + +process.exit(result.status === null ? 1 : result.status); diff --git a/npm/package.json b/npm/package.json new file mode 100644 index 00000000..7072201c --- /dev/null +++ b/npm/package.json @@ -0,0 +1,39 @@ +{ + "name": "@aoagents/ao", + "version": "0.10.0", + "description": "Agent Orchestrator: a local daemon that supervises parallel coding-agent sessions, driven by the `ao` CLI.", + "keywords": [ + "agent-orchestrator", + "ao", + "coding-agent", + "cli", + "daemon" + ], + "license": "MIT", + "homepage": "https://github.com/aoagents/ReverbCode#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/aoagents/ReverbCode.git" + }, + "bugs": { + "url": "https://github.com/aoagents/ReverbCode/issues" + }, + "bin": { + "ao": "bin/ao.js" + }, + "files": [ + "bin/ao.js", + "README.md" + ], + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@aoagents/ao-darwin-arm64": "0.10.0", + "@aoagents/ao-darwin-x64": "0.10.0", + "@aoagents/ao-linux-x64": "0.10.0", + "@aoagents/ao-linux-arm64": "0.10.0", + "@aoagents/ao-win32-x64": "0.10.0", + "@aoagents/ao-win32-arm64": "0.10.0" + } +} diff --git a/npm/scripts/build.mjs b/npm/scripts/build.mjs new file mode 100644 index 00000000..3fa38b05 --- /dev/null +++ b/npm/scripts/build.mjs @@ -0,0 +1,156 @@ +#!/usr/bin/env node +// Cross-compile the Go daemon and assemble the npm packages. +// +// Outputs (all under npm/build/, which is gitignored): +// build/bin/-/ prebuilt daemon per target +// build/packages/ao--/ per-platform sub-package (binary + package.json) +// build/packages/ao/ staged main package (shim + package.json + README) +// +// Usage: +// node scripts/build.mjs build every target in the matrix +// node scripts/build.mjs --host build only the host target +// node scripts/build.mjs --os linux --arch arm64 build one explicit target +// +// Env: +// AO_VERSION override the version stamped into the binary + package.json +// (defaults to VERSION from targets.mjs) +// AO_COMMIT git commit to stamp (default: short HEAD if available) + +import { execFileSync } from "node:child_process"; +import { existsSync, mkdirSync, rmSync, copyFileSync, writeFileSync, chmodSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +import { targets, hostTarget, packageName, binaryName, VERSION, VERSION_PKG } from "../targets.mjs"; + +const here = dirname(fileURLToPath(import.meta.url)); +const npmRoot = join(here, ".."); +const repoRoot = join(npmRoot, ".."); +const backendDir = join(repoRoot, "backend"); +const buildDir = join(npmRoot, "build"); +const binOut = join(buildDir, "bin"); +const pkgOut = join(buildDir, "packages"); + +function parseArgs(argv) { + const args = { host: false, os: null, arch: null }; + for (let i = 0; i < argv.length; i++) { + const a = argv[i]; + if (a === "--host") args.host = true; + else if (a === "--os") args.os = argv[++i]; + else if (a === "--arch") args.arch = argv[++i]; + else throw new Error(`unknown argument: ${a}`); + } + return args; +} + +function selectedTargets(args) { + if (args.host) { + const t = hostTarget(); + if (!t) throw new Error(`unsupported host: ${process.platform}-${process.arch}`); + return [t]; + } + if (args.os || args.arch) { + const t = targets.find((t) => t.os === args.os && t.arch === args.arch); + if (!t) throw new Error(`no matrix target for ${args.os}-${args.arch}`); + return [t]; + } + return targets; +} + +function gitCommit() { + if (process.env.AO_COMMIT) return process.env.AO_COMMIT; + try { + return execFileSync("git", ["rev-parse", "--short", "HEAD"], { cwd: repoRoot }).toString().trim(); + } catch { + return ""; + } +} + +const version = process.env.AO_VERSION || VERSION; +const commit = gitCommit(); + +function ldflags() { + const parts = [`-s`, `-w`, `-X ${VERSION_PKG}.Version=${version}`]; + if (commit) parts.push(`-X ${VERSION_PKG}.Commit=${commit}`); + return parts.join(" "); +} + +function buildBinary(t) { + const outDir = join(binOut, `${t.os}-${t.arch}`); + mkdirSync(outDir, { recursive: true }); + const outFile = join(outDir, binaryName(t.os)); + console.log(` go build ${t.goos}/${t.goarch} -> ${outFile}`); + execFileSync("go", ["build", "-trimpath", "-ldflags", ldflags(), "-o", outFile, "./cmd/ao"], { + cwd: backendDir, + stdio: "inherit", + env: { ...process.env, GOOS: t.goos, GOARCH: t.goarch, CGO_ENABLED: "0" }, + }); + return outFile; +} + +function subPackageJson(t) { + return { + name: packageName(t.os, t.arch), + version, + description: `Prebuilt ao daemon for ${t.os}-${t.arch}. Installed automatically by @aoagents/ao.`, + license: "MIT", + homepage: "https://github.com/aoagents/ReverbCode#readme", + repository: { + type: "git", + url: "git+https://github.com/aoagents/ReverbCode.git", + }, + bugs: { url: "https://github.com/aoagents/ReverbCode/issues" }, + os: [t.os], + cpu: [t.arch], + files: [`bin/${binaryName(t.os)}`], + }; +} + +function assembleSubPackage(t, binaryPath) { + const dir = join(pkgOut, `ao-${t.os}-${t.arch}`); + rmSync(dir, { recursive: true, force: true }); + mkdirSync(join(dir, "bin"), { recursive: true }); + + const dest = join(dir, "bin", binaryName(t.os)); + copyFileSync(binaryPath, dest); + // npm preserves file modes in the tarball; the daemon must be executable. + chmodSync(dest, 0o755); + + writeFileSync(join(dir, "package.json"), JSON.stringify(subPackageJson(t), null, 2) + "\n"); + console.log(` assembled ${packageName(t.os, t.arch)} -> ${dir}`); + return dir; +} + +function stageMainPackage() { + const dir = join(pkgOut, "ao"); + rmSync(dir, { recursive: true, force: true }); + mkdirSync(join(dir, "bin"), { recursive: true }); + copyFileSync(join(npmRoot, "bin", "ao.js"), join(dir, "bin", "ao.js")); + copyFileSync(join(npmRoot, "package.json"), join(dir, "package.json")); + const readme = join(npmRoot, "README.md"); + if (existsSync(readme)) copyFileSync(readme, join(dir, "README.md")); + console.log(` staged @aoagents/ao -> ${dir}`); + return dir; +} + +function main() { + const args = parseArgs(process.argv.slice(2)); + const sel = selectedTargets(args); + + mkdirSync(buildDir, { recursive: true }); + console.log(`Building @aoagents/ao ${version}${commit ? ` (${commit})` : ""}`); + console.log(`Targets: ${sel.map((t) => `${t.os}-${t.arch}`).join(", ")}\n`); + + console.log("Compiling daemon binaries:"); + for (const t of sel) { + const bin = buildBinary(t); + assembleSubPackage(t, bin); + } + + console.log("\nStaging main package:"); + stageMainPackage(); + + console.log("\nDone. Tarballs can be produced with: node scripts/pack.mjs"); +} + +main(); diff --git a/npm/scripts/check.mjs b/npm/scripts/check.mjs new file mode 100644 index 00000000..318845fb --- /dev/null +++ b/npm/scripts/check.mjs @@ -0,0 +1,50 @@ +#!/usr/bin/env node +// Guard that the committed main package.json stays in lockstep with the +// platform matrix in targets.mjs. Run in CI and locally before publishing. +// +// Checks: +// - main package.json version === VERSION +// - optionalDependencies lists exactly one entry per matrix target +// - every optionalDependency is pinned to VERSION +// +// Usage: +// node scripts/check.mjs + +import { readFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +import { targets, packageName, VERSION } from "../targets.mjs"; + +const here = dirname(fileURLToPath(import.meta.url)); +const npmRoot = join(here, ".."); + +const pkg = JSON.parse(readFileSync(join(npmRoot, "package.json"), "utf8")); +const errors = []; + +if (pkg.version !== VERSION) { + errors.push(`package.json version ${pkg.version} != targets.mjs VERSION ${VERSION}`); +} + +const expected = new Set(targets.map((t) => packageName(t.os, t.arch))); +const actual = new Set(Object.keys(pkg.optionalDependencies || {})); + +for (const name of expected) { + if (!actual.has(name)) errors.push(`optionalDependencies missing matrix target: ${name}`); +} +for (const name of actual) { + if (!expected.has(name)) errors.push(`optionalDependencies has non-matrix entry: ${name}`); +} +for (const [name, range] of Object.entries(pkg.optionalDependencies || {})) { + if (range !== VERSION) { + errors.push(`optionalDependencies["${name}"] = "${range}", expected exact "${VERSION}"`); + } +} + +if (errors.length) { + console.error("Packaging consistency check FAILED:"); + for (const e of errors) console.error(` - ${e}`); + process.exit(1); +} + +console.log(`Packaging consistency OK (${targets.length} targets @ ${VERSION}).`); diff --git a/npm/scripts/collect-release.mjs b/npm/scripts/collect-release.mjs new file mode 100644 index 00000000..a4288ace --- /dev/null +++ b/npm/scripts/collect-release.mjs @@ -0,0 +1,54 @@ +#!/usr/bin/env node +// Flatten the per-target daemon binaries into build/release/ with descriptive, +// download-friendly names plus a SHA256SUMS manifest. These are the raw +// artifacts uploaded to the GitHub Release; the npm sub-packages are built from +// the same binaries under build/bin/. +// +// Run `node scripts/build.mjs` first to produce build/bin/-/. +// +// Output: +// build/release/ao--[.exe] +// build/release/SHA256SUMS +// +// Usage: +// node scripts/collect-release.mjs + +import { createHash } from "node:crypto"; +import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +import { targets, binaryName } from "../targets.mjs"; + +const here = dirname(fileURLToPath(import.meta.url)); +const npmRoot = join(here, ".."); +const binDir = join(npmRoot, "build", "bin"); +const releaseDir = join(npmRoot, "build", "release"); + +mkdirSync(releaseDir, { recursive: true }); + +const sums = []; +let collected = 0; +for (const t of targets) { + const src = join(binDir, `${t.os}-${t.arch}`, binaryName(t.os)); + if (!existsSync(src)) { + console.log(` skip ${t.os}-${t.arch} (not built)`); + continue; + } + const ext = t.os === "win32" ? ".exe" : ""; + const name = `ao-${t.os}-${t.arch}${ext}`; + const dest = join(releaseDir, name); + copyFileSync(src, dest); + const hash = createHash("sha256").update(readFileSync(dest)).digest("hex"); + sums.push(`${hash} ${name}`); + console.log(` ${name} ${hash.slice(0, 12)}…`); + collected++; +} + +if (collected === 0) { + console.error("No binaries found under build/bin/. Run scripts/build.mjs first."); + process.exit(1); +} + +writeFileSync(join(releaseDir, "SHA256SUMS"), sums.join("\n") + "\n"); +console.log(`\nCollected ${collected} binary(ies) into ${releaseDir}`); diff --git a/npm/scripts/pack.mjs b/npm/scripts/pack.mjs new file mode 100644 index 00000000..abaab503 --- /dev/null +++ b/npm/scripts/pack.mjs @@ -0,0 +1,44 @@ +#!/usr/bin/env node +// `npm pack` the staged packages from build/packages/ into build/tarballs/. +// +// Run `node scripts/build.mjs [...]` first to stage the packages. Only the +// packages that were actually staged are packed, so `build.mjs --host` followed +// by this script produces just the main + host tarballs for a local smoke test. +// +// Usage: +// node scripts/pack.mjs + +import { execFileSync } from "node:child_process"; +import { existsSync, mkdirSync, readdirSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const here = dirname(fileURLToPath(import.meta.url)); +const npmRoot = join(here, ".."); +const pkgOut = join(npmRoot, "build", "packages"); +const tarballsOut = join(npmRoot, "build", "tarballs"); + +if (!existsSync(pkgOut)) { + console.error("build/packages/ not found. Run scripts/build.mjs first."); + process.exit(1); +} + +mkdirSync(tarballsOut, { recursive: true }); + +const staged = readdirSync(pkgOut, { withFileTypes: true }) + .filter((d) => d.isDirectory()) + .map((d) => join(pkgOut, d.name)); + +if (staged.length === 0) { + console.error("No staged packages found in build/packages/."); + process.exit(1); +} + +console.log(`Packing ${staged.length} package(s) into ${tarballsOut}\n`); +for (const dir of staged) { + const out = execFileSync("npm", ["pack", "--pack-destination", tarballsOut, dir], { + encoding: "utf8", + }); + console.log(` ${out.trim().split("\n").pop()}`); +} +console.log("\nDone."); diff --git a/npm/targets.mjs b/npm/targets.mjs new file mode 100644 index 00000000..5ad77443 --- /dev/null +++ b/npm/targets.mjs @@ -0,0 +1,41 @@ +// Single source of truth for the npm packaging platform matrix. +// +// Each target maps an npm host identity (npm `os`/`cpu`, which use Node's +// process.platform/process.arch values) to the Go cross-compile identity +// (GOOS/GOARCH). The per-platform sub-package name is +// `@aoagents/ao-${os}-${arch}` and the `ao` launcher shim resolves exactly +// that name at runtime, so these strings are load-bearing on both ends. + +export const SCOPE = "@aoagents"; +export const MAIN_PACKAGE = `${SCOPE}/ao`; + +// Keep in lockstep with the main package.json `version` and the +// `optionalDependencies` it lists. `scripts/check.mjs` enforces this. +export const VERSION = "0.10.0"; + +// Go ldflags target for build metadata (matches backend/internal/cli/version.go). +export const VERSION_PKG = "github.com/aoagents/agent-orchestrator/backend/internal/cli"; + +export const targets = [ + { os: "darwin", arch: "arm64", goos: "darwin", goarch: "arm64" }, + { os: "darwin", arch: "x64", goos: "darwin", goarch: "amd64" }, + { os: "linux", arch: "x64", goos: "linux", goarch: "amd64" }, + { os: "linux", arch: "arm64", goos: "linux", goarch: "arm64" }, + { os: "win32", arch: "x64", goos: "windows", goarch: "amd64" }, + { os: "win32", arch: "arm64", goos: "windows", goarch: "arm64" }, +]; + +// Package name for a given target (or the host, given process.platform/arch). +export function packageName(os, arch) { + return `${SCOPE}/ao-${os}-${arch}`; +} + +// Binary file name shipped inside a sub-package's `bin/` directory. +export function binaryName(os) { + return os === "win32" ? "ao.exe" : "ao"; +} + +// The target matching the current host, or undefined if unsupported. +export function hostTarget() { + return targets.find((t) => t.os === process.platform && t.arch === process.arch); +}