opencode-onboard 0.4.3 → 0.4.5

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 (36) hide show
  1. package/README.md +41 -40
  2. package/content/.agents/agents/devops-manager.md +123 -123
  3. package/content/.agents/skills/ob-default/SKILL.md +25 -21
  4. package/content/.agents/skills/ob-generic-guardrails/SKILL.md +36 -32
  5. package/content/.agents/skills/ob-global/SKILL.md +92 -84
  6. package/content/.agents/skills/ob-pullrequest-az/SKILL.md +168 -160
  7. package/content/.agents/skills/ob-pullrequest-gh/SKILL.md +140 -136
  8. package/content/.opencode/commands/create-engineer.md +109 -0
  9. package/content/.opencode/plugins/session-log.js +523 -519
  10. package/content/AGENTS.md +32 -21
  11. package/package.json +1 -1
  12. package/src/commands/wizard.js +124 -113
  13. package/src/presets/browser.json +22 -18
  14. package/src/presets/optimization.json +27 -22
  15. package/src/steps/browser/browser.test.js +115 -81
  16. package/src/steps/browser/index.js +62 -54
  17. package/src/steps/clean/index.js +108 -107
  18. package/src/steps/metadata/index.js +63 -62
  19. package/src/steps/models/format.js +61 -60
  20. package/src/steps/models/write.test.js +117 -117
  21. package/src/steps/openspec/ensemble.test.js +79 -79
  22. package/src/steps/openspec/index.js +121 -32
  23. package/src/steps/openspec/index.test.js +63 -0
  24. package/src/steps/optimization/caveman.js +34 -29
  25. package/src/steps/optimization/codegraph.js +103 -0
  26. package/src/steps/optimization/codegraph.test.js +104 -0
  27. package/src/steps/optimization/global.js +88 -64
  28. package/src/steps/optimization/global.test.js +99 -0
  29. package/src/steps/optimization/index.js +109 -101
  30. package/src/steps/optimization/optimization.test.js +101 -93
  31. package/src/steps/optimization/quota.js +84 -84
  32. package/src/steps/source/source.test.js +124 -124
  33. package/src/utils/__tests__/copy.test.js +117 -117
  34. package/src/utils/exec-spinner.js +47 -47
  35. package/src/utils/exec.js +134 -131
  36. package/src/utils/terminal.js +6 -0
