Skip to content

feat(cli): OAC CLI package — npm-based install, update, and management#298

Open
darrenhinde wants to merge 11 commits intomainfrom
feature/oac-package-refactor-v2
Open

feat(cli): OAC CLI package — npm-based install, update, and management#298
darrenhinde wants to merge 11 commits intomainfrom
feature/oac-package-refactor-v2

Conversation

@darrenhinde
Copy link
Copy Markdown
Owner

Summary

  • Migrates OAC from a 52KB bash installer to a proper npm CLI package (@nextsystems/oac)
  • Adds 8 commands: init, update, add, apply, doctor, list, status, clean
  • SHA256-based manifest tracking — never overwrites user-modified files
  • Bun-first runtime with Node.js wrapper entry point
  • 222 tests passing across 13 test files

Current State: IN PROGRESS

What's done

  • All 8 CLI commands implemented and tested
  • All 10 core library modules complete
  • Build pipeline configured (Bun build + npm publish guard)
  • All 14 review fixes from initial code review applied
  • First round of security fixes applied (path traversal guards, env validation, cache validation)
  • Merged with main

Remaining items from second code review (security hardening)

Critical (must fix before merge):

  1. registry.tsfiles[] array needs same path traversal refine as path field
  2. manifest.tsremoveFileFromManifest needs same POSIX normalization as addFileToManifest

High:
3. add.tsremoveCommand needs containment check before rm({ recursive: true })
4. update-check.tslatestVersion field type not validated in cache reader

Medium:
5. manifest.tsincludes('..') check is substring-based, should be segment-level (split('/').some(s => s === '..'))
6. update.ts — add warning about inconsistent state when manifest write fails
7. bundled.ts — off-by-one in depth limit (11 iterations, not 10)

Not started:
8. Version bump 0.7.1 → 1.0.0 (when ready to ship)

Key files

  • packages/cli/ — the full CLI package
  • bin/oac.js — Node.js entry point
  • docs/planning/fix-plans/ — all review findings and fix plans
  • .npmignore — publish config

Test plan

  • bun test — 222 tests passing, 0 failures
  • Fix remaining Critical/High items from security review
  • npm pack --dry-run — verify published tarball contents
  • npm install -g . — verify global install works
  • oac init in a test project
  • Version bump and final review

🤖 Generated with Claude Code

darrenhinde and others added 11 commits February 20, 2026 01:12
- Replace fs-extra with Bun.file/Bun.write/node:fs/promises across all 9 source files
- Fix --help fast-path so all 8 subcommands are visible
- Replace tsup/tsx/vitest with bun build/bun run/bun:test
- Replace __dirname with import.meta.dir; use JSON import assertion for version
- Fix hardcoded OAC_VERSION in add.ts; add hashesMatch import to status.ts
- Replace computeFileHash with Bun.file().bytes() in sha256.ts
- Rename checkNodeVersion → checkBunVersion; parallelise doctor checks
- Fix TypeScript void-inference errors in update.ts, list.ts, status.ts
  by replacing .catch() with let/try-catch where result is used downstream
- Add ManifestError named error class; remove unsafe type casts
- Add 43 bun:test unit tests (sha256, manifest, installer, version) — 0 failures
Critical bug fixes:
- ide-detect.ts: replace Bun.file(dir).exists() with stat().isDirectory()
  for all directory checks — Bun.file().exists() always returns false for
  directories, breaking Cursor/Windsurf/OpenCode/Claude detection entirely
- bundled.ts: add registry.json exclusion anchor to findPackageRoot() to
  prevent monorepo root from matching before the CLI package root

Standards cleanup (§4.1, §5.1, §6.1, §15.3, §21.1):
- status.ts: parallelise findModifiedFiles + detectIdes with Promise.all
- apply.ts: fix duplicate warn/limit messages; remove else after return in
  reportWarnings; remove redundant mkdir before Bun.write
- version.ts: remove unnecessary (pkgJson as {version?:string}) cast
- manifest.ts: remove redundant mkdir before Bun.write; remove as unknown cast
- installer.ts: remove redundant mkdir calls before Bun.write throughout
- add.ts: add comment explaining why node:fs/promises rm is used

Tests (43 → 142, +99 new tests across 4 new files):
- ide-detect.test.ts: 26 tests covering all 4 IDEs, both claude indicators,
  detectIdes parallel, isIdePresent — directly validates the directory fix
- bundled.test.ts: 27 tests for classifyBundledFile, findPackageRoot,
  listBundledFiles, getBundledFilePath, bundledFileExists
- config.test.ts: 25 tests for readConfig/writeConfig round-trips,
  createDefaultConfig, mergeConfig, isYoloMode, isAutoBackup
- installer-update.test.ts: 14 tests covering all 5 updateFiles decision
  branches (install/update/skip/yolo/dry-run) plus isProjectRoot
- sha256.test.ts: +4 tests for empty file, large file, binary content
…solution

