@higherdev/cli 0.11.1 → 0.12.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 +15 -17
- package/dist/api.js +7 -1
- package/dist/host.js +41 -20
- package/dist/index.js +29 -14
- package/dist/login.js +41 -0
- package/dist/out.js +5 -3
- package/dist/tui/App.js +45 -42
- package/dist/tui/Help.js +5 -2
- package/dist/tui/data.js +11 -5
- package/dist/tui/parse.js +14 -17
- package/dist/tui/theme.js +2 -1
- package/dist/workspace-commands.js +1 -1
- package/package.json +2 -3
- package/dist/plan.js +0 -118
- package/prompts/architect.md +0 -23
package/README.md
CHANGED
|
@@ -8,15 +8,10 @@ Operator controls for HDX workspaces, tickets, agents, and runner hosts.
|
|
|
8
8
|
npm install --global @higherdev/cli
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
Configure this client with an existing workspace API key:
|
|
12
12
|
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
"url": "https://hdx-higher-ops.vercel.app",
|
|
16
|
-
"api_key": "hdx_...",
|
|
17
|
-
"current": "workspace",
|
|
18
|
-
"repos": {}
|
|
19
|
-
}
|
|
13
|
+
```sh
|
|
14
|
+
hd login --url https://hdx-higher-ops.vercel.app --api-key hdx_... --slug workspace
|
|
20
15
|
```
|
|
21
16
|
|
|
22
17
|
One workspace key reaches every workspace. Previous single-workspace and
|
|
@@ -36,7 +31,9 @@ workspace-map config shapes are migrated automatically when they are read.
|
|
|
36
31
|
| `hd ticket cancel KEY` | Cancel a ticket |
|
|
37
32
|
| `hd epic new PATH [--title TITLE]` | Create an epic from a Markdown spec |
|
|
38
33
|
| `hd epic list` | List epics and ticket progress |
|
|
39
|
-
| `hd
|
|
34
|
+
| `hd epic approve ID` | Open a draft epic for orchestrator decomposition |
|
|
35
|
+
| `hd epic rm ID` | Remove a draft epic |
|
|
36
|
+
| `hd plan` | Point to the TUI `/architect` conversation |
|
|
40
37
|
| `hd workspace ls` | List every workspace available to the configured key |
|
|
41
38
|
| `hd workspace new --name NAME --repo OWNER/NAME [options]` | Preflight GitHub, wire the runner, and create a paused workspace |
|
|
42
39
|
| `hd workspace set [options]` | Update settings; max turns uses `--max-turns KIND=N[,KIND=N...]` |
|
|
@@ -52,8 +49,13 @@ workspace-map config shapes are migrated automatically when they are read.
|
|
|
52
49
|
| `hd decide ID --answer TEXT` | Answer a decision |
|
|
53
50
|
| `hd on` / `hd off` | Turn the workspace on or off |
|
|
54
51
|
| `hd pause` / `hd resume` | Pause or resume the workspace |
|
|
55
|
-
| `hd
|
|
56
|
-
| `hd
|
|
52
|
+
| `hd login --url URL --api-key KEY [--slug S]` | Configure only this operator client |
|
|
53
|
+
| `hd init --host HOST [--checkout DIR] [options]` | Configure this host and runner service |
|
|
54
|
+
| `hd upgrade --host HOST [--checkout DIR] [options]` | Refresh this host configuration |
|
|
55
|
+
|
|
56
|
+
`hd init` uses `~/hdx` as its checkout by default. Pass `--checkout DIR` when the runner source is
|
|
57
|
+
elsewhere. If the runner environment or service unit already exists, inspect the paths it prints and
|
|
58
|
+
pass `--force` only when replacing that host configuration is intentional.
|
|
57
59
|
|
|
58
60
|
`hd workspace new` uses the operator's authenticated `gh`, defaults to the repository's real default
|
|
59
61
|
branch, bootstraps an empty repository unless `--no-bootstrap` is set, and invites `mel-ilotus` unless
|
|
@@ -61,12 +63,8 @@ branch, bootstraps an empty repository unless `--no-bootstrap` is set, and invit
|
|
|
61
63
|
the repository is missing, approve private creation interactively, use `--create` to force it, or
|
|
62
64
|
`--no-create` to fail. Fully flagged calls remain non-interactive for scripts and Mel.
|
|
63
65
|
|
|
64
|
-
`hd plan` remembers a matching checkout per workspace. If none exists, it offers to clone into
|
|
65
|
-
`~/HigherDEV/<slug>`; change the parent with `--clone-root DIR`. An explicit `--repo DIR` is saved
|
|
66
|
-
after its Git origin is verified.
|
|
67
|
-
|
|
68
66
|
Inside the TUI, use `/board`, `/inbox`, `/ticket`, `/queue`, `/cancel`, `/epic new`,
|
|
69
|
-
`/epics`, `/
|
|
67
|
+
`/epic approve`, `/epics`, `/architect` (or `/plan`), `/decide`, `/agents add`, `/agents rm`, `/settings`, `/workspace`,
|
|
70
68
|
`/workspace new`, `/workspace set`, `/workspace rotate-key`, `/feed`,
|
|
71
|
-
`/orchestrator`, `/refresh`, `/help`, or `/exit`. The display refreshes from
|
|
69
|
+
`/on`, `/off`, `/orchestrator`, `/refresh`, `/help`, or `/exit`. The display refreshes from
|
|
72
70
|
the HDX API every five seconds.
|
package/dist/api.js
CHANGED
|
@@ -4,7 +4,7 @@ export function apiErrorMessage(status, parsed, fallback) {
|
|
|
4
4
|
const raw = parsed && typeof parsed === "object" && "error" in parsed
|
|
5
5
|
&& typeof parsed.error === "string"
|
|
6
6
|
? parsed.error : fallback;
|
|
7
|
-
const duplicate = raw.match(/agents_one_enabled_(orchestrator|reviewer)_idx/i)?.[1]?.toLowerCase();
|
|
7
|
+
const duplicate = raw.match(/agents_one_enabled_(architect|orchestrator|reviewer)_idx/i)?.[1]?.toLowerCase();
|
|
8
8
|
if (duplicate) {
|
|
9
9
|
return `hd: ${status} Only one enabled ${duplicate} is allowed per workspace. Disable the current ${duplicate} before enabling another.`;
|
|
10
10
|
}
|
|
@@ -111,6 +111,12 @@ export async function listEpics(config = loadConfig()) {
|
|
|
111
111
|
export async function createEpic(fields, config = loadConfig()) {
|
|
112
112
|
return request(config, "POST", `/api/w/${config.slug}/epics`, fields);
|
|
113
113
|
}
|
|
114
|
+
export async function approveEpic(id, config = loadConfig()) {
|
|
115
|
+
return request(config, "PATCH", `/api/w/${config.slug}/epics`, { id });
|
|
116
|
+
}
|
|
117
|
+
export async function deleteEpic(id, config = loadConfig()) {
|
|
118
|
+
return request(config, "DELETE", `/api/w/${config.slug}/epics`, { id });
|
|
119
|
+
}
|
|
114
120
|
export async function listMessages(options = {}, config = loadConfig()) {
|
|
115
121
|
const query = new URLSearchParams();
|
|
116
122
|
if (options.ticketId)
|
package/dist/host.js
CHANGED
|
@@ -1,16 +1,11 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
|
-
import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
-
import { homedir
|
|
2
|
+
import { access, chmod, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
import { fileURLToPath } from "node:url";
|
|
6
5
|
import { promisify } from "node:util";
|
|
7
|
-
import { writeHdConfig } from "./config.js";
|
|
8
6
|
const execFileAsync = promisify(execFile);
|
|
9
7
|
export const LAUNCHD_LABEL = "io.higherops.hdx-runner";
|
|
10
8
|
export const SYSTEMD_UNIT = "hdx-runner.service";
|
|
11
|
-
export function repoRootFrom(fileUrl = import.meta.url) {
|
|
12
|
-
return path.resolve(path.dirname(fileURLToPath(fileUrl)), "../../..");
|
|
13
|
-
}
|
|
14
9
|
export function parseEnvFile(text) {
|
|
15
10
|
const out = {};
|
|
16
11
|
for (const line of text.split(/\r?\n/)) {
|
|
@@ -88,6 +83,10 @@ export function parseHostFlags(argv) {
|
|
|
88
83
|
};
|
|
89
84
|
if (arg === "--host")
|
|
90
85
|
flags.host = next();
|
|
86
|
+
else if (arg === "--checkout")
|
|
87
|
+
flags.checkout = next();
|
|
88
|
+
else if (arg === "--force")
|
|
89
|
+
flags.force = true;
|
|
91
90
|
else if (arg === "--env-out")
|
|
92
91
|
flags.envFile = next();
|
|
93
92
|
else if (arg === "--worktree-root")
|
|
@@ -96,12 +95,6 @@ export function parseHostFlags(argv) {
|
|
|
96
95
|
flags.installLaunchd = true;
|
|
97
96
|
else if (arg === "--install-systemd")
|
|
98
97
|
flags.installSystemd = true;
|
|
99
|
-
else if (arg === "--url")
|
|
100
|
-
flags.url = next();
|
|
101
|
-
else if (arg === "--api-key")
|
|
102
|
-
flags.apiKey = next();
|
|
103
|
-
else if (arg === "--slug")
|
|
104
|
-
flags.slug = next();
|
|
105
98
|
else if (arg === "--plist-out")
|
|
106
99
|
flags.plistOut = next();
|
|
107
100
|
else if (arg === "--unit-out")
|
|
@@ -111,14 +104,47 @@ export function parseHostFlags(argv) {
|
|
|
111
104
|
}
|
|
112
105
|
return flags;
|
|
113
106
|
}
|
|
107
|
+
async function exists(file) {
|
|
108
|
+
try {
|
|
109
|
+
await access(file);
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function expandHome(value, home) {
|
|
117
|
+
if (value === "~")
|
|
118
|
+
return home;
|
|
119
|
+
if (value.startsWith("~/"))
|
|
120
|
+
return path.join(home, value.slice(2));
|
|
121
|
+
return value;
|
|
122
|
+
}
|
|
114
123
|
export async function initHost(flags, env = process.env) {
|
|
115
124
|
const home = homedir();
|
|
116
|
-
const host = flags.host || env.RUNNER_HOST || hostname().replace(/\..*$/, "").toLowerCase();
|
|
117
125
|
const envFile = flags.envFile || path.join(home, ".config/hdx/runner.env");
|
|
118
126
|
const worktreeRoot = flags.worktreeRoot || env.WORKTREE_ROOT || path.join(home, "hdx-worktrees");
|
|
119
|
-
const repo =
|
|
127
|
+
const repo = path.resolve(expandHome(flags.checkout ?? path.join(home, "hdx"), home));
|
|
120
128
|
const runnerPath = path.join(repo, "apps/runner/src/index.ts");
|
|
121
129
|
const nodePath = process.execPath;
|
|
130
|
+
const plistPath = flags.plistOut || path.join(home, "Library/LaunchAgents", `${LAUNCHD_LABEL}.plist`);
|
|
131
|
+
const unitPath = flags.unitOut || path.join(home, ".config/systemd/user", SYSTEMD_UNIT);
|
|
132
|
+
const targets = [envFile];
|
|
133
|
+
if (process.platform === "darwin" || flags.plistOut || flags.installLaunchd)
|
|
134
|
+
targets.push(plistPath);
|
|
135
|
+
if (process.platform === "linux" || flags.unitOut || flags.installSystemd)
|
|
136
|
+
targets.push(unitPath);
|
|
137
|
+
const existingTargets = (await Promise.all(targets.map(async (target) => ({ target, exists: await exists(target) })))).filter((entry) => entry.exists).map((entry) => entry.target);
|
|
138
|
+
if (existingTargets.length && !flags.force) {
|
|
139
|
+
throw new Error([
|
|
140
|
+
"hd init refused to overwrite existing runner files:",
|
|
141
|
+
...existingTargets.map((target) => ` ${target}`),
|
|
142
|
+
"Re-run with --force to overwrite them.",
|
|
143
|
+
].join("\n"));
|
|
144
|
+
}
|
|
145
|
+
if (!flags.host)
|
|
146
|
+
throw new Error("hd init requires --host HOST.");
|
|
147
|
+
const host = flags.host;
|
|
122
148
|
let existing = {};
|
|
123
149
|
try {
|
|
124
150
|
existing = parseEnvFile(await readFile(envFile, "utf8"));
|
|
@@ -137,13 +163,9 @@ export async function initHost(flags, env = process.env) {
|
|
|
137
163
|
await mkdir(path.dirname(envFile), { recursive: true });
|
|
138
164
|
await writeFile(envFile, formatEnvFile(merged), { encoding: "utf8", mode: 0o600 });
|
|
139
165
|
await chmod(envFile, 0o600);
|
|
140
|
-
if (flags.url && flags.apiKey && flags.slug) {
|
|
141
|
-
writeHdConfig({ url: flags.url.replace(/\/$/, ""), api_key: flags.apiKey, slug: flags.slug });
|
|
142
|
-
}
|
|
143
166
|
const lines = [`hd init host=${host}`, `env: ${envFile}`];
|
|
144
167
|
const workDir = repo;
|
|
145
168
|
if (process.platform === "darwin" || flags.plistOut || flags.installLaunchd) {
|
|
146
|
-
const plistPath = flags.plistOut || path.join(home, "Library/LaunchAgents", `${LAUNCHD_LABEL}.plist`);
|
|
147
169
|
const logPath = path.join(home, "Library/Logs/hdx-runner.log");
|
|
148
170
|
await mkdir(path.dirname(plistPath), { recursive: true });
|
|
149
171
|
await writeFile(plistPath, launchdPlist({ nodePath, runnerPath, envFile, logPath, workDir }), "utf8");
|
|
@@ -163,7 +185,6 @@ export async function initHost(flags, env = process.env) {
|
|
|
163
185
|
lines.push(`launchd plist: ${plistPath}${loaded ? " (loaded)" : ""}`);
|
|
164
186
|
}
|
|
165
187
|
if (process.platform === "linux" || flags.unitOut || flags.installSystemd) {
|
|
166
|
-
const unitPath = flags.unitOut || path.join(home, ".config/systemd/user", SYSTEMD_UNIT);
|
|
167
188
|
await mkdir(path.dirname(unitPath), { recursive: true });
|
|
168
189
|
await writeFile(unitPath, systemdUnit({ nodePath, runnerPath, envFile, workDir }), "utf8");
|
|
169
190
|
let loaded = false;
|
package/dist/index.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { realpathSync } from "node:fs";
|
|
3
3
|
import { pathToFileURL } from "node:url";
|
|
4
|
-
import { answerDecision, cancelTicket, createAgent, createEpic, createTicket, deleteAgent, getStatus, listAgents, listEpics, listTicketEvents, listTickets, listWorkspaces, postMessage, queueTicket, setPaused, showTicket, updateAgent, updateCaps, } from "./api.js";
|
|
4
|
+
import { approveEpic, answerDecision, cancelTicket, createAgent, createEpic, createTicket, deleteAgent, deleteEpic, 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 { login, parseLoginFlags } from "./login.js";
|
|
6
7
|
import { loadConfig } from "./config.js";
|
|
7
8
|
import { epicProgressRows, readEpicSpec } from "./epics.js";
|
|
8
9
|
import { banner, c, statusChip, table, truncate, usage } from "./out.js";
|
|
9
|
-
import { launchArchitect } from "./plan.js";
|
|
10
10
|
import { WORKSPACE_USAGE, workspaceNew, workspaceRotateKey, workspaceSet } from "./workspace-commands.js";
|
|
11
11
|
function fail(message) {
|
|
12
12
|
console.error(message);
|
|
@@ -153,18 +153,26 @@ async function cmdEpic(argv) {
|
|
|
153
153
|
console.log(`${c.bold(epic.id)} ${statusChip(epic.status)} ${epic.title}`);
|
|
154
154
|
return;
|
|
155
155
|
}
|
|
156
|
-
|
|
156
|
+
if (action === "approve" || action === "rm") {
|
|
157
|
+
const id = rest[0];
|
|
158
|
+
if (!id || rest.length !== 1)
|
|
159
|
+
fail(`usage: hd epic ${action} ID`);
|
|
160
|
+
if (action === "approve") {
|
|
161
|
+
const { epic } = await approveEpic(id);
|
|
162
|
+
console.log(`${c.bold(epic.id)} ${statusChip(epic.status)} ${epic.title}`);
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
await deleteEpic(id);
|
|
166
|
+
console.log(`removed ${id}`);
|
|
167
|
+
}
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
fail("usage: hd epic new PATH [--title TITLE] | list | approve ID | rm ID");
|
|
157
171
|
}
|
|
158
172
|
async function cmdPlan(argv) {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
}
|
|
163
|
-
const { workspace } = await getStatus();
|
|
164
|
-
const result = await launchArchitect({ workspaceRepo: workspace.repo, workspaceSlug: workspace.slug,
|
|
165
|
-
repo: opts.repo, cloneRoot: opts["clone-root"] });
|
|
166
|
-
if (result.cloneCommand)
|
|
167
|
-
console.log(`Clone it with: ${result.cloneCommand}`);
|
|
173
|
+
if (argv.length)
|
|
174
|
+
fail("usage: hd plan");
|
|
175
|
+
console.log("Open the HDX TUI and use /architect (or /plan).");
|
|
168
176
|
}
|
|
169
177
|
async function cmdLogs(argv) {
|
|
170
178
|
const { rest, bools } = flags(argv);
|
|
@@ -249,7 +257,7 @@ async function cmdWorkspace(argv, deps = {}) {
|
|
|
249
257
|
fail(WORKSPACE_USAGE);
|
|
250
258
|
const result = await workspaceRotateKey();
|
|
251
259
|
console.log(`api key: ${result.apiKey}`);
|
|
252
|
-
console.log("saved locally;
|
|
260
|
+
console.log("saved locally; run hd login with the new key on other machines.");
|
|
253
261
|
return;
|
|
254
262
|
}
|
|
255
263
|
if (action === "set") {
|
|
@@ -264,7 +272,7 @@ async function cmdWorkspace(argv, deps = {}) {
|
|
|
264
272
|
console.log(`invitation pending for ${result.runnerUser}`);
|
|
265
273
|
console.log(`workspace: ${result.slug}`);
|
|
266
274
|
console.log("saved and switched; configure another machine with:");
|
|
267
|
-
console.log(result.
|
|
275
|
+
console.log(result.loginCommand);
|
|
268
276
|
}
|
|
269
277
|
function selectAgent(agents, target, provider) {
|
|
270
278
|
const exact = agents.find((agent) => agent.id === target);
|
|
@@ -407,6 +415,13 @@ export async function main(argv = process.argv.slice(2), deps = {}) {
|
|
|
407
415
|
console.log(cmd === "on" ? "on" : "resumed");
|
|
408
416
|
return;
|
|
409
417
|
}
|
|
418
|
+
if (cmd === "login") {
|
|
419
|
+
const config = login(parseLoginFlags(rest));
|
|
420
|
+
console.log(`logged in to ${config.slug}`);
|
|
421
|
+
const { launchApp } = await import("./tui/launch.js");
|
|
422
|
+
await launchApp(config.slug);
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
410
425
|
if (cmd === "init" || cmd === "upgrade") {
|
|
411
426
|
const report = await initHost(parseHostFlags(rest));
|
|
412
427
|
console.log(report);
|
package/dist/login.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { loadStoredConfig, writeHdConfig } from "./config.js";
|
|
2
|
+
export function parseLoginFlags(argv) {
|
|
3
|
+
const flags = {};
|
|
4
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
5
|
+
const arg = argv[i];
|
|
6
|
+
const next = () => {
|
|
7
|
+
const value = argv[i + 1];
|
|
8
|
+
if (!value || value.startsWith("--"))
|
|
9
|
+
throw new Error(`Missing value for ${arg}`);
|
|
10
|
+
i += 1;
|
|
11
|
+
return value;
|
|
12
|
+
};
|
|
13
|
+
if (arg === "--url")
|
|
14
|
+
flags.url = next();
|
|
15
|
+
else if (arg === "--api-key")
|
|
16
|
+
flags.apiKey = next();
|
|
17
|
+
else if (arg === "--slug")
|
|
18
|
+
flags.slug = next();
|
|
19
|
+
else
|
|
20
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
21
|
+
}
|
|
22
|
+
return flags;
|
|
23
|
+
}
|
|
24
|
+
/** Configure the operator client without touching runner host files. */
|
|
25
|
+
export function login(flags, path) {
|
|
26
|
+
if (!flags.url || !flags.apiKey) {
|
|
27
|
+
throw new Error("usage: hd login --url URL --api-key KEY [--slug S]");
|
|
28
|
+
}
|
|
29
|
+
let current = "workspace";
|
|
30
|
+
try {
|
|
31
|
+
current = loadStoredConfig(path).current;
|
|
32
|
+
}
|
|
33
|
+
catch { }
|
|
34
|
+
const config = {
|
|
35
|
+
url: flags.url,
|
|
36
|
+
api_key: flags.apiKey,
|
|
37
|
+
slug: flags.slug ?? current,
|
|
38
|
+
};
|
|
39
|
+
writeHdConfig(config, path);
|
|
40
|
+
return config;
|
|
41
|
+
}
|
package/dist/out.js
CHANGED
|
@@ -65,8 +65,8 @@ export function usage() {
|
|
|
65
65
|
c.bold("Usage"),
|
|
66
66
|
` ${c.blue("hd status")} workspace overview`,
|
|
67
67
|
` ${c.blue("hd ticket list | show | new | queue | cancel")} ticket operations`,
|
|
68
|
-
` ${c.blue("hd epic new PATH | list")}
|
|
69
|
-
` ${c.blue("hd plan
|
|
68
|
+
` ${c.blue("hd epic new PATH | list | approve | rm")} epic operations`,
|
|
69
|
+
` ${c.blue("hd plan")} use /architect in the TUI`,
|
|
70
70
|
` ${c.blue("hd workspace ls | new | set | rotate-key")} workspace operations`,
|
|
71
71
|
` ${c.blue("hd agents [add | rm | set]")} manage agents`,
|
|
72
72
|
` ${c.blue("hd caps [set PROVIDER N]")} provider concurrency`,
|
|
@@ -74,6 +74,8 @@ export function usage() {
|
|
|
74
74
|
` ${c.blue("hd msg KEY TEXT")} message a builder`,
|
|
75
75
|
` ${c.blue("hd decide [ID --answer TEXT]")} decisions`,
|
|
76
76
|
` ${c.blue("hd on | hd off")} workspace switch`,
|
|
77
|
-
` ${c.blue("hd
|
|
77
|
+
` ${c.blue("hd login --url URL --api-key KEY")} client setup`,
|
|
78
|
+
` ${c.blue("hd init --host HOST [options]")} host setup`,
|
|
79
|
+
` ${c.blue("hd upgrade --host HOST [options]")} refresh host setup`,
|
|
78
80
|
].join("\n");
|
|
79
81
|
}
|
package/dist/tui/App.js
CHANGED
|
@@ -3,7 +3,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
|
3
3
|
import { Box, Static, Text, useApp, useInput, useStdout } from "ink";
|
|
4
4
|
import { loadConfig } from "../config.js";
|
|
5
5
|
import { epicProgressRows } from "../epics.js";
|
|
6
|
-
import { launchArchitect } from "../plan.js";
|
|
7
6
|
import { promptOnStdin } from "../prompt.js";
|
|
8
7
|
import { workspaceNew, workspaceRotateKey, workspaceSet } from "../workspace-commands.js";
|
|
9
8
|
import { Banner } from "./Banner.js";
|
|
@@ -19,7 +18,7 @@ import { alertOnce } from "./alert.js";
|
|
|
19
18
|
import { bubbleRows } from "./height.js";
|
|
20
19
|
import { planLayout, splitPanels } from "./layout.js";
|
|
21
20
|
import { parseLine } from "./parse.js";
|
|
22
|
-
import { configuredSlugs, cancelTicket, createAgent, createEpicFromFile, decisionOptions, deleteAgent, loadLiveEvents, loadTicketDetail, pollSnapshot,
|
|
21
|
+
import { configuredSlugs, approveEpic, cancelTicket, createAgent, createEpicFromFile, decisionOptions, deleteAgent, loadLiveEvents, loadTicketDetail, pollSnapshot, postAgentMessage, queueTicket, resolveDecision, setWorkspacePaused, switchWorkspace, updateAgent, updateProviderCap, updateWorkspace, } from "./data.js";
|
|
23
22
|
import { editFor, editableKeys, nextValue, seedFor, settingsRows } from "./settings-model.js";
|
|
24
23
|
import { appendLines, runLabels, toStreamLines } from "./stream.js";
|
|
25
24
|
import { UI } from "./theme.js";
|
|
@@ -193,18 +192,18 @@ export function App({ initial }) {
|
|
|
193
192
|
setBusy(false);
|
|
194
193
|
}
|
|
195
194
|
}, [config, say]);
|
|
196
|
-
const
|
|
195
|
+
const askAgent = useCallback(async (role, text) => {
|
|
197
196
|
setBusy(true);
|
|
198
197
|
const id = nextId();
|
|
199
|
-
setMessages((prior) => [...prior, { id, speaker:
|
|
198
|
+
setMessages((prior) => [...prior, { id, speaker: role, body: "", pending: true }]);
|
|
200
199
|
const since = new Date().toISOString();
|
|
201
200
|
try {
|
|
202
|
-
await
|
|
201
|
+
await postAgentMessage(role, text, config);
|
|
203
202
|
setMessages((prior) => prior.map((message) => message.id === id
|
|
204
203
|
? { ...message, steps: ["· queued, it answers on the next tick"] }
|
|
205
204
|
: message));
|
|
206
205
|
const { waitForReply } = await import("./data.js");
|
|
207
|
-
const reply = await waitForReply(config, since, 180_000);
|
|
206
|
+
const reply = await waitForReply(config, role, since, 180_000);
|
|
208
207
|
setMessages((prior) => prior.map((message) => message.id === id
|
|
209
208
|
? { ...message, body: reply ?? "No reply yet. It will land in /inbox.", pending: false, steps: [] }
|
|
210
209
|
: message));
|
|
@@ -258,18 +257,20 @@ export function App({ initial }) {
|
|
|
258
257
|
const action = parseLine(text);
|
|
259
258
|
if (action.kind === "say") {
|
|
260
259
|
say("you", text);
|
|
261
|
-
if (mode
|
|
262
|
-
await
|
|
260
|
+
if (mode !== "browse")
|
|
261
|
+
await askAgent(mode, text);
|
|
263
262
|
else
|
|
264
|
-
setNotice("Use /orchestrator before sending a message.");
|
|
263
|
+
setNotice("Use /architect or /orchestrator before sending a message.");
|
|
265
264
|
return;
|
|
266
265
|
}
|
|
267
266
|
switch (action.kind) {
|
|
268
267
|
case "mode":
|
|
269
|
-
setMode(
|
|
268
|
+
setMode(action.mode);
|
|
270
269
|
setCursor(null);
|
|
271
270
|
selectedRef.current = null;
|
|
272
|
-
say("system",
|
|
271
|
+
say("system", action.mode === "architect"
|
|
272
|
+
? "Talking to the architect. Ask questions, refine the epic, then explicitly request a draft."
|
|
273
|
+
: "Talking to the orchestrator. It moves work already in flight.");
|
|
273
274
|
return;
|
|
274
275
|
case "view":
|
|
275
276
|
setView(action.view);
|
|
@@ -311,7 +312,7 @@ export function App({ initial }) {
|
|
|
311
312
|
if (!created)
|
|
312
313
|
throw new Error("Workspace wizard did not finish.");
|
|
313
314
|
await changeWorkspace(created.slug, loadConfig());
|
|
314
|
-
say("system", `Created ${created.slug}. Configure
|
|
315
|
+
say("system", `Created ${created.slug}. Configure another machine with: ${created.loginCommand}`);
|
|
315
316
|
}
|
|
316
317
|
else if (action.command === "set") {
|
|
317
318
|
const updated = await workspaceSet(action.args);
|
|
@@ -321,7 +322,7 @@ export function App({ initial }) {
|
|
|
321
322
|
else {
|
|
322
323
|
const rotated = await workspaceRotateKey();
|
|
323
324
|
setConfig(loadConfig());
|
|
324
|
-
say("system", `API key: ${rotated.apiKey}\nSaved locally.
|
|
325
|
+
say("system", `API key: ${rotated.apiKey}\nSaved locally. Run hd login with the new key on other machines.`);
|
|
325
326
|
}
|
|
326
327
|
}
|
|
327
328
|
catch (error) {
|
|
@@ -363,6 +364,20 @@ export function App({ initial }) {
|
|
|
363
364
|
setBusy(false);
|
|
364
365
|
}
|
|
365
366
|
return;
|
|
367
|
+
case "epic-approve":
|
|
368
|
+
setBusy(true);
|
|
369
|
+
try {
|
|
370
|
+
const { epic } = await approveEpic(config, action.id);
|
|
371
|
+
say("system", `Approved epic ${epic.id}: ${epic.title}`);
|
|
372
|
+
await refresh();
|
|
373
|
+
}
|
|
374
|
+
catch (error) {
|
|
375
|
+
setNotice(error instanceof Error ? error.message : String(error));
|
|
376
|
+
}
|
|
377
|
+
finally {
|
|
378
|
+
setBusy(false);
|
|
379
|
+
}
|
|
380
|
+
return;
|
|
366
381
|
case "epics": {
|
|
367
382
|
const rows = epicProgressRows(board.epics, board.tickets);
|
|
368
383
|
say("system", rows.length
|
|
@@ -370,6 +385,20 @@ export function App({ initial }) {
|
|
|
370
385
|
: "No epics.");
|
|
371
386
|
return;
|
|
372
387
|
}
|
|
388
|
+
case "paused":
|
|
389
|
+
setBusy(true);
|
|
390
|
+
try {
|
|
391
|
+
await setWorkspacePaused(config, action.paused);
|
|
392
|
+
say("system", `${workspace.slug} is now ${action.paused ? "off" : "on"}.`);
|
|
393
|
+
await refresh();
|
|
394
|
+
}
|
|
395
|
+
catch (error) {
|
|
396
|
+
setNotice(error instanceof Error ? error.message : String(error));
|
|
397
|
+
}
|
|
398
|
+
finally {
|
|
399
|
+
setBusy(false);
|
|
400
|
+
}
|
|
401
|
+
return;
|
|
373
402
|
case "queue":
|
|
374
403
|
setBusy(true);
|
|
375
404
|
try {
|
|
@@ -433,32 +462,6 @@ export function App({ initial }) {
|
|
|
433
462
|
}
|
|
434
463
|
return;
|
|
435
464
|
}
|
|
436
|
-
case "plan":
|
|
437
|
-
setBusy(true);
|
|
438
|
-
try {
|
|
439
|
-
let result;
|
|
440
|
-
await suspendTerminal(async () => {
|
|
441
|
-
result = await launchArchitect({
|
|
442
|
-
workspaceRepo: workspace.repo, workspaceSlug: workspace.slug,
|
|
443
|
-
repo: action.repo, cloneRoot: action.cloneRoot,
|
|
444
|
-
}, { prompt: tuiPrompt });
|
|
445
|
-
});
|
|
446
|
-
if (!result)
|
|
447
|
-
throw new Error("Architect did not finish.");
|
|
448
|
-
if (result.cloneCommand)
|
|
449
|
-
say("system", `Clone it with: ${result.cloneCommand}`);
|
|
450
|
-
else {
|
|
451
|
-
say("system", "Architect session ended. Review the spec, then run /epic new PATH.");
|
|
452
|
-
await refresh();
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
catch (error) {
|
|
456
|
-
setNotice(error instanceof Error ? error.message : String(error));
|
|
457
|
-
}
|
|
458
|
-
finally {
|
|
459
|
-
setBusy(false);
|
|
460
|
-
}
|
|
461
|
-
return;
|
|
462
465
|
case "decide": {
|
|
463
466
|
const decision = board.decisions[0];
|
|
464
467
|
if (!decision) {
|
|
@@ -505,8 +508,8 @@ export function App({ initial }) {
|
|
|
505
508
|
default:
|
|
506
509
|
return;
|
|
507
510
|
}
|
|
508
|
-
}, [view, settings, applyEdit, board, browsing, mode, say,
|
|
509
|
-
config,
|
|
511
|
+
}, [view, settings, applyEdit, board, browsing, mode, say, askAgent, order, settingsOrder, changeWorkspace,
|
|
512
|
+
config, refresh, suspendTerminal, exit]);
|
|
510
513
|
useInput((input, key) => {
|
|
511
514
|
if (key.ctrl && input === "c")
|
|
512
515
|
exit();
|
|
@@ -548,7 +551,7 @@ export function App({ initial }) {
|
|
|
548
551
|
setDraft(next);
|
|
549
552
|
if (editingRef.current)
|
|
550
553
|
setEditing({ key: editingRef.current.key, draft: next });
|
|
551
|
-
}, onSubmit: (value) => void run(value), isActive: !busy, placeholder: busy ? "working…" : "message, or /help", prompt: _jsx(Text, { color: mode === "browse" ? UI.dim : UI.cream, children: mode === "
|
|
554
|
+
}, onSubmit: (value) => void run(value), isActive: !busy, placeholder: busy ? "working…" : "message, or /help", prompt: _jsx(Text, { color: mode === "browse" ? UI.dim : UI.cream, children: mode === "browse" ? "> " : `${mode}> ` }), color: UI.text, onCancel: () => {
|
|
552
555
|
if (editing) {
|
|
553
556
|
setEditing(null);
|
|
554
557
|
setDraft("");
|
package/dist/tui/Help.js
CHANGED
|
@@ -10,13 +10,16 @@ export const COMMANDS = [
|
|
|
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
12
|
{ name: "/cancel", args: "HD-12", help: "cancel a ticket" },
|
|
13
|
-
{ name: "/epic", args: "new PATH", help: "create
|
|
13
|
+
{ name: "/epic", args: "new PATH | approve ID", help: "create or approve a draft epic" },
|
|
14
14
|
{ name: "/epics", help: "list epics and ticket progress" },
|
|
15
|
-
{ name: "/
|
|
15
|
+
{ name: "/architect", help: "talk to the agent that shapes draft epics" },
|
|
16
|
+
{ name: "/plan", help: "alias for /architect" },
|
|
16
17
|
{ name: "/decide", args: "2 | text", help: "answer the decision on screen" },
|
|
17
18
|
{ name: "/agents", args: "[add ... | rm ROLE|ID]", help: "view or manage agents" },
|
|
18
19
|
{ name: "/settings", help: "change provider caps and agent settings" },
|
|
19
20
|
{ name: "/workspace", args: "[slug | new | set | rotate-key]", help: "list, switch, create, or configure" },
|
|
21
|
+
{ name: "/on", help: "turn on the current workspace" },
|
|
22
|
+
{ name: "/off", help: "turn off the current workspace" },
|
|
20
23
|
{ name: "/feed", help: "what just happened" },
|
|
21
24
|
{ name: "/orchestrator", help: "talk to the agent that gets work in flight finished" },
|
|
22
25
|
{ name: "/refresh", help: "reload the board now" },
|
package/dist/tui/data.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
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";
|
|
1
|
+
import { approveEpic as approveEpicNow, 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, setPaused as setPausedNow, 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;
|
|
@@ -106,8 +106,8 @@ export async function switchWorkspace(slug, config = loadConfig()) {
|
|
|
106
106
|
selectWorkspace(slug);
|
|
107
107
|
return snapshot;
|
|
108
108
|
}
|
|
109
|
-
export async function
|
|
110
|
-
await sendMessage({ body_md: body, to_role:
|
|
109
|
+
export async function postAgentMessage(role, body, config) {
|
|
110
|
+
await sendMessage({ body_md: body, to_role: role, delivery: "queue" }, config);
|
|
111
111
|
}
|
|
112
112
|
export async function loadTicketDetail(config, key) {
|
|
113
113
|
return showTicket(key, config);
|
|
@@ -115,23 +115,29 @@ export async function loadTicketDetail(config, key) {
|
|
|
115
115
|
export async function createEpicFromFile(config, path) {
|
|
116
116
|
return postEpic(await readEpicSpec(path), config);
|
|
117
117
|
}
|
|
118
|
+
export async function approveEpic(config, id) {
|
|
119
|
+
return approveEpicNow(id, config);
|
|
120
|
+
}
|
|
118
121
|
export async function queueTicket(config, key) {
|
|
119
122
|
return queueTicketNow(key, config);
|
|
120
123
|
}
|
|
121
124
|
export async function cancelTicket(config, key) {
|
|
122
125
|
return cancelTicketNow(key, config);
|
|
123
126
|
}
|
|
127
|
+
export async function setWorkspacePaused(config, paused) {
|
|
128
|
+
return setPausedNow(paused, config);
|
|
129
|
+
}
|
|
124
130
|
export async function createAgent(config, fields) {
|
|
125
131
|
return postAgent(fields, config);
|
|
126
132
|
}
|
|
127
133
|
export async function deleteAgent(config, id) {
|
|
128
134
|
return removeAgent(id, config);
|
|
129
135
|
}
|
|
130
|
-
export async function waitForReply(config, since, timeoutMs) {
|
|
136
|
+
export async function waitForReply(config, role, since, timeoutMs) {
|
|
131
137
|
const deadline = Date.now() + timeoutMs;
|
|
132
138
|
while (Date.now() <= deadline) {
|
|
133
139
|
const { messages } = await listMessages({ since, limit: 50 }, config);
|
|
134
|
-
const reply = messages.find((message) =>
|
|
140
|
+
const reply = messages.find((message) => message.from_role === role &&
|
|
135
141
|
["human", "operator", "all"].includes(message.to_role));
|
|
136
142
|
if (reply)
|
|
137
143
|
return reply.body_md;
|
package/dist/tui/parse.js
CHANGED
|
@@ -13,6 +13,10 @@ export function parseLine(raw) {
|
|
|
13
13
|
switch (word.toLowerCase()) {
|
|
14
14
|
case "orchestrator":
|
|
15
15
|
return { kind: "mode", mode: "orchestrator" };
|
|
16
|
+
case "architect":
|
|
17
|
+
case "plan":
|
|
18
|
+
return rest.length ? { kind: "unknown", command: `${word.toLowerCase()} takes no arguments` }
|
|
19
|
+
: { kind: "mode", mode: "architect" };
|
|
16
20
|
case "board":
|
|
17
21
|
case "feed":
|
|
18
22
|
case "inbox":
|
|
@@ -48,11 +52,18 @@ export function parseLine(raw) {
|
|
|
48
52
|
? { kind: "ticket", key: argument.toUpperCase() }
|
|
49
53
|
: { kind: "unknown", command: "ticket needs a key" };
|
|
50
54
|
case "epic":
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
55
|
+
if (rest[0]?.toLowerCase() === "new" && rest.length > 1) {
|
|
56
|
+
return { kind: "epic-new", path: rest.slice(1).join(" ") };
|
|
57
|
+
}
|
|
58
|
+
return rest[0]?.toLowerCase() === "approve" && rest.length === 2
|
|
59
|
+
? { kind: "epic-approve", id: rest[1] }
|
|
60
|
+
: { kind: "unknown", command: "epic needs new PATH or approve ID" };
|
|
54
61
|
case "epics":
|
|
55
62
|
return { kind: "epics" };
|
|
63
|
+
case "on":
|
|
64
|
+
case "off":
|
|
65
|
+
return rest.length ? { kind: "unknown", command: `${word.toLowerCase()} takes no arguments` }
|
|
66
|
+
: { kind: "paused", paused: word.toLowerCase() === "off" };
|
|
56
67
|
case "queue":
|
|
57
68
|
return argument
|
|
58
69
|
? { kind: "queue", key: argument.toUpperCase() }
|
|
@@ -60,20 +71,6 @@ export function parseLine(raw) {
|
|
|
60
71
|
case "cancel":
|
|
61
72
|
return argument ? { kind: "cancel", key: argument.toUpperCase() }
|
|
62
73
|
: { kind: "unknown", command: "cancel needs a key" };
|
|
63
|
-
case "plan": {
|
|
64
|
-
const opts = {};
|
|
65
|
-
for (let i = 0; i < rest.length; i += 2) {
|
|
66
|
-
if (!rest[i]?.startsWith("--") || !rest[i + 1]) {
|
|
67
|
-
return { kind: "unknown", command: "plan accepts --repo DIR and --clone-root DIR" };
|
|
68
|
-
}
|
|
69
|
-
opts[rest[i].slice(2)] = rest[i + 1];
|
|
70
|
-
}
|
|
71
|
-
if (Object.keys(opts).some((key) => !["repo", "clone-root"].includes(key))) {
|
|
72
|
-
return { kind: "unknown", command: "plan accepts --repo DIR and --clone-root DIR" };
|
|
73
|
-
}
|
|
74
|
-
return { kind: "plan", ...(opts.repo ? { repo: opts.repo } : {}),
|
|
75
|
-
...(opts["clone-root"] ? { cloneRoot: opts["clone-root"] } : {}) };
|
|
76
|
-
}
|
|
77
74
|
case "decide": {
|
|
78
75
|
const dismiss = /(^|\s)--(skip|dismiss)(\s|$)/.test(argument);
|
|
79
76
|
return {
|
package/dist/tui/theme.js
CHANGED
|
@@ -40,11 +40,12 @@ export function speakerStyle(speaker) {
|
|
|
40
40
|
case "you":
|
|
41
41
|
return { borderStyle: DOTTED, borderColor: UI.cream, label: "you" };
|
|
42
42
|
case "orchestrator":
|
|
43
|
+
case "architect":
|
|
43
44
|
return {
|
|
44
45
|
borderStyle: THIN,
|
|
45
46
|
borderColor: UI.orchestratorBorder,
|
|
46
47
|
backgroundColor: UI.orchestratorBg,
|
|
47
|
-
label:
|
|
48
|
+
label: speaker,
|
|
48
49
|
};
|
|
49
50
|
default:
|
|
50
51
|
return { borderStyle: DOTTED, borderColor: UI.dim, label: "" };
|
|
@@ -115,7 +115,7 @@ export async function workspaceNew(argv, deps = {}) {
|
|
|
115
115
|
return { slug: result.workspace.slug, repo: result.workspace.repo,
|
|
116
116
|
invitationPending: preflight.invitationPending,
|
|
117
117
|
runnerUser: opts["runner-user"] ?? HDX_RUNNER_GH_USER,
|
|
118
|
-
|
|
118
|
+
loginCommand: `hd login --url ${result.url} --api-key ${result.api_key} --slug ${result.workspace.slug}` };
|
|
119
119
|
}
|
|
120
120
|
}
|
|
121
121
|
export async function workspaceSet(argv) {
|
package/package.json
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@higherdev/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"hd": "dist/index.js"
|
|
7
7
|
},
|
|
8
8
|
"main": "dist/index.js",
|
|
9
9
|
"files": [
|
|
10
|
-
"dist"
|
|
11
|
-
"prompts"
|
|
10
|
+
"dist"
|
|
12
11
|
],
|
|
13
12
|
"scripts": {
|
|
14
13
|
"build": "tsc",
|
package/dist/plan.js
DELETED
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
-
import { mkdirSync } from "node:fs";
|
|
3
|
-
import { readFile } from "node:fs/promises";
|
|
4
|
-
import { homedir } from "node:os";
|
|
5
|
-
import { isAbsolute, join, resolve } from "node:path";
|
|
6
|
-
import { loadStoredConfig, rememberWorkspaceRepo } from "./config.js";
|
|
7
|
-
import { promptOnStdin } from "./prompt.js";
|
|
8
|
-
import { defaultGh } from "./workspace-preflight.js";
|
|
9
|
-
const ARCHITECT_PROMPT = new URL("../prompts/architect.md", import.meta.url);
|
|
10
|
-
function gitOutput(cwd, args) {
|
|
11
|
-
const result = spawnSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
12
|
-
return result.status === 0 ? result.stdout.trim() : null;
|
|
13
|
-
}
|
|
14
|
-
export function remoteRepo(remote) {
|
|
15
|
-
const trimmed = remote.trim().replace(/\/$/, "").replace(/\.git$/, "");
|
|
16
|
-
const scp = trimmed.match(/^[^@]+@[^:]+:(.+)$/);
|
|
17
|
-
if (scp)
|
|
18
|
-
return scp[1] ?? null;
|
|
19
|
-
try {
|
|
20
|
-
const url = new URL(trimmed);
|
|
21
|
-
return url.pathname.replace(/^\//, "") || null;
|
|
22
|
-
}
|
|
23
|
-
catch {
|
|
24
|
-
return trimmed.includes("/") ? trimmed : null;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
function expanded(path, home = homedir(), cwd = process.cwd()) {
|
|
28
|
-
const value = path.trim();
|
|
29
|
-
if (value === "~")
|
|
30
|
-
return home;
|
|
31
|
-
if (value.startsWith("~/"))
|
|
32
|
-
return join(home, value.slice(2));
|
|
33
|
-
return isAbsolute(value) ? value : resolve(cwd, value);
|
|
34
|
-
}
|
|
35
|
-
export function matchingWorkspaceRepo(directory, workspaceRepo) {
|
|
36
|
-
const root = gitOutput(expanded(directory), ["rev-parse", "--show-toplevel"]);
|
|
37
|
-
if (!root)
|
|
38
|
-
return null;
|
|
39
|
-
const origin = gitOutput(root, ["remote", "get-url", "origin"]);
|
|
40
|
-
return origin && remoteRepo(origin)?.toLowerCase() === workspaceRepo.toLowerCase() ? root : null;
|
|
41
|
-
}
|
|
42
|
-
export function verifyCodex(checkVersion = () => spawnSync("codex", ["--version"], { stdio: "ignore" })) {
|
|
43
|
-
const check = checkVersion();
|
|
44
|
-
if (check.error || check.status !== 0) {
|
|
45
|
-
throw new Error("Codex CLI is unavailable. Install it if needed, then run `codex login`.");
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
function runCodex(cwd, prompt) {
|
|
49
|
-
return new Promise((resolveRun, rejectRun) => {
|
|
50
|
-
const child = spawn("codex", [prompt], { cwd, stdio: "inherit" });
|
|
51
|
-
child.once("error", rejectRun);
|
|
52
|
-
child.once("exit", (code, signal) => {
|
|
53
|
-
if (code === 0)
|
|
54
|
-
resolveRun();
|
|
55
|
-
else
|
|
56
|
-
rejectRun(new Error(`codex exited ${signal ? `with signal ${signal}` : `with status ${code ?? "unknown"}`}.`));
|
|
57
|
-
});
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
function shellPath(path) {
|
|
61
|
-
return /^[A-Za-z0-9_./~-]+$/.test(path) ? path : `'${path.replaceAll("'", "'\\''")}'`;
|
|
62
|
-
}
|
|
63
|
-
export async function resolveWorkspaceRepo(options, runtime = {}) {
|
|
64
|
-
const matchRepo = runtime.matchRepo ?? matchingWorkspaceRepo;
|
|
65
|
-
const cwd = runtime.cwd?.() ?? process.cwd();
|
|
66
|
-
const home = runtime.home?.() ?? homedir();
|
|
67
|
-
const saveRepo = runtime.saveRepo ?? rememberWorkspaceRepo;
|
|
68
|
-
const accept = (directory) => {
|
|
69
|
-
saveRepo(options.workspaceSlug, directory);
|
|
70
|
-
return { directory };
|
|
71
|
-
};
|
|
72
|
-
if (options.repo) {
|
|
73
|
-
const match = matchRepo(options.repo, options.workspaceRepo);
|
|
74
|
-
if (!match)
|
|
75
|
-
throw new Error(`${options.repo} is not a checkout of ${options.workspaceRepo}.`);
|
|
76
|
-
return accept(match);
|
|
77
|
-
}
|
|
78
|
-
const saved = (runtime.savedRepo ?? ((slug) => loadStoredConfig().repos[slug]))(options.workspaceSlug);
|
|
79
|
-
if (saved) {
|
|
80
|
-
const match = matchRepo(saved, options.workspaceRepo);
|
|
81
|
-
if (match)
|
|
82
|
-
return { directory: match };
|
|
83
|
-
}
|
|
84
|
-
const current = matchRepo(cwd, options.workspaceRepo);
|
|
85
|
-
if (current)
|
|
86
|
-
return accept(current);
|
|
87
|
-
const defaultRoot = "~/HigherDEV";
|
|
88
|
-
const rootInput = options.cloneRoot ?? defaultRoot;
|
|
89
|
-
const root = expanded(rootInput, home, cwd);
|
|
90
|
-
const destination = join(root, options.workspaceSlug);
|
|
91
|
-
const shownDestination = join(rootInput, options.workspaceSlug);
|
|
92
|
-
const cloneCommand = `gh repo clone ${options.workspaceRepo} ${shellPath(shownDestination)}`;
|
|
93
|
-
const interactive = runtime.isTTY ?? Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
94
|
-
if (!interactive || ["n", "no"].includes((await (runtime.prompt ?? promptOnStdin)(`Clone ${options.workspaceRepo} to ${shownDestination}? [Y/n] `)).trim().toLowerCase()))
|
|
95
|
-
return { cloneCommand };
|
|
96
|
-
const gh = runtime.gh ?? defaultGh;
|
|
97
|
-
try {
|
|
98
|
-
await gh(["--version"]);
|
|
99
|
-
}
|
|
100
|
-
catch {
|
|
101
|
-
throw new Error("GitHub CLI is unavailable. Install it if needed, then run `gh auth login`.");
|
|
102
|
-
}
|
|
103
|
-
(runtime.mkdir ?? ((path) => mkdirSync(path, { recursive: true })))(root);
|
|
104
|
-
await gh(["repo", "clone", options.workspaceRepo, destination]);
|
|
105
|
-
const cloned = matchRepo(destination, options.workspaceRepo);
|
|
106
|
-
if (!cloned)
|
|
107
|
-
throw new Error(`Clone completed but ${destination} is not a checkout of ${options.workspaceRepo}.`);
|
|
108
|
-
return accept(cloned);
|
|
109
|
-
}
|
|
110
|
-
export async function launchArchitect(options, runtime = {}) {
|
|
111
|
-
const resolved = await resolveWorkspaceRepo(options, runtime);
|
|
112
|
-
if ("cloneCommand" in resolved)
|
|
113
|
-
return { launched: false, cloneCommand: resolved.cloneCommand };
|
|
114
|
-
(runtime.verifyCodex ?? verifyCodex)();
|
|
115
|
-
const prompt = await readFile(ARCHITECT_PROMPT, "utf8");
|
|
116
|
-
await (runtime.invokeCodex ?? runCodex)(resolved.directory, prompt);
|
|
117
|
-
return { launched: true };
|
|
118
|
-
}
|
package/prompts/architect.md
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
You are helping the operator author an HDX epic for this repository.
|
|
2
|
-
|
|
3
|
-
Start by interviewing the operator about the goal. Ask focused questions until the intended outcome, boundaries, and important tradeoffs are clear. Read the repository as needed so the plan reflects the code that actually exists.
|
|
4
|
-
|
|
5
|
-
Draft the epic in this exact structure:
|
|
6
|
-
|
|
7
|
-
# <title>
|
|
8
|
-
|
|
9
|
-
## Goal
|
|
10
|
-
|
|
11
|
-
## Scope
|
|
12
|
-
|
|
13
|
-
## Out of scope
|
|
14
|
-
|
|
15
|
-
## Acceptance
|
|
16
|
-
|
|
17
|
-
## Constraints
|
|
18
|
-
|
|
19
|
-
The operator is the author. Let them review and revise the draft before saving it. Do not submit the epic to HDX yourself.
|
|
20
|
-
|
|
21
|
-
Once the operator accepts the spec, write it to `docs/epics/<slug>.md`, where `<slug>` is a concise kebab-case name for the epic. End by printing the exact next command, with the real path substituted:
|
|
22
|
-
|
|
23
|
-
`hd epic new docs/epics/<slug>.md`
|