@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 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
@@ -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-agent show`.
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-agent analyze <iid> [options]");
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-agent init`.",
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-agent analyze ${target.iid}`,
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-agent apply ${target.iid}`,
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-agent init)");
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-agent init</>
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-agent init</>
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-agent doctor | forge-agent issues | forge-agent start"}</>
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-agent ship ${issue.iid}`}</>
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-agent issue <iid> [--repo <id>]");
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-agent init`.",
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-agent analyze <iid>"}</>
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-agent init`.",
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-agent apply ${target.iid}`,
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-agent show`}</>
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-agent analyze ${target.iid}`,
196
+ `No cached analysis at ${target.repo}#${target.iid}. Run: forge analyze ${target.iid}`,
197
197
  );
198
198
  process.exit(1);
199
199
  }
@@ -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-agent repos add <group/repo>"}</>
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-agent repos ${sub} <group/repo>`);
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-agent repos add ${repoArg}`);
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);
@@ -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-agent apply ${target.iid}`,
95
+ `Session ${target.repo}#${target.iid} has no MR. Run: forge apply ${target.iid}`,
96
96
  );
97
97
  process.exit(1);
98
98
  }
@@ -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-agent analyze <iid>"}</>
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-agent show [<iid>]` — re-renders the cached analysis from
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-agent analyze ${target.iid}`,
217
+ `Run: forge analyze ${target.iid}`,
218
218
  );
219
219
  process.exit(1);
220
220
  }
@@ -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-agent show → HEAD
200
- * forge-agent show 42 → primary repo + iid 42
201
- * forge-agent show 42 --repo X → repo X + iid 42
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-agent ship <iid>` — the headline command.
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-agent show " + (analysis.issue?.iid ?? "") + ")"}</>
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-agent validate");
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-agent refine` separately to iterate)."}</></>
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-agent ship ${iid}`}</>
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-agent ship ${iid})`}</>
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-agent ship </>
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-agent ship <iid> [options]");
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-agent init`.",
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
+ }
@@ -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-agent analyze ${target.iid}`,
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-agent apply ${target.iid}`,
209
+ `Session ${target.repo}#${target.iid} has no MR yet. Run: forge apply ${target.iid}`,
210
210
  );
211
211
  process.exit(1);
212
212
  }
@@ -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-agent — AI-powered code change & deploy control panel
35
+ forge — AI-powered code change & deploy control panel
25
36
 
26
37
  Usage
27
- forge-agent <command> [options]
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-agent init
71
- forge-agent start --port 5055
72
- forge-agent repos add group/other-repo
73
- forge-agent repos set-default group/other-repo
74
- forge-agent issues --limit 10
75
- forge-agent analyze 42 --repo group/repo
76
- forge-agent analyze 42 --json | jq .
77
- forge-agent doctor
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ipul-labs/forge",
3
- "version": "1.15.1",
3
+ "version": "1.16.1",
4
4
  "description": "AI-powered code change & deploy control panel — connects Claude, GitLab, and Kubernetes",
5
5
  "type": "module",
6
6
  "main": "server/forge/routes.js",
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.