@ipul-labs/forge 1.15.1 → 1.16.1
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 +22 -0
- package/bin/cli/analyze.js +3 -3
- package/bin/cli/apply.js +1 -1
- package/bin/cli/cifix.js +1 -1
- package/bin/cli/createIssue.js +112 -0
- package/bin/cli/doctor.js +1 -1
- package/bin/cli/init.js +3 -3
- package/bin/cli/issue.js +3 -3
- package/bin/cli/issues.js +2 -2
- package/bin/cli/merge.js +1 -1
- package/bin/cli/refine.js +2 -2
- package/bin/cli/repl.js +232 -0
- package/bin/cli/repos.js +3 -3
- package/bin/cli/resolution.js +1 -1
- package/bin/cli/session.js +3 -3
- package/bin/cli/sessionStore.js +3 -3
- package/bin/cli/ship.js +9 -9
- package/bin/cli/update.js +95 -0
- package/bin/cli/validate.js +1 -1
- package/bin/cli/watch.js +1 -1
- package/bin/forge-agent.js +39 -11
- package/package.json +1 -1
- package/bin/README.md +0 -210
package/README.md
CHANGED
|
@@ -86,6 +86,25 @@ npm install -g @ipul-labs/forge
|
|
|
86
86
|
npx forge <command>
|
|
87
87
|
```
|
|
88
88
|
|
|
89
|
+
#### Interactive mode
|
|
90
|
+
|
|
91
|
+
Run `forge` with no arguments to enter an interactive terminal session:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
forge
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
forge v1.15.1
|
|
99
|
+
|
|
100
|
+
──────────────────────────────────────────────────
|
|
101
|
+
❯ issues --limit 5
|
|
102
|
+
──────────────────────────────────────────────────
|
|
103
|
+
group/repo | agent:claude | v1.15.1
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Tab-completes command names. Type `use <repo>` to switch repos. Commands that error stay in the session — only `exit`, `quit`, or Ctrl+D leave.
|
|
107
|
+
|
|
89
108
|
#### End-to-end: issue to production
|
|
90
109
|
|
|
91
110
|
```bash
|
|
@@ -99,6 +118,7 @@ Runs: analyze → review → validate → apply → watch CI → merge → watch
|
|
|
99
118
|
```bash
|
|
100
119
|
forge issues # List open issues (--label X, --mine, --limit N)
|
|
101
120
|
forge issue 42 # Show full description
|
|
121
|
+
forge create-issue --title "fix: broken widget" # Create a new issue
|
|
102
122
|
forge analyze 42 # Run two-pass analysis, cache result
|
|
103
123
|
forge show # Re-render cached analysis
|
|
104
124
|
forge refine --feedback "..." # Iterate with feedback
|
|
@@ -109,6 +129,8 @@ forge ci-fix # Re-analyze with failed-pipeline logs
|
|
|
109
129
|
forge merge # Merge the MR
|
|
110
130
|
forge resolution-check # Verify merged MR closes the issue
|
|
111
131
|
forge close # Close the issue
|
|
132
|
+
forge use group/other-repo # Switch active repo
|
|
133
|
+
forge update # Pull latest + reinstall dependencies
|
|
112
134
|
```
|
|
113
135
|
|
|
114
136
|
#### Options
|
package/bin/cli/analyze.js
CHANGED
|
@@ -260,7 +260,7 @@ function AnalyzeView({
|
|
|
260
260
|
/**
|
|
261
261
|
* Render a cached analysis (read from session) through the same ResultBlock
|
|
262
262
|
* the live analyze view uses. No streaming, no progress — just the final
|
|
263
|
-
* payload. Used by `forge
|
|
263
|
+
* payload. Used by `forge show`.
|
|
264
264
|
*/
|
|
265
265
|
export async function renderCachedResult(result) {
|
|
266
266
|
await runApp(
|
|
@@ -405,14 +405,14 @@ export async function cmdAnalyze(argv) {
|
|
|
405
405
|
|
|
406
406
|
const iid = Number(argv[1]);
|
|
407
407
|
if (!Number.isInteger(iid) || iid <= 0) {
|
|
408
|
-
console.error("Usage: forge
|
|
408
|
+
console.error("Usage: forge analyze <iid> [options]");
|
|
409
409
|
process.exit(1);
|
|
410
410
|
}
|
|
411
411
|
|
|
412
412
|
const { primary, secondaries } = await resolveRepoSet(argv);
|
|
413
413
|
if (!primary) {
|
|
414
414
|
console.error(
|
|
415
|
-
"No repo. Pass --repo <group/repo>, set FORGE_GITLAB_PROJECT, or run `forge
|
|
415
|
+
"No repo. Pass --repo <group/repo>, set FORGE_GITLAB_PROJECT, or run `forge init`.",
|
|
416
416
|
);
|
|
417
417
|
process.exit(1);
|
|
418
418
|
}
|
package/bin/cli/apply.js
CHANGED
|
@@ -188,7 +188,7 @@ export async function cmdApply(argv) {
|
|
|
188
188
|
const analysis = await readAnalysis(target.repo, target.iid);
|
|
189
189
|
if (!analysis) {
|
|
190
190
|
console.error(
|
|
191
|
-
`No cached analysis at ${target.repo}#${target.iid}. Run: forge
|
|
191
|
+
`No cached analysis at ${target.repo}#${target.iid}. Run: forge analyze ${target.iid}`,
|
|
192
192
|
);
|
|
193
193
|
process.exit(1);
|
|
194
194
|
}
|
package/bin/cli/cifix.js
CHANGED
|
@@ -253,7 +253,7 @@ export async function cmdCiFix(argv) {
|
|
|
253
253
|
const state = await readState(target.repo, target.iid);
|
|
254
254
|
if (!state?.branch || !state?.mrIid) {
|
|
255
255
|
console.error(
|
|
256
|
-
`Session ${target.repo}#${target.iid} has no branch/MR yet. Run: forge
|
|
256
|
+
`Session ${target.repo}#${target.iid} has no branch/MR yet. Run: forge apply ${target.iid}`,
|
|
257
257
|
);
|
|
258
258
|
process.exit(1);
|
|
259
259
|
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import React, { useEffect, useState } from "react";
|
|
3
|
+
import Spinner from "ink-spinner";
|
|
4
|
+
import { h, Box, Text, useApp, runApp } from "./ink.js";
|
|
5
|
+
import { getFlag, projectRoot, resolvePrimaryRepo } from "./utils.js";
|
|
6
|
+
|
|
7
|
+
function CreateIssueView({ repo, title, description, onDone }) {
|
|
8
|
+
const { exit } = useApp();
|
|
9
|
+
const [phase, setPhase] = useState("creating");
|
|
10
|
+
const [issue, setIssue] = useState(null);
|
|
11
|
+
const [error, setError] = useState(null);
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
if (phase === "done" || phase === "error") {
|
|
15
|
+
onDone({ phase, issue, error });
|
|
16
|
+
exit();
|
|
17
|
+
}
|
|
18
|
+
}, [phase]);
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
let cancelled = false;
|
|
22
|
+
(async () => {
|
|
23
|
+
try {
|
|
24
|
+
const { createProvider } = await import(
|
|
25
|
+
path.join(projectRoot, "server/forge/core/providers/index.js")
|
|
26
|
+
);
|
|
27
|
+
const provider = createProvider(repo);
|
|
28
|
+
const created = await provider.createIssue(title, description);
|
|
29
|
+
if (cancelled) return;
|
|
30
|
+
setIssue(created);
|
|
31
|
+
setPhase("done");
|
|
32
|
+
} catch (err) {
|
|
33
|
+
if (cancelled) return;
|
|
34
|
+
setError(err);
|
|
35
|
+
setPhase("error");
|
|
36
|
+
}
|
|
37
|
+
})();
|
|
38
|
+
return () => {
|
|
39
|
+
cancelled = true;
|
|
40
|
+
};
|
|
41
|
+
}, []);
|
|
42
|
+
|
|
43
|
+
if (phase === "creating") {
|
|
44
|
+
return h`<${Box}>
|
|
45
|
+
<${Text} color="cyan"><${Spinner} type="dots"/></>
|
|
46
|
+
<${Text}> Creating issue in ${repo}…</>
|
|
47
|
+
</>`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (phase === "done") {
|
|
51
|
+
return h`<${Box} flexDirection="column">
|
|
52
|
+
<${Box}>
|
|
53
|
+
<${Text} color="green">✓ </>
|
|
54
|
+
<${Text}>Created </>
|
|
55
|
+
<${Text} color="yellow">#${issue.iid}</>
|
|
56
|
+
<${Text}> — ${issue.title}</>
|
|
57
|
+
</>
|
|
58
|
+
${issue.web_url
|
|
59
|
+
? h`<${Box}><${Text} dimColor>${issue.web_url}</></>`
|
|
60
|
+
: null}
|
|
61
|
+
<${Box} marginTop=${1}>
|
|
62
|
+
<${Text} dimColor>${`Next: forge ship ${issue.iid}`}</>
|
|
63
|
+
</>
|
|
64
|
+
</>`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return h`<${Box}>
|
|
68
|
+
<${Text} color="red">${`✘ ${error?.message ?? String(error)}`}</>
|
|
69
|
+
</>`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function cmdCreateIssue(argv) {
|
|
73
|
+
const title = getFlag(argv, "--title") ?? argv.slice(1).find((a) => !a.startsWith("-"));
|
|
74
|
+
if (!title) {
|
|
75
|
+
console.error("Usage: forge create-issue --title \"Bug: widget broken\" [--description \"...\"] [--repo <id>] [--json]");
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const description = getFlag(argv, "--description") ?? "";
|
|
80
|
+
const repo = await resolvePrimaryRepo(argv);
|
|
81
|
+
if (!repo) {
|
|
82
|
+
console.error(
|
|
83
|
+
"No repo. Pass --repo <group/repo>, set FORGE_GITLAB_PROJECT, or run `forge init`.",
|
|
84
|
+
);
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const json = argv.includes("--json");
|
|
89
|
+
if (json) {
|
|
90
|
+
const { createProvider } = await import(
|
|
91
|
+
path.join(projectRoot, "server/forge/core/providers/index.js")
|
|
92
|
+
);
|
|
93
|
+
try {
|
|
94
|
+
const issue = await createProvider(repo).createIssue(title, description);
|
|
95
|
+
process.stdout.write(`${JSON.stringify(issue, null, 2)}\n`);
|
|
96
|
+
} catch (err) {
|
|
97
|
+
process.stdout.write(`${JSON.stringify({ error: err.message })}\n`);
|
|
98
|
+
process.exit(2);
|
|
99
|
+
}
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let outcome = { phase: null };
|
|
104
|
+
await runApp(
|
|
105
|
+
h`<${CreateIssueView}
|
|
106
|
+
repo=${repo}
|
|
107
|
+
title=${title}
|
|
108
|
+
description=${description}
|
|
109
|
+
onDone=${(o) => (outcome = o)} />`,
|
|
110
|
+
);
|
|
111
|
+
if (outcome.phase === "error") process.exit(2);
|
|
112
|
+
}
|
package/bin/cli/doctor.js
CHANGED
|
@@ -123,7 +123,7 @@ export async function cmdDoctor() {
|
|
|
123
123
|
{
|
|
124
124
|
name: "FORGE_TOKEN",
|
|
125
125
|
fn: () => {
|
|
126
|
-
if (!process.env.FORGE_TOKEN) throw new Error("not set (run: forge
|
|
126
|
+
if (!process.env.FORGE_TOKEN) throw new Error("not set (run: forge init)");
|
|
127
127
|
return "set";
|
|
128
128
|
},
|
|
129
129
|
},
|
package/bin/cli/init.js
CHANGED
|
@@ -103,7 +103,7 @@ function InitWizard({ envPath, hasExisting, force, onDone }) {
|
|
|
103
103
|
|
|
104
104
|
if (step === "overwrite") {
|
|
105
105
|
return h`<${Box} flexDirection="column">
|
|
106
|
-
<${Text}>forge
|
|
106
|
+
<${Text}>forge init</>
|
|
107
107
|
<${Box} marginTop=${1}>
|
|
108
108
|
<${Text} color="yellow">! </>
|
|
109
109
|
<${Text}>${envPath} exists — overwrite?</>
|
|
@@ -121,7 +121,7 @@ function InitWizard({ envPath, hasExisting, force, onDone }) {
|
|
|
121
121
|
|
|
122
122
|
if (step === "token") {
|
|
123
123
|
return h`<${Box} flexDirection="column">
|
|
124
|
-
<${Text}>forge
|
|
124
|
+
<${Text}>forge init</>
|
|
125
125
|
<${Box} marginTop=${1}/>
|
|
126
126
|
<${Field} label="FORGE_TOKEN" hint="press Enter to use generated">
|
|
127
127
|
<${TextInput}
|
|
@@ -265,7 +265,7 @@ function InitWizard({ envPath, hasExisting, force, onDone }) {
|
|
|
265
265
|
<${Text} dimColor>${`Configured ${data.repos.length} repo${data.repos.length === 1 ? "" : "s"} (primary: ${data.repos[0] ?? "none"}).`}</>
|
|
266
266
|
</>
|
|
267
267
|
<${Box}>
|
|
268
|
-
<${Text} dimColor>${"Next: forge
|
|
268
|
+
<${Text} dimColor>${"Next: forge doctor | forge issues | forge start"}</>
|
|
269
269
|
</>
|
|
270
270
|
</>`;
|
|
271
271
|
}
|
package/bin/cli/issue.js
CHANGED
|
@@ -32,7 +32,7 @@ function IssueDetail({ issue, repo }) {
|
|
|
32
32
|
<${Text}>${issue.description ?? "(no description)"}</>
|
|
33
33
|
</>
|
|
34
34
|
<${Box} marginTop=${1}>
|
|
35
|
-
<${Text} dimColor>${`Next: forge
|
|
35
|
+
<${Text} dimColor>${`Next: forge ship ${issue.iid}`}</>
|
|
36
36
|
</>
|
|
37
37
|
</>`;
|
|
38
38
|
}
|
|
@@ -83,13 +83,13 @@ function IssueView({ iid, repo, onDone }) {
|
|
|
83
83
|
export async function cmdIssue(argv) {
|
|
84
84
|
const iid = Number(argv[1]);
|
|
85
85
|
if (!Number.isInteger(iid) || iid <= 0) {
|
|
86
|
-
console.error("Usage: forge
|
|
86
|
+
console.error("Usage: forge issue <iid> [--repo <id>]");
|
|
87
87
|
process.exit(1);
|
|
88
88
|
}
|
|
89
89
|
const repo = (await resolvePrimaryRepo(argv)) ?? null;
|
|
90
90
|
if (!repo) {
|
|
91
91
|
console.error(
|
|
92
|
-
"No repo. Pass --repo <group/repo>, set FORGE_GITLAB_PROJECT, or run `forge
|
|
92
|
+
"No repo. Pass --repo <group/repo>, set FORGE_GITLAB_PROJECT, or run `forge init`.",
|
|
93
93
|
);
|
|
94
94
|
process.exit(1);
|
|
95
95
|
}
|
package/bin/cli/issues.js
CHANGED
|
@@ -101,7 +101,7 @@ function IssuesView({ primary, limit, label, mine, onError }) {
|
|
|
101
101
|
</>`,
|
|
102
102
|
)}
|
|
103
103
|
<${Box} marginTop=${1}>
|
|
104
|
-
<${Text} dimColor>${"Analyze: forge
|
|
104
|
+
<${Text} dimColor>${"Analyze: forge analyze <iid>"}</>
|
|
105
105
|
</>
|
|
106
106
|
</>`;
|
|
107
107
|
}
|
|
@@ -110,7 +110,7 @@ export async function cmdIssues(argv) {
|
|
|
110
110
|
const primary = await resolvePrimaryRepo(argv);
|
|
111
111
|
if (!primary) {
|
|
112
112
|
console.error(
|
|
113
|
-
"No repo. Pass --repo <group/repo>, set FORGE_GITLAB_PROJECT, or run `forge
|
|
113
|
+
"No repo. Pass --repo <group/repo>, set FORGE_GITLAB_PROJECT, or run `forge init`.",
|
|
114
114
|
);
|
|
115
115
|
process.exit(1);
|
|
116
116
|
}
|
package/bin/cli/merge.js
CHANGED
|
@@ -123,7 +123,7 @@ export async function cmdMerge(argv) {
|
|
|
123
123
|
const state = await readState(target.repo, target.iid);
|
|
124
124
|
if (!state?.mrIid) {
|
|
125
125
|
console.error(
|
|
126
|
-
`Session ${target.repo}#${target.iid} has no MR. Run: forge
|
|
126
|
+
`Session ${target.repo}#${target.iid} has no MR. Run: forge apply ${target.iid}`,
|
|
127
127
|
);
|
|
128
128
|
process.exit(1);
|
|
129
129
|
}
|
package/bin/cli/refine.js
CHANGED
|
@@ -171,7 +171,7 @@ function RefineView({ analysis, primary, secondaries, feedback, model, eventSink
|
|
|
171
171
|
})}
|
|
172
172
|
${result
|
|
173
173
|
? h`<${Box} marginTop=${1}>
|
|
174
|
-
<${Text} color="green">${`✓ Refined ${result.changes?.length ?? 0} change(s) — view with: forge
|
|
174
|
+
<${Text} color="green">${`✓ Refined ${result.changes?.length ?? 0} change(s) — view with: forge show`}</>
|
|
175
175
|
</>`
|
|
176
176
|
: null}
|
|
177
177
|
</>`;
|
|
@@ -193,7 +193,7 @@ export async function cmdRefine(argv) {
|
|
|
193
193
|
const analysis = await readAnalysis(target.repo, target.iid);
|
|
194
194
|
if (!analysis) {
|
|
195
195
|
console.error(
|
|
196
|
-
`No cached analysis at ${target.repo}#${target.iid}. Run: forge
|
|
196
|
+
`No cached analysis at ${target.repo}#${target.iid}. Run: forge analyze ${target.iid}`,
|
|
197
197
|
);
|
|
198
198
|
process.exit(1);
|
|
199
199
|
}
|
package/bin/cli/repl.js
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import readline from "node:readline";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { projectRoot, readPkg, resolvePrimaryRepo } from "./utils.js";
|
|
4
|
+
|
|
5
|
+
const COMMANDS = [
|
|
6
|
+
"ship", "init", "start", "repos", "issues", "issue", "create-issue",
|
|
7
|
+
"analyze", "show", "refine", "validate", "apply", "watch",
|
|
8
|
+
"ci-fix", "merge", "resolution-check", "close",
|
|
9
|
+
"session", "doctor", "update", "use", "version", "help",
|
|
10
|
+
"exit", "quit", "clear",
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
class ExitInterrupt extends Error {
|
|
14
|
+
constructor(code) {
|
|
15
|
+
super();
|
|
16
|
+
this.code = code;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const DIM = "\x1b[2m";
|
|
21
|
+
const RESET = "\x1b[0m";
|
|
22
|
+
const BOLD = "\x1b[1m";
|
|
23
|
+
const YELLOW = "\x1b[33m";
|
|
24
|
+
const GREEN = "\x1b[32m";
|
|
25
|
+
const CYAN = "\x1b[36m";
|
|
26
|
+
|
|
27
|
+
function cols() {
|
|
28
|
+
return process.stdout.columns || 80;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function bar() {
|
|
32
|
+
return DIM + "─".repeat(cols()) + RESET;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function resolveContext() {
|
|
36
|
+
let repo = null;
|
|
37
|
+
let agent = "claude";
|
|
38
|
+
try {
|
|
39
|
+
repo = await resolvePrimaryRepo([]);
|
|
40
|
+
} catch {}
|
|
41
|
+
try {
|
|
42
|
+
const { get } = await import(
|
|
43
|
+
path.join(projectRoot, "server/forge/core/configStore.js")
|
|
44
|
+
);
|
|
45
|
+
const cfg = get();
|
|
46
|
+
agent = cfg?.defaults?.agent || "claude";
|
|
47
|
+
} catch {}
|
|
48
|
+
return { repo, agent };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function getConfiguredRepos() {
|
|
52
|
+
try {
|
|
53
|
+
const { get } = await import(
|
|
54
|
+
path.join(projectRoot, "server/forge/core/configStore.js")
|
|
55
|
+
);
|
|
56
|
+
return (get().repos ?? []).map((r) => r.id);
|
|
57
|
+
} catch {
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function switchRepo(repoId) {
|
|
63
|
+
const { get, patch } = await import(
|
|
64
|
+
path.join(projectRoot, "server/forge/core/configStore.js")
|
|
65
|
+
);
|
|
66
|
+
const repos = get().repos ?? [];
|
|
67
|
+
const target = repos.find((r) => r.id === repoId);
|
|
68
|
+
if (!target) {
|
|
69
|
+
console.error(`Not configured: ${repoId}`);
|
|
70
|
+
console.error(`Add it first: repos add ${repoId}`);
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
const rest = repos.filter((r) => r.id !== repoId);
|
|
74
|
+
await patch({ repos: [target, ...rest] });
|
|
75
|
+
return repoId;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function startRepl(dispatch) {
|
|
79
|
+
const pkg = await readPkg();
|
|
80
|
+
const tty = process.stdout.isTTY;
|
|
81
|
+
const ctx = await resolveContext();
|
|
82
|
+
|
|
83
|
+
if (tty) {
|
|
84
|
+
process.stdout.write(
|
|
85
|
+
`\n ${BOLD}forge${RESET} ${DIM}v${pkg.version}${RESET}\n\n`,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function footerText() {
|
|
90
|
+
const parts = [];
|
|
91
|
+
if (ctx.repo) parts.push(`${YELLOW}${ctx.repo}${RESET}`);
|
|
92
|
+
parts.push(`${DIM}agent:${RESET}${ctx.agent}`);
|
|
93
|
+
parts.push(`${DIM}v${pkg.version}${RESET}`);
|
|
94
|
+
return ` ${parts.join(` ${DIM}|${RESET} `)}`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Draw top bar, then let readline show its simple prompt,
|
|
98
|
+
// then draw bottom bar + footer below and move cursor back up.
|
|
99
|
+
// This keeps readline's prompt short so cursor positioning works.
|
|
100
|
+
function showPrompt() {
|
|
101
|
+
if (tty) {
|
|
102
|
+
process.stdout.write(bar() + "\n");
|
|
103
|
+
}
|
|
104
|
+
rl.prompt();
|
|
105
|
+
if (tty) {
|
|
106
|
+
process.stdout.write(
|
|
107
|
+
"\n" + bar() + "\n" + footerText() +
|
|
108
|
+
"\x1b[2A\x1b[4G",
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function clearFrame() {
|
|
114
|
+
if (!tty) return;
|
|
115
|
+
process.stdout.write("\x1b[0J");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const prompt = tty ? ` ${DIM}❯${RESET} ` : "forge> ";
|
|
119
|
+
|
|
120
|
+
const rl = readline.createInterface({
|
|
121
|
+
input: process.stdin,
|
|
122
|
+
output: process.stdout,
|
|
123
|
+
prompt,
|
|
124
|
+
completer: (line) => {
|
|
125
|
+
const hits = COMMANDS.filter((c) => c.startsWith(line.trim()));
|
|
126
|
+
return [hits.length ? hits : COMMANDS, line];
|
|
127
|
+
},
|
|
128
|
+
terminal: tty,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
showPrompt();
|
|
132
|
+
|
|
133
|
+
for await (const line of rl) {
|
|
134
|
+
const trimmed = line.trim();
|
|
135
|
+
|
|
136
|
+
if (!trimmed) {
|
|
137
|
+
showPrompt();
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
clearFrame();
|
|
142
|
+
|
|
143
|
+
if (trimmed === "exit" || trimmed === "quit") {
|
|
144
|
+
if (tty) process.stdout.write(`\n ${DIM}Session ended.${RESET}\n\n`);
|
|
145
|
+
rl.close();
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (trimmed === "clear") {
|
|
150
|
+
if (tty) process.stdout.write("\x1b[2J\x1b[H");
|
|
151
|
+
showPrompt();
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const argv = parseInput(trimmed);
|
|
156
|
+
|
|
157
|
+
if (argv[0] === "use") {
|
|
158
|
+
process.stdout.write("\n");
|
|
159
|
+
if (!argv[1]) {
|
|
160
|
+
const repos = await getConfiguredRepos();
|
|
161
|
+
if (repos.length === 0) {
|
|
162
|
+
console.error("No repos configured. Run: repos add <group/repo>");
|
|
163
|
+
} else {
|
|
164
|
+
for (const id of repos) {
|
|
165
|
+
const marker = id === ctx.repo ? `${GREEN}*${RESET} ` : " ";
|
|
166
|
+
process.stdout.write(`${marker}${id}\n`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
} else {
|
|
170
|
+
const result = await switchRepo(argv[1]);
|
|
171
|
+
if (result) {
|
|
172
|
+
ctx.repo = result;
|
|
173
|
+
process.stdout.write(`${GREEN}*${RESET} ${YELLOW}${result}${RESET}\n`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
process.stdout.write("\n");
|
|
177
|
+
showPrompt();
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
process.stdout.write("\n");
|
|
182
|
+
|
|
183
|
+
const originalExit = process.exit;
|
|
184
|
+
try {
|
|
185
|
+
process.exit = (code) => {
|
|
186
|
+
throw new ExitInterrupt(code ?? 0);
|
|
187
|
+
};
|
|
188
|
+
await dispatch(argv);
|
|
189
|
+
} catch (err) {
|
|
190
|
+
if (err instanceof ExitInterrupt) {
|
|
191
|
+
// swallow
|
|
192
|
+
} else {
|
|
193
|
+
console.error(err.message || String(err));
|
|
194
|
+
}
|
|
195
|
+
} finally {
|
|
196
|
+
process.exit = originalExit;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
process.stdout.write("\n");
|
|
200
|
+
showPrompt();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (tty) process.stdout.write(`\n ${DIM}Session ended.${RESET}\n\n`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function parseInput(line) {
|
|
207
|
+
const tokens = [];
|
|
208
|
+
let current = "";
|
|
209
|
+
let inQuote = null;
|
|
210
|
+
|
|
211
|
+
for (let i = 0; i < line.length; i++) {
|
|
212
|
+
const ch = line[i];
|
|
213
|
+
if (inQuote) {
|
|
214
|
+
if (ch === inQuote) {
|
|
215
|
+
inQuote = null;
|
|
216
|
+
} else {
|
|
217
|
+
current += ch;
|
|
218
|
+
}
|
|
219
|
+
} else if (ch === '"' || ch === "'") {
|
|
220
|
+
inQuote = ch;
|
|
221
|
+
} else if (ch === " " || ch === "\t") {
|
|
222
|
+
if (current) {
|
|
223
|
+
tokens.push(current);
|
|
224
|
+
current = "";
|
|
225
|
+
}
|
|
226
|
+
} else {
|
|
227
|
+
current += ch;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (current) tokens.push(current);
|
|
231
|
+
return tokens;
|
|
232
|
+
}
|
package/bin/cli/repos.js
CHANGED
|
@@ -7,7 +7,7 @@ function ReposList({ repos }) {
|
|
|
7
7
|
if (repos.length === 0) {
|
|
8
8
|
return h`<${Box} flexDirection="column">
|
|
9
9
|
<${Text} dimColor>No repos configured.</>
|
|
10
|
-
<${Text}>${"Add one: forge
|
|
10
|
+
<${Text}>${"Add one: forge repos add <group/repo>"}</>
|
|
11
11
|
</>`;
|
|
12
12
|
}
|
|
13
13
|
return h`<${Box} flexDirection="column" marginTop=${1}>
|
|
@@ -44,7 +44,7 @@ export async function cmdRepos(argv) {
|
|
|
44
44
|
|
|
45
45
|
const repoArg = argv[2];
|
|
46
46
|
if (!repoArg) {
|
|
47
|
-
console.error(`Usage: forge
|
|
47
|
+
console.error(`Usage: forge repos ${sub} <group/repo>`);
|
|
48
48
|
process.exit(1);
|
|
49
49
|
}
|
|
50
50
|
|
|
@@ -76,7 +76,7 @@ export async function cmdRepos(argv) {
|
|
|
76
76
|
if (sub === "set-default" || sub === "default") {
|
|
77
77
|
const target = repos.find((r) => r.id === repoArg);
|
|
78
78
|
if (!target) {
|
|
79
|
-
console.error(`Not configured. Add it first: forge
|
|
79
|
+
console.error(`Not configured. Add it first: forge repos add ${repoArg}`);
|
|
80
80
|
process.exit(1);
|
|
81
81
|
}
|
|
82
82
|
const rest = repos.filter((r) => r.id !== repoArg);
|
package/bin/cli/resolution.js
CHANGED
|
@@ -92,7 +92,7 @@ export async function cmdResolutionCheck(argv) {
|
|
|
92
92
|
const state = await readState(target.repo, target.iid);
|
|
93
93
|
if (!state?.mrIid) {
|
|
94
94
|
console.error(
|
|
95
|
-
`Session ${target.repo}#${target.iid} has no MR. Run: forge
|
|
95
|
+
`Session ${target.repo}#${target.iid} has no MR. Run: forge apply ${target.iid}`,
|
|
96
96
|
);
|
|
97
97
|
process.exit(1);
|
|
98
98
|
}
|
package/bin/cli/session.js
CHANGED
|
@@ -30,7 +30,7 @@ function SessionsTable({ sessions, head }) {
|
|
|
30
30
|
if (sessions.length === 0) {
|
|
31
31
|
return h`<${Box} flexDirection="column">
|
|
32
32
|
<${Text} dimColor>No sessions yet.</>
|
|
33
|
-
<${Text}>${"Run: forge
|
|
33
|
+
<${Text}>${"Run: forge analyze <iid>"}</>
|
|
34
34
|
</>`;
|
|
35
35
|
}
|
|
36
36
|
const rows = sessions.map((s) => {
|
|
@@ -192,7 +192,7 @@ export async function cmdSession(argv) {
|
|
|
192
192
|
}
|
|
193
193
|
|
|
194
194
|
/**
|
|
195
|
-
* Top-level `forge
|
|
195
|
+
* Top-level `forge show [<iid>]` — re-renders the cached analysis from
|
|
196
196
|
* the session through the same Ink view as live analyze. Resolves <iid> via
|
|
197
197
|
* positional arg or HEAD pointer.
|
|
198
198
|
*/
|
|
@@ -214,7 +214,7 @@ export async function cmdShow(argv) {
|
|
|
214
214
|
if (!analysis) {
|
|
215
215
|
console.error(
|
|
216
216
|
`No cached analysis at ${sessionDir(target.repo, target.iid)}.\n` +
|
|
217
|
-
`Run: forge
|
|
217
|
+
`Run: forge analyze ${target.iid}`,
|
|
218
218
|
);
|
|
219
219
|
process.exit(1);
|
|
220
220
|
}
|
package/bin/cli/sessionStore.js
CHANGED
|
@@ -196,9 +196,9 @@ export async function cleanAll() {
|
|
|
196
196
|
* Resolve a session target from CLI argv. Honours an explicit positional iid
|
|
197
197
|
* or HEAD as fallback. Returns { repo, iid } or null.
|
|
198
198
|
*
|
|
199
|
-
* forge
|
|
200
|
-
* forge
|
|
201
|
-
* forge
|
|
199
|
+
* forge show → HEAD
|
|
200
|
+
* forge show 42 → primary repo + iid 42
|
|
201
|
+
* forge show 42 --repo X → repo X + iid 42
|
|
202
202
|
*/
|
|
203
203
|
export async function resolveSessionTarget({ explicitIid, explicitRepo }) {
|
|
204
204
|
if (Number.isInteger(explicitIid) && explicitIid > 0) {
|
package/bin/cli/ship.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `forge
|
|
2
|
+
* `forge ship <iid>` — the headline command.
|
|
3
3
|
*
|
|
4
4
|
* Walks an issue from "open ticket" all the way to "deployed in production"
|
|
5
5
|
* with a yes/no Confirm gate at every state-changing step. Each phase calls
|
|
@@ -114,7 +114,7 @@ function ProposalSummary({ analysis }) {
|
|
|
114
114
|
${changes.map((c, i) =>
|
|
115
115
|
h`<${Text} key=${i} dimColor>${` ${c.action.padEnd(7)} ${c.repo}:${c.file}`}</>`,
|
|
116
116
|
)}
|
|
117
|
-
<${Text} dimColor>${" (full diff: forge
|
|
117
|
+
<${Text} dimColor>${" (full diff: forge show " + (analysis.issue?.iid ?? "") + ")"}</>
|
|
118
118
|
</>`;
|
|
119
119
|
}
|
|
120
120
|
|
|
@@ -305,7 +305,7 @@ function ShipView({ iid, primary, secondaries, bypassYes, autoMerge, skipValidat
|
|
|
305
305
|
const res = await validate(analysis, () => {});
|
|
306
306
|
if (cancelled) return;
|
|
307
307
|
if (!res.allPassed) {
|
|
308
|
-
finishStep("Validating proposal", "fail", "see: forge
|
|
308
|
+
finishStep("Validating proposal", "fail", "see: forge validate");
|
|
309
309
|
setError(new Error("validation failed"));
|
|
310
310
|
setPhase("error");
|
|
311
311
|
return;
|
|
@@ -668,7 +668,7 @@ function ShipView({ iid, primary, secondaries, bypassYes, autoMerge, skipValidat
|
|
|
668
668
|
if (phase === "review") {
|
|
669
669
|
gate = h`<${Box} flexDirection="column" marginTop=${1}>
|
|
670
670
|
${analysis ? h`<${ProposalSummary} analysis=${analysis} />` : null}
|
|
671
|
-
<${Box} marginTop=${1}><${Text} dimColor>${"Use 'no' to abort, 'yes' to continue (run `forge
|
|
671
|
+
<${Box} marginTop=${1}><${Text} dimColor>${"Use 'no' to abort, 'yes' to continue (run `forge refine` separately to iterate)."}</></>
|
|
672
672
|
<${Confirm}
|
|
673
673
|
message="Accept this fix and continue toward production?"
|
|
674
674
|
bypassYes=${bypassYes}
|
|
@@ -703,18 +703,18 @@ function ShipView({ iid, primary, secondaries, bypassYes, autoMerge, skipValidat
|
|
|
703
703
|
</>`;
|
|
704
704
|
} else if (phase === "aborted") {
|
|
705
705
|
footer = h`<${Box} marginTop=${1}>
|
|
706
|
-
<${Text} dimColor>${`Aborted. Resume with: forge
|
|
706
|
+
<${Text} dimColor>${`Aborted. Resume with: forge ship ${iid}`}</>
|
|
707
707
|
</>`;
|
|
708
708
|
} else if (phase === "error") {
|
|
709
709
|
footer = h`<${Box} marginTop=${1}>
|
|
710
710
|
<${Text} color="red">${`✘ ${error?.message ?? String(error)}`}</>
|
|
711
|
-
<${Text} dimColor>${` (resume with: forge
|
|
711
|
+
<${Text} dimColor>${` (resume with: forge ship ${iid})`}</>
|
|
712
712
|
</>`;
|
|
713
713
|
}
|
|
714
714
|
|
|
715
715
|
return h`<${Box} flexDirection="column">
|
|
716
716
|
<${Box}>
|
|
717
|
-
<${Text} bold>forge
|
|
717
|
+
<${Text} bold>forge ship </>
|
|
718
718
|
<${Text} color="yellow">#${iid}</>
|
|
719
719
|
<${Text} dimColor>${` (${primary}${secondaries.length ? ` + ${secondaries.length} secondaries` : ""})`}</>
|
|
720
720
|
</>
|
|
@@ -728,14 +728,14 @@ function ShipView({ iid, primary, secondaries, bypassYes, autoMerge, skipValidat
|
|
|
728
728
|
export async function cmdShip(argv) {
|
|
729
729
|
const iid = Number(argv[1]);
|
|
730
730
|
if (!Number.isInteger(iid) || iid <= 0) {
|
|
731
|
-
console.error("Usage: forge
|
|
731
|
+
console.error("Usage: forge ship <iid> [options]");
|
|
732
732
|
process.exit(1);
|
|
733
733
|
}
|
|
734
734
|
|
|
735
735
|
const { primary, secondaries } = await resolveRepoSet(argv);
|
|
736
736
|
if (!primary) {
|
|
737
737
|
console.error(
|
|
738
|
-
"No repo. Pass --repo <group/repo>, set FORGE_GITLAB_PROJECT, or run `forge
|
|
738
|
+
"No repo. Pass --repo <group/repo>, set FORGE_GITLAB_PROJECT, or run `forge init`.",
|
|
739
739
|
);
|
|
740
740
|
process.exit(1);
|
|
741
741
|
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import React, { useEffect, useState } from "react";
|
|
4
|
+
import Spinner from "ink-spinner";
|
|
5
|
+
import { h, Box, Text, useApp, runApp } from "./ink.js";
|
|
6
|
+
import { projectRoot, readPkg } from "./utils.js";
|
|
7
|
+
|
|
8
|
+
function UpdateView({ onDone }) {
|
|
9
|
+
const { exit } = useApp();
|
|
10
|
+
const [phase, setPhase] = useState("checking");
|
|
11
|
+
const [info, setInfo] = useState({});
|
|
12
|
+
const [error, setError] = useState(null);
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
if (phase === "done" || phase === "error") {
|
|
16
|
+
onDone({ phase, info, error });
|
|
17
|
+
exit();
|
|
18
|
+
}
|
|
19
|
+
}, [phase]);
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
(async () => {
|
|
23
|
+
try {
|
|
24
|
+
const before = (await readPkg()).version;
|
|
25
|
+
setInfo((prev) => ({ ...prev, before }));
|
|
26
|
+
|
|
27
|
+
setPhase("pulling");
|
|
28
|
+
execSync("git pull --rebase", { cwd: projectRoot, stdio: "pipe" });
|
|
29
|
+
|
|
30
|
+
setPhase("installing");
|
|
31
|
+
execSync("npm install --no-audit --no-fund", {
|
|
32
|
+
cwd: projectRoot,
|
|
33
|
+
stdio: "pipe",
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const after = (await readPkg()).version;
|
|
37
|
+
setInfo({ before, after, updated: before !== after });
|
|
38
|
+
setPhase("done");
|
|
39
|
+
} catch (err) {
|
|
40
|
+
setError(err);
|
|
41
|
+
setPhase("error");
|
|
42
|
+
}
|
|
43
|
+
})();
|
|
44
|
+
}, []);
|
|
45
|
+
|
|
46
|
+
if (phase === "checking") {
|
|
47
|
+
return h`<${Box}>
|
|
48
|
+
<${Text} color="cyan"><${Spinner} type="dots"/></>
|
|
49
|
+
<${Text}> Checking for updates…</>
|
|
50
|
+
</>`;
|
|
51
|
+
}
|
|
52
|
+
if (phase === "pulling") {
|
|
53
|
+
return h`<${Box}>
|
|
54
|
+
<${Text} color="cyan"><${Spinner} type="dots"/></>
|
|
55
|
+
<${Text}> Pulling latest changes…</>
|
|
56
|
+
</>`;
|
|
57
|
+
}
|
|
58
|
+
if (phase === "installing") {
|
|
59
|
+
return h`<${Box}>
|
|
60
|
+
<${Text} color="cyan"><${Spinner} type="dots"/></>
|
|
61
|
+
<${Text}> Installing dependencies…</>
|
|
62
|
+
</>`;
|
|
63
|
+
}
|
|
64
|
+
if (phase === "done") {
|
|
65
|
+
if (info.updated) {
|
|
66
|
+
return h`<${Box} flexDirection="column">
|
|
67
|
+
<${Box}>
|
|
68
|
+
<${Text} color="green">✓ </>
|
|
69
|
+
<${Text}>Updated </>
|
|
70
|
+
<${Text} dimColor>${info.before}</>
|
|
71
|
+
<${Text}> → </>
|
|
72
|
+
<${Text} color="green">${info.after}</>
|
|
73
|
+
</>
|
|
74
|
+
<${Box}><${Text} dimColor>Restart forge to use the new version.</>
|
|
75
|
+
</>
|
|
76
|
+
</>`;
|
|
77
|
+
}
|
|
78
|
+
return h`<${Box}>
|
|
79
|
+
<${Text} color="green">✓ </>
|
|
80
|
+
<${Text}>Already up to date </>
|
|
81
|
+
<${Text} dimColor>(v${info.after})</>
|
|
82
|
+
</>`;
|
|
83
|
+
}
|
|
84
|
+
return h`<${Box}>
|
|
85
|
+
<${Text} color="red">${`✘ Update failed: ${error?.message ?? String(error)}`}</>
|
|
86
|
+
</>`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function cmdUpdate() {
|
|
90
|
+
let outcome = { phase: null };
|
|
91
|
+
await runApp(
|
|
92
|
+
h`<${UpdateView} onDone=${(o) => (outcome = o)} />`,
|
|
93
|
+
);
|
|
94
|
+
if (outcome.phase === "error") process.exit(2);
|
|
95
|
+
}
|
package/bin/cli/validate.js
CHANGED
|
@@ -210,7 +210,7 @@ export async function cmdValidate(argv) {
|
|
|
210
210
|
const analysis = await readAnalysis(target.repo, target.iid);
|
|
211
211
|
if (!analysis) {
|
|
212
212
|
console.error(
|
|
213
|
-
`No cached analysis at ${target.repo}#${target.iid}. Run: forge
|
|
213
|
+
`No cached analysis at ${target.repo}#${target.iid}. Run: forge analyze ${target.iid}`,
|
|
214
214
|
);
|
|
215
215
|
process.exit(1);
|
|
216
216
|
}
|
package/bin/cli/watch.js
CHANGED
|
@@ -206,7 +206,7 @@ export async function cmdWatch(argv) {
|
|
|
206
206
|
const state = await readState(target.repo, target.iid);
|
|
207
207
|
if (!state?.mrIid) {
|
|
208
208
|
console.error(
|
|
209
|
-
`Session ${target.repo}#${target.iid} has no MR yet. Run: forge
|
|
209
|
+
`Session ${target.repo}#${target.iid} has no MR yet. Run: forge apply ${target.iid}`,
|
|
210
210
|
);
|
|
211
211
|
process.exit(1);
|
|
212
212
|
}
|
package/bin/forge-agent.js
CHANGED
|
@@ -18,13 +18,24 @@ import { cmdResolutionCheck } from "./cli/resolution.js";
|
|
|
18
18
|
import { cmdClose } from "./cli/close.js";
|
|
19
19
|
import { cmdShip } from "./cli/ship.js";
|
|
20
20
|
import { cmdIssue } from "./cli/issue.js";
|
|
21
|
+
import { cmdCreateIssue } from "./cli/createIssue.js";
|
|
22
|
+
import { startRepl } from "./cli/repl.js";
|
|
23
|
+
import { cmdUpdate } from "./cli/update.js";
|
|
24
|
+
|
|
25
|
+
async function cmdUse(argv) {
|
|
26
|
+
const repoId = argv[1];
|
|
27
|
+
if (!repoId) {
|
|
28
|
+
return cmdRepos(["repos", "list"]);
|
|
29
|
+
}
|
|
30
|
+
return cmdRepos(["repos", "set-default", repoId]);
|
|
31
|
+
}
|
|
21
32
|
|
|
22
33
|
function usage() {
|
|
23
34
|
process.stdout.write(`
|
|
24
|
-
forge
|
|
35
|
+
forge — AI-powered code change & deploy control panel
|
|
25
36
|
|
|
26
37
|
Usage
|
|
27
|
-
forge
|
|
38
|
+
forge <command> [options]
|
|
28
39
|
|
|
29
40
|
Commands
|
|
30
41
|
ship <iid> ★ End-to-end: analyze → review → validate → apply →
|
|
@@ -35,6 +46,7 @@ Commands
|
|
|
35
46
|
repos Manage configured repos (list | add | set-default | remove)
|
|
36
47
|
issues List open issues (--label X, --mine, --limit N)
|
|
37
48
|
issue <iid> Show full description, labels, author, web URL
|
|
49
|
+
create-issue Create a new issue (--title "..." [--description "..."])
|
|
38
50
|
analyze <iid> Run issue analysis; caches result to ~/.forge/sessions
|
|
39
51
|
show [<iid>] Re-render the cached analysis for <iid> (or HEAD)
|
|
40
52
|
refine [<iid>] Iterate the cached analysis with feedback (--feedback "...")
|
|
@@ -45,8 +57,10 @@ Commands
|
|
|
45
57
|
merge [<iid>] Merge the MR (confirms by default; --auto arms auto-merge)
|
|
46
58
|
resolution-check Verify the merged MR actually closes the issue
|
|
47
59
|
close [<iid>] Close the issue (confirms by default)
|
|
60
|
+
use <repo> Switch active repo (interactive mode)
|
|
48
61
|
session Inspect cached sessions (list | show | clean)
|
|
49
62
|
doctor Check prerequisites (node, git, claude CLI, token, config)
|
|
63
|
+
update Pull latest changes + reinstall dependencies
|
|
50
64
|
version Print version
|
|
51
65
|
help Show this help
|
|
52
66
|
|
|
@@ -67,14 +81,15 @@ Options
|
|
|
67
81
|
--feedback <text> Refine feedback (refine command)
|
|
68
82
|
|
|
69
83
|
Examples
|
|
70
|
-
forge
|
|
71
|
-
forge
|
|
72
|
-
forge
|
|
73
|
-
forge
|
|
74
|
-
forge
|
|
75
|
-
forge
|
|
76
|
-
forge
|
|
77
|
-
forge-
|
|
84
|
+
forge init
|
|
85
|
+
forge start --port 5055
|
|
86
|
+
forge repos add group/other-repo
|
|
87
|
+
forge repos set-default group/other-repo
|
|
88
|
+
forge issues --limit 10
|
|
89
|
+
forge analyze 42 --repo group/repo
|
|
90
|
+
forge analyze 42 --json | jq .
|
|
91
|
+
forge create-issue --title "fix: broken widget" --description "Details here"
|
|
92
|
+
forge doctor
|
|
78
93
|
|
|
79
94
|
`);
|
|
80
95
|
}
|
|
@@ -107,6 +122,14 @@ async function main() {
|
|
|
107
122
|
|
|
108
123
|
const argv = process.argv.slice(2);
|
|
109
124
|
const cmd = argv[0];
|
|
125
|
+
|
|
126
|
+
if (!cmd) return startRepl(dispatch);
|
|
127
|
+
|
|
128
|
+
return dispatch(argv);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function dispatch(argv) {
|
|
132
|
+
const cmd = argv[0];
|
|
110
133
|
const flags = {
|
|
111
134
|
force: argv.includes("--force"),
|
|
112
135
|
port: parsePort(argv),
|
|
@@ -123,6 +146,8 @@ async function main() {
|
|
|
123
146
|
return cmdIssues(argv);
|
|
124
147
|
case "issue":
|
|
125
148
|
return cmdIssue(argv);
|
|
149
|
+
case "create-issue":
|
|
150
|
+
return cmdCreateIssue(argv);
|
|
126
151
|
case "analyze":
|
|
127
152
|
return cmdAnalyze(argv);
|
|
128
153
|
case "show":
|
|
@@ -151,6 +176,10 @@ async function main() {
|
|
|
151
176
|
return cmdSession(argv);
|
|
152
177
|
case "doctor":
|
|
153
178
|
return cmdDoctor(flags);
|
|
179
|
+
case "use":
|
|
180
|
+
return cmdUse(argv);
|
|
181
|
+
case "update":
|
|
182
|
+
return cmdUpdate();
|
|
154
183
|
case "version":
|
|
155
184
|
case "--version":
|
|
156
185
|
case "-v":
|
|
@@ -158,7 +187,6 @@ async function main() {
|
|
|
158
187
|
case "help":
|
|
159
188
|
case "--help":
|
|
160
189
|
case "-h":
|
|
161
|
-
case undefined:
|
|
162
190
|
usage();
|
|
163
191
|
return;
|
|
164
192
|
default:
|
package/package.json
CHANGED
package/bin/README.md
DELETED
|
@@ -1,210 +0,0 @@
|
|
|
1
|
-
# `forge-agent` — CLI
|
|
2
|
-
|
|
3
|
-
> Type an issue, accept the suggested fix, and watch it merge to production. One command.
|
|
4
|
-
|
|
5
|
-
`forge-agent` is the headless half of [Forge](../README.md). Same orchestrator the web UI uses, no browser required.
|
|
6
|
-
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
## Install
|
|
10
|
-
|
|
11
|
-
```bash
|
|
12
|
-
npm install -g forge-agent
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
Or run from a checkout without installing:
|
|
16
|
-
|
|
17
|
-
```bash
|
|
18
|
-
node bin/forge-agent.js <command>
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
You'll need:
|
|
22
|
-
|
|
23
|
-
- **Node.js 20+**
|
|
24
|
-
- **`claude` CLI** installed and authenticated (`claude auth login`)
|
|
25
|
-
- A **GitLab / GitHub / Azure DevOps** personal access token
|
|
26
|
-
|
|
27
|
-
---
|
|
28
|
-
|
|
29
|
-
## First-time setup
|
|
30
|
-
|
|
31
|
-
```bash
|
|
32
|
-
forge-agent init # interactive wizard — writes .env + ~/.forge/config.json
|
|
33
|
-
forge-agent doctor # confirm node, git, claude CLI, token, and config are all ready
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
`init` walks you through:
|
|
37
|
-
|
|
38
|
-
1. `FORGE_TOKEN` (a generated default — accept with Enter)
|
|
39
|
-
2. `PORT` (only matters if you also want the web UI)
|
|
40
|
-
3. SCM provider (gitlab / github / azuredevops, arrow keys)
|
|
41
|
-
4. Primary repo, then **add another?** (loop to add secondaries — needed for cross-repo features)
|
|
42
|
-
5. SCM token (masked input)
|
|
43
|
-
|
|
44
|
-
---
|
|
45
|
-
|
|
46
|
-
## The headline command
|
|
47
|
-
|
|
48
|
-
```bash
|
|
49
|
-
forge-agent ship <iid>
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
Walks an issue from "open ticket" to "deployed in production" in one command, pausing for `y/N` at every step that changes shared state:
|
|
53
|
-
|
|
54
|
-
```
|
|
55
|
-
ship <iid>
|
|
56
|
-
├─ analyze (or load cached if you've already analysed)
|
|
57
|
-
├─ review proposal (per-file diff + summary + divergence warnings)
|
|
58
|
-
├─ ?accept → continue, refuse, or refine with feedback
|
|
59
|
-
├─ validate (lint / typecheck / test against the proposed contents)
|
|
60
|
-
├─ ?apply → branch + commit + open MR
|
|
61
|
-
├─ watch CI
|
|
62
|
-
├─ ?ci-fix (only if the pipeline fails — re-analyse with logs, push to same branch)
|
|
63
|
-
├─ ?merge → merge to main
|
|
64
|
-
├─ watch deploy (post-merge pipeline)
|
|
65
|
-
└─ ?close issue
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
**Resumable.** If you abort or the process dies, re-run `forge-agent ship <iid>` and it picks up at the next gate (state lives in `~/.forge/sessions/`).
|
|
69
|
-
|
|
70
|
-
**Confirm-by-default.** Cursor on every gate defaults to *no*. Pass `--yes` (or set `FORGE_YES=1`) for unattended mode.
|
|
71
|
-
|
|
72
|
-
```bash
|
|
73
|
-
forge-agent ship 42 # interactive, all gates
|
|
74
|
-
forge-agent ship 42 --all-repos # cross-repo (web-app + api + …)
|
|
75
|
-
forge-agent ship 42 --yes # unattended, no prompts
|
|
76
|
-
forge-agent ship 42 --auto # arm auto-merge instead of merging now
|
|
77
|
-
forge-agent ship 42 --skip-validate # skip the auto-validate before apply
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
---
|
|
81
|
-
|
|
82
|
-
## Discover what to work on
|
|
83
|
-
|
|
84
|
-
```bash
|
|
85
|
-
forge-agent issues # latest open issues, sorted by recency
|
|
86
|
-
forge-agent issues --label feature # filter by label
|
|
87
|
-
forge-agent issues --mine # filter by author (FORGE_USER || USER)
|
|
88
|
-
forge-agent issues --json | jq . # pipe-friendly
|
|
89
|
-
forge-agent issue 42 # full description, labels, URL
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
---
|
|
93
|
-
|
|
94
|
-
## Drive each phase by hand
|
|
95
|
-
|
|
96
|
-
`ship` is a thin orchestrator — every phase is also a standalone command. Useful when you want to inspect, iterate, or only do part of the flow.
|
|
97
|
-
|
|
98
|
-
```bash
|
|
99
|
-
forge-agent analyze 42 --all-repos # produce + cache a fresh analysis
|
|
100
|
-
forge-agent show # re-render the most recent analysis
|
|
101
|
-
forge-agent show 42 # ...for a specific issue
|
|
102
|
-
forge-agent refine --feedback "use Resend instead of SendGrid"
|
|
103
|
-
forge-agent validate # lint/typecheck/test the proposal
|
|
104
|
-
forge-agent apply --validate # gate apply on green validation
|
|
105
|
-
forge-agent watch # poll the MR pipeline
|
|
106
|
-
forge-agent ci-fix # re-analyse with failed-job logs, push to same branch
|
|
107
|
-
forge-agent merge # confirm + merge
|
|
108
|
-
forge-agent merge --auto # arm auto-merge instead
|
|
109
|
-
forge-agent resolution-check # ask the model: did this MR actually fix the issue?
|
|
110
|
-
forge-agent close # confirm + close the issue
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
`<iid>`-less commands resolve via the **HEAD pointer** — the most recently touched session. Like `git`'s HEAD: whatever you analysed last is what `show`/`validate`/`apply`/`watch`/`merge`/`close` operate on unless you pass an explicit `<iid>`.
|
|
114
|
-
|
|
115
|
-
---
|
|
116
|
-
|
|
117
|
-
## Sessions (`~/.forge/sessions/`)
|
|
118
|
-
|
|
119
|
-
Each issue you work on gets a directory:
|
|
120
|
-
|
|
121
|
-
```
|
|
122
|
-
~/.forge/sessions/<repo>/<iid>/
|
|
123
|
-
analysis.json last cached analyzeIssue result
|
|
124
|
-
state.json { phase, branch, mrIid, mrUrl, lastPipelineStatus, ... }
|
|
125
|
-
events.ndjson append-only audit log
|
|
126
|
-
~/.forge/sessions/HEAD "<repo>/<iid>" — the most recently touched session
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
Inspect and clean:
|
|
130
|
-
|
|
131
|
-
```bash
|
|
132
|
-
forge-agent session list # table of all sessions, HEAD marked *
|
|
133
|
-
forge-agent session list --json
|
|
134
|
-
forge-agent session show # full state of HEAD
|
|
135
|
-
forge-agent session show 42 # ...for a specific issue
|
|
136
|
-
forge-agent session clean 42 # remove one session
|
|
137
|
-
forge-agent session clean --all # wipe everything
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
Override the location with `FORGE_SESSIONS_PATH` (handy for CI or test isolation).
|
|
141
|
-
|
|
142
|
-
---
|
|
143
|
-
|
|
144
|
-
## Common workflows
|
|
145
|
-
|
|
146
|
-
### Ship a small fix, fully unattended
|
|
147
|
-
|
|
148
|
-
```bash
|
|
149
|
-
forge-agent ship 42 --yes
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-
Don't do this on prod the first time. Watch a couple of runs interactively first.
|
|
153
|
-
|
|
154
|
-
### Drive review explicitly
|
|
155
|
-
|
|
156
|
-
```bash
|
|
157
|
-
forge-agent analyze 42 --all-repos # produce
|
|
158
|
-
forge-agent show # review on the same screen later
|
|
159
|
-
forge-agent refine --feedback "..." # iterate
|
|
160
|
-
forge-agent ship 42 # pick up where you left off
|
|
161
|
-
```
|
|
162
|
-
|
|
163
|
-
### Run from CI
|
|
164
|
-
|
|
165
|
-
```bash
|
|
166
|
-
FORGE_YES=1 forge-agent ship 42
|
|
167
|
-
```
|
|
168
|
-
|
|
169
|
-
`--json` is available on `analyze`, `issues`, `issue`, `repos list`, `session list/show`, `watch`, and `validate` — pipe-safe outputs that bypass Ink.
|
|
170
|
-
|
|
171
|
-
### Recover from a failed step
|
|
172
|
-
|
|
173
|
-
Just re-run the same command. State is on disk; it picks up where it left off.
|
|
174
|
-
|
|
175
|
-
```bash
|
|
176
|
-
forge-agent ship 42 # if it died mid-flow, this resumes
|
|
177
|
-
forge-agent ci-fix # if CI failed, fix it without re-running ship
|
|
178
|
-
```
|
|
179
|
-
|
|
180
|
-
---
|
|
181
|
-
|
|
182
|
-
## Doctor & troubleshooting
|
|
183
|
-
|
|
184
|
-
```bash
|
|
185
|
-
forge-agent doctor
|
|
186
|
-
```
|
|
187
|
-
|
|
188
|
-
Sequential checklist — `node`, `git`, `claude`, `FORGE_TOKEN`, config file, frontend build. Exits non-zero if any check fails.
|
|
189
|
-
|
|
190
|
-
If `analyze` is timing out (5-minute Claude CLI cap), narrow the scope:
|
|
191
|
-
|
|
192
|
-
```bash
|
|
193
|
-
forge-agent analyze 42 --secondary group/api # explicit secondary instead of --all-repos
|
|
194
|
-
```
|
|
195
|
-
|
|
196
|
-
If `apply` says "no repos to validate against", check `forge-agent repos list` and your session's `primary` field.
|
|
197
|
-
|
|
198
|
-
---
|
|
199
|
-
|
|
200
|
-
## Reference
|
|
201
|
-
|
|
202
|
-
- [`docs/cli.md`](../docs/cli.md) — full command reference, every flag
|
|
203
|
-
- [`docs/cli-ship-workflow.md`](../docs/cli-ship-workflow.md) — design + status of the end-to-end ship workflow (per-phase delivery, decisions, risks)
|
|
204
|
-
- [`../README.md`](../README.md) — the broader Forge project (web UI, server, multi-provider details)
|
|
205
|
-
|
|
206
|
-
---
|
|
207
|
-
|
|
208
|
-
## License
|
|
209
|
-
|
|
210
|
-
MIT — same as the rest of Forge.
|