04-cli-build-distribution.md 36 KB

04 — CLI, Build Pipeline & Distribution

Owner: Agent D Status: Specification (no implementation) Scope: The oac npm CLI, the neutral-source → per-tool build pipeline, full install.sh/update.sh parity, and cross-platform npm distribution. Reads-from: 01-feature-inventory.md (must-survive list), 02-canonical-schema.md (the IR this CLI parses), 03-adapter-specs.md (the transforms oac build invokes).

This document specifies the CLI that replaces install.sh (1510 lines, 35 functions) and update.sh (344 lines). Both bash scripts are deleted per the locked decisions in 00-INDEX.md. Nothing they do may be lost — §3 is the line-by-line parity contract.


0. Context: what exists today vs. what must change

0.1 What is already built (extend, do not reinvent)

Asset Location Reuse
CLI entry packages/cli/src/index.ts + bin/oac.js wrapper Keep; add build, remove already exists
Commands commands/{init,add,update,apply,doctor,list,status}.ts Keep all; apply becomes an alias/subset of build
Install engine lib/installer.ts (installFiles, updateFiles, sha256 decide-logic) Keep — this is the collision/update strategy engine
Manifest lib/manifest.ts.oac/manifest.json (per-file sha256) Keep — replaces install.sh collision detection
Config lib/config.ts.oac/config.json (yoloMode, autoBackup) Keep
Bundled files lib/bundled.ts (getPackageRoot, listBundledFiles) Rework to bundle /content not .opencode/
Registry lib/registry.ts (Zod-validated registry.json) Keep; extend for profiles + dependency resolution
IDE detect lib/ide-detect.ts Keep; feeds --target auto-detection
Adapters packages/compatibility-layer/{adapters,core,mappers} The engine oac build drives

