@higherdev/cli 0.14.3 → 0.14.5

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
@@ -67,6 +67,10 @@ branch, bootstraps an empty repository unless `--no-bootstrap` is set, and invit
67
67
  the repository is missing, approve private creation interactively, use `--create` to force it, or
68
68
  `--no-create` to fail. Fully flagged calls remain non-interactive for scripts and Mel.
69
69
 
70
+ `hd env set` and `hd env rm` also update GitHub Actions secrets on the current workspace repository.
71
+ They require the operator's authenticated `gh` account to have repository admin permission. Secret values are sent
72
+ to `gh` through stdin and are never printed.
73
+
70
74
  Inside the TUI, use `/board`, `/inbox`, `/ticket`, `/queue`, `/cancel`, `/epic new`,
71
75
  `/epic approve`, `/epics`, `/architect` (or `/plan`), `/decide`, `/agents add`, `/agents rm`, `/env`, `/settings`, `/workspace`,
72
76
  `/workspace new`, `/workspace set`, `/workspace rotate-key`, `/feed`,
package/dist/index.js CHANGED
@@ -8,6 +8,7 @@ import { loadConfig } from "./config.js";
8
8
  import { epicProgressRows, readEpicSpec } from "./epics.js";
9
9
  import { banner, c, statusChip, table, truncate, usage } from "./out.js";
10
10
  import { WORKSPACE_USAGE, workspaceNew, workspaceRotateKey, workspaceSet, workspaceUse } from "./workspace-commands.js";
