@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 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
- Create `~/.config/hd/config.json` with an existing workspace API key:
11
+ Configure this client with an existing workspace API key:
12
12
 
13
- ```json
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 init [options]` | Configure this host and runner service |
58
- | `hd upgrade [options]` | Refresh this host configuration |
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, hostname } from "node:os";
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 = repoRootFrom();
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; update Mel's hd init configuration on the box.");
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.initCommand);
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 init | hd upgrade")} host setup`,
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 the box with: ${created.initCommand}`);
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. Update Mel's hd init configuration on the box.`);
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
- initCommand: `hd init --url ${result.url} --api-key ${result.api_key} --slug ${result.workspace.slug}` };
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@higherdev/cli",
3
- "version": "0.12.0",
3
+ "version": "0.12.1",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "hd": "dist/index.js"