@ipul-labs/forge 1.16.7 → 1.17.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 (280) hide show
  1. package/README.md +58 -21
  2. package/lib/bin/cli/analyze.d.ts +9 -0
  3. package/lib/bin/cli/analyze.d.ts.map +1 -0
  4. package/lib/bin/cli/analyze.js +491 -0
  5. package/lib/bin/cli/analyze.js.map +1 -0
  6. package/lib/bin/cli/apply.d.ts +2 -0
  7. package/lib/bin/cli/apply.d.ts.map +1 -0
  8. package/lib/bin/cli/apply.js +262 -0
  9. package/lib/bin/cli/apply.js.map +1 -0
  10. package/lib/bin/cli/cifix.d.ts +2 -0
  11. package/lib/bin/cli/cifix.d.ts.map +1 -0
  12. package/lib/bin/cli/cifix.js +310 -0
  13. package/lib/bin/cli/cifix.js.map +1 -0
  14. package/lib/bin/cli/close.d.ts +2 -0
  15. package/lib/bin/cli/close.d.ts.map +1 -0
  16. package/lib/bin/cli/close.js +113 -0
  17. package/lib/bin/cli/close.js.map +1 -0
  18. package/lib/bin/cli/createIssue.d.ts +2 -0
  19. package/lib/bin/cli/createIssue.d.ts.map +1 -0
  20. package/lib/bin/cli/createIssue.js +108 -0
  21. package/lib/bin/cli/createIssue.js.map +1 -0
  22. package/lib/bin/cli/doctor.d.ts +2 -0
  23. package/lib/bin/cli/doctor.d.ts.map +1 -0
  24. package/lib/bin/cli/doctor.js +145 -0
  25. package/lib/bin/cli/doctor.js.map +1 -0
  26. package/lib/bin/cli/init.d.ts +4 -0
  27. package/lib/bin/cli/init.d.ts.map +1 -0
  28. package/lib/bin/cli/init.js +288 -0
  29. package/lib/bin/cli/init.js.map +1 -0
  30. package/lib/bin/cli/ink.d.ts +23 -0
  31. package/lib/bin/cli/ink.d.ts.map +1 -0
  32. package/{bin → lib/bin}/cli/ink.js +18 -19
  33. package/lib/bin/cli/ink.js.map +1 -0
  34. package/lib/bin/cli/issue.d.ts +2 -0
  35. package/lib/bin/cli/issue.d.ts.map +1 -0
  36. package/lib/bin/cli/issue.js +114 -0
  37. package/lib/bin/cli/issue.js.map +1 -0
  38. package/lib/bin/cli/issues.d.ts +2 -0
  39. package/lib/bin/cli/issues.d.ts.map +1 -0
  40. package/lib/bin/cli/issues.js +149 -0
  41. package/lib/bin/cli/issues.js.map +1 -0
  42. package/lib/bin/cli/merge.d.ts +2 -0
  43. package/lib/bin/cli/merge.d.ts.map +1 -0
  44. package/lib/bin/cli/merge.js +162 -0
  45. package/lib/bin/cli/merge.js.map +1 -0
  46. package/lib/bin/cli/refine.d.ts +2 -0
  47. package/lib/bin/cli/refine.d.ts.map +1 -0
  48. package/lib/bin/cli/refine.js +249 -0
  49. package/lib/bin/cli/refine.js.map +1 -0
  50. package/lib/bin/cli/repl.d.ts +2 -0
  51. package/lib/bin/cli/repl.d.ts.map +1 -0
  52. package/lib/bin/cli/repl.js +309 -0
  53. package/lib/bin/cli/repl.js.map +1 -0
  54. package/lib/bin/cli/repos.d.ts +2 -0
  55. package/lib/bin/cli/repos.d.ts.map +1 -0
  56. package/lib/bin/cli/repos.js +86 -0
  57. package/lib/bin/cli/repos.js.map +1 -0
  58. package/lib/bin/cli/resolution.d.ts +2 -0
  59. package/lib/bin/cli/resolution.d.ts.map +1 -0
  60. package/lib/bin/cli/resolution.js +139 -0
  61. package/lib/bin/cli/resolution.js.map +1 -0
  62. package/lib/bin/cli/serverClient.d.ts +26 -0
  63. package/lib/bin/cli/serverClient.d.ts.map +1 -0
  64. package/lib/bin/cli/serverClient.js +111 -0
  65. package/lib/bin/cli/serverClient.js.map +1 -0
  66. package/lib/bin/cli/session.d.ts +8 -0
  67. package/lib/bin/cli/session.d.ts.map +1 -0
  68. package/lib/bin/cli/session.js +212 -0
  69. package/lib/bin/cli/session.js.map +1 -0
  70. package/lib/bin/cli/sessionStore.d.ts +69 -0
  71. package/lib/bin/cli/sessionStore.d.ts.map +1 -0
  72. package/lib/bin/cli/sessionStore.js +196 -0
  73. package/lib/bin/cli/sessionStore.js.map +1 -0
  74. package/lib/bin/cli/ship.d.ts +33 -0
  75. package/lib/bin/cli/ship.d.ts.map +1 -0
  76. package/lib/bin/cli/ship.js +737 -0
  77. package/lib/bin/cli/ship.js.map +1 -0
  78. package/lib/bin/cli/types.d.ts +59 -0
  79. package/lib/bin/cli/types.d.ts.map +1 -0
  80. package/lib/bin/cli/types.js +2 -0
  81. package/lib/bin/cli/types.js.map +1 -0
  82. package/lib/bin/cli/update.d.ts +2 -0
  83. package/lib/bin/cli/update.d.ts.map +1 -0
  84. package/lib/bin/cli/update.js +87 -0
  85. package/lib/bin/cli/update.js.map +1 -0
  86. package/lib/bin/cli/utils.d.ts +43 -0
  87. package/lib/bin/cli/utils.d.ts.map +1 -0
  88. package/lib/bin/cli/utils.js +171 -0
  89. package/lib/bin/cli/utils.js.map +1 -0
  90. package/lib/bin/cli/validate.d.ts +2 -0
  91. package/lib/bin/cli/validate.d.ts.map +1 -0
  92. package/lib/bin/cli/validate.js +283 -0
  93. package/lib/bin/cli/validate.js.map +1 -0
  94. package/lib/bin/cli/watch.d.ts +5 -0
  95. package/lib/bin/cli/watch.d.ts.map +1 -0
  96. package/lib/bin/cli/watch.js +263 -0
  97. package/lib/bin/cli/watch.js.map +1 -0
  98. package/lib/bin/forge-agent.d.ts +3 -0
  99. package/lib/bin/forge-agent.d.ts.map +1 -0
  100. package/{bin → lib/bin}/forge-agent.js +104 -105
  101. package/lib/bin/forge-agent.js.map +1 -0
  102. package/lib/server/forge/core/ForgeAgent.d.ts +260 -0
  103. package/lib/server/forge/core/ForgeAgent.d.ts.map +1 -0
  104. package/lib/server/forge/core/ForgeAgent.js +995 -0
  105. package/lib/server/forge/core/ForgeAgent.js.map +1 -0
  106. package/lib/server/forge/core/agentClient.d.ts +39 -0
  107. package/lib/server/forge/core/agentClient.d.ts.map +1 -0
  108. package/{server → lib/server}/forge/core/agentClient.js +7 -12
  109. package/lib/server/forge/core/agentClient.js.map +1 -0
  110. package/lib/server/forge/core/branchName.d.ts +20 -0
  111. package/lib/server/forge/core/branchName.d.ts.map +1 -0
  112. package/lib/server/forge/core/branchName.js +39 -0
  113. package/lib/server/forge/core/branchName.js.map +1 -0
  114. package/lib/server/forge/core/claudeClient.d.ts +60 -0
  115. package/lib/server/forge/core/claudeClient.d.ts.map +1 -0
  116. package/lib/server/forge/core/claudeClient.js +347 -0
  117. package/lib/server/forge/core/claudeClient.js.map +1 -0
  118. package/lib/server/forge/core/configStore.d.ts +76 -0
  119. package/lib/server/forge/core/configStore.d.ts.map +1 -0
  120. package/lib/server/forge/core/configStore.js +213 -0
  121. package/lib/server/forge/core/configStore.js.map +1 -0
  122. package/lib/server/forge/core/gitlabClient.d.ts +144 -0
  123. package/lib/server/forge/core/gitlabClient.d.ts.map +1 -0
  124. package/{server → lib/server}/forge/core/gitlabClient.js +98 -180
  125. package/lib/server/forge/core/gitlabClient.js.map +1 -0
  126. package/lib/server/forge/core/jobs.d.ts +67 -0
  127. package/lib/server/forge/core/jobs.d.ts.map +1 -0
  128. package/lib/server/forge/core/jobs.js +185 -0
  129. package/lib/server/forge/core/jobs.js.map +1 -0
  130. package/lib/server/forge/core/k8s.d.ts +18 -0
  131. package/lib/server/forge/core/k8s.d.ts.map +1 -0
  132. package/lib/server/forge/core/k8s.js +49 -0
  133. package/lib/server/forge/core/k8s.js.map +1 -0
  134. package/lib/server/forge/core/localRepo.d.ts +36 -0
  135. package/lib/server/forge/core/localRepo.d.ts.map +1 -0
  136. package/lib/server/forge/core/localRepo.js +165 -0
  137. package/lib/server/forge/core/localRepo.js.map +1 -0
  138. package/lib/server/forge/core/openclawClient.d.ts +61 -0
  139. package/lib/server/forge/core/openclawClient.d.ts.map +1 -0
  140. package/lib/server/forge/core/openclawClient.js +111 -0
  141. package/lib/server/forge/core/openclawClient.js.map +1 -0
  142. package/lib/server/forge/core/prompts.d.ts +96 -0
  143. package/lib/server/forge/core/prompts.d.ts.map +1 -0
  144. package/lib/server/forge/core/prompts.js +450 -0
  145. package/lib/server/forge/core/prompts.js.map +1 -0
  146. package/lib/server/forge/core/providers/azuredevops.d.ts +134 -0
  147. package/lib/server/forge/core/providers/azuredevops.d.ts.map +1 -0
  148. package/lib/server/forge/core/providers/azuredevops.js +360 -0
  149. package/lib/server/forge/core/providers/azuredevops.js.map +1 -0
  150. package/lib/server/forge/core/providers/base.d.ts +114 -0
  151. package/lib/server/forge/core/providers/base.d.ts.map +1 -0
  152. package/lib/server/forge/core/providers/base.js +98 -0
  153. package/lib/server/forge/core/providers/base.js.map +1 -0
  154. package/lib/server/forge/core/providers/github.d.ts +133 -0
  155. package/lib/server/forge/core/providers/github.d.ts.map +1 -0
  156. package/lib/server/forge/core/providers/github.js +307 -0
  157. package/lib/server/forge/core/providers/github.js.map +1 -0
  158. package/lib/server/forge/core/providers/gitlab.d.ts +144 -0
  159. package/lib/server/forge/core/providers/gitlab.d.ts.map +1 -0
  160. package/lib/server/forge/core/providers/gitlab.js +407 -0
  161. package/lib/server/forge/core/providers/gitlab.js.map +1 -0
  162. package/lib/server/forge/core/providers/index.d.ts +21 -0
  163. package/lib/server/forge/core/providers/index.d.ts.map +1 -0
  164. package/lib/server/forge/core/providers/index.js +43 -0
  165. package/lib/server/forge/core/providers/index.js.map +1 -0
  166. package/lib/server/forge/core/providers/types.d.ts +80 -0
  167. package/lib/server/forge/core/providers/types.d.ts.map +1 -0
  168. package/lib/server/forge/core/providers/types.js +2 -0
  169. package/lib/server/forge/core/providers/types.js.map +1 -0
  170. package/lib/server/forge/core/quotaClient.d.ts +48 -0
  171. package/lib/server/forge/core/quotaClient.d.ts.map +1 -0
  172. package/lib/server/forge/core/quotaClient.js +88 -0
  173. package/lib/server/forge/core/quotaClient.js.map +1 -0
  174. package/lib/server/forge/core/types.d.ts +160 -0
  175. package/lib/server/forge/core/types.d.ts.map +1 -0
  176. package/lib/server/forge/core/types.js +2 -0
  177. package/lib/server/forge/core/types.js.map +1 -0
  178. package/lib/server/forge/core/validator/attribute.d.ts +31 -0
  179. package/lib/server/forge/core/validator/attribute.d.ts.map +1 -0
  180. package/lib/server/forge/core/validator/attribute.js +82 -0
  181. package/lib/server/forge/core/validator/attribute.js.map +1 -0
  182. package/lib/server/forge/core/validator/clone.d.ts +14 -0
  183. package/lib/server/forge/core/validator/clone.d.ts.map +1 -0
  184. package/lib/server/forge/core/validator/clone.js +99 -0
  185. package/lib/server/forge/core/validator/clone.js.map +1 -0
  186. package/lib/server/forge/core/validator/cmd.d.ts +23 -0
  187. package/lib/server/forge/core/validator/cmd.d.ts.map +1 -0
  188. package/lib/server/forge/core/validator/cmd.js +87 -0
  189. package/lib/server/forge/core/validator/cmd.js.map +1 -0
  190. package/lib/server/forge/core/validator/fs.d.ts +5 -0
  191. package/lib/server/forge/core/validator/fs.d.ts.map +1 -0
  192. package/{server → lib/server}/forge/core/validator/fs.js +8 -3
  193. package/lib/server/forge/core/validator/fs.js.map +1 -0
  194. package/lib/server/forge/core/validator/overlay.d.ts +13 -0
  195. package/lib/server/forge/core/validator/overlay.d.ts.map +1 -0
  196. package/lib/server/forge/core/validator/overlay.js +53 -0
  197. package/lib/server/forge/core/validator/overlay.js.map +1 -0
  198. package/lib/server/forge/core/validator/projectType.d.ts +20 -0
  199. package/lib/server/forge/core/validator/projectType.d.ts.map +1 -0
  200. package/lib/server/forge/core/validator/projectType.js +94 -0
  201. package/lib/server/forge/core/validator/projectType.js.map +1 -0
  202. package/lib/server/forge/core/validator/runners/go.d.ts +21 -0
  203. package/lib/server/forge/core/validator/runners/go.d.ts.map +1 -0
  204. package/lib/server/forge/core/validator/runners/go.js +99 -0
  205. package/lib/server/forge/core/validator/runners/go.js.map +1 -0
  206. package/lib/server/forge/core/validator/runners/node.d.ts +12 -0
  207. package/lib/server/forge/core/validator/runners/node.d.ts.map +1 -0
  208. package/lib/server/forge/core/validator/runners/node.js +75 -0
  209. package/lib/server/forge/core/validator/runners/node.js.map +1 -0
  210. package/lib/server/forge/core/validator/tdd.d.ts +30 -0
  211. package/lib/server/forge/core/validator/tdd.d.ts.map +1 -0
  212. package/lib/server/forge/core/validator/tdd.js +96 -0
  213. package/lib/server/forge/core/validator/tdd.js.map +1 -0
  214. package/lib/server/forge/core/validator.d.ts +58 -0
  215. package/lib/server/forge/core/validator.d.ts.map +1 -0
  216. package/lib/server/forge/core/validator.js +150 -0
  217. package/lib/server/forge/core/validator.js.map +1 -0
  218. package/lib/server/forge/routes.d.ts +22 -0
  219. package/lib/server/forge/routes.d.ts.map +1 -0
  220. package/lib/server/forge/routes.js +984 -0
  221. package/lib/server/forge/routes.js.map +1 -0
  222. package/lib/server.d.ts +2 -0
  223. package/lib/server.d.ts.map +1 -0
  224. package/lib/server.js +52 -0
  225. package/lib/server.js.map +1 -0
  226. package/lib/shared/events.d.ts +12 -0
  227. package/lib/shared/events.d.ts.map +1 -0
  228. package/lib/shared/events.js +12 -0
  229. package/lib/shared/events.js.map +1 -0
  230. package/package.json +17 -13
  231. package/scripts/fix-shebang.cjs +10 -0
  232. package/bin/cli/analyze.js +0 -516
  233. package/bin/cli/apply.js +0 -264
  234. package/bin/cli/cifix.js +0 -300
  235. package/bin/cli/close.js +0 -125
  236. package/bin/cli/createIssue.js +0 -112
  237. package/bin/cli/doctor.js +0 -161
  238. package/bin/cli/init.js +0 -306
  239. package/bin/cli/issue.js +0 -116
  240. package/bin/cli/issues.js +0 -155
  241. package/bin/cli/merge.js +0 -175
  242. package/bin/cli/refine.js +0 -253
  243. package/bin/cli/repl.js +0 -301
  244. package/bin/cli/repos.js +0 -91
  245. package/bin/cli/resolution.js +0 -126
  246. package/bin/cli/session.js +0 -230
  247. package/bin/cli/sessionStore.js +0 -211
  248. package/bin/cli/ship.js +0 -762
  249. package/bin/cli/update.js +0 -95
  250. package/bin/cli/utils.js +0 -145
  251. package/bin/cli/validate.js +0 -280
  252. package/bin/cli/watch.js +0 -274
  253. package/server/forge/core/ForgeAgent.js +0 -1179
  254. package/server/forge/core/ForgeAgent.test.js +0 -179
  255. package/server/forge/core/branchName.js +0 -41
  256. package/server/forge/core/claudeClient.js +0 -378
  257. package/server/forge/core/configStore.js +0 -229
  258. package/server/forge/core/configStore.test.js +0 -89
  259. package/server/forge/core/jobs.js +0 -180
  260. package/server/forge/core/k8s.js +0 -54
  261. package/server/forge/core/localRepo.js +0 -153
  262. package/server/forge/core/openclawClient.js +0 -125
  263. package/server/forge/core/prompts.js +0 -483
  264. package/server/forge/core/prompts.test.js +0 -219
  265. package/server/forge/core/providers/azuredevops.js +0 -409
  266. package/server/forge/core/providers/base.js +0 -208
  267. package/server/forge/core/providers/github.js +0 -348
  268. package/server/forge/core/providers/gitlab.js +0 -529
  269. package/server/forge/core/providers/index.js +0 -47
  270. package/server/forge/core/quotaClient.js +0 -101
  271. package/server/forge/core/validator/attribute.js +0 -84
  272. package/server/forge/core/validator/clone.js +0 -105
  273. package/server/forge/core/validator/cmd.js +0 -98
  274. package/server/forge/core/validator/overlay.js +0 -52
  275. package/server/forge/core/validator/projectType.js +0 -87
  276. package/server/forge/core/validator/runners/go.js +0 -107
  277. package/server/forge/core/validator/runners/node.js +0 -84
  278. package/server/forge/core/validator/tdd.js +0 -105
  279. package/server/forge/core/validator.js +0 -167
  280. package/server/forge/routes.js +0 -1026
