@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 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 { pendingUpdateNotice, subscribeUpdateNotice } from "../update-check.js";
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, updateNotice = null }) {
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 [updateLine, setUpdateLine] = useState(updateNotice);
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
- setUpdateLine((current) => current ?? pendingUpdateNotice());
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, updateLine ? _jsxs(Text, { color: UI.warn, children: [updateLine, " "] }) : 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` : ""] })] }), _jsx(Box, { children: _jsx(TextInput, { value: draft, onChange: (next) => {
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) {
@@ -1,9 +1,9 @@
1
1
  import { loadConfig } from "../config.js";
2
- import { pendingUpdateNotice } from "../update-check.js";
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
- updateNotice: pendingUpdateNotice(),
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
  }
@@ -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
- writeUpdateCheck({ latest, checked_at: (deps.now ?? Date.now)() }, deps.path ?? updateCheckPath());
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@higherdev/cli",
3
- "version": "0.24.0",
3
+ "version": "0.25.0",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",