Owner: Agent D Status: Specification (no implementation) Scope: The
oacnpm CLI, the neutral-source → per-tool build pipeline, fullinstall.sh/update.shparity, 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 transformsoac buildinvokes).
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.
| 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 |
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.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).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.Global conventions (all commands):
--dry-run (plan only, no writes), --verbose, --yolo (backup + overwrite
user-modified files), --json (machine output where meaningful), -v/--version, -h/--help.process.cwd(), validated by isProjectRoot() (has package.json or .git)..oac/manifest.json (sha256 ledger), .oac/config.json (prefs),
.oac/backups/{ISO-timestamp}/… (yolo/backup copies).0 success (warnings allowed), 1 hard error. --dry-run never writes.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.
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.
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). |
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).
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 alias → oac build --target <ide> for
Cursor/Claude/Windsurf, so existing muscle memory and docs keep working during migration
(migration detail owned by Agent E).
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)./content? (reuses oac build --check). Reports "run oac build".dependencies[] are also present.--json (CI) and exit 1 on errors.Replaces: install.sh check_dependencies / check_bash_version (now a runtime check).
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.
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.
| 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) |
— |
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 ────┘
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.
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.
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).
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.
Context files referenced by an agent's context[] are copied/wired per target:
.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.context/, wired via a session-start hook; contributes
entries to .claude-plugin/plugin.json.apply.ts already
warns at 80KB / errors at 100KB for Cursor — that guard carries into build).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.
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).
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
"golden snapshot + parse/manifest check on ONE worked agent" that the locked minimal-test decision
(#3) requires, run live.
oac build && oac build is a no-op because both runs generate and compare the same temp tree.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).
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).
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).
.oac.json)Use one discovery chain:
<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..oac.json.<project>/content/ only when its registry/root anchor validates it..oac.json atomically with
{ "version": 1, "context": { "root": "content" } } so the next run takes the fast path.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.
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 gap — add.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.
update.sh parityupdate.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.
Core goal (from the task): better cross-platform install via npm, one system exporting to different AI tools.
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):
lib/fs.ts abstraction over
node:fs/promises (readFile/writeFile/mkdir -p), node:crypto (already used in
sha256.ts), and import.meta.url→fileURLToPath 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.--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.
| 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/ |
--global/--dir are explicit
opt-ins (mirrors install.sh's location menu but flag-driven).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).~/.config/opencode on all platforms (from get_global_install_path).
Windows should additionally honor %APPDATA%/%LOCALAPPDATA% — flagged in Open Questions.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.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.ui/logger.ts should degrade on terminals without color
(install.sh gated on WT_SESSION/ConEmuPID; chalk auto-detects TTY/NO_COLOR).tr -d '\r' hack in jq_exec).| 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).
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.| 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 |
curl | bash usersExisting users have a project-local .opencode/ installed by install.sh but no .oac/
manifest. Migration path (details owned by Agent E; CLI responsibilities here):
oac init (or a dedicated oac migrate) detects an OAC-shaped .opencode/ without a manifest.source: generated/bundled and unknown ones as
custom (never touched). No overwrite without --yolo..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.curl … | bash for npx @controlstack/oac init; the old one-liner can print a
deprecation notice pointing at npm (kept for one release).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)
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.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).
package.json version,
surfaced by readCliVersion() and written into .oac/manifest.json oacVersion on init/update.
doctor compares it to the npm-latest.@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.version: "1") and registry schema version
(registry.schema_version) are independent of the product version and gate migrations.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/.