-
Notifications
You must be signed in to change notification settings - Fork 0
feat(cli): 2.L — packaging & distribution as an engine-inlined npm bundle (ADR-0051) #49
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
44e368b
6718ffe
6a4173e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,147 @@ | ||
| # Relavium CLI release — pack once, smoke the exact artifact on every OS, then publish it (2.L · ADR-0051). | ||
| # | ||
| # Flow: `pack` builds the engine-inlined bundle and `pnpm pack`s it (resolving catalog:/workspace: to | ||
| # concrete versions) into a tarball artifact → `smoke` installs THAT SAME tarball globally on ubuntu / | ||
| # macOS / Windows and asserts the binary works (prebuilt native addons load, the bundled engine runs) → | ||
| # `publish` (only on a `v*` tag, gated on green smoke) publishes the very artifact that was smoked, with | ||
| # npm provenance. The actual publish needs the maintainer's `NPM_TOKEN` secret — it is maintainer-gated, | ||
| # like branch protection. Verifying cross-OS BEFORE publish is the Phase-3 go/no-go #7 gate. | ||
| # | ||
| # Third-party actions are pinned to a commit SHA (the `# vX.Y.Z` comment tracks the release), matching ci.yml. | ||
| name: Release CLI | ||
|
|
||
| on: | ||
| push: | ||
| tags: ['v*'] | ||
| # Manual dry-run: build + smoke cross-OS without publishing (publish is gated on a tag below). | ||
| workflow_dispatch: | ||
|
|
||
| concurrency: | ||
| group: release-${{ github.ref }} | ||
| cancel-in-progress: false | ||
|
|
||
| # Permissions are declared per job (least privilege) rather than workflow-wide. | ||
|
|
||
| jobs: | ||
| pack: | ||
| name: build + pack the tarball | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 15 | ||
| permissions: | ||
| contents: read | ||
| steps: | ||
| - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 | ||
| with: | ||
| persist-credentials: false | ||
| # On a release tag, the tag must equal the CLI's package version — else we'd publish a mismatched | ||
| # version. Fail fast (before build + the cross-OS smoke). Skipped on a manual dry-run (no tag). | ||
| - name: Tag matches the CLI package version | ||
| if: startsWith(github.ref, 'refs/tags/v') | ||
| shell: bash | ||
| run: | | ||
| TAG="${GITHUB_REF_NAME#v}" | ||
| PKG="$(node -p "require('./apps/cli/package.json').version")" | ||
| test "$TAG" = "$PKG" || { echo "::error::release tag v$TAG != apps/cli/package.json version $PKG"; exit 1; } | ||
| - name: Set up pnpm | ||
| uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4.4.0 | ||
| - name: Set up Node | ||
| uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 | ||
| with: | ||
| node-version-file: .nvmrc | ||
| cache: pnpm | ||
| - name: Install (frozen lockfile) | ||
| run: pnpm install --frozen-lockfile | ||
| # Build the engine packages first (turbo resolves the dependency order) so `pnpm pack`'s prepack can | ||
| # inline them into the bundle. | ||
| - name: Build | ||
| run: pnpm turbo run build --filter=relavium... | ||
| # `pnpm pack` (not `npm pack`) resolves catalog:/workspace: in the published manifest; its `prepack` | ||
| # rebuilds the bundle, copies the drizzle migrations beside it, AND runs the bundle-closure guard — so | ||
| # the guard validates exactly the dist that gets tarballed. The tarball is platform-independent (native | ||
| # addons resolve per-OS at install), so one tarball is smoked on, and published to, every target. | ||
| - name: Pack | ||
| working-directory: apps/cli | ||
| run: pnpm pack --pack-destination "${{ runner.temp }}/artifact" | ||
| - name: Upload tarball | ||
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 | ||
| with: | ||
| name: relavium-tarball | ||
| path: ${{ runner.temp }}/artifact/*.tgz | ||
| if-no-files-found: error | ||
|
|
||
| smoke: | ||
| name: install-smoke (${{ matrix.os }}) | ||
| needs: pack | ||
| strategy: | ||
| fail-fast: false | ||
| matrix: | ||
| os: [ubuntu-latest, macos-latest, windows-latest] | ||
| runs-on: ${{ matrix.os }} | ||
| timeout-minutes: 15 | ||
| permissions: | ||
| contents: read | ||
| steps: | ||
| # Checkout only for the fixture workflows the smoke runs; no build here — we install the published tarball. | ||
| - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 | ||
| with: | ||
| persist-credentials: false | ||
| - name: Set up Node | ||
| uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 | ||
| with: | ||
| node-version-file: .nvmrc | ||
| - name: Download tarball | ||
| uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 | ||
| with: | ||
| name: relavium-tarball | ||
| path: artifact | ||
| # One bash script across all three OSes (GitHub provides Git Bash on Windows) so the asserts are uniform. | ||
| # These commands exercise the bundle + BOTH prebuilt native addons (better-sqlite3 opens history.db with | ||
| # the shipped migrations; @napi-rs/keyring's accessor loads) WITHOUT touching the OS credential store, so | ||
| # the Windows leg is headless-safe. | ||
| - name: Install globally + smoke | ||
| shell: bash | ||
| run: | | ||
| set -euo pipefail | ||
| TGZ=$(ls artifact/*.tgz) | ||
| npm install -g "$TGZ" | ||
| echo "## relavium --help" && relavium --help >/dev/null | ||
| echo "## relavium provider list" && relavium provider list | ||
| echo "## relavium run (sequential) --json (exit 0)" | ||
| relavium run apps/cli/src/harness/fixtures/sequential.relavium.yaml --input n=21 --json | ||
| echo "## relavium run (human-gate) --json (exit 3 = gate-paused)" | ||
| set +e; relavium run apps/cli/src/harness/fixtures/human-gate.relavium.yaml --json >/dev/null; ec=$?; set -e | ||
| test "$ec" -eq 3 || { echo "expected exit 3, got $ec"; exit 1; } | ||
| echo "## relavium gate list" && relavium gate list | ||
| echo "## unknown runId → exit 2" | ||
| set +e; relavium logs nope >/dev/null 2>&1; ec=$?; set -e | ||
| test "$ec" -eq 2 || { echo "expected exit 2, got $ec"; exit 1; } | ||
| echo "✓ smoke passed on ${{ matrix.os }}" | ||
|
|
||
| publish: | ||
| name: publish to npm (provenance) | ||
| needs: smoke | ||
| # Only a real release tag publishes; a manual workflow_dispatch run stops after the cross-OS smoke. | ||
| if: startsWith(github.ref, 'refs/tags/v') | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 10 | ||
| permissions: | ||
| contents: read | ||
| id-token: write # npm provenance attestation | ||
| steps: | ||
| - name: Set up Node (npm registry) | ||
| uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 | ||
| with: | ||
| node-version-file: .nvmrc | ||
| registry-url: https://registry.npmjs.org | ||
| - name: Download the smoked tarball | ||
| uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 | ||
| with: | ||
| name: relavium-tarball | ||
| path: artifact | ||
| # Publish the EXACT artifact the smoke matrix proved — not a fresh build. catalog:/workspace: are already | ||
| # resolved in the packed manifest, so `npm publish <tarball>` is correct here (pnpm-pack did the resolving). | ||
| - name: Publish | ||
| shell: bash | ||
| run: npm publish "$(ls artifact/*.tgz)" --provenance --access public | ||
| env: | ||
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,54 @@ | ||
| # `apps/cli` — placeholder | ||
| # relavium | ||
|
|
||
| Terminal CLI (`relavium`) — the engine's first real consumer + regression harness. | ||
| > Run agent workflows from your terminal — a product of [HodeTech](https://github.com/HodeTech). | ||
|
|
||
| > **Not built yet.** Directory placeholder; built out in | ||
| > [Phase 2 — CLI](../../docs/roadmap/phases/phase-2-cli.md). No `package.json` yet, so | ||
| > pnpm/Turborepo do not treat it as a workspace. | ||
| `relavium` is the command-line surface of the [Relavium](https://github.com/HodeTech/Relavium) | ||
| local-first AI agent platform. It runs git-committable `.relavium.yaml` workflows on the same | ||
| pure-TypeScript engine as the desktop and VS Code surfaces — every step debuggable, every token and | ||
| dollar tracked, nothing leaving your machine unless you choose it. | ||
|
|
||
| ## Install | ||
|
|
||
| ```bash | ||
| npm install -g relavium | ||
| ``` | ||
|
|
||
| Requires **Node.js ≥ 20.12**. The package ships an engine-inlined bundle and installs prebuilt native | ||
| binaries, so no C/C++ toolchain is needed. | ||
|
|
||
| ## Quick start | ||
|
|
||
| ```bash | ||
| # run a workflow, streaming live progress in the terminal | ||
| relavium run ./workflows/code-review.relavium.yaml --input file=./src/index.ts | ||
|
|
||
| # CI / scripting: a stable NDJSON RunEvent stream, deterministic exit codes | ||
| relavium run ./workflows/code-review.relavium.yaml --input file=src/index.ts --json | ||
|
|
||
| # store a provider key in the OS keychain (read from stdin, never argv) | ||
| echo "$ANTHROPIC_API_KEY" | relavium provider set-key anthropic | ||
| ``` | ||
|
|
||
| ## Commands | ||
|
|
||
| | Command | Purpose | | ||
| |---|---| | ||
| | `relavium run <workflow> [--input k=v]` | Execute a workflow; streams progress (or `--json` NDJSON). | | ||
| | `relavium list [--agents]` | List discovered workflows (or agents) with last-run status. | | ||
| | `relavium logs <runId>` | Replay a past run's event stream. | | ||
| | `relavium status` | Show active/paused runs and their per-node status. | | ||
| | `relavium gate <runId> --approve\|--reject\|--input …` | Resolve a pending human gate. | | ||
| | `relavium gate list [<runId>]` | List pending human gates. | | ||
| | `relavium provider <list\|add\|set-key\|remove-key\|test>` | Manage providers + API keys (OS keychain). | | ||
|
|
||
| **Exit codes** (CI-friendly): `0` completed · `1` failed · `2` invalid invocation · `3` paused at a | ||
| human gate. Provider keys resolve from the OS keychain → `RELAVIUM_<PROVIDER>_API_KEY` env var → error. | ||
|
|
||
| ## Documentation | ||
|
|
||
| The full command reference, the `--json` machine contract, and the CI guide live in the | ||
| [Relavium docs](https://github.com/HodeTech/Relavium/tree/main/docs/reference/cli/commands.md). | ||
|
|
||
| ## License | ||
|
|
||
| Proprietary — © HodeTech, all rights reserved. See [LICENSE](https://github.com/HodeTech/Relavium/blob/main/LICENSE). |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,15 +1,57 @@ | ||
| import { cpSync, rmSync } from 'node:fs'; | ||
|
|
||
| import { defineConfig } from 'tsup'; | ||
|
|
||
| // Build the CLI to a single ESM `bin` (ADR-0047). Pure-JS deps (commander; smol-toml at | ||
| // 2.B) are inlined; the native dep (@napi-rs/keyring at 2.C) is externalized in packaging | ||
| // (2.L). The shebang is prepended so the published `dist/index.js` is directly executable. | ||
| /** | ||
| * The CLI distribution bundle (2.L, [ADR-0051](../../docs/decisions/0051-cli-distribution-thin-bundle-private-engine.md)). | ||
| * | ||
| * INLINE only the proprietary `@relavium/*` engine packages (`noExternal`); EXTERNALIZE every third-party | ||
| * dependency — they are declared in `package.json` and installed normally by npm (the prebuilt native addons | ||
| * `better-sqlite3` + `@napi-rs/keyring` cannot be bundled; the quickjs WASM + the vendor SDKs + `ink`/`react` | ||
| * are bundler-hostile). `tools/bundle-closure/check.mjs` guards that this `external` list equals the declared | ||
| * runtime `dependencies`, so the two can never drift. | ||
| * | ||
| * `sourcemap: false` + `minify: true`: the published `dist/` must ship **no** original engine source — a source | ||
| * map would embed the inlined engine's TypeScript into the tarball and defeat keeping it unpublished (ADR-0051). | ||
| * The shebang banner makes `dist/index.js` directly executable as the `relavium` bin. | ||
| */ | ||
| const THIRD_PARTY_EXTERNAL = [ | ||
| '@anthropic-ai/sdk', | ||
| '@clack/prompts', | ||
| '@google/genai', | ||
| '@jitl/quickjs-singlefile-mjs-release-sync', | ||
| '@napi-rs/keyring', | ||
| 'better-sqlite3', | ||
| 'commander', | ||
| 'drizzle-orm', | ||
| 'ink', | ||
| 'openai', | ||
| 'quickjs-emscripten-core', | ||
| 'react', | ||
| 'smol-toml', | ||
| 'yaml', | ||
| 'zod', | ||
| ]; | ||
|
|
||
| export default defineConfig({ | ||
| entry: { index: 'src/index.ts' }, | ||
| format: ['esm'], | ||
| target: 'node20', | ||
| platform: 'node', | ||
| clean: true, | ||
| sourcemap: true, | ||
| sourcemap: false, | ||
| minify: true, | ||
| dts: false, | ||
| noExternal: [/^@relavium\//], | ||
| external: THIRD_PARTY_EXTERNAL, | ||
| banner: { js: '#!/usr/bin/env node' }, | ||
| // The inlined `@relavium/db` resolves its drizzle migrations via `new URL('../drizzle', import.meta.url)`, | ||
| // which — once bundled — points beside THIS bundle, not the db package. So ship the migration set alongside | ||
| // `dist/` (`files: ["dist","drizzle"]`); `<pkg>/drizzle` is then what the bundled db code finds at runtime. | ||
| onSuccess: async () => { | ||
| // Clean first: cpSync merges (recursive) and never deletes, so a migration removed/renamed in the source | ||
| // would otherwise linger here and ship stale. Recreate from the source of truth each build. | ||
| rmSync('./drizzle', { recursive: true, force: true }); | ||
| cpSync('../../packages/db/drizzle', './drizzle', { recursive: true }); | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| }, | ||
|
Comment on lines
+51
to
+56
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Stale migration files can accumulate in onSuccess: async () => {
rmSync('./drizzle', { recursive: true, force: true });
cpSync('../../packages/db/drizzle', './drizzle', { recursive: true });
try {
cpSync('../../LICENSE', './LICENSE', { force: true });
} catch (err) {
// Ignore if root LICENSE is missing during local dev builds
}
}, |
||
| }); | ||
Uh oh!
There was an error while loading. Please reload this page.