@higherdev/cli 0.11.1 → 0.12.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 CHANGED
@@ -36,7 +36,9 @@ workspace-map config shapes are migrated automatically when they are read.
36
36
  | `hd ticket cancel KEY` | Cancel a ticket |
37
37
  | `hd epic new PATH [--title TITLE]` | Create an epic from a Markdown spec |
38
38
  | `hd epic list` | List epics and ticket progress |
39
- | `hd plan [--repo DIR] [--clone-root DIR]` | Hand the terminal to Codex to author an epic spec |
39
+ | `hd epic approve ID` | Open a draft epic for orchestrator decomposition |
40
+ | `hd epic rm ID` | Remove a draft epic |
41
+ | `hd plan` | Point to the TUI `/architect` conversation |
40
42
  | `hd workspace ls` | List every workspace available to the configured key |
41
43
  | `hd workspace new --name NAME --repo OWNER/NAME [options]` | Preflight GitHub, wire the runner, and create a paused workspace |
42
44
  | `hd workspace set [options]` | Update settings; max turns uses `--max-turns KIND=N[,KIND=N...]` |
@@ -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`, `/plan [--repo DIR]`, `/decide`, `/agents add`, `/agents rm`, `/settings`, `/workspace`,
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/index.js CHANGED
@@ -1,12 +1,11 @@
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
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
- import { launchArchitect } from "./plan.js";
10
9
  import { WORKSPACE_USAGE, workspaceNew, workspaceRotateKey, workspaceSet } from "./workspace-commands.js";
11
10
  function fail(message) {
12
11
  console.error(message);
@@ -153,18 +152,26 @@ async function cmdEpic(argv) {
153
152
  console.log(`${c.bold(epic.id)} ${statusChip(epic.status)} ${epic.title}`);
154
153
  return;
155
154
  }
156
- fail("usage: hd epic new PATH [--title TITLE] | list");
155
+ if (action === "approve" || action === "rm") {
156
+ const id = rest[0];
157
+ if (!id || rest.length !== 1)
158
+ fail(`usage: hd epic ${action} ID`);
159
+ if (action === "approve") {
160
+ const { epic } = await approveEpic(id);
161
+ console.log(`${c.bold(epic.id)} ${statusChip(epic.status)} ${epic.title}`);
162
+ }
163
+ else {
164
+ await deleteEpic(id);
165
+ console.log(`removed ${id}`);
166
+ }
167
+ return;
168
+ }
169
+ fail("usage: hd epic new PATH [--title TITLE] | list | approve ID | rm ID");
157
170
  }
158
171
  async function cmdPlan(argv) {
159
- const { rest, opts, bools } = flags(argv);
160
- if (rest.length || bools.size || Object.keys(opts).some((key) => !["repo", "clone-root"].includes(key))) {
161
- fail("usage: hd plan [--repo DIR] [--clone-root DIR]");
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}`);
172
+ if (argv.length)
173
+ fail("usage: hd plan");
174
+ console.log("Open the HDX TUI and use /architect (or /plan).");
168
175
  }
169
176
  async function cmdLogs(argv) {
170
177
  const { rest, bools } = flags(argv);
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")} epic operations`,
69
- ` ${c.blue("hd plan [--repo DIR] [--clone-root DIR]")} author an epic with Codex`,
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`,
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, postOrchestrator, queueTicket, resolveDecision, switchWorkspace, updateAgent, updateProviderCap, updateWorkspace, } from "./data.js";
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 askOrchestrator = useCallback(async (text) => {
195
+ const askAgent = useCallback(async (role, text) => {
197
196
  setBusy(true);
198
197
  const id = nextId();
199
- setMessages((prior) => [...prior, { id, speaker: "orchestrator", body: "", pending: true }]);
198
+ setMessages((prior) => [...prior, { id, speaker: role, body: "", pending: true }]);
200
199
  const since = new Date().toISOString();
201
200
  try {
202
- await postOrchestrator(text, config);
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 === "orchestrator")
262
- await askOrchestrator(text);
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("orchestrator");
268
+ setMode(action.mode);
270
269
  setCursor(null);
271
270
  selectedRef.current = null;
272
- say("system", "Talking to the orchestrator. It moves work already in flight.");
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);
@@ -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, askOrchestrator, order, settingsOrder, changeWorkspace,
509
- config, workspace.repo, refresh, suspendTerminal, exit]);
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 === "orchestrator" ? "orchestrator> " : "> " }), color: UI.text, onCancel: () => {
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 an epic from a Markdown spec" },
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: "/plan", args: "[--repo DIR]", help: "author an epic with your Codex CLI" },
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 postOrchestrator(body, config) {
110
- await sendMessage({ body_md: body, to_role: "orchestrator", delivery: "queue" }, config);
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) => ["orchestrator", "reviewer"].includes(message.from_role) &&
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
- return rest[0]?.toLowerCase() === "new" && rest.length > 1
52
- ? { kind: "epic-new", path: rest.slice(1).join(" ") }
53
- : { kind: "unknown", command: "epic needs new PATH" };
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: "orchestrator",
48
+ label: speaker,
48
49
  };
49
50
  default:
50
51
  return { borderStyle: DOTTED, borderColor: UI.dim, label: "" };
package/package.json CHANGED
@@ -1,14 +1,13 @@
1
1
  {
2
2
  "name": "@higherdev/cli",
3
- "version": "0.11.1",
3
+ "version": "0.12.0",
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
- }
@@ -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`