@higherdev/cli 0.14.4 → 0.15.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 +11 -1
- package/dist/api.js +9 -0
- package/dist/index.js +78 -7
- package/dist/out.js +1 -0
- package/dist/workspace-preflight.js +28 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -47,6 +47,9 @@ workspace-map config shapes are migrated automatically when they are read.
|
|
|
47
47
|
| `hd env ls` | List workspace environment variable names |
|
|
48
48
|
| `hd env set NAME=VALUE [NAME=VALUE...]` | Set workspace environment variables |
|
|
49
49
|
| `hd env rm NAME` | Remove a workspace environment variable |
|
|
50
|
+
| `hd host env ls` | List environment variable names on the workspace's default host |
|
|
51
|
+
| `hd host env set NAME=VALUE [NAME=VALUE...]` | Set host-only environment variables |
|
|
52
|
+
| `hd host env rm NAME` | Remove a host-only environment variable |
|
|
50
53
|
| `hd logs KEY [-f]` | Show or follow run events |
|
|
51
54
|
| `hd msg KEY "message" [--interrupt]` | Message a builder |
|
|
52
55
|
| `hd decide` | List open decisions |
|
|
@@ -62,11 +65,18 @@ elsewhere. If the runner environment or service unit already exists, inspect the
|
|
|
62
65
|
pass `--force` only when replacing that host configuration is intentional.
|
|
63
66
|
|
|
64
67
|
`hd workspace new` uses the operator's authenticated `gh`, defaults to the repository's real default
|
|
65
|
-
branch, bootstraps an empty repository unless `--no-bootstrap` is set, and invites `mel-ilotus` unless
|
|
68
|
+
branch, bootstraps an empty repository unless `--no-bootstrap` is set, and invites `mel-ilotus` as an admin unless
|
|
66
69
|
`--runner-user USER` overrides it. On a terminal, missing name or repo flags start a guided wizard. If
|
|
67
70
|
the repository is missing, approve private creation interactively, use `--create` to force it, or
|
|
68
71
|
`--no-create` to fail. Fully flagged calls remain non-interactive for scripts and Mel.
|
|
69
72
|
|
|
73
|
+
`hd env set` and `hd env rm` also update GitHub Actions secrets on the current workspace repository.
|
|
74
|
+
They require the operator's authenticated `gh` account to have repository admin permission. Secret values are sent
|
|
75
|
+
to `gh` through stdin and are never printed.
|
|
76
|
+
|
|
77
|
+
Use `hd host env set SUPABASE_ACCESS_TOKEN=... SUPABASE_ORG_ID=...` once to let the runner provision
|
|
78
|
+
Supabase for workspaces assigned to that host. Host values are never passed to builder processes.
|
|
79
|
+
|
|
70
80
|
Inside the TUI, use `/board`, `/inbox`, `/ticket`, `/queue`, `/cancel`, `/epic new`,
|
|
71
81
|
`/epic approve`, `/epics`, `/architect` (or `/plan`), `/decide`, `/agents add`, `/agents rm`, `/env`, `/settings`, `/workspace`,
|
|
72
82
|
`/workspace new`, `/workspace set`, `/workspace rotate-key`, `/feed`,
|
package/dist/api.js
CHANGED
|
@@ -114,6 +114,15 @@ export async function setWorkspaceEnv(name, value, config = loadConfig()) {
|
|
|
114
114
|
export async function removeWorkspaceEnv(name, config = loadConfig()) {
|
|
115
115
|
return request(config, "DELETE", `/api/w/${config.slug}/env`, { name });
|
|
116
116
|
}
|
|
117
|
+
export async function listHostEnv(host, config = loadConfig()) {
|
|
118
|
+
return request(config, "GET", `/api/hosts/${encodeURIComponent(host)}/env`);
|
|
119
|
+
}
|
|
120
|
+
export async function setHostEnv(host, name, value, config = loadConfig()) {
|
|
121
|
+
return request(config, "PUT", `/api/hosts/${encodeURIComponent(host)}/env`, { name, value });
|
|
122
|
+
}
|
|
123
|
+
export async function removeHostEnv(host, name, config = loadConfig()) {
|
|
124
|
+
return request(config, "DELETE", `/api/hosts/${encodeURIComponent(host)}/env`, { name });
|
|
125
|
+
}
|
|
117
126
|
export async function listEpics(config = loadConfig()) {
|
|
118
127
|
return request(config, "GET", `/api/w/${config.slug}/epics`);
|
|
119
128
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { realpathSync } from "node:fs";
|
|
3
3
|
import { pathToFileURL } from "node:url";
|
|
4
|
-
import { approveEpic, answerDecision, cancelTicket, createAgent, createEpic, createTicket, deleteAgent, deleteEpic, getStatus, listAgents, listEpics, listWorkspaceEnv, listTicketEvents, listTickets, listWorkspaces, postMessage, queueTicket, removeWorkspaceEnv, setPaused, setWorkspaceEnv, showTicket, updateAgent, updateCaps, } from "./api.js";
|
|
4
|
+
import { approveEpic, answerDecision, cancelTicket, createAgent, createEpic, createTicket, deleteAgent, deleteEpic, getStatus, listAgents, listEpics, listHostEnv, listWorkspaceEnv, listTicketEvents, listTickets, listWorkspaces, postMessage, queueTicket, removeWorkspaceEnv, removeHostEnv, setPaused, setWorkspaceEnv, setHostEnv, showTicket, updateAgent, updateCaps, } from "./api.js";
|
|
5
5
|
import { initHost, parseHostFlags } from "./host.js";
|
|
6
6
|
import { login, parseLoginFlags } from "./login.js";
|
|
7
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
|
|
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,22 +371,77 @@ 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
|
-
|
|
379
|
+
assignments.push({ name: assignment.slice(0, at), value: assignment.slice(at + 1) });
|
|
367
380
|
}
|
|
368
|
-
|
|
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
|
-
|
|
373
|
-
|
|
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");
|
|
377
414
|
}
|
|
415
|
+
async function cmdHost(argv) {
|
|
416
|
+
const [scope, action, ...args] = argv;
|
|
417
|
+
const usage = "usage: hd host env ls | set NAME=VALUE [NAME=VALUE...] | rm NAME";
|
|
418
|
+
if (scope !== "env")
|
|
419
|
+
fail(usage);
|
|
420
|
+
const host = (await getStatus()).workspace.default_host;
|
|
421
|
+
if (action === "ls") {
|
|
422
|
+
const { env } = await listHostEnv(host);
|
|
423
|
+
if (!env.length)
|
|
424
|
+
return console.log(c.dim(`No environment variables for host ${host}.`));
|
|
425
|
+
console.log(table(["NAME", "UPDATED"], env.map((row) => [row.name, row.updated_at])));
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
if (action === "set" && args.length) {
|
|
429
|
+
for (const assignment of args) {
|
|
430
|
+
const at = assignment.indexOf("=");
|
|
431
|
+
if (at < 1)
|
|
432
|
+
fail(usage);
|
|
433
|
+
await setHostEnv(host, assignment.slice(0, at), assignment.slice(at + 1));
|
|
434
|
+
}
|
|
435
|
+
console.log(`set ${args.length} host variable${args.length === 1 ? "" : "s"} on ${host}`);
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
if (action === "rm" && args.length === 1) {
|
|
439
|
+
await removeHostEnv(host, args[0]);
|
|
440
|
+
console.log(`removed ${args[0]} from ${host}`);
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
fail(usage);
|
|
444
|
+
}
|
|
378
445
|
export async function main(argv = process.argv.slice(2), deps = {}) {
|
|
379
446
|
const [cmd, ...rest] = argv;
|
|
380
447
|
try {
|
|
@@ -437,7 +504,11 @@ export async function main(argv = process.argv.slice(2), deps = {}) {
|
|
|
437
504
|
return;
|
|
438
505
|
}
|
|
439
506
|
if (cmd === "env") {
|
|
440
|
-
await cmdEnv(rest);
|
|
507
|
+
await cmdEnv(rest, deps);
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
if (cmd === "host") {
|
|
511
|
+
await cmdHost(rest);
|
|
441
512
|
return;
|
|
442
513
|
}
|
|
443
514
|
if (cmd === "pause" || cmd === "off") {
|
package/dist/out.js
CHANGED
|
@@ -71,6 +71,7 @@ export function usage() {
|
|
|
71
71
|
` ${c.blue("hd agents [add | rm | set]")} manage agents`,
|
|
72
72
|
` ${c.blue("hd caps [set PROVIDER N]")} provider concurrency`,
|
|
73
73
|
` ${c.blue("hd env ls | set | rm")} workspace environment`,
|
|
74
|
+
` ${c.blue("hd host env ls | set | rm")} host environment`,
|
|
74
75
|
` ${c.blue("hd logs KEY [-f]")} run events`,
|
|
75
76
|
` ${c.blue("hd msg KEY TEXT")} message a builder`,
|
|
76
77
|
` ${c.blue("hd decide [ID --answer TEXT]")} decisions`,
|
|
@@ -19,20 +19,42 @@ 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
|
}
|
|
33
|
-
function canPush(permission) {
|
|
34
|
-
return ["admin", "maintain", "write", "push"].includes(permission.toLowerCase());
|
|
35
|
-
}
|
|
36
58
|
export async function preflightWorkspace(input, gh = defaultGh) {
|
|
37
59
|
try {
|
|
38
60
|
await gh(["--version"]);
|
|
@@ -69,9 +91,9 @@ export async function preflightWorkspace(input, gh = defaultGh) {
|
|
|
69
91
|
if (!/404|not found/i.test(errorMessage(error)))
|
|
70
92
|
throw error;
|
|
71
93
|
}
|
|
72
|
-
const invitationPending =
|
|
94
|
+
const invitationPending = permission.toLowerCase() !== "admin";
|
|
73
95
|
if (invitationPending) {
|
|
74
|
-
await gh(["api", "-X", "PUT", `repos/${input.repo}/collaborators/${runnerUser}`, "-f", "permission=
|
|
96
|
+
await gh(["api", "-X", "PUT", `repos/${input.repo}/collaborators/${runnerUser}`, "-f", "permission=admin"]);
|
|
75
97
|
}
|
|
76
98
|
return { branch, invitationPending };
|
|
77
99
|
}
|