@@ -1,101 +1,109 @@
1
- import { checkbox, confirm } from '@inquirer/prompts'
2
- import fse from 'fs-extra'
3
- import path from 'path'
4
- import { fileURLToPath } from 'url'
5
- import { code, commandExists, header, info, loading, success, warn } from '../../utils/exec.js'
6
- import { installQuota } from './quota.js'
7
- import { installCaveman } from './caveman.js'
8
- import { enableCavemanGuidance } from './caveman-guidance.js'
9
- import { configureObGlobal } from './global.js'
10
-
11
- const __dirname = path.dirname(fileURLToPath(import.meta.url))
12
- const OPTIMIZATION_PRESET_PATH = path.resolve(__dirname, '../../presets/optimization.json')
13
- const optimizationPreset = await fse.readJson(OPTIMIZATION_PRESET_PATH)
14
-
15
- export async function checkRtk(options = {}) {
16
- if (!options.skipHeader) header('Checking rtk')
17
-
18
- let shouldCheck = true
19
- if (!options.skipPrompt) {
20
- info('Recommended: install and verify rtk for safer agent CLI command execution.')
21
- shouldCheck = await confirm({
22
- message: 'Check rtk now?',
23
- default: true,
24
- })
25
- }
26
-
27
- if (!shouldCheck) {
28
- warn('Skipped rtk check (you can install it later)')
29
- return { optedIn: false, checked: false, available: false }
30
- }
31
-
32
- loading('checking rtk...')
33
-
34
- const available = await commandExists('rtk')
35
-
36
- if (available) {
37
- success('rtk is available')
38
- return { optedIn: true, checked: true, available: true }
39
- }
40
-
41
- warn('rtk not found on PATH.')
42
- console.log()
43
- info('rtk is required for agents to run CLI commands safely.')
44
- info('Install it from: https://github.com/rtk-ai/rtk#pre-built-binaries')
45
- console.log()
46
- info('After installing, verify with:')
47
- code(['rtk --version'])
48
- return { optedIn: true, checked: true, available: false }
49
- }
50
-
51
- export async function tokenOptimizationStep(options = {}) {
52
- header('Step 8, Token optimization tools')
53
-
54
- const defaultSelected = optimizationPreset.choices
55
- .filter(choice => choice.checked)
56
- .map(choice => choice.value)
57
- let selected = defaultSelected
58
-
59
- if (!options.skipPrompt && process.stdin.isTTY) {
60
- info(optimizationPreset.info)
61
- const timeoutMs = optimizationPreset.timeoutMs
62
- const choice = await Promise.race([
63
- checkbox({
64
- message: optimizationPreset.message,
65
- choices: optimizationPreset.choices,
66
- }),
67
- new Promise(resolve => setTimeout(() => resolve(defaultSelected), timeoutMs)),
68
- ])
69
- selected = Array.isArray(choice) ? choice : defaultSelected
70
- }
71
-
72
- loading('applying token optimization selections...')
73
-
74
- const has = value => selected.includes(value)
75
-
76
- const rtk = has('rtk')
77
- ? await checkRtk({ skipHeader: true, skipPrompt: true })
78
- : { optedIn: false, checked: false, available: false }
79
-
80
- const quota = has('quota')
81
- ? await installQuota({ skipHeader: true, skipPrompt: true })
82
- : { optedIn: false, installed: false }
83
-
84
- const caveman = has('caveman')
85
- ? await installCaveman({
86
- skipHeader: true,
87
- skipPrompt: true,
88
- })
89
- : { optedIn: false, installed: false }
90
-
91
- const cavemanGuidance = has('caveman')
92
- ? await enableCavemanGuidance(caveman)
93
- : { enabled: false }
94
-
95
- const obGlobal = await configureObGlobal(options.ctx || {}, { rtk, quota, caveman, cavemanGuidance })
96
-
97
- if (selected.length === 0) warn('No token optimization tools selected')
98
- else success('Token optimization step completed')
99
-
100
- return { rtk, quota, caveman, cavemanGuidance, obGlobal }
101
- }
1
+ import { checkbox, confirm } from '@inquirer/prompts'
2
+ import fse from 'fs-extra'
3
+ import path from 'path'
4
+ import { fileURLToPath } from 'url'
5
+ import { code, commandExists, header, info, loading, success, warn } from '../../utils/exec.js'
6
+ import { installQuota } from './quota.js'
7
+ import { installCaveman } from './caveman.js'
8
+ import { installCodegraph } from './codegraph.js'
9
+ import { enableCavemanGuidance } from './caveman-guidance.js'
10
+ import { configureObGlobal } from './global.js'
11
+
12
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
13
+ const OPTIMIZATION_PRESET_PATH = path.resolve(__dirname, '../../presets/optimization.json')
14
+ const optimizationPreset = await fse.readJson(OPTIMIZATION_PRESET_PATH)
15
+
16
+ export async function checkRtk(options = {}) {
17
+ if (!options.skipHeader) header('Checking rtk')
18
+
19
+ let shouldCheck = true
20
+ if (!options.skipPrompt) {
21
+ info('Recommended: install and verify rtk for safer agent CLI command execution.')
22
+ shouldCheck = await confirm({
23
+ message: 'Check rtk now?',
24
+ default: true,
25
+ })
26
+ }
27
+
28
+ if (!shouldCheck) {
29
+ warn('Skipped rtk check (you can install it later)')
30
+ return { optedIn: false, checked: false, available: false }
31
+ }
32
+
33
+ loading('checking rtk...')
34
+
35
+ const available = await commandExists('rtk')
36
+
37
+ if (available) {
38
+ success('rtk is available')
39
+ return { optedIn: true, checked: true, available: true }
40
+ }
41
+
42
+ warn('rtk not found on PATH.')
43
+ console.log()
44
+ info('rtk is required for agents to run CLI commands safely.')
45
+ info('Install it from: https://github.com/rtk-ai/rtk#pre-built-binaries')
46
+ console.log()
47
+ info('After installing, verify with:')
48
+ code(['rtk --version'])
49
+ return { optedIn: true, checked: true, available: false }
50
+ }
51
+
52
+ export async function tokenOptimizationStep(options = {}) {
53
+ header('Step 8, Token optimization tools')
54
+
55
+ const defaultSelected = optimizationPreset.choices
56
+ .filter(choice => choice.checked)
57
+ .map(choice => choice.value)
58
+ let selected = defaultSelected
59
+
60
+ if (!options.skipPrompt && process.stdin.isTTY) {
61
+ info(optimizationPreset.info)
62
+ const timeoutMs = optimizationPreset.timeoutMs
63
+ const choice = await Promise.race([
64
+ checkbox({
65
+ message: optimizationPreset.message,
66
+ choices: optimizationPreset.choices,
67
+ }),
68
+ new Promise(resolve => { setTimeout(() => resolve(defaultSelected), timeoutMs) }),
69
+ ])
70
+ selected = Array.isArray(choice) ? choice : defaultSelected
71
+ }
72
+
73
+ loading('applying token optimization selections...')
74
+
75
+ const installScope = options.ctx?.installScope || 'local'
76
+
77
+ const has = value => selected.includes(value)
78
+
79
+ const rtk = has('rtk')
80
+ ? await checkRtk({ skipHeader: true, skipPrompt: true })
81
+ : { optedIn: false, checked: false, available: false }
82
+
83
+ const quota = has('quota')
84
+ ? await installQuota({ skipHeader: true, skipPrompt: true })
85
+ : { optedIn: false, installed: false }
86
+
87
+ const caveman = has('caveman')
88
+ ? await installCaveman({
89
+ skipHeader: true,
90
+ skipPrompt: true,
91
+ installScope,
92
+ })
93
+ : { optedIn: false, installed: false }
94
+
95
+ const cavemanGuidance = has('caveman')
96
+ ? await enableCavemanGuidance(caveman)
97
+ : { enabled: false }
98
+
99
+ const codegraph = has('codegraph')
100
+ ? await installCodegraph({ skipHeader: true, installScope })
101
+ : { optedIn: false, installed: false }
102
+
103
+ const obGlobal = await configureObGlobal(options.ctx || {}, { rtk, quota, caveman, cavemanGuidance, codegraph })
104
+
105
+ if (selected.length === 0) warn('No token optimization tools selected')
106
+ else success('Token optimization step completed')
107
+
108
+ return { rtk, quota, caveman, cavemanGuidance, codegraph, obGlobal }
109
+ }
@@ -1,93 +1,101 @@
1
- import { beforeEach, describe, expect, it, vi } from 'vitest'
2
-
3
- vi.mock('@inquirer/prompts', () => ({
4
- checkbox: vi.fn(),
5
- confirm: vi.fn(),
6
- }))
7
-
8
- vi.mock('../../utils/exec.js', () => ({
9
- code: vi.fn(),
10
- commandExists: vi.fn(),
11
- header: vi.fn(),
12
- info: vi.fn(),
13
- loading: vi.fn(),
14
- success: vi.fn(),
15
- warn: vi.fn(),
16
- }))
17
-
18
- vi.mock('./quota.js', () => ({ installQuota: vi.fn() }))
19
- vi.mock('./caveman.js', () => ({ installCaveman: vi.fn() }))
20
- vi.mock('./caveman-guidance.js', () => ({ enableCavemanGuidance: vi.fn() }))
21
- vi.mock('./global.js', () => ({ configureObGlobal: vi.fn() }))
22
-
23
- vi.mock('fs-extra', () => ({
24
- default: {
25
- readJson: vi.fn().mockResolvedValue({
26
- info: 'Token optimization info',
27
- message: 'Select tools',
28
- timeoutMs: 5000,
29
- choices: [
30
- { value: 'rtk', checked: false },
31
- { value: 'quota', checked: false },
32
- { value: 'caveman', checked: false },
33
- ],
34
- }),
35
- },
36
- }))
37
-
38
- import { checkbox } from '@inquirer/prompts'
39
- import { commandExists, warn } from '../../utils/exec.js'
40
- import { installQuota } from './quota.js'
41
- import { installCaveman } from './caveman.js'
42
- import { enableCavemanGuidance } from './caveman-guidance.js'
43
- import { configureObGlobal } from './global.js'
44
- import { tokenOptimizationStep } from './index.js'
45
-
46
- describe('tokenOptimizationStep()', () => {
47
- beforeEach(() => {
48
- vi.clearAllMocks()
49
- })
50
-
51
- it('runs all optimizations by default selection', async () => {
52
- const originalIsTTY = process.stdin.isTTY
53
- Object.defineProperty(process.stdin, 'isTTY', { value: true, configurable: true })
54
-
55
- checkbox.mockResolvedValue(['rtk', 'quota', 'caveman'])
56
- commandExists.mockResolvedValue(true)
57
- installQuota.mockResolvedValue({ optedIn: true, installed: true })
58
- installCaveman.mockResolvedValue({ optedIn: true, installed: true })
59
- enableCavemanGuidance.mockResolvedValue({ enabled: true })
60
- configureObGlobal.mockResolvedValue({ configured: true })
61
-
62
- const result = await tokenOptimizationStep()
63
-
64
- Object.defineProperty(process.stdin, 'isTTY', { value: originalIsTTY, configurable: true })
65
-
66
- expect(commandExists).toHaveBeenCalledWith('rtk')
67
- expect(installQuota).toHaveBeenCalledWith({ skipHeader: true, skipPrompt: true })
68
- expect(installCaveman).toHaveBeenCalledWith({ skipHeader: true, skipPrompt: true })
69
- expect(enableCavemanGuidance).toHaveBeenCalledWith({ optedIn: true, installed: true })
70
- expect(configureObGlobal).toHaveBeenCalled()
71
- expect(result.rtk.available).toBe(true)
72
- expect(result.quota.installed).toBe(true)
73
- expect(result.caveman.installed).toBe(true)
74
- expect(result.cavemanGuidance.enabled).toBe(true)
75
- })
76
-
77
- it('skips all tools when nothing is selected', async () => {
78
- checkbox.mockResolvedValue([])
79
-
80
- const result = await tokenOptimizationStep()
81
-
82
- expect(commandExists).not.toHaveBeenCalled()
83
- expect(installQuota).not.toHaveBeenCalled()
84
- expect(installCaveman).not.toHaveBeenCalled()
85
- expect(enableCavemanGuidance).not.toHaveBeenCalled()
86
- expect(configureObGlobal).toHaveBeenCalled()
87
- expect(warn).toHaveBeenCalledWith('No token optimization tools selected')
88
- expect(result.rtk.optedIn).toBe(false)
89
- expect(result.quota.optedIn).toBe(false)
90
- expect(result.caveman.optedIn).toBe(false)
91
- expect(result.cavemanGuidance.enabled).toBe(false)
92
- })
93
- })
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
2
+
3
+ vi.mock('@inquirer/prompts', () => ({
4
+ checkbox: vi.fn(),
5
+ confirm: vi.fn(),
6
+ }))
7
+
8
+ vi.mock('../../utils/exec.js', () => ({
9
+ code: vi.fn(),
10
+ commandExists: vi.fn(),
11
+ header: vi.fn(),
12
+ info: vi.fn(),
13
+ loading: vi.fn(),
14
+ success: vi.fn(),
15
+ warn: vi.fn(),
16
+ }))
17
+
18
+ vi.mock('./quota.js', () => ({ installQuota: vi.fn() }))
19
+ vi.mock('./caveman.js', () => ({ installCaveman: vi.fn() }))
20
+ vi.mock('./codegraph.js', () => ({ installCodegraph: vi.fn() }))
21
+ vi.mock('./caveman-guidance.js', () => ({ enableCavemanGuidance: vi.fn() }))
22
+ vi.mock('./global.js', () => ({ configureObGlobal: vi.fn() }))
23
+
24
+ vi.mock('fs-extra', () => ({
25
+ default: {
26
+ readJson: vi.fn().mockResolvedValue({
27
+ info: 'Token optimization info',
28
+ message: 'Select tools',
29
+ timeoutMs: 5000,
30
+ choices: [
31
+ { value: 'rtk', checked: false },
32
+ { value: 'quota', checked: false },
33
+ { value: 'caveman', checked: false },
34
+ { value: 'codegraph', checked: false },
35
+ ],
36
+ }),
37
+ },
38
+ }))
39
+
40
+ import { checkbox } from '@inquirer/prompts'
41
+ import { commandExists, warn } from '../../utils/exec.js'
42
+ import { installQuota } from './quota.js'
43
+ import { installCaveman } from './caveman.js'
44
+ import { installCodegraph } from './codegraph.js'
45
+ import { enableCavemanGuidance } from './caveman-guidance.js'
46
+ import { configureObGlobal } from './global.js'
47
+ import { tokenOptimizationStep } from './index.js'
48
+
49
+ describe('tokenOptimizationStep()', () => {
50
+ beforeEach(() => {
51
+ vi.clearAllMocks()
52
+ })
53
+
54
+ it('runs all optimizations by default selection', async () => {
55
+ const originalIsTTY = process.stdin.isTTY
56
+ Object.defineProperty(process.stdin, 'isTTY', { value: true, configurable: true })
57
+
58
+ checkbox.mockResolvedValue(['rtk', 'quota', 'caveman', 'codegraph'])
59
+ commandExists.mockResolvedValue(true)
60
+ installQuota.mockResolvedValue({ optedIn: true, installed: true })
61
+ installCaveman.mockResolvedValue({ optedIn: true, installed: true })
62
+ installCodegraph.mockResolvedValue({ optedIn: true, installed: true })
63
+ enableCavemanGuidance.mockResolvedValue({ enabled: true })
64
+ configureObGlobal.mockResolvedValue({ configured: true })
65
+
66
+ const result = await tokenOptimizationStep()
67
+
68
+ Object.defineProperty(process.stdin, 'isTTY', { value: originalIsTTY, configurable: true })
69
+
70
+ expect(commandExists).toHaveBeenCalledWith('rtk')
71
+ expect(installQuota).toHaveBeenCalledWith({ skipHeader: true, skipPrompt: true })
72
+ expect(installCaveman).toHaveBeenCalledWith(expect.objectContaining({ skipHeader: true, skipPrompt: true }))
73
+ expect(installCodegraph).toHaveBeenCalledWith(expect.objectContaining({ skipHeader: true }))
74
+ expect(enableCavemanGuidance).toHaveBeenCalledWith({ optedIn: true, installed: true })
75
+ expect(configureObGlobal).toHaveBeenCalled()
76
+ expect(result.rtk.available).toBe(true)
77
+ expect(result.quota.installed).toBe(true)
78
+ expect(result.caveman.installed).toBe(true)
79
+ expect(result.cavemanGuidance.enabled).toBe(true)
80
+ expect(result.codegraph.installed).toBe(true)
81
+ })
82
+
83
+ it('skips all tools when nothing is selected', async () => {
84
+ checkbox.mockResolvedValue([])
85
+
86
+ const result = await tokenOptimizationStep()
87
+
88
+ expect(commandExists).not.toHaveBeenCalled()
89
+ expect(installQuota).not.toHaveBeenCalled()
90
+ expect(installCaveman).not.toHaveBeenCalled()
91
+ expect(installCodegraph).not.toHaveBeenCalled()
92
+ expect(enableCavemanGuidance).not.toHaveBeenCalled()
93
+ expect(configureObGlobal).toHaveBeenCalled()
94
+ expect(warn).toHaveBeenCalledWith('No token optimization tools selected')
95
+ expect(result.rtk.optedIn).toBe(false)
96
+ expect(result.quota.optedIn).toBe(false)
97
+ expect(result.caveman.optedIn).toBe(false)
98
+ expect(result.cavemanGuidance.enabled).toBe(false)
99
+ expect(result.codegraph.optedIn).toBe(false)
100
+ })
101
+ })
@@ -1,84 +1,84 @@
1
- import { confirm } from '@inquirer/prompts'
2
- import fse from 'fs-extra'
3
- import path from 'node:path'
4
- import { fileURLToPath } from 'url'
5
- import { error, header, info, loading, success, warn } from '../../utils/exec.js'
6
-
7
- const __dirname = path.dirname(fileURLToPath(import.meta.url))
8
- const QUOTA_PRESET_PATH = path.resolve(__dirname, '../../presets/quota.json')
9
- const quotaPreset = await fse.readJson(QUOTA_PRESET_PATH)
10
- const PLUGIN = quotaPreset.plugin
11
-
12
- function ensurePlugin(config) {
13
- if (!Array.isArray(config.plugin)) config.plugin = []
14
- if (!config.plugin.includes(PLUGIN)) config.plugin.push(PLUGIN)
15
- }
16
-
17
- function addIfMissing(target, key, value) {
18
- if (!(key in target)) target[key] = value
19
- }
20
-
21
- export async function installQuota(options = {}) {
22
- if (!options.skipHeader) header('Installing opencode-quota')
23
-
24
- let shouldInstall = true
25
- if (!options.skipPrompt && process.stdin.isTTY) {
26
- const timeoutMs = quotaPreset.prompt.timeoutMs
27
- const choice = await Promise.race([
28
- confirm({
29
- message: quotaPreset.prompt.message,
30
- default: quotaPreset.prompt.default,
31
- }),
32
- new Promise(resolve => setTimeout(() => resolve(true), timeoutMs)),
33
- ])
34
- shouldInstall = choice !== false
35
- }
36
-
37
- if (!shouldInstall) {
38
- warn('Skipped opencode-quota installation')
39
- return { optedIn: false, installed: false }
40
- }
41
-
42
- loading('configuring opencode-quota...')
43
-
44
- try {
45
- const opencodeDir = path.join(process.cwd(), '.opencode')
46
- const opencodePath = path.join(opencodeDir, 'opencode.json')
47
- const tuiPath = path.join(opencodeDir, 'tui.json')
48
- const quotaDir = path.join(opencodeDir, 'opencode-quota')
49
- const quotaPath = path.join(quotaDir, 'quota-toast.json')
50
-
51
- const opencode = await fse.pathExists(opencodePath)
52
- ? await fse.readJson(opencodePath)
53
- : { $schema: 'https://opencode.ai/config.json' }
54
-
55
- const tui = await fse.pathExists(tuiPath)
56
- ? await fse.readJson(tuiPath)
57
- : { $schema: 'https://opencode.ai/tui.json' }
58
-
59
- ensurePlugin(opencode)
60
- ensurePlugin(tui)
61
-
62
- await fse.ensureDir(opencodeDir)
63
- await fse.writeJson(opencodePath, opencode, { spaces: 2 })
64
- await fse.writeJson(tuiPath, tui, { spaces: 2 })
65
-
66
- const quotaConfig = await fse.pathExists(quotaPath)
67
- ? await fse.readJson(quotaPath)
68
- : {}
69
-
70
- for (const [key, value] of Object.entries(quotaPreset.defaults)) {
71
- addIfMissing(quotaConfig, key, value)
72
- }
73
-
74
- await fse.ensureDir(quotaDir)
75
- await fse.writeJson(quotaPath, quotaConfig, { spaces: 2 })
76
-
77
- success('opencode-quota configured (manual setup)')
78
- info('Restart OpenCode and run /quota to verify')
79
- return { optedIn: true, installed: true }
80
- } catch (err) {
81
- error(`Failed to configure opencode-quota: ${err.message}`)
82
- return { optedIn: true, installed: false }
83
- }
84
- }
1
+ import { confirm } from '@inquirer/prompts'
2
+ import fse from 'fs-extra'
3
+ import path from 'node:path'
4
+ import { fileURLToPath } from 'url'
5
+ import { error, header, info, loading, success, warn } from '../../utils/exec.js'
6
+
7
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
8
+ const QUOTA_PRESET_PATH = path.resolve(__dirname, '../../presets/quota.json')
9
+ const quotaPreset = await fse.readJson(QUOTA_PRESET_PATH)
10
+ const PLUGIN = quotaPreset.plugin
11
+
12
+ function ensurePlugin(config) {
13
+ if (!Array.isArray(config.plugin)) config.plugin = []
14
+ if (!config.plugin.includes(PLUGIN)) config.plugin.push(PLUGIN)
15
+ }
16
+
17
+ function addIfMissing(target, key, value) {
18
+ if (!(key in target)) target[key] = value
19
+ }
20
+
21
+ export async function installQuota(options = {}) {
22
+ if (!options.skipHeader) header('Installing opencode-quota')
23
+
24
+ let shouldInstall = true
25
+ if (!options.skipPrompt && process.stdin.isTTY) {
26
+ const timeoutMs = quotaPreset.prompt.timeoutMs
27
+ const choice = await Promise.race([
28
+ confirm({
29
+ message: quotaPreset.prompt.message,
30
+ default: quotaPreset.prompt.default,
31
+ }),
32
+ new Promise(resolve => { setTimeout(() => resolve(true), timeoutMs) }),
33
+ ])
34
+ shouldInstall = choice !== false
35
+ }
36
+
37
+ if (!shouldInstall) {
38
+ warn('Skipped opencode-quota installation')
39
+ return { optedIn: false, installed: false }
40
+ }
41
+
42
+ loading('configuring opencode-quota...')
43
+
44
+ try {
45
+ const opencodeDir = path.join(process.cwd(), '.opencode')
46
+ const opencodePath = path.join(opencodeDir, 'opencode.json')
47
+ const tuiPath = path.join(opencodeDir, 'tui.json')
48
+ const quotaDir = path.join(opencodeDir, 'opencode-quota')
49
+ const quotaPath = path.join(quotaDir, 'quota-toast.json')
50
+
51
+ const opencode = await fse.pathExists(opencodePath)
52
+ ? await fse.readJson(opencodePath)
53
+ : { $schema: 'https://opencode.ai/config.json' }
54
+
55
+ const tui = await fse.pathExists(tuiPath)
56
+ ? await fse.readJson(tuiPath)
57
+ : { $schema: 'https://opencode.ai/tui.json' }
58
+
59
+ ensurePlugin(opencode)
60
+ ensurePlugin(tui)
61
+
62
+ await fse.ensureDir(opencodeDir)
63
+ await fse.writeJson(opencodePath, opencode, { spaces: 2 })
64
+ await fse.writeJson(tuiPath, tui, { spaces: 2 })
65
+
66
+ const quotaConfig = await fse.pathExists(quotaPath)
67
+ ? await fse.readJson(quotaPath)
68
+ : {}
69
+
70
+ for (const [key, value] of Object.entries(quotaPreset.defaults)) {
71
+ addIfMissing(quotaConfig, key, value)
72
+ }
73
+
74
+ await fse.ensureDir(quotaDir)
75
+ await fse.writeJson(quotaPath, quotaConfig, { spaces: 2 })
76
+
77
+ success('opencode-quota configured (manual setup)')
78
+ info('Restart OpenCode and run /quota to verify')
79
+ return { optedIn: true, installed: true }
80
+ } catch (err) {
81
+ error(`Failed to configure opencode-quota: ${err.message}`)
82
+ return { optedIn: true, installed: false }
83
+ }
84
+ }