@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.
- package/README.md +58 -21
- package/lib/bin/cli/analyze.d.ts +9 -0
- package/lib/bin/cli/analyze.d.ts.map +1 -0
- package/lib/bin/cli/analyze.js +491 -0
- package/lib/bin/cli/analyze.js.map +1 -0
- package/lib/bin/cli/apply.d.ts +2 -0
- package/lib/bin/cli/apply.d.ts.map +1 -0
- package/lib/bin/cli/apply.js +262 -0
- package/lib/bin/cli/apply.js.map +1 -0
- package/lib/bin/cli/cifix.d.ts +2 -0
- package/lib/bin/cli/cifix.d.ts.map +1 -0
- package/lib/bin/cli/cifix.js +310 -0
- package/lib/bin/cli/cifix.js.map +1 -0
- package/lib/bin/cli/close.d.ts +2 -0
- package/lib/bin/cli/close.d.ts.map +1 -0
- package/lib/bin/cli/close.js +113 -0
- package/lib/bin/cli/close.js.map +1 -0
- package/lib/bin/cli/createIssue.d.ts +2 -0
- package/lib/bin/cli/createIssue.d.ts.map +1 -0
- package/lib/bin/cli/createIssue.js +108 -0
- package/lib/bin/cli/createIssue.js.map +1 -0
- package/lib/bin/cli/doctor.d.ts +2 -0
- package/lib/bin/cli/doctor.d.ts.map +1 -0
- package/lib/bin/cli/doctor.js +145 -0
- package/lib/bin/cli/doctor.js.map +1 -0
- package/lib/bin/cli/init.d.ts +4 -0
- package/lib/bin/cli/init.d.ts.map +1 -0
- package/lib/bin/cli/init.js +288 -0
- package/lib/bin/cli/init.js.map +1 -0
- package/lib/bin/cli/ink.d.ts +23 -0
- package/lib/bin/cli/ink.d.ts.map +1 -0
- package/{bin → lib/bin}/cli/ink.js +18 -19
- package/lib/bin/cli/ink.js.map +1 -0
- package/lib/bin/cli/issue.d.ts +2 -0
- package/lib/bin/cli/issue.d.ts.map +1 -0
- package/lib/bin/cli/issue.js +114 -0
- package/lib/bin/cli/issue.js.map +1 -0
- package/lib/bin/cli/issues.d.ts +2 -0
- package/lib/bin/cli/issues.d.ts.map +1 -0
- package/lib/bin/cli/issues.js +149 -0
- package/lib/bin/cli/issues.js.map +1 -0
- package/lib/bin/cli/merge.d.ts +2 -0
- package/lib/bin/cli/merge.d.ts.map +1 -0
- package/lib/bin/cli/merge.js +162 -0
- package/lib/bin/cli/merge.js.map +1 -0
- package/lib/bin/cli/refine.d.ts +2 -0
- package/lib/bin/cli/refine.d.ts.map +1 -0
- package/lib/bin/cli/refine.js +249 -0
- package/lib/bin/cli/refine.js.map +1 -0
- package/lib/bin/cli/repl.d.ts +2 -0
- package/lib/bin/cli/repl.d.ts.map +1 -0
- package/lib/bin/cli/repl.js +309 -0
- package/lib/bin/cli/repl.js.map +1 -0
- package/lib/bin/cli/repos.d.ts +2 -0
- package/lib/bin/cli/repos.d.ts.map +1 -0
- package/lib/bin/cli/repos.js +86 -0
- package/lib/bin/cli/repos.js.map +1 -0
- package/lib/bin/cli/resolution.d.ts +2 -0
- package/lib/bin/cli/resolution.d.ts.map +1 -0
- package/lib/bin/cli/resolution.js +139 -0
- package/lib/bin/cli/resolution.js.map +1 -0
- package/lib/bin/cli/serverClient.d.ts +26 -0
- package/lib/bin/cli/serverClient.d.ts.map +1 -0
- package/lib/bin/cli/serverClient.js +111 -0
- package/lib/bin/cli/serverClient.js.map +1 -0
- package/lib/bin/cli/session.d.ts +8 -0
- package/lib/bin/cli/session.d.ts.map +1 -0
- package/lib/bin/cli/session.js +212 -0
- package/lib/bin/cli/session.js.map +1 -0
- package/lib/bin/cli/sessionStore.d.ts +69 -0
- package/lib/bin/cli/sessionStore.d.ts.map +1 -0
- package/lib/bin/cli/sessionStore.js +196 -0
- package/lib/bin/cli/sessionStore.js.map +1 -0
- package/lib/bin/cli/ship.d.ts +33 -0
- package/lib/bin/cli/ship.d.ts.map +1 -0
- package/lib/bin/cli/ship.js +737 -0
- package/lib/bin/cli/ship.js.map +1 -0
- package/lib/bin/cli/types.d.ts +59 -0
- package/lib/bin/cli/types.d.ts.map +1 -0
- package/lib/bin/cli/types.js +2 -0
- package/lib/bin/cli/types.js.map +1 -0
- package/lib/bin/cli/update.d.ts +2 -0
- package/lib/bin/cli/update.d.ts.map +1 -0
- package/lib/bin/cli/update.js +87 -0
- package/lib/bin/cli/update.js.map +1 -0
- package/lib/bin/cli/utils.d.ts +43 -0
- package/lib/bin/cli/utils.d.ts.map +1 -0
- package/lib/bin/cli/utils.js +171 -0
- package/lib/bin/cli/utils.js.map +1 -0
- package/lib/bin/cli/validate.d.ts +2 -0
- package/lib/bin/cli/validate.d.ts.map +1 -0
- package/lib/bin/cli/validate.js +283 -0
- package/lib/bin/cli/validate.js.map +1 -0
- package/lib/bin/cli/watch.d.ts +5 -0
- package/lib/bin/cli/watch.d.ts.map +1 -0
- package/lib/bin/cli/watch.js +263 -0
- package/lib/bin/cli/watch.js.map +1 -0
- package/lib/bin/forge-agent.d.ts +3 -0
- package/lib/bin/forge-agent.d.ts.map +1 -0
- package/{bin → lib/bin}/forge-agent.js +104 -105
- package/lib/bin/forge-agent.js.map +1 -0
- package/lib/server/forge/core/ForgeAgent.d.ts +260 -0
- package/lib/server/forge/core/ForgeAgent.d.ts.map +1 -0
- package/lib/server/forge/core/ForgeAgent.js +995 -0
- package/lib/server/forge/core/ForgeAgent.js.map +1 -0
- package/lib/server/forge/core/agentClient.d.ts +39 -0
- package/lib/server/forge/core/agentClient.d.ts.map +1 -0
- package/{server → lib/server}/forge/core/agentClient.js +7 -12
- package/lib/server/forge/core/agentClient.js.map +1 -0
- package/lib/server/forge/core/branchName.d.ts +20 -0
- package/lib/server/forge/core/branchName.d.ts.map +1 -0
- package/lib/server/forge/core/branchName.js +39 -0
- package/lib/server/forge/core/branchName.js.map +1 -0
- package/lib/server/forge/core/claudeClient.d.ts +60 -0
- package/lib/server/forge/core/claudeClient.d.ts.map +1 -0
- package/lib/server/forge/core/claudeClient.js +347 -0
- package/lib/server/forge/core/claudeClient.js.map +1 -0
- package/lib/server/forge/core/configStore.d.ts +76 -0
- package/lib/server/forge/core/configStore.d.ts.map +1 -0
- package/lib/server/forge/core/configStore.js +213 -0
- package/lib/server/forge/core/configStore.js.map +1 -0
- package/lib/server/forge/core/gitlabClient.d.ts +144 -0
- package/lib/server/forge/core/gitlabClient.d.ts.map +1 -0
- package/{server → lib/server}/forge/core/gitlabClient.js +98 -180
- package/lib/server/forge/core/gitlabClient.js.map +1 -0
- package/lib/server/forge/core/jobs.d.ts +67 -0
- package/lib/server/forge/core/jobs.d.ts.map +1 -0
- package/lib/server/forge/core/jobs.js +185 -0
- package/lib/server/forge/core/jobs.js.map +1 -0
- package/lib/server/forge/core/k8s.d.ts +18 -0
- package/lib/server/forge/core/k8s.d.ts.map +1 -0
- package/lib/server/forge/core/k8s.js +49 -0
- package/lib/server/forge/core/k8s.js.map +1 -0
- package/lib/server/forge/core/localRepo.d.ts +36 -0
- package/lib/server/forge/core/localRepo.d.ts.map +1 -0
- package/lib/server/forge/core/localRepo.js +165 -0
- package/lib/server/forge/core/localRepo.js.map +1 -0
- package/lib/server/forge/core/openclawClient.d.ts +61 -0
- package/lib/server/forge/core/openclawClient.d.ts.map +1 -0
- package/lib/server/forge/core/openclawClient.js +111 -0
- package/lib/server/forge/core/openclawClient.js.map +1 -0
- package/lib/server/forge/core/prompts.d.ts +96 -0
- package/lib/server/forge/core/prompts.d.ts.map +1 -0
- package/lib/server/forge/core/prompts.js +450 -0
- package/lib/server/forge/core/prompts.js.map +1 -0
- package/lib/server/forge/core/providers/azuredevops.d.ts +134 -0
- package/lib/server/forge/core/providers/azuredevops.d.ts.map +1 -0
- package/lib/server/forge/core/providers/azuredevops.js +360 -0
- package/lib/server/forge/core/providers/azuredevops.js.map +1 -0
- package/lib/server/forge/core/providers/base.d.ts +114 -0
- package/lib/server/forge/core/providers/base.d.ts.map +1 -0
- package/lib/server/forge/core/providers/base.js +98 -0
- package/lib/server/forge/core/providers/base.js.map +1 -0
- package/lib/server/forge/core/providers/github.d.ts +133 -0
- package/lib/server/forge/core/providers/github.d.ts.map +1 -0
- package/lib/server/forge/core/providers/github.js +307 -0
- package/lib/server/forge/core/providers/github.js.map +1 -0
- package/lib/server/forge/core/providers/gitlab.d.ts +144 -0
- package/lib/server/forge/core/providers/gitlab.d.ts.map +1 -0
- package/lib/server/forge/core/providers/gitlab.js +407 -0
- package/lib/server/forge/core/providers/gitlab.js.map +1 -0
- package/lib/server/forge/core/providers/index.d.ts +21 -0
- package/lib/server/forge/core/providers/index.d.ts.map +1 -0
- package/lib/server/forge/core/providers/index.js +43 -0
- package/lib/server/forge/core/providers/index.js.map +1 -0
- package/lib/server/forge/core/providers/types.d.ts +80 -0
- package/lib/server/forge/core/providers/types.d.ts.map +1 -0
- package/lib/server/forge/core/providers/types.js +2 -0
- package/lib/server/forge/core/providers/types.js.map +1 -0
- package/lib/server/forge/core/quotaClient.d.ts +48 -0
- package/lib/server/forge/core/quotaClient.d.ts.map +1 -0
- package/lib/server/forge/core/quotaClient.js +88 -0
- package/lib/server/forge/core/quotaClient.js.map +1 -0
- package/lib/server/forge/core/types.d.ts +160 -0
- package/lib/server/forge/core/types.d.ts.map +1 -0
- package/lib/server/forge/core/types.js +2 -0
- package/lib/server/forge/core/types.js.map +1 -0
- package/lib/server/forge/core/validator/attribute.d.ts +31 -0
- package/lib/server/forge/core/validator/attribute.d.ts.map +1 -0
- package/lib/server/forge/core/validator/attribute.js +82 -0
- package/lib/server/forge/core/validator/attribute.js.map +1 -0
- package/lib/server/forge/core/validator/clone.d.ts +14 -0
- package/lib/server/forge/core/validator/clone.d.ts.map +1 -0
- package/lib/server/forge/core/validator/clone.js +99 -0
- package/lib/server/forge/core/validator/clone.js.map +1 -0
- package/lib/server/forge/core/validator/cmd.d.ts +23 -0
- package/lib/server/forge/core/validator/cmd.d.ts.map +1 -0
- package/lib/server/forge/core/validator/cmd.js +87 -0
- package/lib/server/forge/core/validator/cmd.js.map +1 -0
- package/lib/server/forge/core/validator/fs.d.ts +5 -0
- package/lib/server/forge/core/validator/fs.d.ts.map +1 -0
- package/{server → lib/server}/forge/core/validator/fs.js +8 -3
- package/lib/server/forge/core/validator/fs.js.map +1 -0
- package/lib/server/forge/core/validator/overlay.d.ts +13 -0
- package/lib/server/forge/core/validator/overlay.d.ts.map +1 -0
- package/lib/server/forge/core/validator/overlay.js +53 -0
- package/lib/server/forge/core/validator/overlay.js.map +1 -0
- package/lib/server/forge/core/validator/projectType.d.ts +20 -0
- package/lib/server/forge/core/validator/projectType.d.ts.map +1 -0
- package/lib/server/forge/core/validator/projectType.js +94 -0
- package/lib/server/forge/core/validator/projectType.js.map +1 -0
- package/lib/server/forge/core/validator/runners/go.d.ts +21 -0
- package/lib/server/forge/core/validator/runners/go.d.ts.map +1 -0
- package/lib/server/forge/core/validator/runners/go.js +99 -0
- package/lib/server/forge/core/validator/runners/go.js.map +1 -0
- package/lib/server/forge/core/validator/runners/node.d.ts +12 -0
- package/lib/server/forge/core/validator/runners/node.d.ts.map +1 -0
- package/lib/server/forge/core/validator/runners/node.js +75 -0
- package/lib/server/forge/core/validator/runners/node.js.map +1 -0
- package/lib/server/forge/core/validator/tdd.d.ts +30 -0
- package/lib/server/forge/core/validator/tdd.d.ts.map +1 -0
- package/lib/server/forge/core/validator/tdd.js +96 -0
- package/lib/server/forge/core/validator/tdd.js.map +1 -0
- package/lib/server/forge/core/validator.d.ts +58 -0
- package/lib/server/forge/core/validator.d.ts.map +1 -0
- package/lib/server/forge/core/validator.js +150 -0
- package/lib/server/forge/core/validator.js.map +1 -0
- package/lib/server/forge/routes.d.ts +22 -0
- package/lib/server/forge/routes.d.ts.map +1 -0
- package/lib/server/forge/routes.js +984 -0
- package/lib/server/forge/routes.js.map +1 -0
- package/lib/server.d.ts +2 -0
- package/lib/server.d.ts.map +1 -0
- package/lib/server.js +52 -0
- package/lib/server.js.map +1 -0
- package/lib/shared/events.d.ts +12 -0
- package/lib/shared/events.d.ts.map +1 -0
- package/lib/shared/events.js +12 -0
- package/lib/shared/events.js.map +1 -0
- package/package.json +17 -13
- package/scripts/fix-shebang.cjs +10 -0
- package/bin/cli/analyze.js +0 -516
- package/bin/cli/apply.js +0 -264
- package/bin/cli/cifix.js +0 -300
- package/bin/cli/close.js +0 -125
- package/bin/cli/createIssue.js +0 -112
- package/bin/cli/doctor.js +0 -161
- package/bin/cli/init.js +0 -306
- package/bin/cli/issue.js +0 -116
- package/bin/cli/issues.js +0 -155
- package/bin/cli/merge.js +0 -175
- package/bin/cli/refine.js +0 -253
- package/bin/cli/repl.js +0 -301
- package/bin/cli/repos.js +0 -91
- package/bin/cli/resolution.js +0 -126
- package/bin/cli/session.js +0 -230
- package/bin/cli/sessionStore.js +0 -211
- package/bin/cli/ship.js +0 -762
- package/bin/cli/update.js +0 -95
- package/bin/cli/utils.js +0 -145
- package/bin/cli/validate.js +0 -280
- package/bin/cli/watch.js +0 -274
- package/server/forge/core/ForgeAgent.js +0 -1179
- package/server/forge/core/ForgeAgent.test.js +0 -179
- package/server/forge/core/branchName.js +0 -41
- package/server/forge/core/claudeClient.js +0 -378
- package/server/forge/core/configStore.js +0 -229
- package/server/forge/core/configStore.test.js +0 -89
- package/server/forge/core/jobs.js +0 -180
- package/server/forge/core/k8s.js +0 -54
- package/server/forge/core/localRepo.js +0 -153
- package/server/forge/core/openclawClient.js +0 -125
- package/server/forge/core/prompts.js +0 -483
- package/server/forge/core/prompts.test.js +0 -219
- package/server/forge/core/providers/azuredevops.js +0 -409
- package/server/forge/core/providers/base.js +0 -208
- package/server/forge/core/providers/github.js +0 -348
- package/server/forge/core/providers/gitlab.js +0 -529
- package/server/forge/core/providers/index.js +0 -47
- package/server/forge/core/quotaClient.js +0 -101
- package/server/forge/core/validator/attribute.js +0 -84
- package/server/forge/core/validator/clone.js +0 -105
- package/server/forge/core/validator/cmd.js +0 -98
- package/server/forge/core/validator/overlay.js +0 -52
- package/server/forge/core/validator/projectType.js +0 -87
- package/server/forge/core/validator/runners/go.js +0 -107
- package/server/forge/core/validator/runners/node.js +0 -84
- package/server/forge/core/validator/tdd.js +0 -105
- package/server/forge/core/validator.js +0 -167
- 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
|
|
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.
|
|
296
|
+
forge-agent.ts # CLI entry point (forge command)
|
|
276
297
|
cli/
|
|
277
|
-
ship.
|
|
278
|
-
analyze.
|
|
279
|
-
|
|
280
|
-
|
|
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.
|
|
310
|
+
routes.ts # Express router — all /api/forge/* endpoints
|
|
286
311
|
core/
|
|
287
|
-
ForgeAgent.
|
|
288
|
-
claudeClient.
|
|
289
|
-
openclawClient.
|
|
290
|
-
agentClient.
|
|
291
|
-
k8s.
|
|
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.
|
|
294
|
-
gitlab.
|
|
295
|
-
github.
|
|
296
|
-
azuredevops.
|
|
297
|
-
index.
|
|
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.
|
|
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
|
-
|
|
360
|
+
The npm package ships compiled JavaScript with TypeScript declarations — no `tsx` or TypeScript runtime needed by consumers.
|
|
334
361
|
|
|
335
|
-
```
|
|
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
|
-
```
|
|
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
|