opencode-onboard 0.3.1 → 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.
Files changed (79) hide show
  1. package/README.md +266 -214
  2. package/content/.agents/agents/basic-engineer.md +30 -0
  3. package/content/.agents/agents/devops-manager.md +38 -29
  4. package/content/.agents/session-log.json +41 -0
  5. package/content/.agents/skills/ob-default/SKILL.md +21 -0
  6. package/content/.agents/skills/ob-generic-guardrails/SKILL.md +32 -0
  7. package/content/.agents/skills/ob-global/SKILL.md +49 -0
  8. package/content/.agents/skills/ob-pullrequest-az/SKILL.md +11 -21
  9. package/content/.agents/skills/ob-pullrequest-gh/SKILL.md +14 -24
  10. package/content/.agents/skills/ob-userstory-az/SKILL.md +8 -14
  11. package/content/.agents/skills/ob-userstory-gh/SKILL.md +6 -14
  12. package/content/.opencode/commands/opsx-apply.md +50 -33
  13. package/content/.opencode/opencode.json +3 -3
  14. package/content/.opencode/plugins/session-log.js +1 -1
  15. package/content/.opencode/skills/openspec-apply-change/SKILL.md +50 -33
  16. package/content/AGENTS.md +95 -141
  17. package/content/skills-lock.json +4 -0
  18. package/package.json +6 -1
  19. package/src/index.js +112 -191
  20. package/src/presets/browser.json +18 -0
  21. package/src/presets/clean.json +21 -0
  22. package/src/presets/models.json +33 -0
  23. package/src/presets/optimization.json +22 -0
  24. package/src/presets/platforms.json +29 -2
  25. package/src/presets/quota.json +14 -0
  26. package/src/presets/source.json +17 -0
  27. package/src/steps/browser/browser.test.js +81 -0
  28. package/src/steps/{install-browser.js → browser/index.js} +12 -15
  29. package/src/steps/{__tests__/clean-ai-files.test.js → clean/clean.test.js} +28 -13
  30. package/src/steps/{clean-ai-files.js → clean/index.js} +32 -30
  31. package/src/steps/{patch-agents-md.js → copy/agents.js} +41 -20
  32. package/src/steps/{__tests__/copy-content.test.js → copy/copy.test.js} +10 -1
  33. package/src/steps/copy/index.js +33 -0
  34. package/src/steps/copy/skills.js +55 -0
  35. package/src/steps/{write-onboard-config.js → metadata/index.js} +3 -3
  36. package/src/steps/metadata/metadata.test.js +96 -0
  37. package/src/steps/models/format.js +60 -0
  38. package/src/steps/models/format.test.js +74 -0
  39. package/src/steps/models/index.js +52 -0
  40. package/src/steps/models/write.js +54 -0
  41. package/src/steps/models/write.test.js +119 -0
  42. package/src/steps/{init-openspec.js → openspec/ensemble.js} +27 -61
  43. package/src/steps/openspec/ensemble.test.js +79 -0
  44. package/src/steps/openspec/index.js +32 -0
  45. package/src/steps/optimization/caveman-guidance.js +11 -0
  46. package/src/steps/{install-caveman.js → optimization/caveman.js} +5 -19
  47. package/src/steps/optimization/global.js +64 -0
  48. package/src/steps/optimization/index.js +101 -0
  49. package/src/steps/{__tests__/token-optimization.test.js → optimization/optimization.test.js} +19 -24
  50. package/src/steps/{install-quota.js → optimization/quota.js} +12 -10
  51. package/src/steps/platform/index.js +81 -0
  52. package/src/steps/platform/platform.test.js +129 -0
  53. package/src/steps/{choose-source-scope.js → source/index.js} +11 -17
  54. package/src/steps/source/source.test.js +89 -0
  55. package/src/utils/__tests__/copy.test.js +12 -5
  56. package/src/utils/copy.js +4 -24
  57. package/src/utils/exec-spinner.js +47 -0
  58. package/src/utils/exec.js +120 -162
  59. package/src/utils/models-cache.js +25 -68
  60. package/src/utils/models-pricing.js +42 -0
  61. package/src/utils/models-pricing.test.js +94 -0
  62. package/content/.agents/agents/back-engineer.md +0 -87
  63. package/content/.agents/agents/front-engineer.md +0 -86
  64. package/content/.agents/agents/infra-engineer.md +0 -85
  65. package/content/.agents/agents/quality-engineer.md +0 -86
  66. package/content/.agents/agents/security-auditor.md +0 -86
  67. package/src/steps/__tests__/check-env.test.js +0 -70
  68. package/src/steps/__tests__/check-platform.test.js +0 -104
  69. package/src/steps/__tests__/check-rtk.test.js +0 -38
  70. package/src/steps/__tests__/choose-platform.test.js +0 -38
  71. package/src/steps/check-env.js +0 -26
  72. package/src/steps/check-platform.js +0 -80
  73. package/src/steps/check-rtk.js +0 -38
  74. package/src/steps/choose-models.js +0 -163
  75. package/src/steps/choose-platform.js +0 -22
  76. package/src/steps/choose-skills-provider.js +0 -79
  77. package/src/steps/copy-content.js +0 -89
  78. package/src/steps/enable-caveman-guidance.js +0 -93
  79. package/src/steps/token-optimization.js +0 -59
