opencode-onboard 0.4.3 → 0.4.4
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 +41 -40
- package/content/.agents/agents/devops-manager.md +123 -123
- package/content/.agents/skills/ob-default/SKILL.md +25 -21
- package/content/.agents/skills/ob-generic-guardrails/SKILL.md +36 -32
- package/content/.agents/skills/ob-global/SKILL.md +92 -84
- package/content/.agents/skills/ob-pullrequest-az/SKILL.md +168 -160
- package/content/.agents/skills/ob-pullrequest-gh/SKILL.md +140 -136
- package/content/.opencode/commands/create-engineer.md +109 -0
- package/content/.opencode/plugins/session-log.js +523 -519
- package/content/AGENTS.md +23 -21
- package/package.json +1 -1
- package/src/commands/wizard.js +124 -113
- package/src/presets/browser.json +22 -18
- package/src/presets/optimization.json +27 -22
- package/src/steps/browser/browser.test.js +115 -81
- package/src/steps/browser/index.js +62 -54
- package/src/steps/clean/index.js +108 -107
- package/src/steps/metadata/index.js +63 -62
- package/src/steps/models/format.js +61 -60
- package/src/steps/models/write.test.js +117 -117
- package/src/steps/openspec/ensemble.test.js +79 -79
- package/src/steps/openspec/index.js +121 -32
- package/src/steps/openspec/index.test.js +63 -0
- package/src/steps/optimization/caveman.js +34 -29
- package/src/steps/optimization/codegraph.js +52 -0
- package/src/steps/optimization/global.js +88 -64
- package/src/steps/optimization/global.test.js +99 -0
- package/src/steps/optimization/index.js +109 -101
- package/src/steps/optimization/optimization.test.js +101 -93
- package/src/steps/optimization/quota.js +84 -84
- package/src/steps/source/source.test.js +124 -124
- package/src/utils/__tests__/copy.test.js +117 -117
- package/src/utils/exec-spinner.js +47 -47
- package/src/utils/exec.js +134 -131
- package/src/utils/terminal.js +6 -0
|
@@ -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('./
|
|
21
|
-
vi.mock('./
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
{ value: '
|
|
32
|
-
{ value: '
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
})
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
import {
|
|
41
|
-
import {
|
|
42
|
-
import {
|
|
43
|
-
import {
|
|
44
|
-
import {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
expect(
|
|
71
|
-
expect(
|
|
72
|
-
expect(
|
|
73
|
-
expect(
|
|
74
|
-
expect(
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
expect(
|
|
89
|
-
expect(
|
|
90
|
-
expect(
|
|
91
|
-
expect(
|
|
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
|
+
}
|
|
@@ -1,124 +1,124 @@
|
|
|
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
|
-
{ name: 'Child folders', value: 'children' },
|
|
27
|
-
],
|
|
28
|
-
parentSelectionMessage: 'Select sibling folders',
|
|
29
|
-
childrenSelectionMessage: 'Select child folders',
|
|
30
|
-
}),
|
|
31
|
-
readdir: vi.fn(),
|
|
32
|
-
stat: vi.fn().mockResolvedValue({ isDirectory: () => true }),
|
|
33
|
-
},
|
|
34
|
-
}))
|
|
35
|
-
|
|
36
|
-
import { select, checkbox } from '@inquirer/prompts'
|
|
37
|
-
import fse from 'fs-extra'
|
|
38
|
-
import { chooseSourceScope } from './index.js'
|
|
39
|
-
|
|
40
|
-
describe('chooseSourceScope()', () => {
|
|
41
|
-
let tmpDir, originalCwd
|
|
42
|
-
|
|
43
|
-
beforeEach(() => {
|
|
44
|
-
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'source-test-'))
|
|
45
|
-
originalCwd = process.cwd()
|
|
46
|
-
process.chdir(tmpDir)
|
|
47
|
-
vi.clearAllMocks()
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
afterEach(() => {
|
|
51
|
-
process.chdir(originalCwd)
|
|
52
|
-
fs.rmSync(tmpDir, { recursive: true, force: true })
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
it('returns current folder when user selects current mode', async () => {
|
|
56
|
-
select.mockResolvedValue('current')
|
|
57
|
-
|
|
58
|
-
const result = await chooseSourceScope()
|
|
59
|
-
|
|
60
|
-
expect(result.sourceMode).toBe('current')
|
|
61
|
-
expect(result.sourceRoots).toContain(tmpDir)
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
it('lists parent folders when user selects parent mode', async () => {
|
|
65
|
-
select.mockResolvedValue('parent')
|
|
66
|
-
const parentDir = path.dirname(tmpDir)
|
|
67
|
-
const siblingDir = path.join(parentDir, 'sibling-project')
|
|
68
|
-
fs.mkdirSync(siblingDir, { recursive: true })
|
|
69
|
-
fse.readdir.mockResolvedValue(['sibling-project'])
|
|
70
|
-
|
|
71
|
-
await chooseSourceScope()
|
|
72
|
-
|
|
73
|
-
expect(checkbox).toHaveBeenCalled()
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
it('falls back to current when no siblings found', async () => {
|
|
77
|
-
select.mockResolvedValue('parent')
|
|
78
|
-
fse.readdir.mockResolvedValue([])
|
|
79
|
-
|
|
80
|
-
const result = await chooseSourceScope()
|
|
81
|
-
|
|
82
|
-
expect(result.sourceMode).toBe('current')
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
it('falls back to current when no folders selected', async () => {
|
|
86
|
-
select.mockResolvedValue('parent')
|
|
87
|
-
checkbox.mockResolvedValue([])
|
|
88
|
-
|
|
89
|
-
const result = await chooseSourceScope()
|
|
90
|
-
|
|
91
|
-
expect(result.sourceMode).toBe('current')
|
|
92
|
-
})
|
|
93
|
-
|
|
94
|
-
it('lists child folders when user selects children mode', async () => {
|
|
95
|
-
select.mockResolvedValue('children')
|
|
96
|
-
fse.readdir.mockResolvedValue(['packages', 'apps'])
|
|
97
|
-
checkbox.mockResolvedValue([path.join(tmpDir, 'packages')])
|
|
98
|
-
|
|
99
|
-
const result = await chooseSourceScope()
|
|
100
|
-
|
|
101
|
-
expect(checkbox).toHaveBeenCalled()
|
|
102
|
-
expect(result.sourceMode).toBe('children-selected')
|
|
103
|
-
expect(result.sourceRoots).toContain(path.join(tmpDir, 'packages'))
|
|
104
|
-
})
|
|
105
|
-
|
|
106
|
-
it('falls back to current when no child folders found', async () => {
|
|
107
|
-
select.mockResolvedValue('children')
|
|
108
|
-
fse.readdir.mockResolvedValue([])
|
|
109
|
-
|
|
110
|
-
const result = await chooseSourceScope()
|
|
111
|
-
|
|
112
|
-
expect(result.sourceMode).toBe('current')
|
|
113
|
-
})
|
|
114
|
-
|
|
115
|
-
it('falls back to current when no child folders selected', async () => {
|
|
116
|
-
select.mockResolvedValue('children')
|
|
117
|
-
fse.readdir.mockResolvedValue(['packages'])
|
|
118
|
-
checkbox.mockResolvedValue([])
|
|
119
|
-
|
|
120
|
-
const result = await chooseSourceScope()
|
|
121
|
-
|
|
122
|
-
expect(result.sourceMode).toBe('current')
|
|
123
|
-
})
|
|
124
|
-
})
|
|
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
|
+
{ name: 'Child folders', value: 'children' },
|
|
27
|
+
],
|
|
28
|
+
parentSelectionMessage: 'Select sibling folders',
|
|
29
|
+
childrenSelectionMessage: 'Select child folders',
|
|
30
|
+
}),
|
|
31
|
+
readdir: vi.fn(),
|
|
32
|
+
stat: vi.fn().mockResolvedValue({ isDirectory: () => true }),
|
|
33
|
+
},
|
|
34
|
+
}))
|
|
35
|
+
|
|
36
|
+
import { select, checkbox } from '@inquirer/prompts'
|
|
37
|
+
import fse from 'fs-extra'
|
|
38
|
+
import { chooseSourceScope } from './index.js'
|
|
39
|
+
|
|
40
|
+
describe('chooseSourceScope()', () => {
|
|
41
|
+
let tmpDir, originalCwd
|
|
42
|
+
|
|
43
|
+
beforeEach(() => {
|
|
44
|
+
tmpDir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'source-test-')))
|
|
45
|
+
originalCwd = process.cwd()
|
|
46
|
+
process.chdir(tmpDir)
|
|
47
|
+
vi.clearAllMocks()
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
afterEach(() => {
|
|
51
|
+
process.chdir(originalCwd)
|
|
52
|
+
fs.rmSync(tmpDir, { recursive: true, force: true })
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('returns current folder when user selects current mode', async () => {
|
|
56
|
+
select.mockResolvedValue('current')
|
|
57
|
+
|
|
58
|
+
const result = await chooseSourceScope()
|
|
59
|
+
|
|
60
|
+
expect(result.sourceMode).toBe('current')
|
|
61
|
+
expect(result.sourceRoots).toContain(tmpDir)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('lists parent folders when user selects parent mode', async () => {
|
|
65
|
+
select.mockResolvedValue('parent')
|
|
66
|
+
const parentDir = path.dirname(tmpDir)
|
|
67
|
+
const siblingDir = path.join(parentDir, 'sibling-project')
|
|
68
|
+
fs.mkdirSync(siblingDir, { recursive: true })
|
|
69
|
+
fse.readdir.mockResolvedValue(['sibling-project'])
|
|
70
|
+
|
|
71
|
+
await chooseSourceScope()
|
|
72
|
+
|
|
73
|
+
expect(checkbox).toHaveBeenCalled()
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('falls back to current when no siblings found', async () => {
|
|
77
|
+
select.mockResolvedValue('parent')
|
|
78
|
+
fse.readdir.mockResolvedValue([])
|
|
79
|
+
|
|
80
|
+
const result = await chooseSourceScope()
|
|
81
|
+
|
|
82
|
+
expect(result.sourceMode).toBe('current')
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('falls back to current when no folders selected', async () => {
|
|
86
|
+
select.mockResolvedValue('parent')
|
|
87
|
+
checkbox.mockResolvedValue([])
|
|
88
|
+
|
|
89
|
+
const result = await chooseSourceScope()
|
|
90
|
+
|
|
91
|
+
expect(result.sourceMode).toBe('current')
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('lists child folders when user selects children mode', async () => {
|
|
95
|
+
select.mockResolvedValue('children')
|
|
96
|
+
fse.readdir.mockResolvedValue(['packages', 'apps'])
|
|
97
|
+
checkbox.mockResolvedValue([path.join(tmpDir, 'packages')])
|
|
98
|
+
|
|
99
|
+
const result = await chooseSourceScope()
|
|
100
|
+
|
|
101
|
+
expect(checkbox).toHaveBeenCalled()
|
|
102
|
+
expect(result.sourceMode).toBe('children-selected')
|
|
103
|
+
expect(result.sourceRoots).toContain(path.join(tmpDir, 'packages'))
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('falls back to current when no child folders found', async () => {
|
|
107
|
+
select.mockResolvedValue('children')
|
|
108
|
+
fse.readdir.mockResolvedValue([])
|
|
109
|
+
|
|
110
|
+
const result = await chooseSourceScope()
|
|
111
|
+
|
|
112
|
+
expect(result.sourceMode).toBe('current')
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('falls back to current when no child folders selected', async () => {
|
|
116
|
+
select.mockResolvedValue('children')
|
|
117
|
+
fse.readdir.mockResolvedValue(['packages'])
|
|
118
|
+
checkbox.mockResolvedValue([])
|
|
119
|
+
|
|
120
|
+
const result = await chooseSourceScope()
|
|
121
|
+
|
|
122
|
+
expect(result.sourceMode).toBe('current')
|
|
123
|
+
})
|
|
124
|
+
})
|