@higherdev/cli 0.15.0 → 0.15.2
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 +10 -2
- package/dist/index.js +14 -2
- package/dist/out.js +1 -1
- package/dist/workspace-commands.js +29 -2
- package/dist/workspace-preflight.js +23 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -39,6 +39,7 @@ workspace-map config shapes are migrated automatically when they are read.
|
|
|
39
39
|
| `hd workspace new --name NAME --repo OWNER/NAME [options]` | Preflight GitHub, wire the runner, and create a paused workspace |
|
|
40
40
|
| `hd workspace set [options]` | Update settings; max turns uses `--max-turns KIND=N[,KIND=N...]` |
|
|
41
41
|
| `hd workspace rotate-key` | Rotate the shared API key and save it locally |
|
|
42
|
+
| `hd workspace grant-runner-access [--runner-user USER]` | Grant and confirm write-or-better access for the workspace runner |
|
|
42
43
|
| `hd agents` | List agents |
|
|
43
44
|
| `hd agents add ROLE --provider P --model M [options]` | Add an agent |
|
|
44
45
|
| `hd agents rm ROLE\|ID` | Remove an unambiguous agent |
|
|
@@ -65,11 +66,18 @@ elsewhere. If the runner environment or service unit already exists, inspect the
|
|
|
65
66
|
pass `--force` only when replacing that host configuration is intentional.
|
|
66
67
|
|
|
67
68
|
`hd workspace new` uses the operator's authenticated `gh`, defaults to the repository's real default
|
|
68
|
-
branch, bootstraps an empty repository unless `--no-bootstrap` is set, and invites `mel-ilotus`
|
|
69
|
-
|
|
69
|
+
branch, bootstraps an empty repository unless `--no-bootstrap` is set, and invites `mel-ilotus` with push
|
|
70
|
+
access for user-owned repositories or admin for organization-owned repositories unless `--runner-user USER`
|
|
71
|
+
overrides it. On a terminal, missing name or repo flags start a guided wizard. If
|
|
70
72
|
the repository is missing, approve private creation interactively, use `--create` to force it, or
|
|
71
73
|
`--no-create` to fail. Fully flagged calls remain non-interactive for scripts and Mel.
|
|
72
74
|
|
|
75
|
+
For workspaces created before runner access was wired automatically, run
|
|
76
|
+
`hd workspace grant-runner-access`. It uses the operator's authenticated `gh` account, defaults to
|
|
77
|
+
`mel-ilotus`, grants push on user-owned repositories or admin on organization-owned repositories, and
|
|
78
|
+
confirms write-or-better access through GitHub's collaborator permission endpoint. The old
|
|
79
|
+
`grant-runner-admin` name remains an alias.
|
|
80
|
+
|
|
73
81
|
`hd env set` and `hd env rm` also update GitHub Actions secrets on the current workspace repository.
|
|
74
82
|
They require the operator's authenticated `gh` account to have repository admin permission. Secret values are sent
|
|
75
83
|
to `gh` through stdin and are never printed.
|
package/dist/index.js
CHANGED
|
@@ -7,8 +7,8 @@ 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
|
-
import { WORKSPACE_USAGE, workspaceNew, workspaceRotateKey, workspaceSet, workspaceUse } from "./workspace-commands.js";
|
|
11
|
-
import { defaultGh, requireAdminRepo } from "./workspace-preflight.js";
|
|
10
|
+
import { WORKSPACE_USAGE, workspaceGrantRunnerAccess, workspaceNew, workspaceRotateKey, workspaceSet, workspaceUse } from "./workspace-commands.js";
|
|
11
|
+
import { defaultGh, hasWriteRepoPermission, HDX_RUNNER_GH_USER, requireAdminRepo, runnerRepoPermission } from "./workspace-preflight.js";
|
|
12
12
|
function fail(message) {
|
|
13
13
|
console.error(message);
|
|
14
14
|
process.exit(1);
|
|
@@ -258,6 +258,11 @@ async function cmdWorkspace(argv, deps = {}) {
|
|
|
258
258
|
console.log(`${workspace.slug} ${workspace.name} ${workspace.repo}`);
|
|
259
259
|
return;
|
|
260
260
|
}
|
|
261
|
+
if (action === "grant-runner-access" || action === "grant-runner-admin") {
|
|
262
|
+
const result = await workspaceGrantRunnerAccess(rest, deps);
|
|
263
|
+
console.log(`${result.runnerUser} ${result.permission} ${result.repo}`);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
261
266
|
if (action === "rotate-key") {
|
|
262
267
|
if (rest.length)
|
|
263
268
|
fail(WORKSPACE_USAGE);
|
|
@@ -269,6 +274,13 @@ async function cmdWorkspace(argv, deps = {}) {
|
|
|
269
274
|
if (action === "set") {
|
|
270
275
|
const workspace = await workspaceSet(rest);
|
|
271
276
|
console.log(`${workspace.slug} ${workspace.name} ${workspace.repo}`);
|
|
277
|
+
try {
|
|
278
|
+
const permission = await runnerRepoPermission(workspace.repo, HDX_RUNNER_GH_USER, deps.gh ?? defaultGh);
|
|
279
|
+
if (!hasWriteRepoPermission(permission)) {
|
|
280
|
+
console.log(`hint: runner ${HDX_RUNNER_GH_USER} has ${permission} permission; run hd workspace grant-runner-access`);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
catch { }
|
|
272
284
|
return;
|
|
273
285
|
}
|
|
274
286
|
if (action !== "new")
|
package/dist/out.js
CHANGED
|
@@ -67,7 +67,7 @@ export function usage() {
|
|
|
67
67
|
` ${c.blue("hd ticket list | show | new | queue | cancel")} ticket operations`,
|
|
68
68
|
` ${c.blue("hd epic new PATH | list | approve | rm")} epic operations`,
|
|
69
69
|
` ${c.blue("hd plan")} use /architect in the TUI`,
|
|
70
|
-
` ${c.blue("hd workspace ls | use | new | set | rotate-key")} workspace operations`,
|
|
70
|
+
` ${c.blue("hd workspace ls | use | new | set | rotate-key | grant-runner-access")} workspace operations`,
|
|
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`,
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { createWorkspace, listWorkspaces, rotateWorkspaceApiKey, updateWorkspace } from "./api.js";
|
|
2
2
|
import { loadConfig, switchWorkspace, writeHdConfig } from "./config.js";
|
|
3
3
|
import { promptOnStdin } from "./prompt.js";
|
|
4
|
-
import { defaultGh, HDX_RUNNER_GH_USER, preflightWorkspace } from "./workspace-preflight.js";
|
|
5
|
-
export const WORKSPACE_USAGE = "usage: hd workspace ls | use SLUG | new --name NAME --repo OWNER/NAME [--create|--no-create] [flags] | set [flags] | rotate-key";
|
|
4
|
+
import { defaultGh, hasWriteRepoPermission, HDX_RUNNER_GH_USER, preflightWorkspace, runnerAccessForRepoOwner, runnerRepoPermission } from "./workspace-preflight.js";
|
|
5
|
+
export const WORKSPACE_USAGE = "usage: hd workspace ls | use SLUG | new --name NAME --repo OWNER/NAME [--create|--no-create] [flags] | set [flags] | rotate-key | grant-runner-access [--runner-user USER]";
|
|
6
6
|
function flags(argv) {
|
|
7
7
|
const opts = {};
|
|
8
8
|
const bools = new Set();
|
|
@@ -165,6 +165,33 @@ export async function workspaceUse(argv) {
|
|
|
165
165
|
switchWorkspace(selected.slug);
|
|
166
166
|
return selected;
|
|
167
167
|
}
|
|
168
|
+
export async function workspaceGrantRunnerAccess(argv, deps = {}) {
|
|
169
|
+
const { opts, bools, rest } = flags(argv);
|
|
170
|
+
if (rest.length || bools.size || Object.keys(opts).some((name) => name !== "runner-user")) {
|
|
171
|
+
throw new Error(WORKSPACE_USAGE);
|
|
172
|
+
}
|
|
173
|
+
const runnerUser = opts["runner-user"] ?? HDX_RUNNER_GH_USER;
|
|
174
|
+
const current = loadConfig().slug;
|
|
175
|
+
const workspace = (await listWorkspaces()).find((item) => item.slug === current);
|
|
176
|
+
if (!workspace)
|
|
177
|
+
throw new Error(`No workspace ${current}.`);
|
|
178
|
+
const gh = deps.gh ?? defaultGh;
|
|
179
|
+
try {
|
|
180
|
+
await gh(["--version"]);
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
throw new Error("GitHub CLI is unavailable. Install it if needed, then run `gh auth login`.");
|
|
184
|
+
}
|
|
185
|
+
const access = await runnerAccessForRepoOwner(workspace.repo, gh);
|
|
186
|
+
await gh(["api", "-X", "PUT", `repos/${workspace.repo}/collaborators/${runnerUser}`,
|
|
187
|
+
"-f", `permission=${access}`]);
|
|
188
|
+
const permission = await runnerRepoPermission(workspace.repo, runnerUser, gh);
|
|
189
|
+
if (!hasWriteRepoPermission(permission)) {
|
|
190
|
+
throw new Error(`Runner ${runnerUser} has ${permission} permission on ${workspace.repo}, expected write or better.`);
|
|
191
|
+
}
|
|
192
|
+
return { repo: workspace.repo, runnerUser, permission, requestedPermission: access };
|
|
193
|
+
}
|
|
194
|
+
export const workspaceGrantRunnerAdmin = workspaceGrantRunnerAccess;
|
|
168
195
|
export async function workspaceRotateKey() {
|
|
169
196
|
const config = loadConfig();
|
|
170
197
|
const { api_key } = await rotateWorkspaceApiKey(config);
|
|
@@ -52,6 +52,25 @@ export async function requireAdminRepo(repo, gh = defaultGh) {
|
|
|
52
52
|
throw new Error(`Repository admin permission is required; viewer has ${permission || "none"}.`);
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
|
+
export async function runnerRepoPermission(repo, runnerUser = HDX_RUNNER_GH_USER, gh = defaultGh) {
|
|
56
|
+
try {
|
|
57
|
+
const result = JSON.parse(await gh(["api", `repos/${repo}/collaborators/${runnerUser}/permission`]));
|
|
58
|
+
return result.permission ?? "none";
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
if (/404|not found/i.test(errorMessage(error)))
|
|
62
|
+
return "none";
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
export function hasWriteRepoPermission(permission) {
|
|
67
|
+
return ["admin", "maintain", "write", "push"].includes(permission.toLowerCase());
|
|
68
|
+
}
|
|
69
|
+
export async function runnerAccessForRepoOwner(repo, gh = defaultGh) {
|
|
70
|
+
const owner = repo.split("/")[0];
|
|
71
|
+
const ownerType = (await gh(["api", `users/${owner}`, "--jq", ".type"])).trim();
|
|
72
|
+
return ownerType.toLowerCase() === "organization" ? "admin" : "push";
|
|
73
|
+
}
|
|
55
74
|
async function repoView(repo, gh) {
|
|
56
75
|
return JSON.parse(await gh(["repo", "view", repo, "--json", "defaultBranchRef,isEmpty,viewerPermission"]));
|
|
57
76
|
}
|
|
@@ -82,18 +101,11 @@ export async function preflightWorkspace(input, gh = defaultGh) {
|
|
|
82
101
|
if (!branch)
|
|
83
102
|
throw new Error(`GitHub repository ${input.repo} has no default branch.`);
|
|
84
103
|
const runnerUser = input.runnerUser || HDX_RUNNER_GH_USER;
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const result = JSON.parse(await gh(["api", `repos/${input.repo}/collaborators/${runnerUser}/permission`]));
|
|
88
|
-
permission = result.permission ?? "none";
|
|
89
|
-
}
|
|
90
|
-
catch (error) {
|
|
91
|
-
if (!/404|not found/i.test(errorMessage(error)))
|
|
92
|
-
throw error;
|
|
93
|
-
}
|
|
94
|
-
const invitationPending = permission.toLowerCase() !== "admin";
|
|
104
|
+
const permission = await runnerRepoPermission(input.repo, runnerUser, gh);
|
|
105
|
+
const invitationPending = !hasWriteRepoPermission(permission);
|
|
95
106
|
if (invitationPending) {
|
|
96
|
-
|
|
107
|
+
const access = await runnerAccessForRepoOwner(input.repo, gh);
|
|
108
|
+
await gh(["api", "-X", "PUT", `repos/${input.repo}/collaborators/${runnerUser}`, "-f", `permission=${access}`]);
|
|
97
109
|
}
|
|
98
110
|
return { branch, invitationPending };
|
|
99
111
|
}
|