javi-forge 0.1.0 → 1.1.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/ci-local/ci-local.sh +18 -0
  6. package/ci-local/docker/node.Dockerfile +7 -0
  7. package/ci-local/hooks/commit-msg +0 -0
  8. package/ci-local/hooks/pre-commit +0 -0
  9. package/ci-local/hooks/pre-push +0 -0
  10. package/ci-local/install.sh +0 -0
  11. package/dist/commands/doctor.js +24 -1
  12. package/dist/commands/init.js +48 -1
  13. package/dist/commands/llmstxt.d.ts +9 -0
  14. package/dist/commands/llmstxt.js +93 -0
  15. package/dist/commands/llmstxt.test.d.ts +2 -0
  16. package/dist/commands/plugin.d.ts +24 -0
  17. package/dist/commands/plugin.js +78 -0
  18. package/dist/commands/plugin.test.d.ts +2 -0
  19. package/dist/constants.d.ts +8 -0
  20. package/dist/constants.js +8 -0
  21. package/dist/index.js +33 -4
  22. package/dist/lib/plugin.d.ts +39 -0
  23. package/dist/lib/plugin.js +228 -0
  24. package/dist/lib/plugin.test.d.ts +2 -0
  25. package/dist/types/index.d.ts +42 -0
  26. package/dist/ui/App.d.ts +2 -1
  27. package/dist/ui/App.js +2 -1
  28. package/dist/ui/LlmsTxt.d.ts +8 -0
  29. package/dist/ui/LlmsTxt.js +48 -0
  30. package/dist/ui/Plugin.d.ts +9 -0
  31. package/dist/ui/Plugin.js +96 -0
  32. package/lib/common.sh +183 -0
  33. package/modules/obsidian-brain/README.md +32 -0
  34. package/modules/obsidian-brain/core/templates/braindump.md +15 -0
  35. package/modules/obsidian-brain/core/templates/consolidation.md +42 -0
  36. package/modules/obsidian-brain/core/templates/daily-note.md +18 -0
  37. package/modules/obsidian-brain/core/templates/resource-capture.md +14 -0
  38. package/modules/obsidian-brain/developer/templates/adr.md +40 -0
  39. package/modules/obsidian-brain/developer/templates/coding-session.md +24 -0
  40. package/modules/obsidian-brain/developer/templates/debug-journal.md +22 -0
  41. package/modules/obsidian-brain/developer/templates/sdd-feedback.md +27 -0
  42. package/modules/obsidian-brain/developer/templates/tech-debt.md +20 -0
  43. package/modules/obsidian-brain/pm-lead/templates/daily-brief.md +25 -0
  44. package/modules/obsidian-brain/pm-lead/templates/meeting-notes.md +24 -0
  45. package/modules/obsidian-brain/pm-lead/templates/risk-registry.md +12 -0
  46. package/modules/obsidian-brain/pm-lead/templates/sprint-review.md +27 -0
  47. package/modules/obsidian-brain/pm-lead/templates/stakeholder-update.md +24 -0
  48. package/modules/obsidian-brain/pm-lead/templates/team-intelligence.md +19 -0
  49. package/modules/obsidian-brain/pm-lead/templates/weekly-brief.md +29 -0
  50. package/package.json +12 -12
  51. package/schemas/plugin.schema.json +62 -0
  52. package/dist/commands/analyze.d.ts.map +0 -1
  53. package/dist/commands/analyze.js.map +0 -1
  54. package/dist/commands/analyze.test.d.ts.map +0 -1
  55. package/dist/commands/analyze.test.js +0 -145
  56. package/dist/commands/analyze.test.js.map +0 -1
  57. package/dist/commands/doctor.d.ts.map +0 -1
  58. package/dist/commands/doctor.js.map +0 -1
  59. package/dist/commands/doctor.test.d.ts.map +0 -1
  60. package/dist/commands/doctor.test.js +0 -200
  61. package/dist/commands/doctor.test.js.map +0 -1
  62. package/dist/commands/init.d.ts.map +0 -1
  63. package/dist/commands/init.js.map +0 -1
  64. package/dist/commands/init.test.d.ts.map +0 -1
  65. package/dist/commands/init.test.js +0 -271
  66. package/dist/commands/init.test.js.map +0 -1
  67. package/dist/commands/sync.d.ts.map +0 -1
  68. package/dist/commands/sync.js.map +0 -1
  69. package/dist/constants.d.ts.map +0 -1
  70. package/dist/constants.js.map +0 -1
  71. package/dist/e2e/aggressive.e2e.test.d.ts.map +0 -1
  72. package/dist/e2e/aggressive.e2e.test.js +0 -350
  73. package/dist/e2e/aggressive.e2e.test.js.map +0 -1
  74. package/dist/e2e/commands.e2e.test.d.ts.map +0 -1
  75. package/dist/e2e/commands.e2e.test.js +0 -213
  76. package/dist/e2e/commands.e2e.test.js.map +0 -1
  77. package/dist/index.d.ts.map +0 -1
  78. package/dist/index.js.map +0 -1
  79. package/dist/lib/common.d.ts.map +0 -1
  80. package/dist/lib/common.js.map +0 -1
  81. package/dist/lib/common.test.d.ts.map +0 -1
  82. package/dist/lib/common.test.js +0 -316
  83. package/dist/lib/common.test.js.map +0 -1
  84. package/dist/lib/frontmatter.d.ts.map +0 -1
  85. package/dist/lib/frontmatter.js.map +0 -1
  86. package/dist/lib/frontmatter.test.d.ts.map +0 -1
  87. package/dist/lib/frontmatter.test.js +0 -257
  88. package/dist/lib/frontmatter.test.js.map +0 -1
  89. package/dist/lib/template.d.ts.map +0 -1
  90. package/dist/lib/template.js.map +0 -1
  91. package/dist/lib/template.test.d.ts.map +0 -1
  92. package/dist/lib/template.test.js +0 -201
  93. package/dist/lib/template.test.js.map +0 -1
  94. package/dist/types/index.d.ts.map +0 -1
  95. package/dist/types/index.js.map +0 -1
  96. package/dist/ui/AnalyzeUI.d.ts.map +0 -1
  97. package/dist/ui/AnalyzeUI.js.map +0 -1
  98. package/dist/ui/App.d.ts.map +0 -1
  99. package/dist/ui/App.js.map +0 -1
  100. package/dist/ui/CIContext.d.ts.map +0 -1
  101. package/dist/ui/CIContext.js.map +0 -1
  102. package/dist/ui/CISelector.d.ts.map +0 -1
  103. package/dist/ui/CISelector.js.map +0 -1
  104. package/dist/ui/Doctor.d.ts.map +0 -1
  105. package/dist/ui/Doctor.js.map +0 -1
  106. package/dist/ui/Header.d.ts.map +0 -1
  107. package/dist/ui/Header.js.map +0 -1
  108. package/dist/ui/MemorySelector.d.ts.map +0 -1
  109. package/dist/ui/MemorySelector.js.map +0 -1
  110. package/dist/ui/NameInput.d.ts.map +0 -1
  111. package/dist/ui/NameInput.js.map +0 -1
  112. package/dist/ui/OptionSelector.d.ts.map +0 -1
  113. package/dist/ui/OptionSelector.js.map +0 -1
  114. package/dist/ui/Progress.d.ts.map +0 -1
  115. package/dist/ui/Progress.js.map +0 -1
  116. package/dist/ui/StackSelector.d.ts.map +0 -1
  117. package/dist/ui/StackSelector.js.map +0 -1
  118. package/dist/ui/Summary.d.ts.map +0 -1
  119. package/dist/ui/Summary.js.map +0 -1
  120. package/dist/ui/SyncUI.d.ts.map +0 -1
  121. package/dist/ui/SyncUI.js.map +0 -1
  122. package/dist/ui/Welcome.d.ts.map +0 -1
  123. package/dist/ui/Welcome.js.map +0 -1
  124. package/dist/ui/theme.d.ts.map +0 -1
  125. package/dist/ui/theme.js.map +0 -1
  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,318 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest'
