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.
- package/LICENSE +21 -0
- package/README.md +304 -301
- package/content/.agents/agents/basic-engineer.md +4 -2
- package/content/.agents/agents/devops-manager.md +123 -123
- package/content/.agents/skills/ob-default/SKILL.md +25 -21
- package/content/.agents/skills/ob-generic-guardrails/SKILL.md +36 -32
- package/content/.agents/skills/ob-global/SKILL.md +92 -49
- package/content/.agents/skills/ob-pullrequest-az/SKILL.md +168 -160
- package/content/.agents/skills/ob-pullrequest-gh/SKILL.md +140 -136
- package/content/.opencode/commands/create-engineer.md +109 -0
- package/content/.opencode/commands/init.md +1 -1
- package/content/.opencode/commands/main.md +1 -1
- package/content/.opencode/commands/opsx-apply.md +131 -70
- package/content/.opencode/commands/plan.md +1 -1
- package/content/.opencode/plugins/session-log.js +523 -519
- package/content/.opencode/skills/openspec-apply-change/SKILL.md +86 -64
- package/content/AGENTS.md +67 -39
- package/package.json +1 -1
- package/src/commands/join.js +3 -3
- package/src/commands/single.js +2 -0
- package/src/commands/wizard.js +124 -99
- package/src/presets/browser.json +22 -18
- package/src/presets/optimization.json +27 -22
- package/src/presets/source.json +7 -1
- package/src/steps/browser/browser.test.js +115 -81
- package/src/steps/browser/index.js +62 -54
- package/src/steps/clean/index.js +108 -107
- package/src/steps/copy/agents.js +28 -0
- package/src/steps/copy/copy.test.js +1 -0
- package/src/steps/copy/index.js +2 -1
- package/src/steps/metadata/index.js +63 -61
- package/src/steps/models/format.js +61 -60
- package/src/steps/models/write.test.js +117 -117
- package/src/steps/openspec/ensemble.js +30 -7
- package/src/steps/openspec/ensemble.test.js +79 -79
- package/src/steps/openspec/index.js +121 -32
- package/src/steps/openspec/index.test.js +63 -0
- package/src/steps/optimization/caveman.js +34 -29
- package/src/steps/optimization/codegraph.js +52 -0
- package/src/steps/optimization/global.js +88 -64
- package/src/steps/optimization/global.test.js +99 -0
- package/src/steps/optimization/index.js +109 -101
- package/src/steps/optimization/optimization.test.js +101 -93
- package/src/steps/optimization/quota.js +84 -84
- package/src/steps/source/index.js +48 -0
- package/src/steps/source/source.test.js +124 -91
- package/src/utils/__tests__/copy.test.js +117 -117
- package/src/utils/exec-spinner.js +47 -47
- package/src/utils/exec.js +134 -131
- package/src/utils/terminal.js +6 -0
package/src/presets/browser.json
CHANGED
|
@@ -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
|
-
"
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
{ "trigger": "
|
|
16
|
-
{ "trigger": "
|
|
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
|
+
}
|
package/src/presets/source.json
CHANGED
|
@@ -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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
+
}
|
package/src/steps/clean/index.js
CHANGED
|
@@ -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
|
-
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
if (trimmed
|
|
29
|
-
return
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
if (ctx.
|
|
58
|
-
if (ctx.
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if (
|
|
73
|
-
return
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
+
}
|