opencode-onboard 0.4.1 → 0.4.3

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/content/AGENTS.md CHANGED
@@ -63,13 +63,30 @@ The output must be a real, populated `ARCHITECTURE.md` based on what you found i
63
63
 
64
64
  ---
65
65
 
66
- ### Step 4, Rewrite this file
66
+ ### Step 4, Populate OpenSpec config
67
+
68
+ Read `openspec/config.yaml`. It contains a template with commented-out examples. Fill in the `context:` field with real project information discovered during steps 1-3:
69
+
70
+ ```yaml
71
+ context: |
72
+ Tech stack: <languages, frameworks, libraries found in the codebase>
73
+ Build system: <build tools, package managers>
74
+ Architecture: <monolith, microservices, monorepo, etc.>
75
+ Conventions: <coding style, commit conventions, branching strategy if found>
76
+ Domain: <what this project does, in one line>
77
+ ```
78
+
79
+ Keep the `schema: spec-driven` line. Add `rules:` only if the codebase has clear conventions worth enforcing (e.g., max task size, proposal format). Do not invent rules that aren't evidenced by the codebase.
80
+
81
+ ---
82
+
83
+ ### Step 5, Rewrite this file
67
84
 
68
85
  Replace the entire contents of this file (`AGENTS.md`) with everything below the line `<!-- AGENTS-TEMPLATE-START -->` in this same file. Delete the bootstrap section and the template marker, the file should contain only the template content when done.
69
86
 
70
87
  ---
71
88
 
72
- ### Step 5, Confirm
89
+ ### Step 6, Confirm
73
90
 
74
91
  Tell the user:
75
92
 
@@ -80,6 +97,7 @@ Tell the user:
80
97
 
81
98
  - ARCHITECTURE.md generated
82
99
  - DESIGN.md generated
100
+ - openspec/config.yaml populated
83
101
  - Project history archived in openspec
84
102
  - AGENTS.md updated with real guidance
85
103
 
@@ -98,7 +116,7 @@ After restarting you are ready to work.
98
116
  - Do NOT create branches or PRs
99
117
  - Do NOT modify any project source files
100
118
  - Do NOT create CLI wrapper files or scripts
101
- - Only read source files for analysis, write only to ARCHITECTURE.md, DESIGN.md, AGENTS.md, and openspec/
119
+ - Only read source files for analysis, write only to ARCHITECTURE.md, DESIGN.md, AGENTS.md, openspec/config.yaml, and openspec/
102
120
 
103
121
  <!-- AGENTS-TEMPLATE-START -->
104
122
  # AGENTS.md
@@ -138,18 +156,25 @@ Trigger patterns, I recognize ALL of these, exact wording does not matter:
138
156
 
139
157
  **Never delegate without a plan. Never write implementation code directly, always spawn specialists, no exceptions. "Small feature", "faster to do it directly", or "environment issues" are not valid reasons to skip ensemble.**
140
158
 
141
- ## Multi-Agent Execution, opencode-ensemble
142
-
143
- Parallel execution uses the `opencode-ensemble` plugin (`team_create`, `team_spawn`, etc.).
144
- Works on **all platforms** (Windows, macOS, Linux) via OpenCode's built-in worktree support.
145
-
146
- Core tools used in this workflow:
147
- - `team_create`, `team_spawn`, `team_shutdown`, `team_merge`, `team_cleanup`
148
- - `team_tasks_add`, `team_tasks_list`, `team_claim`, `team_tasks_complete`
149
- - `team_message`, `team_results`, `team_status`
159
+ ## Multi-Agent Execution, opencode-ensemble
160
+
161
+ Parallel execution uses the `opencode-ensemble` plugin (`team_create`, `team_spawn`, etc.).
162
+ Works on **all platforms** (Windows, macOS, Linux) via OpenCode's built-in worktree support.
163
+
164
+ Core tools used in this workflow:
165
+ - `team_create`, `team_spawn`, `team_shutdown`, `team_merge`, `team_cleanup`
166
+ - `team_tasks_add`, `team_tasks_list`, `team_claim`, `team_tasks_complete`
167
+ - `team_message`, `team_results`, `team_status`
150
168
 
151
169
  **Dashboard**: Monitor running agents at **http://localhost:4747/**
152
170
 