11
+ import { defaultGh, requireAdminRepo } from "./workspace-preflight.js";
11
12
  function fail(message) {
12
13
  console.error(message);
13
14
  process.exit(1);
@@ -349,7 +350,18 @@ async function cmdCaps(argv) {
349
350
  const result = await updateCaps({ [provider]: cap });
350
351
  console.log(`${provider} ${result.provider_caps[provider]}`);
351
352
  }
352
- async function cmdEnv(argv) {
353
+ async function currentWorkspaceRepo() {
354
+ const current = loadConfig().slug;
355
+ const workspace = (await listWorkspaces()).find((item) => item.slug === current);
356
+ if (!workspace)
357
+ throw new Error(`No workspace ${current}.`);
358
+ return workspace.repo;
359
+ }
360
+ function githubSecretMissing(error) {
361
+ const message = error instanceof Error ? error.message : String(error);
362
+ return /404|not found|does not exist/i.test(message);
363
+ }
364
+ export async function cmdEnv(argv, deps = {}) {
353
365
  const [action, ...args] = argv;
354
366
  if (action === "ls") {
355
367
  const { env } = await listWorkspaceEnv();
@@ -359,18 +371,43 @@ async function cmdEnv(argv) {
359
371
  return;
360
372
  }
361
373
  if (action === "set" && args.length) {
374
+ const assignments = [];
362
375
  for (const assignment of args) {
363
376
  const at = assignment.indexOf("=");
364
377
  if (at < 1)
365
378
  fail("usage: hd env set NAME=VALUE [NAME=VALUE...]");
366
- await setWorkspaceEnv(assignment.slice(0, at), assignment.slice(at + 1));
379
+ assignments.push({ name: assignment.slice(0, at), value: assignment.slice(at + 1) });
367
380
  }
368
- console.log(`set ${args.length} variable${args.length === 1 ? "" : "s"}`);
381
+ for (const assignment of assignments)
382
+ await setWorkspaceEnv(assignment.name, assignment.value);
383
+ const repo = await currentWorkspaceRepo();
384
+ const gh = deps.gh ?? defaultGh;
385
+ await requireAdminRepo(repo, gh);
386
+ for (const assignment of assignments) {
387
+ try {
388
+ await gh(["secret", "set", assignment.name, "--repo", repo], assignment.value);
389
+ }
390
+ catch {
391
+ throw new Error(`Workspace has ${assignment.name}, but GitHub Actions on ${repo} does not.`);
392
+ }
393
+ }
394
+ console.log(`synced ${assignments.map(({ name }) => name).join(", ")} to ${repo}`);
369
395
  return;
370
396
  }
371
397
  if (action === "rm" && args.length === 1) {
372
- await removeWorkspaceEnv(args[0]);
373
- console.log(`removed ${args[0]}`);
398
+ const name = args[0];
399
+ await removeWorkspaceEnv(name);
400
+ const repo = await currentWorkspaceRepo();
401
+ const gh = deps.gh ?? defaultGh;
402
+ await requireAdminRepo(repo, gh);
403
+ try {
404
+ await gh(["secret", "delete", name, "--repo", repo, "--yes"]);
405
+ }
406
+ catch (error) {
407
+ if (!githubSecretMissing(error))
408
+ throw new Error(`Workspace removed ${name}, but GitHub Actions on ${repo} may still have it.`);
409
+ }
410
+ console.log(`synced removal of ${name} to ${repo}`);
374
411
  return;
375
412
  }
376
413
  fail("usage: hd env ls | set NAME=VALUE [NAME=VALUE...] | rm NAME");
@@ -437,7 +474,7 @@ export async function main(argv = process.argv.slice(2), deps = {}) {
437
474
  return;
438
475
  }
439
476
  if (cmd === "env") {
440
- await cmdEnv(rest);
477
+ await cmdEnv(rest, deps);
441
478
  return;
442
479
  }
443
480
  if (cmd === "pause" || cmd === "off") {
package/dist/tui/App.js CHANGED
@@ -18,7 +18,8 @@ import { alertOnce } from "./alert.js";
18
18
  import { bubbleRows } from "./height.js";
19
19
  import { planLayout, splitPanels } from "./layout.js";
20
20
  import { parseLine } from "./parse.js";
21
- import { configuredSlugs, acknowledgeInbox, approveEpic, cancelTicket, createAgent, createEpicFromFile, decisionOptions, deleteAgent, loadLiveEvents, listWorkspaceEnv, loadTicketDetail, pollSnapshot, postAgentMessage, queueTicket, resolveDecision, setWorkspacePaused, switchWorkspace, updateAgent, updateProviderCap, updateWorkspace, } from "./data.js";
21
+ import { configuredSlugs, acknowledgeInbox, approveEpic, cancelTicket, createAgent, createEpicFromFile, decisionOptions, deleteAgent, loadLiveEvents, listWorkspaceEnv, loadTicketDetail, pollSnapshot, postAgentMessage, queueTicket, resolveDecision, setWorkspacePaused, switchWorkspace, updateAgent, updateProviderCap, updateWorkspace, waitForReply, } from "./data.js";
22
+ import { inputActive, promptPlaceholder, QUEUED_STEP, REPLY_WAIT_MS, settleChatReply } from "./chat-wait.js";
22
23
  import { editFor, editableKeys, nextValue, seedFor, settingsRows } from "./settings-model.js";
23
24
  import { appendLines, runLabels, toStreamLines } from "./stream.js";
24
25
  import { UI } from "./theme.js";
@@ -215,30 +216,26 @@ export function App({ initial }) {
215
216
  setBusy(false);
216
217
  }
217
218
  }, [config, say]);
218
- const askAgent = useCallback(async (role, text) => {
219
- setBusy(true);
219
+ const askAgent = useCallback((role, text) => {
220
220
  const id = nextId();
221
221
  setMessages((prior) => [...prior, { id, speaker: role, body: "", pending: true }]);
222
222
  const since = new Date().toISOString();
223
- try {
224
- await postAgentMessage(role, text, config);
225
- setMessages((prior) => prior.map((message) => message.id === id
226
- ? { ...message, steps: ["· queued, it answers on the next tick"] }
227
- : message));
228
- const { waitForReply } = await import("./data.js");
229
- const reply = await waitForReply(config, role, since, 180_000);
230
- setMessages((prior) => prior.map((message) => message.id === id
231
- ? { ...message, body: reply ?? "No reply yet. It will land in /inbox.", pending: false, steps: [] }
232
- : message));
233
- }
234
- catch (error) {
235
- const body = error instanceof Error ? error.message : String(error);
236
- setMessages((prior) => prior.map((message) => message.id === id ? { ...message, body, pending: false } : message));
237
- }
238
- finally {
239
- setMessages((prior) => prior.map((message) => message.id === id ? { ...message, done: true } : message));
240
- setBusy(false);
241
- }
223
+ void (async () => {
224
+ try {
225
+ await postAgentMessage(role, text, config);
226
+ setMessages((prior) => prior.map((message) => message.id === id
227
+ ? { ...message, steps: [QUEUED_STEP] }
228
+ : message));
229
+ const reply = await waitForReply(config, role, since, REPLY_WAIT_MS);
230
+ setMessages((prior) => prior.map((message) => message.id === id
231
+ ? { ...message, ...settleChatReply(reply) }
232
+ : message));
233
+ }
234
+ catch (error) {
235
+ const body = error instanceof Error ? error.message : String(error);
236
+ setMessages((prior) => prior.map((message) => message.id === id ? { ...message, body, pending: false, done: true } : message));
237
+ }
238
+ })();
242
239
  }, [config]);
243
240
  const run = useCallback(async (raw) => {
244
241
  const text = raw.trim();
@@ -281,7 +278,7 @@ export function App({ initial }) {
281
278
  if (action.kind === "say") {
282
279
  say("you", text);
283
280
  if (mode !== "browse")
284
- await askAgent(mode, text);
281
+ askAgent(mode, text);
285
282
  else
286
283
  setNotice("Use /architect or /orchestrator before sending a message.");
287
284
  return;
@@ -589,7 +586,7 @@ export function App({ initial }) {
589
586
  setDraft(next);
590
587
  if (editingRef.current)
591
588
  setEditing({ key: editingRef.current.key, draft: next });
592
- }, 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: () => {
589
+ }, onSubmit: (value) => void run(value), isActive: inputActive({ busy, pendingChats: inFlight.length }), placeholder: promptPlaceholder({ busy, pendingChats: inFlight.length }), prompt: _jsx(Text, { color: mode === "browse" ? UI.dim : UI.cream, children: mode === "browse" ? "> " : `${mode}> ` }), color: UI.text, onCancel: () => {
593
590
  if (editing) {
594
591
  setEditing(null);
595
592
  setDraft("");
@@ -0,0 +1,18 @@
1
+ /** How long the TUI waits for an architect or orchestrator reply. */
2
+ export const REPLY_WAIT_MS = 180_000;
3
+ /** Shown in the pending bubble when the wait expires. The reply can still land in /inbox. */
4
+ export const NO_REPLY_NOTE = "No reply yet. It will land in /inbox.";
5
+ export const QUEUED_STEP = "· queued, it answers on the next tick";
6
+ /**
7
+ * The prompt stays live while a chat reply is pending so navigation, /decide,
8
+ * and a follow-up send still work. `busy` is the only lock.
9
+ */
10
+ export function inputActive(state) {
11
+ return !state.busy;
12
+ }
13
+ export function promptPlaceholder(state) {
14
+ return state.busy ? "working…" : "message, or /help";
15
+ }
16
+ export function settleChatReply(reply) {
17
+ return { body: reply ?? NO_REPLY_NOTE, pending: false, steps: [], done: true };
18
+ }
@@ -19,14 +19,39 @@ function errorMessage(error) {
19
19
  const value = error;
20
20
  return value.stderr?.trim() || value.message || String(error);
21
21
  }
22
- export async function defaultGh(args) {
22
+ export async function defaultGh(args, stdin) {
23
23
  try {
24
+ if (stdin !== undefined) {
25
+ return await new Promise((resolve, reject) => {
26
+ const child = execFile("gh", args, { encoding: "utf8", maxBuffer: 1024 * 1024 }, (error, stdout, stderr) => error ? reject(new Error(stderr.trim() || error.message)) : resolve(stdout));
27
+ child.stdin?.end(stdin);
28
+ });
29
+ }
24
30
  return (await exec("gh", args, { encoding: "utf8", maxBuffer: 1024 * 1024 })).stdout;
25
31
  }
26
32
  catch (error) {
27
33
  throw new Error(errorMessage(error));
28
34
  }
29
35
  }
36
+ export async function requireAdminRepo(repo, gh = defaultGh) {
37
+ try {
38
+ await gh(["--version"]);
39
+ }
40
+ catch {
41
+ throw new Error("GitHub CLI is unavailable. Install it if needed, then run `gh auth login`.");
42
+ }
43
+ let view;
44
+ try {
45
+ view = JSON.parse(await gh(["repo", "view", repo, "--json", "viewerPermission"]));
46
+ }
47
+ catch (error) {
48
+ throw new Error(`GitHub repository ${repo} was not found or is inaccessible: ${errorMessage(error)}`);
49
+ }
50
+ const permission = view.viewerPermission || "";
51
+ if (permission.toUpperCase() !== "ADMIN") {
52
+ throw new Error(`Repository admin permission is required; viewer has ${permission || "none"}.`);
53
+ }
54
+ }
30
55
  async function repoView(repo, gh) {
31
56
  return JSON.parse(await gh(["repo", "view", repo, "--json", "defaultBranchRef,isEmpty,viewerPermission"]));
32
57
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@higherdev/cli",
3
- "version": "0.14.3",
3
+ "version": "0.14.5",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "hd": "dist/index.js"