opencastle 0.24.1 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/dist/cli/bootstrap.d.ts +8 -0
  2. package/dist/cli/bootstrap.d.ts.map +1 -0
  3. package/dist/cli/bootstrap.js +358 -0
  4. package/dist/cli/bootstrap.js.map +1 -0
  5. package/dist/cli/bootstrap.test.d.ts +6 -0
  6. package/dist/cli/bootstrap.test.d.ts.map +1 -0
  7. package/dist/cli/bootstrap.test.js +196 -0
  8. package/dist/cli/bootstrap.test.js.map +1 -0
  9. package/dist/cli/detect.d.ts +6 -1
  10. package/dist/cli/detect.d.ts.map +1 -1
  11. package/dist/cli/detect.js +18 -0
  12. package/dist/cli/detect.js.map +1 -1
  13. package/dist/cli/init.d.ts.map +1 -1
  14. package/dist/cli/init.js +21 -10
  15. package/dist/cli/init.js.map +1 -1
  16. package/dist/cli/prompt.js +1 -1
  17. package/dist/cli/prompt.js.map +1 -1
  18. package/package.json +1 -1
  19. package/src/cli/bootstrap.test.ts +286 -0
  20. package/src/cli/bootstrap.ts +472 -0
  21. package/src/cli/detect.ts +22 -1
  22. package/src/cli/init.ts +23 -13
  23. package/src/cli/prompt.ts +1 -1
  24. package/src/dashboard/node_modules/.vite/deps/_metadata.json +6 -6
  25. package/src/orchestrator/agents/team-lead.agent.md +4 -2
  26. package/src/orchestrator/customizations/README.md +3 -3
  27. package/src/orchestrator/customizations/agents/agent-registry.md +1 -1
  28. package/src/orchestrator/customizations/project/docs-structure.md +1 -1
  29. package/src/orchestrator/customizations/project/roadmap.md +1 -1
  30. package/src/orchestrator/customizations/project/tracker-config.md +1 -1
  31. package/src/orchestrator/customizations/project.instructions.md +2 -2
  32. package/src/orchestrator/customizations/stack/api-config.md +1 -1
  33. package/src/orchestrator/customizations/stack/cms-config.md +2 -2
  34. package/src/orchestrator/customizations/stack/data-pipeline-config.md +1 -1
  35. package/src/orchestrator/customizations/stack/database-config.md +2 -2
  36. package/src/orchestrator/customizations/stack/deployment-config.md +1 -1
  37. package/src/orchestrator/customizations/stack/notifications-config.md +1 -1
  38. package/src/orchestrator/customizations/stack/testing-config.md +1 -1
  39. package/src/orchestrator/instructions/general.instructions.md +2 -0
  40. package/src/orchestrator/prompts/bootstrap-customizations.prompt.md +127 -132
  41. package/src/orchestrator/skills/agent-hooks/SKILL.md +7 -2