171
+ **Hard limits:**
172
+ - **Max {{MAX_CONCURRENT_AGENTS}} truly concurrent agents.** All {{MAX_CONCURRENT_AGENTS}} must be spawned and running simultaneously, not sequentially. Spawn in waves if more than {{MAX_CONCURRENT_AGENTS}} are needed. Wait for wave N to finish before spawning wave N+1.
173
+ - **Non-overlapping file domains.** Each agent owns exclusive directories. Two agents must NEVER touch the same file.
174
+ - **Immediate shutdown on completion.** The moment an agent's domain has no more pending tasks → `team_shutdown` → `team_merge`. Keep agents alive if more tasks in their domain are pending (rolling batch).
175
+ - **Rolling batch assignment.** Agents receive up to 3 tasks initially. When they complete a batch, lead assigns the next batch of up to 3 from the board. Never leave pending tasks orphaned.
176
+ - **Stall detection at 5 minutes.** No commits after 5 min → nudge message → 2 min grace → force shutdown + respawn.
177
+
153
178
  **Progress inspection commands (tell user explicitly after spawning):**
154
179
  - `team_status` for live team snapshot
155
180
  - `team_tasks_list` for task board state
@@ -159,7 +184,7 @@ Core tools used in this workflow:
159
184
  If a teammate stalls due to model quota/rate-limit exhaustion:
160
185
  1. `team_shutdown name:"<stuck-member>" force:true`
161
186
  2. `team_spawn` same member/task with an available model
162
- 3. `team_message` start instruction with the exact next task ID
187
+ 3. `team_message` start instruction with the exact next task ID
163
188
 
164
189
  ---
165
190
 
@@ -191,15 +216,16 @@ devops-manager (ship mode)
191
216
  5. STOP. Ask user: "Ready to implement? (yes/no)", DO NOT proceed until confirmed.
192
217
  ```
193
218
 
194
- ### Phase 2, Implement
195
-
196
- ```
197
- 1. Run /opsx-apply.
198
- - Lead adds all tasks to board.
199
- - Lead spawns one or more engineers (`basic-engineer` and/or custom engineers) in parallel where safe.
200
- - Each engineer must claim task IDs, load relevant abilities, implement, and complete tasks.
201
- - Lead merges each engineer branch after shutdown, then marks tasks done in tasks.md.
202
- 2. Verify with tests/build/lint according to task scope.
219
+ ### Phase 2, Implement
220
+
221
+ ```
222
+ 1. Run /opsx-apply.
223
+ - Lead adds all tasks to board.
224
+ - Lead spawns engineers with initial batch of up to 3 tasks each (rolling batch model).
225
+ - Each engineer claims tasks, implements, completes, messages lead.
226
+ - Lead assigns next batch (up to 3) to agents that report done. Repeat until board empty.
227
+ - Lead merges each engineer branch after shutdown, then marks tasks done in tasks.md.
228
+ 2. Verify with tests/build/lint according to task scope.
203
229
  ```
204
230
 
205
231
  ### Phase 3, Ship
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-onboard",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
4
4
  "description": "Prepare any brownfield codebase for AI agent workflows using OpenCode, OpenSpec, and ensemble orchestration.",