- Remove duplicate --dry-run/--yolo/--verbose from parent program; Commander.js
  global option stealing caused all safety flags to be silently dropped in every
  subcommand action callback
- Fix isProjectRoot() to use stat() for .git detection; Bun.file().exists()
  returns false for directories, breaking oac init in standard git repos
- Add OAC_PACKAGE_ROOT env var override to getPackageRoot() for dev/monorepo mode;
  registry.json heuristic excluded the repo root causing oac init/add/update to
  throw when run from source
- Fix build script: remove --banner flag that caused double shebang in dist/index.js
- Rewrite bin/oac.js to invoke bun instead of node (dist is bun-only target)
- Refactor installer.ts let accumulators to const using Promise.all + reduce
- Add MVP planning docs (00-MVP-PLAN.md, master synthesis, project breakdown)
The get_registry_key() function was always adding 's' to the type,
causing 'contexts' to become 'contextss' which doesn't exist in registry.json.

Now handles:
- Singular forms: context → contexts, agent → agents, skill → skills
- Plural forms: contexts → contexts (unchanged), agents → agents
- Config stays singular
- Fallback for any type ending in 's'

Fixes #257
…ot, update check

Batch A (subtasks 01, 04-07, 09-11):
- publishConfig.access: public in root + packages/cli package.json
- engines.bun: >=1.0.0 added to root package.json
- repository.directory set in both package.json files
- version synced to 0.7.1; version.ts reads root package.json
- scripts/sync-version.js: keeps packages/cli version in sync on bump
- warn() output moved to stderr (console.error) in logger.ts
- bin/oac.js: injects OAC_PACKAGE_ROOT, Windows bun.cmd support
- bundled.ts: removed !hasRegistryJson guard from findPackageRoot()
- manifest.ts: mkdir guard before writeManifest()
- index.ts: SIGINT/SIGTERM signal handlers

Batch B (subtasks 02, 03, 08, 12):
- .npmignore: anchored /dist/, removed packages/ blanket exclusion
- prepublishOnly: typecheck → build → dist existence check
- packages/cli package.json: removed bin field, added private: true
- update-check.ts: new module with fetchLatestNpmVersion, shouldShowUpdateNotice,
  checkForUpdate (24h cache, 3s timeout, stderr output, never throws)
- doctor.ts: refactored to import fetchLatestNpmVersion from update-check.ts
- index.ts: checkForUpdate() called after parseAsync; help() before update check

Test gates: 202 pass, 14 fail (14 remaining are clean command gates for Batch C)
…tall

- clean.ts: new oac clean command with --force, --dry-run, --keep-opencode, --ide
  Removes .oac/ and .opencode/ (or just .oac/ with --keep-opencode).
  --ide also removes CLAUDE.md. --dry-run previews without removing.
  Registered in index.ts alongside all other commands.

- Help examples: addHelpText('after', ...) on main program, init, and update.
  Main program shows 7 examples including correct 'oac add agent:openagent' syntax.
  init examples cover --dry-run, --verbose, --yolo with post-init doctor tip.
  update examples cover --dry-run, --check, --yolo, --verbose with backup note.

- README: npm install section added before curl section in Quick Start.
  Includes Bun prerequisite warning, global install, npx one-liner, update command.
  Curl section heading renamed to 'Step 2: Install via curl' (no duplicate Step 1).

Tests: 216 pass, 0 fail
…t, exit codes

B-2: oac clean --ide now removes all three IDE output files
  (.cursorrules and .windsurfrules added alongside CLAUDE.md)
  2 new tests added to clean.test.ts

B-3: oac update now writes manifest for successful files even on partial failure
  Removed the errors.length === 0 gate on writeManifest
  Warning message updated: 'manifest updated for successful files. Re-run to retry failures.'
  3 new tests added in update.test.ts

B-4: scripts/ dev tooling removed from npm package (46 files → 1 file)
  package.json files: 'scripts/' → 'scripts/sync-version.js'
  .npmignore: removed !scripts/ negation
  scripts/README.md excluded via files field negation

W-3: oac clean sets process.exitCode = 1 when any removal fails
  hadError flag tracks failures; process.exitCode = 1 set after loop
  1 new test added (chmod-based failure simulation)

O-1: removed dead --verbose option from oac clean
  Option was registered but silently ignored; removed entirely

Tests: 222 pass, 0 fail (was 216)
Remove duplicate "Step 2: Install via curl" heading and renumber
"Step 2: Start Building" since old Step 1 no longer exists.
Add review report and fix plan documents.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add path traversal guard in registry schema (reject absolute paths and ..)
- Add containment check in add.ts before writing files outside project root
- Validate OAC_PACKAGE_ROOT is absolute path before trusting it
- Add depth limit (10) to findPackageRoot directory walk
- Add schema validation to update-check cache reader
- Wrap writeManifest in try/catch in update.ts
- Normalize manifest keys to POSIX paths with traversal rejection
- Fix empty HOME display bug in status.ts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant