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/content/.agents/agents/back-engineer.md +10 -0
- package/content/.agents/agents/front-engineer.md +10 -0
- package/content/.agents/agents/quality-engineer.md +9 -0
- package/content/.opencode/commands/opsx-apply.md +170 -0
- package/content/.opencode/plugins/session-log.js +75 -6
- package/content/.opencode/skills/openspec-apply-change/SKILL.md +176 -0
- package/content/AGENTS.md +39 -25
- package/package.json +1 -1
- package/src/index.js +53 -33
- package/src/steps/check-platform.js +2 -2
- package/src/steps/check-rtk.js +1 -1
- package/src/steps/choose-models.js +8 -7
- package/src/steps/choose-platform.js +1 -1
- package/src/steps/choose-skills-provider.js +1 -1
- package/src/steps/choose-source-scope.js +81 -0
- package/src/steps/clean-ai-files.js +64 -30
- package/src/steps/copy-content.js +10 -3
- package/src/steps/init-openspec.js +84 -67
- package/src/steps/install-browser.js +1 -1
- package/src/steps/patch-agents-md.js +85 -0
- package/src/utils/copy.js +20 -6
package/package.json
CHANGED
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 {
|
|
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.
|
|
61
|
-
await
|
|
62
|
-
|
|
63
|
-
// 3.
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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('
|
|
94
|
-
console.log(chalk.
|
|
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
|
|
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
|
|
54
|
+
header('Step 5, Checking GitHub CLI')
|
|
55
55
|
|
|
56
56
|
const hasGh = await commandExists('gh')
|
|
57
57
|
|
package/src/steps/check-rtk.js
CHANGED
|
@@ -66,7 +66,7 @@ async function writeModelToAgent(agentFile, modelId) {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
export async function chooseModels() {
|
|
69
|
-
header('Step
|
|
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
|
|
95
|
-
info('
|
|
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
|
|
101
|
-
info('
|
|
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
|
|
107
|
-
info('
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
8
|
-
* Skips
|
|
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
|
|
56
|
+
header('Step 3, Existing AI config files')
|
|
23
57
|
|
|
24
58
|
const cwd = process.cwd()
|
|
25
59
|
|
|
26
|
-
//
|
|
27
|
-
const
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
90
|
+
const allToRemove = [...filteredFlat, ...dirEntries]
|
|
45
91
|
|
|
46
|
-
if (
|
|
47
|
-
success('No existing AI config files
|
|
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('
|
|
52
|
-
for (const f of
|
|
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
|
|
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}`)
|