opencode-onboard 0.4.2 → 0.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +304 -301
  3. package/content/.agents/agents/basic-engineer.md +4 -2
  4. package/content/.agents/agents/devops-manager.md +123 -123
  5. package/content/.agents/skills/ob-default/SKILL.md +25 -21
  6. package/content/.agents/skills/ob-generic-guardrails/SKILL.md +36 -32
  7. package/content/.agents/skills/ob-global/SKILL.md +92 -49
  8. package/content/.agents/skills/ob-pullrequest-az/SKILL.md +168 -160
  9. package/content/.agents/skills/ob-pullrequest-gh/SKILL.md +140 -136
  10. package/content/.opencode/commands/create-engineer.md +109 -0
  11. package/content/.opencode/commands/init.md +1 -1
  12. package/content/.opencode/commands/main.md +1 -1
  13. package/content/.opencode/commands/opsx-apply.md +131 -70
  14. package/content/.opencode/commands/plan.md +1 -1
  15. package/content/.opencode/plugins/session-log.js +523 -519
  16. package/content/.opencode/skills/openspec-apply-change/SKILL.md +86 -64
  17. package/content/AGENTS.md +67 -39
  18. package/package.json +1 -1
  19. package/src/commands/join.js +3 -3
  20. package/src/commands/single.js +2 -0
  21. package/src/commands/wizard.js +124 -99
  22. package/src/presets/browser.json +22 -18
  23. package/src/presets/optimization.json +27 -22
  24. package/src/presets/source.json +7 -1
  25. package/src/steps/browser/browser.test.js +115 -81
  26. package/src/steps/browser/index.js +62 -54
  27. package/src/steps/clean/index.js +108 -107
  28. package/src/steps/copy/agents.js +28 -0
  29. package/src/steps/copy/copy.test.js +1 -0
  30. package/src/steps/copy/index.js +2 -1
  31. package/src/steps/metadata/index.js +63 -61
  32. package/src/steps/models/format.js +61 -60
  33. package/src/steps/models/write.test.js +117 -117
  34. package/src/steps/openspec/ensemble.js +30 -7
  35. package/src/steps/openspec/ensemble.test.js +79 -79
  36. package/src/steps/openspec/index.js +121 -32
  37. package/src/steps/openspec/index.test.js +63 -0
  38. package/src/steps/optimization/caveman.js +34 -29
  39. package/src/steps/optimization/codegraph.js +52 -0
  40. package/src/steps/optimization/global.js +88 -64
  41. package/src/steps/optimization/global.test.js +99 -0
  42. package/src/steps/optimization/index.js +109 -101
  43. package/src/steps/optimization/optimization.test.js +101 -93
  44. package/src/steps/optimization/quota.js +84 -84
  45. package/src/steps/source/index.js +48 -0
  46. package/src/steps/source/source.test.js +124 -91
  47. package/src/utils/__tests__/copy.test.js +117 -117
  48. package/src/utils/exec-spinner.js +47 -47
  49. package/src/utils/exec.js +134 -131
  50. package/src/utils/terminal.js +6 -0
