@ipul-labs/forge 1.13.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 (113) hide show
  1. package/README.md +273 -0
  2. package/bin/README.md +210 -0
  3. package/bin/cli/analyze.js +516 -0
  4. package/bin/cli/apply.js +264 -0
  5. package/bin/cli/cifix.js +300 -0
  6. package/bin/cli/close.js +125 -0
  7. package/bin/cli/doctor.js +161 -0
  8. package/bin/cli/init.js +306 -0
  9. package/bin/cli/ink.js +57 -0
  10. package/bin/cli/issue.js +116 -0
  11. package/bin/cli/issues.js +155 -0
  12. package/bin/cli/merge.js +175 -0
  13. package/bin/cli/refine.js +253 -0
  14. package/bin/cli/repos.js +91 -0
  15. package/bin/cli/resolution.js +126 -0
  16. package/bin/cli/session.js +230 -0
  17. package/bin/cli/sessionStore.js +214 -0
  18. package/bin/cli/ship.js +762 -0
  19. package/bin/cli/utils.js +145 -0
  20. package/bin/cli/validate.js +280 -0
  21. package/bin/cli/watch.js +274 -0
  22. package/bin/forge-agent.js +174 -0
  23. package/dist/assets/index-CNTCMbW9.js +80 -0
  24. package/dist/assets/index-InpYS-zg.css +1 -0
  25. package/dist/favicon.svg +8 -0
  26. package/dist/index.html +19 -0
  27. package/dist-lib/favicon.svg +8 -0
  28. package/dist-lib/index.d.ts +1 -0
  29. package/dist-lib/style.css +1 -0
  30. package/dist-lib/ui.d.ts +2 -0
  31. package/dist-lib/ui.js +7390 -0
  32. package/dist-lib/views/Forge/ForgeDashboard.d.ts +5 -0
  33. package/dist-lib/views/Forge/activeJobs.d.ts +29 -0
  34. package/dist-lib/views/Forge/analysisCache.d.ts +64 -0
  35. package/dist-lib/views/Forge/api.d.ts +4 -0
  36. package/dist-lib/views/Forge/components/AnalysisProgress.d.ts +13 -0
  37. package/dist-lib/views/Forge/components/CITerminal.d.ts +44 -0
  38. package/dist-lib/views/Forge/components/DiffViewer.d.ts +37 -0
  39. package/dist-lib/views/Forge/components/ForgeConnect.d.ts +6 -0
  40. package/dist-lib/views/Forge/components/IssueList.d.ts +28 -0
  41. package/dist-lib/views/Forge/components/MRStatus.d.ts +9 -0
  42. package/dist-lib/views/Forge/components/NewTaskModal.d.ts +1 -0
  43. package/dist-lib/views/Forge/components/OpenMRList.d.ts +11 -0
  44. package/dist-lib/views/Forge/components/PipelineStatus.d.ts +12 -0
  45. package/dist-lib/views/Forge/components/PipelinesList.d.ts +9 -0
  46. package/dist-lib/views/Forge/components/RolloutStatus.d.ts +5 -0
  47. package/dist-lib/views/Forge/components/UsageWidget.d.ts +8 -0
  48. package/dist-lib/views/Forge/components/ValidationPanel.d.ts +22 -0
  49. package/dist-lib/views/Forge/context/ForgeProvider.d.ts +23 -0
  50. package/dist-lib/views/Forge/hooks/useConfig.d.ts +54 -0
  51. package/dist-lib/views/Forge/hooks/useForgeDashboard.d.ts +107 -0
  52. package/dist-lib/views/Forge/hooks/useForgeQuota.d.ts +22 -0
  53. package/dist-lib/views/Forge/phases/AnalyzingView.d.ts +1 -0
  54. package/dist-lib/views/Forge/phases/IdleView.d.ts +1 -0
  55. package/dist-lib/views/Forge/phases/MonitoringIssueTab.d.ts +1 -0
  56. package/dist-lib/views/Forge/phases/MonitoringPipelineTab.d.ts +1 -0
  57. package/dist-lib/views/Forge/phases/MonitoringView.d.ts +1 -0
  58. package/dist-lib/views/Forge/phases/ReviewingView.d.ts +1 -0
  59. package/dist-lib/views/Forge/phases/SelectedIssueTab.d.ts +11 -0
  60. package/dist-lib/views/Forge/phases/SelectedMRTab.d.ts +1 -0
  61. package/dist-lib/views/Forge/phases/SelectedView.d.ts +1 -0
  62. package/dist-lib/views/Forge/phases/shared/CloseIssueConfirm.d.ts +1 -0
  63. package/dist-lib/views/Forge/phases/shared/DetailTabs.d.ts +14 -0
  64. package/dist-lib/views/Forge/phases/shared/EnvironmentsPanel.d.ts +9 -0
  65. package/dist-lib/views/Forge/phases/shared/ForgeActivity.d.ts +1 -0
  66. package/dist-lib/views/Forge/phases/shared/IssueHeaderBlock.d.ts +13 -0
  67. package/dist-lib/views/Forge/phases/shared/MergedMRList.d.ts +4 -0
  68. package/dist-lib/views/Forge/phases/shared/PreviousAnalyses.d.ts +1 -0
  69. package/dist-lib/views/Forge/phases/shared/ResolutionCheckPanel.d.ts +6 -0
  70. package/dist-lib/views/Forge/settings/ConnectionRow.d.ts +16 -0
  71. package/dist-lib/views/Forge/settings/ConnectionsSection.d.ts +7 -0
  72. package/dist-lib/views/Forge/settings/DefaultsSection.d.ts +7 -0
  73. package/dist-lib/views/Forge/settings/ReposSection.d.ts +7 -0
  74. package/dist-lib/views/Forge/settings/SettingsView.d.ts +1 -0
  75. package/dist-lib/views/Forge/shell/ConfigBar.d.ts +1 -0
  76. package/dist-lib/views/Forge/shell/ForgeRouter.d.ts +9 -0
  77. package/dist-lib/views/Forge/shell/ForgeShell.d.ts +7 -0
  78. package/dist-lib/views/Forge/shell/Sidebar.d.ts +1 -0
  79. package/dist-lib/views/Forge/shell/SidebarFooterMenu.d.ts +1 -0
  80. package/dist-lib/views/Forge/shell/SidebarNav.d.ts +1 -0
  81. package/dist-lib/views/Forge/shell/SidebarNavItem.d.ts +10 -0
  82. package/dist-lib/views/Forge/shell/TopBar.d.ts +5 -0
  83. package/dist-lib/views/Forge/types.d.ts +235 -0
  84. package/dist-lib/views/Forge/utils.d.ts +11 -0
  85. package/package.json +88 -0
  86. package/server/forge/core/ForgeAgent.js +1144 -0
  87. package/server/forge/core/agentClient.js +36 -0
  88. package/server/forge/core/branchName.js +41 -0
  89. package/server/forge/core/claudeClient.js +356 -0
  90. package/server/forge/core/configStore.js +214 -0
  91. package/server/forge/core/gitlabClient.js +340 -0
  92. package/server/forge/core/jobs.js +180 -0
  93. package/server/forge/core/k8s.js +54 -0
  94. package/server/forge/core/localRepo.js +136 -0
  95. package/server/forge/core/openclawClient.js +125 -0
  96. package/server/forge/core/prompts.js +369 -0
  97. package/server/forge/core/providers/azuredevops.js +408 -0
  98. package/server/forge/core/providers/base.js +208 -0
  99. package/server/forge/core/providers/github.js +349 -0
  100. package/server/forge/core/providers/gitlab.js +529 -0
  101. package/server/forge/core/providers/index.js +47 -0
  102. package/server/forge/core/quotaClient.js +101 -0
  103. package/server/forge/core/validator/attribute.js +84 -0
  104. package/server/forge/core/validator/clone.js +91 -0
  105. package/server/forge/core/validator/cmd.js +98 -0
  106. package/server/forge/core/validator/fs.js +9 -0
  107. package/server/forge/core/validator/overlay.js +52 -0
  108. package/server/forge/core/validator/projectType.js +87 -0
  109. package/server/forge/core/validator/runners/go.js +107 -0
  110. package/server/forge/core/validator/runners/node.js +84 -0
  111. package/server/forge/core/validator/tdd.js +105 -0
  112. package/server/forge/core/validator.js +166 -0
  113. package/server/forge/routes.js +1012 -0