@@ -1,26 +0,0 @@
1
- import { commandExists, error, header, success } from '../utils/exec.js'
2
-
3
- export async function checkEnv() {
4
- header('Step 1, Checking environment')
5
-
6
- // Node version
7
- const nodeVersion = process.version
8
- const major = parseInt(nodeVersion.slice(1).split('.')[0], 10)
9
- if (major < 18) {
10
- error(`Node.js ${nodeVersion} detected. Version 18+ is required.`)
11
- process.exit(1)
12
- }
13
- success(`Node.js ${nodeVersion}`)
14
-
15
- // npm or pnpm
16
- const hasPnpm = await commandExists('pnpm')
17
- const hasNpm = await commandExists('npm')
18
-
19
- if (!hasPnpm && !hasNpm) {
20
- error('Neither npm nor pnpm found. Please install Node.js from https://nodejs.org')
21
- process.exit(1)
22
- }
23
-
24
- if (hasPnpm) success('pnpm available')
25
- else success('npm available')
26
- }
@@ -1,80 +0,0 @@
1
- import { execa } from 'execa'
2
- import { code, commandExists, header, info, success, warn } from '../utils/exec.js'
3
-
4
- export async function checkPlatform(platform) {
5
- if (platform === 'azure') {
6
- await checkAzure()
7
- } else {
8
- await checkGithub()
9
- }
10
- }
11
-
12
- async function checkAzure() {
13
- header('Step 5, Checking Azure DevOps CLI')
14
-
15
- // Check az is installed
16
- const hasAz = await commandExists('az')
17
- if (!hasAz) {
18
- warn('Azure CLI (az) not found.')
19
- info('Install from: https://learn.microsoft.com/en-us/cli/azure/install-azure-cli')
20
- return
21
- }
22
- success('Azure CLI (az) available')
23
-
24
- // Check az devops extension
25
- try {
26
- const result = await execa('az', ['extension', 'list', '--query', "[?name=='azure-devops']", '-o', 'tsv'], {
27
- reject: false,
28
- })
29
- const hasExtension = result.stdout && result.stdout.includes('azure-devops')
30
-
31
- if (hasExtension) {
32
- success('azure-devops extension installed')
33
- } else {
34
- warn('azure-devops extension not found. Run:')
35
- code([
36
- 'az extension add --name azure-devops',
37
- 'az config set extension.dynamic_install_allow_preview=true',
38
- 'az login',
39
- 'az devops login --organization https://dev.azure.com/<your-org>',
40
- ])
41
- }
42
- } catch {
43
- warn('Could not check azure-devops extension. Run:')
44
- code([
45
- 'az extension add --name azure-devops',
46
- 'az config set extension.dynamic_install_allow_preview=true',
47
- 'az login',
48
- 'az devops login --organization https://dev.azure.com/<your-org>',
49
- ])
50
- }
51
- }
52
-
53
- async function checkGithub() {
54
- header('Step 5, Checking GitHub CLI')
55
-
56
- const hasGh = await commandExists('gh')
57
-
58
- if (hasGh) {
59
- success('GitHub CLI (gh) available')
60
-
61
- // Check auth status
62
- try {
63
- const result = await execa('gh', ['auth', 'status'], { reject: false })
64
- if (result.exitCode === 0) {
65
- success('GitHub CLI authenticated')
66
- } else {
67
- warn('GitHub CLI not authenticated. Run:')
68
- code(['gh auth login'])
69
- }
70
- } catch {
71
- warn('Could not check gh auth status.')
72
- }
73
- } else {
74
- warn('GitHub CLI (gh) not found.')
75
- info('Install from: https://cli.github.com')
76
- console.log()
77
- info('After installing, authenticate with:')
78
- code(['gh auth login'])
79
- }
80
- }
@@ -1,38 +0,0 @@
1
- import { confirm } from '@inquirer/prompts'
2
- import { code, commandExists, header, info, loading, success, warn } from '../utils/exec.js'
3
-
4
- export async function checkRtk(options = {}) {
5
- if (!options.skipHeader) header('Checking rtk')
6
-
7
- let shouldCheck = true
8
- if (!options.skipPrompt) {
9
- info('Recommended: install and verify rtk for safer agent CLI command execution.')
10
- shouldCheck = await confirm({
11
- message: 'Check rtk now?',
12
- default: true,
13
- })
14
- }
15
-
16
- if (!shouldCheck) {
17
- warn('Skipped rtk check (you can install it later)')
18
- return { optedIn: false, checked: false, available: false }
19
- }
20
-
21
- loading('checking rtk...')
22
-
23
- const available = await commandExists('rtk')
24
-
25
- if (available) {
26
- success('rtk is available')
27
- return { optedIn: true, checked: true, available: true }
28
- }
29
-
30
- warn('rtk not found on PATH.')
31
- console.log()
32
- info('rtk is required for agents to run CLI commands safely.')
33
- info('Install it from: https://github.com/rtk-ai/rtk#pre-built-binaries')
34
- console.log()
35
- info('After installing, verify with:')
36
- code(['rtk --version'])
37
- return { optedIn: true, checked: true, available: false }
38
- }
@@ -1,163 +0,0 @@
1
- import { search } 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
- import { fetchModels } from '../utils/models-cache.js'
6
-
7
- const COST_TIER = (input) => {
8
- if (input === undefined || input === null) return ''
9
- if (input < 1) return ' [$]'
10
- if (input <= 10) return ' [$$]'
11
- return ' [$$$]'
12
- }
13
-
14
- // Use canonical cost for the tier badge so all providers of the same model
15
- // show the same tier (e.g. github-copilot $0 subscription shows [$$] not [$])
16
- const COST_TIER_DISPLAY = (cost, canonicalCost) =>
17
- COST_TIER(canonicalCost !== undefined ? canonicalCost : cost)
18
-
19
- function formatPrice(price) {
20
- if (price === undefined || price === null) return '?'
21
- if (price === 0) return '$0 (subscription)'
22
- return `$${price}/M`
23
- }
24
-
25
- function buildDisplayModels(rawModels) {
26
- return rawModels.map(m => {
27
- const priceStr = formatPrice(m.cost)
28
- const canonicalNote = m.canonicalCost !== undefined
29
- ? ` · official price: ${formatPrice(m.canonicalCost)}/M`
30
- : ''
31
- return {
32
- ...m,
33
- label: `${m.name}${COST_TIER_DISPLAY(m.cost, m.canonicalCost)}, ${m.id}`,
34
- description: `${priceStr}${canonicalNote} · context: ${m.context ? (m.context / 1000) + 'k' : '?'}`,
35
- }
36
- })
37
- }
38
-
39
- async function pickModel(message, models) {
40
- return await search({
41
- message,
42
- source: (input) => {
43
- const q = (input || '').toLowerCase()
44
- const filtered = q
45
- ? models.filter(m =>
46
- m.label.toLowerCase().includes(q) ||
47
- m.id.toLowerCase().includes(q)
48
- )
49
- : models
50
- return filtered.slice(0, 50).map(m => ({
51
- name: m.label,
52
- value: m.id,
53
- description: m.description,
54
- }))
55
- },
56
- })
57
- }
58
-
59
- async function writeModelToAgent(agentFile, modelId) {
60
- const content = await fse.readFile(agentFile, 'utf-8')
61
- const updated = content.replace(
62
- /^(---\n[\s\S]*?)\n---/m,
63
- `$1\nmodel: ${modelId}\n---`
64
- )
65
- await fse.writeFile(agentFile, updated, 'utf-8')
66
- }
67
-
68
- export async function chooseModels() {
69
- header('Step 9, Choose models')
70
-
71
- info('Fetching available models from models.dev...')
72
- const { models: rawModels, source } = await fetchModels()
73
-
74
- if (!rawModels) {
75
- warn('Could not fetch models (offline and no cache). Skipping model selection.')
76
- warn('Set models later in .agents/agents/<name>.md and .opencode/opencode.json')
77
- return
78
- }
79
-
80
- if (source === 'stale-cache') {
81
- warn('Network unavailable, using cached model list (may be outdated).')
82
- } else if (source === 'cache') {
83
- info('Using cached model list (refreshes weekly).')
84
- }
85
-
86
- const models = buildDisplayModels(rawModels)
87
- success(`${models.length} models available`)
88
- console.log()
89
- info('Cost indicators: [$] cheap [$$] mid [$$$] expensive')
90
- info('Type to search. Change selections later in .agents/agents/ and .opencode/opencode.json')
91
- console.log()
92
-
93
- // Plan model
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.')
97
- const planModel = await pickModel('Plan model:', models)
98
- console.log()
99
-
100
- // Build model
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.')
103
- const buildModel = await pickModel('Build model:', models)
104
- console.log()
105
-
106
- // Fast model
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.')
109
- const fastModel = await pickModel('Fast model:', models)
110
- console.log()
111
-
112
- // Write build model to builder agents
113
- const buildAgents = ['front-engineer', 'back-engineer', 'infra-engineer', 'quality-engineer', 'security-auditor']
114
- const agentsDir = path.join(process.cwd(), '.agents', 'agents')
115
- for (const name of buildAgents) {
116
- const file = path.join(agentsDir, `${name}.md`)
117
- if (await fse.pathExists(file)) {
118
- await writeModelToAgent(file, buildModel)
119
- success(`${name} → ${buildModel}`)
120
- }
121
- }
122
-
123
- // Write fast model to devops-manager
124
- const devopsFile = path.join(agentsDir, 'devops-manager.md')
125
- if (await fse.pathExists(devopsFile)) {
126
- await writeModelToAgent(devopsFile, fastModel)
127
- success(`devops-manager → ${fastModel}`)
128
- }
129
-
130
- // Write plan model to opencode.json
131
- const opencodeJsonPath = path.join(process.cwd(), '.opencode', 'opencode.json')
132
- if (await fse.pathExists(opencodeJsonPath)) {
133
- const config = await fse.readJson(opencodeJsonPath)
134
- config.model = planModel
135
- await fse.writeJson(opencodeJsonPath, config, { spaces: 2 })
136
- success(`plan model -> ${planModel} (written to .opencode/opencode.json)`)
137
- }
138
-
139
- // Write build and fast models to ensemble.json
140
- const ensembleJsonPath = path.join(process.cwd(), '.opencode', 'ensemble.json')
141
- if (await fse.pathExists(ensembleJsonPath)) {
142
- const ensemble = await fse.readJson(ensembleJsonPath)
143
- delete ensemble.defaultModel
144
- ensemble.modelsByAgent = {
145
- ...ensemble.modelsByAgent,
146
- build: buildModel,
147
- explore: fastModel,
148
- }
149
- await fse.writeJson(ensembleJsonPath, ensemble, { spaces: 2 })
150
- success(`build model -> ${buildModel} (written to .opencode/ensemble.json)`)
151
- success(`fast model -> ${fastModel} (written to .opencode/ensemble.json)`)
152
- }
153
-
154
- console.log()
155
- warn('Make sure you have API access to the selected models.')
156
- warn('Change them anytime in .agents/agents/<name>.md and .opencode/opencode.json')
157
-
158
- return {
159
- planModel,
160
- buildModel,
161
- fastModel,
162
- }
163
- }
@@ -1,22 +0,0 @@
1
- import { select } from '@inquirer/prompts'
2
- import fse from 'fs-extra'
3
- import path from 'path'
4
- import { fileURLToPath } from 'url'
5
- import { header, success } from '../utils/exec.js'
6
-
7
- const __dirname = path.dirname(fileURLToPath(import.meta.url))
8
- const PLATFORMS_PRESET_PATH = path.resolve(__dirname, '../presets/platforms.json')
9
-
10
- const platformsPreset = await fse.readJson(PLATFORMS_PRESET_PATH)
11
-
12
- export async function choosePlatform() {
13
- header('Step 4, Version control platform')
14
-
15
- const platform = await select({
16
- message: 'Which platform are you using?',
17
- choices: platformsPreset.map(p => ({ name: p.name, value: p.value })),
18
- })
19
-
20
- success(`Platform: ${platform === 'github' ? 'GitHub' : 'Azure DevOps'}`)
21
- return platform
22
- }
@@ -1,79 +0,0 @@
1
- import { select } from '@inquirer/prompts'
2
- import { execa } from 'execa'
3
- import fse from 'fs-extra'
4
- import path from 'path'
5
- import { fileURLToPath } from 'url'
6
- import { header, info, success, warn } from '../utils/exec.js'
7
-
8
- const __dirname = path.dirname(fileURLToPath(import.meta.url))
9
- const CONTENT_SKILLS_DIR = path.resolve(__dirname, '../../content/.agents/skills')
10
-
11
- async function installObSkills() {
12
- const destSkillsDir = path.join(process.cwd(), '.agents', 'skills')
13
- await fse.ensureDir(destSkillsDir)
14
-
15
- const skills = await fse.readdir(CONTENT_SKILLS_DIR)
16
- for (const skill of skills) {
17
- const src = path.join(CONTENT_SKILLS_DIR, skill)
18
- const dest = path.join(destSkillsDir, skill)
19
- const stat = await fse.stat(src)
20
- if (!stat.isDirectory()) continue
21
- if (await fse.pathExists(dest)) {
22
- info(`${skill} already exists, skipping`)
23
- continue
24
- }
25
- await fse.copy(src, dest)
26
- success(`Installed skill: ${skill}`)
27
- }
28
- }
29
-
30
- export async function chooseSkillsProvider() {
31
- header('Step 8, Installing skills')
32
-
33
- // ob-skills are always installed, mandatory
34
- info('Installing built-in ob-skills...')
35
- await installObSkills()
36
- console.log()
37
-
38
- info('Skills provide platform and tech-specific knowledge to your agents.')
39
- info('Agents detect and load skills automatically, you never need to specify them.')
40
- info('You can add more skills on top of the built-in ones.')
41
- console.log()
42
-
43
- const selected = await select({
44
- message: 'Add additional skills from:',
45
- choices: [
46
- {
47
- name: 'npx skills (vercel-labs/skills)',
48
- value: 'npx-skills',
49
- description: 'Install skills from the vercel-labs community skills registry',
50
- },
51
- {
52
- name: 'None, built-in skills are enough',
53
- value: 'none',
54
- },
55
- ],
56
- })
57
-
58
- if (selected === 'none') {
59
- return { additionalSkillsProvider: 'none' }
60
- }
61
-
62
- if (selected === 'npx-skills') {
63
- info('Running npx skills...')
64
- console.log()
65
- try {
66
- await execa('npx', ['skills'], {
67
- cwd: process.cwd(),
68
- stdio: 'inherit',
69
- reject: false,
70
- })
71
- } catch (err) {
72
- warn(`npx skills failed: ${err.message}`)
73
- }
74
-
75
- return { additionalSkillsProvider: 'npx-skills' }
76
- }
77
-
78
- return { additionalSkillsProvider: selected }
79
- }
@@ -1,89 +0,0 @@
1
- import path from 'path'
2
- import { fileURLToPath } from 'url'
3
- import fse from 'fs-extra'
4
- import { copyContent } from '../utils/copy.js'
5
- import { error, header, success } from '../utils/exec.js'
6
-
7
- const __dirname = path.dirname(fileURLToPath(import.meta.url))
8
- const CONTENT_DIR = path.resolve(__dirname, '../../content')
9
-
10
- function formatRootsForText(roots = [], cwd = process.cwd()) {
11
- if (!roots.length) return ['current folder']
12
- return roots.map(r => {
13
- const rel = path.relative(cwd, r)
14
- if (!rel || rel === '') return 'current folder'
15
- if (!rel.startsWith('..')) return `./${rel.replace(/\\/g, '/')}`
16
- return rel.replace(/\\/g, '/')
17
- })
18
- }
19
-
20
- async function patchSourceScopeFiles(dest, ctx) {
21
- const roots = formatRootsForText(ctx.sourceRoots || [dest], dest)
22
- const rootsInline = roots.join(', ')
23
- const rootsBullets = roots.map(r => ` - ${r}`).join('\n')
24
-
25
- const agentsPath = path.join(dest, 'AGENTS.md')
26
- if (await fse.pathExists(agentsPath)) {
27
- let content = await fse.readFile(agentsPath, 'utf-8')
28
- content = content.replace(
29
- 'Before scanning, load source roots from `.agents/source-roots.json` when present. Only scan those roots plus this repo\'s docs/config files.',
30
- `Source roots selected during onboarding (scan these roots plus this repo docs/config): ${rootsInline}.`
31
- )
32
- content = content.replace(
33
- '4. **Analyze the actual codebase**: use `.agents/source-roots.json` as source roots when present, then read CSS files, Tailwind config, component files, token definitions. Do not rely on prior knowledge, read the files.',
34
- `4. **Analyze the actual codebase** using these source roots:\n${rootsBullets}\n\n Then read CSS files, Tailwind config, component files, token definitions. Do not rely on prior knowledge, read the files.`
35
- )
36
- content = content.replace(
37
- '4. **Analyze the actual codebase**: use `.agents/source-roots.json` as source roots when present, then read folder structure, config files, route definitions, data models, integration points. Do not rely on prior knowledge, read the files.',
38
- `4. **Analyze the actual codebase** using these source roots:\n${rootsBullets}\n\n Then read folder structure, config files, route definitions, data models, integration points. Do not rely on prior knowledge, read the files.`
39
- )
40
- content = content.replace(
41
- '- Read source scope from `.agents/source-roots.json`.',
42
- `- Source roots selected during onboarding: ${rootsInline}.`
43
- )
44
- await fse.writeFile(agentsPath, content, 'utf-8')
45
- }
46
-
47
- const designPath = path.join(dest, 'DESIGN.md')
48
- if (await fse.pathExists(designPath)) {
49
- let content = await fse.readFile(designPath, 'utf-8')
50
- const injection = `\nSource roots selected during onboarding:\n${rootsBullets}\n\nWhen analyzing, read UI/design evidence only from these roots.\n\nIn the generated DESIGN.md, add this section near the top:\n\n## Source Roots Used\n${rootsBullets}\n`
51
- content = content.replace(
52
- 'Analyze the design system of this codebase with the goal of creating a DESIGN.md file in the project root and giving the user a file for easy copy & pasting.',
53
- 'Analyze the design system of this codebase with the goal of creating a DESIGN.md file in the project root and giving the user a file for easy copy & pasting.' + injection
54
- )
55
- await fse.writeFile(designPath, content, 'utf-8')
56
- }
57
-
58
- const architecturePath = path.join(dest, 'ARCHITECTURE.md')
59
- if (await fse.pathExists(architecturePath)) {
60
- let content = await fse.readFile(architecturePath, 'utf-8')
61
- const injection = `\nSource roots selected during onboarding:\n${rootsBullets}\n\nWhen analyzing, read architecture evidence only from these roots.\n\nIn the generated ARCHITECTURE.md, add this section near the top:\n\n## Source Roots Used\n${rootsBullets}\n`
62
- content = content.replace(
63
- 'Analyze the architecture of this codebase with the goal of creating an ARCHITECTURE.md file in the project root and giving the user a file for easy copy & pasting.',
64
- 'Analyze the architecture of this codebase with the goal of creating an ARCHITECTURE.md file in the project root and giving the user a file for easy copy & pasting.' + injection
65
- )
66
- await fse.writeFile(architecturePath, content, 'utf-8')
67
- }
68
- }
69
-
70
- export async function copyContentStep(platform, ctx = {}) {
71
- header('Step 6, Copying opencode-onboard files')
72
-
73
- const dest = process.cwd()
74
-
75
- try {
76
- await copyContent(CONTENT_DIR, dest, platform, ctx)
77
- const rootsFile = path.join(dest, '.agents', 'source-roots.json')
78
- await fse.ensureDir(path.dirname(rootsFile))
79
- await fse.writeJson(rootsFile, {
80
- mode: ctx.sourceMode || 'current',
81
- roots: ctx.sourceRoots || [dest],
82
- }, { spaces: 2 })
83
- await patchSourceScopeFiles(dest, ctx)
84
- success('Files copied to project root')
85
- } catch (err) {
86
- error(`Failed to copy content: ${err.message}`)
87
- process.exit(1)
88
- }
89
- }
@@ -1,93 +0,0 @@
1
- import path from 'node:path'
2
- import fse from 'fs-extra'
3
- import { info, success, warn } from '../utils/exec.js'
4
-
5
- const MARKER_START = '<!-- CAVEMAN-GUIDANCE-START -->'
6
- const MARKER_END = '<!-- CAVEMAN-GUIDANCE-END -->'
7
-
8
- const AGENTS_BLOCK = `${MARKER_START}
9
- ## Caveman Mode (Optional)
10
-
11
- If caveman is installed, prefer concise responses by default to reduce token usage while keeping full technical accuracy.
12
-
13
- Treat any of these as a direct trigger to enable concise style:
14
- - "talk like caveman"
15
- - "caveman mode"
16
- - "less tokens please"
17
-
18
- When enabled:
19
- - Keep explanations short and direct
20
- - Prioritize exact commands, paths, and outcomes
21
- - Avoid filler, greetings, and repeated context
22
-
23
- The user can disable it with phrases like: "normal mode" or "stop caveman".
24
- ${MARKER_END}`
25
-
26
- const OPSX_BLOCK = `${MARKER_START}
27
- ## Caveman Output Style
28
-
29
- If the user requests "talk like caveman", "caveman mode", or "less tokens please", keep all status updates concise:
30
- - Report only key actions, blockers, and next commands
31
- - Keep completion updates brief and factual
32
- - Preserve technical precision; compress wording only
33
- ${MARKER_END}`
34
-
35
- async function appendBlockIfMissing(filePath, block) {
36
- if (!await fse.pathExists(filePath)) return { ok: false, reason: 'missing-file' }
37
-
38
- const current = await fse.readFile(filePath, 'utf-8')
39
- if (current.includes(MARKER_START)) return { ok: true, changed: false }
40
-
41
- const next = `${current.replace(/\s*$/, '')}\n\n${block}\n`
42
- await fse.writeFile(filePath, next, 'utf-8')
43
- return { ok: true, changed: true }
44
- }
45
-
46
- export async function enableCavemanGuidance(cavemanResult) {
47
- if (!cavemanResult?.installed) {
48
- info('Caveman guidance skipped (caveman not installed)')
49
- return { enabled: false }
50
- }
51
-
52
- const targets = [
53
- { rel: 'AGENTS.md', block: AGENTS_BLOCK },
54
- ]
55
-
56
- const commandsDir = path.join(process.cwd(), '.opencode', 'commands')
57
- if (await fse.pathExists(commandsDir)) {
58
- const entries = await fse.readdir(commandsDir)
59
- for (const name of entries) {
60
- if (!/^opsx-.*\.md$/i.test(name)) continue
61
- targets.push({ rel: path.join('.opencode', 'commands', name), block: OPSX_BLOCK })
62
- }
63
- }
64
-
65
- const skillsDir = path.join(process.cwd(), '.opencode', 'skills')
66
- if (await fse.pathExists(skillsDir)) {
67
- const skillEntries = await fse.readdir(skillsDir)
68
- for (const dirName of skillEntries) {
69
- if (!/^openspec-/i.test(dirName)) continue
70
- targets.push({ rel: path.join('.opencode', 'skills', dirName, 'SKILL.md'), block: OPSX_BLOCK })
71
- }
72
- }
73
-
74
- let changedCount = 0
75
- for (const target of targets) {
76
- const abs = path.join(process.cwd(), target.rel)
77
- try {
78
- const res = await appendBlockIfMissing(abs, target.block)
79
- if (!res.ok) {
80
- warn(`Could not apply caveman guidance to ${target.rel} (${res.reason})`)
81
- } else if (res.changed) {
82
- changedCount++
83
- }
84
- } catch (err) {
85
- warn(`Could not apply caveman guidance to ${target.rel}: ${err.message}`)
86
- }
87
- }
88
-
89
- if (changedCount > 0) success('Caveman guidance enabled in AGENTS/OpenSpec prompts')
90
- else info('Caveman guidance already present')
91
-
92
- return { enabled: true, patchedFiles: changedCount }
93
- }
@@ -1,59 +0,0 @@
1
- import { checkbox } from '@inquirer/prompts'
2
- import { header, info, loading, success, warn } from '../utils/exec.js'
3
- import { checkRtk } from './check-rtk.js'
4
- import { installQuota } from './install-quota.js'
5
- import { installCaveman } from './install-caveman.js'
6
- import { enableCavemanGuidance } from './enable-caveman-guidance.js'
7
-
8
- export async function tokenOptimizationStep(options = {}) {
9
- header('Step 10, Token optimization tools')
10
-
11
- const defaultSelected = ['rtk', 'quota', 'caveman']
12
- let selected = defaultSelected
13
-
14
- if (!options.skipPrompt && process.stdin.isTTY) {
15
- info('Choose which optimization tools to enable (recommended: all).')
16
- const timeoutMs = 30000
17
- const choice = await Promise.race([
18
- checkbox({
19
- message: 'Enable tools:',
20
- choices: [
21
- { name: 'RTK check (recommended)', value: 'rtk', checked: true },
22
- { name: 'opencode-quota plugin (recommended)', value: 'quota', checked: true },
23
- { name: 'caveman concise mode (recommended)', value: 'caveman', checked: true },
24
- ],
25
- }),
26
- new Promise(resolve => setTimeout(() => resolve(defaultSelected), timeoutMs)),
27
- ])
28
- selected = Array.isArray(choice) ? choice : defaultSelected
29
- }
30
-
31
- loading('applying token optimization selections...')
32
-
33
- const has = value => selected.includes(value)
34
-
35
- const rtk = has('rtk')
36
- ? await checkRtk({ skipHeader: true, skipPrompt: true })
37
- : { optedIn: false, checked: false, available: false }
38
-
39
- const quota = has('quota')
40
- ? await installQuota({ skipHeader: true, skipPrompt: true })
41
- : { optedIn: false, installed: false }
42
-
43
- const caveman = has('caveman')
44
- ? await installCaveman({
45
- skipHeader: true,
46
- skipPrompt: true,
47
- skillsProvider: options.skillsProvider,
48
- })
49
- : { optedIn: false, installed: false }
50
-
51
- const cavemanGuidance = has('caveman')
52
- ? await enableCavemanGuidance(caveman)
53
- : { enabled: false }
54
-
55
- if (selected.length === 0) warn('No token optimization tools selected')
56
- else success('Token optimization step completed')
57
-
58
- return { rtk, quota, caveman, cavemanGuidance }
59
- }