package/README.md CHANGED
@@ -252,11 +252,30 @@ FORGE_REPOS=gitlab:ipul/web-app,github:my-org/mobile
252
252
 
253
253
  ---
254
254
 
255
+ ## Security
256
+
257
+ Forge is designed for self-hosted deployment on trusted infrastructure. Security measures in place:
258
+
259
+ - **Timing-safe auth** — token comparison uses `crypto.timingSafeEqual` to prevent timing attacks
260
+ - **Rate limiting** — API endpoints are rate-limited (60 req/min per IP) to prevent brute-force
261
+ - **Security headers** — Helmet middleware sets X-Frame-Options, HSTS, X-Content-Type-Options, and CSP (production)
262
+ - **Request size limits** — JSON body capped at 1MB to prevent memory exhaustion
263
+ - **Secret redaction** — error logs and API responses scrub tokens (`glpat-*`, `ghp_*`, `oauth2:*@`) before output
264
+ - **File permissions** — config files (`~/.forge/config.json`) written with `0600`, session directories with `0700`
265
+ - **Git credential isolation** — SCM tokens passed via `GIT_ASKPASS` helper, never embedded in clone URLs
266
+ - **Path traversal protection** — symlink-aware `realpath` checks prevent file writes outside clone directories
267
+ - **No shell injection** — all subprocess calls use `spawn()` with argument arrays, never shell strings
268
+ - **Atomic config writes** — config updates use temp file + rename to prevent corruption
269
+
270
+ `FORGE_TOKEN` is a standalone dashboard password — never reuse an SCM token for this value.
271
+
272
+ ---
273
+
255
274
  ## Production deployment
