From 0b121728ca2fd0325352a78930d5565c68a02e82 Mon Sep 17 00:00:00 2001 From: harshitsinghbhandari <24b4506@iitb.ac.in> Date: Tue, 16 Jun 2026 17:27:35 +0530 Subject: [PATCH 1/3] feat(npm): ship the rewrite as @aoagents/ao via optional-deps Package the Go daemon as the @aoagents/ao npm package (v0.10.0) using the esbuild/swc optionalDependencies pattern: one tiny per-platform sub-package (@aoagents/ao--) carries the prebuilt daemon plus os/cpu fields, and npm installs only the host match. - bin/ao.js launcher shim resolves the installed platform sub-package, execs its daemon, and forwards argv/stdio/exit code/signals. CLI verbs talk to the daemon over loopback as before. Clear error + exit 1 when no platform package is installed. - targets.mjs is the single source of truth for the platform matrix (darwin arm64+x64, linux x64+arm64, win32 x64+arm64); check.mjs enforces that the committed optionalDependencies stay in lockstep. - build.mjs cross-compiles (CGO disabled, pure-Go SQLite) and assembles every sub-package; pack.mjs produces tarballs; collect-release.mjs flattens binaries + SHA256SUMS for GitHub Releases. - repository/homepage/bugs repointed at aoagents/ReverbCode. - release.yml (on v* tag): cross-compile all targets, publish binaries to the GitHub Release, build npm sub-packages, verify with npm publish --dry-run. - npm-publish.yml: manual, dry-run by default; a real publish requires dry_run=false AND an NPM_TOKEN secret (neither wired by default). The Electron app is fetched from GitHub Releases during migration and is out of scope here. Verified locally: full-matrix cross-compile, npm pack, temp-prefix global install, and the host ao execing the Go daemon through start/status/stop. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/npm-publish.yml | 83 +++++++++++++++ .github/workflows/release.yml | 86 +++++++++++++++ .gitignore | 3 + npm/README.md | 77 ++++++++++++++ npm/bin/ao.js | 85 +++++++++++++++ npm/package.json | 39 +++++++ npm/scripts/build.mjs | 169 ++++++++++++++++++++++++++++++ npm/scripts/check.mjs | 50 +++++++++ npm/scripts/collect-release.mjs | 54 ++++++++++ npm/scripts/pack.mjs | 44 ++++++++ npm/targets.mjs | 41 ++++++++ 11 files changed, 731 insertions(+) create mode 100644 .github/workflows/npm-publish.yml create mode 100644 .github/workflows/release.yml create mode 100644 npm/README.md create mode 100644 npm/bin/ao.js create mode 100644 npm/package.json create mode 100644 npm/scripts/build.mjs create mode 100644 npm/scripts/check.mjs create mode 100644 npm/scripts/collect-release.mjs create mode 100644 npm/scripts/pack.mjs create mode 100644 npm/targets.mjs 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..da95c720 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,86 @@ +name: 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 `v` tag that matches npm/package.json (e.g. v0.10.0). + +on: + push: + tags: + - "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/v') + run: | + pkg_version="$(node -p "require('./npm/package.json').version")" + tag_version="${GITHUB_REF_NAME#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/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..f6b04051 --- /dev/null +++ b/npm/README.md @@ -0,0 +1,77 @@ +# @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 `v*` tag): 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..39759696 --- /dev/null +++ b/npm/bin/ao.js @@ -0,0 +1,85 @@ +#!/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..8b35e3c4 --- /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..3375caff --- /dev/null +++ b/npm/scripts/build.mjs @@ -0,0 +1,169 @@ +#!/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..9b2e706c --- /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..8bff3b20 --- /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..94e9ca7d --- /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..b1229f7f --- /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); +} From 22f665cc3914f153bc87aa7bbf6b9e3f48e28372 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 16 Jun 2026 11:58:03 +0000 Subject: [PATCH 2/3] chore: format with prettier [skip ci] --- npm/README.md | 16 +-- npm/bin/ao.js | 85 +++++++------- npm/package.json | 74 ++++++------- npm/scripts/build.mjs | 191 +++++++++++++++----------------- npm/scripts/check.mjs | 18 +-- npm/scripts/collect-release.mjs | 30 ++--- npm/scripts/pack.mjs | 20 ++-- npm/targets.mjs | 18 +-- 8 files changed, 216 insertions(+), 236 deletions(-) diff --git a/npm/README.md b/npm/README.md index f6b04051..08f4001e 100644 --- a/npm/README.md +++ b/npm/README.md @@ -15,14 +15,14 @@ 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 | +| 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 diff --git a/npm/bin/ao.js b/npm/bin/ao.js index 39759696..534f082b 100644 --- a/npm/bin/ao.js +++ b/npm/bin/ao.js @@ -18,68 +18,61 @@ 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", -]; +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); + // 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); + 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); + 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.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 index 8b35e3c4..7072201c 100644 --- a/npm/package.json +++ b/npm/package.json @@ -1,39 +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" - } + "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 index 3375caff..3fa38b05 100644 --- a/npm/scripts/build.mjs +++ b/npm/scripts/build.mjs @@ -21,14 +21,7 @@ import { existsSync, mkdirSync, rmSync, copyFileSync, writeFileSync, chmodSync } import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; -import { - targets, - hostTarget, - packageName, - binaryName, - VERSION, - VERSION_PKG, -} from "../targets.mjs"; +import { targets, hostTarget, packageName, binaryName, VERSION, VERSION_PKG } from "../targets.mjs"; const here = dirname(fileURLToPath(import.meta.url)); const npmRoot = join(here, ".."); @@ -39,131 +32,125 @@ 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; + 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; + 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 ""; - } + 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(" "); + 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; + 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)}`], - }; + 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; + 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; + 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); + 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`); + 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("Compiling daemon binaries:"); + for (const t of sel) { + const bin = buildBinary(t); + assembleSubPackage(t, bin); + } - console.log("\nStaging main package:"); - stageMainPackage(); + console.log("\nStaging main package:"); + stageMainPackage(); - console.log("\nDone. Tarballs can be produced with: node scripts/pack.mjs"); + 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 index 9b2e706c..318845fb 100644 --- a/npm/scripts/check.mjs +++ b/npm/scripts/check.mjs @@ -23,28 +23,28 @@ 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}`); + 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}`); + 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}`); + 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 (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.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 index 8bff3b20..a4288ace 100644 --- a/npm/scripts/collect-release.mjs +++ b/npm/scripts/collect-release.mjs @@ -30,24 +30,24 @@ 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++; + 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); + console.error("No binaries found under build/bin/. Run scripts/build.mjs first."); + process.exit(1); } writeFileSync(join(releaseDir, "SHA256SUMS"), sums.join("\n") + "\n"); diff --git a/npm/scripts/pack.mjs b/npm/scripts/pack.mjs index 94e9ca7d..abaab503 100644 --- a/npm/scripts/pack.mjs +++ b/npm/scripts/pack.mjs @@ -19,26 +19,26 @@ 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); + 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)); + .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.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()}`); + 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 index b1229f7f..5ad77443 100644 --- a/npm/targets.mjs +++ b/npm/targets.mjs @@ -17,25 +17,25 @@ export const VERSION = "0.10.0"; 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" }, + { 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}`; + 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"; + 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); + return targets.find((t) => t.os === process.platform && t.arch === process.arch); } From dd1360aa3c2832c598d15766a30823b48836bbce Mon Sep 17 00:00:00 2001 From: harshitsinghbhandari <24b4506@iitb.ac.in> Date: Tue, 16 Jun 2026 17:31:48 +0530 Subject: [PATCH 3/3] chore(ci): tag the daemon release as daemon-v* to mirror desktop-v* Co-Authored-By: Claude Opus 4.8 --- .github/workflows/release.yml | 14 ++++++++------ npm/README.md | 3 ++- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index da95c720..7daeefd8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,4 +1,4 @@ -name: Release +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 @@ -7,12 +7,14 @@ name: Release # This workflow does NOT publish to npm. Real publishing is the manual, # credential-gated `npm-publish.yml`. # -# Trigger: push a `v` tag that matches npm/package.json (e.g. v0.10.0). +# 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: - - "v*" + - "daemon-v*" workflow_dispatch: permissions: @@ -37,10 +39,10 @@ jobs: run: node npm/scripts/check.mjs - name: Verify tag matches package version - if: startsWith(github.ref, 'refs/tags/v') + if: startsWith(github.ref, 'refs/tags/daemon-v') run: | pkg_version="$(node -p "require('./npm/package.json').version")" - tag_version="${GITHUB_REF_NAME#v}" + 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 @@ -61,7 +63,7 @@ jobs: run: node npm/scripts/pack.mjs - name: Publish binaries to GitHub Release - if: startsWith(github.ref, 'refs/tags/v') + if: startsWith(github.ref, 'refs/tags/daemon-v') uses: softprops/action-gh-release@v2 with: files: | diff --git a/npm/README.md b/npm/README.md index 08f4001e..483e5709 100644 --- a/npm/README.md +++ b/npm/README.md @@ -68,7 +68,8 @@ npm install -g --prefix "$prefix" \ ### Release & publish -- `.github/workflows/release.yml` (on a `v*` tag): cross-compiles every target, +- `.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