opencode-onboard 0.2.6 → 0.2.12
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 +30 -6
- package/content/.opencode/plugins/session-log.js +266 -35
- package/content/AGENTS.md +384 -384
- package/package.json +1 -1
- package/src/index.js +180 -18
- package/src/steps/__tests__/check-rtk.test.js +3 -2
- package/src/steps/__tests__/clean-ai-files.test.js +1 -1
- package/src/steps/__tests__/copy-content.test.js +4 -2
- package/src/steps/__tests__/token-optimization.test.js +78 -0
- package/src/steps/check-rtk.js +22 -4
- package/src/steps/choose-source-scope.js +1 -1
- package/src/steps/enable-caveman-guidance.js +93 -0
- package/src/steps/install-caveman.js +42 -0
- package/src/steps/install-quota.js +82 -0
- package/src/steps/token-optimization.js +59 -0
- package/src/steps/write-onboard-config.js +2 -0
- package/src/utils/exec.js +19 -7
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -1,26 +1,173 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import chalk from 'chalk'
|
|
3
|
-
import { createRequire } from 'node:module'
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
2
|
+
import chalk from 'chalk'
|
|
3
|
+
import { createRequire } from 'node:module'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import fse from 'fs-extra'
|
|
6
|
+
import { checkEnv } from './steps/check-env.js'
|
|
7
|
+
import { checkPlatform } from './steps/check-platform.js'
|
|
8
|
+
import { checkRtk } from './steps/check-rtk.js'
|
|
7
9
|
import { chooseModels } from './steps/choose-models.js'
|
|
8
10
|
import { choosePlatform } from './steps/choose-platform.js'
|
|
9
11
|
import { chooseSourceScope } from './steps/choose-source-scope.js'
|
|
10
12
|
import { chooseSkillsProvider } from './steps/choose-skills-provider.js'
|
|
11
13
|
import { cleanAiFiles } from './steps/clean-ai-files.js'
|
|
12
14
|
import { copyContentStep } from './steps/copy-content.js'
|
|
13
|
-
import { initOpenspec } from './steps/init-openspec.js'
|
|
15
|
+
import { initOpenspec } from './steps/init-openspec.js'
|
|
14
16
|
import { patchAgentsMd } from './steps/patch-agents-md.js'
|
|
17
|
+
import { installQuota } from './steps/install-quota.js'
|
|
18
|
+
import { installCaveman } from './steps/install-caveman.js'
|
|
19
|
+
import { enableCavemanGuidance } from './steps/enable-caveman-guidance.js'
|
|
15
20
|
import { installBrowser } from './steps/install-browser.js'
|
|
16
21
|
import { writeOnboardConfig } from './steps/write-onboard-config.js'
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
import { loading } from './utils/exec.js'
|
|
23
|
+
import { tokenOptimizationStep } from './steps/token-optimization.js'
|
|
24
|
+
|
|
25
|
+
function printHelp(version) {
|
|
26
|
+
console.log(`opencode-onboard v${version}`)
|
|
27
|
+
console.log()
|
|
28
|
+
console.log('Usage:')
|
|
29
|
+
console.log(' npx opencode-onboard Run full onboarding wizard')
|
|
30
|
+
console.log(' npx opencode-onboard <command> Run a single step command')
|
|
31
|
+
console.log()
|
|
32
|
+
console.log('Commands:')
|
|
33
|
+
console.log(' clean Run AI files cleanup step')
|
|
34
|
+
console.log(' platform Run platform selection step')
|
|
35
|
+
console.log(' copy Run content copy step')
|
|
36
|
+
console.log(' openspec Run OpenSpec initialization step')
|
|
37
|
+
console.log(' skills Run skills install step')
|
|
38
|
+
console.log(' models Run models selection step')
|
|
39
|
+
console.log(' optimization Run token optimization tools step')
|
|
40
|
+
console.log(' quota Run opencode-quota installer step')
|
|
41
|
+
console.log(' rtk Run rtk check step')
|
|
42
|
+
console.log(' caveman Run caveman install + guidance steps')
|
|
43
|
+
console.log(' browser Run opencode-browser installer step')
|
|
44
|
+
console.log(' metadata Write onboarding metadata step')
|
|
45
|
+
console.log()
|
|
46
|
+
console.log('Options:')
|
|
47
|
+
console.log(' -h, --help Show this help message')
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function readOnboardConfig() {
|
|
51
|
+
const cfgPath = path.join(process.cwd(), '.opencode', 'opencode-onboard.json')
|
|
52
|
+
if (!await fse.pathExists(cfgPath)) return null
|
|
53
|
+
try {
|
|
54
|
+
return await fse.readJson(cfgPath)
|
|
55
|
+
} catch {
|
|
56
|
+
return null
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function runSingleCommand(command) {
|
|
61
|
+
const saved = await readOnboardConfig()
|
|
62
|
+
const savedWizard = saved?.wizard ?? {}
|
|
63
|
+
const ctx = {
|
|
64
|
+
hasDesign: !!savedWizard?.preserved?.design,
|
|
65
|
+
hasArchitecture: !!savedWizard?.preserved?.architecture,
|
|
66
|
+
hasOpenspec: !!savedWizard?.preserved?.openspec,
|
|
67
|
+
sourceMode: savedWizard?.sourceMode ?? 'current',
|
|
68
|
+
sourceRoots: Array.isArray(savedWizard?.sourceRoots) ? savedWizard.sourceRoots : [],
|
|
69
|
+
}
|
|
70
|
+
const platform = savedWizard?.platform
|
|
71
|
+
const resolvedPlatform = platform === 'azure' || platform === 'github' ? platform : 'github'
|
|
72
|
+
|
|
73
|
+
if (command === 'clean') {
|
|
74
|
+
await cleanAiFiles()
|
|
75
|
+
return true
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (command === 'platform') {
|
|
79
|
+
await choosePlatform()
|
|
80
|
+
return true
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (command === 'copy') {
|
|
84
|
+
await copyContentStep(resolvedPlatform, ctx)
|
|
85
|
+
await patchAgentsMd(ctx)
|
|
86
|
+
return true
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (command === 'openspec') {
|
|
90
|
+
await initOpenspec()
|
|
91
|
+
return true
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (command === 'skills') {
|
|
95
|
+
await chooseSkillsProvider()
|
|
96
|
+
return true
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (command === 'models') {
|
|
100
|
+
await chooseModels()
|
|
101
|
+
return true
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (command === 'optimization') {
|
|
105
|
+
await tokenOptimizationStep({ skillsProvider: savedWizard?.additionalSkillsProvider })
|
|
106
|
+
return true
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (command === 'quota') {
|
|
110
|
+
await installQuota()
|
|
111
|
+
return true
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (command === 'rtk') {
|
|
115
|
+
await checkRtk()
|
|
116
|
+
return true
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (command === 'caveman') {
|
|
120
|
+
const caveman = await installCaveman({ skillsProvider: savedWizard?.additionalSkillsProvider })
|
|
121
|
+
await enableCavemanGuidance(caveman)
|
|
122
|
+
return true
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (command === 'browser') {
|
|
126
|
+
await installBrowser()
|
|
127
|
+
return true
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (command === 'metadata') {
|
|
131
|
+
await writeOnboardConfig({
|
|
132
|
+
...ctx,
|
|
133
|
+
platform: resolvedPlatform,
|
|
134
|
+
additionalSkillsProvider: savedWizard?.additionalSkillsProvider ?? 'none',
|
|
135
|
+
planModel: savedWizard?.models?.plan ?? null,
|
|
136
|
+
buildModel: savedWizard?.models?.build ?? null,
|
|
137
|
+
fastModel: savedWizard?.models?.fast ?? null,
|
|
138
|
+
optionalTools: savedWizard?.optionalTools ?? null,
|
|
139
|
+
cavemanGuidance: savedWizard?.cavemanGuidance ?? null,
|
|
140
|
+
})
|
|
141
|
+
return true
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return false
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (process.stdout.isTTY) console.clear()
|
|
148
|
+
console.log()
|
|
149
|
+
const require = createRequire(import.meta.url)
|
|
150
|
+
const { version } = require('../package.json')
|
|
151
|
+
const args = process.argv.slice(2)
|
|
152
|
+
|
|
153
|
+
if (args.includes('-h') || args.includes('--help')) {
|
|
154
|
+
printHelp(version)
|
|
155
|
+
process.exit(0)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (args.length > 0) {
|
|
159
|
+
const ok = await runSingleCommand(args[0])
|
|
160
|
+
if (!ok) {
|
|
161
|
+
console.log(chalk.red(`Unknown command: ${args[0]}`))
|
|
162
|
+
console.log()
|
|
163
|
+
printHelp(version)
|
|
164
|
+
process.exit(1)
|
|
165
|
+
}
|
|
166
|
+
process.exit(0)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const logo = chalk.hex('#fe3d57')
|
|
170
|
+
const bannerLines = [
|
|
24
171
|
logo(' '),
|
|
25
172
|
logo(' ▒▒▒▒▒▒▒▒▒▒▒▒▒ '),
|
|
26
173
|
logo(' ▒▒▓ ▓▒▓ '),
|
|
@@ -58,42 +205,55 @@ if (process.stdin.isTTY) {
|
|
|
58
205
|
}
|
|
59
206
|
|
|
60
207
|
try {
|
|
61
|
-
// 1. Check Node + pnpm
|
|
62
|
-
await checkEnv()
|
|
63
|
-
|
|
208
|
+
// 1. Check Node + pnpm
|
|
209
|
+
await checkEnv()
|
|
210
|
+
loading('preparing next step...')
|
|
211
|
+
|
|
64
212
|
// 2. Choose source code scope for init analysis
|
|
65
213
|
const scope = await chooseSourceScope()
|
|
214
|
+
loading('preparing next step...')
|
|
66
215
|
|
|
67
216
|
// 3. Clean existing AI config files, detect preserved state
|
|
68
217
|
const preserve = await cleanAiFiles()
|
|
69
218
|
const ctx = { ...preserve, ...scope }
|
|
219
|
+
loading('preparing next step...')
|
|
70
220
|
|
|
71
221
|
// 4. Choose platform
|
|
72
222
|
const platform = await choosePlatform()
|
|
223
|
+
loading('preparing next step...')
|
|
73
224
|
|
|
74
225
|
// 5. Check platform CLI (az or gh)
|
|
75
226
|
await checkPlatform(platform)
|
|
227
|
+
loading('preparing next step...')
|
|
76
228
|
|
|
77
229
|
// 6. Copy content
|
|
78
230
|
await copyContentStep(platform, ctx)
|
|
231
|
+
loading('preparing next step...')
|
|
79
232
|
|
|
80
233
|
// 6b. Patch AGENTS.md to skip steps for already-existing files
|
|
81
234
|
await patchAgentsMd(ctx)
|
|
235
|
+
loading('preparing next step...')
|
|
82
236
|
|
|
83
237
|
// 7. Init OpenSpec
|
|
84
238
|
await initOpenspec()
|
|
239
|
+
loading('preparing next step...')
|
|
85
240
|
|
|
86
241
|
// 8. Install skills
|
|
87
242
|
const skillsSelection = await chooseSkillsProvider()
|
|
243
|
+
loading('preparing next step...')
|
|
88
244
|
|
|
89
245
|
// 9. Choose models
|
|
90
246
|
const selectedModels = await chooseModels()
|
|
247
|
+
loading('preparing next step...')
|
|
91
248
|
|
|
92
|
-
// 10.
|
|
93
|
-
await
|
|
249
|
+
// 10. Token optimization tools
|
|
250
|
+
const tokenOpt = await tokenOptimizationStep({ skillsProvider: skillsSelection.additionalSkillsProvider })
|
|
251
|
+
const { rtk, quota, caveman, cavemanGuidance } = tokenOpt
|
|
252
|
+
loading('preparing next step...')
|
|
94
253
|
|
|
95
254
|
// 11. Install opencode-browser
|
|
96
255
|
await installBrowser()
|
|
256
|
+
loading('preparing next step...')
|
|
97
257
|
|
|
98
258
|
// 12. Write onboarding metadata
|
|
99
259
|
await writeOnboardConfig({
|
|
@@ -101,6 +261,8 @@ try {
|
|
|
101
261
|
platform,
|
|
102
262
|
...skillsSelection,
|
|
103
263
|
...selectedModels,
|
|
264
|
+
optionalTools: { rtk, quota, caveman },
|
|
265
|
+
cavemanGuidance,
|
|
104
266
|
})
|
|
105
267
|
|
|
106
268
|
// Done
|
|
@@ -3,6 +3,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
|
3
3
|
vi.mock('../../utils/exec.js', () => ({
|
|
4
4
|
commandExists: vi.fn(),
|
|
5
5
|
header: vi.fn(),
|
|
6
|
+
loading: vi.fn(),
|
|
6
7
|
success: vi.fn(),
|
|
7
8
|
warn: vi.fn(),
|
|
8
9
|
info: vi.fn(),
|
|
@@ -20,7 +21,7 @@ describe('checkRtk()', () => {
|
|
|
20
21
|
it('prints success when rtk is available', async () => {
|
|
21
22
|
commandExists.mockResolvedValue(true)
|
|
22
23
|
|
|
23
|
-
await checkRtk()
|
|
24
|
+
await checkRtk({ skipPrompt: true })
|
|
24
25
|
|
|
25
26
|
expect(success).toHaveBeenCalledWith('rtk is available')
|
|
26
27
|
expect(warn).not.toHaveBeenCalled()
|
|
@@ -29,7 +30,7 @@ describe('checkRtk()', () => {
|
|
|
29
30
|
it('prints warning with install instructions when rtk is not found', async () => {
|
|
30
31
|
commandExists.mockResolvedValue(false)
|
|
31
32
|
|
|
32
|
-
await checkRtk()
|
|
33
|
+
await checkRtk({ skipPrompt: true })
|
|
33
34
|
|
|
34
35
|
expect(warn).toHaveBeenCalledWith('rtk not found on PATH.')
|
|
35
36
|
expect(success).not.toHaveBeenCalled()
|
|
@@ -39,7 +39,7 @@ describe('cleanAiFiles()', () => {
|
|
|
39
39
|
|
|
40
40
|
await cleanAiFiles()
|
|
41
41
|
|
|
42
|
-
expect(success).toHaveBeenCalledWith('No existing AI config files
|
|
42
|
+
expect(success).toHaveBeenCalledWith('No existing AI config files to remove')
|
|
43
43
|
})
|
|
44
44
|
|
|
45
45
|
it('removes found AI files after Enter', async () => {
|
|
@@ -34,7 +34,8 @@ describe('copyContentStep()', () => {
|
|
|
34
34
|
expect(copyContent).toHaveBeenCalledWith(
|
|
35
35
|
expect.stringContaining('content'),
|
|
36
36
|
process.cwd(),
|
|
37
|
-
'github'
|
|
37
|
+
'github',
|
|
38
|
+
{}
|
|
38
39
|
)
|
|
39
40
|
expect(success).toHaveBeenCalledWith('Files copied to project root')
|
|
40
41
|
})
|
|
@@ -47,7 +48,8 @@ describe('copyContentStep()', () => {
|
|
|
47
48
|
expect(copyContent).toHaveBeenCalledWith(
|
|
48
49
|
expect.stringContaining('content'),
|
|
49
50
|
process.cwd(),
|
|
50
|
-
'azure'
|
|
51
|
+
'azure',
|
|
52
|
+
{}
|
|
51
53
|
)
|
|
52
54
|
})
|
|
53
55
|
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
|
|
3
|
+
vi.mock('@inquirer/prompts', () => ({
|
|
4
|
+
checkbox: vi.fn(),
|
|
5
|
+
}))
|
|
6
|
+
|
|
7
|
+
vi.mock('../../utils/exec.js', () => ({
|
|
8
|
+
header: vi.fn(),
|
|
9
|
+
info: vi.fn(),
|
|
10
|
+
loading: vi.fn(),
|
|
11
|
+
success: vi.fn(),
|
|
12
|
+
warn: vi.fn(),
|
|
13
|
+
}))
|
|
14
|
+
|
|
15
|
+
vi.mock('../check-rtk.js', () => ({
|
|
16
|
+
checkRtk: vi.fn(),
|
|
17
|
+
}))
|
|
18
|
+
|
|
19
|
+
vi.mock('../install-quota.js', () => ({
|
|
20
|
+
installQuota: vi.fn(),
|
|
21
|
+
}))
|
|
22
|
+
|
|
23
|
+
vi.mock('../install-caveman.js', () => ({
|
|
24
|
+
installCaveman: vi.fn(),
|
|
25
|
+
}))
|
|
26
|
+
|
|
27
|
+
vi.mock('../enable-caveman-guidance.js', () => ({
|
|
28
|
+
enableCavemanGuidance: vi.fn(),
|
|
29
|
+
}))
|
|
30
|
+
|
|
31
|
+
import { checkbox } from '@inquirer/prompts'
|
|
32
|
+
import { warn } from '../../utils/exec.js'
|
|
33
|
+
import { checkRtk } from '../check-rtk.js'
|
|
34
|
+
import { installQuota } from '../install-quota.js'
|
|
35
|
+
import { installCaveman } from '../install-caveman.js'
|
|
36
|
+
import { enableCavemanGuidance } from '../enable-caveman-guidance.js'
|
|
37
|
+
import { tokenOptimizationStep } from '../token-optimization.js'
|
|
38
|
+
|
|
39
|
+
describe('tokenOptimizationStep()', () => {
|
|
40
|
+
beforeEach(() => {
|
|
41
|
+
vi.clearAllMocks()
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('runs all optimizations by default selection', async () => {
|
|
45
|
+
checkbox.mockResolvedValue(['rtk', 'quota', 'caveman'])
|
|
46
|
+
checkRtk.mockResolvedValue({ optedIn: true, checked: true, available: true })
|
|
47
|
+
installQuota.mockResolvedValue({ optedIn: true, installed: true })
|
|
48
|
+
installCaveman.mockResolvedValue({ optedIn: true, installed: true })
|
|
49
|
+
enableCavemanGuidance.mockResolvedValue({ enabled: true })
|
|
50
|
+
|
|
51
|
+
const result = await tokenOptimizationStep()
|
|
52
|
+
|
|
53
|
+
expect(checkRtk).toHaveBeenCalledWith({ skipHeader: true, skipPrompt: true })
|
|
54
|
+
expect(installQuota).toHaveBeenCalledWith({ skipHeader: true, skipPrompt: true })
|
|
55
|
+
expect(installCaveman).toHaveBeenCalledWith({ skipHeader: true, skipPrompt: true })
|
|
56
|
+
expect(enableCavemanGuidance).toHaveBeenCalledWith({ optedIn: true, installed: true })
|
|
57
|
+
expect(result.rtk.available).toBe(true)
|
|
58
|
+
expect(result.quota.installed).toBe(true)
|
|
59
|
+
expect(result.caveman.installed).toBe(true)
|
|
60
|
+
expect(result.cavemanGuidance.enabled).toBe(true)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('skips all tools when nothing is selected', async () => {
|
|
64
|
+
checkbox.mockResolvedValue([])
|
|
65
|
+
|
|
66
|
+
const result = await tokenOptimizationStep()
|
|
67
|
+
|
|
68
|
+
expect(checkRtk).not.toHaveBeenCalled()
|
|
69
|
+
expect(installQuota).not.toHaveBeenCalled()
|
|
70
|
+
expect(installCaveman).not.toHaveBeenCalled()
|
|
71
|
+
expect(enableCavemanGuidance).not.toHaveBeenCalled()
|
|
72
|
+
expect(warn).toHaveBeenCalledWith('No token optimization tools selected')
|
|
73
|
+
expect(result.rtk.optedIn).toBe(false)
|
|
74
|
+
expect(result.quota.optedIn).toBe(false)
|
|
75
|
+
expect(result.caveman.optedIn).toBe(false)
|
|
76
|
+
expect(result.cavemanGuidance.enabled).toBe(false)
|
|
77
|
+
})
|
|
78
|
+
})
|
package/src/steps/check-rtk.js
CHANGED
|
@@ -1,13 +1,30 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { confirm } from '@inquirer/prompts'
|
|
2
|
+
import { code, commandExists, header, info, loading, success, warn } from '../utils/exec.js'
|
|
2
3
|
|
|
3
|
-
export async function checkRtk() {
|
|
4
|
-
header('
|
|
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...')
|
|
5
22
|
|
|
6
23
|
const available = await commandExists('rtk')
|
|
7
24
|
|
|
8
25
|
if (available) {
|
|
9
26
|
success('rtk is available')
|
|
10
|
-
return
|
|
27
|
+
return { optedIn: true, checked: true, available: true }
|
|
11
28
|
}
|
|
12
29
|
|
|
13
30
|
warn('rtk not found on PATH.')
|
|
@@ -17,4 +34,5 @@ export async function checkRtk() {
|
|
|
17
34
|
console.log()
|
|
18
35
|
info('After installing, verify with:')
|
|
19
36
|
code(['rtk --version'])
|
|
37
|
+
return { optedIn: true, checked: true, available: false }
|
|
20
38
|
}
|
|
@@ -0,0 +1,93 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { execa } from 'execa'
|
|
2
|
+
import fse from 'fs-extra'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { header, success, warn, error, loading, info } from '../utils/exec.js'
|
|
5
|
+
|
|
6
|
+
const SKILLS_LOCK_CANDIDATES = [
|
|
7
|
+
'skills-lock.json',
|
|
8
|
+
'.skills-lock.json',
|
|
9
|
+
'.skills/skills-lock.json',
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
export async function installCaveman(options = {}) {
|
|
13
|
+
if (!options.skipHeader) header('Installing caveman')
|
|
14
|
+
|
|
15
|
+
loading('installing caveman...')
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
info('Installing caveman via npx skills')
|
|
19
|
+
const result = await execa('npx', ['skills', 'add', 'JuliusBrussee/caveman', '-a', 'opencode'], {
|
|
20
|
+
reject: false,
|
|
21
|
+
timeout: 600000,
|
|
22
|
+
stdio: 'inherit',
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
if (result.exitCode === 0) {
|
|
26
|
+
if (options.skillsProvider !== 'npx-skills') {
|
|
27
|
+
for (const rel of SKILLS_LOCK_CANDIDATES) {
|
|
28
|
+
const abs = path.join(process.cwd(), rel)
|
|
29
|
+
if (await fse.pathExists(abs)) await fse.remove(abs)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
success('caveman installed')
|
|
33
|
+
return { optedIn: true, installed: true }
|
|
34
|
+
} else {
|
|
35
|
+
warn('caveman install exited with non-zero code')
|
|
36
|
+
return { optedIn: true, installed: false }
|
|
37
|
+
}
|
|
38
|
+
} catch (err) {
|
|
39
|
+
error(`Failed to install caveman: ${err.message}`)
|
|
40
|
+
return { optedIn: true, installed: false }
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { confirm } from '@inquirer/prompts'
|
|
2
|
+
import fse from 'fs-extra'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { header, success, warn, error, loading, info } from '../utils/exec.js'
|
|
5
|
+
|
|
6
|
+
const PLUGIN = '@slkiser/opencode-quota'
|
|
7
|
+
|
|
8
|
+
function ensurePlugin(config) {
|
|
9
|
+
if (!Array.isArray(config.plugin)) config.plugin = []
|
|
10
|
+
if (!config.plugin.includes(PLUGIN)) config.plugin.push(PLUGIN)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function addIfMissing(target, key, value) {
|
|
14
|
+
if (!(key in target)) target[key] = value
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function installQuota(options = {}) {
|
|
18
|
+
if (!options.skipHeader) header('Installing opencode-quota')
|
|
19
|
+
|
|
20
|
+
let shouldInstall = true
|
|
21
|
+
if (!options.skipPrompt && process.stdin.isTTY) {
|
|
22
|
+
const timeoutMs = 20000
|
|
23
|
+
const choice = await Promise.race([
|
|
24
|
+
confirm({
|
|
25
|
+
message: 'Install opencode-quota with recommended defaults?',
|
|
26
|
+
default: true,
|
|
27
|
+
}),
|
|
28
|
+
new Promise(resolve => setTimeout(() => resolve(true), timeoutMs)),
|
|
29
|
+
])
|
|
30
|
+
shouldInstall = choice !== false
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (!shouldInstall) {
|
|
34
|
+
warn('Skipped opencode-quota installation')
|
|
35
|
+
return { optedIn: false, installed: false }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
loading('configuring opencode-quota...')
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const opencodeDir = path.join(process.cwd(), '.opencode')
|
|
42
|
+
const opencodePath = path.join(opencodeDir, 'opencode.json')
|
|
43
|
+
const tuiPath = path.join(opencodeDir, 'tui.json')
|
|
44
|
+
const quotaDir = path.join(opencodeDir, 'opencode-quota')
|
|
45
|
+
const quotaPath = path.join(quotaDir, 'quota-toast.json')
|
|
46
|
+
|
|
47
|
+
const opencode = await fse.pathExists(opencodePath)
|
|
48
|
+
? await fse.readJson(opencodePath)
|
|
49
|
+
: { $schema: 'https://opencode.ai/config.json' }
|
|
50
|
+
|
|
51
|
+
const tui = await fse.pathExists(tuiPath)
|
|
52
|
+
? await fse.readJson(tuiPath)
|
|
53
|
+
: { $schema: 'https://opencode.ai/tui.json' }
|
|
54
|
+
|
|
55
|
+
ensurePlugin(opencode)
|
|
56
|
+
ensurePlugin(tui)
|
|
57
|
+
|
|
58
|
+
await fse.ensureDir(opencodeDir)
|
|
59
|
+
await fse.writeJson(opencodePath, opencode, { spaces: 2 })
|
|
60
|
+
await fse.writeJson(tuiPath, tui, { spaces: 2 })
|
|
61
|
+
|
|
62
|
+
const quotaConfig = await fse.pathExists(quotaPath)
|
|
63
|
+
? await fse.readJson(quotaPath)
|
|
64
|
+
: {}
|
|
65
|
+
|
|
66
|
+
// Keep installer semantics append-only: add defaults only when missing.
|
|
67
|
+
addIfMissing(quotaConfig, 'enabledProviders', 'auto')
|
|
68
|
+
addIfMissing(quotaConfig, 'formatStyle', 'singleWindow')
|
|
69
|
+
addIfMissing(quotaConfig, 'percentDisplayMode', 'used')
|
|
70
|
+
addIfMissing(quotaConfig, 'showSessionTokens', true)
|
|
71
|
+
|
|
72
|
+
await fse.ensureDir(quotaDir)
|
|
73
|
+
await fse.writeJson(quotaPath, quotaConfig, { spaces: 2 })
|
|
74
|
+
|
|
75
|
+
success('opencode-quota configured (manual setup)')
|
|
76
|
+
info('Restart OpenCode and run /quota to verify')
|
|
77
|
+
return { optedIn: true, installed: true }
|
|
78
|
+
} catch (err) {
|
|
79
|
+
error(`Failed to configure opencode-quota: ${err.message}`)
|
|
80
|
+
return { optedIn: true, installed: false }
|
|
81
|
+
}
|
|
82
|
+
}
|