javi-forge 0.1.0 → 1.0.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 (159) hide show
  1. package/.releaserc +2 -1
  2. package/README.md +143 -31
  3. package/ai-config/commands/workflows/diagnose.md +70 -0
  4. package/ai-config/commands/workflows/discover.md +86 -0
  5. package/dist/commands/doctor.js +24 -1
  6. package/dist/commands/init.js +48 -1
  7. package/dist/commands/llmstxt.d.ts +9 -0
  8. package/dist/commands/llmstxt.js +93 -0
  9. package/dist/commands/llmstxt.test.d.ts +2 -0
  10. package/dist/commands/plugin.d.ts +24 -0
  11. package/dist/commands/plugin.js +78 -0
  12. package/dist/commands/plugin.test.d.ts +2 -0
  13. package/dist/constants.d.ts +8 -0
  14. package/dist/constants.js +8 -0
  15. package/dist/index.js +33 -4
  16. package/dist/lib/plugin.d.ts +39 -0
  17. package/dist/lib/plugin.js +228 -0
  18. package/dist/lib/plugin.test.d.ts +2 -0
  19. package/dist/types/index.d.ts +42 -0
  20. package/dist/ui/App.d.ts +2 -1
  21. package/dist/ui/App.js +2 -1
  22. package/dist/ui/LlmsTxt.d.ts +8 -0
  23. package/dist/ui/LlmsTxt.js +48 -0
  24. package/dist/ui/Plugin.d.ts +9 -0
  25. package/dist/ui/Plugin.js +96 -0
  26. package/modules/obsidian-brain/README.md +32 -0
  27. package/modules/obsidian-brain/core/templates/braindump.md +15 -0
  28. package/modules/obsidian-brain/core/templates/consolidation.md +42 -0
  29. package/modules/obsidian-brain/core/templates/daily-note.md +18 -0
  30. package/modules/obsidian-brain/core/templates/resource-capture.md +14 -0
  31. package/modules/obsidian-brain/developer/templates/adr.md +40 -0
  32. package/modules/obsidian-brain/developer/templates/coding-session.md +24 -0
  33. package/modules/obsidian-brain/developer/templates/debug-journal.md +22 -0
  34. package/modules/obsidian-brain/developer/templates/sdd-feedback.md +27 -0
  35. package/modules/obsidian-brain/developer/templates/tech-debt.md +20 -0
  36. package/modules/obsidian-brain/pm-lead/templates/daily-brief.md +25 -0
  37. package/modules/obsidian-brain/pm-lead/templates/meeting-notes.md +24 -0
  38. package/modules/obsidian-brain/pm-lead/templates/risk-registry.md +12 -0
  39. package/modules/obsidian-brain/pm-lead/templates/sprint-review.md +27 -0
  40. package/modules/obsidian-brain/pm-lead/templates/stakeholder-update.md +24 -0
  41. package/modules/obsidian-brain/pm-lead/templates/team-intelligence.md +19 -0
  42. package/modules/obsidian-brain/pm-lead/templates/weekly-brief.md +29 -0
  43. package/package.json +1 -1
  44. package/schemas/plugin.schema.json +62 -0
  45. package/ai-config/skills/docs/api-documentation/SKILL.md +0 -293
  46. package/ai-config/skills/docs/docs-spring/SKILL.md +0 -377
  47. package/ai-config/skills/docs/mustache-templates/SKILL.md +0 -190
  48. package/ai-config/skills/docs/technical-docs/SKILL.md +0 -447
  49. package/dist/commands/analyze.d.ts.map +0 -1
  50. package/dist/commands/analyze.js.map +0 -1
  51. package/dist/commands/analyze.test.d.ts.map +0 -1
  52. package/dist/commands/analyze.test.js +0 -145
  53. package/dist/commands/analyze.test.js.map +0 -1
  54. package/dist/commands/doctor.d.ts.map +0 -1
  55. package/dist/commands/doctor.js.map +0 -1
  56. package/dist/commands/doctor.test.d.ts.map +0 -1
  57. package/dist/commands/doctor.test.js +0 -200
  58. package/dist/commands/doctor.test.js.map +0 -1
  59. package/dist/commands/init.d.ts.map +0 -1
  60. package/dist/commands/init.js.map +0 -1
  61. package/dist/commands/init.test.d.ts.map +0 -1
  62. package/dist/commands/init.test.js +0 -271
  63. package/dist/commands/init.test.js.map +0 -1
  64. package/dist/commands/sync.d.ts.map +0 -1
  65. package/dist/commands/sync.js.map +0 -1
  66. package/dist/constants.d.ts.map +0 -1
  67. package/dist/constants.js.map +0 -1
  68. package/dist/e2e/aggressive.e2e.test.d.ts.map +0 -1
  69. package/dist/e2e/aggressive.e2e.test.js +0 -350
  70. package/dist/e2e/aggressive.e2e.test.js.map +0 -1
  71. package/dist/e2e/commands.e2e.test.d.ts.map +0 -1
  72. package/dist/e2e/commands.e2e.test.js +0 -213
  73. package/dist/e2e/commands.e2e.test.js.map +0 -1
  74. package/dist/index.d.ts.map +0 -1
  75. package/dist/index.js.map +0 -1
  76. package/dist/lib/common.d.ts.map +0 -1
  77. package/dist/lib/common.js.map +0 -1
  78. package/dist/lib/common.test.d.ts.map +0 -1
  79. package/dist/lib/common.test.js +0 -316
  80. package/dist/lib/common.test.js.map +0 -1
  81. package/dist/lib/frontmatter.d.ts.map +0 -1
  82. package/dist/lib/frontmatter.js.map +0 -1
  83. package/dist/lib/frontmatter.test.d.ts.map +0 -1
  84. package/dist/lib/frontmatter.test.js +0 -257
  85. package/dist/lib/frontmatter.test.js.map +0 -1
  86. package/dist/lib/template.d.ts.map +0 -1
  87. package/dist/lib/template.js.map +0 -1
  88. package/dist/lib/template.test.d.ts.map +0 -1
  89. package/dist/lib/template.test.js +0 -201
  90. package/dist/lib/template.test.js.map +0 -1
  91. package/dist/types/index.d.ts.map +0 -1
  92. package/dist/types/index.js.map +0 -1
  93. package/dist/ui/AnalyzeUI.d.ts.map +0 -1
  94. package/dist/ui/AnalyzeUI.js.map +0 -1
  95. package/dist/ui/App.d.ts.map +0 -1
  96. package/dist/ui/App.js.map +0 -1
  97. package/dist/ui/CIContext.d.ts.map +0 -1
  98. package/dist/ui/CIContext.js.map +0 -1
  99. package/dist/ui/CISelector.d.ts.map +0 -1
  100. package/dist/ui/CISelector.js.map +0 -1
  101. package/dist/ui/Doctor.d.ts.map +0 -1
  102. package/dist/ui/Doctor.js.map +0 -1
  103. package/dist/ui/Header.d.ts.map +0 -1
  104. package/dist/ui/Header.js.map +0 -1
  105. package/dist/ui/MemorySelector.d.ts.map +0 -1
  106. package/dist/ui/MemorySelector.js.map +0 -1
  107. package/dist/ui/NameInput.d.ts.map +0 -1
  108. package/dist/ui/NameInput.js.map +0 -1
  109. package/dist/ui/OptionSelector.d.ts.map +0 -1
  110. package/dist/ui/OptionSelector.js.map +0 -1
  111. package/dist/ui/Progress.d.ts.map +0 -1
  112. package/dist/ui/Progress.js.map +0 -1
  113. package/dist/ui/StackSelector.d.ts.map +0 -1
  114. package/dist/ui/StackSelector.js.map +0 -1
  115. package/dist/ui/Summary.d.ts.map +0 -1
  116. package/dist/ui/Summary.js.map +0 -1
  117. package/dist/ui/SyncUI.d.ts.map +0 -1
  118. package/dist/ui/SyncUI.js.map +0 -1
  119. package/dist/ui/Welcome.d.ts.map +0 -1
  120. package/dist/ui/Welcome.js.map +0 -1
  121. package/dist/ui/theme.d.ts.map +0 -1
  122. package/dist/ui/theme.js.map +0 -1
  123. package/modules/obsidian-brain/.obsidian/plugins/dataview/data.json +0 -25
  124. package/modules/obsidian-brain/.obsidian/plugins/obsidian-kanban/data.json +0 -29
  125. package/modules/obsidian-brain/.obsidian/plugins/templater-obsidian/data.json +0 -18
  126. package/src/commands/analyze.test.ts +0 -145
  127. package/src/commands/analyze.ts +0 -69
  128. package/src/commands/doctor.test.ts +0 -208
  129. package/src/commands/doctor.ts +0 -163
  130. package/src/commands/init.test.ts +0 -298
  131. package/src/commands/init.ts +0 -285
  132. package/src/constants.ts +0 -69
  133. package/src/e2e/aggressive.e2e.test.ts +0 -557
  134. package/src/e2e/commands.e2e.test.ts +0 -298
  135. package/src/index.tsx +0 -106
  136. package/src/lib/common.test.ts +0 -318
  137. package/src/lib/common.ts +0 -127
  138. package/src/lib/frontmatter.test.ts +0 -291
  139. package/src/lib/frontmatter.ts +0 -77
  140. package/src/lib/template.test.ts +0 -226
  141. package/src/lib/template.ts +0 -99
  142. package/src/types/index.ts +0 -53
  143. package/src/ui/AnalyzeUI.tsx +0 -133
  144. package/src/ui/App.tsx +0 -175
  145. package/src/ui/CIContext.tsx +0 -25
  146. package/src/ui/CISelector.tsx +0 -72
  147. package/src/ui/Doctor.tsx +0 -122
  148. package/src/ui/Header.tsx +0 -48
  149. package/src/ui/MemorySelector.tsx +0 -73
  150. package/src/ui/NameInput.tsx +0 -82
  151. package/src/ui/OptionSelector.tsx +0 -100
  152. package/src/ui/Progress.tsx +0 -88
  153. package/src/ui/StackSelector.tsx +0 -101
  154. package/src/ui/Summary.tsx +0 -134
  155. package/src/ui/Welcome.tsx +0 -54
  156. package/src/ui/theme.ts +0 -10
  157. package/stryker.config.json +0 -19
  158. package/tsconfig.json +0 -19
  159. package/vitest.config.ts +0 -16