256
275
 
257
276
  ```bash
258
277
  npm run build:app # builds Vite SPA to dist/
259
- NODE_ENV=production node server.js
278
+ NODE_ENV=production tsx server.ts
260
279
  ```
261
280
 
262
281
  Or with Docker:
@@ -270,31 +289,38 @@ docker run -p 5001:5001 --env-file .env forge
270
289
 
271
290
  ## Architecture
272
291
 
292
+ Full-stack TypeScript. The backend runs via `tsx` (no build step); the frontend compiles through Vite.
293
+
273
294
  ```
274
295
  bin/
275
- forge-agent.js # CLI entry point (forge command)
296
+ forge-agent.ts # CLI entry point (forge command)
276
297
  cli/
277
- ship.js # End-to-end orchestrator (Ink UI)
278
- analyze.js # Two-pass issue analysis
279
- resolution.js # Post-merge resolution check
280
- sessionStore.js # ~/.forge/sessions/ persistence
298
+ ship.ts # End-to-end orchestrator (Ink UI)
299
+ analyze.ts # Two-pass issue analysis
300
+ repl.ts # Interactive terminal (Ink-based)
301
+ serverClient.ts # CLI → Server API client (server/local mode)
302
+ sessionStore.ts # ~/.forge/sessions/ persistence
281
303
  ...
282
304
 
305
+ shared/
306
+ events.ts # FORGE_EVENTS constants (server + client)
307
+
283
308
  server/
284
309
  forge/
285
- routes.js # Express router — all /api/forge/* endpoints
310
+ routes.ts # Express router — all /api/forge/* endpoints
286
311
  core/
287
- ForgeAgent.js # Orchestrates the full pipeline (analyze -> apply -> refine)
288
- claudeClient.js # Shells out to the claude CLI for structured output
289
- openclawClient.js # OpenClaw Gateway HTTP client
290
- agentClient.js # Dispatcher: routes to claudeClient or openclawClient
291
- k8s.js # Kubernetes client for rollout monitoring
312
+ ForgeAgent.ts # Orchestrates the full pipeline (analyze -> apply -> refine)
313
+ claudeClient.ts # Shells out to the claude CLI for structured output
314
+ openclawClient.ts # OpenClaw Gateway HTTP client
315
+ agentClient.ts # Dispatcher: routes to claudeClient or openclawClient
316
+ k8s.ts # Kubernetes client for rollout monitoring
317
+ jobs.ts # Job store with SSE event buffering + replay
292
318
  providers/
293
- base.js # Abstract ScmProvider — normalized shapes + interface
294
- gitlab.js # GitLab REST API v4
295
- github.js # GitHub REST API v3 + Git Data API
296
- azuredevops.js # Azure DevOps REST API
297
- index.js # createProvider() factory
319
+ base.ts # Abstract ScmProvider — normalized shapes + interface
320
+ gitlab.ts # GitLab REST API v4
321
+ github.ts # GitHub REST API v3 + Git Data API
322
+ azuredevops.ts # Azure DevOps REST API
323
+ index.ts # createProvider() factory
298
324
 
299
325
  src/
300
326
  views/Forge/
@@ -305,6 +331,7 @@ src/
305
331
  components/ # Analysis progress, pipeline status, diff viewer
306
332
  hooks/
307
333
  useForgeDashboard.ts # All state, phase transitions, and API calls
334
+ useForgeEvents.ts # SSE event bus subscription
308
335
  ```
