opencode-onboard 0.3.3 → 0.4.1
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/README.md +266 -214
- package/content/.agents/agents/basic-engineer.md +30 -0
- package/content/.agents/agents/devops-manager.md +38 -29
- package/content/.agents/session-log.json +41 -0
- package/content/.agents/skills/ob-default/SKILL.md +21 -0
- package/content/.agents/skills/ob-generic-guardrails/SKILL.md +32 -0
- package/content/.agents/skills/ob-global/SKILL.md +49 -0
- package/content/.agents/skills/ob-pullrequest-az/SKILL.md +11 -21
- package/content/.agents/skills/ob-pullrequest-gh/SKILL.md +14 -24
- package/content/.agents/skills/ob-userstory-az/SKILL.md +8 -14
- package/content/.agents/skills/ob-userstory-gh/SKILL.md +6 -14
- package/content/.opencode/commands/opsx-apply.md +50 -33
- package/content/.opencode/plugins/session-log.js +1 -1
- package/content/.opencode/skills/openspec-apply-change/SKILL.md +50 -33
- package/content/AGENTS.md +94 -144
- package/content/skills-lock.json +4 -0
- package/package.json +6 -1
- package/src/index.js +13 -47
- package/src/presets/browser.json +18 -0
- package/src/presets/clean.json +21 -0
- package/src/presets/models.json +33 -0
- package/src/presets/optimization.json +22 -0
- package/src/presets/platforms.json +29 -2
- package/src/presets/quota.json +14 -0
- package/src/presets/source.json +17 -0
- package/src/steps/browser/browser.test.js +81 -0
- package/src/steps/{install-browser.js → browser/index.js} +12 -15
- package/src/steps/{__tests__/clean-ai-files.test.js → clean/clean.test.js} +28 -13
- package/src/steps/{clean-ai-files.js → clean/index.js} +32 -30
- package/src/steps/copy/agents.js +106 -0
- package/src/steps/{__tests__/copy-content.test.js → copy/copy.test.js} +10 -1
- package/src/steps/copy/index.js +33 -0
- package/src/steps/copy/skills.js +55 -0
- package/src/steps/{write-onboard-config.js → metadata/index.js} +3 -3
- package/src/steps/metadata/metadata.test.js +96 -0
- package/src/steps/models/format.js +60 -0
- package/src/steps/models/format.test.js +74 -0
- package/src/steps/models/index.js +52 -0
- package/src/steps/models/write.js +54 -0
- package/src/steps/models/write.test.js +119 -0
- package/src/steps/{init-openspec.js → openspec/ensemble.js} +20 -57
- package/src/steps/openspec/ensemble.test.js +79 -0
- package/src/steps/openspec/index.js +32 -0
- package/src/steps/optimization/caveman-guidance.js +11 -0
- package/src/steps/{install-caveman.js → optimization/caveman.js} +5 -19
- package/src/steps/optimization/global.js +64 -0
- package/src/steps/optimization/index.js +101 -0
- package/src/steps/{__tests__/token-optimization.test.js → optimization/optimization.test.js} +19 -24
- package/src/steps/{install-quota.js → optimization/quota.js} +12 -10
- package/src/steps/platform/index.js +81 -0
- package/src/steps/platform/platform.test.js +129 -0
- package/src/steps/{choose-source-scope.js → source/index.js} +11 -17
- package/src/steps/source/source.test.js +89 -0
- package/src/utils/__tests__/copy.test.js +12 -5
- package/src/utils/copy.js +4 -24
- package/src/utils/exec-spinner.js +47 -0
- package/src/utils/exec.js +120 -162
- package/src/utils/models-cache.js +25 -68
- package/src/utils/models-pricing.js +42 -0
- package/src/utils/models-pricing.test.js +94 -0
- package/content/.agents/agents/back-engineer.md +0 -87
- package/content/.agents/agents/front-engineer.md +0 -86
- package/content/.agents/agents/infra-engineer.md +0 -85
- package/content/.agents/agents/quality-engineer.md +0 -86
- package/content/.agents/agents/security-auditor.md +0 -86
- package/src/steps/__tests__/check-env.test.js +0 -70
- package/src/steps/__tests__/check-platform.test.js +0 -104
- package/src/steps/__tests__/check-rtk.test.js +0 -38
- package/src/steps/__tests__/choose-platform.test.js +0 -38
- package/src/steps/check-env.js +0 -26
- package/src/steps/check-platform.js +0 -80
- package/src/steps/check-rtk.js +0 -38
- package/src/steps/choose-models.js +0 -165
- package/src/steps/choose-platform.js +0 -22
- package/src/steps/choose-skills-provider.js +0 -79
- package/src/steps/copy-content.js +0 -89
- package/src/steps/enable-caveman-guidance.js +0 -78
- package/src/steps/patch-agents-md.js +0 -153
- package/src/steps/token-optimization.js +0 -59
package/src/index.js
CHANGED
|
@@ -3,23 +3,15 @@ import chalk from 'chalk'
|
|
|
3
3
|
import fse from 'fs-extra'
|
|
4
4
|
import { createRequire } from 'node:module'
|
|
5
5
|
import path from 'node:path'
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import { chooseModels } from './steps/
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import { enableCavemanGuidance } from './steps/enable-caveman-guidance.js'
|
|
16
|
-
import { initOpenspec } from './steps/init-openspec.js'
|
|
17
|
-
import { installBrowser } from './steps/install-browser.js'
|
|
18
|
-
import { installCaveman } from './steps/install-caveman.js'
|
|
19
|
-
import { installQuota } from './steps/install-quota.js'
|
|
20
|
-
import { patchAgentsMd } from './steps/patch-agents-md.js'
|
|
21
|
-
import { tokenOptimizationStep } from './steps/token-optimization.js'
|
|
22
|
-
import { writeOnboardConfig } from './steps/write-onboard-config.js'
|
|
6
|
+
import { installBrowser } from './steps/browser/index.js'
|
|
7
|
+
import { cleanAiFiles } from './steps/clean/index.js'
|
|
8
|
+
import { copyContentStep } from './steps/copy/index.js'
|
|
9
|
+
import { chooseModels } from './steps/models/index.js'
|
|
10
|
+
import { initOpenspec } from './steps/openspec/index.js'
|
|
11
|
+
import { tokenOptimizationStep } from './steps/optimization/index.js'
|
|
12
|
+
import { choosePlatform } from './steps/platform/index.js'
|
|
13
|
+
import { chooseSourceScope } from './steps/source/index.js'
|
|
14
|
+
import { writeOnboardConfig } from './steps/metadata/index.js'
|
|
23
15
|
|
|
24
16
|
function printHelp(version) {
|
|
25
17
|
console.log(`opencode-onboard v${version}`)
|
|
@@ -33,12 +25,8 @@ function printHelp(version) {
|
|
|
33
25
|
console.log(' platform Run platform selection step')
|
|
34
26
|
console.log(' copy Run content copy step')
|
|
35
27
|
console.log(' openspec Run OpenSpec initialization step')
|
|
36
|
-
console.log(' skills Run skills install step')
|
|
37
28
|
console.log(' models Run models selection step')
|
|
38
29
|
console.log(' optimization Run token optimization tools step')
|
|
39
|
-
console.log(' quota Run opencode-quota installer step')
|
|
40
|
-
console.log(' rtk Run rtk check step')
|
|
41
|
-
console.log(' caveman Run caveman install + guidance steps')
|
|
42
30
|
console.log(' browser Run opencode-browser installer step')
|
|
43
31
|
console.log(' metadata Write onboarding metadata step')
|
|
44
32
|
console.log()
|
|
@@ -78,29 +66,15 @@ async function runSingleCommand(command) {
|
|
|
78
66
|
},
|
|
79
67
|
copy: async () => {
|
|
80
68
|
await copyContentStep(resolvedPlatform, ctx)
|
|
81
|
-
await patchAgentsMd(ctx)
|
|
82
69
|
},
|
|
83
70
|
openspec: async () => {
|
|
84
71
|
await initOpenspec()
|
|
85
72
|
},
|
|
86
|
-
skills: async () => {
|
|
87
|
-
await chooseSkillsProvider()
|
|
88
|
-
},
|
|
89
73
|
models: async () => {
|
|
90
74
|
await chooseModels()
|
|
91
75
|
},
|
|
92
76
|
optimization: async () => {
|
|
93
|
-
await tokenOptimizationStep({
|
|
94
|
-
},
|
|
95
|
-
quota: async () => {
|
|
96
|
-
await installQuota()
|
|
97
|
-
},
|
|
98
|
-
rtk: async () => {
|
|
99
|
-
await checkRtk()
|
|
100
|
-
},
|
|
101
|
-
caveman: async () => {
|
|
102
|
-
const caveman = await installCaveman({ skillsProvider: savedWizard?.additionalSkillsProvider })
|
|
103
|
-
await enableCavemanGuidance(caveman)
|
|
77
|
+
await tokenOptimizationStep({ ctx })
|
|
104
78
|
},
|
|
105
79
|
browser: async () => {
|
|
106
80
|
await installBrowser()
|
|
@@ -109,7 +83,7 @@ async function runSingleCommand(command) {
|
|
|
109
83
|
await writeOnboardConfig({
|
|
110
84
|
...ctx,
|
|
111
85
|
platform: resolvedPlatform,
|
|
112
|
-
additionalSkillsProvider:
|
|
86
|
+
additionalSkillsProvider: 'npx-skills',
|
|
113
87
|
planModel: savedWizard?.models?.plan ?? null,
|
|
114
88
|
buildModel: savedWizard?.models?.build ?? null,
|
|
115
89
|
fastModel: savedWizard?.models?.fast ?? null,
|
|
@@ -186,28 +160,20 @@ if (process.stdin.isTTY) {
|
|
|
186
160
|
}
|
|
187
161
|
|
|
188
162
|
try {
|
|
189
|
-
await checkEnv()
|
|
190
|
-
|
|
191
163
|
const scope = await chooseSourceScope()
|
|
192
164
|
|
|
193
165
|
const preserve = await cleanAiFiles()
|
|
194
166
|
const ctx = { ...preserve, ...scope }
|
|
195
167
|
|
|
196
168
|
const platform = await choosePlatform()
|
|
197
|
-
|
|
198
|
-
await checkPlatform(platform)
|
|
199
169
|
|
|
200
170
|
await copyContentStep(platform, ctx)
|
|
201
171
|
|
|
202
|
-
await patchAgentsMd(ctx)
|
|
203
|
-
|
|
204
172
|
await initOpenspec()
|
|
205
173
|
|
|
206
|
-
const skillsSelection = await chooseSkillsProvider()
|
|
207
|
-
|
|
208
174
|
const selectedModels = await chooseModels()
|
|
209
175
|
|
|
210
|
-
const tokenOpt = await tokenOptimizationStep({
|
|
176
|
+
const tokenOpt = await tokenOptimizationStep({ ctx })
|
|
211
177
|
const { rtk, quota, caveman, cavemanGuidance } = tokenOpt
|
|
212
178
|
|
|
213
179
|
await installBrowser()
|
|
@@ -215,7 +181,7 @@ try {
|
|
|
215
181
|
await writeOnboardConfig({
|
|
216
182
|
...ctx,
|
|
217
183
|
platform,
|
|
218
|
-
|
|
184
|
+
additionalSkillsProvider: 'npx-skills',
|
|
219
185
|
...selectedModels,
|
|
220
186
|
optionalTools: { rtk, quota, caveman },
|
|
221
187
|
cavemanGuidance,
|
|
@@ -0,0 +1,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
|
+
"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
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"preserve": ["DESIGN.md", "ARCHITECTURE.md", "openspec"],
|
|
3
|
+
"detectFiles": [
|
|
4
|
+
"AGENTS.md",
|
|
5
|
+
"CLAUDE.md",
|
|
6
|
+
"ARCHITECTURE.md",
|
|
7
|
+
"DESIGN.md",
|
|
8
|
+
".cursorrules",
|
|
9
|
+
".clinerules",
|
|
10
|
+
".windsurfrules",
|
|
11
|
+
".github/copilot-instructions.md",
|
|
12
|
+
"copilot-instructions.md",
|
|
13
|
+
".aider.conf.yml",
|
|
14
|
+
".aider",
|
|
15
|
+
".opencode",
|
|
16
|
+
".agents"
|
|
17
|
+
],
|
|
18
|
+
"directoryTargets": [".opencode", ".agents"],
|
|
19
|
+
"preserveSubfolders": ["skills"],
|
|
20
|
+
"selectionMessage": "Select AI config files to remove:"
|
|
21
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"costTiers": [
|
|
3
|
+
{ "max": 1, "label": "[$]" },
|
|
4
|
+
{ "max": 10, "label": "[$$]" },
|
|
5
|
+
{ "label": "[$$$]" }
|
|
6
|
+
],
|
|
7
|
+
"roles": {
|
|
8
|
+
"plan": {
|
|
9
|
+
"prompt": "Plan model:",
|
|
10
|
+
"info": [
|
|
11
|
+
"PLAN model: used by the main agent to read issues, write proposals, coordinate the team.",
|
|
12
|
+
"This model needs to be strong. Use Claude Sonnet/Opus, GPT-4o, o3, or equivalent.",
|
|
13
|
+
"A weak model here will silently skip steps and break the workflow."
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
"build": {
|
|
17
|
+
"prompt": "Build model:",
|
|
18
|
+
"info": [
|
|
19
|
+
"BUILD model: used by front-engineer, back-engineer, infra-engineer, quality-engineer, security-auditor.",
|
|
20
|
+
"Needs to be capable for implementation work. Claude Sonnet, GPT-4o, or equivalent."
|
|
21
|
+
],
|
|
22
|
+
"agents": ["front-engineer", "back-engineer", "infra-engineer", "quality-engineer", "security-auditor"]
|
|
23
|
+
},
|
|
24
|
+
"fast": {
|
|
25
|
+
"prompt": "Fast model:",
|
|
26
|
+
"info": [
|
|
27
|
+
"FAST model: used by devops-manager for reading issues and classifying PR comments.",
|
|
28
|
+
"Something fast and cheap is fine here, no heavy reasoning needed."
|
|
29
|
+
],
|
|
30
|
+
"agents": ["devops-manager"]
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,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
|
+
}
|
|
@@ -1,10 +1,37 @@
|
|
|
1
1
|
[
|
|
2
2
|
{
|
|
3
3
|
"value": "github",
|
|
4
|
-
"name": "GitHub"
|
|
4
|
+
"name": "GitHub",
|
|
5
|
+
"cli": {
|
|
6
|
+
"command": "gh",
|
|
7
|
+
"displayName": "GitHub CLI (gh)",
|
|
8
|
+
"installUrl": "https://cli.github.com",
|
|
9
|
+
"authCheck": {
|
|
10
|
+
"args": ["auth", "status"],
|
|
11
|
+
"notAuthenticatedMessage": "GitHub CLI not authenticated. Run:",
|
|
12
|
+
"commands": ["gh auth login"]
|
|
13
|
+
}
|
|
14
|
+
}
|
|
5
15
|
},
|
|
6
16
|
{
|
|
7
17
|
"value": "azure",
|
|
8
|
-
"name": "Azure DevOps"
|
|
18
|
+
"name": "Azure DevOps",
|
|
19
|
+
"cli": {
|
|
20
|
+
"command": "az",
|
|
21
|
+
"displayName": "Azure CLI (az)",
|
|
22
|
+
"installUrl": "https://learn.microsoft.com/en-us/cli/azure/install-azure-cli",
|
|
23
|
+
"extensionCheck": {
|
|
24
|
+
"args": ["extension", "list", "--query", "[?name=='azure-devops']", "-o", "tsv"],
|
|
25
|
+
"expectedOutput": "azure-devops",
|
|
26
|
+
"missingMessage": "azure-devops extension not found. Run:",
|
|
27
|
+
"errorMessage": "Could not check azure-devops extension. Run:",
|
|
28
|
+
"commands": [
|
|
29
|
+
"az extension add --name azure-devops",
|
|
30
|
+
"az config set extension.dynamic_install_allow_preview=true",
|
|
31
|
+
"az login",
|
|
32
|
+
"az devops login --organization https://dev.azure.com/<your-org>"
|
|
33
|
+
]
|
|
34
|
+
}
|
|
35
|
+
}
|
|
9
36
|
}
|
|
10
37
|
]
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"plugin": "@slkiser/opencode-quota@latest",
|
|
3
|
+
"prompt": {
|
|
4
|
+
"message": "Install opencode-quota with recommended defaults?",
|
|
5
|
+
"default": true,
|
|
6
|
+
"timeoutMs": 20000
|
|
7
|
+
},
|
|
8
|
+
"defaults": {
|
|
9
|
+
"enabledProviders": "auto",
|
|
10
|
+
"formatStyle": "singleWindow",
|
|
11
|
+
"percentDisplayMode": "used",
|
|
12
|
+
"showSessionTokens": true
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"message": "Source code location:",
|
|
3
|
+
"default": "current",
|
|
4
|
+
"choices": [
|
|
5
|
+
{
|
|
6
|
+
"name": "Current folder (default)",
|
|
7
|
+
"value": "current",
|
|
8
|
+
"description": "Use this repository only"
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"name": "Select folders in parent (../)",
|
|
12
|
+
"value": "parent",
|
|
13
|
+
"description": "Use when this repo only contains agent config"
|
|
14
|
+
}
|
|
15
|
+
],
|
|
16
|
+
"parentSelectionMessage": "Select source folders from parent directory:"
|
|
17
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
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,35 +1,32 @@
|
|
|
1
1
|
import { execa } from 'execa'
|
|
2
|
-
import
|
|
2
|
+
import fse from 'fs-extra'
|
|
3
|
+
import { header, info, success, warn, error } from '../../utils/exec.js'
|
|
3
4
|
import os from 'os'
|
|
5
|
+
import path from 'path'
|
|
6
|
+
import { fileURLToPath } from 'url'
|
|
4
7
|
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
{ trigger: 'Add plugin automatically?', response: 'y' },
|
|
9
|
-
{ trigger: 'Create one?', response: 'y' },
|
|
10
|
-
{ trigger: 'Add browser-automation skill', response: 'n' },
|
|
11
|
-
{ trigger: 'Check broker', response: 'n' },
|
|
12
|
-
]
|
|
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)
|
|
13
11
|
|
|
14
12
|
export async function installBrowser() {
|
|
15
|
-
header('Step
|
|
13
|
+
header('Step 9, Installing opencode-browser')
|
|
16
14
|
|
|
17
15
|
try {
|
|
18
|
-
const child = execa(
|
|
16
|
+
const child = execa(browserPreset.installer.command, browserPreset.installer.args, {
|
|
19
17
|
cwd: os.homedir(),
|
|
20
18
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
21
19
|
reject: false,
|
|
22
20
|
})
|
|
23
21
|
|
|
24
|
-
const pendingTriggers = [...
|
|
22
|
+
const pendingTriggers = [...browserPreset.autoAnswers]
|
|
25
23
|
let show = false
|
|
26
24
|
|
|
27
25
|
child.stdout.on('data', (chunk) => {
|
|
28
26
|
const text = chunk.toString()
|
|
29
27
|
|
|
30
|
-
|
|
31
|
-
if (text.includes(
|
|
32
|
-
if (text.includes('Press Enter when')) show = false
|
|
28
|
+
if (text.includes(browserPreset.output.showAfter)) show = true
|
|
29
|
+
if (text.includes(browserPreset.output.hideAfter)) show = false
|
|
33
30
|
|
|
34
31
|
if (show) process.stdout.write(chunk)
|
|
35
32
|
|
|
@@ -3,6 +3,10 @@ import fse from 'fs-extra'
|
|
|
3
3
|
import os from 'os'
|
|
4
4
|
import path from 'path'
|
|
5
5
|
|
|
6
|
+
vi.mock('@inquirer/prompts', () => ({
|
|
7
|
+
checkbox: vi.fn(),
|
|
8
|
+
}))
|
|
9
|
+
|
|
6
10
|
vi.mock('../../utils/exec.js', () => ({
|
|
7
11
|
header: vi.fn(),
|
|
8
12
|
success: vi.fn(),
|
|
@@ -11,7 +15,8 @@ vi.mock('../../utils/exec.js', () => ({
|
|
|
11
15
|
prompt: vi.fn(),
|
|
12
16
|
}))
|
|
13
17
|
|
|
14
|
-
import { success
|
|
18
|
+
import { success } from '../../utils/exec.js'
|
|
19
|
+
import { checkbox } from '@inquirer/prompts'
|
|
15
20
|
|
|
16
21
|
describe('cleanAiFiles()', () => {
|
|
17
22
|
let tmpDir
|
|
@@ -31,24 +36,22 @@ describe('cleanAiFiles()', () => {
|
|
|
31
36
|
})
|
|
32
37
|
|
|
33
38
|
it('prints success when no AI files are found', async () => {
|
|
34
|
-
const { cleanAiFiles } = await import('
|
|
35
|
-
|
|
36
|
-
// Simulate immediate Enter key
|
|
37
|
-
const stdinPush = () => process.stdin.emit('data', '\n')
|
|
38
|
-
setTimeout(stdinPush, 10)
|
|
39
|
+
const { cleanAiFiles } = await import('./index.js')
|
|
39
40
|
|
|
40
41
|
await cleanAiFiles()
|
|
41
42
|
|
|
42
43
|
expect(success).toHaveBeenCalledWith('No existing AI config files to remove')
|
|
43
44
|
})
|
|
44
45
|
|
|
45
|
-
it('removes
|
|
46
|
+
it('removes selected AI files', async () => {
|
|
46
47
|
await fse.writeFile(path.join(tmpDir, 'AGENTS.md'), '# agents')
|
|
47
48
|
await fse.writeFile(path.join(tmpDir, 'CLAUDE.md'), '# claude')
|
|
49
|
+
checkbox.mockResolvedValue([
|
|
50
|
+
path.join(tmpDir, 'AGENTS.md'),
|
|
51
|
+
path.join(tmpDir, 'CLAUDE.md'),
|
|
52
|
+
])
|
|
48
53
|
|
|
49
|
-
const { cleanAiFiles } = await import('
|
|
50
|
-
|
|
51
|
-
setTimeout(() => process.stdin.emit('data', '\n'), 10)
|
|
54
|
+
const { cleanAiFiles } = await import('./index.js')
|
|
52
55
|
|
|
53
56
|
await cleanAiFiles()
|
|
54
57
|
|
|
@@ -57,16 +60,28 @@ describe('cleanAiFiles()', () => {
|
|
|
57
60
|
expect(success).toHaveBeenCalledWith('Removed existing AI config files')
|
|
58
61
|
})
|
|
59
62
|
|
|
63
|
+
it('keeps unselected AI files', async () => {
|
|
64
|
+
await fse.writeFile(path.join(tmpDir, 'AGENTS.md'), '# agents')
|
|
65
|
+
await fse.writeFile(path.join(tmpDir, 'CLAUDE.md'), '# claude')
|
|
66
|
+
checkbox.mockResolvedValue([path.join(tmpDir, 'AGENTS.md')])
|
|
67
|
+
|
|
68
|
+
const { cleanAiFiles } = await import('./index.js')
|
|
69
|
+
|
|
70
|
+
await cleanAiFiles()
|
|
71
|
+
|
|
72
|
+
expect(await fse.pathExists(path.join(tmpDir, 'AGENTS.md'))).toBe(false)
|
|
73
|
+
expect(await fse.pathExists(path.join(tmpDir, 'CLAUDE.md'))).toBe(true)
|
|
74
|
+
})
|
|
75
|
+
|
|
60
76
|
it('removes .agents sub-entries but preserves .agents/skills', async () => {
|
|
61
77
|
const agentsDir = path.join(tmpDir, '.agents')
|
|
62
78
|
await fse.ensureDir(path.join(agentsDir, 'agents'))
|
|
63
79
|
await fse.ensureDir(path.join(agentsDir, 'skills', 'my-skill'))
|
|
64
80
|
await fse.writeFile(path.join(agentsDir, 'agents', 'front-engineer.md'), 'agent')
|
|
65
81
|
await fse.writeFile(path.join(agentsDir, 'skills', 'my-skill', 'SKILL.md'), 'skill')
|
|
82
|
+
checkbox.mockResolvedValue([path.join(agentsDir, 'agents')])
|
|
66
83
|
|
|
67
|
-
const { cleanAiFiles } = await import('
|
|
68
|
-
|
|
69
|
-
setTimeout(() => process.stdin.emit('data', '\n'), 10)
|
|
84
|
+
const { cleanAiFiles } = await import('./index.js')
|
|
70
85
|
|
|
71
86
|
await cleanAiFiles()
|
|
72
87
|
|
|
@@ -1,43 +1,34 @@
|
|
|
1
|
+
import { checkbox } from '@inquirer/prompts'
|
|
1
2
|
import fse from 'fs-extra'
|
|
2
3
|
import path from 'path'
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
4
|
+
import { fileURLToPath } from 'url'
|
|
5
|
+
import { findAiFiles } from '../../utils/copy.js'
|
|
6
|
+
import { header, info, success, warn } from '../../utils/exec.js'
|
|
5
7
|
|
|
6
|
-
|
|
7
|
-
const
|
|
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)
|
|
8
11
|
|
|
9
|
-
|
|
10
|
-
* Enumerate immediate children of a directory.
|
|
11
|
-
* Skips 'skills' to preserve user-installed skills.
|
|
12
|
-
*/
|
|
13
|
-
async function childrenExcludingSkills(dir) {
|
|
12
|
+
async function childrenExcludingPreserved(dir) {
|
|
14
13
|
const results = []
|
|
15
14
|
if (!await fse.pathExists(dir)) return results
|
|
16
15
|
const entries = await fse.readdir(dir)
|
|
17
16
|
for (const entry of entries) {
|
|
18
|
-
if (entry
|
|
17
|
+
if (cleanPreset.preserveSubfolders.includes(entry)) continue
|
|
19
18
|
results.push(path.join(dir, entry))
|
|
20
19
|
}
|
|
21
20
|
return results
|
|
22
21
|
}
|
|
23
22
|
|
|
24
|
-
/**
|
|
25
|
-
* Returns true if the file exists and has real content (not empty, not a prompt template).
|
|
26
|
-
* Prompt templates contain a specific marker written by the onboard CLI.
|
|
27
|
-
*/
|
|
28
23
|
async function isPopulated(filePath) {
|
|
29
24
|
if (!await fse.pathExists(filePath)) return false
|
|
30
25
|
const content = await fse.readFile(filePath, 'utf-8')
|
|
31
26
|
const trimmed = content.trim()
|
|
32
27
|
if (!trimmed) return false
|
|
33
|
-
// DESIGN.md and ARCHITECTURE.md shipped as prompt templates contain this marker
|
|
34
28
|
if (trimmed.startsWith('<!-- onboard-prompt')) return false
|
|
35
29
|
return true
|
|
36
30
|
}
|
|
37
31
|
|
|
38
|
-
/**
|
|
39
|
-
* Returns true if openspec/ exists and has at least one change or archive entry.
|
|
40
|
-
*/
|
|
41
32
|
async function hasOpenspecHistory(cwd) {
|
|
42
33
|
const changesDir = path.join(cwd, 'openspec', 'changes')
|
|
43
34
|
const archiveDir = path.join(cwd, 'openspec', 'archive')
|
|
@@ -53,11 +44,9 @@ async function hasOpenspecHistory(cwd) {
|
|
|
53
44
|
}
|
|
54
45
|
|
|
55
46
|
export async function cleanAiFiles() {
|
|
56
|
-
header('Step
|
|
47
|
+
header('Step 2, Existing AI config files')
|
|
57
48
|
|
|
58
49
|
const cwd = process.cwd()
|
|
59
|
-
|
|
60
|
-
// Detect what should be preserved before touching anything
|
|
61
50
|
const ctx = {
|
|
62
51
|
hasDesign: await isPopulated(path.join(cwd, 'DESIGN.md')),
|
|
63
52
|
hasArchitecture: await isPopulated(path.join(cwd, 'ARCHITECTURE.md')),
|
|
@@ -68,22 +57,19 @@ export async function cleanAiFiles() {
|
|
|
68
57
|
if (ctx.hasArchitecture) info('ARCHITECTURE.md exists and is populated, keeping it')
|
|
69
58
|
if (ctx.hasOpenspec) info('openspec/ history exists, keeping it')
|
|
70
59
|
|
|
71
|
-
|
|
72
|
-
const
|
|
73
|
-
const dirTargets = ['.opencode', '.agents']
|
|
60
|
+
const flatFiles = await findAiFiles(cwd, cleanPreset.detectFiles)
|
|
61
|
+
const dirTargets = cleanPreset.directoryTargets
|
|
74
62
|
const dirEntries = []
|
|
75
63
|
for (const dirName of dirTargets) {
|
|
76
64
|
const dirPath = path.join(cwd, dirName)
|
|
77
|
-
const children = await
|
|
65
|
+
const children = await childrenExcludingPreserved(dirPath)
|
|
78
66
|
dirEntries.push(...children)
|
|
79
67
|
}
|
|
80
68
|
|
|
81
|
-
// Remove directory targets themselves from flat list (handled via children)
|
|
82
|
-
// Also remove any preserved entries
|
|
83
69
|
const filteredFlat = flatFiles.filter(f => {
|
|
84
70
|
const rel = path.relative(cwd, f)
|
|
85
71
|
if (dirTargets.includes(rel)) return false
|
|
86
|
-
if (
|
|
72
|
+
if (cleanPreset.preserve.some(p => rel === p || rel.startsWith(p + path.sep))) return false
|
|
87
73
|
return true
|
|
88
74
|
})
|
|
89
75
|
|
|
@@ -94,8 +80,24 @@ export async function cleanAiFiles() {
|
|
|
94
80
|
return ctx
|
|
95
81
|
}
|
|
96
82
|
|
|
97
|
-
|
|
98
|
-
|
|
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) {
|
|
99
101
|
info(' ' + f.replace(cwd + path.sep, ''))
|
|
100
102
|
await fse.remove(f)
|
|
101
103
|
}
|