@higherdev/cli 0.12.0 → 0.12.1
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 -10
- package/dist/host.js +41 -20
- package/dist/index.js +10 -2
- package/dist/login.js +41 -0
- package/dist/out.js +3 -1
- package/dist/tui/App.js +2 -2
- package/dist/workspace-commands.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,15 +8,10 @@ Operator controls for HDX workspaces, tickets, agents, and runner hosts.
|
|
|
8
8
|
npm install --global @higherdev/cli
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
Configure this client with an existing workspace API key:
|
|
12
12
|
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
"url": "https://hdx-higher-ops.vercel.app",
|
|
16
|
-
"api_key": "hdx_...",
|
|
17
|
-
"current": "workspace",
|
|
18
|
-
"repos": {}
|
|
19
|
-
}
|
|
13
|
+
```sh
|
|
14
|
+
hd login --url https://hdx-higher-ops.vercel.app --api-key hdx_... --slug workspace
|
|
20
15
|
```
|
|
21
16
|
|
|
22
17
|
One workspace key reaches every workspace. Previous single-workspace and
|
|
@@ -54,8 +49,13 @@ workspace-map config shapes are migrated automatically when they are read.
|
|
|
54
49
|
| `hd decide ID --answer TEXT` | Answer a decision |
|
|
55
50
|
| `hd on` / `hd off` | Turn the workspace on or off |
|
|
56
51
|
| `hd pause` / `hd resume` | Pause or resume the workspace |
|
|
57
|
-
| `hd
|
|
58
|
-
| `hd
|
|
52
|
+
| `hd login --url URL --api-key KEY [--slug S]` | Configure only this operator client |
|
|
53
|
+
| `hd init --host HOST [--checkout DIR] [options]` | Configure this host and runner service |
|
|
54
|
+
| `hd upgrade --host HOST [--checkout DIR] [options]` | Refresh this host configuration |
|
|
55
|
+
|
|
56
|
+
`hd init` uses `~/hdx` as its checkout by default. Pass `--checkout DIR` when the runner source is
|
|
57
|
+
elsewhere. If the runner environment or service unit already exists, inspect the paths it prints and
|
|
58
|
+
pass `--force` only when replacing that host configuration is intentional.
|
|
59
59
|
|
|
60
60
|
`hd workspace new` uses the operator's authenticated `gh`, defaults to the repository's real default
|
|
61
61
|
branch, bootstraps an empty repository unless `--no-bootstrap` is set, and invites `mel-ilotus` unless
|
package/dist/host.js
CHANGED
|
@@ -1,16 +1,11 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
|
-
import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
-
import { homedir
|
|
2
|
+
import { access, chmod, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
import { fileURLToPath } from "node:url";
|
|
6
5
|
import { promisify } from "node:util";
|
|
7
|
-
import { writeHdConfig } from "./config.js";
|
|
8
6
|
const execFileAsync = promisify(execFile);
|
|
9
7
|
export const LAUNCHD_LABEL = "io.higherops.hdx-runner";
|
|
10
8
|
export const SYSTEMD_UNIT = "hdx-runner.service";
|
|
11
|
-
export function repoRootFrom(fileUrl = import.meta.url) {
|
|
12
|
-
return path.resolve(path.dirname(fileURLToPath(fileUrl)), "../../..");
|
|
13
|
-
}
|
|
14
9
|
export function parseEnvFile(text) {
|
|
15
10
|
const out = {};
|
|
16
11
|
for (const line of text.split(/\r?\n/)) {
|
|
@@ -88,6 +83,10 @@ export function parseHostFlags(argv) {
|
|
|
88
83
|
};
|
|
89
84
|
if (arg === "--host")
|
|
90
85
|
flags.host = next();
|
|
86
|
+
else if (arg === "--checkout")
|
|
87
|
+
flags.checkout = next();
|
|
88
|
+
else if (arg === "--force")
|
|
89
|
+
flags.force = true;
|
|
91
90
|
else if (arg === "--env-out")
|
|
92
91
|
flags.envFile = next();
|
|
93
92
|
else if (arg === "--worktree-root")
|
|
@@ -96,12 +95,6 @@ export function parseHostFlags(argv) {
|
|
|
96
95
|
flags.installLaunchd = true;
|
|
97
96
|
else if (arg === "--install-systemd")
|
|
98
97
|
flags.installSystemd = true;
|
|
99
|
-
else if (arg === "--url")
|
|
100
|
-
flags.url = next();
|
|
101
|
-
else if (arg === "--api-key")
|
|
102
|
-
flags.apiKey = next();
|
|
103
|
-
else if (arg === "--slug")
|
|
104
|
-
flags.slug = next();
|
|
105
98
|
else if (arg === "--plist-out")
|
|
106
99
|
flags.plistOut = next();
|
|
107
100
|
else if (arg === "--unit-out")
|
|
@@ -111,14 +104,47 @@ export function parseHostFlags(argv) {
|
|
|
111
104
|
}
|
|
112
105
|
return flags;
|
|
113
106
|
}
|
|
107
|
+
async function exists(file) {
|
|
108
|
+
try {
|
|
109
|
+
await access(file);
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function expandHome(value, home) {
|
|
117
|
+
if (value === "~")
|
|
118
|
+
return home;
|
|
119
|
+
if (value.startsWith("~/"))
|
|
120
|
+
return path.join(home, value.slice(2));
|
|
121
|
+
return value;
|
|
122
|
+
}
|
|
114
123
|
export async function initHost(flags, env = process.env) {
|
|
115
124
|
const home = homedir();
|
|
116
|
-
const host = flags.host || env.RUNNER_HOST || hostname().replace(/\..*$/, "").toLowerCase();
|
|
117
125
|
const envFile = flags.envFile || path.join(home, ".config/hdx/runner.env");
|
|
118
126
|
const worktreeRoot = flags.worktreeRoot || env.WORKTREE_ROOT || path.join(home, "hdx-worktrees");
|
|
119
|
-
const repo =
|
|
127
|
+
const repo = path.resolve(expandHome(flags.checkout ?? path.join(home, "hdx"), home));
|
|
120
128
|
const runnerPath = path.join(repo, "apps/runner/src/index.ts");
|
|
121
129
|
const nodePath = process.execPath;
|
|
130
|
+
const plistPath = flags.plistOut || path.join(home, "Library/LaunchAgents", `${LAUNCHD_LABEL}.plist`);
|
|
131
|
+
const unitPath = flags.unitOut || path.join(home, ".config/systemd/user", SYSTEMD_UNIT);
|
|
132
|
+
const targets = [envFile];
|
|
133
|
+
if (process.platform === "darwin" || flags.plistOut || flags.installLaunchd)
|
|
134
|
+
targets.push(plistPath);
|
|
135
|
+
if (process.platform === "linux" || flags.unitOut || flags.installSystemd)
|
|
136
|
+
targets.push(unitPath);
|
|
137
|
+
const existingTargets = (await Promise.all(targets.map(async (target) => ({ target, exists: await exists(target) })))).filter((entry) => entry.exists).map((entry) => entry.target);
|
|
138
|
+
if (existingTargets.length && !flags.force) {
|
|
139
|
+
throw new Error([
|
|
140
|
+
"hd init refused to overwrite existing runner files:",
|
|
141
|
+
...existingTargets.map((target) => ` ${target}`),
|
|
142
|
+
"Re-run with --force to overwrite them.",
|
|
143
|
+
].join("\n"));
|
|
144
|
+
}
|
|
145
|
+
if (!flags.host)
|
|
146
|
+
throw new Error("hd init requires --host HOST.");
|
|
147
|
+
const host = flags.host;
|
|
122
148
|
let existing = {};
|
|
123
149
|
try {
|
|
124
150
|
existing = parseEnvFile(await readFile(envFile, "utf8"));
|
|
@@ -137,13 +163,9 @@ export async function initHost(flags, env = process.env) {
|
|
|
137
163
|
await mkdir(path.dirname(envFile), { recursive: true });
|
|
138
164
|
await writeFile(envFile, formatEnvFile(merged), { encoding: "utf8", mode: 0o600 });
|
|
139
165
|
await chmod(envFile, 0o600);
|
|
140
|
-
if (flags.url && flags.apiKey && flags.slug) {
|
|
141
|
-
writeHdConfig({ url: flags.url.replace(/\/$/, ""), api_key: flags.apiKey, slug: flags.slug });
|
|
142
|
-
}
|
|
143
166
|
const lines = [`hd init host=${host}`, `env: ${envFile}`];
|
|
144
167
|
const workDir = repo;
|
|
145
168
|
if (process.platform === "darwin" || flags.plistOut || flags.installLaunchd) {
|
|
146
|
-
const plistPath = flags.plistOut || path.join(home, "Library/LaunchAgents", `${LAUNCHD_LABEL}.plist`);
|
|
147
169
|
const logPath = path.join(home, "Library/Logs/hdx-runner.log");
|
|
148
170
|
await mkdir(path.dirname(plistPath), { recursive: true });
|
|
149
171
|
await writeFile(plistPath, launchdPlist({ nodePath, runnerPath, envFile, logPath, workDir }), "utf8");
|
|
@@ -163,7 +185,6 @@ export async function initHost(flags, env = process.env) {
|
|
|
163
185
|
lines.push(`launchd plist: ${plistPath}${loaded ? " (loaded)" : ""}`);
|
|
164
186
|
}
|
|
165
187
|
if (process.platform === "linux" || flags.unitOut || flags.installSystemd) {
|
|
166
|
-
const unitPath = flags.unitOut || path.join(home, ".config/systemd/user", SYSTEMD_UNIT);
|
|
167
188
|
await mkdir(path.dirname(unitPath), { recursive: true });
|
|
168
189
|
await writeFile(unitPath, systemdUnit({ nodePath, runnerPath, envFile, workDir }), "utf8");
|
|
169
190
|
let loaded = false;
|
package/dist/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import { realpathSync } from "node:fs";
|
|
|
3
3
|
import { pathToFileURL } from "node:url";
|
|
4
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
|
+
import { login, parseLoginFlags } from "./login.js";
|
|
6
7
|
import { loadConfig } from "./config.js";
|
|
7
8
|
import { epicProgressRows, readEpicSpec } from "./epics.js";
|
|
8
9
|
import { banner, c, statusChip, table, truncate, usage } from "./out.js";
|
|
@@ -256,7 +257,7 @@ async function cmdWorkspace(argv, deps = {}) {
|
|
|
256
257
|
fail(WORKSPACE_USAGE);
|
|
257
258
|
const result = await workspaceRotateKey();
|
|
258
259
|
console.log(`api key: ${result.apiKey}`);
|
|
259
|
-
console.log("saved locally;
|
|
260
|
+
console.log("saved locally; run hd login with the new key on other machines.");
|
|
260
261
|
return;
|
|
261
262
|
}
|
|
262
263
|
if (action === "set") {
|
|
@@ -271,7 +272,7 @@ async function cmdWorkspace(argv, deps = {}) {
|
|
|
271
272
|
console.log(`invitation pending for ${result.runnerUser}`);
|
|
272
273
|
console.log(`workspace: ${result.slug}`);
|
|
273
274
|
console.log("saved and switched; configure another machine with:");
|
|
274
|
-
console.log(result.
|
|
275
|
+
console.log(result.loginCommand);
|
|
275
276
|
}
|
|
276
277
|
function selectAgent(agents, target, provider) {
|
|
277
278
|
const exact = agents.find((agent) => agent.id === target);
|
|
@@ -414,6 +415,13 @@ export async function main(argv = process.argv.slice(2), deps = {}) {
|
|
|
414
415
|
console.log(cmd === "on" ? "on" : "resumed");
|
|
415
416
|
return;
|
|
416
417
|
}
|
|
418
|
+
if (cmd === "login") {
|
|
419
|
+
const config = login(parseLoginFlags(rest));
|
|
420
|
+
console.log(`logged in to ${config.slug}`);
|
|
421
|
+
const { launchApp } = await import("./tui/launch.js");
|
|
422
|
+
await launchApp(config.slug);
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
417
425
|
if (cmd === "init" || cmd === "upgrade") {
|
|
418
426
|
const report = await initHost(parseHostFlags(rest));
|
|
419
427
|
console.log(report);
|
package/dist/login.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { loadStoredConfig, writeHdConfig } from "./config.js";
|
|
2
|
+
export function parseLoginFlags(argv) {
|
|
3
|
+
const flags = {};
|
|
4
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
5
|
+
const arg = argv[i];
|
|
6
|
+
const next = () => {
|
|
7
|
+
const value = argv[i + 1];
|
|
8
|
+
if (!value || value.startsWith("--"))
|
|
9
|
+
throw new Error(`Missing value for ${arg}`);
|
|
10
|
+
i += 1;
|
|
11
|
+
return value;
|
|
12
|
+
};
|
|
13
|
+
if (arg === "--url")
|
|
14
|
+
flags.url = next();
|
|
15
|
+
else if (arg === "--api-key")
|
|
16
|
+
flags.apiKey = next();
|
|
17
|
+
else if (arg === "--slug")
|
|
18
|
+
flags.slug = next();
|
|
19
|
+
else
|
|
20
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
21
|
+
}
|
|
22
|
+
return flags;
|
|
23
|
+
}
|
|
24
|
+
/** Configure the operator client without touching runner host files. */
|
|
25
|
+
export function login(flags, path) {
|
|
26
|
+
if (!flags.url || !flags.apiKey) {
|
|
27
|
+
throw new Error("usage: hd login --url URL --api-key KEY [--slug S]");
|
|
28
|
+
}
|
|
29
|
+
let current = "workspace";
|
|
30
|
+
try {
|
|
31
|
+
current = loadStoredConfig(path).current;
|
|
32
|
+
}
|
|
33
|
+
catch { }
|
|
34
|
+
const config = {
|
|
35
|
+
url: flags.url,
|
|
36
|
+
api_key: flags.apiKey,
|
|
37
|
+
slug: flags.slug ?? current,
|
|
38
|
+
};
|
|
39
|
+
writeHdConfig(config, path);
|
|
40
|
+
return config;
|
|
41
|
+
}
|
package/dist/out.js
CHANGED
|
@@ -74,6 +74,8 @@ export function usage() {
|
|
|
74
74
|
` ${c.blue("hd msg KEY TEXT")} message a builder`,
|
|
75
75
|
` ${c.blue("hd decide [ID --answer TEXT]")} decisions`,
|
|
76
76
|
` ${c.blue("hd on | hd off")} workspace switch`,
|
|
77
|
-
` ${c.blue("hd
|
|
77
|
+
` ${c.blue("hd login --url URL --api-key KEY")} client setup`,
|
|
78
|
+
` ${c.blue("hd init --host HOST [options]")} host setup`,
|
|
79
|
+
` ${c.blue("hd upgrade --host HOST [options]")} refresh host setup`,
|
|
78
80
|
].join("\n");
|
|
79
81
|
}
|
package/dist/tui/App.js
CHANGED
|
@@ -312,7 +312,7 @@ export function App({ initial }) {
|
|
|
312
312
|
if (!created)
|
|
313
313
|
throw new Error("Workspace wizard did not finish.");
|
|
314
314
|
await changeWorkspace(created.slug, loadConfig());
|
|
315
|
-
say("system", `Created ${created.slug}. Configure
|
|
315
|
+
say("system", `Created ${created.slug}. Configure another machine with: ${created.loginCommand}`);
|
|
316
316
|
}
|
|
317
317
|
else if (action.command === "set") {
|
|
318
318
|
const updated = await workspaceSet(action.args);
|
|
@@ -322,7 +322,7 @@ export function App({ initial }) {
|
|
|
322
322
|
else {
|
|
323
323
|
const rotated = await workspaceRotateKey();
|
|
324
324
|
setConfig(loadConfig());
|
|
325
|
-
say("system", `API key: ${rotated.apiKey}\nSaved locally.
|
|
325
|
+
say("system", `API key: ${rotated.apiKey}\nSaved locally. Run hd login with the new key on other machines.`);
|
|
326
326
|
}
|
|
327
327
|
}
|
|
328
328
|
catch (error) {
|
|
@@ -115,7 +115,7 @@ export async function workspaceNew(argv, deps = {}) {
|
|
|
115
115
|
return { slug: result.workspace.slug, repo: result.workspace.repo,
|
|
116
116
|
invitationPending: preflight.invitationPending,
|
|
117
117
|
runnerUser: opts["runner-user"] ?? HDX_RUNNER_GH_USER,
|
|
118
|
-
|
|
118
|
+
loginCommand: `hd login --url ${result.url} --api-key ${result.api_key} --slug ${result.workspace.slug}` };
|
|
119
119
|
}
|
|
120
120
|
}
|
|
121
121
|
export async function workspaceSet(argv) {
|