@@ -0,0 +1,349 @@
1
+ /**
2
+ * server/forge/core/providers/github.js
3
+ *
4
+ * GitHub REST API v3 implementation of ScmProvider.
5
+ * Multi-file commits use the Git Data API (blob → tree → commit → ref).
6
+ *
7
+ * Required env vars:
8
+ * GITHUB_TOKEN — personal access token (repo + workflow scopes)
9
+ * GITHUB_OWNER — default org or user, e.g. "ipul-labs"
10
+ * GITHUB_REPO — default repository name, e.g. "web-app"
11
+ *
12
+ * Optional:
13
+ * GITHUB_BASE_BRANCH — default target branch (default: "main")
14
+ */
15
+
16
+ import { getConnection } from "../configStore.js";
17
+ import { ScmProvider } from "./base.js";
18
+
19
+ export class GitHubProvider extends ScmProvider {
20
+ /**
21
+ * @param {string} [repo] "owner/repo" format — overrides env vars
22
+ */
23
+ constructor(repo) {
24
+ super();
25
+ if (repo && repo.includes("/")) {
26
+ const [owner, repoName] = repo.split("/");
27
+ this._owner = owner;
28
+ this._repo = repoName;
29
+ } else {
30
+ this._owner = process.env.GITHUB_OWNER ?? "";
31
+ this._repo = repo ?? process.env.GITHUB_REPO ?? "";
32
+ }
33
+ this._base = "https://api.github.com";
34
+ this._defaultBranch = process.env.GITHUB_BASE_BRANCH ?? "main";
35
+ }
36
+
37
+ get name() { return "github"; }
38
+ get supportsRebase() { return false; }
39
+
40
+ // ─── Internal helpers ──────────────────────────────────────────────────────
41
+
42
+ _headers() {
43
+ return {
44
+ Authorization: `Bearer ${getConnection("github").token}`,
45
+ Accept: "application/vnd.github+json",
46
+ "X-GitHub-Api-Version": "2022-11-28",
47
+ "Content-Type": "application/json",
48
+ };
49
+ }
50
+
51
+ _repoBase() {
52
+ return `${this._base}/repos/${this._owner}/${this._repo}`;
53
+ }
54
+
55
+ async _req(path, options = {}) {
56
+ const url = path.startsWith("http") ? path : `${this._repoBase()}${path}`;
57
+ const res = await fetch(url, {
58
+ ...options,
59
+ headers: { ...this._headers(), ...options.headers },
60
+ });
61
+ if (res.status === 204) return null;
62
+ if (!res.ok) {
63
+ const text = await res.text().catch(() => "");
64
+ throw new Error(`GitHub ${res.status} on ${path}: ${text}`);
65
+ }
66
+ return res.json();
67
+ }
68
+
69
+ // ─── Normalizers ──────────────────────────────────────────────────────────
70
+
71
+ /** GitHub issues map closely to NormalIssue */
72
+ _issue(raw) {
73
+ return {
74
+ iid: raw.number,
75
+ title: raw.title,
76
+ description: raw.body ?? null,
77
+ labels: (raw.labels ?? []).map((l) => l.name),
78
+ author: { name: raw.user?.login ?? "" },
79
+ created_at: raw.created_at,
80
+ web_url: raw.html_url,
81
+ };
82
+ }
83
+
84
+ /** GitHub PRs → NormalMR. GitHub has no server-side rebase API. */
85
+ _mr(raw) {
86
+ return {
87
+ iid: raw.number,
88
+ title: raw.title,
89
+ web_url: raw.html_url,
90
+ source_branch: raw.head?.ref ?? "",
91
+ target_branch: raw.base?.ref ?? "",
92
+ state: raw.merged_at ? "merged" : raw.state,
93
+ has_conflicts: raw.mergeable === false,
94
+ rebase_in_progress: false,
95
+ merge_status: raw.mergeable_state ?? "",
96
+ detailed_merge_status: null,
97
+ draft: raw.draft ?? false,
98
+ head_pipeline: raw.head?.sha
99
+ ? null // populated separately via getCheckRuns if needed
100
+ : null,
101
+ author: { name: raw.user?.login ?? "" },
102
+ created_at: raw.created_at,
103
+ merged_at: raw.merged_at ?? null,
104
+ };
105
+ }
106
+
107
+ /** GitHub Actions workflow runs → NormalPipeline */
108
+ _pipeline(raw) {
109
+ return {
110
+ id: raw.id,
111
+ status: this._runStatus(raw),
112
+ web_url: raw.html_url,
113
+ created_at: raw.created_at,
114
+ };
115
+ }
116
+
117
+ _runStatus(run) {
118
+ if (run.conclusion) return run.conclusion; // success / failure / cancelled
119
+ if (run.status === "in_progress") return "running";
120
+ return run.status ?? "unknown";
121
+ }
122
+
123
+ /** GitHub Actions jobs → NormalJob */
124
+ _job(raw) {
125
+ return {
126
+ id: raw.id,
127
+ name: raw.name,
128
+ stage: raw.workflow_name ?? "",
129
+ status: raw.conclusion ?? raw.status ?? "unknown",
130
+ web_url: raw.html_url,
131
+ duration: raw.completed_at && raw.started_at
132
+ ? Math.round((new Date(raw.completed_at) - new Date(raw.started_at)) / 1000)
133
+ : null,
134
+ };
135
+ }
136
+
137
+ _diff(raw) {
138
+ return {
139
+ old_path: raw.previous_filename ?? raw.filename,
140
+ new_path: raw.filename,
141
+ diff: raw.patch ?? "",
142
+ new_file: raw.status === "added",
143
+ renamed_file: raw.status === "renamed",
144
+ deleted_file: raw.status === "removed",
145
+ };
146
+ }
147
+
148
+ // ─── Issues ───────────────────────────────────────────────────────────────
149
+
150
+ async listOpenIssues() {
151
+ const raw = await this._req(
152
+ `/issues?state=open&sort=updated&per_page=25&pulls=false`,
153
+ );
154
+ // GitHub returns PRs as issues; filter them out
155
+ return (raw ?? []).filter((i) => !i.pull_request).map((i) => this._issue(i));
156
+ }
157
+
158
+ async getIssue(iid) {
159
+ const raw = await this._req(`/issues/${iid}`);
160
+ return this._issue(raw);
161
+ }
162
+
163
+ async createIssue(title, description = "") {
164
+ const raw = await this._req("/issues", {
165
+ method: "POST",
166
+ body: JSON.stringify({ title, body: description }),
167
+ });
168
+ return this._issue(raw);
169
+ }
170
+
171
+ async updateIssue(iid, { title, description }) {
172
+ const raw = await this._req(`/issues/${iid}`, {
173
+ method: "PATCH",
174
+ body: JSON.stringify({ title, body: description }),
175
+ });
176
+ return this._issue(raw);
177
+ }
178
+
179
+ async closeIssue(iid) {
180
+ const raw = await this._req(`/issues/${iid}`, {
181
+ method: "PATCH",
182
+ body: JSON.stringify({ state: "closed" }),
183
+ });
184
+ return this._issue(raw);
185
+ }
186
+
187
+ // ─── Repository ───────────────────────────────────────────────────────────
188
+
189
+ async getFileTree(srcPath = "src", ref = this._defaultBranch) {
190
+ // Use recursive tree API — much faster than walking
191
+ const data = await this._req(`/git/trees/${ref}?recursive=1`);
192
+ return (data.tree ?? [])
193
+ .filter((item) => item.type === "blob")
194
+ .filter((item) => item.path.startsWith(srcPath + "/") || item.path === srcPath)
195
+ .filter((item) => /\.(ts|tsx|js|jsx|json|css|yaml|yml|md)$/.test(item.path))
196
+ .map((item) => item.path);
197
+ }
198
+
199
+ async getFileContent(filePath, ref = this._defaultBranch) {
200
+ const encoded = encodeURIComponent(filePath);
201
+ const data = await this._req(`/contents/${encoded}?ref=${ref}`);
202
+ return Buffer.from(data.content, "base64").toString("utf-8");
203
+ }
204
+
205
+ // ─── Mutations ────────────────────────────────────────────────────────────
206
+
207
+ async createBranch(branchName, ref = this._defaultBranch) {
208
+ // Resolve ref to SHA
209
+ const refData = await this._req(`/git/refs/heads/${ref}`);
210
+ const sha = refData.object.sha;
211
+ return this._req("/git/refs", {
212
+ method: "POST",
213
+ body: JSON.stringify({ ref: `refs/heads/${branchName}`, sha }),
214
+ });
215
+ }
216
+
217
+ /**
218
+ * Multi-file commit using GitHub Git Data API.
219
+ * @param {string} branch
220
+ * @param {string} message
221
+ * @param {import('./base.js').CommitAction[]} actions
222
+ */
223
+ async commitFiles(branch, message, actions) {
224
+ // 1. Get current HEAD
225
+ const refData = await this._req(`/git/refs/heads/${branch}`);
226
+ const headSha = refData.object.sha;
227
+
228
+ // 2. Get base tree SHA from commit
229
+ const commitData = await this._req(`/git/commits/${headSha}`);
230
+ const baseTreeSha = commitData.tree.sha;
231
+
232
+ // 3. Create blobs for each file (skip deletes)
233
+ const treeEntries = await Promise.all(
234
+ actions.map(async (action) => {
235
+ if (action.action === "delete") {
236
+ return { path: action.file_path, mode: "100644", type: "blob", sha: null };
237
+ }
238
+ const blob = await this._req("/git/blobs", {
239
+ method: "POST",
240
+ body: JSON.stringify({ content: action.content, encoding: "utf-8" }),
241
+ });
242
+ return { path: action.file_path, mode: "100644", type: "blob", sha: blob.sha };
243
+ }),
244
+ );
245
+
246
+ // 4. Create new tree
247
+ const tree = await this._req("/git/trees", {
248
+ method: "POST",
249
+ body: JSON.stringify({ base_tree: baseTreeSha, tree: treeEntries }),
250
+ });
251
+
252
+ // 5. Create commit
253
+ const newCommit = await this._req("/git/commits", {
254
+ method: "POST",
255
+ body: JSON.stringify({ message, tree: tree.sha, parents: [headSha] }),
256
+ });
257
+
258
+ // 6. Update ref
259
+ return this._req(`/git/refs/heads/${branch}`, {
260
+ method: "PATCH",
261
+ body: JSON.stringify({ sha: newCommit.sha }),
262
+ });
263
+ }
264
+
265
+ async createMR({ sourceBranch, targetBranch = this._defaultBranch, title, description = "" }) {
266
+ const raw = await this._req("/pulls", {
267
+ method: "POST",
268
+ body: JSON.stringify({
269
+ head: sourceBranch,
270
+ base: targetBranch,
271
+ title,
272
+ body: description,
273
+ }),
274
+ });
275
+ return this._mr(raw);
276
+ }
277
+
278
+ // ─── PR queries ───────────────────────────────────────────────────────────
279
+
280
+ async listAllForgeMRs(state = "open") {
281
+ const ghState = state === "open" ? "open" : "closed";
282
+ const all = await this._req(`/pulls?state=${ghState}&per_page=50`);
283
+ const forge = (all ?? []).filter((pr) => pr.head?.ref?.startsWith("forge/"));
284
+ // GitHub rolls merged + closed-without-merge into `state=closed`; split
285
+ // them apart on merged_at so the UI tabs stay meaningful.
286
+ const filtered =
287
+ state === "merged" ? forge.filter((pr) => pr.merged_at)
288
+ : state === "closed" ? forge.filter((pr) => !pr.merged_at)
289
+ : forge;
290
+ return filtered.map((pr) => this._mr(pr));
291
+ }
292
+
293
+ async listForgeMRsForIssue(issueIid) {
294
+ const all = await this._req("/pulls?state=open&per_page=50");
295
+ return (all ?? [])
296
+ .filter((pr) => pr.head?.ref?.startsWith(`forge/issue-${issueIid}-`))
297
+ .map((pr) => this._mr(pr));
298
+ }
299
+
300
+ async listMergedForgeMRsForIssue(issueIid) {
301
+ const all = await this._req("/pulls?state=closed&per_page=50");
302
+ return (all ?? [])
303
+ .filter((pr) => pr.merged_at && pr.head?.ref?.startsWith(`forge/issue-${issueIid}-`))
304
+ .map((pr) => this._mr(pr));
305
+ }
306
+
307
+ async getMR(mrIid) {
308
+ const raw = await this._req(`/pulls/${mrIid}`);
309
+ return this._mr(raw);
310
+ }
311
+
312
+ async getMRDiffs(mrIid) {
313
+ const raw = await this._req(`/pulls/${mrIid}/files?per_page=100`);
314
+ return (raw ?? []).map((f) => this._diff(f));
315
+ }
316
+
317
+ // rebaseMR not supported — supportsRebase is false
318
+
319
+ // ─── CI / Pipelines (GitHub Actions) ─────────────────────────────────────
320
+
321
+ async getMRPipelines(mrIid) {
322
+ const pr = await this._req(`/pulls/${mrIid}`);
323
+ const sha = pr.head?.sha;
324
+ if (!sha) return [];
325
+ const data = await this._req(`/actions/runs?head_sha=${sha}&per_page=10`);
326
+ return (data.workflow_runs ?? []).map((r) => this._pipeline(r));
327
+ }
328
+
329
+ async getPipeline(pipelineId) {
330
+ const raw = await this._req(`/actions/runs/${pipelineId}`);
331
+ return this._pipeline(raw);
332
+ }
333
+
334
+ async getPipelineJobs(pipelineId) {
335
+ const data = await this._req(`/actions/runs/${pipelineId}/jobs`);
336
+ return (data.jobs ?? []).map((j) => this._job(j));
337
+ }
338
+
339
+ async getJobTrace(jobId) {
340
+ const url = `${this._repoBase()}/actions/jobs/${jobId}/logs`;
341
+ const res = await fetch(url, { headers: this._headers() });
342
+ if (!res.ok) {
343
+ const text = await res.text().catch(() => "");
344
+ throw new Error(`GitHub ${res.status} on jobs/${jobId}/logs: ${text}`);
345
+ }
346
+ // GitHub redirects to a pre-signed S3 URL; fetch follows it automatically
347
+ return res.text();
348
+ }
349
+ }