@@ -0,0 +1,286 @@
1
+ /**
2
+ * Tests for bootstrapCustomizations — validates programmatic population of
3
+ * .opencastle/ template files from RepoInfo and package.json data.
4
+ */
5
+
6
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest'
7
+ import { mkdtemp, readFile, rm, writeFile, mkdir } from 'node:fs/promises'
8
+ import { join, resolve } from 'node:path'
9
+ import { tmpdir } from 'node:os'
10
+ import { existsSync } from 'node:fs'
11
+ import type { StackConfig, RepoInfo } from './types.js'
12
+ import { bootstrapCustomizations } from './bootstrap.js'
13
+ import { copyDir, getOrchestratorRoot } from './copy.js'
14
+
15
+ // ── Helpers ────────────────────────────────────────────────────
16
+
17
+ /** The real package root — tests run against the actual source tree. */
18
+ const PKG_ROOT = resolve(import.meta.dirname, '../..')
19
+
20
+ const STACK_EMPTY: StackConfig = {
21
+ ides: ['vscode'],
22
+ techTools: [],
23
+ teamTools: [],
24
+ }
25
+
26
+ /** Copy raw customization templates to <projectRoot>/.opencastle/ */
27
+ async function scaffoldTemplates(projectRoot: string): Promise<void> {
28
+ const custSrcDir = resolve(getOrchestratorRoot(PKG_ROOT), 'customizations')
29
+ const custDestDir = join(projectRoot, '.opencastle')
30
+ await copyDir(custSrcDir, custDestDir)
31
+ }
32
+
33
+ // ═══════════════════════════════════════════════════════════════
34
+ // § Test suite
35
+ // ═══════════════════════════════════════════════════════════════
36
+
37
+ describe('bootstrapCustomizations', () => {
38
+ let tempDir: string
39
+
40
+ beforeEach(async () => {
41
+ tempDir = await mkdtemp(join(tmpdir(), 'bootstrap-test-'))
42
+ await scaffoldTemplates(tempDir)
43
+ })
44
+
45
+ afterEach(async () => {
46
+ await rm(tempDir, { recursive: true, force: true })
47
+ })
48
+
49
+ // ── 1. Tech stack table ──────────────────────────────────────
50
+
51
+ it('populates project.instructions.md tech stack table from repoInfo', async () => {
52
+ const info: RepoInfo = {
53
+ language: 'typescript',
54
+ frameworks: ['next'],
55
+ testing: ['vitest'],
56
+ deployment: ['vercel'],
57
+ }
58
+ await bootstrapCustomizations(tempDir, info, STACK_EMPTY)
59
+ const content = await readFile(join(tempDir, '.opencastle', 'project.instructions.md'), 'utf8')
60
+ expect(content).toContain('| Language | typescript |')
61
+ expect(content).toContain('| Framework | next |')
62
+ expect(content).toContain('| Testing | vitest |')
63
+ expect(content).toContain('| Deployment | vercel |')
64
+ // Empty placeholder row should be replaced
65
+ expect(content).not.toContain('| | | | |')
66
+ })
67
+
68
+ // ── 2. Testing config ────────────────────────────────────────
69
+
70
+ it('populates testing-config.md with test framework and config file', async () => {
71
+ const info: RepoInfo = {
72
+ testing: ['vitest'],
73
+ configFiles: ['vitest.config.ts'],
74
+ }
75
+ await bootstrapCustomizations(tempDir, info, STACK_EMPTY)
76
+ const content = await readFile(
77
+ join(tempDir, '.opencastle', 'stack', 'testing-config.md'),
78
+ 'utf8',
79
+ )
80
+ expect(content).toContain('vitest')
81
+ expect(content).toContain('`vitest.config.ts`')
82
+ })
83
+
84
+ // ── 3. Deployment config ─────────────────────────────────────
85
+
86
+ it('populates deployment-config.md with deployment platform and config file', async () => {
87
+ const info: RepoInfo = {
88
+ deployment: ['vercel'],
89
+ configFiles: ['vercel.json'],
90
+ }
91
+ await bootstrapCustomizations(tempDir, info, STACK_EMPTY)
92
+ const content = await readFile(
93
+ join(tempDir, '.opencastle', 'stack', 'deployment-config.md'),
94
+ 'utf8',
95
+ )
96
+ expect(content).toContain('vercel')
97
+ expect(content).toContain('`vercel.json`')
98
+ })
99
+
100
+ // ── 4. Remove unused stack files ─────────────────────────────
101
+
102
+ it('removes stack/database-config.md when no databases detected', async () => {
103
+ await bootstrapCustomizations(tempDir, {}, STACK_EMPTY)
104
+ expect(existsSync(join(tempDir, '.opencastle', 'stack', 'database-config.md'))).toBe(false)
105
+ })
106
+
107
+ it('removes stack/testing-config.md when no testing detected', async () => {
108
+ await bootstrapCustomizations(tempDir, {}, STACK_EMPTY)
109
+ expect(existsSync(join(tempDir, '.opencastle', 'stack', 'testing-config.md'))).toBe(false)
110
+ })
111
+
112
+ it('removes stack/deployment-config.md when no deployment detected', async () => {
113
+ await bootstrapCustomizations(tempDir, {}, STACK_EMPTY)
114
+ expect(existsSync(join(tempDir, '.opencastle', 'stack', 'deployment-config.md'))).toBe(false)
115
+ })
116
+
117
+ // ── 5. Rename database-config.md ────────────────────────────
118
+
119
+ it('renames database-config.md to supabase-config.md when supabase detected', async () => {
120
+ const info: RepoInfo = { databases: ['supabase'] }
121
+ const result = await bootstrapCustomizations(tempDir, info, STACK_EMPTY)
122
+ expect(
123
+ existsSync(join(tempDir, '.opencastle', 'stack', 'supabase-config.md')),
124
+ ).toBe(true)
125
+ expect(
126
+ existsSync(join(tempDir, '.opencastle', 'stack', 'database-config.md')),
127
+ ).toBe(false)
128
+ expect(result.renamed.some(r => r.includes('supabase-config.md'))).toBe(true)
129
+ })
130
+
131
+ // ── 6. Rename cms-config.md ──────────────────────────────────
132
+
133
+ it('renames cms-config.md to sanity-config.md when sanity detected', async () => {
134
+ const info: RepoInfo = { cms: ['sanity'] }
135
+ const result = await bootstrapCustomizations(tempDir, info, STACK_EMPTY)
136
+ expect(existsSync(join(tempDir, '.opencastle', 'stack', 'sanity-config.md'))).toBe(true)
137
+ expect(existsSync(join(tempDir, '.opencastle', 'stack', 'cms-config.md'))).toBe(false)
138
+ expect(result.renamed.some(r => r.includes('sanity-config.md'))).toBe(true)
139
+ })
140
+
141
+ // ── 7. Project name and description ──────────────────────────
142
+
143
+ it('fills project name and description from package.json', async () => {
144
+ await writeFile(
145
+ join(tempDir, 'package.json'),
146
+ JSON.stringify({ name: 'my-cool-project', description: 'A really cool project' }),
147
+ 'utf8',
148
+ )
149
+ await bootstrapCustomizations(tempDir, {}, STACK_EMPTY)
150
+ const content = await readFile(
151
+ join(tempDir, '.opencastle', 'project.instructions.md'),
152
+ 'utf8',
153
+ )
154
+ expect(content).toContain('**Project:** my-cool-project')
155
+ expect(content).toContain('**Description:** A really cool project')
156
+ })
157
+
158
+ // ── 8. Key commands from scripts ─────────────────────────────
159
+
160
+ it('populates key commands from package.json scripts', async () => {
161
+ await writeFile(
162
+ join(tempDir, 'package.json'),
163
+ JSON.stringify({
164
+ scripts: { dev: 'next dev', build: 'next build', test: 'vitest' },
165
+ }),
166
+ 'utf8',
167
+ )
168
+ const info: RepoInfo = { packageManager: 'pnpm' }
169
+ await bootstrapCustomizations(tempDir, info, STACK_EMPTY)
170
+ const content = await readFile(
171
+ join(tempDir, '.opencastle', 'project.instructions.md'),
172
+ 'utf8',
173
+ )
174
+ expect(content).toContain('pnpm run dev')
175
+ expect(content).toContain('pnpm run build')
176
+ expect(content).toContain('pnpm run test')
177
+ })
178
+
179
+ // ── 9. Empty repoInfo ────────────────────────────────────────
180
+
181
+ it('handles empty repoInfo gracefully without crashing', async () => {
182
+ const result = await bootstrapCustomizations(tempDir, {}, STACK_EMPTY)
183
+ expect(result).toBeDefined()
184
+ expect(result.populated).toBeInstanceOf(Array)
185
+ expect(result.removed).toBeInstanceOf(Array)
186
+ expect(result.renamed).toBeInstanceOf(Array)
187
+ // project.instructions.md should still exist and keep the empty row (no stack rows added)
188
+ const content = await readFile(
189
+ join(tempDir, '.opencastle', 'project.instructions.md'),
190
+ 'utf8',
191
+ )
192
+ expect(content).toContain('| | | | |')
193
+ })
194
+
195
+ // ── 10. Monorepo workspace listing ────────────────────────────
196
+
197
+ it('lists monorepo workspace packages in project structure table', async () => {
198
+ await mkdir(join(tempDir, 'apps', 'web'), { recursive: true })
199
+ await writeFile(
200
+ join(tempDir, 'apps', 'web', 'package.json'),
201
+ JSON.stringify({ name: '@myproject/web', description: 'Main web app' }),
202
+ 'utf8',
203
+ )
204
+ await mkdir(join(tempDir, 'packages', 'ui'), { recursive: true })
205
+ await writeFile(
206
+ join(tempDir, 'packages', 'ui', 'package.json'),
207
+ JSON.stringify({ name: '@myproject/ui', description: 'UI components' }),
208
+ 'utf8',
209
+ )
210
+
211
+ const info: RepoInfo = { monorepo: 'nx' }
212
+ await bootstrapCustomizations(tempDir, info, STACK_EMPTY)
213
+ const content = await readFile(
214
+ join(tempDir, '.opencastle', 'project.instructions.md'),
215
+ 'utf8',
216
+ )
217
+ expect(content).toContain('@myproject/web')
218
+ expect(content).toContain('Main web app')
219
+ expect(content).toContain('@myproject/ui')
220
+ expect(content).toContain('UI components')
221
+ })
222
+
223
+ // ── 11. API config ───────────────────────────────────────────
224
+
225
+ it('removes api-config.md when no frameworks detected', async () => {
226
+ const result = await bootstrapCustomizations(tempDir, {}, STACK_EMPTY)
227
+ expect(existsSync(join(tempDir, '.opencastle', 'stack', 'api-config.md'))).toBe(false)
228
+ expect(result.removed).toContain('stack/api-config.md')
229
+ })
230
+
231
+ it('populates api-config.md with framework name when frameworks detected', async () => {
232
+ const info: RepoInfo = { frameworks: ['next'] }
233
+ const result = await bootstrapCustomizations(tempDir, info, STACK_EMPTY)
234
+ const content = await readFile(
235
+ join(tempDir, '.opencastle', 'stack', 'api-config.md'),
236
+ 'utf8',
237
+ )
238
+ expect(content).toContain('Framework: next')
239
+ expect(result.populated).toContain('stack/api-config.md')
240
+ })
241
+
242
+ // ── 12. Data pipeline config ─────────────────────────────────
243
+
244
+ it('always removes data-pipeline-config.md', async () => {
245
+ const result = await bootstrapCustomizations(tempDir, {}, STACK_EMPTY)
246
+ expect(
247
+ existsSync(join(tempDir, '.opencastle', 'stack', 'data-pipeline-config.md')),
248
+ ).toBe(false)
249
+ expect(result.removed).toContain('stack/data-pipeline-config.md')
250
+ })
251
+
252
+ // ── 13. Tracker config ───────────────────────────────────────
253
+
254
+ it('renames tracker-config.md to linear-config.md when linear in teamTools', async () => {
255
+ const stack: StackConfig = { ides: ['vscode'], techTools: [], teamTools: ['linear'] }
256
+ const result = await bootstrapCustomizations(tempDir, {}, stack)
257
+ expect(existsSync(join(tempDir, '.opencastle', 'project', 'linear-config.md'))).toBe(true)
258
+ expect(existsSync(join(tempDir, '.opencastle', 'project', 'tracker-config.md'))).toBe(false)
259
+ const content = await readFile(
260
+ join(tempDir, '.opencastle', 'project', 'linear-config.md'),
261
+ 'utf8',
262
+ )
263
+ expect(content).toContain('# Linear Configuration')
264
+ expect(content).not.toContain('# Task Tracker Configuration')
265
+ expect(result.renamed.some(r => r.includes('linear-config.md'))).toBe(true)
266
+ })
267
+
268
+ it('renames tracker-config.md to jira-config.md when jira in teamTools', async () => {
269
+ const stack: StackConfig = { ides: ['vscode'], techTools: [], teamTools: ['jira'] }
270
+ const result = await bootstrapCustomizations(tempDir, {}, stack)
271
+ expect(existsSync(join(tempDir, '.opencastle', 'project', 'jira-config.md'))).toBe(true)
272
+ expect(existsSync(join(tempDir, '.opencastle', 'project', 'tracker-config.md'))).toBe(false)
273
+ const content = await readFile(
274
+ join(tempDir, '.opencastle', 'project', 'jira-config.md'),
275
+ 'utf8',
276
+ )
277
+ expect(content).toContain('# Jira Configuration')
278
+ expect(result.renamed.some(r => r.includes('jira-config.md'))).toBe(true)
279
+ })
280
+
281
+ it('removes tracker-config.md when no tracker in teamTools', async () => {
282
+ const result = await bootstrapCustomizations(tempDir, {}, STACK_EMPTY)
283
+ expect(existsSync(join(tempDir, '.opencastle', 'project', 'tracker-config.md'))).toBe(false)
284
+ expect(result.removed).toContain('project/tracker-config.md')
285
+ })
286
+ })