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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions .github/workflows/npm-publish.yml
Original file line number Diff line number Diff line change
@@ -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 }}
88 changes: 88 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -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<version>` 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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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/
Expand Down
78 changes: 78 additions & 0 deletions npm/README.md
Original file line number Diff line number Diff line change
@@ -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.
78 changes: 78 additions & 0 deletions npm/bin/ao.js
Original file line number Diff line number Diff line change
@@ -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-<os>-<arch>` 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);
39 changes: 39 additions & 0 deletions npm/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
Loading
Loading