@higherdev/cli 0.5.0 → 0.6.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 +6 -8
- package/dist/api.js +20 -3
- package/dist/config.js +28 -33
- package/dist/index.js +15 -3
- package/dist/out.js +1 -1
- package/dist/tui/App.js +31 -8
- package/dist/tui/Dashboard.js +1 -1
- package/dist/tui/data.js +12 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,17 +12,14 @@ Create `~/.config/hd/config.json` with an existing workspace API key:
|
|
|
12
12
|
|
|
13
13
|
```json
|
|
14
14
|
{
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
|
|
18
|
-
"url": "https://hdx-higher-ops.vercel.app",
|
|
19
|
-
"api_key": "hdx_..."
|
|
20
|
-
}
|
|
21
|
-
}
|
|
15
|
+
"url": "https://hdx-higher-ops.vercel.app",
|
|
16
|
+
"api_key": "hdx_...",
|
|
17
|
+
"current": "workspace"
|
|
22
18
|
}
|
|
23
19
|
```
|
|
24
20
|
|
|
25
|
-
|
|
21
|
+
One workspace key reaches every workspace. Previous single-workspace and
|
|
22
|
+
workspace-map config shapes are migrated automatically when they are read.
|
|
26
23
|
|
|
27
24
|
## Commands
|
|
28
25
|
|
|
@@ -34,6 +31,7 @@ The previous single-workspace config shape is migrated automatically when it is
|
|
|
34
31
|
| `hd ticket list` | List tickets |
|
|
35
32
|
| `hd ticket show KEY` | Show one ticket |
|
|
36
33
|
| `hd ticket new --title TITLE [options]` | Create a ticket |
|
|
34
|
+
| `hd workspace ls` | List every workspace available to the configured key |
|
|
37
35
|
| `hd workspace new --name NAME --repo OWNER/NAME [options]` | Create a paused workspace |
|
|
38
36
|
| `hd agents` | List agents |
|
|
39
37
|
| `hd agents set ROLE --provider P --model M [--effort E]` | Update an agent |
|
package/dist/api.js
CHANGED
|
@@ -1,4 +1,19 @@
|
|
|
1
1
|
import { loadConfig } from "./config.js";
|
|
2
|
+
export const MAX_API_ERROR_BODY = 500;
|
|
3
|
+
export function apiErrorMessage(status, parsed, fallback) {
|
|
4
|
+
const raw = parsed && typeof parsed === "object" && "error" in parsed
|
|
5
|
+
&& typeof parsed.error === "string"
|
|
6
|
+
? parsed.error : fallback;
|
|
7
|
+
const duplicate = raw.match(/agents_one_enabled_(orchestrator|reviewer)_idx/i)?.[1]?.toLowerCase();
|
|
8
|
+
if (duplicate) {
|
|
9
|
+
return `hd: ${status} Only one enabled ${duplicate} is allowed per workspace. Disable the current ${duplicate} before enabling another.`;
|
|
10
|
+
}
|
|
11
|
+
const trimmed = raw.trim();
|
|
12
|
+
const body = trimmed.length > MAX_API_ERROR_BODY
|
|
13
|
+
? `${trimmed.slice(0, MAX_API_ERROR_BODY)}... [truncated ${trimmed.length - MAX_API_ERROR_BODY} chars]`
|
|
14
|
+
: trimmed;
|
|
15
|
+
return `hd: ${status} ${body}`;
|
|
16
|
+
}
|
|
2
17
|
async function request(config, method, path, body) {
|
|
3
18
|
const response = await fetch(`${config.url}${path}`, {
|
|
4
19
|
method,
|
|
@@ -14,11 +29,10 @@ async function request(config, method, path, body) {
|
|
|
14
29
|
parsed = text ? JSON.parse(text) : null;
|
|
15
30
|
}
|
|
16
31
|
catch {
|
|
17
|
-
throw new Error(
|
|
32
|
+
throw new Error(apiErrorMessage(response.status, null, text || response.statusText));
|
|
18
33
|
}
|
|
19
34
|
if (!response.ok) {
|
|
20
|
-
|
|
21
|
-
throw new Error(`hd: ${response.status} ${err.error ?? text}`);
|
|
35
|
+
throw new Error(apiErrorMessage(response.status, parsed, text || response.statusText));
|
|
22
36
|
}
|
|
23
37
|
return parsed;
|
|
24
38
|
}
|
|
@@ -57,6 +71,9 @@ export async function createWorkspace(fields, config = loadConfig()) {
|
|
|
57
71
|
const result = await request(config, "POST", "/api/workspaces", fields);
|
|
58
72
|
return { ...result, url: config.url };
|
|
59
73
|
}
|
|
74
|
+
export async function listWorkspaces(config = loadConfig()) {
|
|
75
|
+
return request(config, "GET", "/api/workspaces");
|
|
76
|
+
}
|
|
60
77
|
export async function listAgents(config = loadConfig()) {
|
|
61
78
|
return request(config, "GET", `/api/w/${config.slug}/agents`);
|
|
62
79
|
}
|
package/dist/config.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { homedir } from "node:os";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
|
-
import { chmodSync,
|
|
3
|
+
import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
4
|
export function configPath() {
|
|
5
5
|
return process.env.HD_CONFIG ?? join(homedir(), ".config/hd/config.json");
|
|
6
6
|
}
|
|
7
|
-
function
|
|
7
|
+
function normalizeConnection(value) {
|
|
8
8
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
9
9
|
return null;
|
|
10
10
|
const row = value;
|
|
@@ -14,7 +14,7 @@ function normalizeWorkspace(value) {
|
|
|
14
14
|
return { url: row.url.replace(/\/$/, ""), api_key: row.api_key };
|
|
15
15
|
}
|
|
16
16
|
function missing(path) {
|
|
17
|
-
return new Error(`missing ${path}\nWrite { "
|
|
17
|
+
return new Error(`missing ${path}\nWrite { "url": "https://hdx-higher-ops.vercel.app", "api_key": "hdx_...", "current": "workspace" }`);
|
|
18
18
|
}
|
|
19
19
|
export function loadStoredConfig(path = configPath()) {
|
|
20
20
|
let raw;
|
|
@@ -25,56 +25,51 @@ export function loadStoredConfig(path = configPath()) {
|
|
|
25
25
|
throw missing(path);
|
|
26
26
|
}
|
|
27
27
|
const parsed = JSON.parse(raw);
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
28
|
+
const connection = normalizeConnection(parsed);
|
|
29
|
+
const current = typeof parsed.current === "string" ? parsed.current : "";
|
|
30
|
+
if (connection && current)
|
|
31
|
+
return { ...connection, current };
|
|
32
|
+
// 0.4 stored one workspace at the top level with `slug` rather than
|
|
33
|
+
// `current`. One key can now reach every workspace, so only the selected
|
|
34
|
+
// slug needs to survive.
|
|
35
|
+
if (connection && typeof parsed.slug === "string" && parsed.slug) {
|
|
36
|
+
const migrated = { ...connection, current: parsed.slug };
|
|
33
37
|
writeStoredConfig(migrated, path);
|
|
34
38
|
return migrated;
|
|
35
39
|
}
|
|
36
|
-
|
|
40
|
+
// 0.5 stored one connection per workspace. Keep the current workspace's
|
|
41
|
+
// credentials as the shared operator connection and discard the duplicates.
|
|
37
42
|
const source = parsed.workspaces;
|
|
38
43
|
if (!current || !source || typeof source !== "object" || Array.isArray(source)) {
|
|
39
|
-
throw new Error(`${path} needs
|
|
40
|
-
}
|
|
41
|
-
const workspaces = {};
|
|
42
|
-
for (const [slug, value] of Object.entries(source)) {
|
|
43
|
-
const workspace = normalizeWorkspace(value);
|
|
44
|
-
if (workspace)
|
|
45
|
-
workspaces[slug] = workspace;
|
|
44
|
+
throw new Error(`${path} needs url, api_key, and current`);
|
|
46
45
|
}
|
|
47
|
-
|
|
46
|
+
const selected = normalizeConnection(source[current]);
|
|
47
|
+
if (!selected)
|
|
48
48
|
throw new Error(`${path} current workspace ${current} is not configured`);
|
|
49
|
-
|
|
49
|
+
const migrated = { ...selected, current };
|
|
50
|
+
writeStoredConfig(migrated, path);
|
|
51
|
+
return migrated;
|
|
50
52
|
}
|
|
51
53
|
export function loadConfig(slug) {
|
|
52
54
|
const stored = loadStoredConfig();
|
|
53
55
|
const selected = slug ?? stored.current;
|
|
54
|
-
|
|
55
|
-
if (!workspace)
|
|
56
|
-
throw new Error(`No configured workspace ${selected}.`);
|
|
57
|
-
return { ...workspace, slug: selected };
|
|
56
|
+
return { url: stored.url, api_key: stored.api_key, slug: selected };
|
|
58
57
|
}
|
|
59
58
|
export function writeStoredConfig(config, path = configPath()) {
|
|
60
59
|
mkdirSync(dirname(path), { recursive: true });
|
|
61
60
|
writeFileSync(path, `${JSON.stringify(config, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
62
61
|
chmodSync(path, 0o600);
|
|
63
62
|
}
|
|
64
|
-
/** Store
|
|
63
|
+
/** Store one operator connection and make its selected workspace current. */
|
|
65
64
|
export function writeHdConfig(config, path = configPath()) {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
:
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
writeStoredConfig(stored, path);
|
|
65
|
+
writeStoredConfig({
|
|
66
|
+
url: config.url.replace(/\/$/, ""),
|
|
67
|
+
api_key: config.api_key,
|
|
68
|
+
current: config.slug,
|
|
69
|
+
}, path);
|
|
72
70
|
}
|
|
73
71
|
export function switchWorkspace(slug, path = configPath()) {
|
|
74
72
|
const stored = loadStoredConfig(path);
|
|
75
|
-
const workspace = stored.workspaces[slug];
|
|
76
|
-
if (!workspace)
|
|
77
|
-
throw new Error(`No configured workspace ${slug}.`);
|
|
78
73
|
writeStoredConfig({ ...stored, current: slug }, path);
|
|
79
|
-
return {
|
|
74
|
+
return { url: stored.url, api_key: stored.api_key, slug };
|
|
80
75
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { realpathSync } from "node:fs";
|
|
3
3
|
import { pathToFileURL } from "node:url";
|
|
4
|
-
import { answerDecision, createTicket, createWorkspace, getStatus, listAgents, listTicketEvents, listTickets, postMessage, setPaused, showTicket, updateAgent, } from "./api.js";
|
|
4
|
+
import { answerDecision, createTicket, createWorkspace, getStatus, listAgents, listTicketEvents, listTickets, listWorkspaces, postMessage, setPaused, showTicket, updateAgent, } from "./api.js";
|
|
5
5
|
import { initHost, parseHostFlags } from "./host.js";
|
|
6
|
-
import { writeHdConfig } from "./config.js";
|
|
6
|
+
import { loadConfig, writeHdConfig } from "./config.js";
|
|
7
7
|
import { banner, c, statusChip, table, truncate, usage } from "./out.js";
|
|
8
8
|
function fail(message) {
|
|
9
9
|
console.error(message);
|
|
@@ -176,8 +176,20 @@ async function cmdDecide(argv) {
|
|
|
176
176
|
}
|
|
177
177
|
async function cmdWorkspace(argv) {
|
|
178
178
|
const [action, ...rest] = argv;
|
|
179
|
+
if (action === "ls") {
|
|
180
|
+
const current = loadConfig().slug;
|
|
181
|
+
const workspaces = await listWorkspaces();
|
|
182
|
+
console.log(table(["", "SLUG", "NAME", "REPO", "STATE"], workspaces.map((workspace) => [
|
|
183
|
+
workspace.slug === current ? "*" : "",
|
|
184
|
+
workspace.slug,
|
|
185
|
+
workspace.name,
|
|
186
|
+
workspace.repo,
|
|
187
|
+
workspace.on ? c.green("on") : c.dim("off"),
|
|
188
|
+
])));
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
179
191
|
if (action !== "new")
|
|
180
|
-
fail("usage: hd workspace new --name NAME --repo OWNER/NAME [--slug S] [--branch main] [--host box]");
|
|
192
|
+
fail("usage: hd workspace ls | new --name NAME --repo OWNER/NAME [--slug S] [--branch main] [--host box]");
|
|
181
193
|
const { opts } = flags(rest);
|
|
182
194
|
if (!opts.name || !opts.repo) {
|
|
183
195
|
fail("usage: hd workspace new --name NAME --repo OWNER/NAME [--slug S] [--branch main] [--host box]");
|
package/dist/out.js
CHANGED
|
@@ -65,7 +65,7 @@ export function usage() {
|
|
|
65
65
|
c.bold("Usage"),
|
|
66
66
|
` ${c.blue("hd status")} workspace overview`,
|
|
67
67
|
` ${c.blue("hd ticket list | show | new")} ticket operations`,
|
|
68
|
-
` ${c.blue("hd workspace new")}
|
|
68
|
+
` ${c.blue("hd workspace ls | new")} list or create workspaces`,
|
|
69
69
|
` ${c.blue("hd agents [set]")} inspect or update agents`,
|
|
70
70
|
` ${c.blue("hd logs KEY [-f]")} run events`,
|
|
71
71
|
` ${c.blue("hd msg KEY TEXT")} message a builder`,
|
package/dist/tui/App.js
CHANGED
|
@@ -57,6 +57,23 @@ export function App({ initial }) {
|
|
|
57
57
|
if (messages.length > 0 || view !== "home")
|
|
58
58
|
setStarted(true);
|
|
59
59
|
}, [messages.length, view]);
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
if (!ready)
|
|
62
|
+
return;
|
|
63
|
+
const live = planLayout({
|
|
64
|
+
rows,
|
|
65
|
+
columns,
|
|
66
|
+
width,
|
|
67
|
+
splash: false,
|
|
68
|
+
ready: true,
|
|
69
|
+
decision: decisionRows(board.decisions),
|
|
70
|
+
inFlight: 0,
|
|
71
|
+
notice: false,
|
|
72
|
+
home: true,
|
|
73
|
+
});
|
|
74
|
+
if (live.cockpit > 0)
|
|
75
|
+
setStarted(true);
|
|
76
|
+
}, [ready, rows, columns, width, board.decisions]);
|
|
60
77
|
const applySnapshot = useCallback((snapshot) => {
|
|
61
78
|
const token = loads.current.start(snapshot.workspace.id);
|
|
62
79
|
if (!loads.current.isCurrent(token))
|
|
@@ -144,13 +161,9 @@ export function App({ initial }) {
|
|
|
144
161
|
}
|
|
145
162
|
}, [settings, config, refresh]);
|
|
146
163
|
const changeWorkspace = useCallback(async (slug) => {
|
|
147
|
-
if (!configuredSlugs().includes(slug)) {
|
|
148
|
-
setNotice(`No configured workspace ${slug}. You can reach: ${configuredSlugs().join(", ")}.`);
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
164
|
setBusy(true);
|
|
152
165
|
try {
|
|
153
|
-
const snapshot = await switchWorkspace(slug);
|
|
166
|
+
const snapshot = await switchWorkspace(slug, config);
|
|
154
167
|
loads.current.switchTo(snapshot.workspace.id);
|
|
155
168
|
setConfig(snapshot.config);
|
|
156
169
|
setWorkspace(snapshot.workspace);
|
|
@@ -265,10 +278,20 @@ export function App({ initial }) {
|
|
|
265
278
|
}
|
|
266
279
|
return;
|
|
267
280
|
case "workspace":
|
|
268
|
-
if (
|
|
269
|
-
say("system", `Workspaces: ${configuredSlugs().join(", ")}.`);
|
|
270
|
-
else
|
|
281
|
+
if (action.slug)
|
|
271
282
|
await changeWorkspace(action.slug);
|
|
283
|
+
else {
|
|
284
|
+
setBusy(true);
|
|
285
|
+
try {
|
|
286
|
+
say("system", `Workspaces: ${(await configuredSlugs(config)).join(", ")}.`);
|
|
287
|
+
}
|
|
288
|
+
catch (error) {
|
|
289
|
+
setNotice(error instanceof Error ? error.message : String(error));
|
|
290
|
+
}
|
|
291
|
+
finally {
|
|
292
|
+
setBusy(false);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
272
295
|
return;
|
|
273
296
|
case "ticket":
|
|
274
297
|
setTicketKey(action.key);
|
package/dist/tui/Dashboard.js
CHANGED
|
@@ -130,7 +130,7 @@ export function AgentsColumn({ board, width, rows, }) {
|
|
|
130
130
|
const name = Math.max(8, Math.min(22, width - 14));
|
|
131
131
|
return (_jsxs(Box, { flexWrap: "nowrap", children: [_jsxs(Text, { color: inkColor(tone), children: [DOT, " "] }), _jsx(Box, { width: name, flexShrink: 0, children: _jsx(Text, { color: UI.text, wrap: "truncate", children: truncate(agent.display_name, name - 1) }) }), _jsx(Text, { color: UI.dim, wrap: "truncate", children: run
|
|
132
132
|
? `${ticket ? `${ticket.key} ` : ""}${elapsed(run.started_at ?? run.created_at)}`
|
|
133
|
-
: blocked || (agent.enabled ? "idle" : "
|
|
133
|
+
: blocked || (agent.enabled ? "idle" : "offline") })] }, agent.id));
|
|
134
134
|
}),
|
|
135
135
|
_jsx(More, { count: ordered.length - shown.length }, "more"),
|
|
136
136
|
] }));
|
package/dist/tui/data.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { answerDecision, getStatus, listAgents, listEpics, listFeed, listMessages, listTicketEvents, listTickets, postMessage as sendMessage, showTicket, updateAgent as patchAgent, updateCaps, } from "../api.js";
|
|
2
|
-
import { loadConfig,
|
|
1
|
+
import { answerDecision, getStatus, listAgents, listEpics, listFeed, listMessages, listTicketEvents, listTickets, listWorkspaces, postMessage as sendMessage, showTicket, updateAgent as patchAgent, updateCaps, } from "../api.js";
|
|
2
|
+
import { loadConfig, switchWorkspace as selectWorkspace, } from "../config.js";
|
|
3
3
|
export const POLL_MS = 5_000;
|
|
4
4
|
export const providers = ["claude", "codex", "gemini", "grok"];
|
|
5
5
|
export const efforts = ["low", "medium", "high"];
|
|
@@ -17,8 +17,8 @@ export const BOARD_COLUMNS = [
|
|
|
17
17
|
"merged",
|
|
18
18
|
"cancelled",
|
|
19
19
|
];
|
|
20
|
-
export function configuredSlugs() {
|
|
21
|
-
return
|
|
20
|
+
export async function configuredSlugs(config = loadConfig()) {
|
|
21
|
+
return (await listWorkspaces(config)).map((workspace) => workspace.slug).sort();
|
|
22
22
|
}
|
|
23
23
|
export async function loadSnapshot(config = loadConfig()) {
|
|
24
24
|
const [status, ticketData, agentData, epicData, feedData] = await Promise.all([
|
|
@@ -83,8 +83,14 @@ export function pollSnapshot(config, onSnapshot, onState, onError) {
|
|
|
83
83
|
void refresh();
|
|
84
84
|
return { refresh, close: () => { closed = true; clearInterval(timer); } };
|
|
85
85
|
}
|
|
86
|
-
export async function switchWorkspace(slug) {
|
|
87
|
-
|
|
86
|
+
export async function switchWorkspace(slug, config = loadConfig()) {
|
|
87
|
+
const slugs = await configuredSlugs(config);
|
|
88
|
+
if (!slugs.includes(slug)) {
|
|
89
|
+
throw new Error(`No workspace ${slug}. You can reach: ${slugs.join(", ")}.`);
|
|
90
|
+
}
|
|
91
|
+
const snapshot = await loadSnapshot({ ...config, slug });
|
|
92
|
+
selectWorkspace(slug);
|
|
93
|
+
return snapshot;
|
|
88
94
|
}
|
|
89
95
|
export async function postOrchestrator(body, config) {
|
|
90
96
|
await sendMessage({ body_md: body, to_role: "orchestrator", delivery: "queue" }, config);
|