@higherdev/cli 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -4
- package/dist/api.js +18 -0
- package/dist/index.js +87 -36
- package/dist/out.js +4 -3
- package/dist/tui/App.js +88 -5
- package/dist/tui/Help.js +3 -2
- package/dist/tui/data.js +28 -3
- package/dist/tui/parse.js +22 -1
- package/dist/tui/settings-model.js +30 -2
- package/dist/workspace-commands.js +170 -0
- package/dist/workspace-preflight.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -32,13 +32,19 @@ workspace-map config shapes are migrated automatically when they are read.
|
|
|
32
32
|
| `hd ticket show KEY` | Show one ticket |
|
|
33
33
|
| `hd ticket new --title TITLE [--acceptance TEXT] [options]` | Create a ticket |
|
|
34
34
|
| `hd ticket queue KEY` | Queue a complete ticket now |
|
|
35
|
+
| `hd ticket cancel KEY` | Cancel a ticket |
|
|
35
36
|
| `hd epic new PATH [--title TITLE]` | Create an epic from a Markdown spec |
|
|
36
37
|
| `hd epic list` | List epics and ticket progress |
|
|
37
38
|
| `hd plan [--repo DIR]` | Hand the terminal to Codex to author an epic spec |
|
|
38
39
|
| `hd workspace ls` | List every workspace available to the configured key |
|
|
39
40
|
| `hd workspace new --name NAME --repo OWNER/NAME [options]` | Preflight GitHub, wire the runner, and create a paused workspace |
|
|
41
|
+
| `hd workspace set [options]` | Update settings; max turns uses `--max-turns KIND=N[,KIND=N...]` |
|
|
42
|
+
| `hd workspace rotate-key` | Rotate the shared API key and save it locally |
|
|
40
43
|
| `hd agents` | List agents |
|
|
41
|
-
| `hd agents
|
|
44
|
+
| `hd agents add ROLE --provider P --model M [options]` | Add an agent |
|
|
45
|
+
| `hd agents rm ROLE\|ID` | Remove an unambiguous agent |
|
|
46
|
+
| `hd agents set ROLE\|ID [options]` | Rename, configure, enable, or disable an agent |
|
|
47
|
+
| `hd caps [set PROVIDER N]` | Show or update provider concurrency caps |
|
|
42
48
|
| `hd logs KEY [-f]` | Show or follow run events |
|
|
43
49
|
| `hd msg KEY "message" [--interrupt]` | Message a builder |
|
|
44
50
|
| `hd decide` | List open decisions |
|
|
@@ -50,9 +56,12 @@ workspace-map config shapes are migrated automatically when they are read.
|
|
|
50
56
|
|
|
51
57
|
`hd workspace new` uses the operator's authenticated `gh`, defaults to the repository's real default
|
|
52
58
|
branch, bootstraps an empty repository unless `--no-bootstrap` is set, and invites `mel-ilotus` unless
|
|
53
|
-
`--runner-user USER` overrides it.
|
|
59
|
+
`--runner-user USER` overrides it. On a terminal, missing name or repo flags start a guided wizard. If
|
|
60
|
+
the repository is missing, approve private creation interactively, use `--create` to force it, or
|
|
61
|
+
`--no-create` to fail. Fully flagged calls remain non-interactive for scripts and Mel.
|
|
54
62
|
|
|
55
|
-
Inside the TUI, use `/board`, `/inbox`, `/ticket`, `/queue`, `/epic new`,
|
|
56
|
-
`/epics`, `/plan`, `/decide`, `/agents`, `/
|
|
63
|
+
Inside the TUI, use `/board`, `/inbox`, `/ticket`, `/queue`, `/cancel`, `/epic new`,
|
|
64
|
+
`/epics`, `/plan`, `/decide`, `/agents add`, `/agents rm`, `/settings`, `/workspace`,
|
|
65
|
+
`/workspace new`, `/workspace set`, `/workspace rotate-key`, `/feed`,
|
|
57
66
|
`/orchestrator`, `/refresh`, `/help`, or `/exit`. The display refreshes from
|
|
58
67
|
the HDX API every five seconds.
|
package/dist/api.js
CHANGED
|
@@ -55,6 +55,9 @@ export async function createTicket(fields, config = loadConfig()) {
|
|
|
55
55
|
export async function queueTicket(key, config = loadConfig()) {
|
|
56
56
|
return request(config, "POST", `/api/w/${config.slug}/tickets/${encodeURIComponent(key)}/queue`, undefined, true);
|
|
57
57
|
}
|
|
58
|
+
export async function cancelTicket(key, config = loadConfig()) {
|
|
59
|
+
return request(config, "POST", `/api/w/${config.slug}/tickets/${encodeURIComponent(key)}/cancel`);
|
|
60
|
+
}
|
|
58
61
|
export async function listTicketEvents(key, afterAt, afterId, config = loadConfig()) {
|
|
59
62
|
const query = new URLSearchParams();
|
|
60
63
|
if (afterAt)
|
|
@@ -81,12 +84,27 @@ export async function createWorkspace(fields, config = loadConfig()) {
|
|
|
81
84
|
export async function listWorkspaces(config = loadConfig()) {
|
|
82
85
|
return request(config, "GET", "/api/workspaces");
|
|
83
86
|
}
|
|
87
|
+
export async function getWorkspace(config = loadConfig()) {
|
|
88
|
+
return request(config, "GET", `/api/w/${config.slug}`);
|
|
89
|
+
}
|
|
90
|
+
export async function updateWorkspace(fields, config = loadConfig()) {
|
|
91
|
+
return request(config, "PATCH", `/api/w/${config.slug}`, fields);
|
|
92
|
+
}
|
|
93
|
+
export async function rotateWorkspaceApiKey(config = loadConfig()) {
|
|
94
|
+
return request(config, "POST", `/api/w/${config.slug}/api-key`);
|
|
95
|
+
}
|
|
84
96
|
export async function listAgents(config = loadConfig()) {
|
|
85
97
|
return request(config, "GET", `/api/w/${config.slug}/agents`);
|
|
86
98
|
}
|
|
99
|
+
export async function createAgent(fields, config = loadConfig()) {
|
|
100
|
+
return request(config, "POST", `/api/w/${config.slug}/agents`, fields);
|
|
101
|
+
}
|
|
87
102
|
export async function updateAgent(id, fields, config = loadConfig()) {
|
|
88
103
|
return request(config, "PATCH", `/api/w/${config.slug}/agents`, { id, ...fields });
|
|
89
104
|
}
|
|
105
|
+
export async function deleteAgent(id, config = loadConfig()) {
|
|
106
|
+
return request(config, "DELETE", `/api/w/${config.slug}/agents`, { id });
|
|
107
|
+
}
|
|
90
108
|
export async function listEpics(config = loadConfig()) {
|
|
91
109
|
return request(config, "GET", `/api/w/${config.slug}/epics`);
|
|
92
110
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { realpathSync } from "node:fs";
|
|
3
3
|
import { pathToFileURL } from "node:url";
|
|
4
|
-
import { answerDecision, createEpic, createTicket,
|
|
4
|
+
import { answerDecision, cancelTicket, createAgent, createEpic, createTicket, deleteAgent, getStatus, listAgents, listEpics, listTicketEvents, listTickets, listWorkspaces, postMessage, queueTicket, setPaused, showTicket, updateAgent, updateCaps, } from "./api.js";
|
|
5
5
|
import { initHost, parseHostFlags } from "./host.js";
|
|
6
|
-
import { loadConfig
|
|
6
|
+
import { loadConfig } from "./config.js";
|
|
7
7
|
import { epicProgressRows, readEpicSpec } from "./epics.js";
|
|
8
8
|
import { banner, c, statusChip, table, truncate, usage } from "./out.js";
|
|
9
9
|
import { launchArchitect } from "./plan.js";
|
|
10
|
-
import {
|
|
10
|
+
import { WORKSPACE_USAGE, workspaceNew, workspaceRotateKey, workspaceSet } from "./workspace-commands.js";
|
|
11
11
|
function fail(message) {
|
|
12
12
|
console.error(message);
|
|
13
13
|
process.exit(1);
|
|
@@ -119,7 +119,15 @@ async function cmdTicket(argv) {
|
|
|
119
119
|
console.log(`${c.bold(ticket.key)} ${statusChip(ticket.status)}`);
|
|
120
120
|
return;
|
|
121
121
|
}
|
|
122
|
-
|
|
122
|
+
if (action === "cancel") {
|
|
123
|
+
const key = rest[0];
|
|
124
|
+
if (!key)
|
|
125
|
+
fail("usage: hd ticket cancel KEY");
|
|
126
|
+
const { ticket } = await cancelTicket(key.toUpperCase());
|
|
127
|
+
console.log(`${c.bold(ticket.key)} ${statusChip(ticket.status)}`);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
fail("usage: hd ticket list | show KEY | new --title TITLE | queue KEY | cancel KEY");
|
|
123
131
|
}
|
|
124
132
|
async function cmdEpic(argv) {
|
|
125
133
|
const [action, ...rest] = argv;
|
|
@@ -233,30 +241,36 @@ async function cmdWorkspace(argv, deps = {}) {
|
|
|
233
241
|
])));
|
|
234
242
|
return;
|
|
235
243
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
244
|
+
if (action === "rotate-key") {
|
|
245
|
+
if (rest.length)
|
|
246
|
+
fail(WORKSPACE_USAGE);
|
|
247
|
+
const result = await workspaceRotateKey();
|
|
248
|
+
console.log(`api key: ${result.apiKey}`);
|
|
249
|
+
console.log("saved locally; update Mel's hd init configuration on the box.");
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
if (action === "set") {
|
|
253
|
+
const workspace = await workspaceSet(rest);
|
|
254
|
+
console.log(`${workspace.slug} ${workspace.name} ${workspace.repo}`);
|
|
255
|
+
return;
|
|
242
256
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
if (
|
|
247
|
-
console.log(`invitation pending for ${
|
|
248
|
-
|
|
249
|
-
default_branch: preflight.branch, default_host: opts.host ?? "box" });
|
|
250
|
-
console.log(`workspace: ${result.workspace.slug}`);
|
|
251
|
-
writeHdConfig({ url: result.url, api_key: result.api_key, slug: result.workspace.slug });
|
|
257
|
+
if (action !== "new")
|
|
258
|
+
fail(WORKSPACE_USAGE);
|
|
259
|
+
const result = await workspaceNew(rest, deps);
|
|
260
|
+
if (result.invitationPending)
|
|
261
|
+
console.log(`invitation pending for ${result.runnerUser}`);
|
|
262
|
+
console.log(`workspace: ${result.slug}`);
|
|
252
263
|
console.log("saved and switched; configure another machine with:");
|
|
253
|
-
console.log(
|
|
264
|
+
console.log(result.initCommand);
|
|
254
265
|
}
|
|
255
|
-
function selectAgent(agents,
|
|
256
|
-
const
|
|
266
|
+
function selectAgent(agents, target, provider) {
|
|
267
|
+
const exact = agents.find((agent) => agent.id === target);
|
|
268
|
+
if (exact)
|
|
269
|
+
return exact;
|
|
270
|
+
const matches = agents.filter((agent) => agent.role === target);
|
|
257
271
|
const selected = matches.length === 1 ? matches[0] : matches.find((agent) => agent.provider === provider);
|
|
258
272
|
if (!selected)
|
|
259
|
-
fail(`No unambiguous ${
|
|
273
|
+
fail(`No unambiguous agent ${target}${provider ? ` for provider ${provider}` : ""}.`);
|
|
260
274
|
return selected;
|
|
261
275
|
}
|
|
262
276
|
async function cmdAgents(argv) {
|
|
@@ -267,24 +281,57 @@ async function cmdAgents(argv) {
|
|
|
267
281
|
console.log("No agents.");
|
|
268
282
|
return;
|
|
269
283
|
}
|
|
270
|
-
console.log(table(["ROLE", "PROVIDER", "MODEL", "EFFORT", "STATE"], agents.map((agent) => [
|
|
271
|
-
agent.
|
|
284
|
+
console.log(table(["ID", "NAME", "ROLE", "PROVIDER", "MODEL", "EFFORT", "STATE"], agents.map((agent) => [
|
|
285
|
+
agent.id, agent.display_name, agent.role, agent.provider, agent.model, agent.effort,
|
|
286
|
+
agent.enabled ? c.green("on") : c.dim("off"),
|
|
272
287
|
])));
|
|
273
288
|
return;
|
|
274
289
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
290
|
+
const agentUsage = "usage: hd agents add ROLE --provider P --model M [--effort E] [--name N] | rm ROLE|ID | set ROLE|ID [flags]";
|
|
291
|
+
const { rest: args, opts, bools } = flags(rest);
|
|
292
|
+
const target = args[0];
|
|
293
|
+
if (action === "add") {
|
|
294
|
+
if (!target || !opts.provider || !opts.model)
|
|
295
|
+
fail(agentUsage);
|
|
296
|
+
const { agent } = await createAgent({ role: target, provider: opts.provider, model: opts.model,
|
|
297
|
+
effort: opts.effort, display_name: opts.name ?? opts.provider });
|
|
298
|
+
console.log(`${agent.id} ${agent.display_name} ${agent.role} ${agent.provider}`);
|
|
299
|
+
return;
|
|
281
300
|
}
|
|
301
|
+
if (!target)
|
|
302
|
+
fail(agentUsage);
|
|
282
303
|
const { agents } = await listAgents();
|
|
283
|
-
const current = selectAgent(agents,
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
304
|
+
const current = selectAgent(agents, target, opts.provider);
|
|
305
|
+
if (action === "rm") {
|
|
306
|
+
await deleteAgent(current.id);
|
|
307
|
+
console.log(`removed ${current.display_name} (${current.id})`);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
if (action !== "set" || (bools.has("on") && bools.has("off")))
|
|
311
|
+
fail(agentUsage);
|
|
312
|
+
const fields = {
|
|
313
|
+
...(opts.provider ? { provider: opts.provider } : {}), ...(opts.model ? { model: opts.model } : {}),
|
|
314
|
+
...(opts.effort ? { effort: opts.effort } : {}), ...(opts.name ? { display_name: opts.name } : {}),
|
|
315
|
+
...(bools.has("on") || bools.has("off") ? { enabled: bools.has("on") } : {}),
|
|
316
|
+
};
|
|
317
|
+
if (!Object.keys(fields).length)
|
|
318
|
+
fail(agentUsage);
|
|
319
|
+
const { agent } = await updateAgent(current.id, fields);
|
|
320
|
+
console.log(`${agent.display_name} ${agent.role} ${agent.provider} ${agent.model} ${agent.enabled ? "on" : "off"}`);
|
|
321
|
+
}
|
|
322
|
+
async function cmdCaps(argv) {
|
|
323
|
+
const [action, provider, raw, ...extra] = argv;
|
|
324
|
+
if (!action) {
|
|
325
|
+
const caps = (await getStatus()).workspace.provider_caps;
|
|
326
|
+
console.log(table(["PROVIDER", "CAP"], Object.entries(caps).sort().map(([name, cap]) => [name, String(cap)])));
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
const cap = Number(raw);
|
|
330
|
+
if (action !== "set" || !provider || extra.length || !Number.isInteger(cap) || cap < 0) {
|
|
331
|
+
fail("usage: hd caps | hd caps set PROVIDER N");
|
|
332
|
+
}
|
|
333
|
+
const result = await updateCaps({ [provider]: cap });
|
|
334
|
+
console.log(`${provider} ${result.provider_caps[provider]}`);
|
|
288
335
|
}
|
|
289
336
|
export async function main(argv = process.argv.slice(2), deps = {}) {
|
|
290
337
|
const [cmd, ...rest] = argv;
|
|
@@ -343,6 +390,10 @@ export async function main(argv = process.argv.slice(2), deps = {}) {
|
|
|
343
390
|
await cmdAgents(rest);
|
|
344
391
|
return;
|
|
345
392
|
}
|
|
393
|
+
if (cmd === "caps") {
|
|
394
|
+
await cmdCaps(rest);
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
346
397
|
if (cmd === "pause" || cmd === "off") {
|
|
347
398
|
await setPaused(true);
|
|
348
399
|
console.log(cmd === "off" ? "off" : "paused");
|
package/dist/out.js
CHANGED
|
@@ -64,11 +64,12 @@ export function usage() {
|
|
|
64
64
|
return [
|
|
65
65
|
c.bold("Usage"),
|
|
66
66
|
` ${c.blue("hd status")} workspace overview`,
|
|
67
|
-
` ${c.blue("hd ticket list | show | new | queue")} ticket operations`,
|
|
67
|
+
` ${c.blue("hd ticket list | show | new | queue | cancel")} ticket operations`,
|
|
68
68
|
` ${c.blue("hd epic new PATH | list")} epic operations`,
|
|
69
69
|
` ${c.blue("hd plan [--repo DIR]")} author an epic with Codex`,
|
|
70
|
-
` ${c.blue("hd workspace ls | new")}
|
|
71
|
-
` ${c.blue("hd agents [set]")}
|
|
70
|
+
` ${c.blue("hd workspace ls | new | set | rotate-key")} workspace operations`,
|
|
71
|
+
` ${c.blue("hd agents [add | rm | set]")} manage agents`,
|
|
72
|
+
` ${c.blue("hd caps [set PROVIDER N]")} provider concurrency`,
|
|
72
73
|
` ${c.blue("hd logs KEY [-f]")} run events`,
|
|
73
74
|
` ${c.blue("hd msg KEY TEXT")} message a builder`,
|
|
74
75
|
` ${c.blue("hd decide [ID --answer TEXT]")} decisions`,
|
package/dist/tui/App.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
3
|
import { Box, Static, Text, useApp, useInput, useStdout } from "ink";
|
|
4
|
+
import { loadConfig } from "../config.js";
|
|
4
5
|
import { epicProgressRows } from "../epics.js";
|
|
5
6
|
import { launchArchitect } from "../plan.js";
|
|
7
|
+
import { workspaceNew, workspaceRotateKey, workspaceSet } from "../workspace-commands.js";
|
|
6
8
|
import { Banner } from "./Banner.js";
|
|
7
9
|
import { Bubble } from "./Bubble.js";
|
|
8
10
|
import { Cockpit, StreamPanel, boardTicketIds, nextCursor } from "./Dashboard.js";
|
|
@@ -16,7 +18,7 @@ import { alertOnce } from "./alert.js";
|
|
|
16
18
|
import { bubbleRows } from "./height.js";
|
|
17
19
|
import { planLayout, splitPanels } from "./layout.js";
|
|
18
20
|
import { parseLine } from "./parse.js";
|
|
19
|
-
import { configuredSlugs, createEpicFromFile, decisionOptions, loadLiveEvents, loadTicketDetail, pollSnapshot, postOrchestrator, queueTicket, resolveDecision, switchWorkspace, updateAgent, updateProviderCap, } from "./data.js";
|
|
21
|
+
import { configuredSlugs, cancelTicket, createAgent, createEpicFromFile, decisionOptions, deleteAgent, loadLiveEvents, loadTicketDetail, pollSnapshot, postOrchestrator, queueTicket, resolveDecision, switchWorkspace, updateAgent, updateProviderCap, updateWorkspace, } from "./data.js";
|
|
20
22
|
import { editFor, editableKeys, nextValue, seedFor, settingsRows } from "./settings-model.js";
|
|
21
23
|
import { appendLines, runLabels, toStreamLines } from "./stream.js";
|
|
22
24
|
import { UI } from "./theme.js";
|
|
@@ -149,9 +151,12 @@ export function App({ initial }) {
|
|
|
149
151
|
if (edit.value.target === "cap") {
|
|
150
152
|
await updateProviderCap(config, edit.value.provider, edit.value.cap);
|
|
151
153
|
}
|
|
152
|
-
else {
|
|
154
|
+
else if (edit.value.target === "agent") {
|
|
153
155
|
await updateAgent(config, edit.value.id, edit.value.fields);
|
|
154
156
|
}
|
|
157
|
+
else {
|
|
158
|
+
await updateWorkspace(config, edit.value.fields);
|
|
159
|
+
}
|
|
155
160
|
setNotice(null);
|
|
156
161
|
await refresh();
|
|
157
162
|
}
|
|
@@ -162,10 +167,10 @@ export function App({ initial }) {
|
|
|
162
167
|
setBusy(false);
|
|
163
168
|
}
|
|
164
169
|
}, [settings, config, refresh]);
|
|
165
|
-
const changeWorkspace = useCallback(async (slug) => {
|
|
170
|
+
const changeWorkspace = useCallback(async (slug, source = config) => {
|
|
166
171
|
setBusy(true);
|
|
167
172
|
try {
|
|
168
|
-
const snapshot = await switchWorkspace(slug,
|
|
173
|
+
const snapshot = await switchWorkspace(slug, source);
|
|
169
174
|
loads.current.switchTo(snapshot.workspace.id);
|
|
170
175
|
setConfig(snapshot.config);
|
|
171
176
|
setWorkspace(snapshot.workspace);
|
|
@@ -185,7 +190,7 @@ export function App({ initial }) {
|
|
|
185
190
|
finally {
|
|
186
191
|
setBusy(false);
|
|
187
192
|
}
|
|
188
|
-
}, [say]);
|
|
193
|
+
}, [config, say]);
|
|
189
194
|
const askOrchestrator = useCallback(async (text) => {
|
|
190
195
|
setBusy(true);
|
|
191
196
|
const id = nextId();
|
|
@@ -295,6 +300,35 @@ export function App({ initial }) {
|
|
|
295
300
|
}
|
|
296
301
|
}
|
|
297
302
|
return;
|
|
303
|
+
case "workspace-command":
|
|
304
|
+
setBusy(true);
|
|
305
|
+
try {
|
|
306
|
+
if (action.command === "new") {
|
|
307
|
+
let created;
|
|
308
|
+
await suspendTerminal(async () => { created = await workspaceNew(action.args); });
|
|
309
|
+
if (!created)
|
|
310
|
+
throw new Error("Workspace wizard did not finish.");
|
|
311
|
+
await changeWorkspace(created.slug, loadConfig());
|
|
312
|
+
say("system", `Created ${created.slug}. Configure the box with: ${created.initCommand}`);
|
|
313
|
+
}
|
|
314
|
+
else if (action.command === "set") {
|
|
315
|
+
const updated = await workspaceSet(action.args);
|
|
316
|
+
say("system", `Updated ${updated.slug}: ${updated.name} (${updated.repo}).`);
|
|
317
|
+
await refresh();
|
|
318
|
+
}
|
|
319
|
+
else {
|
|
320
|
+
const rotated = await workspaceRotateKey();
|
|
321
|
+
setConfig(loadConfig());
|
|
322
|
+
say("system", `API key: ${rotated.apiKey}\nSaved locally. Update Mel's hd init configuration on the box.`);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
catch (error) {
|
|
326
|
+
setNotice(error instanceof Error ? error.message : String(error));
|
|
327
|
+
}
|
|
328
|
+
finally {
|
|
329
|
+
setBusy(false);
|
|
330
|
+
}
|
|
331
|
+
return;
|
|
298
332
|
case "ticket":
|
|
299
333
|
setTicketKey(action.key);
|
|
300
334
|
setView("ticket");
|
|
@@ -348,6 +382,55 @@ export function App({ initial }) {
|
|
|
348
382
|
setBusy(false);
|
|
349
383
|
}
|
|
350
384
|
return;
|
|
385
|
+
case "cancel":
|
|
386
|
+
setBusy(true);
|
|
387
|
+
try {
|
|
388
|
+
const { ticket: cancelled } = await cancelTicket(config, action.key);
|
|
389
|
+
say("system", `${cancelled.key} cancelled.`);
|
|
390
|
+
await refresh();
|
|
391
|
+
}
|
|
392
|
+
catch (error) {
|
|
393
|
+
setNotice(error instanceof Error ? error.message : String(error));
|
|
394
|
+
}
|
|
395
|
+
finally {
|
|
396
|
+
setBusy(false);
|
|
397
|
+
}
|
|
398
|
+
return;
|
|
399
|
+
case "agent-add":
|
|
400
|
+
setBusy(true);
|
|
401
|
+
try {
|
|
402
|
+
const { agent } = await createAgent(config, { role: action.role, provider: action.provider,
|
|
403
|
+
model: action.model, effort: action.effort, display_name: action.name ?? action.provider });
|
|
404
|
+
say("system", `Added ${agent.display_name} (${agent.id}).`);
|
|
405
|
+
await refresh();
|
|
406
|
+
}
|
|
407
|
+
catch (error) {
|
|
408
|
+
setNotice(error instanceof Error ? error.message : String(error));
|
|
409
|
+
}
|
|
410
|
+
finally {
|
|
411
|
+
setBusy(false);
|
|
412
|
+
}
|
|
413
|
+
return;
|
|
414
|
+
case "agent-rm": {
|
|
415
|
+
const matches = board.agents.filter((agent) => agent.id === action.target || agent.role === action.target);
|
|
416
|
+
if (matches.length !== 1) {
|
|
417
|
+
setNotice(`Agent ${action.target} is missing or ambiguous; use its ID.`);
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
setBusy(true);
|
|
421
|
+
try {
|
|
422
|
+
await deleteAgent(config, matches[0].id);
|
|
423
|
+
say("system", `Removed ${matches[0].display_name}.`);
|
|
424
|
+
await refresh();
|
|
425
|
+
}
|
|
426
|
+
catch (error) {
|
|
427
|
+
setNotice(error instanceof Error ? error.message : String(error));
|
|
428
|
+
}
|
|
429
|
+
finally {
|
|
430
|
+
setBusy(false);
|
|
431
|
+
}
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
351
434
|
case "plan":
|
|
352
435
|
setBusy(true);
|
|
353
436
|
try {
|
package/dist/tui/Help.js
CHANGED
|
@@ -9,13 +9,14 @@ export const COMMANDS = [
|
|
|
9
9
|
{ name: "/inbox", help: "decisions and messages waiting on you" },
|
|
10
10
|
{ name: "/ticket", args: "HD-12", help: "open one ticket" },
|
|
11
11
|
{ name: "/queue", args: "HD-12", help: "queue a complete ticket now" },
|
|
12
|
+
{ name: "/cancel", args: "HD-12", help: "cancel a ticket" },
|
|
12
13
|
{ name: "/epic", args: "new PATH", help: "create an epic from a Markdown spec" },
|
|
13
14
|
{ name: "/epics", help: "list epics and ticket progress" },
|
|
14
15
|
{ name: "/plan", help: "author an epic with your Codex CLI" },
|
|
15
16
|
{ name: "/decide", args: "2 | text", help: "answer the decision on screen" },
|
|
16
|
-
{ name: "/agents",
|
|
17
|
+
{ name: "/agents", args: "[add ... | rm ROLE|ID]", help: "view or manage agents" },
|
|
17
18
|
{ name: "/settings", help: "change provider caps and agent settings" },
|
|
18
|
-
{ name: "/workspace", args: "[slug]", help: "switch
|
|
19
|
+
{ name: "/workspace", args: "[slug | new | set | rotate-key]", help: "list, switch, create, or configure" },
|
|
19
20
|
{ name: "/feed", help: "what just happened" },
|
|
20
21
|
{ name: "/orchestrator", help: "talk to the agent that gets work in flight finished" },
|
|
21
22
|
{ name: "/refresh", help: "reload the board now" },
|
package/dist/tui/data.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { answerDecision, createEpic as postEpic, getStatus, listAgents, listEpics, listFeed, listMessages, listTicketEvents, listTickets, listWorkspaces, postMessage as sendMessage, queueTicket as queueTicketNow, showTicket, updateAgent as patchAgent, updateCaps, } from "../api.js";
|
|
1
|
+
import { answerDecision, cancelTicket as cancelTicketNow, createAgent as postAgent, createEpic as postEpic, getStatus, getWorkspace, listAgents, listEpics, listFeed, listMessages, listTicketEvents, listTickets, listWorkspaces, postMessage as sendMessage, queueTicket as queueTicketNow, showTicket, updateAgent as patchAgent, updateCaps, updateWorkspace as patchWorkspace, deleteAgent as removeAgent, } from "../api.js";
|
|
2
2
|
import { loadConfig, switchWorkspace as selectWorkspace, } from "../config.js";
|
|
3
3
|
import { readEpicSpec } from "../epics.js";
|
|
4
4
|
export const POLL_MS = 5_000;
|
|
@@ -22,8 +22,9 @@ export async function configuredSlugs(config = loadConfig()) {
|
|
|
22
22
|
return (await listWorkspaces(config)).map((workspace) => workspace.slug).sort();
|
|
23
23
|
}
|
|
24
24
|
export async function loadSnapshot(config = loadConfig()) {
|
|
25
|
-
const [status, ticketData, agentData, epicData, feedData] = await Promise.all([
|
|
25
|
+
const [status, workspaceData, ticketData, agentData, epicData, feedData] = await Promise.all([
|
|
26
26
|
getStatus(config),
|
|
27
|
+
getWorkspace(config),
|
|
27
28
|
listTickets(config),
|
|
28
29
|
listAgents(config),
|
|
29
30
|
listEpics(config),
|
|
@@ -44,7 +45,19 @@ export async function loadSnapshot(config = loadConfig()) {
|
|
|
44
45
|
});
|
|
45
46
|
return {
|
|
46
47
|
config,
|
|
47
|
-
workspace:
|
|
48
|
+
workspace: {
|
|
49
|
+
...status.workspace,
|
|
50
|
+
...workspaceData.workspace,
|
|
51
|
+
paused: status.workspace.paused,
|
|
52
|
+
provider_caps: status.workspace.provider_caps,
|
|
53
|
+
max_turns: {
|
|
54
|
+
build: Number(workspaceData.workspace.settings.max_turns?.build ?? 40),
|
|
55
|
+
followup: Number(workspaceData.workspace.settings.max_turns?.followup ?? 15),
|
|
56
|
+
review: Number(workspaceData.workspace.settings.max_turns?.review ?? 50),
|
|
57
|
+
orchestrate: Number(workspaceData.workspace.settings.max_turns?.orchestrate ?? 30),
|
|
58
|
+
},
|
|
59
|
+
max_attempts: Number(workspaceData.workspace.settings.max_attempts ?? 3),
|
|
60
|
+
},
|
|
48
61
|
board: {
|
|
49
62
|
tickets,
|
|
50
63
|
agents: agentData.agents,
|
|
@@ -105,6 +118,15 @@ export async function createEpicFromFile(config, path) {
|
|
|
105
118
|
export async function queueTicket(config, key) {
|
|
106
119
|
return queueTicketNow(key, config);
|
|
107
120
|
}
|
|
121
|
+
export async function cancelTicket(config, key) {
|
|
122
|
+
return cancelTicketNow(key, config);
|
|
123
|
+
}
|
|
124
|
+
export async function createAgent(config, fields) {
|
|
125
|
+
return postAgent(fields, config);
|
|
126
|
+
}
|
|
127
|
+
export async function deleteAgent(config, id) {
|
|
128
|
+
return removeAgent(id, config);
|
|
129
|
+
}
|
|
108
130
|
export async function waitForReply(config, since, timeoutMs) {
|
|
109
131
|
const deadline = Date.now() + timeoutMs;
|
|
110
132
|
while (Date.now() <= deadline) {
|
|
@@ -126,6 +148,9 @@ export async function updateAgent(config, id, fields) {
|
|
|
126
148
|
export async function updateProviderCap(config, provider, cap) {
|
|
127
149
|
return updateCaps({ [provider]: cap }, config);
|
|
128
150
|
}
|
|
151
|
+
export async function updateWorkspace(config, fields) {
|
|
152
|
+
return patchWorkspace(fields, config);
|
|
153
|
+
}
|
|
129
154
|
export async function loadLiveEvents(config, board) {
|
|
130
155
|
const liveIds = new Set(board.runs.filter((run) => run.status === "running").map((run) => run.id));
|
|
131
156
|
const keys = new Set(board.runs
|
package/dist/tui/parse.js
CHANGED
|
@@ -14,15 +14,33 @@ export function parseLine(raw) {
|
|
|
14
14
|
case "orchestrator":
|
|
15
15
|
return { kind: "mode", mode: "orchestrator" };
|
|
16
16
|
case "board":
|
|
17
|
-
case "agents":
|
|
18
17
|
case "feed":
|
|
19
18
|
case "inbox":
|
|
20
19
|
case "settings":
|
|
21
20
|
return { kind: "view", view: word.toLowerCase() };
|
|
21
|
+
case "agents": {
|
|
22
|
+
if (!argument)
|
|
23
|
+
return { kind: "view", view: "agents" };
|
|
24
|
+
const [verb, target, ...tail] = rest;
|
|
25
|
+
if (verb === "rm" && target && !tail.length)
|
|
26
|
+
return { kind: "agent-rm", target };
|
|
27
|
+
const opts = {};
|
|
28
|
+
for (let i = 0; i < tail.length; i += 2)
|
|
29
|
+
if (tail[i]?.startsWith("--") && tail[i + 1])
|
|
30
|
+
opts[tail[i].slice(2)] = tail[i + 1];
|
|
31
|
+
return verb === "add" && target && opts.provider && opts.model
|
|
32
|
+
? { kind: "agent-add", role: target, provider: opts.provider, model: opts.model,
|
|
33
|
+
...(opts.effort ? { effort: opts.effort } : {}), ...(opts.name ? { name: opts.name } : {}) }
|
|
34
|
+
: { kind: "unknown", command: "agents needs add ROLE --provider P --model M or rm ROLE|ID" };
|
|
35
|
+
}
|
|
22
36
|
case "help":
|
|
23
37
|
return { kind: "help" };
|
|
24
38
|
case "workspace":
|
|
25
39
|
case "ws": {
|
|
40
|
+
const command = rest[0]?.toLowerCase();
|
|
41
|
+
if (command && ["new", "set", "rotate-key"].includes(command)) {
|
|
42
|
+
return { kind: "workspace-command", command: command, args: rest.slice(1) };
|
|
43
|
+
}
|
|
26
44
|
return { kind: "workspace", slug: argument || null };
|
|
27
45
|
}
|
|
28
46
|
case "ticket":
|
|
@@ -39,6 +57,9 @@ export function parseLine(raw) {
|
|
|
39
57
|
return argument
|
|
40
58
|
? { kind: "queue", key: argument.toUpperCase() }
|
|
41
59
|
: { kind: "unknown", command: "queue needs a key" };
|
|
60
|
+
case "cancel":
|
|
61
|
+
return argument ? { kind: "cancel", key: argument.toUpperCase() }
|
|
62
|
+
: { kind: "unknown", command: "cancel needs a key" };
|
|
42
63
|
case "plan":
|
|
43
64
|
return argument ? { kind: "unknown", command: "plan takes no arguments" } : { kind: "plan" };
|
|
44
65
|
case "decide": {
|
|
@@ -2,8 +2,16 @@ import { efforts, providers } from "./data.js";
|
|
|
2
2
|
export function settingsRows(workspace, agents) {
|
|
3
3
|
const rows = [
|
|
4
4
|
{ key: "h:workspace", kind: "heading", label: workspace.slug, value: workspace.repo },
|
|
5
|
-
{ key: "w:
|
|
6
|
-
{ key: "w:
|
|
5
|
+
{ key: "w:name", kind: "text", label: "name", value: workspace.name },
|
|
6
|
+
{ key: "w:repo", kind: "text", label: "repo", value: workspace.repo, hint: "owner/name" },
|
|
7
|
+
{ key: "w:default_branch", kind: "text", label: "branch", value: workspace.default_branch },
|
|
8
|
+
{ key: "w:default_host", kind: "text", label: "host", value: workspace.default_host },
|
|
9
|
+
{ key: "w:auto_merge", kind: "toggle", label: "auto merge", value: workspace.auto_merge ? "yes" : "no" },
|
|
10
|
+
...["build", "followup", "review", "orchestrate"].map((kind) => ({
|
|
11
|
+
key: `w:max_turns:${kind}`, kind: "number", label: `turns ${kind}`,
|
|
12
|
+
value: String(workspace.max_turns[kind]), hint: "integer >= 1",
|
|
13
|
+
})),
|
|
14
|
+
{ key: "w:max_attempts", kind: "number", label: "max attempts", value: String(workspace.max_attempts) },
|
|
7
15
|
];
|
|
8
16
|
for (const provider of providers) {
|
|
9
17
|
rows.push({
|
|
@@ -16,6 +24,7 @@ export function settingsRows(workspace, agents) {
|
|
|
16
24
|
}
|
|
17
25
|
for (const agent of agents) {
|
|
18
26
|
rows.push({ key: `h:${agent.id}`, kind: "heading", label: agent.display_name, value: agent.role });
|
|
27
|
+
rows.push({ key: `a:${agent.id}:display_name`, kind: "text", label: "name", value: agent.display_name });
|
|
19
28
|
rows.push({
|
|
20
29
|
key: `a:${agent.id}:enabled`, kind: "toggle", label: "enabled",
|
|
21
30
|
value: agent.enabled ? "yes" : "no", agent: agent.display_name,
|
|
@@ -59,6 +68,25 @@ export function editFor(row, raw) {
|
|
|
59
68
|
return { ok: false, error: `${field} cap must be an integer >= 0.` };
|
|
60
69
|
return { ok: true, value: { target: "cap", provider: field, cap } };
|
|
61
70
|
}
|
|
71
|
+
if (scope === "w" && id) {
|
|
72
|
+
if (id === "max_turns" && field) {
|
|
73
|
+
const number = Number(value);
|
|
74
|
+
if (!Number.isInteger(number) || number < 1)
|
|
75
|
+
return { ok: false, error: `max_turns.${field} must be an integer >= 1.` };
|
|
76
|
+
return { ok: true, value: { target: "workspace", fields: { max_turns: { [field]: number } } } };
|
|
77
|
+
}
|
|
78
|
+
if (id === "max_attempts") {
|
|
79
|
+
const number = Number(value);
|
|
80
|
+
if (!Number.isInteger(number) || number < 1)
|
|
81
|
+
return { ok: false, error: "max_attempts must be an integer >= 1." };
|
|
82
|
+
return { ok: true, value: { target: "workspace", fields: { max_attempts: number } } };
|
|
83
|
+
}
|
|
84
|
+
if (id === "auto_merge")
|
|
85
|
+
return { ok: true, value: { target: "workspace", fields: { auto_merge: value === "yes" } } };
|
|
86
|
+
if (!value)
|
|
87
|
+
return { ok: false, error: `${row.label} is required.` };
|
|
88
|
+
return { ok: true, value: { target: "workspace", fields: { [id]: value } } };
|
|
89
|
+
}
|
|
62
90
|
if (scope !== "a" || !id || !field)
|
|
63
91
|
return { ok: false, error: `Nothing to change on ${row.label}.` };
|
|
64
92
|
if (field === "enabled") {
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { createInterface } from "node:readline/promises";
|
|
2
|
+
import { createWorkspace, rotateWorkspaceApiKey, updateWorkspace } from "./api.js";
|
|
3
|
+
import { loadConfig, writeHdConfig } from "./config.js";
|
|
4
|
+
import { defaultGh, HDX_RUNNER_GH_USER, preflightWorkspace } from "./workspace-preflight.js";
|
|
5
|
+
export const WORKSPACE_USAGE = "usage: hd workspace ls | new --name NAME --repo OWNER/NAME [--create|--no-create] [flags] | set [flags] | rotate-key";
|
|
6
|
+
function flags(argv) {
|
|
7
|
+
const opts = {};
|
|
8
|
+
const bools = new Set();
|
|
9
|
+
const rest = [];
|
|
10
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
11
|
+
const arg = argv[i];
|
|
12
|
+
if (!arg.startsWith("--")) {
|
|
13
|
+
rest.push(arg);
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
const next = argv[i + 1];
|
|
17
|
+
if (next && !next.startsWith("-")) {
|
|
18
|
+
opts[arg.slice(2)] = next;
|
|
19
|
+
i += 1;
|
|
20
|
+
}
|
|
21
|
+
else
|
|
22
|
+
bools.add(arg.slice(2));
|
|
23
|
+
}
|
|
24
|
+
return { opts, bools, rest };
|
|
25
|
+
}
|
|
26
|
+
export function workspaceSlug(name) {
|
|
27
|
+
return name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "workspace";
|
|
28
|
+
}
|
|
29
|
+
function yes(answer) {
|
|
30
|
+
return !["n", "no"].includes(answer.trim().toLowerCase());
|
|
31
|
+
}
|
|
32
|
+
function message(error) {
|
|
33
|
+
return error instanceof Error ? error.message : String(error);
|
|
34
|
+
}
|
|
35
|
+
async function repoState(repo, gh) {
|
|
36
|
+
try {
|
|
37
|
+
const value = JSON.parse(await gh(["repo", "view", repo, "--json", "defaultBranchRef"]));
|
|
38
|
+
return { found: true, branch: value.defaultBranchRef?.name };
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
if (/404|not found|could not resolve/i.test(message(error)))
|
|
42
|
+
return { found: false };
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function lazyPrompt(deps) {
|
|
47
|
+
let readline;
|
|
48
|
+
const ask = deps.prompt ?? (async (question) => {
|
|
49
|
+
readline ??= createInterface({ input: process.stdin, output: process.stdout });
|
|
50
|
+
return readline.question(question);
|
|
51
|
+
});
|
|
52
|
+
return { ask, close: () => readline?.close() };
|
|
53
|
+
}
|
|
54
|
+
async function askDefault(ask, label, fallback = "") {
|
|
55
|
+
const answer = (await ask(`${label}${fallback ? ` [${fallback}]` : ""}: `)).trim();
|
|
56
|
+
return answer || fallback;
|
|
57
|
+
}
|
|
58
|
+
export async function workspaceNew(argv, deps = {}) {
|
|
59
|
+
const { opts, bools, rest } = flags(argv);
|
|
60
|
+
if (rest.length || (bools.has("create") && bools.has("no-create")))
|
|
61
|
+
throw new Error(WORKSPACE_USAGE);
|
|
62
|
+
const interactive = deps.isTTY ?? Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
63
|
+
const guided = !opts.name || !opts.repo;
|
|
64
|
+
if (guided && !interactive)
|
|
65
|
+
throw new Error(WORKSPACE_USAGE);
|
|
66
|
+
const gh = deps.gh ?? defaultGh;
|
|
67
|
+
const prompt = lazyPrompt(deps);
|
|
68
|
+
try {
|
|
69
|
+
let name = opts.name ?? "";
|
|
70
|
+
let repo = opts.repo ?? "";
|
|
71
|
+
let branch = opts.branch;
|
|
72
|
+
let host = opts.host ?? "box";
|
|
73
|
+
if (guided || interactive || bools.has("create") || bools.has("no-create")) {
|
|
74
|
+
try {
|
|
75
|
+
await gh(["--version"]);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
throw new Error("GitHub CLI is unavailable. Install it if needed, then run `gh auth login`.");
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (guided) {
|
|
82
|
+
name = await askDefault(prompt.ask, "Name", name);
|
|
83
|
+
if (!name)
|
|
84
|
+
throw new Error("Name is required.");
|
|
85
|
+
let defaultRepo = repo;
|
|
86
|
+
if (!defaultRepo) {
|
|
87
|
+
const owner = (await gh(["api", "user", "--jq", ".login"])).trim();
|
|
88
|
+
defaultRepo = `${owner}/${workspaceSlug(name)}`;
|
|
89
|
+
}
|
|
90
|
+
repo = await askDefault(prompt.ask, "Repo", defaultRepo);
|
|
91
|
+
if (!repo)
|
|
92
|
+
throw new Error("Repo is required.");
|
|
93
|
+
}
|
|
94
|
+
let state;
|
|
95
|
+
if (guided || interactive || bools.has("create") || bools.has("no-create"))
|
|
96
|
+
state = await repoState(repo, gh);
|
|
97
|
+
if (state && !state.found) {
|
|
98
|
+
if (bools.has("no-create"))
|
|
99
|
+
throw new Error(`GitHub repository ${repo} does not exist and --no-create was set.`);
|
|
100
|
+
const create = bools.has("create") || (interactive && yes(await prompt.ask(`Create ${repo} as a private GitHub repo? [Y/n] `)));
|
|
101
|
+
if (!create)
|
|
102
|
+
throw new Error(`GitHub repository ${repo} was not created.`);
|
|
103
|
+
await gh(["repo", "create", repo, "--private"]);
|
|
104
|
+
state = { found: true, branch: "main" };
|
|
105
|
+
}
|
|
106
|
+
if (guided) {
|
|
107
|
+
branch = await askDefault(prompt.ask, "Branch", branch ?? state?.branch ?? "main");
|
|
108
|
+
host = await askDefault(prompt.ask, "Host", host);
|
|
109
|
+
const summary = `${name} | ${repo} | ${branch} | host ${host}`;
|
|
110
|
+
if (!yes(await prompt.ask(`Create ${summary}. Proceed? [Y/n] `)))
|
|
111
|
+
throw new Error("Workspace creation cancelled.");
|
|
112
|
+
}
|
|
113
|
+
const preflight = await (deps.preflightWorkspace ?? preflightWorkspace)({
|
|
114
|
+
name, repo, branch, noBootstrap: bools.has("no-bootstrap"),
|
|
115
|
+
runnerUser: opts["runner-user"] ?? HDX_RUNNER_GH_USER,
|
|
116
|
+
}, gh);
|
|
117
|
+
const result = await createWorkspace({ name, repo, slug: opts.slug,
|
|
118
|
+
default_branch: preflight.branch, default_host: host });
|
|
119
|
+
writeHdConfig({ url: result.url, api_key: result.api_key, slug: result.workspace.slug });
|
|
120
|
+
return { slug: result.workspace.slug, repo: result.workspace.repo,
|
|
121
|
+
invitationPending: preflight.invitationPending,
|
|
122
|
+
runnerUser: opts["runner-user"] ?? HDX_RUNNER_GH_USER,
|
|
123
|
+
initCommand: `hd init --url ${result.url} --api-key ${result.api_key} --slug ${result.workspace.slug}` };
|
|
124
|
+
}
|
|
125
|
+
finally {
|
|
126
|
+
prompt.close();
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
export async function workspaceSet(argv) {
|
|
130
|
+
const { opts, bools, rest } = flags(argv);
|
|
131
|
+
if (rest.length || bools.size)
|
|
132
|
+
throw new Error(WORKSPACE_USAGE);
|
|
133
|
+
const fields = {};
|
|
134
|
+
for (const [flag, field] of [["name", "name"], ["repo", "repo"], ["branch", "default_branch"], ["host", "default_host"]]) {
|
|
135
|
+
if (opts[flag])
|
|
136
|
+
fields[field] = opts[flag];
|
|
137
|
+
}
|
|
138
|
+
if (opts["auto-merge"]) {
|
|
139
|
+
if (!["on", "off"].includes(opts["auto-merge"]))
|
|
140
|
+
throw new Error("--auto-merge must be on or off");
|
|
141
|
+
fields.auto_merge = opts["auto-merge"] === "on";
|
|
142
|
+
}
|
|
143
|
+
if (opts["max-turns"]) {
|
|
144
|
+
const turns = {};
|
|
145
|
+
for (const entry of opts["max-turns"].split(",")) {
|
|
146
|
+
const [kind, raw, ...extra] = entry.split("=");
|
|
147
|
+
const value = Number(raw);
|
|
148
|
+
if (extra.length || !["build", "followup", "review", "orchestrate"].includes(kind)
|
|
149
|
+
|| !Number.isInteger(value) || value < 1)
|
|
150
|
+
throw new Error("--max-turns must be KIND=N[,KIND=N...]");
|
|
151
|
+
turns[kind] = value;
|
|
152
|
+
}
|
|
153
|
+
fields.max_turns = turns;
|
|
154
|
+
}
|
|
155
|
+
if (opts["max-attempts"]) {
|
|
156
|
+
const value = Number(opts["max-attempts"]);
|
|
157
|
+
if (!Number.isInteger(value) || value < 1)
|
|
158
|
+
throw new Error("--max-attempts must be an integer >= 1");
|
|
159
|
+
fields.max_attempts = value;
|
|
160
|
+
}
|
|
161
|
+
if (!Object.keys(fields).length)
|
|
162
|
+
throw new Error(WORKSPACE_USAGE);
|
|
163
|
+
return (await updateWorkspace(fields)).workspace;
|
|
164
|
+
}
|
|
165
|
+
export async function workspaceRotateKey() {
|
|
166
|
+
const config = loadConfig();
|
|
167
|
+
const { api_key } = await rotateWorkspaceApiKey(config);
|
|
168
|
+
writeHdConfig({ ...config, api_key });
|
|
169
|
+
return { apiKey: api_key, slug: config.slug };
|
|
170
|
+
}
|
|
@@ -19,7 +19,7 @@ function errorMessage(error) {
|
|
|
19
19
|
const value = error;
|
|
20
20
|
return value.stderr?.trim() || value.message || String(error);
|
|
21
21
|
}
|
|
22
|
-
async function defaultGh(args) {
|
|
22
|
+
export async function defaultGh(args) {
|
|
23
23
|
try {
|
|
24
24
|
return (await exec("gh", args, { encoding: "utf8", maxBuffer: 1024 * 1024 })).stdout;
|
|
25
25
|
}
|