0.2 The three structural changes this refactor forces on the CLI

  1. Source flips. Today installFiles/bundled.ts copy .opencode/** verbatim into the user's project. After the refactor, the bundle is /content (tool-neutral), and .opencode/ is a generated build target — so a plain copy is no longer enough for OpenCode users. oac init for a tool now means install /content + build the target.
  2. apply generalises into build. commands/apply.ts already does loadAgents → adapter.fromOAC → write for Cursor/Claude/Windsurf. oac build is that pipeline, promoted to a first-class command, reading /content (not .opencode/agent/) and adding OpenCode as just another target (§1, §2).
  3. Runtime must be Node-portable. bin/oac.js currently execFileSync('bun', …) and the whole CLI uses Bun-only APIs (Bun.file, Bun.write, import.meta.dir). A user running npm i -g @controlstack/oac will not have Bun. This is the single biggest cross-platform install blocker and is resolved in §4.1.

1. Command spec

Global conventions (all commands):

  • Flags: --dry-run (plan only, no writes), --verbose, --yolo (backup + overwrite user-modified files), --json (machine output where meaningful), -v/--version, -h/--help.
  • Project root = process.cwd(), validated by isProjectRoot() (has package.json or .git).
  • State files: .oac/manifest.json (sha256 ledger), .oac/config.json (prefs), .oac/backups/{ISO-timestamp}/… (yolo/backup copies).
  • Exit codes: 0 success (warnings allowed), 1 hard error. --dry-run never writes.

1.1 oac init — set up a project (EXTENDED)

Set up OAC in the current project: install neutral /content and generate the chosen target(s).

Aspect Spec
Args none
Flags --target <tool...> (opencode|claude|cursor|windsurf; repeatable; default = auto-detect via ide-detect, fallback opencode), --profile <name> (essential|developer|business|full|advanced; default essential), --global (install to user-global location, §4.2), --dir <path> (explicit install dir; replaces install.sh --install-dir), --no-build (install /content only, skip target generation), --yolo, --dry-run, --verbose
Reads bundled /content/** + registry.json from package root; existing .oac/manifest.json (if re-init)
Writes content/** (neutral source, tracked in manifest) + target output (e.g. .opencode/**, CLAUDE.md, .cursorrules) + .oac/manifest.json + .oac/config.json (only if absent)
Behavior 1) assert project root; 2) resolve profile → component set + dependency closure (§2.3); 3) resolve install location (§4.2); 4) print plan (files, targets, location) + collision preview; 5) installFiles(profileFiles) into content/; 6) unless --no-build, run build pipeline (§2) for each --target; 7) write manifest + config; 8) summary. CI=true implies --yolo (already implemented).

Replaces: install.sh interactive location menu + main menu + profile menu + preview + install.

1.2 oac add <ref> — add one component (EXTENDED with deps)

Aspect Spec
Args ref = <type>:<id> (e.g. agent:code-reviewer, context:security, skill:task-management). No arg → list available components grouped by type (current behavior).
Flags --target <tool...> (rebuild these targets after add; default = targets already present per manifest), --force (reinstall if present), --no-deps (skip dependency resolution — new), --dry-run, --yolo, --verbose
Reads registry.json, bundled /content, manifest
Writes component file(s) into content/** (skills copy their files[] array — already supported by registry files), manifest entry per file, then re-build affected targets
Behavior resolve ref → component; resolve dependencies recursively (§2.3) unless --no-deps (this is a parity gap — install.sh resolve_dependencies exists, current add.ts does not resolve); wildcard context refs (context:core/*) expand via registry (§2.3); copy files; update manifest; rebuild targets so the new component appears in generated output.

Replaces: install.sh custom component selection + resolve_dependencies + expand_context_wildcard.

1.3 oac remove <ref> — remove a component (EXISTS)

Aspect Spec
Args ref = <type>:<id> (required)
Flags --target <tool...> (rebuild after removal), --dry-run, --verbose
Reads registry.json, manifest
Writes deletes content/** file(s), removes manifest entry, rebuilds targets
Behavior current impl already deletes file + updates manifest. Add: rebuild targets so generated output no longer references the removed component; warn if other installed components depend on it (reverse-dependency check via registry).

1.4 oac update — update installed files (EXISTS, keep)

Aspect Spec
Args none
Flags --dry-run / --check (alias), --yolo, --verbose, --target <tool...> (rebuild after content update)
Reads manifest, bundled /content, config (picks up persisted yoloMode)
Writes updated content/**, manifest, rebuilt targets
Behavior updateFiles() algorithm (already built): per bundled file — in-manifest+hash-matches → safe update; in-manifest+hash-differs → skip (or --yolo: backup+overwrite); not-in-manifest → install. Files in manifest but no longer in bundle → drop from manifest, leave user copy, warn. Add: after content update, rebuild every target recorded in the manifest so generated output tracks the new source.

Replaces: all of update.sh — and does it far better (sha256 protection of user edits vs. update.sh's blind curl-overwrite-with-backup).

1.5 oac build — generate tool output from neutral source (NEW)

The centerpiece of the refactor. Loads /content, parses to IR, runs the adapter, writes the tool's file layout, validates the result.

Aspect Spec
Args [target] optional positional (e.g. oac build opencode)
Flags --target <tool...> (repeatable; alternative to positional), --all (build every detected/known target), --out <dir> (override output root; default per-target convention), --dry-run (preview + diff, write nothing), --verbose (per-transform + degradation warnings), --strict (treat adapter warnings as errors → exit 1), --check (build to temp, diff against on-disk output, exit 1 if drift — for CI), --json
Reads content/** (agents, skills, commands, context) + registry.json (dependency graph, ordering) + .oac/config.json
Writes target-specific layout (from 03-adapter-specs.md): opencode → .opencode/agent/** + agent-metadata.json sidecars + .opencode/skills/** + .opencode/command/** + opencode.json; claude → agents/*.md + .claude-plugin/plugin.json + wired context/; cursor → .cursorrules; windsurf → .windsurfrules. Each written file is added to the manifest with source: "generated" and its sha256.
Behavior full pipeline in §2. Target selection: explicit --target/positional > --all > auto-detect via ide-detect > default opencode. Emits degradation warnings predicted by CapabilityMatrix (e.g. "temperature dropped for Claude") — never silent loss.

oac apply (current) is retained as a thin deprecated aliasoac build --target <ide> for Cursor/Claude/Windsurf, so existing muscle memory and docs keep working during migration (migration detail owned by Agent E).

1.6 oac doctor — health check (EXISTS, extend)

Runs the 7 checks already implemented (Bun/Node runtime, config, manifest, files-on-disk, sha256 modified-file detection, IDE detection, npm version vs latest). Extend with:

  • /content parse check — every content/** agent/skill validates against the Zod IR (§2.1).
  • Build-drift check — for each target in the manifest, is on-disk output in sync with a fresh build of current /content? (reuses oac build --check). Reports "run oac build".
  • Dependency check — every installed component's dependencies[] are also present.
  • Keep --json (CI) and exit 1 on errors.

Replaces: install.sh check_dependencies / check_bash_version (now a runtime check).

1.7 oac status — quick project summary (EXISTS)

Reads manifest + config + ide-detect. Prints component counts by type, modified-file count (sha256 diff), detected IDEs, and installed targets. Add: show which targets are built and whether they are stale relative to /content. Read-only, exit 0.

1.8 oac list — list components (EXISTS, two modes)

  • oac list (no manifest context) / oac add (no ref) → list available registry components grouped by type with oac add <type>:<id> hints.
  • oac list --installed → list components tracked in the manifest (current list.ts behavior), filterable by --type/--agents/--context/--skills, showing install date + user-modified flag.

Replaces: install.sh list_components.

1.9 Command → target-selection summary

Command Touches /content Runs build pipeline Target selection
init install yes (unless --no-build) --target > auto-detect > opencode
add install file yes (affected targets) manifest targets or --target
remove delete file yes (affected targets) manifest targets or --target
update update files yes (all manifest targets) manifest targets or --target
build read only yes --target/positional > --all > auto-detect > opencode
doctor/status/list read only no (--check only)

2. Build pipeline

oac build is a pure, deterministic function of /content + registry.json + target adapter. Same inputs ⇒ byte-identical output (required for golden-snapshot tests and --check drift detection). Stages:

/content/**                                                    packages/compatibility-layer
   │                                                                    │
   ▼                                                                    ▼
[1] LOAD ──► [2] PARSE→IR ──► [3] RESOLVE deps/order ──► [4] ADAPT ──► [5] BUNDLE context
                                                                          │
                                            [7] MANIFEST ◄── [6] WRITE ◄──┤
                                                    │                     │
                                                    └──► [8] VALIDATE ────┘

Stage 1 — Load

Enumerate content/{agents,skills,commands,context}/**. Reuse the enumeration pattern from bundled.ts (collectFiles), but rooted at content/ and skipping node_modules/ (issue #308 — the current listBundledFiles already excludes it structurally; oac build must too, and update.sh's -not -path "*/node_modules/*" guard must survive here). Split frontmatter + body.

Stage 2 — Parse to IR

Validate each file's frontmatter against the canonical Zod schema (OpenAgentSchema / 02-canonical-schema.md) via compatibility-layer's AgentLoader.loadAgents. On invalid frontmatter: collect a structured error with file path + Zod issue list, and fail the file, not the build (partial builds are useful — mirrors installer.ts per-file error handling). --strict promotes any parse warning to a build failure.

Stage 3 — Resolve dependencies & order

Use registry.json dependencies[] to compute the transitive closure of what a target needs, and a stable topological order so output is deterministic. This is where the neutral graph lives — resolve_dependencies + expand_context_wildcard from install.sh move here as typed functions (see §2.3). Cycles → reported error, not infinite recursion (the bash version could self-reference; the TS version must guard with a visited-set).

Stage 4 — Adapt (IR → tool)

Select the adapter from AdapterRegistry for the target and call adapter.fromOAC(agent) for each agent (and the analogous skill/command transforms per 03-adapter-specs.md). The adapter returns a ConversionResult with configs[] (path + content) and warnings[]. CapabilityMatrix predicts lossy transforms (temperature dropped for Claude, granular permissions flattened to a tool allowlist, semantic tier resolved by project/adapter configuration or omitted for the tool default — no hardcoded models, locked decision #2). All warnings surface to the user; --verbose lists each, otherwise a count.

Stage 5 — Bundle context

Context files referenced by an agent's context[] are copied/wired per target:

  • opencode — context files copied under .opencode/context/, and path references rewritten. This is the home of install.sh's global-path rewrite (@.opencode/context/@<install-dir>/context/) — see §2.4. Metadata split into agent-metadata.json sidecars.
  • claude — context copied into the plugin context/, wired via a session-start hook; contributes entries to .claude-plugin/plugin.json.
  • cursor/windsurf — context inlined into the single rules file (size-limited; apply.ts already warns at 80KB / errors at 100KB for Cursor — that guard carries into build).

Stage 6 — Write

oac build is a pure projection of canonical content and build configuration. It writes every config.path under the target output root (or --out) through a temp directory followed by an atomic replace. Existing generated files are replaced even when manually edited; generated output is not an authoring surface. --check builds to a temp directory and fails on any diff, while --dry-run prints that diff without writing.

The collision/update engine applies only when init, add, or update changes user-owned canonical content/ or custom files. Those commands skip user-modified source unless --yolo (backup then overwrite). Keeping source-update safety outside build preserves both user edits and the invariant that identical content/config always produces identical generated output.

Stage 7 — Manifest generation

Every written file gets a deterministic manifest entry: { sha256, type, source: "generated", target }. The manifest becomes the single source of truth for "what OAC produced" — replacing install.sh's ad-hoc collision scan and update.sh's find-everything approach. source distinguishes bundled (neutral /content copied), registry (added component), generated (build output), custom (user file — never touched).

Stage 8 — Validate

After writing, re-load the generated output and assert it is well-formed for the target (e.g. parse the generated OpenCode frontmatter; validate plugin.json against Claude's plugin schema; assert Cursor file ≤ size limit). Validation failures in --strict/--check → exit 1. This is the &#34;golden snapshot + parse/manifest check on ONE worked agent" that the locked minimal-test decision (#3) requires, run live.

2.1 Determinism requirements

  • Stable sort of inputs (by id) before writing.
  • No timestamps inside generated content or the deterministic manifest. Optional operational timestamps live in separate state/telemetry and are ignored by build/check comparisons.
  • Frontmatter key order fixed by the adapter, not by object-iteration order.
  • oac build && oac build is a no-op because both runs generate and compare the same temp tree.

2.2 Profiles

Profiles (essential, developer, business, full, advanced) live in registry.json profiles.*.components[] (25/41/25/50/68 components; advanced adds additionalPaths .Building/, .github/workflows/). oac init --profile selects the component set; the CLI resolves it exactly as install.sh get_profile_components did, then runs dependency closure. additionalPaths (advanced) — install.sh only printed these as "manual download required"; the CLI must actually copy them from the bundle (parity improvement, not just parity).

2.3 Dependency & wildcard resolution (moves from bash to packages/core)

install.sh did this in resolve_dependencies (recursive) + expand_context_wildcard (context:core/* → all matching context ids) + expand_selected_components (dedupe). Reimplement as typed, cycle-safe functions in packages/core (or lib/registry.ts), consumed by init, add, and Stage 3. Must handle: singular/plural type keys (get_registry_key), aliases (.aliases[] in registry), and non-.md context paths like paths.json (issue #251/#252 — resolve_component_path already special-cases this; preserve it).

2.4 Path rewriting for non-local installs

install.sh perform_installation rewrote @.opencode/context/ and .opencode/context references to the absolute install dir whenever installing anywhere other than a local .opencode/. This must survive as a Stage-5 concern: when the OpenCode target root is not the default project-local .opencode/ (i.e. --global or --dir), the adapter/bundler rewrites context path references to the resolved absolute location. This is central to issues #321/#326 (global vs project-local paths).

2.5 Project discovery (.oac.json)

Use one discovery chain:

  1. If <cwd>/.oac.json exists, parse context.root; resolve it relative to that file and require the directory plus its registry/root anchor. Invalid config is a hard error, not a silent scan.
  2. Otherwise walk parent directories to the filesystem root looking for .oac.json.
  3. If none exists, accept <project>/content/ only when its registry/root anchor validates it.
  4. After successful fallback discovery in a writable project, write .oac.json atomically with { "version": 1, "context": { "root": "content" } } so the next run takes the fast path.
  5. If discovery fails, stop with oac init guidance; never fall back to .opencode/context or a global directory implicitly.

OAC_PACKAGE_ROOT is separate: it locates OAC's bundled starter content, not the user's editable project content. All commands call the same typed discovery function from packages/core.


3. install.sh → CLI parity checklist (all 35 functions)

Every function in install.sh maps to a CLI equivalent or is justified obsolete. Nothing lost.

# install.sh function CLI equivalent Notes
1 jq_exec lib/registry.ts (readRegistry, Zod) Native JSON parse; no jq dependency. Also strips \r — Node parse is CRLF-safe.
2 print_header ui/logger.ts + ui/spinner.ts Banner via logger.
3 print_success ui/logger.ts success()
4 print_error ui/logger.ts error()
5 print_info ui/logger.ts info()
6 print_warning ui/logger.ts warn()
7 print_step ui/logger.ts bold()/info() + spinner
8 normalize_and_validate_path lib/paths.ts normalizeInstallPath() (new) Tilde expansion, \/, trailing-slash strip, relative→absolute. Node path+os.homedir(). Cross-platform (#304/#312).
9 validate_install_path lib/paths.ts validateInstallPath() (new) Parent-exists + writable checks via fs.access.
10 get_global_install_path lib/paths.ts getGlobalInstallDir() (new) ~/.config/opencode (all platforms today); §4.2 revisits Windows %APPDATA%.
11 check_bash_version obsolete No bash. Replaced by Node/Bun runtime check in doctor (checkBunVersion → generalized).
12 check_dependencies (curl, jq) obsolete No curl/jq — fetch + native JSON built into runtime. doctor checks the runtime instead.
13 fetch_registry (file:// or curl) lib/registry.ts readRegistry() + §4.5 remote/cache Registry is bundled in the package (offline by default); optional remote fetch + cache in §4.5.
14 get_profile_components lib/registry.ts getProfileComponents() (new) Reads profiles.*.components[]. §2.2.
15 get_component_info lib/registry.ts resolveComponent() Already exists; extend for context-path special case.
16 resolve_component_path lib/registry.ts getDestRelativePath() / getBundledSourcePath() Exists; preserve .md-then-raw fallback for paths.json (#251).
17 get_registry_key (singular/plural) lib/registry.ts type normalization ComponentTypeSchema + plural section keys; fold aliases in.
18 get_install_path (strip .opencode/) lib/registry.ts INSTALL_DIRS + install-dir join Post-refactor: source lands in content/, targets under their own roots.
19 expand_context_wildcard packages/core expandWildcard() (new) §2.3. Typed, cycle-safe.
20 expand_selected_components (dedupe) packages/core dedupeComponents() (new) Set-based dedupe.
21 resolve_dependencies (recursive) packages/core resolveDependencyClosure() (new) §2.3. Current parity gapadd.ts does not yet resolve deps; must add.
22 check_interactive_mode (TTY guard) commander + process.stdin.isTTY curl|bash pipe → non-interactive; profile/flags drive it. CI=true--yolo.
23 show_install_location_menu oac init flags --global/--dir (+ optional prompt) Non-interactive first; interactive prompt optional (Open Q).
24 show_main_menu oac init / oac add command routing Commander subcommands replace the menu.
25 show_profile_menu oac init --profile <name> Flag replaces menu; oac list --profiles to browse (Open Q).
26 show_custom_menu oac add <ref> (repeatable) Per-component add replaces category menu.
27 show_component_selection oac add (no ref) lists; user picks refs printAvailableComponents() exists.
28 show_installation_preview oac init/add --dry-run + pre-write plan printPlan() exists in init.ts; extend with collision preview.
29 show_collision_report lib/installer.ts decide-logic + summary printers sha256 manifest diff replaces filename-only collision scan; grouped report in summary.
30 get_install_strategy (skip/overwrite/backup/cancel) --yolo flag + config.autoBackup + skip-default Default = skip user-modified (safe); --yolo = backup+overwrite; cancel = don't run. Backups → .oac/backups/{ts}/.
31 perform_installation lib/installer.ts installFiles/updateFiles + oac build The core engine, already built + Stage 6 write.
32 show_post_install init.ts printSummary() Exists; add "run oac build/next steps".
33 list_components oac list / oac add (no ref) Exists (§1.8).
34 cleanup_and_exit Node process exit + try/finally temp cleanup No global temp dir needed (no downloads by default); §4.5 cache cleanup if remote used.
35 main (arg parse, profile shortcuts, env vars) index.ts + commander OPENCODE_INSTALL_DIR--dir, OPENCODE_BRANCH→(obsolete; bundled), profile posit:onals→--profile.

Obsolete-and-why summary: check_bash_version, check_dependencies (no bash/curl/jq — Node runtime + fetch + native JSON), the interactive show_*_menu chain (Commander flags + non-interactive-first design; optional prompts are a nice-to-have, see Open Questions). Everything else has a living equivalent.

3.1 update.sh parity

update.sh capability CLI equivalent
get_global_install_path / normalize_path / resolve_install_dir (CLI→env→local→global auto-detect) oac update resolves target from manifest location; --dir/--global; lib/paths.ts
update_component (backup, curl, restore-on-fail) installer.ts updateFiles — sha256-gated, atomic per file, .oac/backups/
update_all_components (find *.md/*.ts/*.sh, skip node_modules) listBundledFiles (already excludes node_modules, #308) + manifest-driven set
Blind overwrite-with-backup Superseded by user-edit protection: user-modified files skipped unless --yolo
cleanup_backups (trap) .oac/backups/{ts}/ retained (not auto-deleted) — user-recoverable; oac cleanup skill can prune

oac update is strictly safer than update.sh: it never clobbers a user's local edits by default, whereas update.sh overwrote every file and relied on a transient .backup.


4. Cross-platform distribution

Core goal (from the task): better cross-platform install via npm, one system exporting to different AI tools.

4.1 Runtime portability (THE blocker) — Node-first, Bun-optional

Problem: bin/oac.js does execFileSync('bun', …) and the CLI uses Bun.file, Bun.write, Bun.version, import.meta.dir. A npm i -g user on Windows/macOS/Linux without Bun gets "Bun is required". That defeats npm distribution.

Decision: ship a Node-compatible build. Two viable paths (pick in Open Questions):

  • (A) Portability shim — replace Bun APIs with a tiny lib/fs.ts abstraction over node:fs/promises (readFile/writeFile/mkdir -p), node:crypto (already used in sha256.ts), and import.meta.urlfileURLToPath for __dirname. Build with tsc/esbuild targeting Node ≥18. bin/oac.js runs node dist/index.js directly — no Bun. Recommended: smallest dependency surface, works everywhere npm works.
  • (B) Bundle a runtime — compile a standalone binary per platform (Bun --compile) and publish via optional-deps or postinstall. Heavier, platform matrix to maintain, npm-global unfriendly.

Either way: keep Bun as the dev/test runtime (bun test is fast), but the published artifact must run on stock Node. Root package.json already declares engines.node >=18.

4.2 Install locations — local vs global (issues #321, #326)

Mode Neutral source (content/) Target output State (.oac/)
Project-local (default) <project>/content/ <project>/.opencode/, <project>/CLAUDE.md, … <project>/.oac/
Global (--global) ~/.config/oac/content/ ~/.config/opencode/ (OpenCode global), tool-global dirs ~/.config/oac/
Custom (--dir <path>) <path>/content/ <path>/… <path>/.oac/
  • Default is project-local — the safe, no-surprise default. --global/--dir are explicit opt-ins (mirrors install.sh's location menu but flag-driven).
  • No accidental source overwrite (#321/#326): init/add/update writes to canonical or custom files are gated by the sha256 manifest; generated targets are replaced by pure build. Path normalization (lib/paths.ts) rejects paths that escape the install root (update.sh already guarded ../ — preserve that: reject ../absolute escapes before any write).
  • Global path today is ~/.config/opencode on all platforms (from get_global_install_path). Windows should additionally honor %APPDATA%/%LOCALAPPDATA% — flagged in Open Questions.

4.3 Windows support (issues #304, #312)

  • No shell scripts: deleting install.sh/update.sh removes the bash-3.2/Git-Bash dependency entirely — the #1 Windows friction. Pure Node CLI runs in PowerShell/CMD natively.
  • Path handling: use node:path throughout (never string-concat /); normalizeInstallPath converts \/ internally and resolves drive-letter paths (C:\…). The bash version already handled ^[A-Za-z]: — the TS port must match.
  • No color/emoji assumptions: ui/logger.ts should degrade on terminals without color (install.sh gated on WT_SESSION/ConEmuPID; chalk auto-detects TTY/NO_COLOR).
  • Line endings: JSON parsing is CRLF-safe natively (removes the tr -d '\r' hack in jq_exec).
  • Case-insensitive FS: manifest keys are stored as authored (POSIX-style relative paths); do not assume case-sensitivity when checking existence.

4.4 npx vs global install

Method Command Use case Notes
npx (no install) npx @controlstack/oac init one-off / trial / CI Downloads on demand; needs Node ≥20; the modern replacement for curl \| bash.
Global npm i -g @controlstack/oac && oac init frequent users oac on PATH; oac doctor checks for updates vs npm registry.
Dev/local pnpm --dir packages/cli dev contributors OAC_PACKAGE_ROOT lets the CLI find bundled /content in the monorepo.

Registry is bundled, so npx and global both work offline after download — no network at run time (unlike install.sh, which curled every file from raw.githubusercontent.com).

4.5 Registry fetching, caching, offline

  • Bundled + offline only in v1. registry.json and /content ship inside the package (readRegistry reads from getPackageRoot()). No network is needed for init/add/build/update. A remote catalog without matching bundled source cannot install a component reliably, so v1 has no --registry flag or registry cache.
  • doctor already does a non-blocking npm-latest check with a 5s timeout and offline fallback — the model for all network calls: never block on the network.

4.6 Checksum / collision / update strategy (from install.sh, upgraded)

Concern install.sh CLI
Collision detect filename existence scan (perform_installation) sha256 manifest — knows if the existing file is OAC's unchanged copy or a user edit
Strategy choice interactive menu (skip/overwrite/backup/cancel) default skip user-modified; --yolo=backup+overwrite; config.autoBackup
Backup .opencode.backup.<timestamp>/ mirror .oac/backups/<ISO-timestamp>/<relpath>
Update safety blind curl overwrite + .backup/restore user edits never lost without --yolo
Integrity none every tracked file has a sha256; doctor detects drift/corruption

4.7 Migration for existing curl | bash users

Existing users have a project-local .opencode/ installed by install.sh but no .oac/ manifest. Migration path (details owned by Agent E; CLI responsibilities here):

  1. oac init (or a dedicated oac migrate) detects an OAC-shaped .opencode/ without a manifest.
  2. It adopts the existing files: sha256-hashes each known OAC file and writes a manifest marking files that match a bundled/known hash as source: generated/bundled and unknown ones as custom (never touched). No overwrite without --yolo.
  3. Because the pre-refactor repo had .opencode/ as source, migration also seeds content/ from the bundled neutral source so future oac build works — offering a diff if the user hand-edited .opencode/ agents.
  4. README swaps curl … | bash for npx @controlstack/oac init; the old one-liner can print a deprecation notice pointing at npm (kept for one release).

5. npm packaging

5.1 What ships in the package

Post-refactor files (root package.json) replaces the .opencode/** enumeration with:

"files": [
  "content/",            // ← THE bundled neutral source of truth (agents, skills, commands, context)
  "registry.json",       // component + profile + dependency catalog
  "packages/cli/dist/",  // Node-runnable compiled CLI
  "packages/core/dist/", // IR schema + parser + registry loader
  "packages/adapters/dist/", // per-tool adapters used by `oac build`
  "bin/",                // oac entrypoint (node, not bun)
  "VERSION", "LICENSE", "README.md", "CHANGELOG.md"
]
// Explicitly NOT shipped: install.sh, update.sh, scripts/bridge/*, .opencode/ (now generated),
// **/node_modules/  (#308)
  • The current files list bundles .opencode/** and install.sh — both removed. /content becomes the shipped asset.
  • bundled.ts getPackageRoot()/listBundledFiles() retarget from .opencode/{agent,context, skills} to content/{agents,skills,commands,context}. The registry.json-absence heuristic used to distinguish CLI-package-root from monorepo-root needs revisiting since registry.json now ships with the package — replace the heuristic with an explicit package.json name check or an OAC_PACKAGE_ROOT-style anchor file.
  • node_modules exclusion (#308) stays enforced both in files globs and in the runtime file walker.

5.2 Bundling pattern (bundled.ts)

Keep the proven pattern: assets travel inside the published tarball, resolved at runtime by walking up from the CLI's own location to the package root, with OAC_PACKAGE_ROOT as the dev/monorepo override. This is why the CLI works offline. The only change is the root directory (content/ not .opencode/) and the root-detection anchor (§5.1).

5.3 Versioning

  • Single version for the whole product = root package.json version, surfaced by readCliVersion() and written into .oac/manifest.json oacVersion on init/update. doctor compares it to the npm-latest.
  • Publish exactly one package, @controlstack/oac; core and adapters are private and bundled into its CLI output. Legacy package names remain installable only for rollback and are deprecated with a pointer to the canonical package. /content, adapters, and CLI ship as one coherent version so a manifest's oacVersion identifies the source that produced the output.
  • Manifest schema version (manifest.version: "1") and registry schema version (registry.schema_version) are independent of the product version and gate migrations.

Stage 1 decisions

08-STRUCTURE-AND-PACKAGING.md closes the former questions: Node-portable CLI; one published @controlstack/oac package; flags-first prompts with TTY guidance; agents-md as the non-interactive fallback target; native per-OS config paths; bundled/offline registry; apply retained for one minor release; 30-day backup retention through oac clean; and editable project content/.