309
336
 
310
337
  ### Two-pass analysis
@@ -321,7 +348,7 @@ When a pipeline fails, Forge fetches the last 150 lines of each failed job's tra
321
348
 
322
349
  ### Provider abstraction
323
350
 
324
- All three SCM providers implement `ScmProvider` from `providers/base.js`. The contract is:
351
+ All three SCM providers implement `ScmProvider` from `providers/base.ts`. The contract is:
325
352
  - **Normalized return types** — all methods return `NormalIssue`, `NormalMR`, `NormalPipeline`, etc., regardless of provider
326
353
  - **Instance-scoped** — each provider is instantiated with a repo string; no global state
327
354
  - **`supportsRebase`** — providers that don't support server-side rebase return `false`; the UI and API layer check this before showing the rebase button
@@ -330,20 +357,30 @@ All three SCM providers implement `ScmProvider` from `providers/base.js`. The co
330
357
 
331
358
  ## Programmatic usage
332
359
 
333
- Mount the Forge Express router or use the core classes directly:
360
+ The npm package ships compiled JavaScript with TypeScript declarations — no `tsx` or TypeScript runtime needed by consumers.
334
361
 
335
- ```js
362
+ ```ts
336
363
  import forgeRouter from "@ipul-labs/forge"; // Express router
