@higherdev/cli 0.24.0 → 0.25.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/dist/index.js +2 -2
- package/dist/tui/App.js +60 -9
- package/dist/tui/launch.js +8 -3
- package/dist/update-check.js +43 -2
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -585,7 +585,7 @@ export async function main(argv = process.argv.slice(2), deps = {}) {
|
|
|
585
585
|
if (canLaunchApp()) {
|
|
586
586
|
launchedTui = true;
|
|
587
587
|
const { launchApp } = await import("./tui/launch.js");
|
|
588
|
-
await launchApp();
|
|
588
|
+
await launchApp(undefined, deps);
|
|
589
589
|
return;
|
|
590
590
|
}
|
|
591
591
|
console.log(banner());
|
|
@@ -670,7 +670,7 @@ export async function main(argv = process.argv.slice(2), deps = {}) {
|
|
|
670
670
|
console.log(`logged in to ${config.slug}`);
|
|
671
671
|
launchedTui = true;
|
|
672
672
|
const { launchApp } = await import("./tui/launch.js");
|
|
673
|
-
await launchApp(config.slug);
|
|
673
|
+
await launchApp(config.slug, deps);
|
|
674
674
|
return;
|
|
675
675
|
}
|
|
676
676
|
if (cmd === "init" || cmd === "upgrade") {
|
package/dist/tui/App.js
CHANGED
|
@@ -2,7 +2,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
2
2
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
3
|
import { Box, Static, Text, useApp, useInput, useStdout } from "ink";
|
|
4
4
|
import { loadConfig } from "../config.js";
|
|
5
|
-
import {
|
|
5
|
+
import { pendingTuiUpdate, relaunchHd, runPromptedUpdate, skipUpdate, subscribeUpdateNotice, tuiUpdatePrompt, updatePromptAction, } from "../update-check.js";
|
|
6
6
|
import { epicProgressRows } from "../epics.js";
|
|
7
7
|
import { promptOnStdin } from "../prompt.js";
|
|
8
8
|
import { ticketNew } from "../ticket-commands.js";
|
|
@@ -36,7 +36,7 @@ import { WorkspaceLoads } from "./workspace-load.js";
|
|
|
36
36
|
let messageSeq = 0;
|
|
37
37
|
const nextId = () => `m${messageSeq++}`;
|
|
38
38
|
const tuiPrompt = (question) => promptOnStdin(question, true);
|
|
39
|
-
export function App({ initial,
|
|
39
|
+
export function App({ initial, availableUpdate: initialUpdate = null, updateDeps = {}, onRelaunch, }) {
|
|
40
40
|
const { exit, suspendTerminal } = useApp();
|
|
41
41
|
const { stdout } = useStdout();
|
|
42
42
|
const columns = stdout?.columns && stdout.columns > 0 ? stdout.columns : 80;
|
|
@@ -56,7 +56,9 @@ export function App({ initial, updateNotice = null }) {
|
|
|
56
56
|
const [draft, setDraft] = useState("");
|
|
57
57
|
const [busy, setBusy] = useState(false);
|
|
58
58
|
const [notice, setNotice] = useState(null);
|
|
59
|
-
const [
|
|
59
|
+
const [availableUpdate, setAvailableUpdate] = useState(initialUpdate);
|
|
60
|
+
const [updateProgress, setUpdateProgress] = useState(null);
|
|
61
|
+
const [updating, setUpdating] = useState(false);
|
|
60
62
|
const [ticketKey, setTicketKey] = useState(null);
|
|
61
63
|
const [ticketOffset, setTicketOffset] = useState(0);
|
|
62
64
|
const [roadmapOffset, setRoadmapOffset] = useState(0);
|
|
@@ -86,9 +88,48 @@ export function App({ initial, updateNotice = null }) {
|
|
|
86
88
|
const loads = useRef(new WorkspaceLoads(initial.workspace.id));
|
|
87
89
|
editingRef.current = editing;
|
|
88
90
|
useEffect(() => {
|
|
89
|
-
|
|
90
|
-
return subscribeUpdateNotice(setUpdateLine);
|
|
91
|
+
return subscribeUpdateNotice(() => setAvailableUpdate(pendingTuiUpdate(updateDeps)));
|
|
91
92
|
}, []);
|
|
93
|
+
const dismissUpdate = useCallback(() => {
|
|
94
|
+
if (!availableUpdate)
|
|
95
|
+
return;
|
|
96
|
+
try {
|
|
97
|
+
skipUpdate(availableUpdate, updateDeps);
|
|
98
|
+
setAvailableUpdate(pendingTuiUpdate(updateDeps));
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
setNotice(`Could not remember skipped update: ${error instanceof Error ? error.message : String(error)}`);
|
|
102
|
+
}
|
|
103
|
+
}, [availableUpdate, updateDeps]);
|
|
104
|
+
const acceptUpdate = useCallback(async () => {
|
|
105
|
+
if (!availableUpdate || updating)
|
|
106
|
+
return;
|
|
107
|
+
setUpdating(true);
|
|
108
|
+
setUpdateProgress(`Updating hd ${availableUpdate.current}...`);
|
|
109
|
+
try {
|
|
110
|
+
await runPromptedUpdate(availableUpdate, {
|
|
111
|
+
...updateDeps,
|
|
112
|
+
log: (line) => {
|
|
113
|
+
updateDeps.log?.(line);
|
|
114
|
+
if (line !== `hd ${availableUpdate.current}`) {
|
|
115
|
+
setUpdateProgress(`Installed ${line}. Relaunching...`);
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
relaunch: () => {
|
|
119
|
+
exit();
|
|
120
|
+
if (onRelaunch)
|
|
121
|
+
onRelaunch();
|
|
122
|
+
else
|
|
123
|
+
setTimeout(updateDeps.relaunch ?? relaunchHd, 0);
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
setNotice(`Update failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
129
|
+
setUpdateProgress(null);
|
|
130
|
+
setUpdating(false);
|
|
131
|
+
}
|
|
132
|
+
}, [availableUpdate, exit, onRelaunch, updateDeps, updating]);
|
|
92
133
|
useEffect(() => {
|
|
93
134
|
if (messages.length > 0 || view !== "home")
|
|
94
135
|
setStarted(true);
|
|
@@ -837,8 +878,18 @@ export function App({ initial, updateNotice = null }) {
|
|
|
837
878
|
changeWorkspace, config, refresh, suspendTerminal, exit, inbox, inboxFocus, openTicket, answering,
|
|
838
879
|
selectedDecisionId, submitDecision, ticketKey, ticketCollapsed, ticketOffset, width]);
|
|
839
880
|
useInput((input, key) => {
|
|
840
|
-
if (key.ctrl && input === "c")
|
|
881
|
+
if (key.ctrl && input === "c") {
|
|
841
882
|
exit();
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
if (availableUpdate && !updating) {
|
|
886
|
+
const action = updatePromptAction(input, key.escape);
|
|
887
|
+
if (action === "skip")
|
|
888
|
+
dismissUpdate();
|
|
889
|
+
else if (action === "accept")
|
|
890
|
+
void acceptUpdate();
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
842
893
|
});
|
|
843
894
|
const decisions = board.decisions;
|
|
844
895
|
const answeringDecision = answering ? decisions.find((decision) => decision.id === answering) : null;
|
|
@@ -870,7 +921,7 @@ export function App({ initial, updateNotice = null }) {
|
|
|
870
921
|
const plan = planLayout({
|
|
871
922
|
rows, columns, width, splash, ready, decision: chatting ? 0 : decisionRows(decisions),
|
|
872
923
|
inFlight: chatting ? 0 : inFlight.reduce((total, message) => total + bubbleRows(message, width), 0),
|
|
873
|
-
notice: Boolean(notice), home: view === "home",
|
|
924
|
+
notice: Boolean(notice || availableUpdate || updateProgress), home: view === "home",
|
|
874
925
|
});
|
|
875
926
|
const agentsView = splitPanels(plan.panels);
|
|
876
927
|
const running = board.runs.filter((run) => run.status === "running").length;
|
|
@@ -883,11 +934,11 @@ export function App({ initial, updateNotice = null }) {
|
|
|
883
934
|
return _jsx(Bubble, { message: item.message, width: width }, item.key);
|
|
884
935
|
} }), splash ? (_jsx(Splash, { columns: columns, rows: rows, width: width, ready: ready, helpFull: plan.helpFull, animate: plan.fits, onDone: () => setReady(true) })) : null, _jsxs(Box, { flexDirection: "column", width: width, children: [view === "board" && plan.panels > 0 ? _jsx(BoardPanel, { board: board, width: width, rows: plan.panels, cursor: cursor }) : null, view === "roadmap" && plan.panels > 0 ? (_jsx(RoadmapPanel, { board: board, width: width, rows: plan.panels, offset: roadmapOffset })) : null, view === "agents" && plan.panels > 0 ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(AgentsPanel, { board: board, width: width, rows: agentsView.top }), agentsView.bottom > 0 ? _jsxs(_Fragment, { children: [_jsx(Box, { height: 1 }), _jsx(StreamPanel, { lines: stream, width: width, rows: agentsView.bottom, live: running > 0 })] }) : null] })) : null, view === "feed" && plan.panels > 0 ? _jsx(FeedPanel, { entries: feed, width: width, rows: plan.panels }) : null, view === "settings" && plan.panels > 0 ? _jsx(SettingsPanel, { entries: settings, width: width, rows: plan.panels, title: "Settings", cursor: field, editing: editing }) : null, view === "inbox" && plan.panels > 0 ? _jsx(InboxPanel, { board: board, width: width, rows: plan.panels, focus: inboxFocus, selectedId: selectedDecisionId, answeringId: answering }) : null, view === "ticket" && plan.panels > 0 ? ticket
|
|
885
936
|
? _jsx(TicketPanel, { ticket: ticket, width: width, rows: plan.panels, offset: ticketOffset, collapsed: ticketCollapsed })
|
|
886
|
-
: _jsxs(Text, { color: UI.warn, children: ["No ticket ", ticketKey, " here."] }) : null, view === "home" && plan.cockpit > 0 ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Cockpit, { board: board, width: width, rows: plan.cockpit, cursor: cursor }), _jsx(Box, { height: 1 }), _jsx(StreamPanel, { lines: stream, width: width, rows: plan.stream, live: running > 0 })] })) : null, chatting && plan.panels > 0 && activeThread ? (_jsx(ChatPanel, { thread: activeThread, width: width, rows: plan.panels, offset: chatOffset, label: chatLabel, now: now })) : null, chatting ? null : _jsx(DecisionPanel, { decisions: decisions, board: board, width: width, rows: plan.decision, selectedId: selectedDecisionId }), notice ? _jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: UI.warn, children: notice }) }) : null, _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: UI.text, bold: true, children: workspace.slug }), _jsxs(Text, { color: UI.dim, children: [" ", workspace.repo, " "] }), _jsx(Text, { color: live === "live" ? UI.accent : UI.dim, children: "\u25CF " }), _jsxs(Text, { color: UI.dim, children: [live === "live" ? "5s poll" : live, " "] }), _jsx(Text, { color: UI.dim, children: running ? `${running} running ` : "" }), board.decisions.length ? _jsxs(Text, { color: UI.warn, children: [board.decisions.length, " decisions "] }) : null, workspace.paused ? _jsx(Text, { color: UI.warn, children: "paused " }) : null,
|
|
937
|
+
: _jsxs(Text, { color: UI.warn, children: ["No ticket ", ticketKey, " here."] }) : null, view === "home" && plan.cockpit > 0 ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Cockpit, { board: board, width: width, rows: plan.cockpit, cursor: cursor }), _jsx(Box, { height: 1 }), _jsx(StreamPanel, { lines: stream, width: width, rows: plan.stream, live: running > 0 })] })) : null, chatting && plan.panels > 0 && activeThread ? (_jsx(ChatPanel, { thread: activeThread, width: width, rows: plan.panels, offset: chatOffset, label: chatLabel, now: now })) : null, chatting ? null : _jsx(DecisionPanel, { decisions: decisions, board: board, width: width, rows: plan.decision, selectedId: selectedDecisionId }), notice ? _jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: UI.warn, children: notice }) }) : null, _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: UI.text, bold: true, children: workspace.slug }), _jsxs(Text, { color: UI.dim, children: [" ", workspace.repo, " "] }), _jsx(Text, { color: live === "live" ? UI.accent : UI.dim, children: "\u25CF " }), _jsxs(Text, { color: UI.dim, children: [live === "live" ? "5s poll" : live, " "] }), _jsx(Text, { color: UI.dim, children: running ? `${running} running ` : "" }), board.decisions.length ? _jsxs(Text, { color: UI.warn, children: [board.decisions.length, " decisions "] }) : null, workspace.paused ? _jsx(Text, { color: UI.warn, children: "paused " }) : null, logsFilter || rawLogs ? _jsxs(Text, { color: UI.warn, children: ["logs ", rawLogs ? "raw " : "", logsFilter ?? "all", " "] }) : null, _jsxs(Text, { color: UI.dim, wrap: "truncate", children: ["\u00B7 ", chatting ? chatLabel : mode, answering ? " esc cancels" : chatting ? " ↑↓ scroll · pgup/pgdn · esc hides" : view === "ticket" ? " ↑↓ scroll · pgup/pgdn · enter folds" : view === "roadmap" ? " ↑↓ scroll · pgup/pgdn" : view === "inbox" ? " ↑↓ decisions · enter answers" : cursor && selected ? ` ${selected.key} ↑↓ move · enter opens · esc leaves` : ""] })] }), availableUpdate || updateProgress ? (_jsx(Box, { children: _jsx(Text, { color: UI.warn, children: updateProgress ?? tuiUpdatePrompt(availableUpdate) }) })) : null, _jsx(Box, { children: _jsx(TextInput, { value: draft, onChange: (next) => {
|
|
887
938
|
setDraft(next);
|
|
888
939
|
if (editingRef.current)
|
|
889
940
|
setEditing({ key: editingRef.current.key, draft: next });
|
|
890
|
-
}, onSubmit: (value) => void run(value), isActive: inputActive({ busy, pendingChats: inFlight.length }), placeholder: answering
|
|
941
|
+
}, onSubmit: (value) => void run(value), isActive: !availableUpdate && !updating && inputActive({ busy, pendingChats: inFlight.length }), placeholder: answering
|
|
891
942
|
? (answeringOptions.length ? `1-${answeringOptions.length} or your answer` : "your answer")
|
|
892
943
|
: promptPlaceholder({ busy, pendingChats: inFlight.length }), prompt: _jsx(Text, { color: answering || chatting || mode !== "browse" ? UI.cream : UI.dim, children: answering ? `answer ${answeringNumber}> ` : chatting ? `${chatLabel}> ` : mode === "browse" ? "> " : `${mode}> ` }), color: UI.text, onCancel: () => {
|
|
893
944
|
if (answering) {
|
package/dist/tui/launch.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { loadConfig } from "../config.js";
|
|
2
|
-
import {
|
|
2
|
+
import { pendingTuiUpdate, relaunchHd } from "../update-check.js";
|
|
3
3
|
import { canLaunchApp } from "./capability.js";
|
|
4
4
|
import { loadSnapshot } from "./data.js";
|
|
5
5
|
/** Opens HigherDEV. Shared by `hd` and by the end of `hd login`. */
|
|
6
|
-
export async function launchApp(slug) {
|
|
6
|
+
export async function launchApp(slug, updateDeps = {}) {
|
|
7
7
|
// Keep this guard here as well as at command call sites. Ink cannot enter raw
|
|
8
8
|
// mode unless both streams are terminals.
|
|
9
9
|
if (!canLaunchApp())
|
|
@@ -14,9 +14,14 @@ export async function launchApp(slug) {
|
|
|
14
14
|
import("react"),
|
|
15
15
|
import("./App.js"),
|
|
16
16
|
]);
|
|
17
|
+
let shouldRelaunch = false;
|
|
17
18
|
const instance = render(React.createElement(App, {
|
|
18
19
|
initial,
|
|
19
|
-
|
|
20
|
+
availableUpdate: pendingTuiUpdate(updateDeps),
|
|
21
|
+
updateDeps,
|
|
22
|
+
onRelaunch: () => { shouldRelaunch = true; },
|
|
20
23
|
}), { exitOnCtrlC: false });
|
|
21
24
|
await instance.waitUntilExit();
|
|
25
|
+
if (shouldRelaunch)
|
|
26
|
+
(updateDeps.relaunch ?? relaunchHd)();
|
|
22
27
|
}
|
package/dist/update-check.js
CHANGED
|
@@ -56,6 +56,9 @@ export function readUpdateCheck(path = updateCheckPath()) {
|
|
|
56
56
|
return {
|
|
57
57
|
latest: parsed.latest,
|
|
58
58
|
checked_at: typeof parsed.checked_at === "number" ? parsed.checked_at : 0,
|
|
59
|
+
...(typeof parsed.skipped_version === "string" && parsed.skipped_version
|
|
60
|
+
? { skipped_version: parsed.skipped_version }
|
|
61
|
+
: {}),
|
|
59
62
|
};
|
|
60
63
|
}
|
|
61
64
|
catch {
|
|
@@ -73,6 +76,34 @@ export function pendingUpdateNotice(deps = {}) {
|
|
|
73
76
|
return null;
|
|
74
77
|
return updateNoticeLine(stored.latest, current);
|
|
75
78
|
}
|
|
79
|
+
export function pendingTuiUpdate(deps = {}) {
|
|
80
|
+
const stored = readUpdateCheck(deps.path ?? updateCheckPath());
|
|
81
|
+
const current = deps.version ?? runningVersion();
|
|
82
|
+
if (!stored || !isNewer(stored.latest, current))
|
|
83
|
+
return null;
|
|
84
|
+
if (stored.skipped_version && !isNewer(stored.latest, stored.skipped_version))
|
|
85
|
+
return null;
|
|
86
|
+
return { latest: stored.latest, current };
|
|
87
|
+
}
|
|
88
|
+
export function tuiUpdatePrompt(update) {
|
|
89
|
+
return `hd ${update.latest} is available, you have ${update.current}. Update now? (y/n)`;
|
|
90
|
+
}
|
|
91
|
+
export function updatePromptAction(input, escape = false) {
|
|
92
|
+
if (escape || input.toLowerCase() === "n")
|
|
93
|
+
return "skip";
|
|
94
|
+
if (input.toLowerCase() === "y")
|
|
95
|
+
return "accept";
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
export function skipUpdate(update, deps = {}) {
|
|
99
|
+
const path = deps.path ?? updateCheckPath();
|
|
100
|
+
const stored = readUpdateCheck(path);
|
|
101
|
+
writeUpdateCheck({
|
|
102
|
+
latest: stored?.latest ?? update.latest,
|
|
103
|
+
checked_at: stored?.checked_at ?? (deps.now ?? Date.now)(),
|
|
104
|
+
skipped_version: update.latest,
|
|
105
|
+
}, path);
|
|
106
|
+
}
|
|
76
107
|
export function subscribeUpdateNotice(listener) {
|
|
77
108
|
listeners.add(listener);
|
|
78
109
|
return () => {
|
|
@@ -111,8 +142,15 @@ export function beginUpdateCheck(deps = {}) {
|
|
|
111
142
|
inFlight = (async () => {
|
|
112
143
|
try {
|
|
113
144
|
const latest = await fetchLatest(deps);
|
|
114
|
-
if (latest)
|
|
115
|
-
|
|
145
|
+
if (latest) {
|
|
146
|
+
const path = deps.path ?? updateCheckPath();
|
|
147
|
+
const stored = readUpdateCheck(path);
|
|
148
|
+
writeUpdateCheck({
|
|
149
|
+
latest,
|
|
150
|
+
checked_at: (deps.now ?? Date.now)(),
|
|
151
|
+
...(stored?.skipped_version ? { skipped_version: stored.skipped_version } : {}),
|
|
152
|
+
}, path);
|
|
153
|
+
}
|
|
116
154
|
}
|
|
117
155
|
catch {
|
|
118
156
|
// Offline, timed out, or unwritable cache: the command still succeeds.
|
|
@@ -163,3 +201,6 @@ export async function runUpdate(argv, deps = {}) {
|
|
|
163
201
|
(deps.relaunch ?? relaunchHd)();
|
|
164
202
|
return lines;
|
|
165
203
|
}
|
|
204
|
+
export function runPromptedUpdate(update, deps = {}) {
|
|
205
|
+
return runUpdate([], { ...deps, version: update.current, tuiRunning: true });
|
|
206
|
+
}
|