@@ -1,18 +1,22 @@
1
- {
2
- "installer": {
3
- "command": "npx",
4
- "args": ["@different-ai/opencode-browser", "install"]
5
- },
6
- "output": {
7
- "showAfter": "To load the extension",
8
- "hideAfter": "Press Enter when"
9
- },
10
- "autoAnswers": [
11
- { "trigger": "Press Enter when", "response": "" },
12
- { "trigger": "Choose config location", "response": "2" },
13
- { "trigger": "Add plugin automatically?", "response": "y" },
14
- { "trigger": "Create one?", "response": "y" },
15
- { "trigger": "Add browser-automation skill", "response": "n" },
16
- { "trigger": "Check broker", "response": "n" }
17
- ]
18
- }
1
+ {
2
+ "installer": {
3
+ "command": "npx",
4
+ "args": ["@different-ai/opencode-browser", "install"]
5
+ },
6
+ "output": {
7
+ "showAfter": "To load the extension",
8
+ "hideAfter": "Press Enter when"
9
+ },
10
+ "locationChoices": {
11
+ "local": "2",
12
+ "global": "1"
13
+ },
14
+ "autoAnswers": [
15
+ { "trigger": "Press Enter when", "response": "" },
16
+ { "trigger": "Choose config location", "response": "__LOCATION__" },
17
+ { "trigger": "Add plugin automatically?", "response": "y" },
18
+ { "trigger": "Create one?", "response": "y" },
19
+ { "trigger": "Add browser-automation skill", "response": "n" },
20
+ { "trigger": "Check broker", "response": "n" }
21
+ ]
22
+ }
@@ -1,22 +1,27 @@
1
- {
2
- "message": "Enable tools:",
3
- "info": "Choose which optimization tools to enable (recommended: all).",
4
- "timeoutMs": 30000,
5
- "choices": [
6
- {
7
- "name": "RTK check (recommended)",
8
- "value": "rtk",
9
- "checked": true
10
- },
11
- {
12
- "name": "opencode-quota plugin (recommended)",
13
- "value": "quota",
14
- "checked": true
15
- },
16
- {
17
- "name": "caveman concise mode (recommended)",
18
- "value": "caveman",
19
- "checked": true
20
- }
21
- ]
22
- }
1
+ {
2
+ "message": "Enable tools:",
3
+ "info": "Choose which optimization tools to enable (recommended: all).",
4
+ "timeoutMs": 30000,
5
+ "choices": [
6
+ {
7
+ "name": "RTK check (recommended)",
8
+ "value": "rtk",
9
+ "checked": true
10
+ },
11
+ {
12
+ "name": "opencode-quota plugin (recommended)",
13
+ "value": "quota",
14
+ "checked": true
15
+ },
16
+ {
17
+ "name": "caveman concise mode (recommended)",
18
+ "value": "caveman",
19
+ "checked": true
20
+ },
21
+ {
22
+ "name": "codegraph semantic index (recommended)",
23
+ "value": "codegraph",
24
+ "checked": true
25
+ }
26
+ ]
27
+ }
@@ -11,7 +11,13 @@
11
11
  "name": "Select folders in parent (../)",
12
12
  "value": "parent",
13
13
  "description": "Use when this repo only contains agent config"
14
+ },
15
+ {
16
+ "name": "Select child folders (./*/)",
17
+ "value": "children",
18
+ "description": "Use when source code lives in subdirectories of this repo"
14
19
  }
15
20
  ],
16
- "parentSelectionMessage": "Select source folders from parent directory:"
21
+ "parentSelectionMessage": "Select source folders from parent directory:",
22
+ "childrenSelectionMessage": "Select child folders to include as source:"
17
23
  }