5
5
  "keywords": [
6
6
  "opencode",
@@ -0,0 +1,43 @@
1
+ import chalk from 'chalk'
2
+ import { installBrowser } from '../steps/browser/index.js'
3
+ import { checkRtk } from '../steps/optimization/index.js'
4
+ import { checkPlatform, choosePlatform } from '../steps/platform/index.js'
5
+ import { header, info } from '../utils/exec.js'
6
+ import { readOnboardConfig } from './shared.js'
7
+
8
+ export async function runJoin() {
9
+ const logo = chalk.hex('#fe3d57')
10
+ console.log()
11
+ console.log(logo(' 🤝 opencode-onboard join'))
12
+ console.log(chalk.dim(' New team member setup, checks & local installs only'))
13
+ console.log(chalk.dim(' This will NOT modify any project files.'))
14
+ console.log()
15
+
16
+ // Step 1: Platform CLI check
17
+ header('Step 1, Platform CLI check')
18
+ const saved = await readOnboardConfig()
19
+ const savedPlatform = saved?.wizard?.platform
20
+ if (savedPlatform) {
21
+ info(`Detected project platform: ${savedPlatform === 'github' ? 'GitHub' : 'Azure DevOps'}`)
22
+ await checkPlatform(savedPlatform)
23
+ } else {
24
+ const platform = await choosePlatform()
25
+ void platform // result not persisted in join mode
26
+ }
27
+
28
+ // Step 2: rtk check
29
+ header('Step 2, Checking rtk')
30
+ await checkRtk({ skipHeader: true })
31
+
32
+ // Step 3: Browser extension
33
+ await installBrowser()
34
+
35
+ console.log()
36
+ console.log(chalk.bold.green('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'))
37
+ console.log(chalk.bold.green(' Join setup complete!'))
38
+ console.log(chalk.bold.green('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'))
39
+ console.log()
40
+ console.log(' Your local environment is ready.')
41
+ console.log(' Open the project in OpenCode and start coding!')
42
+ console.log()
43
+ }
@@ -0,0 +1,12 @@
1
+ import fse from 'fs-extra'
2
+ import path from 'node:path'
3
+
4
+ export async function readOnboardConfig() {
5
+ const cfgPath = path.join(process.cwd(), '.opencode', 'opencode-onboard.json')
6
+ if (!await fse.pathExists(cfgPath)) return null
7
+ try {
8
+ return await fse.readJson(cfgPath)
9
+ } catch {
10
+ return null
11
+ }
12
+ }
@@ -0,0 +1,56 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest'
2
+ import fs from 'node:fs'
3
+ import path from 'node:path'
4
+ import os from 'node:os'
5
+ import { readOnboardConfig } from './shared.js'
6
+
7
+ describe('readOnboardConfig()', () => {
8
+ let tmpDir, originalCwd
9
+
10
+ beforeEach(() => {
11
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'shared-test-'))
12
+ originalCwd = process.cwd()
13
+ process.chdir(tmpDir)
14
+ })
15
+
16
+ afterEach(() => {
17
+ process.chdir(originalCwd)
18
+ fs.rmSync(tmpDir, { recursive: true, force: true })
19
+ })
20
+
21
+ it('returns null when config file does not exist', async () => {
22
+ const result = await readOnboardConfig()
23
+
24
+ expect(result).toBeNull()
25
+ })
26
+
27
+ it('returns parsed config when file exists', async () => {
28
+ const configDir = path.join(tmpDir, '.opencode')
29
+ fs.mkdirSync(configDir)
30
+ fs.writeFileSync(
31
+ path.join(configDir, 'opencode-onboard.json'),
32
+ JSON.stringify({ schema: 1, wizard: { platform: 'github' } }),
33
+ 'utf-8'
34
+ )
35
+
36
+ const result = await readOnboardConfig()
37
+
38
+ expect(result).not.toBeNull()
39
+ expect(result.schema).toBe(1)
40
+ expect(result.wizard.platform).toBe('github')
41
+ })
42
+
43
+ it('returns null when file contains invalid JSON', async () => {
44
+ const configDir = path.join(tmpDir, '.opencode')
45
+ fs.mkdirSync(configDir)
46
+ fs.writeFileSync(
47
+ path.join(configDir, 'opencode-onboard.json'),
48
+ 'not valid json',
49
+ 'utf-8'
50
+ )
51
+
52
+ const result = await readOnboardConfig()
53
+
54
+ expect(result).toBeNull()
55
+ })
56
+ })
@@ -0,0 +1,66 @@
1
+ import { cleanAiFiles } from '../steps/clean/index.js'
2
+ import { copyContentStep } from '../steps/copy/index.js'
3
+ import { chooseModels } from '../steps/models/index.js'
4
+ import { initOpenspec } from '../steps/openspec/index.js'
5
+ import { tokenOptimizationStep } from '../steps/optimization/index.js'
6
+ import { choosePlatform } from '../steps/platform/index.js'
7
+ import { installBrowser } from '../steps/browser/index.js'
8
+ import { writeOnboardConfig } from '../steps/metadata/index.js'
9
+ import { readOnboardConfig } from './shared.js'
10
+
11
+ export async function runSingleCommand(command) {
12
+ const saved = await readOnboardConfig()
13
+ const savedWizard = saved?.wizard ?? {}
14
+ const ctx = {
15
+ hasDesign: !!savedWizard?.preserved?.design,
16
+ hasArchitecture: !!savedWizard?.preserved?.architecture,
17
+ hasOpenspec: !!savedWizard?.preserved?.openspec,
18
+ sourceMode: savedWizard?.sourceMode ?? 'current',
19
+ sourceRoots: Array.isArray(savedWizard?.sourceRoots) ? savedWizard.sourceRoots : [],
20
+ maxConcurrentAgents: savedWizard?.maxConcurrentAgents ?? 4,
21
+ }
22
+ const platform = savedWizard?.platform
23
+ const resolvedPlatform = platform === 'azure' || platform === 'github' ? platform : 'github'
24
+
25
+ const handlers = {
26
+ clean: async () => {
27
+ await cleanAiFiles()
28
+ },
29
+ platform: async () => {
30
+ await choosePlatform()
31
+ },
32
+ copy: async () => {
33
+ await copyContentStep(resolvedPlatform, ctx)
34
+ },
35
+ openspec: async () => {
36
+ await initOpenspec()
37
+ },
38
+ models: async () => {
39
+ await chooseModels()
40
+ },
41
+ optimization: async () => {
42
+ await tokenOptimizationStep({ ctx })
43
+ },
44
+ browser: async () => {
45
+ await installBrowser()
46
+ },
47
+ metadata: async () => {
48
+ await writeOnboardConfig({
49
+ ...ctx,
50
+ platform: resolvedPlatform,
51
+ maxConcurrentAgents: savedWizard?.maxConcurrentAgents ?? 4,
52
+ additionalSkillsProvider: 'npx-skills',
53
+ planModel: savedWizard?.models?.plan ?? null,
54
+ buildModel: savedWizard?.models?.build ?? null,
55
+ fastModel: savedWizard?.models?.fast ?? null,
56
+ optionalTools: savedWizard?.optionalTools ?? null,
57
+ cavemanGuidance: savedWizard?.cavemanGuidance ?? null,
58
+ })
59
+ },
60
+ }
61
+
62
+ const handler = handlers[command]
63
+ if (!handler) return false
64
+ await handler()
65
+ return true
66
+ }
@@ -0,0 +1,113 @@
1
+ import { select as wizardSelect } from '@inquirer/prompts'
2
+ import chalk from 'chalk'
3
+ import { chooseSourceScope } from '../steps/source/index.js'
4
+ import { cleanAiFiles } from '../steps/clean/index.js'
5
+ import { choosePlatform } from '../steps/platform/index.js'
6
+ import { copyContentStep } from '../steps/copy/index.js'
7
+ import { initOpenspec } from '../steps/openspec/index.js'
8
+ import { chooseModels } from '../steps/models/index.js'
9
+ import { tokenOptimizationStep } from '../steps/optimization/index.js'
10
+ import { installBrowser } from '../steps/browser/index.js'
11
+ import { writeOnboardConfig } from '../steps/metadata/index.js'
12
+
13
+ export async function runWizard(version) {
14
+ const logo = chalk.hex('#fe3d57')
15
+ const bannerLines = [
16
+ logo(' '),
17
+ logo(' ▒▒▒▒▒▒▒▒▒▒▒▒▒ '),
18
+ logo(' ▒▒▓ ▓▒▓ '),
19
+ logo(' ▒▒▒▒▒▒▓▒▒▒▒▒▒▒▒▒▓▓▒▒▒▒▒ '),
20
+ logo(' ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓ '),
21
+ logo(' ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓ '),
22
+ logo(' ▓▒▒▒▒░░░▒▒▒▒▒▒▒▒▒▒▒░░░▒▒▒▓▓ '),
23
+ logo(' ▓▓▓▓▒▒▒▓▓▓▓▓▓▓▓▓▓▓▒▒▒▓▓▓▓ '),
24
+ logo(' ▓▓▒▒▒▒▒▒░▒▒▒▒▒▒▒░▒▒▒▒▒▒▓▓ '),
25
+ logo(' ▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓ '),
26
+ logo(' ▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓ '),
27
+ logo(' ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ '),
28
+ '',
29
+ chalk.bold(' 🧰 opencode-onboard') + chalk.dim(` v${version}`),
30
+ chalk.dim(' Prepare your codebase for AI agents'),
31
+ ]
32
+
33
+ for (const line of bannerLines) console.log(line)
34
+ console.log()
35
+ console.log(' This tool will set up your project with a team of AI agents,')
36
+ console.log(' install skills, select models, and configure OpenCode.')
37
+ console.log()
38
+
39
+ // Only wait for Enter in a real interactive TTY
40
+ if (process.stdin.isTTY) {
41
+ console.log(chalk.bold(' Press Enter to begin...'))
42
+ console.log()
43
+ await new Promise(resolve => {
44
+ process.stdin.resume()
45
+ process.stdin.once('data', () => {
46
+ process.stdin.pause()
47
+ resolve()
48
+ })
49
+ })
50
+ }
51
+
52
+ const scope = await chooseSourceScope()
53
+
54
+ const maxConcurrentAgents = await wizardSelect({
55
+ message: 'Max concurrent agents:',
56
+ default: 4,
57
+ choices: [
58
+ { name: '2', value: 2, description: 'Conservative — lower resource usage' },
59
+ { name: '3', value: 3, description: 'Moderate parallelism' },
60
+ { name: '4 (default)', value: 4, description: 'Recommended for most projects' },
61
+ { name: '5', value: 5, description: 'High parallelism — requires more resources' },
62
+ { name: '6', value: 6, description: 'Maximum parallelism' },
63
+ ],
64
+ })
65
+
66
+ const preserve = await cleanAiFiles()
67
+ const ctx = { ...preserve, ...scope, maxConcurrentAgents }
68
+
69
+ const platform = await choosePlatform()
70
+
71
+ await copyContentStep(platform, ctx)
72
+
73
+ await initOpenspec()
74
+
75
+ const selectedModels = await chooseModels()
76
+
77
+ const tokenOpt = await tokenOptimizationStep({ ctx })
78
+ const { rtk, quota, caveman, cavemanGuidance } = tokenOpt
79
+
80
+ await installBrowser()
81
+
82
+ await writeOnboardConfig({
83
+ ...ctx,
84
+ platform,
85
+ maxConcurrentAgents,
86
+ additionalSkillsProvider: 'npx-skills',
87
+ ...selectedModels,
88
+ optionalTools: { rtk, quota, caveman },
89
+ cavemanGuidance,
90
+ })
91
+
92
+ const toGenerate = [
93
+ !ctx.hasDesign && 'DESIGN.md',
94
+ !ctx.hasArchitecture && 'ARCHITECTURE.md',
95
+ ].filter(Boolean)
96
+
97
+ console.log()
98
+ console.log(chalk.bold.green('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'))
99
+ console.log(chalk.bold.green(' Onboarding complete!'))
100
+ console.log(chalk.bold.green('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'))
101
+ console.log()
102
+ console.log(' Open this project in OpenCode and type:')
103
+ console.log(chalk.bold(' "init"'))
104
+ console.log()
105
+ if (toGenerate.length > 0) {
106
+ console.log(` OpenCode will generate ${toGenerate.join(' and ')}`)
107
+ console.log(' from your actual codebase, then activate the agent team.')
108
+ } else {
109
+ console.log(' OpenCode will activate the agent team.')
110
+ }
111
+ console.log(` Source scope: ${ctx.sourceMode === 'parent-selected' ? ctx.sourceRoots.map(p => `../${p.split(/[/\\]/).pop()}`).join(', ') : 'current folder'}`)
112
+ console.log()
113
+ }
package/src/index.js CHANGED
@@ -1,17 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import chalk from 'chalk'
3
- import fse from 'fs-extra'
4
3
  import { createRequire } from 'node:module'
