opencode-onboard 0.1.13 → 0.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-onboard",
3
- "version": "0.1.13",
3
+ "version": "0.2.3",
4
4
  "description": "Prepare any brownfield codebase for AI agent workflows using OpenCode, OpenSpec, and ensemble orchestration.",
5
5
  "keywords": [
6
6
  "opencode",
package/src/index.js CHANGED
@@ -5,11 +5,14 @@ import { checkEnv } from './steps/check-env.js'
5
5
  import { checkPlatform } from './steps/check-platform.js'
6
6
  import { checkRtk } from './steps/check-rtk.js'
7
7
  import { chooseModels } from './steps/choose-models.js'
8
- import { choosePlatform } from './steps/choose-platform.js'
9
- import { chooseSkillsProvider } from './steps/choose-skills-provider.js'
8
+ import { choosePlatform } from './steps/choose-platform.js'
9
+ import { chooseSourceScope } from './steps/choose-source-scope.js'
10
+ import { chooseSkillsProvider } from './steps/choose-skills-provider.js'
10
11
  import { cleanAiFiles } from './steps/clean-ai-files.js'
11
12
  import { copyContentStep } from './steps/copy-content.js'
12
13
  import { initOpenspec } from './steps/init-openspec.js'
14
+ import { patchAgentsMd } from './steps/patch-agents-md.js'
15
+ import { installBrowser } from './steps/install-browser.js'
13
16
 
14
17
  if (process.stdout.isTTY) console.clear()
15
18
  console.log()
@@ -57,45 +60,62 @@ try {
57
60
  // 1. Check Node + pnpm
58
61
  await checkEnv()
59
62
 
60
- // 2. Clean existing AI config files
61
- await cleanAiFiles()
62
-
63
- // 3. Choose platform
64
- const platform = await choosePlatform()
65
-
66
- // 4. Check platform CLI (az or gh)
67
- await checkPlatform(platform)
68
-
69
- // 5. Copy content
70
- await copyContentStep(platform)
71
-
72
- // 6. Init OpenSpec
73
- await initOpenspec()
74
-
75
- // 7. Install skills
76
- await chooseSkillsProvider()
77
-
78
- // 8. Choose models
79
- await chooseModels()
80
-
81
- // 9. Check RTK
82
- await checkRtk()
83
-
84
- // 10. Install opencode-browser
85
- await installBrowser()
63
+ // 2. Choose source code scope for init analysis
64
+ const scope = await chooseSourceScope()
65
+
66
+ // 3. Clean existing AI config files, detect preserved state
67
+ const preserve = await cleanAiFiles()
68
+ const ctx = { ...preserve, ...scope }
69
+
70
+ // 4. Choose platform
71
+ const platform = await choosePlatform()
72
+
73
+ // 5. Check platform CLI (az or gh)
74
+ await checkPlatform(platform)
75
+
76
+ // 6. Copy content
77
+ await copyContentStep(platform, ctx)
78
+
79
+ // 6b. Patch AGENTS.md to skip steps for already-existing files
80
+ await patchAgentsMd(ctx)
81
+
82
+ // 7. Init OpenSpec
83
+ await initOpenspec()
84
+
85
+ // 8. Install skills
86
+ await chooseSkillsProvider()
87
+
88
+ // 9. Choose models
89
+ await chooseModels()
90
+
91
+ // 10. Check RTK
92
+ await checkRtk()
93
+
94
+ // 11. Install opencode-browser
95
+ await installBrowser()
86
96
 
87
97
  // Done
98
+ const toGenerate = [
99
+ !ctx.hasDesign && 'DESIGN.md',
100
+ !ctx.hasArchitecture && 'ARCHITECTURE.md',
101
+ ].filter(Boolean)
102
+
88
103
  console.log()
89
104
  console.log(chalk.bold.green('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'))
90
105
  console.log(chalk.bold.green(' Onboarding complete!'))
91
106
  console.log(chalk.bold.green('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'))
92
107
  console.log()
93
- console.log(' Next step:')
94
- console.log(chalk.hex('#fe3d57')(' Open OpenCode in this project and type: ') + chalk.bold('"init"'))
95
- console.log()
96
- console.log(' OpenCode will generate ARCHITECTURE.md and DESIGN.md')
97
- console.log(' from your actual codebase, then activate the agent team.')
108
+ console.log(' Open this project in OpenCode and type:')
109
+ console.log(chalk.bold(' "init"'))
98
110
  console.log()
111
+ if (toGenerate.length > 0) {
112
+ console.log(` OpenCode will generate ${toGenerate.join(' and ')}`)
113
+ console.log(' from your actual codebase, then activate the agent team.')
114
+ } else {
115
+ console.log(' OpenCode will activate the agent team.')
116
+ }
117
+ console.log(` Source scope: ${ctx.sourceMode === 'parent-selected' ? ctx.sourceRoots.map(p => `../${p.split(/[/\\]/).pop()}`).join(', ') : 'current folder'}`)
118
+ console.log()
99
119
  } catch (err) {
100
120
  if (err.name === 'ExitPromptError') {
101
121
  console.log()
@@ -10,7 +10,7 @@ export async function checkPlatform(platform) {
10
10
  }
11
11
 
12
12
  async function checkAzure() {
13
- header('Step 4, Checking Azure DevOps CLI')
13
+ header('Step 5, Checking Azure DevOps CLI')
14
14
 
15
15
  // Check az is installed
16
16
  const hasAz = await commandExists('az')
@@ -51,7 +51,7 @@ async function checkAzure() {
51
51
  }
52
52
 
53
53
  async function checkGithub() {
54
- header('Step 4, Checking GitHub CLI')
54
+ header('Step 5, Checking GitHub CLI')
55
55
 
56
56
  const hasGh = await commandExists('gh')
57
57
 
@@ -1,7 +1,7 @@
1
1
  import { code, commandExists, header, info, success, warn } from '../utils/exec.js'
2
2
 
3
3
  export async function checkRtk() {
4
- header('Step 9, Checking rtk')
4
+ header('Step 10, Checking rtk')
5
5
 
6
6
  const available = await commandExists('rtk')
7
7
 
@@ -66,7 +66,7 @@ async function writeModelToAgent(agentFile, modelId) {
66
66
  }
67
67
 
68
68
  export async function chooseModels() {
69
- header('Step 8, Choose models')
69
+ header('Step 9, Choose models')
70
70
 
71
71
  info('Fetching available models from models.dev...')
72
72
  const { models: rawModels, source } = await fetchModels()
@@ -91,20 +91,21 @@ export async function chooseModels() {
91
91
  console.log()
92
92
 
93
93
  // Plan model
94
- info('PLAN model, used by the main agent for proposals, specs, architecture decisions.')
95
- info('Pick something capable with strong reasoning.')
94
+ info('PLAN model: used by the main agent to read issues, write proposals, coordinate the team.')
95
+ info('This model needs to be strong. Use Claude Sonnet/Opus, GPT-4o, o3, or equivalent.')
96
+ info('A weak model here will silently skip steps and break the workflow.')
96
97
  const planModel = await pickModel('Plan model:', models)
97
98
  console.log()
98
99
 
99
100
  // Build model
100
- info('BUILD model, used by front-engineer, back-engineer, infra-engineer, quality-engineer, security-auditor.')
101
- info('Pick something capable for implementation work.')
101
+ info('BUILD model: used by front-engineer, back-engineer, infra-engineer, quality-engineer, security-auditor.')
102
+ info('Needs to be capable for implementation work. Claude Sonnet, GPT-4o, or equivalent.')
102
103
  const buildModel = await pickModel('Build model:', models)
103
104
  console.log()
104
105
 
105
106
  // Fast model
106
- info('FAST model, used by devops-manager for reading issues, classifying PR comments.')
107
- info('Pick something fast and cheap, no heavy reasoning needed.')
107
+ info('FAST model: used by devops-manager for reading issues and classifying PR comments.')
108
+ info('Something fast and cheap is fine here, no heavy reasoning needed.')
108
109
  const fastModel = await pickModel('Fast model:', models)
109
110
  console.log()
110
111
 
@@ -10,7 +10,7 @@ const PLATFORMS_PRESET_PATH = path.resolve(__dirname, '../presets/platforms.json
10
10
  const platformsPreset = await fse.readJson(PLATFORMS_PRESET_PATH)
11
11
 
12
12
  export async function choosePlatform() {
13
- header('Step 3, Version control platform')
13
+ header('Step 4, Version control platform')
14
14
 
15
15
  const platform = await select({
16
16
  message: 'Which platform are you using?',
@@ -28,7 +28,7 @@ async function installObSkills() {
28
28
  }
29
29
 
30
30
  export async function chooseSkillsProvider() {
31
- header('Step 7, Installing skills')
31
+ header('Step 8, Installing skills')
32
32
 
33
33
  // ob-skills are always installed, mandatory
34
34
  info('Installing built-in ob-skills...')
@@ -0,0 +1,81 @@
1
+ import { checkbox, select } from '@inquirer/prompts'
2
+ import fse from 'fs-extra'
3
+ import path from 'path'
4
+ import { header, info, success, warn } from '../utils/exec.js'
5
+
6
+ async function listParentFolders(cwd) {
7
+ const parent = path.resolve(cwd, '..')
8
+ const entries = await fse.readdir(parent)
9
+ const dirs = []
10
+
11
+ for (const name of entries) {
12
+ if (name.startsWith('.')) continue
13
+ const abs = path.join(parent, name)
14
+ try {
15
+ const stat = await fse.stat(abs)
16
+ if (!stat.isDirectory()) continue
17
+ if (path.resolve(abs) === path.resolve(cwd)) continue
18
+ dirs.push({ name, abs })
19
+ } catch {
20
+ // ignore invalid entries
21
+ }
22
+ }
23
+
24
+ dirs.sort((a, b) => a.name.localeCompare(b.name))
25
+ return dirs
26
+ }
27
+
28
+ export async function chooseSourceScope() {
29
+ header('Step 2, Source code scope')
30
+
31
+ const cwd = process.cwd()
32
+ info('Choose where agents should read source code from during init analysis.')
33
+
34
+ const mode = await select({
35
+ message: 'Source code location:',
36
+ default: 'current',
37
+ choices: [
38
+ {
39
+ name: 'Current folder (default)',
40
+ value: 'current',
41
+ description: 'Use this repository only',
42
+ },
43
+ {
44
+ name: 'Select folders in parent (../)',
45
+ value: 'parent',
46
+ description: 'Use when this repo only contains agent config',
47
+ },
48
+ ],
49
+ })
50
+
51
+ if (mode === 'current') {
52
+ success(`Source scope: ${cwd}`)
53
+ return { sourceMode: 'current', sourceRoots: [cwd] }
54
+ }
55
+
56
+ const parentFolders = await listParentFolders(cwd)
57
+ if (parentFolders.length === 0) {
58
+ warn('No sibling folders found in parent directory. Falling back to current folder.')
59
+ success(`Source scope: ${cwd}`)
60
+ return { sourceMode: 'current', sourceRoots: [cwd] }
61
+ }
62
+
63
+ const selected = await checkbox({
64
+ message: 'Select source folders from parent directory:',
65
+ choices: parentFolders.map(d => ({
66
+ name: `../${d.name}`,
67
+ value: d.abs,
68
+ checked: false,
69
+ })),
70
+ required: true,
71
+ })
72
+
73
+ if (!selected || selected.length === 0) {
74
+ warn('No folders selected. Falling back to current folder.')
75
+ success(`Source scope: ${cwd}`)
76
+ return { sourceMode: 'current', sourceRoots: [cwd] }
77
+ }
78
+
79
+ success(`Source scope: ${selected.map(p => path.basename(p)).join(', ')}`)
80
+ return { sourceMode: 'parent-selected', sourceRoots: selected }
81
+ }
@@ -1,11 +1,14 @@
1
1
  import fse from 'fs-extra'
2
2
  import path from 'path'
3
3
  import { findAiFiles } from '../utils/copy.js'
4
- import { header, info, prompt, success, warn } from '../utils/exec.js'
4
+ import { header, info, success, warn } from '../utils/exec.js'
5
+
6
+ // Files/dirs that are valuable pre-existing work, never removed
7
+ const PRESERVE = ['DESIGN.md', 'ARCHITECTURE.md', 'openspec']
5
8
 
6
9
  /**
7
- * Enumerate immediate children of a directory, returning their absolute paths.
8
- * Skips any entry named 'skills' at any level to preserve user-installed skills.
10
+ * Enumerate immediate children of a directory.
11
+ * Skips 'skills' to preserve user-installed skills.
9
12
  */
10
13
  async function childrenExcludingSkills(dir) {
11
14
  const results = []
@@ -18,15 +21,55 @@ async function childrenExcludingSkills(dir) {
18
21
  return results
19
22
  }
20
23
 
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
+ async function isPopulated(filePath) {
29
+ if (!await fse.pathExists(filePath)) return false
30
+ const content = await fse.readFile(filePath, 'utf-8')
31
+ const trimmed = content.trim()
32
+ if (!trimmed) return false
33
+ // DESIGN.md and ARCHITECTURE.md shipped as prompt templates contain this marker
34
+ if (trimmed.startsWith('<!-- onboard-prompt')) return false
35
+ return true
36
+ }
37
+
38
+ /**
39
+ * Returns true if openspec/ exists and has at least one change or archive entry.
40
+ */
41
+ async function hasOpenspecHistory(cwd) {
42
+ const changesDir = path.join(cwd, 'openspec', 'changes')
43
+ const archiveDir = path.join(cwd, 'openspec', 'archive')
44
+ if (await fse.pathExists(changesDir)) {
45
+ const entries = await fse.readdir(changesDir)
46
+ if (entries.length > 0) return true
47
+ }
48
+ if (await fse.pathExists(archiveDir)) {
49
+ const entries = await fse.readdir(archiveDir)
50
+ if (entries.length > 0) return true
51
+ }
52
+ return false
53
+ }
54
+
21
55
  export async function cleanAiFiles() {
22
- header('Step 2, Existing AI config files')
56
+ header('Step 3, Existing AI config files')
23
57
 
24
58
  const cwd = process.cwd()
25
59
 
26
- // Flat AI config files (not directories)
27
- const flatFiles = await findAiFiles(cwd)
60
+ // Detect what should be preserved before touching anything
61
+ const ctx = {
62
+ hasDesign: await isPopulated(path.join(cwd, 'DESIGN.md')),
63
+ hasArchitecture: await isPopulated(path.join(cwd, 'ARCHITECTURE.md')),
64
+ hasOpenspec: await hasOpenspecHistory(cwd),
65
+ }
28
66
 
29
- // For directory targets (.opencode, .agents), enumerate children and skip skills/
67
+ if (ctx.hasDesign) info('DESIGN.md exists and is populated, keeping it')
68
+ if (ctx.hasArchitecture) info('ARCHITECTURE.md exists and is populated, keeping it')
69
+ if (ctx.hasOpenspec) info('openspec/ history exists, keeping it')
70
+
71
+ // Build the list of files to remove
72
+ const flatFiles = await findAiFiles(cwd)
30
73
  const dirTargets = ['.opencode', '.agents']
31
74
  const dirEntries = []
32
75
  for (const dirName of dirTargets) {
@@ -35,37 +78,28 @@ export async function cleanAiFiles() {
35
78
  dirEntries.push(...children)
36
79
  }
37
80
 
38
- // Remove the directory targets themselves from flat list (we handle them via children)
81
+ // Remove directory targets themselves from flat list (handled via children)
82
+ // Also remove any preserved entries
39
83
  const filteredFlat = flatFiles.filter(f => {
40
84
  const rel = path.relative(cwd, f)
41
- return !dirTargets.includes(rel)
85
+ if (dirTargets.includes(rel)) return false
86
+ if (PRESERVE.some(p => rel === p || rel.startsWith(p + path.sep))) return false
87
+ return true
42
88
  })
43
89
 
44
- const allFiles = [...filteredFlat, ...dirEntries]
90
+ const allToRemove = [...filteredFlat, ...dirEntries]
45
91
 
46
- if (allFiles.length === 0) {
47
- success('No existing AI config files found')
48
- return
92
+ if (allToRemove.length === 0) {
93
+ success('No existing AI config files to remove')
94
+ return ctx
49
95
  }
50
96
 
51
- warn('Found the following AI config files:')
52
- for (const f of allFiles) {
53
- info(f.replace(cwd, '.'))
54
- }
55
- console.log()
56
- prompt('Press Enter to remove them all (skills/ folders will be kept)')
57
- console.log()
58
-
59
- await new Promise(resolve => {
60
- process.stdin.resume()
61
- process.stdin.once('data', () => {
62
- process.stdin.pause()
63
- resolve()
64
- })
65
- })
66
-
67
- for (const f of allFiles) {
97
+ warn('Removing existing AI config files:')
98
+ for (const f of allToRemove) {
99
+ info(' ' + f.replace(cwd + path.sep, ''))
68
100
  await fse.remove(f)
69
101
  }
70
102
  success('Removed existing AI config files')
103
+
104
+ return ctx
71
105
  }
@@ -1,18 +1,25 @@
1
1
  import path from 'path'
2
2
  import { fileURLToPath } from 'url'
3
+ import fse from 'fs-extra'
3
4
  import { copyContent } from '../utils/copy.js'
4
5
  import { error, header, success } from '../utils/exec.js'
5
6
 
6
7
  const __dirname = path.dirname(fileURLToPath(import.meta.url))
7
8
  const CONTENT_DIR = path.resolve(__dirname, '../../content')
8
9
 
9
- export async function copyContentStep(platform) {
10
- header('Step 5, Copying opencode-onboard files')
10
+ export async function copyContentStep(platform, ctx = {}) {
11
+ header('Step 6, Copying opencode-onboard files')
11
12
 
12
13
  const dest = process.cwd()
13
14
 
14
15
  try {
15
- await copyContent(CONTENT_DIR, dest, platform)
16
+ await copyContent(CONTENT_DIR, dest, platform, ctx)
17
+ const rootsFile = path.join(dest, '.agents', 'source-roots.json')
18
+ await fse.ensureDir(path.dirname(rootsFile))
19
+ await fse.writeJson(rootsFile, {
20
+ mode: ctx.sourceMode || 'current',
21
+ roots: ctx.sourceRoots || [dest],
22
+ }, { spaces: 2 })
16
23
  success('Files copied to project root')
17
24
  } catch (err) {
18
25
  error(`Failed to copy content: ${err.message}`)