@higherdev/cli 0.8.0 → 0.9.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 +5 -1
- package/dist/index.js +14 -7
- package/dist/workspace-preflight.js +77 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -36,7 +36,7 @@ workspace-map config shapes are migrated automatically when they are read.
|
|
|
36
36
|
| `hd epic list` | List epics and ticket progress |
|
|
37
37
|
| `hd plan [--repo DIR]` | Hand the terminal to Codex to author an epic spec |
|
|
38
38
|
| `hd workspace ls` | List every workspace available to the configured key |
|
|
39
|
-
| `hd workspace new --name NAME --repo OWNER/NAME [options]` |
|
|
39
|
+
| `hd workspace new --name NAME --repo OWNER/NAME [options]` | Preflight GitHub, wire the runner, and create a paused workspace |
|
|
40
40
|
| `hd agents` | List agents |
|
|
41
41
|
| `hd agents set ROLE --provider P --model M [--effort E]` | Update an agent |
|
|
42
42
|
| `hd logs KEY [-f]` | Show or follow run events |
|
|
@@ -48,6 +48,10 @@ workspace-map config shapes are migrated automatically when they are read.
|
|
|
48
48
|
| `hd init [options]` | Configure this host and runner service |
|
|
49
49
|
| `hd upgrade [options]` | Refresh this host configuration |
|
|
50
50
|
|
|
51
|
+
`hd workspace new` uses the operator's authenticated `gh`, defaults to the repository's real default
|
|
52
|
+
branch, bootstraps an empty repository unless `--no-bootstrap` is set, and invites `mel-ilotus` unless
|
|
53
|
+
`--runner-user USER` overrides it.
|
|
54
|
+
|
|
51
55
|
Inside the TUI, use `/board`, `/inbox`, `/ticket`, `/queue`, `/epic new`,
|
|
52
56
|
`/epics`, `/plan`, `/decide`, `/agents`, `/settings`, `/workspace`, `/feed`,
|
|
53
57
|
`/orchestrator`, `/refresh`, `/help`, or `/exit`. The display refreshes from
|
package/dist/index.js
CHANGED
|
@@ -7,6 +7,7 @@ import { loadConfig, writeHdConfig } from "./config.js";
|
|
|
7
7
|
import { epicProgressRows, readEpicSpec } from "./epics.js";
|
|
8
8
|
import { banner, c, statusChip, table, truncate, usage } from "./out.js";
|
|
9
9
|
import { launchArchitect } from "./plan.js";
|
|
10
|
+
import { HDX_RUNNER_GH_USER, preflightWorkspace } from "./workspace-preflight.js";
|
|
10
11
|
function fail(message) {
|
|
11
12
|
console.error(message);
|
|
12
13
|
process.exit(1);
|
|
@@ -218,7 +219,7 @@ async function cmdDecide(argv) {
|
|
|
218
219
|
await answerDecision(id, opts.answer);
|
|
219
220
|
console.log("answered");
|
|
220
221
|
}
|
|
221
|
-
async function cmdWorkspace(argv) {
|
|
222
|
+
async function cmdWorkspace(argv, deps = {}) {
|
|
222
223
|
const [action, ...rest] = argv;
|
|
223
224
|
if (action === "ls") {
|
|
224
225
|
const current = loadConfig().slug;
|
|
@@ -232,14 +233,20 @@ async function cmdWorkspace(argv) {
|
|
|
232
233
|
])));
|
|
233
234
|
return;
|
|
234
235
|
}
|
|
236
|
+
const workspaceUsage = "usage: hd workspace ls | new --name NAME --repo OWNER/NAME [--slug S] [--branch B] [--host box] [--runner-user USER] [--no-bootstrap]";
|
|
235
237
|
if (action !== "new")
|
|
236
|
-
fail(
|
|
237
|
-
const { opts } = flags(rest);
|
|
238
|
+
fail(workspaceUsage);
|
|
239
|
+
const { opts, bools } = flags(rest);
|
|
238
240
|
if (!opts.name || !opts.repo) {
|
|
239
|
-
fail(
|
|
241
|
+
fail(workspaceUsage);
|
|
240
242
|
}
|
|
243
|
+
const preflight = await (deps.preflightWorkspace ?? preflightWorkspace)({ name: opts.name, repo: opts.repo,
|
|
244
|
+
branch: opts.branch, noBootstrap: bools.has("no-bootstrap"),
|
|
245
|
+
runnerUser: opts["runner-user"] ?? HDX_RUNNER_GH_USER });
|
|
246
|
+
if (preflight.invitationPending)
|
|
247
|
+
console.log(`invitation pending for ${opts["runner-user"] ?? HDX_RUNNER_GH_USER}`);
|
|
241
248
|
const result = await createWorkspace({ name: opts.name, repo: opts.repo, slug: opts.slug,
|
|
242
|
-
default_branch:
|
|
249
|
+
default_branch: preflight.branch, default_host: opts.host ?? "box" });
|
|
243
250
|
console.log(`workspace: ${result.workspace.slug}`);
|
|
244
251
|
writeHdConfig({ url: result.url, api_key: result.api_key, slug: result.workspace.slug });
|
|
245
252
|
console.log("saved and switched; configure another machine with:");
|
|
@@ -279,7 +286,7 @@ async function cmdAgents(argv) {
|
|
|
279
286
|
});
|
|
280
287
|
console.log(`${agent.role} ${agent.provider} ${agent.model} ${agent.effort}`);
|
|
281
288
|
}
|
|
282
|
-
export async function main(argv = process.argv.slice(2)) {
|
|
289
|
+
export async function main(argv = process.argv.slice(2), deps = {}) {
|
|
283
290
|
const [cmd, ...rest] = argv;
|
|
284
291
|
try {
|
|
285
292
|
if (!cmd) {
|
|
@@ -329,7 +336,7 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
329
336
|
return;
|
|
330
337
|
}
|
|
331
338
|
if (cmd === "workspace") {
|
|
332
|
-
await cmdWorkspace(rest);
|
|
339
|
+
await cmdWorkspace(rest, deps);
|
|
333
340
|
return;
|
|
334
341
|
}
|
|
335
342
|
if (cmd === "agents") {
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
const exec = promisify(execFile);
|
|
4
|
+
export const HDX_RUNNER_GH_USER = "mel-ilotus";
|
|
5
|
+
export function preflightDecision(input) {
|
|
6
|
+
if (!input.found)
|
|
7
|
+
return { ok: false, error: "Repository was not found or is inaccessible." };
|
|
8
|
+
if (input.viewerPermission.toUpperCase() !== "ADMIN") {
|
|
9
|
+
return { ok: false, error: `Repository admin permission is required; viewer has ${input.viewerPermission || "none"}.` };
|
|
10
|
+
}
|
|
11
|
+
if (input.empty && input.noBootstrap) {
|
|
12
|
+
return { ok: false, error: "Repository is empty and --no-bootstrap was set." };
|
|
13
|
+
}
|
|
14
|
+
return { ok: true, bootstrap: input.empty };
|
|
15
|
+
}
|
|
16
|
+
function errorMessage(error) {
|
|
17
|
+
if (!error || typeof error !== "object")
|
|
18
|
+
return String(error);
|
|
19
|
+
const value = error;
|
|
20
|
+
return value.stderr?.trim() || value.message || String(error);
|
|
21
|
+
}
|
|
22
|
+
async function defaultGh(args) {
|
|
23
|
+
try {
|
|
24
|
+
return (await exec("gh", args, { encoding: "utf8", maxBuffer: 1024 * 1024 })).stdout;
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
throw new Error(errorMessage(error));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
async function repoView(repo, gh) {
|
|
31
|
+
return JSON.parse(await gh(["repo", "view", repo, "--json", "defaultBranchRef,isEmpty,viewerPermission"]));
|
|
32
|
+
}
|
|
33
|
+
function canPush(permission) {
|
|
34
|
+
return ["admin", "maintain", "write", "push"].includes(permission.toLowerCase());
|
|
35
|
+
}
|
|
36
|
+
export async function preflightWorkspace(input, 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 = await repoView(input.repo, gh);
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
throw new Error(`GitHub repository ${input.repo} was not found or is inaccessible: ${errorMessage(error)}`);
|
|
49
|
+
}
|
|
50
|
+
const decision = preflightDecision({ found: true, empty: view.isEmpty,
|
|
51
|
+
viewerPermission: view.viewerPermission, noBootstrap: input.noBootstrap });
|
|
52
|
+
if (!decision.ok)
|
|
53
|
+
throw new Error(decision.error);
|
|
54
|
+
if (decision.bootstrap) {
|
|
55
|
+
await gh(["api", "-X", "PUT", `repos/${input.repo}/contents/README.md`,
|
|
56
|
+
"-f", "message=Initial commit", "-f", `content=${Buffer.from(`# ${input.name}`).toString("base64")}`]);
|
|
57
|
+
view = await repoView(input.repo, gh);
|
|
58
|
+
}
|
|
59
|
+
const branch = input.branch || view.defaultBranchRef?.name;
|
|
60
|
+
if (!branch)
|
|
61
|
+
throw new Error(`GitHub repository ${input.repo} has no default branch.`);
|
|
62
|
+
const runnerUser = input.runnerUser || HDX_RUNNER_GH_USER;
|
|
63
|
+
let permission = "none";
|
|
64
|
+
try {
|
|
65
|
+
const result = JSON.parse(await gh(["api", `repos/${input.repo}/collaborators/${runnerUser}/permission`]));
|
|
66
|
+
permission = result.permission ?? "none";
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
if (!/404|not found/i.test(errorMessage(error)))
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
const invitationPending = !canPush(permission);
|
|
73
|
+
if (invitationPending) {
|
|
74
|
+
await gh(["api", "-X", "PUT", `repos/${input.repo}/collaborators/${runnerUser}`, "-f", "permission=push"]);
|
|
75
|
+
}
|
|
76
|
+
return { branch, invitationPending };
|
|
77
|
+
}
|