|
|
@@ -0,0 +1,216 @@
|
|
|
+ISSUE: No oac clean command to remove installed files
|
|
|
+SEVERITY: Important
|
|
|
+FILE(S): packages/cli/src/commands/clean.ts (new file), packages/cli/src/index.ts
|
|
|
+
|
|
|
+CURRENT STATE:
|
|
|
+packages/cli/src/index.ts imports and registers these commands (lines 25-41):
|
|
|
+ import('./commands/init.js')
|
|
|
+ import('./commands/update.js')
|
|
|
+ import('./commands/add.js')
|
|
|
+ import('./commands/apply.js')
|
|
|
+ import('./commands/doctor.js')
|
|
|
+ import('./commands/list.js')
|
|
|
+ import('./commands/status.js')
|
|
|
+
|
|
|
+There is no `clean` command. When a user uninstalls the npm package
|
|
|
+(`npm uninstall -g @nextsystems/oac`), the `.opencode/` and `.oac/` directories
|
|
|
+remain in every project where they ran `oac init`. There is no supported way to
|
|
|
+remove them.
|
|
|
+
|
|
|
+ROOT CAUSE:
|
|
|
+The clean/uninstall lifecycle was not implemented. This is a common oversight
|
|
|
+in CLI tools that install files into user projects.
|
|
|
+
|
|
|
+FIX:
|
|
|
+
|
|
|
+--- New file: packages/cli/src/commands/clean.ts ---
|
|
|
+
|
|
|
+Follow the exact same Commander registration pattern as init.ts:
|
|
|
+ - Export a `CleanOptions` type
|
|
|
+ - Export a `cleanCommand(options)` async function
|
|
|
+ - Export a `registerCleanCommand(program)` function
|
|
|
+
|
|
|
+Full implementation plan:
|
|
|
+
|
|
|
+```typescript
|
|
|
+import { type Command } from 'commander'
|
|
|
+import { existsSync } from 'node:fs'
|
|
|
+import { rm } from 'node:fs/promises'
|
|
|
+import { join } from 'node:path'
|
|
|
+import { readManifest } from '../lib/manifest.js'
|
|
|
+import { log, info, warn, error, success } from '../ui/logger.js'
|
|
|
+
|
|
|
+export type CleanOptions = {
|
|
|
+ force: boolean
|
|
|
+ dryRun: boolean
|
|
|
+ /** Remove IDE-specific files generated by oac apply (e.g. CLAUDE.md) */
|
|
|
+ ide: boolean
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Implements `oac clean`:
|
|
|
+ * 1. Reads the manifest to know which files OAC installed
|
|
|
+ * 2. Prompts for confirmation (unless --force)
|
|
|
+ * 3. Removes each file listed in the manifest
|
|
|
+ * 4. Removes .oac/ directory (manifest + config)
|
|
|
+ * 5. Removes .opencode/ directory
|
|
|
+ * 6. Optionally removes IDE output files (--ide flag)
|
|
|
+ */
|
|
|
+export async function cleanCommand(options: CleanOptions): Promise<void> {
|
|
|
+ const projectRoot = process.cwd()
|
|
|
+
|
|
|
+ // Read manifest to know what OAC installed
|
|
|
+ const manifest = await readManifest(projectRoot).catch(() => null)
|
|
|
+ const trackedFiles = manifest ? Object.keys(manifest.files) : []
|
|
|
+
|
|
|
+ // Directories always removed
|
|
|
+ const dirsToRemove = [
|
|
|
+ join(projectRoot, '.opencode'),
|
|
|
+ join(projectRoot, '.oac'),
|
|
|
+ ]
|
|
|
+
|
|
|
+ // IDE files (only with --ide flag)
|
|
|
+ const ideFiles = options.ide
|
|
|
+ ? ['CLAUDE.md', 'AGENTS.md', 'GEMINI.md', 'CURSOR.md', '.cursorrules']
|
|
|
+ .map((f) => join(projectRoot, f))
|
|
|
+ .filter((f) => existsSync(f))
|
|
|
+ : []
|
|
|
+
|
|
|
+ // Print plan
|
|
|
+ log('')
|
|
|
+ info(options.dryRun ? '[dry-run] oac clean — no files will be removed' : 'oac clean')
|
|
|
+ log('')
|
|
|
+ info(`Will remove: .opencode/, .oac/ (${trackedFiles.length} tracked files)`)
|
|
|
+ if (ideFiles.length > 0) {
|
|
|
+ info(`IDE files: ${ideFiles.map((f) => f.replace(projectRoot + '/', '')).join(', ')}`)
|
|
|
+ }
|
|
|
+ log('')
|
|
|
+
|
|
|
+ // Confirmation prompt (unless --force or --dry-run)
|
|
|
+ if (!options.force && !options.dryRun) {
|
|
|
+ // Use Bun's built-in prompt (synchronous)
|
|
|
+ const answer = prompt('Remove all OAC files from this project? [y/N] ')
|
|
|
+ if (answer?.toLowerCase() !== 'y') {
|
|
|
+ info('Aborted.')
|
|
|
+ process.exit(0)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (options.dryRun) {
|
|
|
+ info('[dry-run] Would remove:')
|
|
|
+ for (const dir of dirsToRemove) {
|
|
|
+ if (existsSync(dir)) info(` ${dir.replace(projectRoot + '/', '')}`)
|
|
|
+ }
|
|
|
+ for (const f of ideFiles) {
|
|
|
+ info(` ${f.replace(projectRoot + '/', '')}`)
|
|
|
+ }
|
|
|
+ info('No changes made. Remove --dry-run to apply.')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // Remove directories
|
|
|
+ let removed = 0
|
|
|
+ for (const dir of dirsToRemove) {
|
|
|
+ if (existsSync(dir)) {
|
|
|
+ await rm(dir, { recursive: true, force: true })
|
|
|
+ removed++
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Remove IDE files
|
|
|
+ for (const f of ideFiles) {
|
|
|
+ await rm(f, { force: true })
|
|
|
+ removed++
|
|
|
+ }
|
|
|
+
|
|
|
+ success(`Done! Removed ${removed} item${removed !== 1 ? 's' : ''}.`)
|
|
|
+ log('')
|
|
|
+}
|
|
|
+
|
|
|
+export function registerCleanCommand(program: Command): void {
|
|
|
+ program
|
|
|
+ .command('clean')
|
|
|
+ .description('Remove all OAC-installed files from the current project')
|
|
|
+ .option('--force', 'Skip confirmation prompt', false)
|
|
|
+ .option('--dry-run', 'Show what would be removed without making changes', false)
|
|
|
+ .option('--ide', 'Also remove IDE output files (CLAUDE.md, AGENTS.md, etc.)', false)
|
|
|
+ .action(async (opts: { force: boolean; dryRun: boolean; ide: boolean }) => {
|
|
|
+ await cleanCommand({ force: opts.force, dryRun: opts.dryRun, ide: opts.ide })
|
|
|
+ })
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+--- Update packages/cli/src/index.ts ---
|
|
|
+
|
|
|
+Add `registerCleanCommand` to the parallel import block and call it.
|
|
|
+
|
|
|
+BEFORE (index.ts lines 25-41):
|
|
|
+ const [
|
|
|
+ { registerInitCommand },
|
|
|
+ { registerUpdateCommand },
|
|
|
+ { registerAddCommand },
|
|
|
+ { registerApplyCommand },
|
|
|
+ { registerDoctorCommand },
|
|
|
+ { registerListCommand },
|
|
|
+ { registerStatusCommand },
|
|
|
+ ] = await Promise.all([
|
|
|
+ import('./commands/init.js'),
|
|
|
+ import('./commands/update.js'),
|
|
|
+ import('./commands/add.js'),
|
|
|
+ import('./commands/apply.js'),
|
|
|
+ import('./commands/doctor.js'),
|
|
|
+ import('./commands/list.js'),
|
|
|
+ import('./commands/status.js'),
|
|
|
+ ])
|
|
|
+
|
|
|
+ registerInitCommand(program)
|
|
|
+ registerUpdateCommand(program)
|
|
|
+ registerAddCommand(program)
|
|
|
+ registerApplyCommand(program)
|
|
|
+ registerDoctorCommand(program)
|
|
|
+ registerListCommand(program)
|
|
|
+ registerStatusCommand(program)
|
|
|
+
|
|
|
+AFTER:
|
|
|
+ const [
|
|
|
+ { registerInitCommand },
|
|
|
+ { registerUpdateCommand },
|
|
|
+ { registerAddCommand },
|
|
|
+ { registerApplyCommand },
|
|
|
+ { registerDoctorCommand },
|
|
|
+ { registerListCommand },
|
|
|
+ { registerStatusCommand },
|
|
|
+ { registerCleanCommand },
|
|
|
+ ] = await Promise.all([
|
|
|
+ import('./commands/init.js'),
|
|
|
+ import('./commands/update.js'),
|
|
|
+ import('./commands/add.js'),
|
|
|
+ import('./commands/apply.js'),
|
|
|
+ import('./commands/doctor.js'),
|
|
|
+ import('./commands/list.js'),
|
|
|
+ import('./commands/status.js'),
|
|
|
+ import('./commands/clean.js'),
|
|
|
+ ])
|
|
|
+
|
|
|
+ registerInitCommand(program)
|
|
|
+ registerUpdateCommand(program)
|
|
|
+ registerAddCommand(program)
|
|
|
+ registerApplyCommand(program)
|
|
|
+ registerDoctorCommand(program)
|
|
|
+ registerListCommand(program)
|
|
|
+ registerStatusCommand(program)
|
|
|
+ registerCleanCommand(program)
|
|
|
+
|
|
|
+VALIDATION:
|
|
|
+1. Run: oac clean --dry-run (in a project with oac init already run)
|
|
|
+ Should list .opencode/ and .oac/ without removing anything
|
|
|
+2. Run: oac clean --force (in a test project)
|
|
|
+ Should remove .opencode/ and .oac/ without prompting
|
|
|
+3. Run: oac clean --ide --force
|
|
|
+ Should also remove CLAUDE.md / AGENTS.md if present
|
|
|
+4. Run: oac --help
|
|
|
+ Should show 'clean' in the command list
|
|
|
+5. Run: oac clean --help
|
|
|
+ Should show --force, --dry-run, --ide options
|
|
|
+
|
|
|
+DEPENDENCIES: none
|