opencode-onboard 0.4.1 → 0.4.2
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 +12 -0
- package/content/.opencode/commands/init.md +8 -0
- package/content/.opencode/commands/main.md +17 -0
- package/content/.opencode/commands/plan.md +37 -0
- package/package.json +1 -1
- package/src/commands/join.js +43 -0
- package/src/commands/shared.js +12 -0
- package/src/commands/shared.test.js +56 -0
- package/src/commands/single.js +64 -0
- package/src/commands/wizard.js +99 -0
- package/src/index.js +25 -168
- package/src/steps/browser/browser.test.js +1 -1
- package/src/steps/metadata/metadata.test.js +8 -5
- package/src/steps/models/format.test.js +8 -7
- package/src/steps/models/write.test.js +11 -13
- package/src/steps/optimization/optimization.test.js +20 -0
- package/src/steps/platform/platform.test.js +1 -1
- package/src/steps/source/source.test.js +5 -3
- package/src/utils/models-pricing.test.js +0 -1
package/README.md
CHANGED
|
@@ -90,6 +90,18 @@ OpenCode generates `ARCHITECTURE.md` and `DESIGN.md` from your actual codebase,
|
|
|
90
90
|
|
|
91
91
|
---
|
|
92
92
|
|
|
93
|
+
## Commands
|
|
94
|
+
|
|
95
|
+
Custom slash commands are installed into `.opencode/commands/` and are available directly in OpenCode.
|
|
96
|
+
|
|
97
|
+
| Command | Description |
|
|
98
|
+
|---------|-------------|
|
|
99
|
+
| `/init` | Initialize the project: generate `ARCHITECTURE.md`, `DESIGN.md`, archive history, activate agent team |
|
|
100
|
+
| `/plan <url>` | Parse a user story URL and produce a plan — proposal, specs, and tasks. Stops before implementation. |
|
|
101
|
+
| `/main <task>` | Quick direct implementation — no OpenSpec, no ensemble, no PRs. Just do it. |
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
93
105
|
## Agents and Skills
|
|
94
106
|
|
|
95
107
|
opencode-onboard draws a hard line between two concepts:
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Initialize the project — runs the bootstrap sequence defined in AGENTS.md if not yet initialized.
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Check if `AGENTS.md` is in bootstrap mode (contains `<!-- AGENTS-TEMPLATE-START -->`).
|
|
6
|
+
|
|
7
|
+
- If yes: run the full initialization sequence defined in `AGENTS.md` now.
|
|
8
|
+
- If no: tell the user the project is already initialized.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Quick direct implementation — no OpenSpec, no ensemble, no PRs. Just do it.
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Implement the task described after `/main` directly and immediately.
|
|
6
|
+
|
|
7
|
+
**Rules:**
|
|
8
|
+
- No OpenSpec artifacts (no proposal, no specs, no tasks.md)
|
|
9
|
+
- No ensemble team (no team_create, no team_spawn)
|
|
10
|
+
- No branches, no PRs
|
|
11
|
+
- Work directly in the current branch
|
|
12
|
+
- Keep changes minimal and focused on exactly what was asked
|
|
13
|
+
- Use Read/Glob/Grep to locate relevant files before editing
|
|
14
|
+
- After editing, run `pnpm run typecheck` to catch type errors; fix any that are caused by your changes
|
|
15
|
+
- Do NOT run lint or tests unless the user asks
|
|
16
|
+
|
|
17
|
+
**Input**: Everything after `/main` is the task. Execute it now.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Parse a user story URL and produce a plan — proposal, specs, and tasks. Stops before implementation.
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Parse the work item at the URL provided after `/plan` and produce a full implementation plan.
|
|
6
|
+
|
|
7
|
+
**Input**: A GitHub Issue URL or Azure DevOps work item URL. Example: `/plan https://github.com/org/repo/issues/42`
|
|
8
|
+
|
|
9
|
+
**Steps:**
|
|
10
|
+
|
|
11
|
+
1. **Load baseline**
|
|
12
|
+
|
|
13
|
+
Load `@ob-global` first.
|
|
14
|
+
|
|
15
|
+
2. **Detect URL type and load matching skill**
|
|
16
|
+
|
|
17
|
+
- GitHub Issue URL → load `ob-userstory-gh` skill
|
|
18
|
+
- Azure DevOps URL → load `ob-userstory-az` skill
|
|
19
|
+
|
|
20
|
+
Follow the skill steps exactly: fetch the issue/work item via CLI and create an OpenSpec change.
|
|
21
|
+
|
|
22
|
+
3. **Propose**
|
|
23
|
+
|
|
24
|
+
Run `/opsx-propose` to generate `proposal.md`, specs, and `tasks.md`.
|
|
25
|
+
|
|
26
|
+
4. **Show the plan**
|
|
27
|
+
|
|
28
|
+
Display:
|
|
29
|
+
- Change name
|
|
30
|
+
- Total number of tasks
|
|
31
|
+
- Full task list summary
|
|
32
|
+
|
|
33
|
+
5. **Stop**
|
|
34
|
+
|
|
35
|
+
Ask the user: "Ready to implement? Type `/opsx-apply` to start."
|
|
36
|
+
|
|
37
|
+
Do NOT proceed to implementation. Do NOT run `/opsx-apply` automatically.
|
package/package.json
CHANGED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
import { header, info } from '../utils/exec.js'
|
|
3
|
+
import { installBrowser } from '../steps/browser/index.js'
|
|
4
|
+
import { checkRtk } from '../steps/optimization/index.js'
|
|
5
|
+
import { choosePlatform, checkPlatform } from '../steps/platform/index.js'
|
|
6
|
+
import { readOnboardConfig } from './shared.js'
|
|
7
|
+
|
|
8
|
+
export async function runJoin() {
|
|
9
|
+
const logo = chalk.hex('#fe3d57')
|
|
10
|
+
console.log()
|
|
11
|
+
console.log(logo(' 🤝 opencode-onboard join'))
|
|
12
|
+
console.log(chalk.dim(' New team member setup — checks & local installs only'))
|
|
13
|
+
console.log(chalk.dim(' This will NOT modify any project files.'))
|
|
14
|
+
console.log()
|
|
15
|
+
|
|
16
|
+
// Step 1: Platform CLI check
|
|
17
|
+
header('Step 1, Platform CLI check')
|
|
18
|
+
const saved = await readOnboardConfig()
|
|
19
|
+
const savedPlatform = saved?.wizard?.platform
|
|
20
|
+
if (savedPlatform) {
|
|
21
|
+
info(`Detected project platform: ${savedPlatform === 'github' ? 'GitHub' : 'Azure DevOps'}`)
|
|
22
|
+
await checkPlatform(savedPlatform)
|
|
23
|
+
} else {
|
|
24
|
+
const platform = await choosePlatform()
|
|
25
|
+
void platform // result not persisted in join mode
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Step 2: rtk check
|
|
29
|
+
header('Step 2, Checking rtk')
|
|
30
|
+
await checkRtk({ skipHeader: true })
|
|
31
|
+
|
|
32
|
+
// Step 3: Browser extension
|
|
33
|
+
await installBrowser()
|
|
34
|
+
|
|
35
|
+
console.log()
|
|
36
|
+
console.log(chalk.bold.green('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'))
|
|
37
|
+
console.log(chalk.bold.green(' Join setup complete!'))
|
|
38
|
+
console.log(chalk.bold.green('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'))
|
|
39
|
+
console.log()
|
|
40
|
+
console.log(' Your local environment is ready.')
|
|
41
|
+
console.log(' Open the project in OpenCode and start coding!')
|
|
42
|
+
console.log()
|
|
43
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import fse from 'fs-extra'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
export async function readOnboardConfig() {
|
|
5
|
+
const cfgPath = path.join(process.cwd(), '.opencode', 'opencode-onboard.json')
|
|
6
|
+
if (!await fse.pathExists(cfgPath)) return null
|
|
7
|
+
try {
|
|
8
|
+
return await fse.readJson(cfgPath)
|
|
9
|
+
} catch {
|
|
10
|
+
return null
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import os from 'node:os'
|
|
5
|
+
import { readOnboardConfig } from './shared.js'
|
|
6
|
+
|
|
7
|
+
describe('readOnboardConfig()', () => {
|
|
8
|
+
let tmpDir, originalCwd
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'shared-test-'))
|
|
12
|
+
originalCwd = process.cwd()
|
|
13
|
+
process.chdir(tmpDir)
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
process.chdir(originalCwd)
|
|
18
|
+
fs.rmSync(tmpDir, { recursive: true, force: true })
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('returns null when config file does not exist', async () => {
|
|
22
|
+
const result = await readOnboardConfig()
|
|
23
|
+
|
|
24
|
+
expect(result).toBeNull()
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('returns parsed config when file exists', async () => {
|
|
28
|
+
const configDir = path.join(tmpDir, '.opencode')
|
|
29
|
+
fs.mkdirSync(configDir)
|
|
30
|
+
fs.writeFileSync(
|
|
31
|
+
path.join(configDir, 'opencode-onboard.json'),
|
|
32
|
+
JSON.stringify({ schema: 1, wizard: { platform: 'github' } }),
|
|
33
|
+
'utf-8'
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
const result = await readOnboardConfig()
|
|
37
|
+
|
|
38
|
+
expect(result).not.toBeNull()
|
|
39
|
+
expect(result.schema).toBe(1)
|
|
40
|
+
expect(result.wizard.platform).toBe('github')
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('returns null when file contains invalid JSON', async () => {
|
|
44
|
+
const configDir = path.join(tmpDir, '.opencode')
|
|
45
|
+
fs.mkdirSync(configDir)
|
|
46
|
+
fs.writeFileSync(
|
|
47
|
+
path.join(configDir, 'opencode-onboard.json'),
|
|
48
|
+
'not valid json',
|
|
49
|
+
'utf-8'
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
const result = await readOnboardConfig()
|
|
53
|
+
|
|
54
|
+
expect(result).toBeNull()
|
|
55
|
+
})
|
|
56
|
+
})
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { cleanAiFiles } from '../steps/clean/index.js'
|
|
2
|
+
import { copyContentStep } from '../steps/copy/index.js'
|
|
3
|
+
import { chooseModels } from '../steps/models/index.js'
|
|
4
|
+
import { initOpenspec } from '../steps/openspec/index.js'
|
|
5
|
+
import { tokenOptimizationStep } from '../steps/optimization/index.js'
|
|
6
|
+
import { choosePlatform } from '../steps/platform/index.js'
|
|
7
|
+
import { installBrowser } from '../steps/browser/index.js'
|
|
8
|
+
import { writeOnboardConfig } from '../steps/metadata/index.js'
|
|
9
|
+
import { readOnboardConfig } from './shared.js'
|
|
10
|
+
|
|
11
|
+
export async function runSingleCommand(command) {
|
|
12
|
+
const saved = await readOnboardConfig()
|
|
13
|
+
const savedWizard = saved?.wizard ?? {}
|
|
14
|
+
const ctx = {
|
|
15
|
+
hasDesign: !!savedWizard?.preserved?.design,
|
|
16
|
+
hasArchitecture: !!savedWizard?.preserved?.architecture,
|
|
17
|
+
hasOpenspec: !!savedWizard?.preserved?.openspec,
|
|
18
|
+
sourceMode: savedWizard?.sourceMode ?? 'current',
|
|
19
|
+
sourceRoots: Array.isArray(savedWizard?.sourceRoots) ? savedWizard.sourceRoots : [],
|
|
20
|
+
}
|
|
21
|
+
const platform = savedWizard?.platform
|
|
22
|
+
const resolvedPlatform = platform === 'azure' || platform === 'github' ? platform : 'github'
|
|
23
|
+
|
|
24
|
+
const handlers = {
|
|
25
|
+
clean: async () => {
|
|
26
|
+
await cleanAiFiles()
|
|
27
|
+
},
|
|
28
|
+
platform: async () => {
|
|
29
|
+
await choosePlatform()
|
|
30
|
+
},
|
|
31
|
+
copy: async () => {
|
|
32
|
+
await copyContentStep(resolvedPlatform, ctx)
|
|
33
|
+
},
|
|
34
|
+
openspec: async () => {
|
|
35
|
+
await initOpenspec()
|
|
36
|
+
},
|
|
37
|
+
models: async () => {
|
|
38
|
+
await chooseModels()
|
|
39
|
+
},
|
|
40
|
+
optimization: async () => {
|
|
41
|
+
await tokenOptimizationStep({ ctx })
|
|
42
|
+
},
|
|
43
|
+
browser: async () => {
|
|
44
|
+
await installBrowser()
|
|
45
|
+
},
|
|
46
|
+
metadata: async () => {
|
|
47
|
+
await writeOnboardConfig({
|
|
48
|
+
...ctx,
|
|
49
|
+
platform: resolvedPlatform,
|
|
50
|
+
additionalSkillsProvider: 'npx-skills',
|
|
51
|
+
planModel: savedWizard?.models?.plan ?? null,
|
|
52
|
+
buildModel: savedWizard?.models?.build ?? null,
|
|
53
|
+
fastModel: savedWizard?.models?.fast ?? null,
|
|
54
|
+
optionalTools: savedWizard?.optionalTools ?? null,
|
|
55
|
+
cavemanGuidance: savedWizard?.cavemanGuidance ?? null,
|
|
56
|
+
})
|
|
57
|
+
},
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const handler = handlers[command]
|
|
61
|
+
if (!handler) return false
|
|
62
|
+
await handler()
|
|
63
|
+
return true
|
|
64
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
import { chooseSourceScope } from '../steps/source/index.js'
|
|
3
|
+
import { cleanAiFiles } from '../steps/clean/index.js'
|
|
4
|
+
import { choosePlatform } from '../steps/platform/index.js'
|
|
5
|
+
import { copyContentStep } from '../steps/copy/index.js'
|
|
6
|
+
import { initOpenspec } from '../steps/openspec/index.js'
|
|
7
|
+
import { chooseModels } from '../steps/models/index.js'
|
|
8
|
+
import { tokenOptimizationStep } from '../steps/optimization/index.js'
|
|
9
|
+
import { installBrowser } from '../steps/browser/index.js'
|
|
10
|
+
import { writeOnboardConfig } from '../steps/metadata/index.js'
|
|
11
|
+
|
|
12
|
+
export async function runWizard(version) {
|
|
13
|
+
const logo = chalk.hex('#fe3d57')
|
|
14
|
+
const bannerLines = [
|
|
15
|
+
logo(' '),
|
|
16
|
+
logo(' ▒▒▒▒▒▒▒▒▒▒▒▒▒ '),
|
|
17
|
+
logo(' ▒▒▓ ▓▒▓ '),
|
|
18
|
+
logo(' ▒▒▒▒▒▒▓▒▒▒▒▒▒▒▒▒▓▓▒▒▒▒▒ '),
|
|
19
|
+
logo(' ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓ '),
|
|
20
|
+
logo(' ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓ '),
|
|
21
|
+
logo(' ▓▒▒▒▒░░░▒▒▒▒▒▒▒▒▒▒▒░░░▒▒▒▓▓ '),
|
|
22
|
+
logo(' ▓▓▓▓▒▒▒▓▓▓▓▓▓▓▓▓▓▓▒▒▒▓▓▓▓ '),
|
|
23
|
+
logo(' ▓▓▒▒▒▒▒▒░▒▒▒▒▒▒▒░▒▒▒▒▒▒▓▓ '),
|
|
24
|
+
logo(' ▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓ '),
|
|
25
|
+
logo(' ▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓ '),
|
|
26
|
+
logo(' ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ '),
|
|
27
|
+
'',
|
|
28
|
+
chalk.bold(' 🧰 opencode-onboard') + chalk.dim(` v${version}`),
|
|
29
|
+
chalk.dim(' Prepare your codebase for AI agents'),
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
for (const line of bannerLines) console.log(line)
|
|
33
|
+
console.log()
|
|
34
|
+
console.log(' This tool will set up your project with a team of AI agents,')
|
|
35
|
+
console.log(' install skills, select models, and configure OpenCode.')
|
|
36
|
+
console.log()
|
|
37
|
+
|
|
38
|
+
// Only wait for Enter in a real interactive TTY
|
|
39
|
+
if (process.stdin.isTTY) {
|
|
40
|
+
console.log(chalk.bold(' Press Enter to begin...'))
|
|
41
|
+
console.log()
|
|
42
|
+
await new Promise(resolve => {
|
|
43
|
+
process.stdin.resume()
|
|
44
|
+
process.stdin.once('data', () => {
|
|
45
|
+
process.stdin.pause()
|
|
46
|
+
resolve()
|
|
47
|
+
})
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const scope = await chooseSourceScope()
|
|
52
|
+
|
|
53
|
+
const preserve = await cleanAiFiles()
|
|
54
|
+
const ctx = { ...preserve, ...scope }
|
|
55
|
+
|
|
56
|
+
const platform = await choosePlatform()
|
|
57
|
+
|
|
58
|
+
await copyContentStep(platform, ctx)
|
|
59
|
+
|
|
60
|
+
await initOpenspec()
|
|
61
|
+
|
|
62
|
+
const selectedModels = await chooseModels()
|
|
63
|
+
|
|
64
|
+
const tokenOpt = await tokenOptimizationStep({ ctx })
|
|
65
|
+
const { rtk, quota, caveman, cavemanGuidance } = tokenOpt
|
|
66
|
+
|
|
67
|
+
await installBrowser()
|
|
68
|
+
|
|
69
|
+
await writeOnboardConfig({
|
|
70
|
+
...ctx,
|
|
71
|
+
platform,
|
|
72
|
+
additionalSkillsProvider: 'npx-skills',
|
|
73
|
+
...selectedModels,
|
|
74
|
+
optionalTools: { rtk, quota, caveman },
|
|
75
|
+
cavemanGuidance,
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
const toGenerate = [
|
|
79
|
+
!ctx.hasDesign && 'DESIGN.md',
|
|
80
|
+
!ctx.hasArchitecture && 'ARCHITECTURE.md',
|
|
81
|
+
].filter(Boolean)
|
|
82
|
+
|
|
83
|
+
console.log()
|
|
84
|
+
console.log(chalk.bold.green('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'))
|
|
85
|
+
console.log(chalk.bold.green(' Onboarding complete!'))
|
|
86
|
+
console.log(chalk.bold.green('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'))
|
|
87
|
+
console.log()
|
|
88
|
+
console.log(' Open this project in OpenCode and type:')
|
|
89
|
+
console.log(chalk.bold(' "init"'))
|
|
90
|
+
console.log()
|
|
91
|
+
if (toGenerate.length > 0) {
|
|
92
|
+
console.log(` OpenCode will generate ${toGenerate.join(' and ')}`)
|
|
93
|
+
console.log(' from your actual codebase, then activate the agent team.')
|
|
94
|
+
} else {
|
|
95
|
+
console.log(' OpenCode will activate the agent team.')
|
|
96
|
+
}
|
|
97
|
+
console.log(` Source scope: ${ctx.sourceMode === 'parent-selected' ? ctx.sourceRoots.map(p => `../${p.split(/[/\\]/).pop()}`).join(', ') : 'current folder'}`)
|
|
98
|
+
console.log()
|
|
99
|
+
}
|
package/src/index.js
CHANGED
|
@@ -1,17 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import chalk from 'chalk'
|
|
3
|
-
import fse from 'fs-extra'
|
|
4
3
|
import { createRequire } from 'node:module'
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import { copyContentStep } from './steps/copy/index.js'
|
|
9
|
-
import { chooseModels } from './steps/models/index.js'
|
|
10
|
-
import { initOpenspec } from './steps/openspec/index.js'
|
|
11
|
-
import { tokenOptimizationStep } from './steps/optimization/index.js'
|
|
12
|
-
import { choosePlatform } from './steps/platform/index.js'
|
|
13
|
-
import { chooseSourceScope } from './steps/source/index.js'
|
|
14
|
-
import { writeOnboardConfig } from './steps/metadata/index.js'
|
|
4
|
+
import { runJoin } from './commands/join.js'
|
|
5
|
+
import { runSingleCommand } from './commands/single.js'
|
|
6
|
+
import { runWizard } from './commands/wizard.js'
|
|
15
7
|
|
|
16
8
|
function printHelp(version) {
|
|
17
9
|
console.log(`opencode-onboard v${version}`)
|
|
@@ -21,6 +13,7 @@ function printHelp(version) {
|
|
|
21
13
|
console.log(' npx opencode-onboard <command> Run a single step command')
|
|
22
14
|
console.log()
|
|
23
15
|
console.log('Commands:')
|
|
16
|
+
console.log(' join New team member setup (checks & local installs only)')
|
|
24
17
|
console.log(' clean Run AI files cleanup step')
|
|
25
18
|
console.log(' platform Run platform selection step')
|
|
26
19
|
console.log(' copy Run content copy step')
|
|
@@ -34,71 +27,6 @@ function printHelp(version) {
|
|
|
34
27
|
console.log(' -h, --help Show this help message')
|
|
35
28
|
}
|
|
36
29
|
|
|
37
|
-
async function readOnboardConfig() {
|
|
38
|
-
const cfgPath = path.join(process.cwd(), '.opencode', 'opencode-onboard.json')
|
|
39
|
-
if (!await fse.pathExists(cfgPath)) return null
|
|
40
|
-
try {
|
|
41
|
-
return await fse.readJson(cfgPath)
|
|
42
|
-
} catch {
|
|
43
|
-
return null
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
async function runSingleCommand(command) {
|
|
48
|
-
const saved = await readOnboardConfig()
|
|
49
|
-
const savedWizard = saved?.wizard ?? {}
|
|
50
|
-
const ctx = {
|
|
51
|
-
hasDesign: !!savedWizard?.preserved?.design,
|
|
52
|
-
hasArchitecture: !!savedWizard?.preserved?.architecture,
|
|
53
|
-
hasOpenspec: !!savedWizard?.preserved?.openspec,
|
|
54
|
-
sourceMode: savedWizard?.sourceMode ?? 'current',
|
|
55
|
-
sourceRoots: Array.isArray(savedWizard?.sourceRoots) ? savedWizard.sourceRoots : [],
|
|
56
|
-
}
|
|
57
|
-
const platform = savedWizard?.platform
|
|
58
|
-
const resolvedPlatform = platform === 'azure' || platform === 'github' ? platform : 'github'
|
|
59
|
-
|
|
60
|
-
const handlers = {
|
|
61
|
-
clean: async () => {
|
|
62
|
-
await cleanAiFiles()
|
|
63
|
-
},
|
|
64
|
-
platform: async () => {
|
|
65
|
-
await choosePlatform()
|
|
66
|
-
},
|
|
67
|
-
copy: async () => {
|
|
68
|
-
await copyContentStep(resolvedPlatform, ctx)
|
|
69
|
-
},
|
|
70
|
-
openspec: async () => {
|
|
71
|
-
await initOpenspec()
|
|
72
|
-
},
|
|
73
|
-
models: async () => {
|
|
74
|
-
await chooseModels()
|
|
75
|
-
},
|
|
76
|
-
optimization: async () => {
|
|
77
|
-
await tokenOptimizationStep({ ctx })
|
|
78
|
-
},
|
|
79
|
-
browser: async () => {
|
|
80
|
-
await installBrowser()
|
|
81
|
-
},
|
|
82
|
-
metadata: async () => {
|
|
83
|
-
await writeOnboardConfig({
|
|
84
|
-
...ctx,
|
|
85
|
-
platform: resolvedPlatform,
|
|
86
|
-
additionalSkillsProvider: 'npx-skills',
|
|
87
|
-
planModel: savedWizard?.models?.plan ?? null,
|
|
88
|
-
buildModel: savedWizard?.models?.build ?? null,
|
|
89
|
-
fastModel: savedWizard?.models?.fast ?? null,
|
|
90
|
-
optionalTools: savedWizard?.optionalTools ?? null,
|
|
91
|
-
cavemanGuidance: savedWizard?.cavemanGuidance ?? null,
|
|
92
|
-
})
|
|
93
|
-
},
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const handler = handlers[command]
|
|
97
|
-
if (!handler) return false
|
|
98
|
-
await handler()
|
|
99
|
-
return true
|
|
100
|
-
}
|
|
101
|
-
|
|
102
30
|
if (process.stdout.isTTY) console.clear()
|
|
103
31
|
console.log()
|
|
104
32
|
const require = createRequire(import.meta.url)
|
|
@@ -111,103 +39,32 @@ if (args.includes('-h') || args.includes('--help')) {
|
|
|
111
39
|
}
|
|
112
40
|
|
|
113
41
|
if (args.length > 0) {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
42
|
+
try {
|
|
43
|
+
if (args[0] === 'join') {
|
|
44
|
+
await runJoin()
|
|
45
|
+
} else {
|
|
46
|
+
const ok = await runSingleCommand(args[0])
|
|
47
|
+
if (!ok) {
|
|
48
|
+
console.log(chalk.red(`Unknown command: ${args[0]}`))
|
|
49
|
+
console.log()
|
|
50
|
+
printHelp(version)
|
|
51
|
+
process.exit(1)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
} catch (err) {
|
|
55
|
+
if (err.name === 'ExitPromptError') {
|
|
56
|
+
console.log()
|
|
57
|
+
console.log(chalk.yellow('Cancelled.'))
|
|
58
|
+
} else {
|
|
59
|
+
console.error(chalk.red('\nUnexpected error:'), err.message)
|
|
60
|
+
process.exit(1)
|
|
61
|
+
}
|
|
120
62
|
}
|
|
121
63
|
process.exit(0)
|
|
122
64
|
}
|
|
123
65
|
|
|
124
|
-
const logo = chalk.hex('#fe3d57')
|
|
125
|
-
const bannerLines = [
|
|
126
|
-
logo(' '),
|
|
127
|
-
logo(' ▒▒▒▒▒▒▒▒▒▒▒▒▒ '),
|
|
128
|
-
logo(' ▒▒▓ ▓▒▓ '),
|
|
129
|
-
logo(' ▒▒▒▒▒▒▓▒▒▒▒▒▒▒▒▒▓▓▒▒▒▒▒ '),
|
|
130
|
-
logo(' ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓ '),
|
|
131
|
-
logo(' ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓ '),
|
|
132
|
-
logo(' ▓▒▒▒▒░░░▒▒▒▒▒▒▒▒▒▒▒░░░▒▒▒▓▓ '),
|
|
133
|
-
logo(' ▓▓▓▓▒▒▒▓▓▓▓▓▓▓▓▓▓▓▒▒▒▓▓▓▓ '),
|
|
134
|
-
logo(' ▓▓▒▒▒▒▒▒░▒▒▒▒▒▒▒░▒▒▒▒▒▒▓▓ '),
|
|
135
|
-
logo(' ▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓ '),
|
|
136
|
-
logo(' ▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓ '),
|
|
137
|
-
logo(' ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ '),
|
|
138
|
-
'',
|
|
139
|
-
chalk.bold(' 🧰 opencode-onboard') + chalk.dim(` v${version}`),
|
|
140
|
-
chalk.dim(' Prepare your codebase for AI agents'),
|
|
141
|
-
]
|
|
142
|
-
|
|
143
|
-
for (const line of bannerLines) console.log(line)
|
|
144
|
-
console.log()
|
|
145
|
-
console.log(' This tool will set up your project with a team of AI agents,')
|
|
146
|
-
console.log(' install skills, select models, and configure OpenCode.')
|
|
147
|
-
console.log()
|
|
148
|
-
|
|
149
|
-
// Only wait for Enter in a real interactive TTY
|
|
150
|
-
if (process.stdin.isTTY) {
|
|
151
|
-
console.log(chalk.bold(' Press Enter to begin...'))
|
|
152
|
-
console.log()
|
|
153
|
-
await new Promise(resolve => {
|
|
154
|
-
process.stdin.resume()
|
|
155
|
-
process.stdin.once('data', () => {
|
|
156
|
-
process.stdin.pause()
|
|
157
|
-
resolve()
|
|
158
|
-
})
|
|
159
|
-
})
|
|
160
|
-
}
|
|
161
|
-
|
|
162
66
|
try {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
const preserve = await cleanAiFiles()
|
|
166
|
-
const ctx = { ...preserve, ...scope }
|
|
167
|
-
|
|
168
|
-
const platform = await choosePlatform()
|
|
169
|
-
|
|
170
|
-
await copyContentStep(platform, ctx)
|
|
171
|
-
|
|
172
|
-
await initOpenspec()
|
|
173
|
-
|
|
174
|
-
const selectedModels = await chooseModels()
|
|
175
|
-
|
|
176
|
-
const tokenOpt = await tokenOptimizationStep({ ctx })
|
|
177
|
-
const { rtk, quota, caveman, cavemanGuidance } = tokenOpt
|
|
178
|
-
|
|
179
|
-
await installBrowser()
|
|
180
|
-
|
|
181
|
-
await writeOnboardConfig({
|
|
182
|
-
...ctx,
|
|
183
|
-
platform,
|
|
184
|
-
additionalSkillsProvider: 'npx-skills',
|
|
185
|
-
...selectedModels,
|
|
186
|
-
optionalTools: { rtk, quota, caveman },
|
|
187
|
-
cavemanGuidance,
|
|
188
|
-
})
|
|
189
|
-
|
|
190
|
-
const toGenerate = [
|
|
191
|
-
!ctx.hasDesign && 'DESIGN.md',
|
|
192
|
-
!ctx.hasArchitecture && 'ARCHITECTURE.md',
|
|
193
|
-
].filter(Boolean)
|
|
194
|
-
|
|
195
|
-
console.log()
|
|
196
|
-
console.log(chalk.bold.green('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'))
|
|
197
|
-
console.log(chalk.bold.green(' Onboarding complete!'))
|
|
198
|
-
console.log(chalk.bold.green('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'))
|
|
199
|
-
console.log()
|
|
200
|
-
console.log(' Open this project in OpenCode and type:')
|
|
201
|
-
console.log(chalk.bold(' "init"'))
|
|
202
|
-
console.log()
|
|
203
|
-
if (toGenerate.length > 0) {
|
|
204
|
-
console.log(` OpenCode will generate ${toGenerate.join(' and ')}`)
|
|
205
|
-
console.log(' from your actual codebase, then activate the agent team.')
|
|
206
|
-
} else {
|
|
207
|
-
console.log(' OpenCode will activate the agent team.')
|
|
208
|
-
}
|
|
209
|
-
console.log(` Source scope: ${ctx.sourceMode === 'parent-selected' ? ctx.sourceRoots.map(p => `../${p.split(/[/\\]/).pop()}`).join(', ') : 'current folder'}`)
|
|
210
|
-
console.log()
|
|
67
|
+
await runWizard(version)
|
|
211
68
|
} catch (err) {
|
|
212
69
|
if (err.name === 'ExitPromptError') {
|
|
213
70
|
console.log()
|
|
@@ -44,7 +44,7 @@ describe('installBrowser()', () => {
|
|
|
44
44
|
|
|
45
45
|
await installBrowser()
|
|
46
46
|
|
|
47
|
-
expect(execa).toHaveBeenCalledWith('npx', expect.arrayContaining('@different-ai/opencode-browser'), expect.any(Object))
|
|
47
|
+
expect(execa).toHaveBeenCalledWith('npx', expect.arrayContaining(['@different-ai/opencode-browser']), expect.any(Object))
|
|
48
48
|
})
|
|
49
49
|
|
|
50
50
|
it('logs success when exit code is 0', async () => {
|
|
@@ -25,14 +25,17 @@ import fse from 'fs-extra'
|
|
|
25
25
|
import { writeOnboardConfig } from './index.js'
|
|
26
26
|
|
|
27
27
|
describe('writeOnboardConfig()', () => {
|
|
28
|
-
let tmpDir
|
|
28
|
+
let tmpDir, originalCwd
|
|
29
29
|
|
|
30
30
|
beforeEach(() => {
|
|
31
|
+
vi.clearAllMocks()
|
|
31
32
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'metadata-test-'))
|
|
33
|
+
originalCwd = process.cwd()
|
|
32
34
|
process.chdir(tmpDir)
|
|
33
35
|
})
|
|
34
36
|
|
|
35
37
|
afterEach(() => {
|
|
38
|
+
process.chdir(originalCwd)
|
|
36
39
|
fs.rmSync(tmpDir, { recursive: true, force: true })
|
|
37
40
|
})
|
|
38
41
|
|
|
@@ -57,7 +60,7 @@ describe('writeOnboardConfig()', () => {
|
|
|
57
60
|
expect(fse.ensureDir).toHaveBeenCalled()
|
|
58
61
|
expect(fse.writeJson).toHaveBeenCalled()
|
|
59
62
|
const call = fse.writeJson.mock.calls[0]
|
|
60
|
-
const payload = call[
|
|
63
|
+
const payload = call[1]
|
|
61
64
|
expect(payload.schema).toBe(1)
|
|
62
65
|
expect(payload.wizard.platform).toBe('github')
|
|
63
66
|
expect(payload.wizard.models.build).toBe('build-model')
|
|
@@ -70,7 +73,7 @@ describe('writeOnboardConfig()', () => {
|
|
|
70
73
|
await writeOnboardConfig({ platform: 'github', sourceMode: 'current', sourceRoots: [] })
|
|
71
74
|
|
|
72
75
|
const call = fse.writeJson.mock.calls[0]
|
|
73
|
-
const payload = call[
|
|
76
|
+
const payload = call[1]
|
|
74
77
|
expect(payload.opencodeVersion).toBe('2.0.0')
|
|
75
78
|
})
|
|
76
79
|
|
|
@@ -80,7 +83,7 @@ describe('writeOnboardConfig()', () => {
|
|
|
80
83
|
await writeOnboardConfig({ platform: 'github', sourceMode: 'current', sourceRoots: [] })
|
|
81
84
|
|
|
82
85
|
const call = fse.writeJson.mock.calls[0]
|
|
83
|
-
const payload = call[
|
|
86
|
+
const payload = call[1]
|
|
84
87
|
expect(payload.opencodeVersion).toBe(null)
|
|
85
88
|
})
|
|
86
89
|
|
|
@@ -90,7 +93,7 @@ describe('writeOnboardConfig()', () => {
|
|
|
90
93
|
await writeOnboardConfig({ platform: 'github', sourceMode: 'current', sourceRoots: [] })
|
|
91
94
|
|
|
92
95
|
const call = fse.writeJson.mock.calls[0]
|
|
93
|
-
const payload = call[
|
|
96
|
+
const payload = call[1]
|
|
94
97
|
expect(payload.note).toContain('Informational file only')
|
|
95
98
|
})
|
|
96
99
|
})
|
|
@@ -7,7 +7,7 @@ describe('buildDisplayModels()', () => {
|
|
|
7
7
|
|
|
8
8
|
const result = buildDisplayModels(raw)
|
|
9
9
|
|
|
10
|
-
expect(result[0].label).toContain('[
|
|
10
|
+
expect(result[0].label).toContain('[$$]')
|
|
11
11
|
})
|
|
12
12
|
|
|
13
13
|
it('adds cost tier label for mid-range models', () => {
|
|
@@ -15,7 +15,7 @@ describe('buildDisplayModels()', () => {
|
|
|
15
15
|
|
|
16
16
|
const result = buildDisplayModels(raw)
|
|
17
17
|
|
|
18
|
-
expect(result[0].label).toContain('[
|
|
18
|
+
expect(result[0].label).toContain('[$$$]')
|
|
19
19
|
})
|
|
20
20
|
|
|
21
21
|
it('adds cost tier label for expensive models', () => {
|
|
@@ -47,7 +47,8 @@ describe('buildDisplayModels()', () => {
|
|
|
47
47
|
|
|
48
48
|
const result = buildDisplayModels(raw)
|
|
49
49
|
|
|
50
|
-
expect(result[0].description).toContain('
|
|
50
|
+
expect(result[0].description).toContain('?')
|
|
51
|
+
expect(result[0].label).not.toContain('[')
|
|
51
52
|
})
|
|
52
53
|
|
|
53
54
|
it('handles $0 subscription pricing', () => {
|
|
@@ -58,7 +59,7 @@ describe('buildDisplayModels()', () => {
|
|
|
58
59
|
expect(result[0].description).toContain('$0 (subscription)')
|
|
59
60
|
})
|
|
60
61
|
|
|
61
|
-
it('
|
|
62
|
+
it('preserves input order (sorting is done upstream by parseModels)', () => {
|
|
62
63
|
const raw = [
|
|
63
64
|
{ id: 'expensive/model', name: 'Expensive', cost: 100, context: 1000 },
|
|
64
65
|
{ id: 'cheap/model', name: 'Cheap', cost: 1, context: 1000 },
|
|
@@ -67,8 +68,8 @@ describe('buildDisplayModels()', () => {
|
|
|
67
68
|
|
|
68
69
|
const result = buildDisplayModels(raw)
|
|
69
70
|
|
|
70
|
-
expect(result[0].id).toBe('
|
|
71
|
-
expect(result[1].id).toBe('
|
|
72
|
-
expect(result[2].id).toBe('
|
|
71
|
+
expect(result[0].id).toBe('expensive/model')
|
|
72
|
+
expect(result[1].id).toBe('cheap/model')
|
|
73
|
+
expect(result[2].id).toBe('mid/model')
|
|
73
74
|
})
|
|
74
75
|
})
|
|
@@ -7,16 +7,6 @@ vi.mock('../../utils/exec.js', () => ({
|
|
|
7
7
|
success: vi.fn(),
|
|
8
8
|
}))
|
|
9
9
|
|
|
10
|
-
vi.mock('fs-extra', () => ({
|
|
11
|
-
default: {
|
|
12
|
-
readFile: vi.fn(),
|
|
13
|
-
writeFile: vi.fn(),
|
|
14
|
-
writeJson: vi.fn(),
|
|
15
|
-
pathExists: vi.fn(),
|
|
16
|
-
},
|
|
17
|
-
}))
|
|
18
|
-
|
|
19
|
-
import fse from 'fs-extra'
|
|
20
10
|
import { success } from '../../utils/exec.js'
|
|
21
11
|
import { writeModelToAgent, writeModelsToConfigs } from './write.js'
|
|
22
12
|
|
|
@@ -24,6 +14,7 @@ describe('writeModelToAgent()', () => {
|
|
|
24
14
|
let tmpDir
|
|
25
15
|
|
|
26
16
|
beforeEach(() => {
|
|
17
|
+
vi.clearAllMocks()
|
|
27
18
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'models-write-test-'))
|
|
28
19
|
})
|
|
29
20
|
|
|
@@ -69,18 +60,22 @@ custom_field: custom_value
|
|
|
69
60
|
})
|
|
70
61
|
|
|
71
62
|
describe('writeModelsToConfigs()', () => {
|
|
72
|
-
let tmpDir, agentsDir, opencodeJsonPath, ensembleJsonPath
|
|
63
|
+
let tmpDir, agentsDir, opencodeJsonPath, ensembleJsonPath, originalCwd
|
|
73
64
|
|
|
74
65
|
beforeEach(() => {
|
|
66
|
+
vi.clearAllMocks()
|
|
67
|
+
originalCwd = process.cwd()
|
|
75
68
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'models-config-test-'))
|
|
76
69
|
agentsDir = path.join(tmpDir, '.agents', 'agents')
|
|
77
70
|
fs.mkdirSync(agentsDir, { recursive: true })
|
|
78
71
|
opencodeJsonPath = path.join(tmpDir, '.opencode', 'opencode.json')
|
|
79
72
|
ensembleJsonPath = path.join(tmpDir, '.opencode', 'ensemble.json')
|
|
80
73
|
fs.mkdirSync(path.dirname(opencodeJsonPath), { recursive: true })
|
|
74
|
+
process.chdir(tmpDir)
|
|
81
75
|
})
|
|
82
76
|
|
|
83
77
|
afterEach(() => {
|
|
78
|
+
process.chdir(originalCwd)
|
|
84
79
|
fs.rmSync(tmpDir, { recursive: true, force: true })
|
|
85
80
|
})
|
|
86
81
|
|
|
@@ -106,12 +101,15 @@ describe('writeModelsToConfigs()', () => {
|
|
|
106
101
|
})
|
|
107
102
|
|
|
108
103
|
it('reports success when writing configs', async () => {
|
|
104
|
+
const agentFile = path.join(agentsDir, 'back-engineer.md')
|
|
105
|
+
fs.writeFileSync(agentFile, '---\nname: Back\n---', 'utf-8')
|
|
106
|
+
|
|
109
107
|
await writeModelsToConfigs({
|
|
110
108
|
planModel: 'plan-model',
|
|
111
109
|
buildModel: 'build-model',
|
|
112
110
|
fastModel: 'fast-model',
|
|
113
|
-
agentsDir
|
|
114
|
-
preset: { roles: { build: { agents: [] }, fast: { agents: [] } } },
|
|
111
|
+
agentsDir,
|
|
112
|
+
preset: { roles: { build: { agents: ['back-engineer'] }, fast: { agents: [] } } },
|
|
115
113
|
})
|
|
116
114
|
|
|
117
115
|
expect(success).toHaveBeenCalled()
|
|
@@ -20,6 +20,21 @@ vi.mock('./caveman.js', () => ({ installCaveman: vi.fn() }))
|
|
|
20
20
|
vi.mock('./caveman-guidance.js', () => ({ enableCavemanGuidance: vi.fn() }))
|
|
21
21
|
vi.mock('./global.js', () => ({ configureObGlobal: vi.fn() }))
|
|
22
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
|
+
|
|
23
38
|
import { checkbox } from '@inquirer/prompts'
|
|
24
39
|
import { commandExists, warn } from '../../utils/exec.js'
|
|
25
40
|
import { installQuota } from './quota.js'
|
|
@@ -34,6 +49,9 @@ describe('tokenOptimizationStep()', () => {
|
|
|
34
49
|
})
|
|
35
50
|
|
|
36
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
|
+
|
|
37
55
|
checkbox.mockResolvedValue(['rtk', 'quota', 'caveman'])
|
|
38
56
|
commandExists.mockResolvedValue(true)
|
|
39
57
|
installQuota.mockResolvedValue({ optedIn: true, installed: true })
|
|
@@ -43,6 +61,8 @@ describe('tokenOptimizationStep()', () => {
|
|
|
43
61
|
|
|
44
62
|
const result = await tokenOptimizationStep()
|
|
45
63
|
|
|
64
|
+
Object.defineProperty(process.stdin, 'isTTY', { value: originalIsTTY, configurable: true })
|
|
65
|
+
|
|
46
66
|
expect(commandExists).toHaveBeenCalledWith('rtk')
|
|
47
67
|
expect(installQuota).toHaveBeenCalledWith({ skipHeader: true, skipPrompt: true })
|
|
48
68
|
expect(installCaveman).toHaveBeenCalledWith({ skipHeader: true, skipPrompt: true })
|
|
@@ -54,7 +54,7 @@ describe('choosePlatform()', () => {
|
|
|
54
54
|
await checkPlatform('github')
|
|
55
55
|
|
|
56
56
|
expect(success).toHaveBeenCalledWith('GitHub CLI (gh) available')
|
|
57
|
-
expect(success).toHaveBeenCalledWith('GitHub CLI authenticated')
|
|
57
|
+
expect(success).toHaveBeenCalledWith('GitHub CLI (gh) authenticated')
|
|
58
58
|
})
|
|
59
59
|
|
|
60
60
|
it('warns when gh is installed but not authenticated', async () => {
|
|
@@ -27,7 +27,7 @@ vi.mock('fs-extra', () => ({
|
|
|
27
27
|
parentSelectionMessage: 'Select sibling folders',
|
|
28
28
|
}),
|
|
29
29
|
readdir: vi.fn(),
|
|
30
|
-
stat: vi.fn(),
|
|
30
|
+
stat: vi.fn().mockResolvedValue({ isDirectory: () => true }),
|
|
31
31
|
},
|
|
32
32
|
}))
|
|
33
33
|
|
|
@@ -36,15 +36,17 @@ import fse from 'fs-extra'
|
|
|
36
36
|
import { chooseSourceScope } from './index.js'
|
|
37
37
|
|
|
38
38
|
describe('chooseSourceScope()', () => {
|
|
39
|
-
let tmpDir
|
|
39
|
+
let tmpDir, originalCwd
|
|
40
40
|
|
|
41
41
|
beforeEach(() => {
|
|
42
42
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'source-test-'))
|
|
43
|
+
originalCwd = process.cwd()
|
|
43
44
|
process.chdir(tmpDir)
|
|
44
45
|
vi.clearAllMocks()
|
|
45
46
|
})
|
|
46
47
|
|
|
47
48
|
afterEach(() => {
|
|
49
|
+
process.chdir(originalCwd)
|
|
48
50
|
fs.rmSync(tmpDir, { recursive: true, force: true })
|
|
49
51
|
})
|
|
50
52
|
|
|
@@ -61,7 +63,7 @@ describe('chooseSourceScope()', () => {
|
|
|
61
63
|
select.mockResolvedValue('parent')
|
|
62
64
|
const parentDir = path.dirname(tmpDir)
|
|
63
65
|
const siblingDir = path.join(parentDir, 'sibling-project')
|
|
64
|
-
fs.mkdirSync(siblingDir)
|
|
66
|
+
fs.mkdirSync(siblingDir, { recursive: true })
|
|
65
67
|
fse.readdir.mockResolvedValue(['sibling-project'])
|
|
66
68
|
|
|
67
69
|
await chooseSourceScope()
|