2
- import path from 'path'
3
-
4
- // ── Mock fs-extra ────────────────────────────────────────────────────────────
5
- vi.mock('fs-extra', () => {
6
- const mockFs = {
7
- pathExists: vi.fn(),
8
- readJson: vi.fn(),
9
- readFile: vi.fn(),
10
- copy: vi.fn(),
11
- ensureDir: vi.fn(),
12
- }
13
- return { default: mockFs, ...mockFs }
14
- })
15
-
16
- import fs from 'fs-extra'
17
- import { detectStack, backupIfExists, isGitRepo } from './common.js'
18
-
19
- const mockedFs = vi.mocked(fs)
20
-
21
- beforeEach(() => {
22
- vi.resetAllMocks()
23
- })
24
-
25
- // ═══════════════════════════════════════════════════════════════════════════════
26
- // detectStack
27
- // ═══════════════════════════════════════════════════════════════════════════════
28
- describe('detectStack', () => {
29
- function mockFileExists(existingFiles: string[]) {
30
- mockedFs.pathExists.mockImplementation(async (filePath: unknown) => {
31
- const p = String(filePath)
32
- return existingFiles.some(f => p.endsWith(f))
33
- })
34
- }
35
-
36
- it('detects build.gradle → java-gradle', async () => {
37
- mockFileExists(['build.gradle'])
38
- mockedFs.readFile.mockResolvedValue('sourceCompatibility = 21' as never)
39
- const result = await detectStack('/project')
40
- expect(result).not.toBeNull()
41
- expect(result!.stackType).toBe('java-gradle')
42
- expect(result!.buildTool).toBe('gradle')
43
- expect(result!.javaVersion).toBe('21')
44
- })
45
-
46
- it('detects build.gradle.kts → java-gradle', async () => {
47
- mockedFs.pathExists.mockImplementation(async (filePath: unknown) => {
48
- const p = String(filePath)
49
- if (p.endsWith('build.gradle')) return false
50
- if (p.endsWith('build.gradle.kts')) return true
51
- return false
52
- })
53
- mockedFs.readFile.mockResolvedValue('jvmTarget = "17"' as never)
54
- const result = await detectStack('/project')
55
- expect(result).not.toBeNull()
56
- expect(result!.stackType).toBe('java-gradle')
57
- expect(result!.javaVersion).toBe('17')
58
- })
59
-
60
- it('detects pom.xml → java-maven', async () => {
61
- mockedFs.pathExists.mockImplementation(async (filePath: unknown) => {
62
- const p = String(filePath)
63
- if (p.endsWith('build.gradle') || p.endsWith('build.gradle.kts')) return false
64
- if (p.endsWith('pom.xml')) return true
65
- return false
66
- })
67
- mockedFs.readFile.mockResolvedValue('<java.version>21</java.version>' as never)
68
- const result = await detectStack('/project')
69
- expect(result).not.toBeNull()
70
- expect(result!.stackType).toBe('java-maven')
71
- expect(result!.buildTool).toBe('maven')
72
- })
73
-
74
- it('detects package.json + pnpm-lock.yaml → node/pnpm', async () => {
75
- mockedFs.pathExists.mockImplementation(async (filePath: unknown) => {
76
- const p = String(filePath)
77
- if (p.endsWith('build.gradle') || p.endsWith('build.gradle.kts') || p.endsWith('pom.xml')) return false
78
- if (p.endsWith('package.json') || p.endsWith('pnpm-lock.yaml')) return true
79
- return false
80
- })
81
- mockedFs.readJson.mockResolvedValue({} as never)
82
- const result = await detectStack('/project')
83
- expect(result).not.toBeNull()
84
- expect(result!.stackType).toBe('node')
85
- expect(result!.buildTool).toBe('pnpm')
86
- })
87
-
88
- it('detects package.json + yarn.lock → node/yarn', async () => {
89
- mockedFs.pathExists.mockImplementation(async (filePath: unknown) => {
90
- const p = String(filePath)
91
- if (p.endsWith('build.gradle') || p.endsWith('build.gradle.kts') || p.endsWith('pom.xml')) return false
92
- if (p.endsWith('package.json') || p.endsWith('yarn.lock')) return true
93
- if (p.endsWith('pnpm-lock.yaml')) return false
94
- return false
95
- })
96
- mockedFs.readJson.mockResolvedValue({} as never)
97
- const result = await detectStack('/project')
98
- expect(result).not.toBeNull()
99
- expect(result!.stackType).toBe('node')
100
- expect(result!.buildTool).toBe('yarn')
101
- })
102
-
103
- it('detects package.json alone → node/npm', async () => {
104
- mockedFs.pathExists.mockImplementation(async (filePath: unknown) => {
105
- const p = String(filePath)
106
- if (p.endsWith('build.gradle') || p.endsWith('build.gradle.kts') || p.endsWith('pom.xml')) return false
107
- if (p.endsWith('package.json')) return true
108
- return false
109
- })
110
- mockedFs.readJson.mockResolvedValue({} as never)
111
- const result = await detectStack('/project')
112
- expect(result).not.toBeNull()
113
- expect(result!.stackType).toBe('node')
114
- expect(result!.buildTool).toBe('npm')
115
- })
116
-
117
- it('detects pyproject.toml → python/pyproject', async () => {
118
- mockedFs.pathExists.mockImplementation(async (filePath: unknown) => {
119
- const p = String(filePath)
120
- if (p.endsWith('build.gradle') || p.endsWith('build.gradle.kts') || p.endsWith('pom.xml') || p.endsWith('package.json')) return false
121
- if (p.endsWith('pyproject.toml')) return true
122
- return false
123
- })
124
- const result = await detectStack('/project')
125
- expect(result).not.toBeNull()
126
- expect(result!.stackType).toBe('python')
127
- expect(result!.buildTool).toBe('pyproject')
128
- })
129
-
130
- it('detects requirements.txt → python/pip', async () => {
131
- mockedFs.pathExists.mockImplementation(async (filePath: unknown) => {
132
- const p = String(filePath)
133
- if (p.endsWith('build.gradle') || p.endsWith('build.gradle.kts') || p.endsWith('pom.xml') || p.endsWith('package.json')) return false
134
- if (p.endsWith('pyproject.toml')) return false
135
- if (p.endsWith('requirements.txt')) return true
136
- if (p.endsWith('Pipfile')) return false
137
- return false
138
- })
139
- const result = await detectStack('/project')
140
- expect(result).not.toBeNull()
141
- expect(result!.stackType).toBe('python')
142
- expect(result!.buildTool).toBe('pip')
143
- })
144
-
145
- it('detects Pipfile → python/pipenv', async () => {
146
- mockedFs.pathExists.mockImplementation(async (filePath: unknown) => {
147
- const p = String(filePath)
148
- if (p.endsWith('build.gradle') || p.endsWith('build.gradle.kts') || p.endsWith('pom.xml') || p.endsWith('package.json')) return false
149
- if (p.endsWith('pyproject.toml')) return false
150
- if (p.endsWith('requirements.txt') || p.endsWith('setup.py')) return true
151
- if (p.endsWith('Pipfile')) return true
152
- return false
153
- })
154
- const result = await detectStack('/project')
155
- expect(result).not.toBeNull()
156
- expect(result!.stackType).toBe('python')
157
- expect(result!.buildTool).toBe('pipenv')
158
- })
159
-
160
- it('detects go.mod → go', async () => {
161
- mockedFs.pathExists.mockImplementation(async (filePath: unknown) => {
162
- const p = String(filePath)
163
- if (p.endsWith('go.mod')) return true
164
- return false
165
- })
166
- const result = await detectStack('/project')
167
- expect(result).not.toBeNull()
168
- expect(result!.stackType).toBe('go')
169
- expect(result!.buildTool).toBe('go')
170
- })
171
-
172
- it('detects Cargo.toml → rust', async () => {
173
- mockedFs.pathExists.mockImplementation(async (filePath: unknown) => {
174
- const p = String(filePath)
175
- if (p.endsWith('Cargo.toml')) return true
176
- return false
177
- })
178
- const result = await detectStack('/project')
179
- expect(result).not.toBeNull()
180
- expect(result!.stackType).toBe('rust')
181
- expect(result!.buildTool).toBe('cargo')
182
- })
183
-
184
- it('detects mix.exs → elixir', async () => {
185
- mockedFs.pathExists.mockImplementation(async (filePath: unknown) => {
186
- const p = String(filePath)
187
- if (p.endsWith('mix.exs')) return true
188
- return false
189
- })
190
- const result = await detectStack('/project')
191
- expect(result).not.toBeNull()
192
- expect(result!.stackType).toBe('elixir')
193
- expect(result!.buildTool).toBe('mix')
194
- })
195
-
196
- it('returns null for empty directory', async () => {
197
- mockedFs.pathExists.mockResolvedValue(false as never)
198
- const result = await detectStack('/empty-project')
199
- expect(result).toBeNull()
200
- })
201
-
202
- it('gives java-gradle precedence when both gradle + package.json exist', async () => {
203
- mockedFs.pathExists.mockImplementation(async (filePath: unknown) => {
204
- const p = String(filePath)
205
- if (p.endsWith('build.gradle') || p.endsWith('package.json')) return true
206
- return false
207
- })
208
- mockedFs.readFile.mockResolvedValue('sourceCompatibility = 21' as never)
209
- const result = await detectStack('/project')
210
- expect(result).not.toBeNull()
211
- expect(result!.stackType).toBe('java-gradle')
212
- })
213
-
214
- it('detects JavaVersion.VERSION_21 in build.gradle', async () => {
215
- mockedFs.pathExists.mockImplementation(async (filePath: unknown) => {
216
- const p = String(filePath)
217
- if (p.endsWith('build.gradle')) return true
218
- return false
219
- })
220
- mockedFs.readFile.mockResolvedValue('JavaVersion.VERSION_21' as never)
221
- const result = await detectStack('/project')
222
- expect(result!.javaVersion).toBe('21')
223
- })
224
-
225
- it('detects JavaVersion.VERSION_17 in build.gradle.kts', async () => {
226
- mockedFs.pathExists.mockImplementation(async (filePath: unknown) => {
227
- const p = String(filePath)
228
- if (p.endsWith('build.gradle')) return false
229
- if (p.endsWith('build.gradle.kts')) return true
230
- return false
231
- })
232
- mockedFs.readFile.mockResolvedValue('JavaVersion.VERSION_17' as never)
233
- const result = await detectStack('/project')
234
- expect(result!.javaVersion).toBe('17')
235
- })
236
-
237
- it('detects maven.compiler.source in pom.xml', async () => {
238
- mockedFs.pathExists.mockImplementation(async (filePath: unknown) => {
239
- const p = String(filePath)
240
- if (p.endsWith('build.gradle') || p.endsWith('build.gradle.kts')) return false
241
- if (p.endsWith('pom.xml')) return true
242
- return false
243
- })
244
- mockedFs.readFile.mockResolvedValue('<maven.compiler.source>17</maven.compiler.source>' as never)
245
- const result = await detectStack('/project')
246
- expect(result!.javaVersion).toBe('17')
247
- })
248
-
249
- it('returns undefined javaVersion when no version pattern matches', async () => {
250
- mockedFs.pathExists.mockImplementation(async (filePath: unknown) => {
251
- const p = String(filePath)
252
- if (p.endsWith('build.gradle')) return true
253
- return false
254
- })
255
- mockedFs.readFile.mockResolvedValue('apply plugin: java' as never)
256
- const result = await detectStack('/project')
257
- expect(result!.javaVersion).toBeUndefined()
258
- })
259
-
260
- it('handles readJson error for package.json gracefully', async () => {
261
- mockedFs.pathExists.mockImplementation(async (filePath: unknown) => {
262
- const p = String(filePath)
263
- if (p.endsWith('build.gradle') || p.endsWith('build.gradle.kts') || p.endsWith('pom.xml')) return false
264
- if (p.endsWith('package.json')) return true
265
- return false
266
- })
267
- mockedFs.readJson.mockRejectedValue(new Error('invalid json') as never)
268
- const result = await detectStack('/project')
269
- expect(result).not.toBeNull()
270
- expect(result!.stackType).toBe('node')
271
- })
272
- })
273
-
274
- // ═══════════════════════════════════════════════════════════════════════════════
275
- // backupIfExists
276
- // ═══════════════════════════════════════════════════════════════════════════════
277
- describe('backupIfExists', () => {
278
- it('creates .bak and returns true when file exists', async () => {
279
- mockedFs.pathExists.mockResolvedValue(true as never)
280
- mockedFs.copy.mockResolvedValue(undefined as never)
281
-
282
- const result = await backupIfExists('/project/ci.yml')
283
- expect(result).toBe(true)
284
- expect(mockedFs.copy).toHaveBeenCalledWith(
285
- '/project/ci.yml',
286
- '/project/ci.yml.bak',
287
- { overwrite: true }
288
- )
289
- })
290
-
291
- it('returns false when file does not exist', async () => {
292
- mockedFs.pathExists.mockResolvedValue(false as never)
293
-
294
- const result = await backupIfExists('/project/nonexistent.yml')
295
- expect(result).toBe(false)
296
- expect(mockedFs.copy).not.toHaveBeenCalled()
297
- })
298
- })
299
-
300
- // ═══════════════════════════════════════════════════════════════════════════════
301
- // isGitRepo
302
- // ═══════════════════════════════════════════════════════════════════════════════
303
- describe('isGitRepo', () => {
304
- it('returns true when .git exists', async () => {
305
- mockedFs.pathExists.mockResolvedValue(true as never)
306
- const result = await isGitRepo('/project')
307
- expect(result).toBe(true)
308
- expect(mockedFs.pathExists).toHaveBeenCalledWith(
309
- path.join('/project', '.git')
310
- )
311
- })
312
-
313
- it('returns false when .git does not exist', async () => {
314
- mockedFs.pathExists.mockResolvedValue(false as never)
315
- const result = await isGitRepo('/project')
316
- expect(result).toBe(false)
317
- })
318
- })
package/src/lib/common.ts DELETED
@@ -1,127 +0,0 @@
1
- import fs from 'fs-extra'
2
- import path from 'path'
3
- import type { Stack, StackDetection } from '../types/index.js'
4
-
5
- /**
6
- * Detect the project stack by looking for well-known build/config files.
7
- * Returns the first match (precedence order matters).
8
- */
9
- export async function detectStack(projectDir: string): Promise<StackDetection | null> {
10
- const exists = async (file: string) =>
11
- fs.pathExists(path.join(projectDir, file))
12
-
13
- // Java — Gradle
14
- if (await exists('build.gradle') || await exists('build.gradle.kts')) {
15
- const javaVersion = await detectJavaVersion(projectDir)
16
- return { stackType: 'java-gradle', buildTool: 'gradle', javaVersion }
17
- }
18
-
19
- // Java — Maven
20
- if (await exists('pom.xml')) {
21
- const javaVersion = await detectJavaVersion(projectDir)
22
- return { stackType: 'java-maven', buildTool: 'maven', javaVersion }
23
- }
24
-
25
- // Node.js
26
- if (await exists('package.json')) {
27
- const pkg = await fs.readJson(path.join(projectDir, 'package.json')).catch(() => ({}))
28
- const buildTool = await exists('pnpm-lock.yaml') ? 'pnpm'
29
- : await exists('yarn.lock') ? 'yarn'
30
- : 'npm'
31
- return { stackType: 'node', buildTool }
32
- }
33
-
34
- // Python
35
- if (await exists('pyproject.toml') || await exists('requirements.txt') || await exists('setup.py')) {
36
- const buildTool = await exists('pyproject.toml') ? 'pyproject'
37
- : await exists('Pipfile') ? 'pipenv'
38
- : 'pip'
39
- return { stackType: 'python', buildTool }
40
- }
41
-
42
- // Go
43
- if (await exists('go.mod')) {
44
- return { stackType: 'go', buildTool: 'go' }
45
- }
46
-
47
- // Rust
48
- if (await exists('Cargo.toml')) {
49
- return { stackType: 'rust', buildTool: 'cargo' }
50
- }
51
-
52
- // Elixir
53
- if (await exists('mix.exs')) {
54
- return { stackType: 'elixir', buildTool: 'mix' }
55
- }
56
-
57
- return null
58
- }
59
-
60
- /** Try to detect Java version from gradle or maven config */
61
- async function detectJavaVersion(projectDir: string): Promise<string | undefined> {
62
- // Try build.gradle
63
- const gradleFile = path.join(projectDir, 'build.gradle')
64
- if (await fs.pathExists(gradleFile)) {
65
- const content = await fs.readFile(gradleFile, 'utf-8')
66
- const match = content.match(/sourceCompatibility\s*=\s*['"]?(\d+)/)
67
- || content.match(/JavaVersion\.VERSION_(\d+)/)
68
- if (match) return match[1]
69
- }
70
-
71
- // Try build.gradle.kts
72
- const ktsFile = path.join(projectDir, 'build.gradle.kts')
73
- if (await fs.pathExists(ktsFile)) {
74
- const content = await fs.readFile(ktsFile, 'utf-8')
75
- const match = content.match(/jvmTarget\s*=\s*['"](\d+)['"]/)
76
- || content.match(/JavaVersion\.VERSION_(\d+)/)
77
- if (match) return match[1]
78
- }
79
-
80
- // Try pom.xml
81
- const pomFile = path.join(projectDir, 'pom.xml')
82
- if (await fs.pathExists(pomFile)) {
83
- const content = await fs.readFile(pomFile, 'utf-8')
84
- const match = content.match(/<java\.version>(\d+)</)
85
- || content.match(/<maven\.compiler\.source>(\d+)</)
86
- if (match) return match[1]
87
- }
88
-
89
- return undefined
90
- }
91
-
92
- /** Back up a file by copying to filePath.bak (only if it exists) */
93
- export async function backupIfExists(filePath: string): Promise<boolean> {
94
- if (await fs.pathExists(filePath)) {
95
- await fs.copy(filePath, `${filePath}.bak`, { overwrite: true })
96
- return true
97
- }
98
- return false
99
- }
100
-
101
- /** Create a directory recursively if it doesn't exist */
102
- export async function ensureDirExists(dirPath: string): Promise<void> {
103
- await fs.ensureDir(dirPath)
104
- }
105
-
106
- /** Check if a directory is a git repository */
107
- export async function isGitRepo(dir: string): Promise<boolean> {
108
- return fs.pathExists(path.join(dir, '.git'))
109
- }
110
-
111
- /** Resolve the forge assets root (the package root directory) */
112
- export function getForgeRoot(): string {
113
- // When running from dist/, go up one level
114
- const thisDir = path.dirname(new URL(import.meta.url).pathname)
115
- return path.resolve(thisDir, '..')
116
- }
117
-
118
- /** Stack display names */
119
- export const STACK_LABELS: Record<Stack, string> = {
120
- 'node': 'Node.js / TypeScript',
121
- 'python': 'Python',
122
- 'go': 'Go',
123
- 'rust': 'Rust',
124
- 'java-gradle': 'Java (Gradle)',
125
- 'java-maven': 'Java (Maven)',
126
- 'elixir': 'Elixir',
127
- }