@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
@@ -1,84 +0,0 @@
1
- /**
2
- * Auto-attribution: figure out which configured repo a proposed change
3
- * actually belongs to.
4
- *
5
- * Uses the SCM provider's `getFileContent` as a cheap remote existence check
6
- * — no clone needed. We only fall back to the model's `c.repo` for `create`
7
- * actions where the file doesn't yet exist anywhere. The result: only repos
8
- * that actually own at least one change end up in the validation plan, and
9
- * we never clone a repo that won't be validated.
10
- */
11
-
12
- import { createProvider } from "../providers/index.js";
13
-
14
- async function existsRemote(provider, file) {
15
- try {
16
- await provider.getFileContent(file);
17
- return true;
18
- } catch {
19
- return false;
20
- }
21
- }
22
-
23
- /**
24
- * Convenience wrapper: returns a flat list of changes with their `repo`
25
- * field rewritten to the auto-detected target. Used by callers (ForgeAgent's
26
- * apply/commit/deploy paths) that want a corrected change list rather than
27
- * the per-repo grouping the validator needs.
28
- *
29
- * @returns {Promise<{ changes: object[], unattributed: object[] }>}
30
- */
31
- export async function reattributeChanges(changes, candidates) {
32
- const { byRepo, unattributed } = await attributeChanges(changes, candidates);
33
- const out = [];
34
- for (const [repo, list] of byRepo) {
35
- for (const c of list) out.push({ ...c, repo });
36
- }
37
- return { changes: out, unattributed };
38
- }
39
-
40
- /**
41
- * @returns {Promise<{ byRepo: Map<string, object[]>, unattributed: object[] }>}
42
- */
43
- export async function attributeChanges(changes, candidates, emit = () => {}) {
44
- const providers = new Map();
45
- for (const repo of candidates) {
46
- try {
47
- providers.set(repo, createProvider(repo));
48
- } catch (err) {
49
- emit({ type: "log", message: `Couldn't build provider for ${repo}: ${err?.message ?? err}` });
50
- }
51
- }
52
-
53
- const byRepo = new Map();
54
- const unattributed = [];
55
-
56
- for (const c of changes ?? []) {
57
- let target = null;
58
-
59
- if (c.action === "modify" || c.action === "delete") {
60
- emit({ type: "log", message: `Locating ${c.file}…` });
61
- for (const [repo, provider] of providers) {
62
- if (await existsRemote(provider, c.file)) {
63
- target = repo;
64
- emit({ type: "log", message: ` → ${repo}` });
65
- break;
66
- }
67
- }
68
- } else if (c.action === "create") {
69
- // New file — no remote evidence to consult. Honour the model's
70
- // attribution if it points to a real candidate.
71
- if (c.repo && candidates.includes(c.repo)) target = c.repo;
72
- }
73
-
74
- if (target) {
75
- const list = byRepo.get(target) ?? [];
76
- list.push(c);
77
- byRepo.set(target, list);
78
- } else {
79
- unattributed.push(c);
80
- }
81
- }
82
-
83
- return { byRepo, unattributed };
84
- }
@@ -1,105 +0,0 @@
1
- /**
2
- * Cache + clone management for the validator's per-repo workspaces.
3
- *
4
- * ensureClone(repo, branchName, emit)
5
- * - Lazily clones into ~/.forge/cache/<repo>/ (or FORGE_VALIDATE_CACHE).
6
- * - Always fetch + reset to origin/main, then check out a stable per-issue
7
- * branch so validation runs are deterministic and old overlays can't
8
- * linger across runs.
9
- * - GitLab-only for v1; GitHub / Azure DevOps raise a clear error.
10
- * - Emits clone-step progress / log / step_result events.
11
- */
12
-
13
- import fs from "node:fs/promises";
14
- import os from "node:os";
15
- import path from "node:path";
16
-
17
- import { getConnection } from "../configStore.js";
18
- import { runCmd } from "./cmd.js";
19
- import { exists } from "./fs.js";
20
-
21
- const CACHE_ROOT = process.env.FORGE_VALIDATE_CACHE
22
- ?? path.join(os.homedir(), ".forge", "cache");
23
-
24
- function sanitizeRepo(repo) {
25
- return repo.replace(/[^a-zA-Z0-9_./-]/g, "_");
26
- }
27
-
28
- function gitlabCloneUrl(project) {
29
- return `https://gitlab.com/${project}.git`;
30
- }
31
-
32
- function gitEnvWithToken() {
33
- const { token } = getConnection("gitlab");
34
- if (!token) {
35
- throw new Error("GitLab token is required to clone the repo for validation. Set it in Settings → Connections or via GITLAB_TOKEN env var.");
36
- }
37
- return {
38
- GIT_ASKPASS: "echo",
39
- GIT_TERMINAL_PROMPT: "0",
40
- GIT_CONFIG_COUNT: "2",
41
- GIT_CONFIG_KEY_0: "credential.helper",
42
- GIT_CONFIG_VALUE_0: "",
43
- GIT_CONFIG_KEY_1: "http.extraHeader",
44
- GIT_CONFIG_VALUE_1: `Authorization: Bearer ${token}`,
45
- };
46
- }
47
-
48
- function resolveCloneUrl(repoInput) {
49
- // Repos may arrive prefixed — "github:owner/name", "azuredevops:org/proj/repo".
50
- // v1 only supports GitLab; tell the caller clearly when it's something else.
51
- const colon = repoInput.indexOf(":");
52
- if (colon > 0) {
53
- const prefix = repoInput.slice(0, colon).toLowerCase();
54
- if (prefix === "gitlab") return gitlabCloneUrl(repoInput.slice(colon + 1));
55
- if (prefix === "github" || prefix === "azuredevops" || prefix === "azure") {
56
- throw new Error(`Validation for ${prefix} repos isn't implemented yet — GitLab only for now.`);
57
- }
58
- }
59
- return gitlabCloneUrl(repoInput);
60
- }
61
-
62
- export async function ensureClone(repo, branchName, emit) {
63
- const dir = path.join(CACHE_ROOT, sanitizeRepo(repo));
64
- const hasRepo = await exists(path.join(dir, ".git"));
65
-
66
- emit({ type: "progress", step: "clone", status: "running" });
67
- emit({ type: "log", step: "clone", message: hasRepo ? `Reusing clone at ${dir}` : `Cloning ${repo} → ${dir}` });
68
-
69
- const gitEnv = gitEnvWithToken();
70
- if (!hasRepo) {
71
- await fs.mkdir(dir, { recursive: true });
72
- const clone = await runCmd(
73
- "git", ["clone", "--depth=1", resolveCloneUrl(repo), dir],
74
- { env: { ...process.env, ...gitEnv } },
75
- (l) => emit({ type: "log", step: "clone", message: l }),
76
- );
77
- if (clone.code !== 0) throw new Error(`git clone failed (code ${clone.code})`);
78
- }
79
-
80
- const fetch = await runCmd(
81
- "git", ["fetch", "--depth=1", "origin", "main"],
82
- { cwd: dir, env: { ...process.env, ...gitEnv } },
83
- (l) => emit({ type: "log", step: "clone", message: l }),
84
- );
85
- if (fetch.code !== 0) throw new Error(`git fetch failed (code ${fetch.code})`);
86
-
87
- // Two-step reset+clean before the branch switch so any lingering overlay
88
- // from a prior run is wiped — `git checkout -B` alone refuses if dirty.
89
- // `clean -fd` preserves node_modules because it's .gitignored.
90
- emit({ type: "log", step: "clone", message: `Resetting workspace to origin/main` });
91
- const reset = await runCmd("git", ["reset", "--hard", "origin/main"], { cwd: dir });
92
- if (reset.code !== 0) throw new Error(`git reset failed (code ${reset.code})`);
93
- await runCmd("git", ["clean", "-fd"], { cwd: dir });
94
-
95
- emit({ type: "log", step: "clone", message: `Checking out workspace branch ${branchName}` });
96
- const checkout = await runCmd(
97
- "git", ["checkout", "-B", branchName],
98
- { cwd: dir },
99
- (l) => emit({ type: "log", step: "clone", message: l }),
100
- );
101
- if (checkout.code !== 0) throw new Error(`git checkout ${branchName} failed (code ${checkout.code})`);
102
-
103
- emit({ type: "step_result", step: "clone", passed: true });
104
- return dir;
105
- }
@@ -1,98 +0,0 @@
1
- /**
2
- * Low-level child-process helpers shared by every runner.
3
- *
4
- * runCmd — spawn → resolve with { code, output, error }; line-buffers
5
- * stdout+stderr through `onLine` for live streaming.
6
- * runStep — runCmd + emits the standard progress / step_result events
7
- * the SSE consumer expects, returning a `{ passed, ... }` row.
8
- *
9
- * Children inherit a forced no-colour CI environment so eslint/tsc/jest/vitest
10
- * don't spam ANSI into the SSE stream.
11
- */
12
-
13
- import { spawn } from "node:child_process";
14
-
15
- export const MAX_TAIL_LINES = 60;
16
-
17
- // CSI (colour, cursor moves), OSC (titles), and simple ESC[ sequences.
18
- // eslint-disable-next-line no-control-regex
19
- const ANSI_RX = /\x1b\[[0-9;?]*[a-zA-Z]|\x1b\][^\x07]*\x07|\x1b[@-Z\\-_]/g;
20
-
21
- export function stripAnsi(s) {
22
- return s.replace(ANSI_RX, "");
23
- }
24
-
25
- export function runCmd(cmd, args, opts = {}, onLine) {
26
- return new Promise((resolve) => {
27
- const env = {
28
- ...process.env,
29
- ...(opts.env ?? {}),
30
- FORCE_COLOR: "0",
31
- NO_COLOR: "1",
32
- CI: "1",
33
- TERM: "dumb",
34
- };
35
- const proc = spawn(cmd, args, { ...opts, env, stdio: ["ignore", "pipe", "pipe"] });
36
- const lines = [];
37
- let outBuf = "";
38
- let errBuf = "";
39
-
40
- const flush = (buf, sink) => {
41
- let i;
42
- while ((i = buf.indexOf("\n")) !== -1) {
43
- const line = stripAnsi(buf.slice(0, i)).replace(/\r$/, "");
44
- lines.push(line);
45
- sink(line);
46
- buf = buf.slice(i + 1);
47
- }
48
- return buf;
49
- };
50
-
51
- proc.stdout.on("data", (d) => {
52
- outBuf += d.toString();
53
- outBuf = flush(outBuf, (l) => onLine?.(l));
54
- });
55
- proc.stderr.on("data", (d) => {
56
- errBuf += d.toString();
57
- errBuf = flush(errBuf, (l) => onLine?.(l));
58
- });
59
- proc.on("error", (err) => {
60
- resolve({ code: -1, output: lines.join("\n"), error: err.message });
61
- });
62
- proc.on("close", (code) => {
63
- if (outBuf) {
64
- const line = stripAnsi(outBuf).replace(/\r$/, "");
65
- lines.push(line);
66
- onLine?.(line);
67
- }
68
- if (errBuf) {
69
- const line = stripAnsi(errBuf).replace(/\r$/, "");
70
- lines.push(line);
71
- onLine?.(line);
72
- }
73
- resolve({ code: code ?? -1, output: lines.join("\n"), error: null });
74
- });
75
- });
76
- }
77
-
78
- export async function runStep(dir, stepName, cmd, args, emit) {
79
- emit({ type: "progress", step: stepName, status: "running" });
80
- const start = Date.now();
81
- const result = await runCmd(
82
- cmd, args,
83
- { cwd: dir },
84
- (l) => emit({ type: "log", step: stepName, message: l }),
85
- );
86
- const durationMs = Date.now() - start;
87
- const tail = result.output.split("\n").slice(-MAX_TAIL_LINES).join("\n");
88
- const passed = result.code === 0;
89
- emit({
90
- type: "step_result",
91
- step: stepName,
92
- passed,
93
- exitCode: result.code,
94
- durationMs,
95
- tail,
96
- });
97
- return { passed, exitCode: result.code, durationMs, tail };
98
- }
@@ -1,52 +0,0 @@
1
- /**
2
- * Overlay proposed changes onto the cloned working tree, and read formatter
3
- * rewrites back into the analysis so committed text matches what was validated.
4
- */
5
-
6
- import fs from "node:fs/promises";
7
- import path from "node:path";
8
-
9
- export async function overlayChanges(dir, changes, emit) {
10
- emit({ type: "progress", step: "overlay", status: "running" });
11
- const realBase = await fs.realpath(dir);
12
- for (const c of changes) {
13
- const full = path.resolve(dir, c.file);
14
- if (!full.startsWith(realBase + path.sep) && full !== realBase) {
15
- throw new Error(`Refusing to write outside clone dir: ${c.file}`);
16
- }
17
- if (c.action === "delete") {
18
- await fs.rm(full, { force: true });
19
- emit({ type: "log", step: "overlay", message: `- ${c.file}` });
20
- } else {
21
- await fs.mkdir(path.dirname(full), { recursive: true });
22
- await fs.writeFile(full, c.proposed);
23
- emit({ type: "log", step: "overlay", message: `+ ${c.file} (${c.action})` });
24
- }
25
- }
26
- emit({ type: "step_result", step: "overlay", passed: true });
27
- }
28
-
29
- /**
30
- * After a formatter step (npm run format, go fmt …), re-read the overlayed
31
- * files. If the formatter rewrote any of them, mutate `c.proposed` in place
32
- * and emit a `changes_formatted` event so the frontend can sync.
33
- */
34
- export async function readbackFormatted(dir, changes, emit) {
35
- const updated = [];
36
- for (const c of changes) {
37
- if (c.action === "delete") continue;
38
- try {
39
- const after = await fs.readFile(path.join(dir, c.file), "utf8");
40
- if (after !== c.proposed) {
41
- c.proposed = after;
42
- updated.push({ file: c.file, proposed: after });
43
- }
44
- } catch {
45
- // File may have been removed or renamed by the formatter
46
- // (uncommon for prettier/eslint --fix); skip silently.
47
- }
48
- }
49
- if (updated.length) {
50
- emit({ type: "changes_formatted", files: updated });
51
- }
52
- }
@@ -1,87 +0,0 @@
1
- /**
2
- * Per-repo project-type resolution.
3
- *
4
- * Resolution order (strongest → weakest signal):
5
- * 1. configStore — the user's explicit choice in Settings → Repositories.
6
- * Honoured verbatim when not "auto".
7
- * 2. Changeset extensions — what the user is actually editing. Safe to
8
- * trust as the primary signal because auto-attribution upstream
9
- * guarantees that only repos that actually own a change ever reach
10
- * this resolver. A polyglot repo with a root `package.json` and a
11
- * nested Go module still classifies as Go when the changeset is `.go`.
12
- * 3. Root marker — `go.mod` / `package.json` at the repo root.
13
- * 4. Shallow tree scan — last resort, walks one level to find a nested
14
- * marker (monorepo layouts).
15
- * 5. Fallback — Node.
16
- */
17
-
18
- import fs from "node:fs/promises";
19
- import path from "node:path";
20
-
21
- import * as configStore from "../configStore.js";
22
- import { exists } from "./fs.js";
23
-
24
- export const SUPPORTED_TYPES = ["node", "go"];
25
-
26
- const GO_EXTS = new Set([".go"]);
27
- const NODE_EXTS = new Set([".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs"]);
28
-
29
- function detectFromChanges(changes) {
30
- let go = 0;
31
- let node = 0;
32
- for (const c of changes ?? []) {
33
- const ext = path.extname(c.file ?? "").toLowerCase();
34
- if (GO_EXTS.has(ext)) go++;
35
- else if (NODE_EXTS.has(ext)) node++;
36
- }
37
- if (go > 0 && node === 0) return "go";
38
- if (node > 0 && go === 0) return "node";
39
- return null;
40
- }
41
-
42
- async function detectFromRoot(dir) {
43
- const hasGo = await exists(path.join(dir, "go.mod"));
44
- const hasNode = await exists(path.join(dir, "package.json"));
45
- if (hasGo && !hasNode) return "go";
46
- if (hasNode && !hasGo) return "node";
47
- if (hasGo && hasNode) return "go"; // both at root — prefer the more specific toolchain.
48
- return null;
49
- }
50
-
51
- async function detectFromShallowScan(dir) {
52
- let hasGo = false;
53
- let hasNode = false;
54
- try {
55
- const entries = await fs.readdir(dir, { withFileTypes: true });
56
- for (const entry of entries) {
57
- if (!entry.isDirectory()) continue;
58
- if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === "vendor") continue;
59
- const sub = path.join(dir, entry.name);
60
- if (!hasGo && (await exists(path.join(sub, "go.mod")))) hasGo = true;
61
- if (!hasNode && (await exists(path.join(sub, "package.json")))) hasNode = true;
62
- if (hasGo && hasNode) break;
63
- }
64
- } catch {
65
- // readdir failed — fall through with whatever we already have.
66
- }
67
- if (hasGo && !hasNode) return "go";
68
- if (hasNode && !hasGo) return "node";
69
- if (hasGo) return "go";
70
- return null;
71
- }
72
-
73
- export async function resolveProjectType(repoId, dir, changes) {
74
- const repos = configStore.get().repos ?? [];
75
- const cfg = repos.find((r) => r.id === repoId);
76
- const configured = cfg?.projectType;
77
- if (configured && configured !== "auto") return configured;
78
-
79
- // Auto-attribution already filtered out misattributed changes upstream,
80
- // so the changeset here genuinely reflects this repo's diff. Trust it.
81
- return (
82
- detectFromChanges(changes)
83
- ?? (await detectFromRoot(dir))
84
- ?? (await detectFromShallowScan(dir))
85
- ?? "node"
86
- );
87
- }
@@ -1,107 +0,0 @@
1
- /**
2
- * Go runner: emits Go-canonical step names so the UI shows what's actually
3
- * being run instead of forcing Node-isms onto the toolchain.
4
- *
5
- * fmt → go fmt ./... (rewrites .go files; readback feeds the diff)
6
- * vet → go vet ./... (replaced by golangci-lint when installed)
7
- * build → go build ./... (compile + type check)
8
- * test → go test -race -count=1 ./...
9
- *
10
- * Module location: `go` commands need to run inside the directory containing
11
- * the relevant go.mod. Monorepos commonly nest the module under e.g.
12
- * `services/api/go.mod`, so we walk up from a changed .go file (and shallow-
13
- * scan the clone) to find it. Multi-module repos with several go.mod files
14
- * are out of scope for v1 — we use whichever module the changeset hits first.
15
- */
16
-
17
- import fs from "node:fs/promises";
18
- import path from "node:path";
19
-
20
- import { runCmd, runStep } from "../cmd.js";
21
- import { exists } from "../fs.js";
22
- import { readbackFormatted } from "../overlay.js";
23
-
24
- // Public step contract for the orchestrator → frontend handshake.
25
- export const STEP_ORDER = ["clone", "install", "overlay", "fmt", "vet", "build", "test"];
26
- export const FIXABLE_STEPS = ["vet", "build", "test"];
27
-
28
- async function which(bin) {
29
- const r = await runCmd("which", [bin]);
30
- return r.code === 0;
31
- }
32
-
33
- async function findGoModDir(cloneDir, changes) {
34
- // Walk up from the first changed .go file looking for the nearest go.mod.
35
- const goChange = (changes ?? []).find((c) => path.extname(c.file ?? "").toLowerCase() === ".go");
36
- if (goChange) {
37
- let cur = path.dirname(path.join(cloneDir, goChange.file));
38
- while (cur.startsWith(cloneDir)) {
39
- if (await exists(path.join(cur, "go.mod"))) return cur;
40
- const parent = path.dirname(cur);
41
- if (parent === cur) break;
42
- cur = parent;
43
- }
44
- }
45
-
46
- if (await exists(path.join(cloneDir, "go.mod"))) return cloneDir;
47
-
48
- // Shallow scan for monorepos with no .go changes (e.g. an MR that only
49
- // touches a config file but the module is in a subdir).
50
- try {
51
- const entries = await fs.readdir(cloneDir, { withFileTypes: true });
52
- for (const entry of entries) {
53
- if (!entry.isDirectory()) continue;
54
- if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === "vendor") continue;
55
- const sub = path.join(cloneDir, entry.name);
56
- if (await exists(path.join(sub, "go.mod"))) return sub;
57
- }
58
- } catch {
59
- // fall through
60
- }
61
- return null;
62
- }
63
-
64
- async function ensureInstalled(goDir, emit) {
65
- emit({ type: "progress", step: "install", status: "running" });
66
- emit({ type: "log", step: "install", message: "Running go mod download…" });
67
- const r = await runCmd(
68
- "go", ["mod", "download"],
69
- { cwd: goDir },
70
- (l) => emit({ type: "log", step: "install", message: l }),
71
- );
72
- if (r.code !== 0) throw new Error(`go mod download failed (code ${r.code})`);
73
- emit({ type: "step_result", step: "install", passed: true });
74
- }
75
-
76
- export async function runGoRepo(cloneDir, changes, emit) {
77
- const goDir = await findGoModDir(cloneDir, changes);
78
- if (!goDir) {
79
- throw new Error("Couldn't locate a go.mod in the clone — repo doesn't look like a Go module.");
80
- }
81
- const rel = path.relative(cloneDir, goDir) || ".";
82
- if (rel !== ".") emit({ type: "log", step: "overlay", message: `Go module rooted at ${rel}` });
83
-
84
- await ensureInstalled(goDir, emit);
85
-
86
- const results = {};
87
-
88
- results.fmt = await runStep(goDir, "fmt", "go", ["fmt", "./..."], emit);
89
- if (results.fmt.passed) await readbackFormatted(cloneDir, changes, emit);
90
-
91
- // `go vet` is the baseline. If the repo (or developer) has golangci-lint
92
- // installed, prefer it — it covers gofmt, vet, staticcheck, errcheck, and
93
- // more in a single pass and is the de-facto community standard.
94
- if (await which("golangci-lint")) {
95
- results.vet = await runStep(goDir, "vet", "golangci-lint", ["run", "./..."], emit);
96
- } else {
97
- results.vet = await runStep(goDir, "vet", "go", ["vet", "./..."], emit);
98
- }
99
-
100
- results.build = await runStep(goDir, "build", "go", ["build", "./..."], emit);
101
-
102
- // -race catches data races; -count=1 disables the test cache so we always
103
- // re-execute against the overlayed source. Both are standard Go CI practice.
104
- results.test = await runStep(goDir, "test", "go", ["test", "-race", "-count=1", "./..."], emit);
105
-
106
- return results;
107
- }
@@ -1,84 +0,0 @@
1
- /**
2
- * Node runner: `npm ci` (lockfile-hash cached) followed by the standard
3
- * format / lint / typecheck / test scripts. Missing scripts are skipped, not
4
- * failed. The formatter step is read back so its rewrites flow into the
5
- * proposal the user commits.
6
- */
7
-
8
- import crypto from "node:crypto";
9
- import fs from "node:fs/promises";
10
- import path from "node:path";
11
-
12
- import { runCmd, runStep } from "../cmd.js";
13
- import { exists } from "../fs.js";
14
- import { readbackFormatted } from "../overlay.js";
15
- import { runTddPrecheckNode } from "../tdd.js";
16
-
17
- const SCRIPTS = ["format", "lint", "typecheck", "test"];
18
-
19
- // Public step contract for the orchestrator → frontend handshake. `tdd_precheck`
20
- // sits before `test`; it only actually runs (and appears in the UI) when the
21
- // proposal bundles new test files with impl changes — see runTddPrecheckNode.
22
- export const STEP_ORDER = ["clone", "install", "overlay", "format", "lint", "typecheck", "tdd_precheck", "test"];
23
- export const FIXABLE_STEPS = ["lint", "typecheck", "test"];
24
-
25
- async function readPackageScripts(dir) {
26
- try {
27
- const raw = await fs.readFile(path.join(dir, "package.json"), "utf8");
28
- return JSON.parse(raw).scripts ?? {};
29
- } catch {
30
- return {};
31
- }
32
- }
33
-
34
- async function ensureInstalled(dir, emit) {
35
- const lockPath = path.join(dir, "package-lock.json");
36
- if (!(await exists(lockPath))) return; // not an npm-managed project — omit the row entirely.
37
-
38
- const hashPath = path.join(dir, "node_modules", ".forge-lock-hash");
39
- const lockContent = await fs.readFile(lockPath);
40
- const lockHash = crypto.createHash("sha256").update(lockContent).digest("hex");
41
-
42
- if (await exists(hashPath)) {
43
- const stored = (await fs.readFile(hashPath, "utf8")).trim();
44
- if (stored === lockHash) return; // node_modules already matches; nothing to install.
45
- }
46
-
47
- emit({ type: "progress", step: "install", status: "running" });
48
- emit({ type: "log", step: "install", message: "Running npm ci (first run for this lockfile may take a minute)…" });
49
- const install = await runCmd(
50
- "npm", ["ci", "--prefer-offline", "--no-audit", "--no-fund"],
51
- { cwd: dir },
52
- (l) => emit({ type: "log", step: "install", message: l }),
53
- );
54
- if (install.code !== 0) {
55
- throw new Error(`npm ci failed (code ${install.code})`);
56
- }
57
- await fs.mkdir(path.dirname(hashPath), { recursive: true });
58
- await fs.writeFile(hashPath, lockHash);
59
- emit({ type: "step_result", step: "install", passed: true });
60
- }
61
-
62
- export async function runNodeRepo(dir, changes, emit) {
63
- await ensureInstalled(dir, emit);
64
-
65
- const scripts = await readPackageScripts(dir);
66
- const results = {};
67
- // Only run (and thus only render) scripts the project actually defines —
68
- // missing scripts are omitted from the UI rather than shown as "skipped".
69
- for (const s of SCRIPTS) {
70
- if (!scripts[s]) continue;
71
- // TDD precheck runs only when the `test` script exists, a new test file
72
- // is part of the proposal, and there's impl code to revert. It records
73
- // its own step_result under `tdd_precheck`.
74
- if (s === "test") {
75
- const precheck = await runTddPrecheckNode(dir, changes, emit);
76
- if (precheck) results.tdd_precheck = precheck;
77
- }
78
- results[s] = await runStep(dir, s, "npm", ["run", "--silent", s], emit);
79
- if (s === "format" && results[s].passed) {
80
- await readbackFormatted(dir, changes, emit);
81
- }
82
- }
83
- return results;
84
- }