337
364
  import { ForgeAgent } from "@ipul-labs/forge/agent"; // Core pipeline class
338
365
  import { createProvider } from "@ipul-labs/forge/providers"; // SCM factory
339
366
  ```
340
367
 
341
- ```js
368
+ ```ts
342
369
  const agent = new ForgeAgent({ project: "group/repo" });
343
370
  const result = await agent.analyzeIssue(42, (event) => console.log(event));
344
371
  const { branchName, mr } = await agent.applyFix(result);
345
372
  ```
346
373
 
374
+ ### Development
375
+
376
+ Source is TypeScript (`.ts`). The dev server runs via `tsx` (no build step). For the npm package, `tsc` compiles to `lib/` with declarations during `prepack`.
377
+
378
+ ```bash
379
+ npm run dev # tsx watch + vite (dev)
380
+ npm run build:server # compile server/bin/shared → lib/
381
+ npm run typecheck # check both frontend + server configs
382
+ ```
383
+
347
384
  ---
348
385
 
349
386
  ## License
@@ -0,0 +1,9 @@
1
+ import type { AnalysisResult } from "../../server/forge/core/types.ts";
2
+ /**
3
+ * Render a cached analysis (read from session) through the same ResultBlock
4
+ * the live analyze view uses. No streaming, no progress — just the final
5
+ * payload. Used by `forge show`.
6
+ */
7
+ export declare function renderCachedResult(result: AnalysisResult): Promise<void>;
8
+ export declare function cmdAnalyze(argv: string[]): Promise<void>;
9
+ //# sourceMappingURL=analyze.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analyze.d.ts","sourceRoot":"","sources":["../../../bin/cli/analyze.ts"],"names":[],"mappings":"AA2BA,OAAO,KAAK,EAAE,cAAc,EAA2B,MAAM,kCAAkC,CAAC;AAsQhG;;;;GAIG;AACH,wBAAsB,kBAAkB,CAAC,MAAM,EAAE,cAAc,iBAM9D;AAiJD,wBAAsB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,iBAsH9C"}
@@ -0,0 +1,491 @@
1
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
2
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
3
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
4
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
5
+ });
6
+ }
7
+ return path;
8
+ };
9
+ import fs from "node:fs/promises";
10
+ import path from "node:path";
11
+ import { useEffect, useState } from "react";
12
+ import Spinner from "ink-spinner";
13
+ import { createPatch } from "diff";
14
+ import { h, Box, Text, useApp, runApp } from "./ink.js";
15
+ import { isServerAvailable, streamJob, SERVER_MODE, LOCAL_MODE, } from "./serverClient.js";
16
+ import { projectRoot, getFlag, notifyServer, resolveRepoSet, prefixDivergence, } from "./utils.js";
17
+ import { PHASES, appendEvent, setHead, updateState, writeAnalysis, } from "./sessionStore.js";
18
+ const DIFF_MAX_LINES = 60;
19
+ function StepRow({ step, message, status }) {
20
+ let glyph;
21
+ if (status === "running")
22
+ glyph = h `<${Text} color="cyan"><${Spinner} type="dots"/></>`;
23
+ else if (status === "done")
24
+ glyph = h `<${Text} color="green">✓</>`;
25
+ else
26
+ glyph = h `<${Text} color="red">✘</>`;
27
+ return h `<${Box} marginLeft=${2}>
28
+ ${glyph}
29
+ <${Text} dimColor> [${step}]</>
30
+ <${Text}> ${message}</>
31
+ </>`;
32
+ }
33
+ function ItemRow({ item }) {
34
+ if (item.kind === "step")
35
+ return h `<${StepRow} ...${item} />`;
36
+ if (item.kind === "log")
37
+ return h `<${Box} marginLeft=${4}><${Text} dimColor>${item.text}</></>`;
38
+ if (item.kind === "usage") {
39
+ const cost = typeof item.cost === "number" ? `$${item.cost.toFixed(4)}` : "$?";
40
+ return h `<${Box} marginLeft=${4}>
41
+ <${Text} dimColor>${item.inputTokens}in / ${item.outputTokens}out tokens, ${cost}</>
42
+ </>`;
43
+ }
44
+ if (item.kind === "error")
45
+ return h `<${Box} marginLeft=${2}><${Text} color="red">ERROR</><${Text}>: ${item.message}</></>`;
46
+ return null;
47
+ }
48
+ function colorForDiffLine(line) {
49
+ if (line.startsWith("+++") || line.startsWith("---"))
50
+ return "white";
51
+ if (line.startsWith("@@"))
52
+ return "cyan";
53
+ if (line.startsWith("+"))
54
+ return "green";
55
+ if (line.startsWith("-"))
56
+ return "red";
57
+ return undefined;
58
+ }
59
+ function DiffBlock({ original, proposed, file }) {
60
+ const patch = createPatch(file, original ?? "", proposed ?? "", "current", "proposed");
61
+ const lines = patch.split("\n").slice(2);
62
+ const truncated = lines.length > DIFF_MAX_LINES;
63
+ const shown = truncated ? lines.slice(0, DIFF_MAX_LINES) : lines;
64
+ return h `<${Box} flexDirection="column" marginLeft=${4}>
65
+ ${shown.map((line, i) => {
66
+ const color = colorForDiffLine(line);
67
+ return h `<${Text} key=${i} color=${color} dimColor=${color === undefined}>${line || " "}</>`;
68
+ })}
69
+ ${truncated
70
+ ? h `<${Text} dimColor>${` … ${lines.length - DIFF_MAX_LINES} more diff lines (use --json to see full payload)`}</>`
71
+ : null}
72
+ </>`;
73
+ }
74
+ function ChangeBlock({ change, index }) {
75
+ const isCreate = change.action === "create";
76
+ const isDelete = change.action === "delete";
77
+ const showDiff = !isDelete; // create: diff vs empty; delete: skip
78
+ const header = `${index + 1}. ${change.action.toUpperCase()} ${change.repo}:${change.file}`;
79
+ return h `<${Box} flexDirection="column" marginTop=${1} marginLeft=${2}>
80
+ <${Text} color="yellow">${header}</>
81
+ ${change.summary
82
+ ? h `<${Box} marginLeft=${3}><${Text} dimColor>${change.summary}</></>`
83
+ : null}
84
+ ${showDiff
85
+ ? h `<${DiffBlock} file=${change.file} original=${isCreate ? "" : change.original ?? ""} proposed=${change.proposed ?? ""} />`
86
+ : null}
87
+ </>`;
88
+ }
89
+ function DivergenceWarning({ divergence }) {
90
+ if (!divergence)
91
+ return null;
92
+ const text = `⚠ Issue prefix "${divergence.issue}:" but proposed MR prefix "${divergence.mr}:" — model may have misinterpreted the issue.`;
93
+ return h `<${Box} marginTop=${1}>
94
+ <${Text} color="yellow">${text}</>
95
+ </>`;
96
+ }
97
+ function ResultBlock({ result }) {
98
+ const changes = result.changes ?? [];
99
+ const issueTitle = result.issue?.title ?? "";
100
+ const divergence = prefixDivergence(issueTitle, result.mrTitle);
101
+ return h `<${Box} flexDirection="column" marginTop=${1} marginLeft=${2}>
102
+ <${Text} dimColor>${"─── result ───"}</>
103
+ <${Box} marginTop=${1}>
104
+ <${Text} color="yellow">Issue </>
105
+ <${Text}>#${result.issue?.iid}: ${issueTitle}</>
106
+ </>
107
+ ${result.mrTitle
108
+ ? h `<${Box}>
109
+ <${Text} color="yellow">MR </>
110
+ <${Text}>${result.mrTitle}</>
111
+ </>`
112
+ : null}
113
+ ${result.repos?.length
114
+ ? h `<${Box}>
115
+ <${Text} color="yellow">Repos </>
116
+ <${Text}>${result.repos.join(", ")}</>
117
+ </>`
118
+ : null}
119
+ <${DivergenceWarning} divergence=${divergence} />
120
+ ${result.explanation
121
+ ? h `<${Box} flexDirection="column" marginTop=${1}>
122
+ <${Text} dimColor>Explanation</>
123
+ <${Box} marginLeft=${2}><${Text}>${result.explanation}</></>
124
+ </>`
125
+ : null}
126
+ <${Box} marginTop=${1}>
127
+ <${Text} dimColor>${`Changes (${changes.length}):`}</>
128
+ </>
129
+ ${changes.length === 0
130
+ ? h `<${Box} marginLeft=${2}><${Text} dimColor>(none — model proposed no changes)</></>`
131
+ : changes.map((c, i) => h `<${ChangeBlock} key=${i} change=${c} index=${i} />`)}
132
+ </>`;
133
+ }
134
+ function AnalyzeView({ iid, primary, secondaries, model, agentKind, userPrompt, eventSink, onDone, }) {
135
+ const { exit } = useApp();
136
+ const [items, setItems] = useState([]);
137
+ const [mode, setMode] = useState(null);
138
+ const [result, setResult] = useState(null);
139
+ const [finished, setFinished] = useState(null);
140
+ useEffect(() => {
141
+ if (!finished)
142
+ return;
143
+ onDone(finished);
144
+ exit();
145
+ }, [finished]);
146
+ const handleEvent = (evt, cancelled) => {
147
+ if (cancelled?.current)
148
+ return;
149
+ eventSink?.(evt);
150
+ if (evt.type === "result")
151
+ return evt.data;
152
+ setItems((prev) => {
153
+ const closeRunning = (list) => list.map((it) => it.kind === "step" && it.status === "running"
154
+ ? { ...it, status: "done" }
155
+ : it);
156
+ switch (evt.type) {
157
+ case "progress":
158
+ return [
159
+ ...closeRunning(prev),
160
+ { kind: "step", step: evt.step, message: evt.message, status: "running" },
161
+ ];
162
+ case "log":
163
+ return [...prev, { kind: "log", text: evt.message }];
164
+ case "usage":
165
+ return [
166
+ ...prev,
167
+ { kind: "usage", inputTokens: evt.inputTokens, outputTokens: evt.outputTokens, cost: evt.cost },
168
+ ];
169
+ case "error":
170
+ return [
171
+ ...prev.map((it) => it.kind === "step" && it.status === "running" ? { ...it, status: "failed" } : it),
172
+ { kind: "error", message: evt.message },
173
+ ];
174
+ default:
175
+ return prev;
176
+ }
177
+ });
178
+ return null;
179
+ };
180
+ const finalize = (final) => {
181
+ setItems((prev) => prev.map((it) => it.kind === "step" && it.status === "running" ? { ...it, status: "done" } : it));
182
+ if (final)
183
+ setResult(final);
184
+ setFinished({ ok: true, result: final });
185
+ };
186
+ useEffect(() => {
187
+ const cancelRef = { current: false };
188
+ (async () => {
189
+ try {
190
+ const serverUp = await isServerAvailable();
191
+ setMode(serverUp ? SERVER_MODE : LOCAL_MODE);
192
+ let final = null;
193
+ if (serverUp) {
194
+ const { events } = streamJob(`/issues/${iid}/analyze`, {
195
+ repo: primary,
196
+ secondaryRepos: secondaries,
197
+ agent: agentKind,
198
+ model,
199
+ prompt: userPrompt,
200
+ kind: "fresh",
201
+ });
202
+ for await (const evt of events) {
203
+ if (cancelRef.current)
204
+ return;
205
+ const data = handleEvent(evt, cancelRef);
206
+ if (data)
207
+ final = data;
208
+ }
209
+ }
210
+ else {
211
+ const { ForgeAgent } = await import(__rewriteRelativeImportExtension(path.join(projectRoot, "server/forge/core/ForgeAgent.ts")));
212
+ const agent = new ForgeAgent({
213
+ project: primary,
214
+ secondaryProjects: secondaries,
215
+ agent: agentKind,
216
+ });
217
+ const { FORGE_EVENTS: FE } = await import("../../shared/events.js");
218
+ await notifyServer({ type: FE.ANALYSIS_STARTED, iid });
219
+ await agent.analyzeIssue(iid, (evt) => {
220
+ const data = handleEvent(evt, cancelRef);
221
+ if (data)
222
+ final = data;
223
+ }, model, undefined, userPrompt, "fresh");
224
+ }
225
+ if (cancelRef.current)
226
+ return;
227
+ finalize(final);
228
+ }
229
+ catch (err) {
230
+ if (cancelRef.current)
231
+ return;
232
+ const errObj = err instanceof Error ? err : new Error(String(err));
233
+ setItems((prev) => [
234
+ ...prev.map((it) => it.kind === "step" && it.status === "running" ? { ...it, status: "failed" } : it),
235
+ { kind: "error", message: errObj.message },
236
+ ]);
237
+ setFinished({ ok: false, error: errObj });
238
+ }
239
+ })();
240
+ return () => { cancelRef.current = true; };
241
+ }, []);
242
+ return h `<${Box} flexDirection="column">
243
+ ${mode ? h `<${Box} marginLeft=${2} marginBottom=${1}><${Text}>${mode}</></>` : null}
244
+ ${items.map((it, i) => h `<${ItemRow} key=${i} item=${it} />`)}
245
+ ${result ? h `<${ResultBlock} result=${result} />` : null}
246
+ </>`;
247
+ }
248
+ /**
249
+ * Render a cached analysis (read from session) through the same ResultBlock
250
+ * the live analyze view uses. No streaming, no progress — just the final
251
+ * payload. Used by `forge show`.
252
+ */
253
+ export async function renderCachedResult(result) {
254
+ await runApp(h `<${Box} flexDirection="column">
255
+ <${ResultBlock} result=${result} />
256
+ </>`);
257
+ }
258
+ async function persistAnalyzeOutcome({ repo, iid, primary, secondaries, result, events, error }) {
259
+ if (!repo || !iid)
260
+ return;
261
+ try {
262
+ if (result)
263
+ await writeAnalysis(repo, iid, result);
264
+ await updateState(repo, iid, {
265
+ repo,
266
+ iid: Number(iid),
267
+ primary,
268
+ secondaries,
269
+ phase: result ? PHASES.ANALYZED : "analyze-failed",
270
+ lastError: error?.message ?? null,
271
+ });
272
+ await appendEvent(repo, iid, { type: "session:analyze-start" });
273
+ for (const evt of events)
274
+ await appendEvent(repo, iid, evt);
275
+ await appendEvent(repo, iid, {
276
+ type: "session:analyze-end",
277
+ ok: !error,
278
+ error: error?.message,
279
+ });
280
+ await setHead(repo, iid);
281
+ if (result) {
282
+ const { FORGE_EVENTS } = await import("../../shared/events.js");
283
+ await notifyServer({ type: FORGE_EVENTS.ANALYSIS_DONE, iid: Number(iid) });
284
+ }
285
+ }
286
+ catch (err) {
287
+ process.stderr.write(`forge: failed to persist session: ${err instanceof Error ? err.message : String(err)}\n`);
288
+ }
289
+ }
290
+ /**
291
+ * Render a saved NDJSON event stream through the same Ink view, without
292
+ * re-running the agent. Useful for verifying renderer changes against a
293
+ * known-good payload, and for users who want to re-inspect a prior result.
294
+ */
295
+ async function cmdAnalyzeReplay(replayPath) {
296
+ const raw = await fs.readFile(replayPath, "utf8");
297
+ const events = raw
298
+ .split("\n")
299
+ .filter(Boolean)
300
+ .map((line) => {
301
+ try {
302
+ return JSON.parse(line);
303
+ }
304
+ catch {
305
+ return null;
306
+ }
307
+ })
308
+ .filter(Boolean);
309
+ let outcome = { ok: true };
310
+ await runApp(h `<${ReplayView} events=${events} onDone=${(o) => (outcome = o)} />`);
311
+ if (!outcome.ok)
312
+ process.exit(2);
313
+ }
314
+ function ReplayView({ events, onDone }) {
315
+ const { exit } = useApp();
316
+ const [items, setItems] = useState([]);
317
+ const [result, setResult] = useState(null);
318
+ const [finished, setFinished] = useState(null);
319
+ useEffect(() => {
320
+ if (!finished)
321
+ return;
322
+ onDone(finished);
323
+ exit();
324
+ }, [finished]);
325
+ useEffect(() => {
326
+ let final = null;
327
+ const next = [];
328
+ const closeRunning = (list) => list.map((it) => it.kind === "step" && it.status === "running" ? { ...it, status: "done" } : it);
329
+ for (const evt of events) {
330
+ if (evt.type === "result") {
331
+ final = evt.data ?? null;
332
+ continue;
333
+ }
334
+ switch (evt.type) {
335
+ case "progress": {
336
+ const closed = closeRunning(next);
337
+ next.length = 0;
338
+ next.push(...closed, {
339
+ kind: "step",
340
+ step: evt.step,
341
+ message: evt.message,
342
+ status: "running",
343
+ });
344
+ break;
345
+ }
346
+ case "log":
347
+ next.push({ kind: "log", text: evt.message });
348
+ break;
349
+ case "usage":
350
+ next.push({
351
+ kind: "usage",
352
+ inputTokens: evt.inputTokens,
353
+ outputTokens: evt.outputTokens,
354
+ cost: evt.cost,
355
+ });
356
+ break;
357
+ case "error": {
358
+ const closed = next.map((it) => it.kind === "step" && it.status === "running"
359
+ ? { ...it, status: "failed" }
360
+ : it);
361
+ next.length = 0;
362
+ next.push(...closed, { kind: "error", message: evt.message });
363
+ break;
364
+ }
365
+ default:
366
+ }
367
+ }
368
+ setItems(closeRunning(next));
369
+ if (final)
370
+ setResult(final);
371
+ setFinished({ ok: true });
372
+ }, []);
373
+ return h `<${Box} flexDirection="column">
374
+ ${items.map((it, i) => h `<${ItemRow} key=${i} item=${it} />`)}
375
+ ${result ? h `<${ResultBlock} result=${result} />` : null}
376
+ </>`;
377
+ }
378
+ export async function cmdAnalyze(argv) {
379
+ const replayPath = getFlag(argv, "--replay");
380
+ if (replayPath)
381
+ return cmdAnalyzeReplay(replayPath);
382
+ const iid = Number(argv[1]);
383
+ if (!Number.isInteger(iid) || iid <= 0) {
384
+ console.error("Usage: forge analyze <iid> [options]");
385
+ process.exit(1);
386
+ }
387
+ const { primary, secondaries } = await resolveRepoSet(argv);
388
+ if (!primary) {
389
+ console.error("No repo. Pass --repo <group/repo>, set FORGE_GITLAB_PROJECT, or run `forge init`.");
390
+ process.exit(1);
391
+ }
392
+ const model = getFlag(argv, "--model") || undefined;
393
+ const agentKind = getFlag(argv, "--agent") || "claude";
394
+ const userPrompt = getFlag(argv, "--prompt") || undefined;
395
+ const json = argv.includes("--json");
396
+ const quiet = argv.includes("--quiet");
397
+ const { ForgeAgent } = await import(__rewriteRelativeImportExtension(path.join(projectRoot, "server/forge/core/ForgeAgent.ts")));
398
+ if (json || quiet) {
399
+ const agent = new ForgeAgent({
400
+ project: primary,
401
+ secondaryProjects: secondaries,
402
+ agent: agentKind,
403
+ });
404
+ let finalResult = null;
405
+ const events = [];
406
+ const onEvent = (evt) => {
407
+ if (evt.type === "result")
408
+ finalResult = evt.data;
409
+ events.push(evt);
410
+ if (json)
411
+ process.stdout.write(`${JSON.stringify(evt)}\n`);
412
+ };
413
+ try {
414
+ const { FORGE_EVENTS: FE } = await import("../../shared/events.js");
415
+ await notifyServer({ type: FE.ANALYSIS_STARTED, iid });
416
+ await agent.analyzeIssue(iid, onEvent, model, undefined, userPrompt, "fresh");
417
+ }
418
+ catch (err) {
419
+ const errObj = err instanceof Error ? err : new Error(String(err));
420
+ if (json) {
421
+ process.stdout.write(`${JSON.stringify({ type: "error", message: errObj.message })}\n`);
422
+ }
423
+ else {
424
+ process.stderr.write(`ERROR: ${errObj.message}\n`);
425
+ }
426
+ await persistAnalyzeOutcome({
427
+ repo: primary,
428
+ iid,
429
+ primary,
430
+ secondaries,
431
+ result: null,
432
+ events,
433
+ error: errObj,
434
+ });
435
+ process.exit(2);
436
+ }
437
+ if (!finalResult) {
438
+ if (!json)
439
+ console.error("Analysis produced no result.");
440
+ await persistAnalyzeOutcome({
441
+ repo: primary,
442
+ iid,
443
+ primary,
444
+ secondaries,
445
+ result: null,
446
+ events,
447
+ error: new Error("no result"),
448
+ });
449
+ process.exit(2);
450
+ }
451
+ await persistAnalyzeOutcome({
452
+ repo: primary,
453
+ iid,
454
+ primary,
455
+ secondaries,
456
+ result: finalResult,
457
+ events,
458
+ error: null,
459
+ });
460
+ if (quiet && !json)
461
+ console.log(JSON.stringify(finalResult, null, 2));
462
+ return;
463
+ }
464
+ let outcome = { ok: true };
465
+ const events = [];
466
+ await runApp(h `<${AnalyzeView}
467
+ iid=${iid}
468
+ primary=${primary}
469
+ secondaries=${secondaries}
470
+ model=${model}
471
+ agentKind=${agentKind}
472
+ userPrompt=${userPrompt}
473
+ eventSink=${(e) => events.push(e)}
474
+ onDone=${(o) => (outcome = o)} />`);
475
+ await persistAnalyzeOutcome({
476
+ repo: primary,
477
+ iid,
478
+ primary,
479
+ secondaries,
480
+ result: outcome.result ?? null,
481
+ events,
482
+ error: outcome.error ?? null,
483
+ });
484
+ if (!outcome.ok)
485
+ process.exit(2);
486
+ if (!outcome.result) {
487
+ console.error("Analysis produced no result.");
488
+ process.exit(2);
489
+ }
490
+ }
491
+ //# sourceMappingURL=analyze.js.map