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.
- 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 +32 -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 +103 -0
- package/src/steps/optimization/codegraph.test.js +104 -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,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
|
+
})
|
|
@@ -1,117 +1,117 @@
|
|
|
1
|
-
import { describe, it, expect,
|
|
2
|
-
import path from 'path'
|
|
3
|
-
import os from 'os'
|
|
4
|
-
import fse from 'fs-extra'
|
|
5
|
-
|
|
6
|
-
// Use real fs-extra for file system tests (temp dirs)
|
|
7
|
-
import { copyContent, findAiFiles } from '../copy.js'
|
|
8
|
-
|
|
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
|
-
]
|
|
17
|
-
|
|
18
|
-
describe('copy utils', () => {
|
|
19
|
-
describe('findAiFiles()', () => {
|
|
20
|
-
let dir
|
|
21
|
-
|
|
22
|
-
beforeEach(() => {
|
|
23
|
-
dir = tmpDir()
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
afterEach(async () => {
|
|
27
|
-
await fse.remove(dir)
|
|
28
|
-
})
|
|
29
|
-
|
|
30
|
-
it('returns empty array when no AI files exist', async () => {
|
|
31
|
-
const found = await findAiFiles(dir, aiFiles)
|
|
32
|
-
expect(found).toEqual([])
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
it('detects AGENTS.md', async () => {
|
|
36
|
-
await fse.writeFile(path.join(dir, 'AGENTS.md'), '# agents')
|
|
37
|
-
const found = await findAiFiles(dir, aiFiles)
|
|
38
|
-
expect(found).toHaveLength(1)
|
|
39
|
-
expect(found[0]).toContain('AGENTS.md')
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
it('detects CLAUDE.md', async () => {
|
|
43
|
-
await fse.writeFile(path.join(dir, 'CLAUDE.md'), '# claude')
|
|
44
|
-
const found = await findAiFiles(dir, aiFiles)
|
|
45
|
-
expect(found).toHaveLength(1)
|
|
46
|
-
expect(found[0]).toContain('CLAUDE.md')
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
it('detects multiple AI files at once', async () => {
|
|
50
|
-
await fse.writeFile(path.join(dir, 'AGENTS.md'), '')
|
|
51
|
-
await fse.writeFile(path.join(dir, '.cursorrules'), '')
|
|
52
|
-
await fse.writeFile(path.join(dir, '.clinerules'), '')
|
|
53
|
-
const found = await findAiFiles(dir, aiFiles)
|
|
54
|
-
expect(found).toHaveLength(3)
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
it('detects nested copilot-instructions.md', async () => {
|
|
58
|
-
const ghDir = path.join(dir, '.github')
|
|
59
|
-
await fse.ensureDir(ghDir)
|
|
60
|
-
await fse.writeFile(path.join(ghDir, 'copilot-instructions.md'), '')
|
|
61
|
-
const found = await findAiFiles(dir, aiFiles)
|
|
62
|
-
expect(found).toHaveLength(1)
|
|
63
|
-
expect(found[0]).toContain('copilot-instructions.md')
|
|
64
|
-
})
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
describe('copyContent()', () => {
|
|
68
|
-
let src, dest
|
|
69
|
-
|
|
70
|
-
beforeEach(async () => {
|
|
71
|
-
src = tmpDir()
|
|
72
|
-
dest = tmpDir()
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
afterEach(async () => {
|
|
76
|
-
await fse.remove(src)
|
|
77
|
-
await fse.remove(dest)
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
it('copies files that match neither platform exclusion', async () => {
|
|
81
|
-
await fse.writeFile(path.join(src, 'AGENTS.md'), '# agents')
|
|
82
|
-
await copyContent(src, dest, 'github')
|
|
83
|
-
expect(await fse.pathExists(path.join(dest, 'AGENTS.md'))).toBe(true)
|
|
84
|
-
})
|
|
85
|
-
|
|
86
|
-
it('always excludes .bootstrap folder', async () => {
|
|
87
|
-
await fse.ensureDir(path.join(src, '.bootstrap'))
|
|
88
|
-
await fse.writeFile(path.join(src, '.bootstrap', 'secret.md'), 'internal')
|
|
89
|
-
|
|
90
|
-
await copyContent(src, dest, 'github')
|
|
91
|
-
|
|
92
|
-
expect(await fse.pathExists(path.join(dest, '.bootstrap', 'secret.md'))).toBe(false)
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
it('does not overwrite existing files', async () => {
|
|
96
|
-
await fse.writeFile(path.join(src, 'AGENTS.md'), 'new content')
|
|
97
|
-
await fse.writeFile(path.join(dest, 'AGENTS.md'), 'original content')
|
|
98
|
-
|
|
99
|
-
await copyContent(src, dest, 'github')
|
|
100
|
-
|
|
101
|
-
const content = await fse.readFile(path.join(dest, 'AGENTS.md'), 'utf-8')
|
|
102
|
-
expect(content).toBe('original content')
|
|
103
|
-
})
|
|
104
|
-
|
|
105
|
-
it('copies github-specific files when platform is github', async () => {
|
|
106
|
-
await fse.writeFile(path.join(src, 'agent-gh.md'), 'github agent')
|
|
107
|
-
await copyContent(src, dest, 'github')
|
|
108
|
-
expect(await fse.pathExists(path.join(dest, 'agent-gh.md'))).toBe(true)
|
|
109
|
-
})
|
|
110
|
-
|
|
111
|
-
it('copies azure-specific files when platform is azure', async () => {
|
|
112
|
-
await fse.writeFile(path.join(src, 'agent-az.md'), 'azure agent')
|
|
113
|
-
await copyContent(src, dest, 'azure')
|
|
114
|
-
expect(await fse.pathExists(path.join(dest, 'agent-az.md'))).toBe(true)
|
|
115
|
-
})
|
|
116
|
-
})
|
|
117
|
-
})
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import os from 'os'
|
|
4
|
+
import fse from 'fs-extra'
|
|
5
|
+
|
|
6
|
+
// Use real fs-extra for file system tests (temp dirs)
|
|
7
|
+
import { copyContent, findAiFiles } from '../copy.js'
|
|
8
|
+
|
|
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
|
+
]
|
|
17
|
+
|
|
18
|
+
describe('copy utils', () => {
|
|
19
|
+
describe('findAiFiles()', () => {
|
|
20
|
+
let dir
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
dir = tmpDir()
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
afterEach(async () => {
|
|
27
|
+
await fse.remove(dir)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('returns empty array when no AI files exist', async () => {
|
|
31
|
+
const found = await findAiFiles(dir, aiFiles)
|
|
32
|
+
expect(found).toEqual([])
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('detects AGENTS.md', async () => {
|
|
36
|
+
await fse.writeFile(path.join(dir, 'AGENTS.md'), '# agents')
|
|
37
|
+
const found = await findAiFiles(dir, aiFiles)
|
|
38
|
+
expect(found).toHaveLength(1)
|
|
39
|
+
expect(found[0]).toContain('AGENTS.md')
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('detects CLAUDE.md', async () => {
|
|
43
|
+
await fse.writeFile(path.join(dir, 'CLAUDE.md'), '# claude')
|
|
44
|
+
const found = await findAiFiles(dir, aiFiles)
|
|
45
|
+
expect(found).toHaveLength(1)
|
|
46
|
+
expect(found[0]).toContain('CLAUDE.md')
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('detects multiple AI files at once', async () => {
|
|
50
|
+
await fse.writeFile(path.join(dir, 'AGENTS.md'), '')
|
|
51
|
+
await fse.writeFile(path.join(dir, '.cursorrules'), '')
|
|
52
|
+
await fse.writeFile(path.join(dir, '.clinerules'), '')
|
|
53
|
+
const found = await findAiFiles(dir, aiFiles)
|
|
54
|
+
expect(found).toHaveLength(3)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('detects nested copilot-instructions.md', async () => {
|
|
58
|
+
const ghDir = path.join(dir, '.github')
|
|
59
|
+
await fse.ensureDir(ghDir)
|
|
60
|
+
await fse.writeFile(path.join(ghDir, 'copilot-instructions.md'), '')
|
|
61
|
+
const found = await findAiFiles(dir, aiFiles)
|
|
62
|
+
expect(found).toHaveLength(1)
|
|
63
|
+
expect(found[0]).toContain('copilot-instructions.md')
|
|
64
|
+
})
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
describe('copyContent()', () => {
|
|
68
|
+
let src, dest
|
|
69
|
+
|
|
70
|
+
beforeEach(async () => {
|
|
71
|
+
src = tmpDir()
|
|
72
|
+
dest = tmpDir()
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
afterEach(async () => {
|
|
76
|
+
await fse.remove(src)
|
|
77
|
+
await fse.remove(dest)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('copies files that match neither platform exclusion', async () => {
|
|
81
|
+
await fse.writeFile(path.join(src, 'AGENTS.md'), '# agents')
|
|
82
|
+
await copyContent(src, dest, 'github')
|
|
83
|
+
expect(await fse.pathExists(path.join(dest, 'AGENTS.md'))).toBe(true)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('always excludes .bootstrap folder', async () => {
|
|
87
|
+
await fse.ensureDir(path.join(src, '.bootstrap'))
|
|
88
|
+
await fse.writeFile(path.join(src, '.bootstrap', 'secret.md'), 'internal')
|
|
89
|
+
|
|
90
|
+
await copyContent(src, dest, 'github')
|
|
91
|
+
|
|
92
|
+
expect(await fse.pathExists(path.join(dest, '.bootstrap', 'secret.md'))).toBe(false)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('does not overwrite existing files', async () => {
|
|
96
|
+
await fse.writeFile(path.join(src, 'AGENTS.md'), 'new content')
|
|
97
|
+
await fse.writeFile(path.join(dest, 'AGENTS.md'), 'original content')
|
|
98
|
+
|
|
99
|
+
await copyContent(src, dest, 'github')
|
|
100
|
+
|
|
101
|
+
const content = await fse.readFile(path.join(dest, 'AGENTS.md'), 'utf-8')
|
|
102
|
+
expect(content).toBe('original content')
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('copies github-specific files when platform is github', async () => {
|
|
106
|
+
await fse.writeFile(path.join(src, 'agent-gh.md'), 'github agent')
|
|
107
|
+
await copyContent(src, dest, 'github')
|
|
108
|
+
expect(await fse.pathExists(path.join(dest, 'agent-gh.md'))).toBe(true)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('copies azure-specific files when platform is azure', async () => {
|
|
112
|
+
await fse.writeFile(path.join(src, 'agent-az.md'), 'azure agent')
|
|
113
|
+
await copyContent(src, dest, 'azure')
|
|
114
|
+
expect(await fse.pathExists(path.join(dest, 'agent-az.md'))).toBe(true)
|
|
115
|
+
})
|
|
116
|
+
})
|
|
117
|
+
})
|
|
@@ -1,47 +1,47 @@
|
|
|
1
|
-
import chalk from 'chalk'
|
|
2
|
-
import ora from 'ora'
|
|
3
|
-
|
|
4
|
-
// ── Screen / step state ──────────────────────────────────────────────────────
|
|
5
|
-
|
|
6
|
-
const previousSteps = []; // up to 2 completed steps, each is an array of lines
|
|
7
|
-
let currentStepLines = []; // lines accumulated in the current step
|
|
8
|
-
let stepSpinner = null; // ora spinner shown while step is working
|
|
9
|
-
|
|
10
|
-
export function appendLine(line) {
|
|
11
|
-
currentStepLines.push(line);
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function stopSpinner() {
|
|
15
|
-
if (stepSpinner) {
|
|
16
|
-
stepSpinner.stop();
|
|
17
|
-
stepSpinner = null;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function startSpinner(text = 'working...') {
|
|
22
|
-
stopSpinner();
|
|
23
|
-
stepSpinner = ora({ text: chalk.dim(text), color: 'red' }).start();
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function redraw() {
|
|
27
|
-
if (process.stdout.isTTY) console.clear();
|
|
28
|
-
|
|
29
|
-
// Show up to 2 previous steps dimmed
|
|
30
|
-
for (const stepLines of previousSteps) {
|
|
31
|
-
for (const line of stepLines) {
|
|
32
|
-
process.stdout.write(chalk.dim(line)
|
|
33
|
-
}
|
|
34
|
-
process.stdout.write('\n');
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// Current step output
|
|
38
|
-
for (const line of currentStepLines) {
|
|
39
|
-
process.stdout.write(line
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export function rotateStep() {
|
|
44
|
-
previousSteps.push(currentStepLines);
|
|
45
|
-
if (previousSteps.length > 2) previousSteps.shift();
|
|
46
|
-
currentStepLines = [];
|
|
47
|
-
}
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
import ora from 'ora'
|
|
3
|
+
|
|
4
|
+
// ── Screen / step state ──────────────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
const previousSteps = []; // up to 2 completed steps, each is an array of lines
|
|
7
|
+
let currentStepLines = []; // lines accumulated in the current step
|
|
8
|
+
let stepSpinner = null; // ora spinner shown while step is working
|
|
9
|
+
|
|
10
|
+
export function appendLine(line) {
|
|
11
|
+
currentStepLines.push(line);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function stopSpinner() {
|
|
15
|
+
if (stepSpinner) {
|
|
16
|
+
stepSpinner.stop();
|
|
17
|
+
stepSpinner = null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function startSpinner(text = 'working...') {
|
|
22
|
+
stopSpinner();
|
|
23
|
+
stepSpinner = ora({ text: chalk.dim(text), color: 'red' }).start();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function redraw() {
|
|
27
|
+
if (process.stdout.isTTY) console.clear();
|
|
28
|
+
|
|
29
|
+
// Show up to 2 previous steps dimmed
|
|
30
|
+
for (const stepLines of previousSteps) {
|
|
31
|
+
for (const line of stepLines) {
|
|
32
|
+
process.stdout.write(`${chalk.dim(line)}\n`);
|
|
33
|
+
}
|
|
34
|
+
process.stdout.write('\n');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Current step output
|
|
38
|
+
for (const line of currentStepLines) {
|
|
39
|
+
process.stdout.write(`${line}\n`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function rotateStep() {
|
|
44
|
+
previousSteps.push(currentStepLines);
|
|
45
|
+
if (previousSteps.length > 2) previousSteps.shift();
|
|
46
|
+
currentStepLines = [];
|
|
47
|
+
}
|