@@ -1,81 +1,115 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest'
2
-
3
- vi.mock('../../utils/exec.js', () => ({
4
- header: vi.fn(),
5
- info: vi.fn(),
6
- success: vi.fn(),
7
- warn: vi.fn(),
8
- error: vi.fn(),
9
- }))
10
-
11
- vi.mock('fs-extra', () => ({
12
- default: {
13
- readJson: vi.fn().mockResolvedValue({
14
- installer: { command: 'npx', args: ['@different-ai/opencode-browser', 'install'] },
15
- output: { showAfter: '===', hideAfter: '===' },
16
- autoAnswers: [
17
- { trigger: 'Install', response: 'y' },
18
- ],
19
- }),
20
- },
21
- }))
22
-
23
- vi.mock('execa', () => ({
24
- execa: vi.fn(),
25
- }))
26
-
27
- import fse from 'fs-extra'
28
- import { installBrowser } from './index.js'
29
-
30
- describe('installBrowser()', () => {
31
- beforeEach(() => {
32
- vi.clearAllMocks()
33
- })
34
-
35
- it('calls installer command from preset', async () => {
36
- const { execa } = await import('execa')
37
- const mockChild = {
38
- stdout: { on: vi.fn() },
39
- stderr: { on: vi.fn() },
40
- stdin: { write: vi.fn() },
41
- then: (cb) => cb({ exitCode: 0 }),
42
- }
43
- execa.mockReturnValue(mockChild)
44
-
45
- await installBrowser()
46
-
47
- expect(execa).toHaveBeenCalledWith('npx', expect.arrayContaining(['@different-ai/opencode-browser']), expect.any(Object))
48
- })
49
-
50
- it('logs success when exit code is 0', async () => {
51
- const { execa } = await import('execa')
52
- const mockChild = {
53
- stdout: { on: vi.fn() },
54
- stderr: { on: vi.fn() },
55
- stdin: { write: vi.fn() },
56
- then: (cb) => cb({ exitCode: 0 }),
57
- }
58
- execa.mockReturnValue(mockChild)
59
- const { success } = await import('../../utils/exec.js')
60
-
61
- await installBrowser()
62
-
63
- expect(success).toHaveBeenCalledWith('opencode-browser installed')
64
- })
65
-
66
- it('logs warning when exit code is non-zero', async () => {
67
- const { execa } = await import('execa')
68
- const mockChild = {
69
- stdout: { on: vi.fn() },
70
- stderr: { on: vi.fn() },
71
- stdin: { write: vi.fn() },
72
- then: (cb) => cb({ exitCode: 1 }),
73
- }
74
- execa.mockReturnValue(mockChild)
75
- const { warn } = await import('../../utils/exec.js')
76
-
77
- await installBrowser()
78
-
79
- expect(warn).toHaveBeenCalledWith('opencode-browser install exited with non-zero code')
80
- })
81
- })
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest'
2
+ import { installBrowser } from './index.js'
3
+
4
+ vi.mock('../../utils/exec.js', () => ({
5
+ header: vi.fn(),
6
+ info: vi.fn(),
7
+ success: vi.fn(),
8
+ warn: vi.fn(),
9
+ error: vi.fn(),
10
+ }))
11
+
12
+ vi.mock('fs-extra', () => ({
13
+ default: {
14
+ readJson: vi.fn().mockResolvedValue({
15
+ installer: { command: 'npx', args: ['@different-ai/opencode-browser', 'install'] },
16
+ output: { showAfter: '===', hideAfter: '===' },
17
+ locationChoices: { local: '2', global: '1' },
18
+ autoAnswers: [
19
+ { trigger: 'Install', response: 'y' },
20
+ { trigger: 'Choose config location', response: '__LOCATION__' },
21
+ ],
22
+ }),
23
+ },
24
+ }))
25
+
26
+ vi.mock('execa', () => ({
27
+ execa: vi.fn(),
28
+ }))
29
+
30
+ describe('installBrowser()', () => {
31
+ beforeEach(() => {
32
+ vi.clearAllMocks()
33
+ })
34
+
35
+ it('calls installer command from preset', async () => {
36
+ const { execa } = await import('execa')
37
+ const mockChild = {
38
+ stdout: { on: vi.fn() },
39
+ stderr: { on: vi.fn() },
40
+ stdin: { write: vi.fn() },
41
+ then: (cb) => cb({ exitCode: 0 }),
42
+ }
43
+ execa.mockReturnValue(mockChild)
44
+
45
+ await installBrowser()
46
+
47
+ expect(execa).toHaveBeenCalledWith('npx', expect.arrayContaining(['@different-ai/opencode-browser']), expect.any(Object))
48
+ })
49
+
50
+ it('logs success when exit code is 0', async () => {
51
+ const { execa } = await import('execa')
52
+ const mockChild = {
53
+ stdout: { on: vi.fn() },
54
+ stderr: { on: vi.fn() },
55
+ stdin: { write: vi.fn() },
56
+ then: (cb) => cb({ exitCode: 0 }),
57
+ }
58
+ execa.mockReturnValue(mockChild)
59
+ const { success } = await import('../../utils/exec.js')
60
+
61
+ await installBrowser()
62
+
63
+ expect(success).toHaveBeenCalledWith('opencode-browser installed')
64
+ })
65
+
66
+ it('logs warning when exit code is non-zero', async () => {
67
+ const { execa } = await import('execa')
68
+ const mockChild = {
69
+ stdout: { on: vi.fn() },
70
+ stderr: { on: vi.fn() },
71
+ stdin: { write: vi.fn() },
72
+ then: (cb) => cb({ exitCode: 1 }),
73
+ }
74
+ execa.mockReturnValue(mockChild)
75
+ const { warn } = await import('../../utils/exec.js')
76
+
77
+ await installBrowser()
78
+
79
+ expect(warn).toHaveBeenCalledWith('opencode-browser install exited with non-zero code')
80
+ })
81
+
82
+ it('resolves __LOCATION__ to local answer by default', async () => {
83
+ const { execa } = await import('execa')
84
+ let capturedTriggers = null
85
+ const mockChild = {
86
+ stdout: { on: vi.fn((_, cb) => { capturedTriggers = cb }) },
87
+ stderr: { on: vi.fn() },
88
+ stdin: { write: vi.fn() },
89
+ then: (cb) => cb({ exitCode: 0 }),
90
+ }
91
+ execa.mockReturnValue(mockChild)
92
+
93
+ await installBrowser()
94
+
95
+ if (capturedTriggers) capturedTriggers(Buffer.from('Choose config location'))
96
+ expect(mockChild.stdin.write).toHaveBeenCalledWith('2\n')
97
+ })
98
+
99
+ it('resolves __LOCATION__ to global answer when installScope is global', async () => {
100
+ const { execa } = await import('execa')
101
+ let capturedTriggers = null
102
+ const mockChild = {
103
+ stdout: { on: vi.fn((_, cb) => { capturedTriggers = cb }) },
104
+ stderr: { on: vi.fn() },
105
+ stdin: { write: vi.fn() },
106
+ then: (cb) => cb({ exitCode: 0 }),
107
+ }
108
+ execa.mockReturnValue(mockChild)
109
+
110
+ await installBrowser({ installScope: 'global' })
111
+
112
+ if (capturedTriggers) capturedTriggers(Buffer.from('Choose config location'))
113
+ expect(mockChild.stdin.write).toHaveBeenCalledWith('1\n')
114
+ })
115
+ })
@@ -1,54 +1,62 @@
1
- import { execa } from 'execa'
2
- import fse from 'fs-extra'
3
- import { header, info, success, warn, error } from '../../utils/exec.js'
4
- import os from 'os'
5
- import path from 'path'
6
- import { fileURLToPath } from 'url'
7
-
8
- const __dirname = path.dirname(fileURLToPath(import.meta.url))
9
- const BROWSER_PRESET_PATH = path.resolve(__dirname, '../../presets/browser.json')
10
- const browserPreset = await fse.readJson(BROWSER_PRESET_PATH)
11
-
12
- export async function installBrowser() {
13
- header('Step 9, Installing opencode-browser')
14
-
15
- try {
16
- const child = execa(browserPreset.installer.command, browserPreset.installer.args, {
17
- cwd: os.homedir(),
18
- stdio: ['pipe', 'pipe', 'pipe'],
19
- reject: false,
20
- })
21
-
22
- const pendingTriggers = [...browserPreset.autoAnswers]
23
- let show = false
24
-
25
- child.stdout.on('data', (chunk) => {
26
- const text = chunk.toString()
27
-
28
- if (text.includes(browserPreset.output.showAfter)) show = true
29
- if (text.includes(browserPreset.output.hideAfter)) show = false
30
-
31
- if (show) process.stdout.write(chunk)
32
-
33
- for (let i = 0; i < pendingTriggers.length; i++) {
34
- if (text.includes(pendingTriggers[i].trigger)) {
35
- child.stdin.write(pendingTriggers[i].response + '\n')
36
- pendingTriggers.splice(i, 1)
37
- break
38
- }
39
- }
40
- })
41
-
42
- child.stderr.on('data', (chunk) => process.stderr.write(chunk))
43
-
44
- const result = await child
45
-
46
- if (result.exitCode === 0) {
47
- success('opencode-browser installed')
48
- } else {
49
- warn('opencode-browser install exited with non-zero code')
50
- }
51
- } catch (err) {
52
- error(`Failed to install opencode-browser: ${err.message}`)
53
- }
54
- }
1
+ import { execa } from 'execa'
2
+ import fse from 'fs-extra'
3
+ import { header, success, warn, error } from '../../utils/exec.js'
4
+ import os from 'os'
5
+ import path from 'path'
6
+ import { fileURLToPath } from 'url'
7
+
8
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
9
+ const BROWSER_PRESET_PATH = path.resolve(__dirname, '../../presets/browser.json')
10
+ const browserPreset = await fse.readJson(BROWSER_PRESET_PATH)
11
+
12
+ export async function installBrowser(ctx = {}) {
13
+ header('Step 9, Installing opencode-browser')
14
+
15
+ const installScope = ctx.installScope || 'local'
16
+ const locationAnswer = browserPreset.locationChoices?.[installScope] ?? browserPreset.locationChoices?.local ?? '2'
17
+
18
+ const pendingTriggers = browserPreset.autoAnswers.map(a => ({
19
+ ...a,
20
+ response: a.response === '__LOCATION__' ? locationAnswer : a.response,
21
+ }))
22
+
23
+ try {
24
+ const child = execa(browserPreset.installer.command, browserPreset.installer.args, {
25
+ cwd: os.homedir(),
26
+ stdio: ['pipe', 'pipe', 'pipe'],
27
+ reject: false,
28
+ })
29
+
30
+ let show = false
31
+ const triggers = [...pendingTriggers]
32
+
33
+ child.stdout.on('data', (chunk) => {
34
+ const text = chunk.toString()
35
+
36
+ if (text.includes(browserPreset.output.showAfter)) show = true
37
+ if (text.includes(browserPreset.output.hideAfter)) show = false
38
+
39
+ if (show) process.stdout.write(chunk)
40
+
41
+ for (let i = 0; i < triggers.length; i++) {
42
+ if (text.includes(triggers[i].trigger)) {
43
+ child.stdin.write(`${triggers[i].response}\n`)
44
+ triggers.splice(i, 1)
45
+ break
46
+ }
47
+ }
48
+ })
49
+
50
+ child.stderr.on('data', (chunk) => process.stderr.write(chunk))
51
+
52
+ const result = await child
53
+
54
+ if (result.exitCode === 0) {
55
+ success('opencode-browser installed')
56
+ } else {
57
+ warn('opencode-browser install exited with non-zero code')
58
+ }
59
+ } catch (err) {
60
+ error(`Failed to install opencode-browser: ${err.message}`)
61
+ }
62
+ }
@@ -1,107 +1,108 @@
1
- import { checkbox } from '@inquirer/prompts'
2
- import fse from 'fs-extra'
3
- import path from 'path'
4
- import { fileURLToPath } from 'url'
5
- import { findAiFiles } from '../../utils/copy.js'
6
- import { header, info, success, warn } from '../../utils/exec.js'
7
-
8
- const __dirname = path.dirname(fileURLToPath(import.meta.url))
9
- const CLEAN_PRESET_PATH = path.resolve(__dirname, '../../presets/clean.json')
10
- const cleanPreset = await fse.readJson(CLEAN_PRESET_PATH)
11
-
12
- async function childrenExcludingPreserved(dir) {
13
- const results = []
14
- if (!await fse.pathExists(dir)) return results
15
- const entries = await fse.readdir(dir)
16
- for (const entry of entries) {
17
- if (cleanPreset.preserveSubfolders.includes(entry)) continue
18
- results.push(path.join(dir, entry))
19
- }
20
- return results
21
- }
22
-
23
- async function isPopulated(filePath) {
24
- if (!await fse.pathExists(filePath)) return false
25
- const content = await fse.readFile(filePath, 'utf-8')
26
- const trimmed = content.trim()
27
- if (!trimmed) return false
28
- if (trimmed.startsWith('<!-- onboard-prompt')) return false
29
- return true
30
- }
31
-
32
- async function hasOpenspecHistory(cwd) {
33
- const changesDir = path.join(cwd, 'openspec', 'changes')
34
- const archiveDir = path.join(cwd, 'openspec', 'archive')
35
- if (await fse.pathExists(changesDir)) {
36
- const entries = await fse.readdir(changesDir)
37
- if (entries.length > 0) return true
38
- }
39
- if (await fse.pathExists(archiveDir)) {
40
- const entries = await fse.readdir(archiveDir)
41
- if (entries.length > 0) return true
42
- }
43
- return false
44
- }
45
-
46
- export async function cleanAiFiles() {
47
- header('Step 2, Existing AI config files')
48
-
49
- const cwd = process.cwd()
50
- const ctx = {
51
- hasDesign: await isPopulated(path.join(cwd, 'DESIGN.md')),
52
- hasArchitecture: await isPopulated(path.join(cwd, 'ARCHITECTURE.md')),
53
- hasOpenspec: await hasOpenspecHistory(cwd),
54
- }
55
-
56
- if (ctx.hasDesign) info('DESIGN.md exists and is populated, keeping it')
57
- if (ctx.hasArchitecture) info('ARCHITECTURE.md exists and is populated, keeping it')
58
- if (ctx.hasOpenspec) info('openspec/ history exists, keeping it')
59
-
60
- const flatFiles = await findAiFiles(cwd, cleanPreset.detectFiles)
61
- const dirTargets = cleanPreset.directoryTargets
62
- const dirEntries = []
63
- for (const dirName of dirTargets) {
64
- const dirPath = path.join(cwd, dirName)
65
- const children = await childrenExcludingPreserved(dirPath)
66
- dirEntries.push(...children)
67
- }
68
-
69
- const filteredFlat = flatFiles.filter(f => {
70
- const rel = path.relative(cwd, f)
71
- if (dirTargets.includes(rel)) return false
72
- if (cleanPreset.preserve.some(p => rel === p || rel.startsWith(p + path.sep))) return false
73
- return true
74
- })
75
-
76
- const allToRemove = [...filteredFlat, ...dirEntries]
77
-
78
- if (allToRemove.length === 0) {
79
- success('No existing AI config files to remove')
80
- return ctx
81
- }
82
-
83
- const choices = allToRemove.map(f => ({
84
- name: path.relative(cwd, f).replace(/\\/g, '/'),
85
- value: f,
86
- checked: true,
87
- }))
88
-
89
- const selected = await checkbox({
90
- message: cleanPreset.selectionMessage,
91
- choices,
92
- })
93
-
94
- if (!selected || selected.length === 0) {
95
- success('No AI config files selected for removal')
96
- return ctx
97
- }
98
-
99
- warn('Removing selected AI config files:')
100
- for (const f of selected) {
101
- info(' ' + f.replace(cwd + path.sep, ''))
102
- await fse.remove(f)
103
- }
104
- success('Removed existing AI config files')
105
-
106
- return ctx
107
- }
1
+ import { checkbox } from '@inquirer/prompts'
2
+ import fse from 'fs-extra'
3
+ import path from 'path'
4
+ import { fileURLToPath } from 'url'
5
+ import { findAiFiles } from '../../utils/copy.js'
6
+ import { header, info, success, warn } from '../../utils/exec.js'
7
+ import { MARKERS } from '../../utils/terminal.js'
8
+
9
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
10
+ const CLEAN_PRESET_PATH = path.resolve(__dirname, '../../presets/clean.json')
11
+ const cleanPreset = await fse.readJson(CLEAN_PRESET_PATH)
12
+
13
+ async function childrenExcludingPreserved(dir) {
14
+ const results = []
15
+ if (!await fse.pathExists(dir)) return results
16
+ const entries = await fse.readdir(dir)
17
+ for (const entry of entries) {
18
+ if (cleanPreset.preserveSubfolders.includes(entry)) continue
19
+ results.push(path.join(dir, entry))
20
+ }
21
+ return results
22
+ }
23
+
24
+ async function isPopulated(filePath) {
25
+ if (!await fse.pathExists(filePath)) return false
26
+ const content = await fse.readFile(filePath, 'utf-8')
27
+ const trimmed = content.trim()
28
+ if (!trimmed) return false
29
+ if (trimmed.startsWith('<!-- onboard-prompt')) return false
30
+ return true
31
+ }
32
+
33
+ async function hasOpenspecHistory(cwd) {
34
+ const changesDir = path.join(cwd, 'openspec', 'changes')
35
+ const archiveDir = path.join(cwd, 'openspec', 'archive')
36
+ if (await fse.pathExists(changesDir)) {
37
+ const entries = await fse.readdir(changesDir)
38
+ if (entries.length > 0) return true
39
+ }
40
+ if (await fse.pathExists(archiveDir)) {
41
+ const entries = await fse.readdir(archiveDir)
42
+ if (entries.length > 0) return true
43
+ }
44
+ return false
45
+ }
46
+
47
+ export async function cleanAiFiles() {
48
+ header('Step 2, Existing AI config files')
49
+
50
+ const cwd = process.cwd()
51
+ const ctx = {
52
+ hasDesign: await isPopulated(path.join(cwd, 'DESIGN.md')),
53
+ hasArchitecture: await isPopulated(path.join(cwd, 'ARCHITECTURE.md')),
54
+ hasOpenspec: await hasOpenspecHistory(cwd),
55
+ }
56
+
57
+ if (ctx.hasDesign) info('DESIGN.md exists and is populated, keeping it')
58
+ if (ctx.hasArchitecture) info('ARCHITECTURE.md exists and is populated, keeping it')
59
+ if (ctx.hasOpenspec) info('openspec/ history exists, keeping it')
60
+
61
+ const flatFiles = await findAiFiles(cwd, cleanPreset.detectFiles)
62
+ const dirTargets = cleanPreset.directoryTargets
63
+ const dirEntries = []
64
+ for (const dirName of dirTargets) {
65
+ const dirPath = path.join(cwd, dirName)
66
+ const children = await childrenExcludingPreserved(dirPath)
67
+ dirEntries.push(...children)
68
+ }
69
+
70
+ const filteredFlat = flatFiles.filter(f => {
71
+ const rel = path.relative(cwd, f)
72
+ if (dirTargets.includes(rel)) return false
73
+ if (cleanPreset.preserve.some(p => rel === p || rel.startsWith(p + path.sep))) return false
74
+ return true
75
+ })
76
+
77
+ const allToRemove = [...filteredFlat, ...dirEntries]
78
+
79
+ if (allToRemove.length === 0) {
80
+ success('No existing AI config files to remove')
81
+ return ctx
82
+ }
83
+
84
+ const choices = allToRemove.map(f => ({
85
+ name: path.relative(cwd, f).replace(/\\/g, '/'),
86
+ value: f,
87
+ checked: true,
88
+ }))
89
+
90
+ const selected = await checkbox({
91
+ message: cleanPreset.selectionMessage,
92
+ choices,
93
+ })
94
+
95
+ if (!selected || selected.length === 0) {
96
+ success('No AI config files selected for removal')
97
+ return ctx
98
+ }
99
+
100
+ warn('Removing selected AI config files:')
101
+ for (const f of selected) {
102
+ info(`${MARKERS.EMPTY}${f.replace(cwd + path.sep, '')}`)
103
+ await fse.remove(f)
104
+ }
105
+ success('Removed existing AI config files')
106
+
107
+ return ctx
108
+ }