5
- import path from 'node:path'
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'
4
+ import { runJoin } from './commands/join.js'
5
+ import { runSingleCommand } from './commands/single.js'
6
+ import { runWizard } from './commands/wizard.js'
15
7
 
16
8
  function printHelp(version) {
17
9
  console.log(`opencode-onboard v${version}`)
@@ -21,6 +13,7 @@ function printHelp(version) {
21
13
  console.log(' npx opencode-onboard <command> Run a single step command')
22
14
  console.log()
23
15
  console.log('Commands:')
16
+ console.log(' join New team member setup (checks & local installs only)')
24
17
  console.log(' clean Run AI files cleanup step')
25
18
  console.log(' platform Run platform selection step')
26
19
  console.log(' copy Run content copy step')
@@ -34,71 +27,6 @@ function printHelp(version) {
34
27
  console.log(' -h, --help Show this help message')
35
28
  }
36
29
 
37
- async function readOnboardConfig() {
38
- const cfgPath = path.join(process.cwd(), '.opencode', 'opencode-onboard.json')
39
- if (!await fse.pathExists(cfgPath)) return null
40
- try {
41
- return await fse.readJson(cfgPath)
42
- } catch {
43
- return null
44
- }
45
- }
46
-
47
- async function runSingleCommand(command) {
48
- const saved = await readOnboardConfig()
49
- const savedWizard = saved?.wizard ?? {}
50
- const ctx = {
51
- hasDesign: !!savedWizard?.preserved?.design,
52
- hasArchitecture: !!savedWizard?.preserved?.architecture,
53
- hasOpenspec: !!savedWizard?.preserved?.openspec,
54
- sourceMode: savedWizard?.sourceMode ?? 'current',
55
- sourceRoots: Array.isArray(savedWizard?.sourceRoots) ? savedWizard.sourceRoots : [],
56
- }
57
- const platform = savedWizard?.platform
58
- const resolvedPlatform = platform === 'azure' || platform === 'github' ? platform : 'github'
59
-
60
- const handlers = {
61
- clean: async () => {
62
- await cleanAiFiles()
63
- },
64
- platform: async () => {
65
- await choosePlatform()
66
- },
67
- copy: async () => {
68
- await copyContentStep(resolvedPlatform, ctx)
69
- },
70
- openspec: async () => {
71
- await initOpenspec()
72
- },
73
- models: async () => {
74
- await chooseModels()
75
- },
76
- optimization: async () => {
77
- await tokenOptimizationStep({ ctx })
78
- },
79
- browser: async () => {
80
- await installBrowser()
81
- },
82
- metadata: async () => {
83
- await writeOnboardConfig({
84
- ...ctx,
85
- platform: resolvedPlatform,
86
- additionalSkillsProvider: 'npx-skills',
87
- planModel: savedWizard?.models?.plan ?? null,
88
- buildModel: savedWizard?.models?.build ?? null,
89
- fastModel: savedWizard?.models?.fast ?? null,
90
- optionalTools: savedWizard?.optionalTools ?? null,
91
- cavemanGuidance: savedWizard?.cavemanGuidance ?? null,
92
- })
93
- },
94
- }
95
-
96
- const handler = handlers[command]
97
- if (!handler) return false
98
- await handler()
99
- return true
100
- }
101
-
102
30
  if (process.stdout.isTTY) console.clear()
103
31
  console.log()
104
32
  const require = createRequire(import.meta.url)
@@ -111,103 +39,32 @@ if (args.includes('-h') || args.includes('--help')) {
111
39
  }
112
40
 
113
41
  if (args.length > 0) {
114
- const ok = await runSingleCommand(args[0])
115
- if (!ok) {
116
- console.log(chalk.red(`Unknown command: ${args[0]}`))
117
- console.log()
118
- printHelp(version)
119
- process.exit(1)
42
+ try {
43
+ if (args[0] === 'join') {
44
+ await runJoin()
45
+ } else {
46
+ const ok = await runSingleCommand(args[0])
47
+ if (!ok) {
48
+ console.log(chalk.red(`Unknown command: ${args[0]}`))
49
+ console.log()
50
+ printHelp(version)
51
+ process.exit(1)
52
+ }
53
+ }
54
+ } catch (err) {
55
+ if (err.name === 'ExitPromptError') {
56
+ console.log()
57
+ console.log(chalk.yellow('Cancelled.'))
58
+ } else {
59
+ console.error(chalk.red('\nUnexpected error:'), err.message)
60
+ process.exit(1)
61
+ }
120
62
  }
121
63
  process.exit(0)
122
64
  }
123
65
 
124
- const logo = chalk.hex('#fe3d57')
125
- const bannerLines = [
126
- logo(' '),
127
- logo(' ▒▒▒▒▒▒▒▒▒▒▒▒▒ '),
128
- logo(' ▒▒▓ ▓▒▓ '),
129
- logo(' ▒▒▒▒▒▒▓▒▒▒▒▒▒▒▒▒▓▓▒▒▒▒▒ '),
130
- logo(' ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓ '),
131
- logo(' ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓ '),
132
- logo(' ▓▒▒▒▒░░░▒▒▒▒▒▒▒▒▒▒▒░░░▒▒▒▓▓ '),
133
- logo(' ▓▓▓▓▒▒▒▓▓▓▓▓▓▓▓▓▓▓▒▒▒▓▓▓▓ '),
134
- logo(' ▓▓▒▒▒▒▒▒░▒▒▒▒▒▒▒░▒▒▒▒▒▒▓▓ '),
135
- logo(' ▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓ '),
136
- logo(' ▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓ '),
137
- logo(' ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ '),
138
- '',
139
- chalk.bold(' 🧰 opencode-onboard') + chalk.dim(` v${version}`),
140
- chalk.dim(' Prepare your codebase for AI agents'),
141
- ]
142
-
143
- for (const line of bannerLines) console.log(line)
144
- console.log()
145
- console.log(' This tool will set up your project with a team of AI agents,')
146
- console.log(' install skills, select models, and configure OpenCode.')
147
- console.log()
148
-
149
- // Only wait for Enter in a real interactive TTY
150
- if (process.stdin.isTTY) {
151
- console.log(chalk.bold(' Press Enter to begin...'))
152
- console.log()
153
- await new Promise(resolve => {
154
- process.stdin.resume()
155
- process.stdin.once('data', () => {
156
- process.stdin.pause()
157
- resolve()
158
- })
159
- })
160
- }
161
-
162
66
  try {
163
- const scope = await chooseSourceScope()
164
-
165
- const preserve = await cleanAiFiles()
166
- const ctx = { ...preserve, ...scope }
167
-
168
- const platform = await choosePlatform()
169
-
170
- await copyContentStep(platform, ctx)
171
-
172
- await initOpenspec()
173
-
174
- const selectedModels = await chooseModels()
175
-
176
- const tokenOpt = await tokenOptimizationStep({ ctx })
177
- const { rtk, quota, caveman, cavemanGuidance } = tokenOpt
178
-
179
- await installBrowser()
180
-
181
- await writeOnboardConfig({
182
- ...ctx,
183
- platform,
184
- additionalSkillsProvider: 'npx-skills',
185
- ...selectedModels,
186
- optionalTools: { rtk, quota, caveman },
187
- cavemanGuidance,
188
- })
189
-
190
- const toGenerate = [
191
- !ctx.hasDesign && 'DESIGN.md',
192
- !ctx.hasArchitecture && 'ARCHITECTURE.md',
193
- ].filter(Boolean)
194
-
195
- console.log()
196
- console.log(chalk.bold.green('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'))
197
- console.log(chalk.bold.green(' Onboarding complete!'))
198
- console.log(chalk.bold.green('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'))
199
- console.log()
200
- console.log(' Open this project in OpenCode and type:')
201
- console.log(chalk.bold(' "init"'))
202
- console.log()
203
- if (toGenerate.length > 0) {
204
- console.log(` OpenCode will generate ${toGenerate.join(' and ')}`)
205
- console.log(' from your actual codebase, then activate the agent team.')
206
- } else {
207
- console.log(' OpenCode will activate the agent team.')
208
- }
209
- console.log(` Source scope: ${ctx.sourceMode === 'parent-selected' ? ctx.sourceRoots.map(p => `../${p.split(/[/\\]/).pop()}`).join(', ') : 'current folder'}`)
210
- console.log()
67
+ await runWizard(version)
211
68
  } catch (err) {
212
69
  if (err.name === 'ExitPromptError') {
213
70
  console.log()
@@ -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
  }
@@ -44,7 +44,7 @@ describe('installBrowser()', () => {
44
44
 
45
45
  await installBrowser()
46
46
 
47
- expect(execa).toHaveBeenCalledWith('npx', expect.arrayContaining('@different-ai/opencode-browser'), expect.any(Object))
47
+ expect(execa).toHaveBeenCalledWith('npx', expect.arrayContaining(['@different-ai/opencode-browser']), expect.any(Object))
48
48
  })
49
49
 
50
50
  it('logs success when exit code is 0', async () => {