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.
- package/README.md +266 -214
- package/content/.agents/agents/basic-engineer.md +30 -0
- package/content/.agents/agents/devops-manager.md +38 -29
- package/content/.agents/session-log.json +41 -0
- package/content/.agents/skills/ob-default/SKILL.md +21 -0
- package/content/.agents/skills/ob-generic-guardrails/SKILL.md +32 -0
- package/content/.agents/skills/ob-global/SKILL.md +49 -0
- package/content/.agents/skills/ob-pullrequest-az/SKILL.md +11 -21
- package/content/.agents/skills/ob-pullrequest-gh/SKILL.md +14 -24
- package/content/.agents/skills/ob-userstory-az/SKILL.md +8 -14
- package/content/.agents/skills/ob-userstory-gh/SKILL.md +6 -14
- package/content/.opencode/commands/opsx-apply.md +50 -33
- package/content/.opencode/opencode.json +3 -3
- package/content/.opencode/plugins/session-log.js +1 -1
- package/content/.opencode/skills/openspec-apply-change/SKILL.md +50 -33
- package/content/AGENTS.md +95 -141
- package/content/skills-lock.json +4 -0
- package/package.json +6 -1
- package/src/index.js +112 -191
- package/src/presets/browser.json +18 -0
- package/src/presets/clean.json +21 -0
- package/src/presets/models.json +33 -0
- package/src/presets/optimization.json +22 -0
- package/src/presets/platforms.json +29 -2
- package/src/presets/quota.json +14 -0
- package/src/presets/source.json +17 -0
- package/src/steps/browser/browser.test.js +81 -0
- package/src/steps/{install-browser.js → browser/index.js} +12 -15
- package/src/steps/{__tests__/clean-ai-files.test.js → clean/clean.test.js} +28 -13
- package/src/steps/{clean-ai-files.js → clean/index.js} +32 -30
- package/src/steps/{patch-agents-md.js → copy/agents.js} +41 -20
- package/src/steps/{__tests__/copy-content.test.js → copy/copy.test.js} +10 -1
- package/src/steps/copy/index.js +33 -0
- package/src/steps/copy/skills.js +55 -0
- package/src/steps/{write-onboard-config.js → metadata/index.js} +3 -3
- package/src/steps/metadata/metadata.test.js +96 -0
- package/src/steps/models/format.js +60 -0
- package/src/steps/models/format.test.js +74 -0
- package/src/steps/models/index.js +52 -0
- package/src/steps/models/write.js +54 -0
- package/src/steps/models/write.test.js +119 -0
- package/src/steps/{init-openspec.js → openspec/ensemble.js} +27 -61
- package/src/steps/openspec/ensemble.test.js +79 -0
- package/src/steps/openspec/index.js +32 -0
- package/src/steps/optimization/caveman-guidance.js +11 -0
- package/src/steps/{install-caveman.js → optimization/caveman.js} +5 -19
- package/src/steps/optimization/global.js +64 -0
- package/src/steps/optimization/index.js +101 -0
- package/src/steps/{__tests__/token-optimization.test.js → optimization/optimization.test.js} +19 -24
- package/src/steps/{install-quota.js → optimization/quota.js} +12 -10
- package/src/steps/platform/index.js +81 -0
- package/src/steps/platform/platform.test.js +129 -0
- package/src/steps/{choose-source-scope.js → source/index.js} +11 -17
- package/src/steps/source/source.test.js +89 -0
- package/src/utils/__tests__/copy.test.js +12 -5
- package/src/utils/copy.js +4 -24
- package/src/utils/exec-spinner.js +47 -0
- package/src/utils/exec.js +120 -162
- package/src/utils/models-cache.js +25 -68
- package/src/utils/models-pricing.js +42 -0
- package/src/utils/models-pricing.test.js +94 -0
- package/content/.agents/agents/back-engineer.md +0 -87
- package/content/.agents/agents/front-engineer.md +0 -86
- package/content/.agents/agents/infra-engineer.md +0 -85
- package/content/.agents/agents/quality-engineer.md +0 -86
- package/content/.agents/agents/security-auditor.md +0 -86
- package/src/steps/__tests__/check-env.test.js +0 -70
- package/src/steps/__tests__/check-platform.test.js +0 -104
- package/src/steps/__tests__/check-rtk.test.js +0 -38
- package/src/steps/__tests__/choose-platform.test.js +0 -38
- package/src/steps/check-env.js +0 -26
- package/src/steps/check-platform.js +0 -80
- package/src/steps/check-rtk.js +0 -38
- package/src/steps/choose-models.js +0 -163
- package/src/steps/choose-platform.js +0 -22
- package/src/steps/choose-skills-provider.js +0 -79
- package/src/steps/copy-content.js +0 -89
- package/src/steps/enable-caveman-guidance.js +0 -93
- package/src/steps/token-optimization.js +0 -59
|
@@ -0,0 +1,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 { 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
|
+
}
|
package/src/steps/{__tests__/token-optimization.test.js → optimization/optimization.test.js}
RENAMED
|
@@ -2,9 +2,12 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
2
2
|
|
|
3
3
|
vi.mock('@inquirer/prompts', () => ({
|
|
4
4
|
checkbox: vi.fn(),
|
|
5
|
+
confirm: vi.fn(),
|
|
5
6
|
}))
|
|
6
7
|
|
|
7
8
|
vi.mock('../../utils/exec.js', () => ({
|
|
9
|
+
code: vi.fn(),
|
|
10
|
+
commandExists: vi.fn(),
|
|
8
11
|
header: vi.fn(),
|
|
9
12
|
info: vi.fn(),
|
|
10
13
|
loading: vi.fn(),
|
|
@@ -12,29 +15,18 @@ vi.mock('../../utils/exec.js', () => ({
|
|
|
12
15
|
warn: vi.fn(),
|
|
13
16
|
}))
|
|
14
17
|
|
|
15
|
-
vi.mock('
|
|
16
|
-
|
|
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
|
-
}))
|
|
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() }))
|
|
30
22
|
|
|
31
23
|
import { checkbox } from '@inquirer/prompts'
|
|
32
|
-
import { warn } from '../../utils/exec.js'
|
|
33
|
-
import {
|
|
34
|
-
import {
|
|
35
|
-
import {
|
|
36
|
-
import {
|
|
37
|
-
import { tokenOptimizationStep } from '
|
|
24
|
+
import { commandExists, warn } from '../../utils/exec.js'
|
|
25
|
+
import { installQuota } from './quota.js'
|
|
26
|
+
import { installCaveman } from './caveman.js'
|
|
27
|
+
import { enableCavemanGuidance } from './caveman-guidance.js'
|
|
28
|
+
import { configureObGlobal } from './global.js'
|
|
29
|
+
import { tokenOptimizationStep } from './index.js'
|
|
38
30
|
|
|
39
31
|
describe('tokenOptimizationStep()', () => {
|
|
40
32
|
beforeEach(() => {
|
|
@@ -43,17 +35,19 @@ describe('tokenOptimizationStep()', () => {
|
|
|
43
35
|
|
|
44
36
|
it('runs all optimizations by default selection', async () => {
|
|
45
37
|
checkbox.mockResolvedValue(['rtk', 'quota', 'caveman'])
|
|
46
|
-
|
|
38
|
+
commandExists.mockResolvedValue(true)
|
|
47
39
|
installQuota.mockResolvedValue({ optedIn: true, installed: true })
|
|
48
40
|
installCaveman.mockResolvedValue({ optedIn: true, installed: true })
|
|
49
41
|
enableCavemanGuidance.mockResolvedValue({ enabled: true })
|
|
42
|
+
configureObGlobal.mockResolvedValue({ configured: true })
|
|
50
43
|
|
|
51
44
|
const result = await tokenOptimizationStep()
|
|
52
45
|
|
|
53
|
-
expect(
|
|
46
|
+
expect(commandExists).toHaveBeenCalledWith('rtk')
|
|
54
47
|
expect(installQuota).toHaveBeenCalledWith({ skipHeader: true, skipPrompt: true })
|
|
55
48
|
expect(installCaveman).toHaveBeenCalledWith({ skipHeader: true, skipPrompt: true })
|
|
56
49
|
expect(enableCavemanGuidance).toHaveBeenCalledWith({ optedIn: true, installed: true })
|
|
50
|
+
expect(configureObGlobal).toHaveBeenCalled()
|
|
57
51
|
expect(result.rtk.available).toBe(true)
|
|
58
52
|
expect(result.quota.installed).toBe(true)
|
|
59
53
|
expect(result.caveman.installed).toBe(true)
|
|
@@ -65,10 +59,11 @@ describe('tokenOptimizationStep()', () => {
|
|
|
65
59
|
|
|
66
60
|
const result = await tokenOptimizationStep()
|
|
67
61
|
|
|
68
|
-
expect(
|
|
62
|
+
expect(commandExists).not.toHaveBeenCalled()
|
|
69
63
|
expect(installQuota).not.toHaveBeenCalled()
|
|
70
64
|
expect(installCaveman).not.toHaveBeenCalled()
|
|
71
65
|
expect(enableCavemanGuidance).not.toHaveBeenCalled()
|
|
66
|
+
expect(configureObGlobal).toHaveBeenCalled()
|
|
72
67
|
expect(warn).toHaveBeenCalledWith('No token optimization tools selected')
|
|
73
68
|
expect(result.rtk.optedIn).toBe(false)
|
|
74
69
|
expect(result.quota.optedIn).toBe(false)
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { confirm } from '@inquirer/prompts'
|
|
2
2
|
import fse from 'fs-extra'
|
|
3
3
|
import path from 'node:path'
|
|
4
|
-
import {
|
|
4
|
+
import { fileURLToPath } from 'url'
|
|
5
|
+
import { error, header, info, loading, success, warn } from '../../utils/exec.js'
|
|
5
6
|
|
|
6
|
-
const
|
|
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
|
|
7
11
|
|
|
8
12
|
function ensurePlugin(config) {
|
|
9
13
|
if (!Array.isArray(config.plugin)) config.plugin = []
|
|
@@ -19,11 +23,11 @@ export async function installQuota(options = {}) {
|
|
|
19
23
|
|
|
20
24
|
let shouldInstall = true
|
|
21
25
|
if (!options.skipPrompt && process.stdin.isTTY) {
|
|
22
|
-
const timeoutMs =
|
|
26
|
+
const timeoutMs = quotaPreset.prompt.timeoutMs
|
|
23
27
|
const choice = await Promise.race([
|
|
24
28
|
confirm({
|
|
25
|
-
message:
|
|
26
|
-
default:
|
|
29
|
+
message: quotaPreset.prompt.message,
|
|
30
|
+
default: quotaPreset.prompt.default,
|
|
27
31
|
}),
|
|
28
32
|
new Promise(resolve => setTimeout(() => resolve(true), timeoutMs)),
|
|
29
33
|
])
|
|
@@ -63,11 +67,9 @@ export async function installQuota(options = {}) {
|
|
|
63
67
|
? await fse.readJson(quotaPath)
|
|
64
68
|
: {}
|
|
65
69
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
addIfMissing(quotaConfig, 'percentDisplayMode', 'used')
|
|
70
|
-
addIfMissing(quotaConfig, 'showSessionTokens', true)
|
|
70
|
+
for (const [key, value] of Object.entries(quotaPreset.defaults)) {
|
|
71
|
+
addIfMissing(quotaConfig, key, value)
|
|
72
|
+
}
|
|
71
73
|
|
|
72
74
|
await fse.ensureDir(quotaDir)
|
|
73
75
|
await fse.writeJson(quotaPath, quotaConfig, { spaces: 2 })
|
|
@@ -0,0 +1,81 @@
|
|
|
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 { code, commandExists, header, info, success, warn } from '../../utils/exec.js'
|
|
7
|
+
|
|
8
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
9
|
+
const PLATFORMS_PRESET_PATH = path.resolve(__dirname, '../../presets/platforms.json')
|
|
10
|
+
const platformsPreset = await fse.readJson(PLATFORMS_PRESET_PATH)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
export async function checkPlatform(platform) {
|
|
14
|
+
const preset = platformsPreset.find(p => p.value === platform) || platformsPreset[0]
|
|
15
|
+
await checkPlatformCli(preset)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function choosePlatform() {
|
|
19
|
+
header('Step 3, Version control platform')
|
|
20
|
+
|
|
21
|
+
const platform = await select({
|
|
22
|
+
message: 'Which platform are you using?',
|
|
23
|
+
choices: platformsPreset.map(p => ({ name: p.name, value: p.value })),
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
success(`Platform: ${platform === 'github' ? 'GitHub' : 'Azure DevOps'}`)
|
|
27
|
+
await checkPlatform(platform)
|
|
28
|
+
return platform
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function checkPlatformCli(platformPreset) {
|
|
32
|
+
const { cli } = platformPreset
|
|
33
|
+
header(`Step 4, Checking ${platformPreset.name} CLI`)
|
|
34
|
+
|
|
35
|
+
const hasCli = await commandExists(cli.command)
|
|
36
|
+
if (!hasCli) {
|
|
37
|
+
warn(`${cli.displayName} not found.`)
|
|
38
|
+
info(`Install from: ${cli.installUrl}`)
|
|
39
|
+
if (cli.authCheck?.commands?.length) {
|
|
40
|
+
console.log()
|
|
41
|
+
info('After installing, authenticate with:')
|
|
42
|
+
code(cli.authCheck.commands)
|
|
43
|
+
}
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
success(`${cli.displayName} available`)
|
|
47
|
+
|
|
48
|
+
if (cli.authCheck) await runAuthCheck(cli)
|
|
49
|
+
if (cli.extensionCheck) await runExtensionCheck(cli)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function runAuthCheck(cli) {
|
|
53
|
+
try {
|
|
54
|
+
const result = await execa(cli.command, cli.authCheck.args, { reject: false })
|
|
55
|
+
if (result.exitCode === 0) {
|
|
56
|
+
success(`${cli.displayName} authenticated`)
|
|
57
|
+
} else {
|
|
58
|
+
warn(cli.authCheck.notAuthenticatedMessage)
|
|
59
|
+
code(cli.authCheck.commands)
|
|
60
|
+
}
|
|
61
|
+
} catch {
|
|
62
|
+
warn(`Could not check ${cli.command} auth status.`)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function runExtensionCheck(cli) {
|
|
67
|
+
try {
|
|
68
|
+
const result = await execa(cli.command, cli.extensionCheck.args, { reject: false })
|
|
69
|
+
const hasExtension = result.stdout && result.stdout.includes(cli.extensionCheck.expectedOutput)
|
|
70
|
+
|
|
71
|
+
if (hasExtension) {
|
|
72
|
+
success(`${cli.extensionCheck.expectedOutput} extension installed`)
|
|
73
|
+
} else {
|
|
74
|
+
warn(cli.extensionCheck.missingMessage)
|
|
75
|
+
code(cli.extensionCheck.commands)
|
|
76
|
+
}
|
|
77
|
+
} catch {
|
|
78
|
+
warn(cli.extensionCheck.errorMessage)
|
|
79
|
+
code(cli.extensionCheck.commands)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
2
|
+
|
|
3
|
+
vi.mock('../../utils/exec.js', () => ({
|
|
4
|
+
code: vi.fn(),
|
|
5
|
+
commandExists: vi.fn(),
|
|
6
|
+
header: vi.fn(),
|
|
7
|
+
info: vi.fn(),
|
|
8
|
+
success: vi.fn(),
|
|
9
|
+
warn: vi.fn(),
|
|
10
|
+
}))
|
|
11
|
+
|
|
12
|
+
vi.mock('@inquirer/prompts', () => ({
|
|
13
|
+
select: vi.fn(),
|
|
14
|
+
}))
|
|
15
|
+
|
|
16
|
+
vi.mock('execa', () => ({
|
|
17
|
+
execa: vi.fn(),
|
|
18
|
+
}))
|
|
19
|
+
|
|
20
|
+
import { select } from '@inquirer/prompts'
|
|
21
|
+
import { execa } from 'execa'
|
|
22
|
+
import { commandExists, success, warn } from '../../utils/exec.js'
|
|
23
|
+
import { checkPlatform, choosePlatform } from './index.js'
|
|
24
|
+
|
|
25
|
+
describe('choosePlatform()', () => {
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
vi.clearAllMocks()
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('returns "github" when user selects GitHub', async () => {
|
|
31
|
+
select.mockResolvedValue('github')
|
|
32
|
+
|
|
33
|
+
const result = await choosePlatform()
|
|
34
|
+
|
|
35
|
+
expect(result).toBe('github')
|
|
36
|
+
expect(success).toHaveBeenCalledWith('Platform: GitHub')
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('returns "azure" when user selects Azure DevOps', async () => {
|
|
40
|
+
select.mockResolvedValue('azure')
|
|
41
|
+
|
|
42
|
+
const result = await choosePlatform()
|
|
43
|
+
|
|
44
|
+
expect(result).toBe('azure')
|
|
45
|
+
expect(success).toHaveBeenCalledWith('Platform: Azure DevOps')
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
describe('checkPlatform()', () => {
|
|
49
|
+
describe('github path', () => {
|
|
50
|
+
it('prints success when gh is installed and authenticated', async () => {
|
|
51
|
+
commandExists.mockResolvedValue(true)
|
|
52
|
+
execa.mockResolvedValue({ exitCode: 0, stdout: '' })
|
|
53
|
+
|
|
54
|
+
await checkPlatform('github')
|
|
55
|
+
|
|
56
|
+
expect(success).toHaveBeenCalledWith('GitHub CLI (gh) available')
|
|
57
|
+
expect(success).toHaveBeenCalledWith('GitHub CLI authenticated')
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('warns when gh is installed but not authenticated', async () => {
|
|
61
|
+
commandExists.mockResolvedValue(true)
|
|
62
|
+
execa.mockResolvedValue({ exitCode: 1, stdout: '' })
|
|
63
|
+
|
|
64
|
+
await checkPlatform('github')
|
|
65
|
+
|
|
66
|
+
expect(success).toHaveBeenCalledWith('GitHub CLI (gh) available')
|
|
67
|
+
expect(warn).toHaveBeenCalledWith(expect.stringContaining('not authenticated'))
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('warns when gh is not installed', async () => {
|
|
71
|
+
commandExists.mockResolvedValue(false)
|
|
72
|
+
|
|
73
|
+
await checkPlatform('github')
|
|
74
|
+
|
|
75
|
+
expect(warn).toHaveBeenCalledWith('GitHub CLI (gh) not found.')
|
|
76
|
+
expect(success).not.toHaveBeenCalled()
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('warns when gh auth status check throws', async () => {
|
|
80
|
+
commandExists.mockResolvedValue(true)
|
|
81
|
+
execa.mockRejectedValue(new Error('spawn error'))
|
|
82
|
+
|
|
83
|
+
await checkPlatform('github')
|
|
84
|
+
|
|
85
|
+
expect(warn).toHaveBeenCalledWith('Could not check gh auth status.')
|
|
86
|
+
})
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
describe('azure path', () => {
|
|
90
|
+
it('prints success when az is installed and azure-devops extension present', async () => {
|
|
91
|
+
commandExists.mockResolvedValue(true)
|
|
92
|
+
execa.mockResolvedValue({ exitCode: 0, stdout: 'azure-devops\tsome info' })
|
|
93
|
+
|
|
94
|
+
await checkPlatform('azure')
|
|
95
|
+
|
|
96
|
+
expect(success).toHaveBeenCalledWith('Azure CLI (az) available')
|
|
97
|
+
expect(success).toHaveBeenCalledWith('azure-devops extension installed')
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('warns when az is installed but azure-devops extension is missing', async () => {
|
|
101
|
+
commandExists.mockResolvedValue(true)
|
|
102
|
+
execa.mockResolvedValue({ exitCode: 0, stdout: '' })
|
|
103
|
+
|
|
104
|
+
await checkPlatform('azure')
|
|
105
|
+
|
|
106
|
+
expect(success).toHaveBeenCalledWith('Azure CLI (az) available')
|
|
107
|
+
expect(warn).toHaveBeenCalledWith(expect.stringContaining('azure-devops extension not found'))
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('warns when az is not installed', async () => {
|
|
111
|
+
commandExists.mockResolvedValue(false)
|
|
112
|
+
|
|
113
|
+
await checkPlatform('azure')
|
|
114
|
+
|
|
115
|
+
expect(warn).toHaveBeenCalledWith('Azure CLI (az) not found.')
|
|
116
|
+
expect(success).not.toHaveBeenCalled()
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('warns when extension check throws', async () => {
|
|
120
|
+
commandExists.mockResolvedValue(true)
|
|
121
|
+
execa.mockRejectedValue(new Error('spawn error'))
|
|
122
|
+
|
|
123
|
+
await checkPlatform('azure')
|
|
124
|
+
|
|
125
|
+
expect(warn).toHaveBeenCalledWith('Could not check azure-devops extension. Run:')
|
|
126
|
+
})
|
|
127
|
+
})
|
|
128
|
+
})
|
|
129
|
+
})
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { checkbox, select } from '@inquirer/prompts'
|
|
2
2
|
import fse from 'fs-extra'
|
|
3
3
|
import path from 'path'
|
|
4
|
-
import {
|
|
4
|
+
import { fileURLToPath } from 'url'
|
|
5
|
+
import { header, info, success, warn } from '../../utils/exec.js'
|
|
6
|
+
|
|
7
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
8
|
+
const SOURCE_PRESET_PATH = path.resolve(__dirname, '../../presets/source.json')
|
|
9
|
+
const sourcePreset = await fse.readJson(SOURCE_PRESET_PATH)
|
|
5
10
|
|
|
6
11
|
async function listParentFolders(cwd) {
|
|
7
12
|
const parent = path.resolve(cwd, '..')
|
|
@@ -26,26 +31,15 @@ async function listParentFolders(cwd) {
|
|
|
26
31
|
}
|
|
27
32
|
|
|
28
33
|
export async function chooseSourceScope() {
|
|
29
|
-
header('Step
|
|
34
|
+
header('Step 1, Source code scope')
|
|
30
35
|
|
|
31
36
|
const cwd = process.cwd()
|
|
32
37
|
info('Choose where agents should read source code from during init analysis.')
|
|
33
38
|
|
|
34
39
|
const mode = await select({
|
|
35
|
-
message:
|
|
36
|
-
default:
|
|
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
|
-
],
|
|
40
|
+
message: sourcePreset.message,
|
|
41
|
+
default: sourcePreset.default,
|
|
42
|
+
choices: sourcePreset.choices,
|
|
49
43
|
})
|
|
50
44
|
|
|
51
45
|
if (mode === 'current') {
|
|
@@ -61,7 +55,7 @@ export async function chooseSourceScope() {
|
|
|
61
55
|
}
|
|
62
56
|
|
|
63
57
|
const selected = await checkbox({
|
|
64
|
-
message:
|
|
58
|
+
message: sourcePreset.parentSelectionMessage,
|
|
65
59
|
choices: parentFolders.map(d => ({
|
|
66
60
|
name: `../${d.name}`,
|
|
67
61
|
value: d.abs,
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import os from 'node:os'
|
|
5
|
+
|
|
6
|
+
vi.mock('../../utils/exec.js', () => ({
|
|
7
|
+
header: vi.fn(),
|
|
8
|
+
info: vi.fn(),
|
|
9
|
+
success: vi.fn(),
|
|
10
|
+
warn: vi.fn(),
|
|
11
|
+
}))
|
|
12
|
+
|
|
13
|
+
vi.mock('@inquirer/prompts', () => ({
|
|
14
|
+
select: vi.fn(),
|
|
15
|
+
checkbox: vi.fn(),
|
|
16
|
+
}))
|
|
17
|
+
|
|
18
|
+
vi.mock('fs-extra', () => ({
|
|
19
|
+
default: {
|
|
20
|
+
readJson: vi.fn().mockResolvedValue({
|
|
21
|
+
message: 'Select source scope',
|
|
22
|
+
default: 'current',
|
|
23
|
+
choices: [
|
|
24
|
+
{ name: 'Current folder', value: 'current' },
|
|
25
|
+
{ name: 'Parent folder', value: 'parent' },
|
|
26
|
+
],
|
|
27
|
+
parentSelectionMessage: 'Select sibling folders',
|
|
28
|
+
}),
|
|
29
|
+
readdir: vi.fn(),
|
|
30
|
+
stat: vi.fn(),
|
|
31
|
+
},
|
|
32
|
+
}))
|
|
33
|
+
|
|
34
|
+
import { select, checkbox } from '@inquirer/prompts'
|
|
35
|
+
import fse from 'fs-extra'
|
|
36
|
+
import { chooseSourceScope } from './index.js'
|
|
37
|
+
|
|
38
|
+
describe('chooseSourceScope()', () => {
|
|
39
|
+
let tmpDir
|
|
40
|
+
|
|
41
|
+
beforeEach(() => {
|
|
42
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'source-test-'))
|
|
43
|
+
process.chdir(tmpDir)
|
|
44
|
+
vi.clearAllMocks()
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
afterEach(() => {
|
|
48
|
+
fs.rmSync(tmpDir, { recursive: true, force: true })
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('returns current folder when user selects current mode', async () => {
|
|
52
|
+
select.mockResolvedValue('current')
|
|
53
|
+
|
|
54
|
+
const result = await chooseSourceScope()
|
|
55
|
+
|
|
56
|
+
expect(result.sourceMode).toBe('current')
|
|
57
|
+
expect(result.sourceRoots).toContain(tmpDir)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('lists parent folders when user selects parent mode', async () => {
|
|
61
|
+
select.mockResolvedValue('parent')
|
|
62
|
+
const parentDir = path.dirname(tmpDir)
|
|
63
|
+
const siblingDir = path.join(parentDir, 'sibling-project')
|
|
64
|
+
fs.mkdirSync(siblingDir)
|
|
65
|
+
fse.readdir.mockResolvedValue(['sibling-project'])
|
|
66
|
+
|
|
67
|
+
await chooseSourceScope()
|
|
68
|
+
|
|
69
|
+
expect(checkbox).toHaveBeenCalled()
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('falls back to current when no siblings found', async () => {
|
|
73
|
+
select.mockResolvedValue('parent')
|
|
74
|
+
fse.readdir.mockResolvedValue([])
|
|
75
|
+
|
|
76
|
+
const result = await chooseSourceScope()
|
|
77
|
+
|
|
78
|
+
expect(result.sourceMode).toBe('current')
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('falls back to current when no folders selected', async () => {
|
|
82
|
+
select.mockResolvedValue('parent')
|
|
83
|
+
checkbox.mockResolvedValue([])
|
|
84
|
+
|
|
85
|
+
const result = await chooseSourceScope()
|
|
86
|
+
|
|
87
|
+
expect(result.sourceMode).toBe('current')
|
|
88
|
+
})
|
|
89
|
+
})
|
|
@@ -7,6 +7,13 @@ import fse from 'fs-extra'
|
|
|
7
7
|
import { copyContent, findAiFiles } from '../copy.js'
|
|
8
8
|
|
|
9
9
|
const tmpDir = () => fse.mkdtempSync(path.join(os.tmpdir(), 'ob-test-'))
|
|
10
|
+
const aiFiles = [
|
|
11
|
+
'AGENTS.md',
|
|
12
|
+
'CLAUDE.md',
|
|
13
|
+
'.cursorrules',
|
|
14
|
+
'.clinerules',
|
|
15
|
+
'.github/copilot-instructions.md',
|
|
16
|
+
]
|
|
10
17
|
|
|
11
18
|
describe('copy utils', () => {
|
|
12
19
|
describe('findAiFiles()', () => {
|
|
@@ -21,20 +28,20 @@ describe('copy utils', () => {
|
|
|
21
28
|
})
|
|
22
29
|
|
|
23
30
|
it('returns empty array when no AI files exist', async () => {
|
|
24
|
-
const found = await findAiFiles(dir)
|
|
31
|
+
const found = await findAiFiles(dir, aiFiles)
|
|
25
32
|
expect(found).toEqual([])
|
|
26
33
|
})
|
|
27
34
|
|
|
28
35
|
it('detects AGENTS.md', async () => {
|
|
29
36
|
await fse.writeFile(path.join(dir, 'AGENTS.md'), '# agents')
|
|
30
|
-
const found = await findAiFiles(dir)
|
|
37
|
+
const found = await findAiFiles(dir, aiFiles)
|
|
31
38
|
expect(found).toHaveLength(1)
|
|
32
39
|
expect(found[0]).toContain('AGENTS.md')
|
|
33
40
|
})
|
|
34
41
|
|
|
35
42
|
it('detects CLAUDE.md', async () => {
|
|
36
43
|
await fse.writeFile(path.join(dir, 'CLAUDE.md'), '# claude')
|
|
37
|
-
const found = await findAiFiles(dir)
|
|
44
|
+
const found = await findAiFiles(dir, aiFiles)
|
|
38
45
|
expect(found).toHaveLength(1)
|
|
39
46
|
expect(found[0]).toContain('CLAUDE.md')
|
|
40
47
|
})
|
|
@@ -43,7 +50,7 @@ describe('copy utils', () => {
|
|
|
43
50
|
await fse.writeFile(path.join(dir, 'AGENTS.md'), '')
|
|
44
51
|
await fse.writeFile(path.join(dir, '.cursorrules'), '')
|
|
45
52
|
await fse.writeFile(path.join(dir, '.clinerules'), '')
|
|
46
|
-
const found = await findAiFiles(dir)
|
|
53
|
+
const found = await findAiFiles(dir, aiFiles)
|
|
47
54
|
expect(found).toHaveLength(3)
|
|
48
55
|
})
|
|
49
56
|
|
|
@@ -51,7 +58,7 @@ describe('copy utils', () => {
|
|
|
51
58
|
const ghDir = path.join(dir, '.github')
|
|
52
59
|
await fse.ensureDir(ghDir)
|
|
53
60
|
await fse.writeFile(path.join(ghDir, 'copilot-instructions.md'), '')
|
|
54
|
-
const found = await findAiFiles(dir)
|
|
61
|
+
const found = await findAiFiles(dir, aiFiles)
|
|
55
62
|
expect(found).toHaveLength(1)
|
|
56
63
|
expect(found[0]).toContain('copilot-instructions.md')
|
|
57
64
|
})
|