瀏覽代碼

feat(cli): Batch C — oac clean command, help examples, README npm install

- 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
darrenhinde 4 月之前
父節點
當前提交
16b50bec9b
共有 5 個文件被更改,包括 199 次插入1 次删除
  1. 35 1
      README.md
  2. 129 0
      packages/cli/src/commands/clean.ts
  3. 9 0
      packages/cli/src/commands/init.ts
  4. 11 0
      packages/cli/src/commands/update.ts
  5. 15 0
      packages/cli/src/index.ts

+ 35 - 1
README.md

@@ -115,9 +115,43 @@ Use any AI model (Claude, GPT, Gemini, local). No vendor lock-in.
 
 ## 🚀 Quick Start
 
+### Install via npm (recommended)
+
+**Prerequisites:** [Bun](https://bun.sh) ≥ 1.0 • Node.js ≥ 18
+
+> ⚠️ **Bun is required.** The OAC CLI runs on the [Bun](https://bun.sh) runtime.
+> Install Bun first: `curl -fsSL https://bun.sh/install | bash`
+
+**Global install (use `oac` anywhere):**
+```bash
+npm install -g @nextsystems/oac
+```
+
+**Then set up a project:**
+```bash
+cd your-project
+oac init
+```
+
+**No-install (try without committing):**
+```bash
+npx @nextsystems/oac init
+```
+
+**Keep updated:**
+```bash
+npm update -g @nextsystems/oac
+# or check your current version:
+oac doctor
+```
+
+---
+
+### Install via curl (alternative)
+
 **Prerequisites:** [OpenCode CLI](https://opencode.ai/docs) (free, open-source) • Bash 3.2+ • Git
 
-### Step 1: Install
+### Step 2: Install via curl
 
 **One command:**
 

+ 129 - 0
packages/cli/src/commands/clean.ts

@@ -0,0 +1,129 @@
+import { rm, access } from 'node:fs/promises'
+import { join } from 'node:path'
+import { type Command } from 'commander'
+import { log, warn, success, dim } from '../ui/logger.js'
+
+// ── Types ─────────────────────────────────────────────────────────────────────
+
+export type CleanOptions = {
+  force: boolean
+  keepOpencode: boolean
+  dryRun: boolean
+  ide: boolean
+}
+
+// ── Helpers ───────────────────────────────────────────────────────────────────
+
+/** Returns true if the path exists (file or directory). */
+async function pathExists(p: string): Promise<boolean> {
+  try {
+    await access(p)
+    return true
+  } catch {
+    return false
+  }
+}
+
+// ── Core command ──────────────────────────────────────────────────────────────
+
+/**
+ * Implements `oac clean`:
+ *  Removes .oac/ and (by default) .opencode/ from the current project directory.
+ *  --keep-opencode: removes only .oac/, preserves .opencode/
+ *  --dry-run: prints what would be removed without removing anything
+ *  --force: skips the destructive-action warning prompt
+ *  --ide: also removes IDE-specific files (e.g. CLAUDE.md)
+ *
+ * Never throws — all errors are caught and reported via warn().
+ */
+export async function cleanCommand(options: CleanOptions): Promise<void> {
+  const projectRoot = process.cwd()
+
+  // Build the list of targets to remove
+  const targets: Array<{ path: string; label: string }> = []
+
+  const oacDir = join(projectRoot, '.oac')
+  if (await pathExists(oacDir)) {
+    targets.push({ path: oacDir, label: '.oac/' })
+  }
+
+  if (!options.keepOpencode) {
+    const opencodeDir = join(projectRoot, '.opencode')
+    if (await pathExists(opencodeDir)) {
+      targets.push({ path: opencodeDir, label: '.opencode/' })
+    }
+  }
+
+  if (options.ide) {
+    const claudeMd = join(projectRoot, 'CLAUDE.md')
+    if (await pathExists(claudeMd)) {
+      targets.push({ path: claudeMd, label: 'CLAUDE.md' })
+    }
+  }
+
+  // Nothing to do
+  if (targets.length === 0) {
+    log('Nothing to clean — no OAC directories found.')
+    return
+  }
+
+  // Dry-run: list what would be removed and exit
+  if (options.dryRun) {
+    log('')
+    log('Dry run — the following would be removed:')
+    for (const t of targets) {
+      dim(`  ${t.label}`)
+    }
+    log('')
+    log('Run without --dry-run to remove them.')
+    return
+  }
+
+  // Print destructive warning listing targets
+  if (!options.force) {
+    warn('')
+    warn('This will permanently remove:')
+    for (const t of targets) {
+      warn(`  ${t.label}`)
+    }
+    warn('')
+    warn('Use --force to confirm, or --dry-run to preview.')
+    return
+  }
+
+  // Force mode — remove all targets
+  for (const t of targets) {
+    try {
+      await rm(t.path, { recursive: true, force: true })
+      success(`Removed ${t.label}`)
+    } catch (err) {
+      const msg = err instanceof Error ? err.message : String(err)
+      warn(`Failed to remove ${t.label}: ${msg}`)
+    }
+  }
+}
+
+// ── Commander registration ────────────────────────────────────────────────────
+
+/**
+ * Registers the `clean` subcommand on the given Commander program.
+ * Called by the CLI entry point (index.ts).
+ */
+export function registerCleanCommand(program: Command): void {
+  program
+    .command('clean')
+    .description('Remove OAC-managed directories (.oac/ and .opencode/) from the project')
+    .option('--force', 'Skip confirmation and remove immediately', false)
+    .option('--dry-run', 'Preview what would be removed without removing anything', false)
+    .option('--keep-opencode', 'Remove only .oac/ — preserve .opencode/', false)
+    .option('--ide', 'Also remove IDE-specific files (e.g. CLAUDE.md)', false)
+    .option('--verbose', 'Show additional output', false)
+    .action(async (opts: { force: boolean; dryRun: boolean; keepOpencode: boolean; ide: boolean }) => {
+      await cleanCommand({
+        force: opts.force,
+        dryRun: opts.dryRun,
+        keepOpencode: opts.keepOpencode,
+        ide: opts.ide,
+      })
+    })
+}

+ 9 - 0
packages/cli/src/commands/init.ts

@@ -253,6 +253,15 @@ export function registerInitCommand(program: Command): void {
     .option('--yolo', 'Skip conflict checks and overwrite user-modified files', false)
     .option('--dry-run', 'Print what would happen without making any changes', false)
     .option('--verbose', 'Show each file being copied', false)
+    .addHelpText('after', `
+Examples:
+  $ oac init                    Install all OAC agents and context files
+  $ oac init --dry-run          Preview what would be installed
+  $ oac init --verbose          Show each file as it is copied
+  $ oac init --yolo             Overwrite files you have modified (backs them up first)
+
+After init, run 'oac doctor' to verify your setup.
+`)
     .action(async (opts: { yolo: boolean; dryRun: boolean; verbose: boolean }) => {
       await initCommand({
         yolo: opts.yolo,

+ 11 - 0
packages/cli/src/commands/update.ts

@@ -186,6 +186,17 @@ export function registerUpdateCommand(program: Command): void {
     .option('--check', 'Alias for --dry-run: show what would change')
     .option('--yolo', 'Back up user-modified files and overwrite them anyway')
     .option('--verbose', 'Show SHA256 comparison details per file')
+    .addHelpText('after', `
+Examples:
+  $ oac update                  Update all OAC files (skips files you modified)
+  $ oac update --dry-run        Preview what would change without modifying anything
+  $ oac update --check          Same as --dry-run (alias)
+  $ oac update --yolo           Back up modified files and overwrite them
+  $ oac update --verbose        Show SHA256 hash comparison for each file
+
+Files you have modified since 'oac init' are skipped by default.
+Use --yolo to overwrite them (your changes are backed up to .oac/backups/).
+`)
     .action(async (cmdOpts: { dryRun?: boolean; check?: boolean; yolo?: boolean; verbose?: boolean }) => {
       await handleUpdate({
         dryRun: cmdOpts.dryRun ?? false,

+ 15 - 0
packages/cli/src/index.ts

@@ -10,6 +10,18 @@ program
   .name('oac')
   .description('OpenAgents Control — install, manage, and update AI agents and context files')
   .version(readCliVersion(), '-v, --version', 'Print version and exit')
+  .addHelpText('after', `
+Examples:
+  $ oac init                    Set up OAC in the current project
+  $ oac update                  Update OAC files (skips files you modified)
+  $ oac update --dry-run        Preview what would be updated
+  $ oac doctor                  Check your setup and report issues
+  $ oac add agent:openagent     Add a specific agent from the registry
+  $ oac apply cursor            Generate Cursor IDE rules file
+  $ oac clean --dry-run         Preview what oac clean would remove
+
+Docs: https://github.com/darrenhinde/OpenAgentsControl#readme
+`)
 
 // Restore terminal state on Ctrl-C or kill signal
 // Exit codes follow Unix convention: 128 + signal number
@@ -36,6 +48,7 @@ async function main(): Promise<void> {
     { registerDoctorCommand },
     { registerListCommand },
     { registerStatusCommand },
+    { registerCleanCommand },
   ] = await Promise.all([
     import('./commands/init.js'),
     import('./commands/update.js'),
@@ -44,6 +57,7 @@ async function main(): Promise<void> {
     import('./commands/doctor.js'),
     import('./commands/list.js'),
     import('./commands/status.js'),
+    import('./commands/clean.js'),
   ])
 
   registerInitCommand(program)
@@ -53,6 +67,7 @@ async function main(): Promise<void> {
   registerDoctorCommand(program)
   registerListCommand(program)
   registerStatusCommand(program)
+  registerCleanCommand(program)
 
   // Unknown commands: print a helpful error and exit 1
   program.on('command:*', (operands: string[]) => {