Files
openrig/scripts/build-package.sh
T
Mike SchwarzandOpenRig contributors c9be421b9c fix(package): install with Bun without the unpublished @openrig/daemon (#66) (#68)
* fix(package): install without the unpublished @openrig/daemon (#66)

The CLI package declared the private @openrig/daemon in dependencies and
shipped it as a bundled dependency. npm used the bundle, but package managers
that resolve every dependency from the registry, such as Bun, failed with a
404.

The package already ships the daemon under daemon/. The package build now
rewrites the compiled CLI and TUI imports of @openrig/daemon/<subpath> to that
copy, using the daemon's exports map, and fails on any unmapped or unstaged
subpath or any daemon import left afterwards. The dependency, the bundled
copy and their lockfile entries are removed. Source imports and development
resolution are unchanged.

Reported by @drewpayment.

* fix(package): rewrite daemon imports from the syntax tree, not text

Parse each staged file with the TypeScript compiler and rewrite only real
import/export specifiers and literal import()/require() arguments. Text in
comments, strings and templates is no longer changed, and imports that carry
comments or use template literals are no longer missed. Unparseable files and
non-literal module arguments naming the package now fail the build.

* docs: note Bun installation and its postinstall limit

Bun can install the CLI, but OpenRig still runs on Node.js and Bun may
block the package's postinstall check.

* fix(package): run the daemon import rewrite from any checkout path

The direct-run check compared import.meta.url with a hand-built file:// URL,
which never matched when the path needed percent-encoding (spaces, #) or went
through a symlink. The script then exited 0 without rewriting anything. Compare
the decoded module path with the real path of argv[1] instead.

---------

Co-authored-by: OpenRig contributors <[email protected]>
2026-09-26 22:47:33 -07:00

170 lines
7.6 KiB
Bash
Executable File

#!/bin/bash
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
CLI_DIR="$REPO_ROOT/packages/cli"
DAEMON_DIR="$REPO_ROOT/packages/daemon"
UI_DIR="$REPO_ROOT/packages/ui"
TUI_DIR="$REPO_ROOT/packages/tui"
SUBSTANCE_SURFACE_ROOTS=()
echo "=== OpenRig Package Build ==="
echo "Repo root: $REPO_ROOT"
echo ""
# 1. Clean previous build artifacts
echo "[1/6] Cleaning previous artifacts..."
rm -rf "$CLI_DIR/daemon" "$CLI_DIR/ui" "$CLI_DIR/tui"
# 2. Build daemon
echo "[2/6] Building daemon..."
(cd "$DAEMON_DIR" && npm run build)
# 3. Build UI
echo "[3/6] Building UI..."
(cd "$UI_DIR" && npm run build)
# 4. Build TUI
echo "[4/6] Building TUI..."
(cd "$TUI_DIR" && npm run build)
# 5. Build CLI
echo "[5/6] Building CLI..."
(cd "$CLI_DIR" && npm run build)
# 6. Assemble: copy daemon + UI + TUI into CLI package
echo "[6/6] Assembling package..."
# OPR.0.4.4.11 FR-6 — deploy-identity stamp. Computed ONCE here (the runtime
# worktree is at the SHA — available at build), then written as the compiled
# build-info module into EACH package's dist (per-package generated modules;
# the committed src/build-info.ts files stay the honest dev stubs). No
# runtime cross-package import.
STAMP_SEMVER="$(node -p "require('$CLI_DIR/package.json').version")"
STAMP_COMMIT="$(git -C "$REPO_ROOT" rev-parse HEAD)"
# DIRTY = the BUILD INPUTS differ from the commit, not "the working dir has scratch in it".
# Bare `git status --porcelain` counts UNTRACKED files repo-wide, so ordinary scratch
# (.mcp.json, workspace/, scratch dirs) stamped every build on a working machine as
# dirty=true — and a dirty stamp is unidentifiable, which defeats the whole point of
# pinning a runtime to a commit. Untracked files are NOT blanket-ignored (a new
# untracked .ts under packages/*/src really would compile in), so the check is SCOPED
# to the build inputs: anything under packages/ or scripts/, tracked or not, counts
# (scripts/ included because build-package.sh IS the builder — if it differs from the
# commit, the artifact is not reproducible from that commit either).
if [ -n "$(git -C "$REPO_ROOT" status --porcelain -- packages scripts)" ]; then STAMP_DIRTY=true; else STAMP_DIRTY=false; fi
STAMP_BUILT_AT="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
echo "Build identity: ${STAMP_SEMVER} (${STAMP_COMMIT}) dirty=${STAMP_DIRTY} at ${STAMP_BUILT_AT}"
write_build_info() {
cat > "$1" <<EOF
// GENERATED by scripts/build-package.sh — the packaged build-identity stamp.
export const BUILD_INFO = {
semver: "${STAMP_SEMVER}",
commit: "${STAMP_COMMIT}",
dirty: ${STAMP_DIRTY},
builtAt: "${STAMP_BUILT_AT}",
};
export function stampFields(info = BUILD_INFO) {
if (!info.commit) return {};
return { semver: info.semver, commit: info.commit, dirty: info.dirty, builtAt: info.builtAt };
}
EOF
}
write_build_info "$DAEMON_DIR/dist/build-info.js"
write_build_info "$CLI_DIR/dist/build-info.js"
# OPR.0.5.3.7 R2 — generate the context-pack projection of canonical skills at
# PACKAGE TIME (into $DAEMON_DIR/context-packs; never committed, regenerated every
# build). Validated through the DAEMON's own manifest parser — a malformed
# projection FAILS THE BUILD here, never at serve time. Pinned to the build semver
# so served packs carry the binary's version.
echo "Generating shipped context-packs..."
(cd "$REPO_ROOT" && node scripts/generate-context-packs.mjs --version="$STAMP_SEMVER")
# Daemon: dist + assets + specs
mkdir -p "$CLI_DIR/daemon/dist"
cp -r "$DAEMON_DIR/dist/"* "$CLI_DIR/daemon/dist/"
if [ -d "$DAEMON_DIR/assets" ]; then
cp -r "$DAEMON_DIR/assets" "$CLI_DIR/daemon/assets"
SUBSTANCE_SURFACE_ROOTS+=("daemon/assets")
fi
if [ -d "$DAEMON_DIR/specs" ]; then
# OPR.0.5.6.12 LP-7 — the specs tree is copied wholesale into the npm
# artifact, so scan the actual filesystem input (including untracked build
# inputs) before the copy. Path-safety and manifest validation solve other
# problems; this is the public-substance refusal floor.
node "$REPO_ROOT/scripts/check-internal-leak-guard.mjs" \
--repo "$REPO_ROOT" \
--rules "$REPO_ROOT/scripts/internal-tokens.generated.json" \
--mode tree \
--tree "$DAEMON_DIR/specs"
cp -r "$DAEMON_DIR/specs" "$CLI_DIR/daemon/specs"
SUBSTANCE_SURFACE_ROOTS+=("daemon/specs")
fi
# OPR.0.5.3.7 R2 — the shipped context-pack projection (generated above). The daemon
# registers this as a `builtin` discovery root resolved RELATIVE TO THE BINARY
# (startup.ts: import.meta.dirname/../context-packs -> <cli>/daemon/context-packs),
# so `rig context get` serves bytes that match `rig --version` by construction.
if [ -d "$DAEMON_DIR/context-packs" ]; then
cp -r "$DAEMON_DIR/context-packs" "$CLI_DIR/daemon/context-packs"
SUBSTANCE_SURFACE_ROOTS+=("daemon/context-packs")
fi
# The named substance gate reviews the roots the packager ACTUALLY staged. Keep
# that inventory beside the artifact instead of duplicating a source-root list
# in the gate; npm's own dry-run file list supplies the complete scan surface.
node -e '
const fs = require("fs");
const [out, ...roots] = process.argv.slice(1);
if (roots.length === 0) throw new Error("no shippable substance roots were staged");
fs.writeFileSync(out, JSON.stringify({ schemaVersion: 1, roots: [...new Set(roots)].sort() }, null, 2) + "\n");
' "$CLI_DIR/daemon/substance-surfaces.json" "${SUBSTANCE_SURFACE_ROOTS[@]}"
# Built-in policies (OPR.0.4.8.3): canonical source packages/daemon/policies/builtin/
# ships with the daemon; startup materializes read-only inspection copies at
# $OPENRIG_HOME/reference/policies/builtin/ (same chain as reference docs below).
if [ -d "$DAEMON_DIR/policies" ]; then
cp -r "$DAEMON_DIR/policies" "$CLI_DIR/daemon/policies"
fi
# Reference docs: copy from repo root docs/reference/ into the assembled package
if [ -d "$REPO_ROOT/docs/reference" ]; then
mkdir -p "$CLI_DIR/daemon/docs/reference"
cp -r "$REPO_ROOT/docs/reference/"* "$CLI_DIR/daemon/docs/reference/"
fi
# Earlier builds assembled a bundled copy of the unpublished @openrig/daemon here. The package now
# imports the shipped daemon/dist directly (rewrite step below), so remove any stale copy: it would
# otherwise shadow the development workspace link for code under packages/cli.
rm -rf "$CLI_DIR/node_modules/@openrig/daemon"
# UI: dist
mkdir -p "$CLI_DIR/ui/dist"
cp -r "$UI_DIR/dist/"* "$CLI_DIR/ui/dist/"
# TUI: dist (public openrig-tui bin declared by the CLI package)
mkdir -p "$CLI_DIR/tui/dist"
cp -r "$TUI_DIR/dist/"* "$CLI_DIR/tui/dist/"
# The CLI and TUI import daemon code as @openrig/daemon/<subpath>. That package is not published, so
# rewrite those specifiers in the staged JS to the daemon/dist copy shipped above (#66). Fails the
# build on an unmapped or unstaged subpath and on any daemon import left afterwards.
node "$REPO_ROOT/scripts/rewrite-daemon-imports.mjs"
# Report
echo ""
echo "=== Package Assembled ==="
echo "CLI dist: $(find "$CLI_DIR/dist" -name '*.js' | wc -l | tr -d ' ') JS files"
echo "Daemon dist: $(find "$CLI_DIR/daemon/dist" -name '*.js' | wc -l | tr -d ' ') JS files"
echo "Daemon assets: $(find "$CLI_DIR/daemon/assets" -type f 2>/dev/null | wc -l | tr -d ' ') files"
echo "Daemon specs: $(find "$CLI_DIR/daemon/specs" -type f 2>/dev/null | wc -l | tr -d ' ') files"
echo "Daemon policies: $(find "$CLI_DIR/daemon/policies" -type f 2>/dev/null | wc -l | tr -d ' ') files"
echo "Daemon docs: $(find "$CLI_DIR/daemon/docs" -type f 2>/dev/null | wc -l | tr -d ' ') files"
echo "UI dist: $(find "$CLI_DIR/ui/dist" -type f | wc -l | tr -d ' ') files"
echo "TUI dist: $(find "$CLI_DIR/tui/dist" -type f | wc -l | tr -d ' ') files"
echo ""
echo "Ready to publish: cd packages/cli && npm publish --access public"