@@ -1,557 +0,0 @@
1
- /**
2
- * Aggressive E2E tests for javi-forge init command.
3
- *
4
- * These tests do REAL filesystem operations in sandboxed temp directories.
5
- * They verify that `init` actually creates the expected project structure,
6
- * files, and content — not dry-run.
7
- *
8
- * Prerequisites: `pnpm build` must be run before these tests.
9
- */
10
- import { execFile } from 'child_process'
11
- import { promisify } from 'util'
12
- import path from 'path'
13
- import os from 'os'
14
- import fs from 'fs-extra'
15
- import crypto from 'crypto'
16
- import { describe, it, expect, afterEach } from 'vitest'
17
-
18
- const execFileAsync = promisify(execFile)
19
- const CLI_PATH = path.resolve(__dirname, '../../dist/index.js')
20
-
21
- // ── Helpers ──────────────────────────────────────────────────────────────────
22
-
23
- const sandboxes: string[] = []
24
-
25
- async function createSandbox(): Promise<string> {
26
- const dir = path.join(os.tmpdir(), `javi-forge-aggressive-${crypto.randomUUID()}`)
27
- await fs.ensureDir(dir)
28
- sandboxes.push(dir)
29
- return dir
30
- }
31
-
32
- afterEach(async () => {
33
- for (const dir of sandboxes) {
34
- await fs.remove(dir).catch(() => {})
35
- }
36
- sandboxes.length = 0
37
- })
38
-
39
- /**
40
- * Run the CLI for real (no --dry-run) in a sandbox.
41
- * Always sets CI=1 and --batch for non-interactive mode.
42
- * Defaults: --no-ai-sync by NOT including aiSync (OptionSelector auto-confirms
43
- * with defaults which includes aiSync, so we accept javi-ai errors gracefully).
44
- */
45
- async function runInit(
46
- args: string[],
47
- cwd: string,
48
- timeout = 60_000
49
- ): Promise<{ stdout: string; stderr: string; exitCode: number }> {
50
- try {
51
- const { stdout, stderr } = await execFileAsync(
52
- 'node',
53
- [CLI_PATH, 'init', '--batch', ...args],
54
- {
55
- timeout,
56
- cwd,
57
- env: { ...process.env, FORCE_COLOR: '0', CI: '1' },
58
- }
59
- )
60
- return { stdout, stderr, exitCode: 0 }
61
- } catch (e: any) {
62
- return {
63
- stdout: e.stdout ?? '',
64
- stderr: e.stderr ?? '',
65
- exitCode: e.code ?? 1,
66
- }
67
- }
68
- }
69
-
70
- /** Get the project directory inside a sandbox */
71
- function projectDir(sandbox: string, name: string): string {
72
- return path.join(sandbox, name)
73
- }
74
-
75
- /** Check if a file exists in the project */
76
- async function fileExists(sandbox: string, name: string, ...segments: string[]): Promise<boolean> {
77
- return fs.pathExists(path.join(sandbox, name, ...segments))
78
- }
79
-
80
- /** Read a file from the project */
81
- async function readProjectFile(sandbox: string, name: string, ...segments: string[]): Promise<string> {
82
- return fs.readFile(path.join(sandbox, name, ...segments), 'utf-8')
83
- }
84
-
85
- // ── Project creation tests ───────────────────────────────────────────────────
86
-
87
- describe('Project creation: init creates complete project', () => {
88
- it('init --stack node --ci github: creates complete project structure', async () => {
89
- const sandbox = await createSandbox()
90
- const { exitCode } = await runInit(
91
- ['--project-name', 'test-app', '--stack', 'node', '--ci', 'github', '--memory', 'none'],
92
- sandbox
93
- )
94
-
95
- expect(exitCode).toBe(0)
96
-
97
- // .git/ directory (git initialized)
98
- expect(await fileExists(sandbox, 'test-app', '.git')).toBe(true)
99
-
100
- // .github/workflows/ci.yml (CI template)
101
- expect(await fileExists(sandbox, 'test-app', '.github', 'workflows', 'ci.yml')).toBe(true)
102
-
103
- // .github/dependabot.yml (dependabot config)
104
- expect(await fileExists(sandbox, 'test-app', '.github', 'dependabot.yml')).toBe(true)
105
-
106
- // .gitignore (non-empty)
107
- expect(await fileExists(sandbox, 'test-app', '.gitignore')).toBe(true)
108
- const gitignore = await readProjectFile(sandbox, 'test-app', '.gitignore')
109
- expect(gitignore.length).toBeGreaterThan(0)
110
-
111
- // .javi-forge/manifest.json (valid JSON)
112
- expect(await fileExists(sandbox, 'test-app', '.javi-forge', 'manifest.json')).toBe(true)
113
- const manifest = await fs.readJson(path.join(sandbox, 'test-app', '.javi-forge', 'manifest.json'))
114
- expect(manifest).toBeDefined()
115
- expect(manifest.stack).toBe('node')
116
- })
117
- })
118
-
119
- // ── CI content per stack ─────────────────────────────────────────────────────
120
-
121
- describe('CI content per stack', () => {
122
- it('init --stack python --ci github: CI contains Python content', async () => {
123
- const sandbox = await createSandbox()
124
- await runInit(
125
- ['--project-name', 'py-app', '--stack', 'python', '--ci', 'github', '--memory', 'none'],
126
- sandbox
127
- )
128
-
129
- const ciContent = await readProjectFile(sandbox, 'py-app', '.github', 'workflows', 'ci.yml')
130
- const lower = ciContent.toLowerCase()
131
- expect(
132
- lower.includes('pytest') || lower.includes('pip') || lower.includes('python')
133
- ).toBe(true)
134
- })
135
-
136
- it('init --stack go --ci github: CI contains Go content', async () => {
137
- const sandbox = await createSandbox()
138
- await runInit(
139
- ['--project-name', 'go-app', '--stack', 'go', '--ci', 'github', '--memory', 'none'],
140
- sandbox
141
- )
142
-
143
- const ciContent = await readProjectFile(sandbox, 'go-app', '.github', 'workflows', 'ci.yml')
144
- const lower = ciContent.toLowerCase()
145
- expect(
146
- lower.includes('go test') || lower.includes('golangci') || lower.includes('go-version')
147
- ).toBe(true)
148
- })
149
-
150
- it('init --stack java-gradle --ci github: CI contains Gradle content', async () => {
151
- const sandbox = await createSandbox()
152
- await runInit(
153
- ['--project-name', 'java-app', '--stack', 'java-gradle', '--ci', 'github', '--memory', 'none'],
154
- sandbox
155
- )
156
-
157
- const ciContent = await readProjectFile(sandbox, 'java-app', '.github', 'workflows', 'ci.yml')
158
- const lower = ciContent.toLowerCase()
159
- expect(
160
- lower.includes('gradle') || lower.includes('java') || lower.includes('jdk')
161
- ).toBe(true)
162
- })
163
-
164
- it('init --stack rust --ci github: CI contains Rust/Cargo content', async () => {
165
- const sandbox = await createSandbox()
166
- await runInit(
167
- ['--project-name', 'rust-app', '--stack', 'rust', '--ci', 'github', '--memory', 'none'],
168
- sandbox
169
- )
170
-
171
- const ciContent = await readProjectFile(sandbox, 'rust-app', '.github', 'workflows', 'ci.yml')
172
- const lower = ciContent.toLowerCase()
173
- expect(
174
- lower.includes('cargo') || lower.includes('clippy') || lower.includes('rust')
175
- ).toBe(true)
176
- })
177
- })
178
-
179
- // ── CI providers ─────────────────────────────────────────────────────────────
180
-
181
- describe('CI providers', () => {
182
- it('init --ci gitlab: creates .gitlab-ci.yml', async () => {
183
- const sandbox = await createSandbox()
184
- await runInit(
185
- ['--project-name', 'gl-app', '--stack', 'node', '--ci', 'gitlab', '--memory', 'none'],
186
- sandbox
187
- )
188
-
189
- expect(await fileExists(sandbox, 'gl-app', '.gitlab-ci.yml')).toBe(true)
190
- // Should NOT have GitHub workflows
191
- expect(await fileExists(sandbox, 'gl-app', '.github', 'workflows')).toBe(false)
192
- })
193
-
194
- it('init --ci woodpecker: creates .woodpecker.yml', async () => {
195
- const sandbox = await createSandbox()
196
- await runInit(
197
- ['--project-name', 'wp-app', '--stack', 'node', '--ci', 'woodpecker', '--memory', 'none'],
198
- sandbox
199
- )
200
-
201
- const hasYml = await fileExists(sandbox, 'wp-app', '.woodpecker.yml')
202
- const hasDir = await fileExists(sandbox, 'wp-app', '.woodpecker')
203
- expect(hasYml || hasDir).toBe(true)
204
- })
205
- })
206
-
207
- // ── Memory module tests ──────────────────────────────────────────────────────
208
-
209
- describe('Memory modules', () => {
210
- it('init --memory engram: installs engram module', async () => {
211
- const sandbox = await createSandbox()
212
- await runInit(
213
- ['--project-name', 'eng-app', '--stack', 'node', '--ci', 'github', '--memory', 'engram'],
214
- sandbox
215
- )
216
-
217
- // Engram module files copied to .javi-forge/modules/engram/
218
- expect(await fileExists(sandbox, 'eng-app', '.javi-forge', 'modules', 'engram')).toBe(true)
219
-
220
- // .mcp-config-snippet.json copied to project root
221
- expect(await fileExists(sandbox, 'eng-app', '.mcp-config-snippet.json')).toBe(true)
222
- })
223
-
224
- it('init --memory obsidian-brain: installs obsidian brain module', async () => {
225
- const sandbox = await createSandbox()
226
- await runInit(
227
- ['--project-name', 'obs-app', '--stack', 'node', '--ci', 'github', '--memory', 'obsidian-brain'],
228
- sandbox
229
- )
230
-
231
- // Module copied to .javi-forge/modules/obsidian-brain/
232
- expect(await fileExists(sandbox, 'obs-app', '.javi-forge', 'modules', 'obsidian-brain')).toBe(true)
233
-
234
- // .project/Memory structure inside the module copy
235
- expect(
236
- await fileExists(sandbox, 'obs-app', '.javi-forge', 'modules', 'obsidian-brain', '.project', 'Memory', 'CONTEXT.md')
237
- ).toBe(true)
238
- expect(
239
- await fileExists(sandbox, 'obs-app', '.javi-forge', 'modules', 'obsidian-brain', '.project', 'Memory', 'KANBAN.md')
240
- ).toBe(true)
241
- })
242
-
243
- it('init --memory memory-simple: installs simple memory module', async () => {
244
- const sandbox = await createSandbox()
245
- await runInit(
246
- ['--project-name', 'sim-app', '--stack', 'node', '--ci', 'github', '--memory', 'memory-simple'],
247
- sandbox
248
- )
249
-
250
- // Module copied to .javi-forge/modules/memory-simple/
251
- expect(await fileExists(sandbox, 'sim-app', '.javi-forge', 'modules', 'memory-simple')).toBe(true)
252
-
253
- // .project/NOTES.md inside the module copy
254
- expect(
255
- await fileExists(sandbox, 'sim-app', '.javi-forge', 'modules', 'memory-simple', '.project', 'NOTES.md')
256
- ).toBe(true)
257
- })
258
-
259
- it('init --memory none: no memory module installed', async () => {
260
- const sandbox = await createSandbox()
261
- await runInit(
262
- ['--project-name', 'no-mem', '--stack', 'node', '--ci', 'github', '--memory', 'none'],
263
- sandbox
264
- )
265
-
266
- // No modules directory for memory
267
- const modulesDir = path.join(sandbox, 'no-mem', '.javi-forge', 'modules')
268
- const hasModulesDir = await fs.pathExists(modulesDir)
269
- if (hasModulesDir) {
270
- const entries = await fs.readdir(modulesDir)
271
- // Should not contain engram, obsidian-brain, or memory-simple
272
- const memoryModules = entries.filter(e =>
273
- ['engram', 'obsidian-brain', 'memory-simple'].includes(e)
274
- )
275
- expect(memoryModules).toHaveLength(0)
276
- }
277
- })
278
- })
279
-
280
- // ── Dependabot ecosystem tests ───────────────────────────────────────────────
281
-
282
- describe('Dependabot ecosystems', () => {
283
- it('node project: dependabot has npm ecosystem', async () => {
284
- const sandbox = await createSandbox()
285
- await runInit(
286
- ['--project-name', 'dep-node', '--stack', 'node', '--ci', 'github', '--memory', 'none'],
287
- sandbox
288
- )
289
-
290
- const content = await readProjectFile(sandbox, 'dep-node', '.github', 'dependabot.yml')
291
- expect(content).toContain('npm')
292
- })
293
-
294
- it('python project: dependabot has pip ecosystem', async () => {
295
- const sandbox = await createSandbox()
296
- await runInit(
297
- ['--project-name', 'dep-py', '--stack', 'python', '--ci', 'github', '--memory', 'none'],
298
- sandbox
299
- )
300
-
301
- const content = await readProjectFile(sandbox, 'dep-py', '.github', 'dependabot.yml')
302
- expect(content).toContain('pip')
303
- })
304
-
305
- it('go project: dependabot has gomod ecosystem', async () => {
306
- const sandbox = await createSandbox()
307
- await runInit(
308
- ['--project-name', 'dep-go', '--stack', 'go', '--ci', 'github', '--memory', 'none'],
309
- sandbox
310
- )
311
-
312
- const content = await readProjectFile(sandbox, 'dep-go', '.github', 'dependabot.yml')
313
- expect(content).toContain('gomod')
314
- })
315
- })
316
-
317
- // ── SDD tests ────────────────────────────────────────────────────────────────
318
-
319
- describe('SDD (Spec-Driven Development)', () => {
320
- it('init with SDD default (CI auto-confirms with sdd=true): creates openspec/', async () => {
321
- const sandbox = await createSandbox()
322
- // In CI/batch mode, OptionSelector defaults to sdd=true
323
- await runInit(
324
- ['--project-name', 'sdd-app', '--stack', 'node', '--ci', 'github', '--memory', 'none'],
325
- sandbox
326
- )
327
-
328
- expect(await fileExists(sandbox, 'sdd-app', 'openspec')).toBe(true)
329
- expect(await fileExists(sandbox, 'sdd-app', 'openspec', 'README.md')).toBe(true)
330
- })
331
- })
332
-
333
- // ── GHAGGA tests ─────────────────────────────────────────────────────────────
334
-
335
- describe('GHAGGA review system', () => {
336
- it('init --ghagga --ci github: creates ghagga workflow', async () => {
337
- const sandbox = await createSandbox()
338
- await runInit(
339
- ['--project-name', 'ghagga-app', '--stack', 'node', '--ci', 'github', '--memory', 'none', '--ghagga'],
340
- sandbox
341
- )
342
-
343
- // GHAGGA module installed
344
- expect(await fileExists(sandbox, 'ghagga-app', '.javi-forge', 'modules', 'ghagga')).toBe(true)
345
-
346
- // Ghagga workflow file
347
- const workflowDir = path.join(sandbox, 'ghagga-app', '.github', 'workflows')
348
- if (await fs.pathExists(workflowDir)) {
349
- const files = await fs.readdir(workflowDir)
350
- const hasGhaggaWorkflow = files.some(f => f.toLowerCase().includes('ghagga'))
351
- expect(hasGhaggaWorkflow).toBe(true)
352
- }
353
- })
354
-
355
- it('init without --ghagga: no ghagga module or workflow', async () => {
356
- const sandbox = await createSandbox()
357
- await runInit(
358
- ['--project-name', 'no-ghagga', '--stack', 'node', '--ci', 'github', '--memory', 'none'],
359
- sandbox
360
- )
361
-
362
- // No ghagga module
363
- expect(
364
- await fileExists(sandbox, 'no-ghagga', '.javi-forge', 'modules', 'ghagga')
365
- ).toBe(false)
366
-
367
- // No ghagga workflow
368
- const workflowDir = path.join(sandbox, 'no-ghagga', '.github', 'workflows')
369
- if (await fs.pathExists(workflowDir)) {
370
- const files = await fs.readdir(workflowDir)
371
- const hasGhaggaWorkflow = files.some(f => f.toLowerCase().includes('ghagga'))
372
- expect(hasGhaggaWorkflow).toBe(false)
373
- }
374
- })
375
- })
376
-
377
- // ── Manifest tests ───────────────────────────────────────────────────────────
378
-
379
- describe('Manifest metadata', () => {
380
- it('manifest contains correct metadata fields', async () => {
381
- const sandbox = await createSandbox()
382
- await runInit(
383
- ['--project-name', 'meta-app', '--stack', 'python', '--ci', 'github', '--memory', 'engram'],
384
- sandbox
385
- )
386
-
387
- const manifest = await fs.readJson(
388
- path.join(sandbox, 'meta-app', '.javi-forge', 'manifest.json')
389
- )
390
-
391
- expect(manifest).toHaveProperty('stack')
392
- expect(manifest).toHaveProperty('ciProvider')
393
- expect(manifest).toHaveProperty('memory')
394
- expect(manifest).toHaveProperty('createdAt')
395
- expect(manifest.stack).toBe('python')
396
- expect(manifest.ciProvider).toBe('github')
397
- expect(manifest.memory).toBe('engram')
398
- expect(manifest.projectName).toBe('meta-app')
399
- })
400
-
401
- it('different stacks produce different manifests', async () => {
402
- const sandbox = await createSandbox()
403
-
404
- await runInit(
405
- ['--project-name', 'proj-node', '--stack', 'node', '--ci', 'github', '--memory', 'none'],
406
- sandbox
407
- )
408
- await runInit(
409
- ['--project-name', 'proj-go', '--stack', 'go', '--ci', 'github', '--memory', 'none'],
410
- sandbox
411
- )
412
-
413
- const manifestNode = await fs.readJson(
414
- path.join(sandbox, 'proj-node', '.javi-forge', 'manifest.json')
415
- )
416
- const manifestGo = await fs.readJson(
417
- path.join(sandbox, 'proj-go', '.javi-forge', 'manifest.json')
418
- )
419
-
420
- expect(manifestNode.stack).toBe('node')
421
- expect(manifestGo.stack).toBe('go')
422
- expect(manifestNode.stack).not.toBe(manifestGo.stack)
423
- })
424
- })
425
-
426
- // ── Gitignore tests ──────────────────────────────────────────────────────────
427
-
428
- describe('Gitignore generation', () => {
429
- it('generated .gitignore is non-empty with at least 5 lines', async () => {
430
- const sandbox = await createSandbox()
431
- await runInit(
432
- ['--project-name', 'gi-app', '--stack', 'node', '--ci', 'github', '--memory', 'none'],
433
- sandbox
434
- )
435
-
436
- const content = await readProjectFile(sandbox, 'gi-app', '.gitignore')
437
- const lines = content.split('\n').filter(l => l.trim().length > 0)
438
- expect(lines.length).toBeGreaterThanOrEqual(5)
439
- })
440
-
441
- it('generated .gitignore contains common patterns', async () => {
442
- const sandbox = await createSandbox()
443
- await runInit(
444
- ['--project-name', 'gi-app2', '--stack', 'node', '--ci', 'github', '--memory', 'none'],
445
- sandbox
446
- )
447
-
448
- const content = await readProjectFile(sandbox, 'gi-app2', '.gitignore')
449
- // The template has common patterns like .env, .idea, .DS_Store
450
- expect(
451
- content.includes('.env') ||
452
- content.includes('node_modules') ||
453
- content.includes('.DS_Store') ||
454
- content.includes('.idea')
455
- ).toBe(true)
456
- })
457
- })
458
-
459
- // ── Idempotency tests ────────────────────────────────────────────────────────
460
-
461
- describe('Idempotency', () => {
462
- it('init in already-initialized project does not crash on second run', async () => {
463
- const sandbox = await createSandbox()
464
-
465
- // First run
466
- const first = await runInit(
467
- ['--project-name', 'idem-app', '--stack', 'node', '--ci', 'github', '--memory', 'none'],
468
- sandbox
469
- )
470
- expect(first.exitCode).toBe(0)
471
-
472
- // Second run — same project name in same sandbox
473
- const second = await runInit(
474
- ['--project-name', 'idem-app', '--stack', 'node', '--ci', 'github', '--memory', 'none'],
475
- sandbox
476
- )
477
- // Should not crash — exits 0
478
- expect(second.exitCode).toBe(0)
479
-
480
- // Project should still be valid
481
- expect(await fileExists(sandbox, 'idem-app', '.javi-forge', 'manifest.json')).toBe(true)
482
- })
483
- })
484
-
485
- // ── CI Local tests ───────────────────────────────────────────────────────────
486
-
487
- describe('CI Local setup', () => {
488
- it('init creates ci-local if source dir exists', async () => {
489
- const sandbox = await createSandbox()
490
- await runInit(
491
- ['--project-name', 'ci-app', '--stack', 'node', '--ci', 'github', '--memory', 'none'],
492
- sandbox
493
- )
494
-
495
- // ci-local dir should exist (copied from forge root)
496
- const ciLocalDir = path.join(sandbox, 'ci-app', 'ci-local')
497
- const hasCILocal = await fs.pathExists(ciLocalDir)
498
- if (hasCILocal) {
499
- // Should have ci-local.sh or hooks
500
- const hasCIScript = await fs.pathExists(path.join(ciLocalDir, 'ci-local.sh'))
501
- const hasHooks = await fs.pathExists(path.join(ciLocalDir, 'hooks'))
502
- expect(hasCIScript || hasHooks).toBe(true)
503
- }
504
- // If ci-local source doesn't exist at forge root, this step is skipped — that's OK
505
- expect(true).toBe(true)
506
- })
507
- })
508
-
509
- // ── Cross-stack CI generation ────────────────────────────────────────────────
510
-
511
- describe('Cross-stack CI generation', () => {
512
- const stacks = ['node', 'python', 'go', 'rust', 'java-gradle'] as const
513
-
514
- for (const stack of stacks) {
515
- it(`init --stack ${stack} --ci github: generates CI workflow file`, async () => {
516
- const sandbox = await createSandbox()
517
- const name = `ci-${stack}`
518
- await runInit(
519
- ['--project-name', name, '--stack', stack, '--ci', 'github', '--memory', 'none'],
520
- sandbox
521
- )
522
-
523
- expect(await fileExists(sandbox, name, '.github', 'workflows', 'ci.yml')).toBe(true)
524
- const content = await readProjectFile(sandbox, name, '.github', 'workflows', 'ci.yml')
525
- expect(content.length).toBeGreaterThan(10)
526
- })
527
- }
528
- })
529
-
530
- // ── Dependabot with github-actions fragment ──────────────────────────────────
531
-
532
- describe('Dependabot includes github-actions', () => {
533
- it('dependabot.yml contains github-actions update section', async () => {
534
- const sandbox = await createSandbox()
535
- await runInit(
536
- ['--project-name', 'dbot-app', '--stack', 'node', '--ci', 'github', '--memory', 'none'],
537
- sandbox
538
- )
539
-
540
- const content = await readProjectFile(sandbox, 'dbot-app', '.github', 'dependabot.yml')
541
- expect(content).toContain('github-actions')
542
- })
543
- })
544
-
545
- // ── GitLab does not create dependabot ────────────────────────────────────────
546
-
547
- describe('Non-GitHub CI skips dependabot', () => {
548
- it('gitlab CI does not create .github/dependabot.yml', async () => {
549
- const sandbox = await createSandbox()
550
- await runInit(
551
- ['--project-name', 'gl-nodep', '--stack', 'node', '--ci', 'gitlab', '--memory', 'none'],
552
- sandbox
553
- )
554
-
555
- expect(await fileExists(sandbox, 'gl-nodep', '.github', 'dependabot.yml')).toBe(false)
556
- })
557
- })