tmux-ide 2.6.0 → 2.6.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/bin/cli.js +13 -5
- package/bin/cli.ts +1 -1
- package/bunfig.toml +4 -0
- package/package.json +11 -3
- package/packages/contracts/package.json +22 -0
- package/packages/contracts/src/__tests__/ide-config.test.ts +46 -0
- package/packages/contracts/src/__tests__/terminals.test.ts +87 -0
- package/packages/contracts/src/actions-contract.ts +310 -0
- package/packages/contracts/src/actions-errors.ts +41 -0
- package/packages/contracts/src/domain.ts +36 -0
- package/packages/contracts/src/ide-config.ts +170 -0
- package/packages/contracts/src/index.ts +24 -0
- package/packages/contracts/src/lib-internal/auth.ts +13 -0
- package/packages/contracts/src/lib-internal/hq.ts +38 -0
- package/packages/contracts/src/terminals.ts +116 -0
- package/packages/contracts/src/tmux.ts +60 -0
- package/packages/contracts/src/workspace.ts +67 -0
- package/packages/daemon/src/agent-explain.ts +270 -0
- package/packages/daemon/src/attach.ts +20 -0
- package/packages/daemon/src/bin.ts +4 -0
- package/packages/daemon/src/canonical.ts +7 -0
- package/packages/daemon/src/cli.ts +499 -0
- package/packages/daemon/src/command-center/actions/contract.ts +2 -0
- package/packages/daemon/src/command-center/actions/dispatcher.ts +137 -0
- package/packages/daemon/src/command-center/actions/errors.ts +105 -0
- package/packages/daemon/src/command-center/actions/handlers/_project-context.ts +30 -0
- package/packages/daemon/src/command-center/actions/handlers/_resolve-project.ts +78 -0
- package/packages/daemon/src/command-center/actions/handlers/app-set-remote-access.ts +118 -0
- package/packages/daemon/src/command-center/actions/handlers/config-actions.ts +113 -0
- package/packages/daemon/src/command-center/actions/handlers/daemon-shutdown.ts +38 -0
- package/packages/daemon/src/command-center/actions/handlers/project-activate.ts +30 -0
- package/packages/daemon/src/command-center/actions/handlers/project-launch.ts +70 -0
- package/packages/daemon/src/command-center/actions/handlers/project-open-terminal.ts +87 -0
- package/packages/daemon/src/command-center/actions/handlers/project-restart.ts +38 -0
- package/packages/daemon/src/command-center/actions/handlers/project-stop.ts +62 -0
- package/packages/daemon/src/command-center/actions/handlers/terminal-respawn.ts +119 -0
- package/packages/daemon/src/command-center/actions/handlers/terminal-stop.ts +35 -0
- package/packages/daemon/src/command-center/actions/registry.ts +149 -0
- package/packages/daemon/src/command-center/discovery.ts +96 -0
- package/packages/daemon/src/command-center/index.ts +31 -0
- package/packages/daemon/src/command-center/schemas.ts +85 -0
- package/packages/daemon/src/command-center/server.ts +1260 -0
- package/packages/daemon/src/command-center/ws-events.ts +316 -0
- package/packages/daemon/src/config.ts +549 -0
- package/packages/daemon/src/detect.ts +248 -0
- package/packages/daemon/src/doctor.ts +242 -0
- package/packages/daemon/src/embed.ts +5 -0
- package/packages/daemon/src/index.ts +12 -0
- package/packages/daemon/src/init.ts +211 -0
- package/packages/daemon/src/inspect.ts +178 -0
- package/packages/daemon/src/js-yaml.d.ts +10 -0
- package/packages/daemon/src/launch.ts +349 -0
- package/packages/daemon/src/lib/active-projects.ts +49 -0
- package/packages/daemon/src/lib/agent-discovery.ts +121 -0
- package/packages/daemon/src/lib/app-config.ts +336 -0
- package/packages/daemon/src/lib/app-settings.ts +53 -0
- package/packages/daemon/src/lib/auth/auth-service.ts +227 -0
- package/packages/daemon/src/lib/auth/middleware.ts +56 -0
- package/packages/daemon/src/lib/auth/types.ts +2 -0
- package/packages/daemon/src/lib/auth-token.ts +5 -0
- package/packages/daemon/src/lib/authorship.ts +280 -0
- package/packages/daemon/src/lib/canonical-daemon.ts +122 -0
- package/packages/daemon/src/lib/cli-action-bridge.ts +216 -0
- package/packages/daemon/src/lib/daemon-embed.ts +782 -0
- package/packages/daemon/src/lib/daemon-watchdog.ts +111 -0
- package/packages/daemon/src/lib/daemon.ts +79 -0
- package/packages/daemon/src/lib/dot-path.ts +17 -0
- package/packages/daemon/src/lib/errors.ts +67 -0
- package/packages/daemon/src/lib/filesystem-browser.ts +292 -0
- package/packages/daemon/src/lib/launch-plan.ts +90 -0
- package/packages/daemon/src/lib/log.ts +134 -0
- package/packages/daemon/src/lib/output.ts +76 -0
- package/packages/daemon/src/lib/project-init-runner.ts +150 -0
- package/packages/daemon/src/lib/project-inspect.ts +80 -0
- package/packages/daemon/src/lib/project-onboard.ts +149 -0
- package/packages/daemon/src/lib/project-probe.ts +92 -0
- package/packages/daemon/src/lib/project-registry.ts +296 -0
- package/packages/daemon/src/lib/session-monitor.ts +122 -0
- package/packages/daemon/src/lib/session-options.ts +100 -0
- package/packages/daemon/src/lib/shell.ts +8 -0
- package/packages/daemon/src/lib/sizes.ts +36 -0
- package/packages/daemon/src/lib/skill-sync.ts +155 -0
- package/packages/daemon/src/lib/slugify.ts +10 -0
- package/packages/daemon/src/lib/terminals-store.ts +125 -0
- package/packages/daemon/src/lib/update-check.ts +298 -0
- package/packages/daemon/src/lib/update.ts +158 -0
- package/packages/daemon/src/lib/workspace-registry.ts +229 -0
- package/packages/daemon/src/lib/worktree.ts +289 -0
- package/packages/daemon/src/lib/yaml-io.ts +27 -0
- package/packages/daemon/src/ls.ts +40 -0
- package/packages/daemon/src/restart.ts +24 -0
- package/packages/daemon/src/restore.ts +514 -0
- package/packages/daemon/src/schemas/domain.ts +2 -0
- package/packages/daemon/src/schemas/filesystem.ts +34 -0
- package/packages/daemon/src/schemas/ide-config.ts +2 -0
- package/packages/daemon/src/schemas/index.ts +59 -0
- package/packages/daemon/src/schemas/inspect.ts +67 -0
- package/packages/daemon/src/schemas/registry.ts +55 -0
- package/packages/daemon/src/schemas/ws-events.ts +135 -0
- package/packages/daemon/src/send.ts +171 -0
- package/packages/daemon/src/server/README.md +15 -0
- package/packages/daemon/src/server/index.ts +74 -0
- package/packages/daemon/src/server/pty-bridge.ts +532 -0
- package/packages/daemon/src/server/standalone.ts +18 -0
- package/packages/daemon/src/server/ws-route.ts +483 -0
- package/packages/daemon/src/status.ts +59 -0
- package/packages/daemon/src/stop.ts +28 -0
- package/packages/daemon/src/terminal/NodePtyAdapter.ts +271 -0
- package/packages/daemon/src/terminal/PtyAdapter.ts +140 -0
- package/packages/daemon/src/terminal/README.md +92 -0
- package/packages/daemon/src/tui/chrome/cheatsheet.ts +260 -0
- package/packages/daemon/src/tui/chrome/chip.ts +30 -0
- package/packages/daemon/src/tui/chrome/events.ts +119 -0
- package/packages/daemon/src/tui/chrome/kitty-keys.ts +55 -0
- package/packages/daemon/src/tui/chrome/menu.ts +289 -0
- package/packages/daemon/src/tui/chrome/notify.ts +195 -0
- package/packages/daemon/src/tui/chrome/panels.ts +111 -0
- package/packages/daemon/src/tui/chrome/sidebar.ts +222 -0
- package/packages/daemon/src/tui/chrome/snapshot.ts +425 -0
- package/packages/daemon/src/tui/chrome/statusline.ts +595 -0
- package/packages/daemon/src/tui/chrome/updater.ts +428 -0
- package/packages/daemon/src/tui/chrome/welcome.ts +124 -0
- package/packages/daemon/src/tui/compiled.ts +113 -0
- package/packages/daemon/src/tui/detect/classify.ts +193 -0
- package/packages/daemon/src/tui/detect/manifest-loader.ts +192 -0
- package/packages/daemon/src/tui/detect/manifest.ts +184 -0
- package/packages/daemon/src/tui/detect/manifests.ts +226 -0
- package/packages/daemon/src/tui/detect/process-tree.ts +197 -0
- package/packages/daemon/src/tui/detect/snapshot.ts +70 -0
- package/packages/daemon/src/tui/integrations/claude.ts +176 -0
- package/packages/daemon/src/tui/integrations/offer.ts +145 -0
- package/packages/daemon/src/tui/main.ts +70 -0
- package/packages/daemon/src/tui/mirror/control-client.ts +143 -0
- package/packages/daemon/src/tui/mirror/control.ts +97 -0
- package/packages/daemon/src/tui/mirror/pane-mirror.ts +81 -0
- package/packages/daemon/src/tui/mirror/viewer.tsx +166 -0
- package/packages/daemon/src/tui/team/CONTROL.md +50 -0
- package/packages/daemon/src/tui/team/entry.ts +11 -0
- package/packages/daemon/src/tui/team/fuzzy.ts +133 -0
- package/packages/daemon/src/tui/team/home.ts +170 -0
- package/packages/daemon/src/tui/team/index.tsx +1521 -0
- package/packages/daemon/src/tui/team/input.ts +34 -0
- package/packages/daemon/src/tui/team/keymap.ts +127 -0
- package/packages/daemon/src/tui/team/mouse.ts +29 -0
- package/packages/daemon/src/tui/team/nav.ts +31 -0
- package/packages/daemon/src/tui/team/preview.ts +34 -0
- package/packages/daemon/src/tui/team/projects.ts +191 -0
- package/packages/daemon/src/tui/team/report.ts +73 -0
- package/packages/daemon/src/tui/team/sessions.ts +344 -0
- package/packages/daemon/src/tui/team/tree.ts +62 -0
- package/packages/daemon/src/types.ts +13 -0
- package/packages/daemon/src/ui/index.ts +32 -0
- package/packages/daemon/src/ui/terminal/index.ts +9 -0
- package/packages/daemon/src/ui/types.ts +91 -0
- package/packages/daemon/src/ui/web/base.css +80 -0
- package/packages/daemon/src/ui/web/components/Box.tsx +59 -0
- package/packages/daemon/src/ui/web/components/Input.tsx +32 -0
- package/packages/daemon/src/ui/web/components/ScrollBox.tsx +60 -0
- package/packages/daemon/src/ui/web/components/Text.tsx +28 -0
- package/packages/daemon/src/ui/web/hooks.ts +106 -0
- package/packages/daemon/src/ui/web/index.ts +27 -0
- package/packages/daemon/src/ui/web/render.ts +77 -0
- package/packages/daemon/src/ui/web/utils/color.ts +27 -0
- package/packages/daemon/src/validate.ts +217 -0
- package/packages/daemon/src/widgets/changes/README.md +3 -0
- package/packages/daemon/src/widgets/changes/index.tsx +691 -0
- package/packages/daemon/src/widgets/config/README.md +3 -0
- package/packages/daemon/src/widgets/config/index.tsx +481 -0
- package/packages/daemon/src/widgets/explorer/README.md +3 -0
- package/packages/daemon/src/widgets/explorer/breadcrumbs.tsx +77 -0
- package/packages/daemon/src/widgets/explorer/footer.tsx +20 -0
- package/packages/daemon/src/widgets/explorer/header.tsx +23 -0
- package/packages/daemon/src/widgets/explorer/index.tsx +456 -0
- package/packages/daemon/src/widgets/explorer/tree-model.ts +103 -0
- package/packages/daemon/src/widgets/explorer/tree.tsx +165 -0
- package/packages/daemon/src/widgets/lib/config-model.ts +116 -0
- package/packages/daemon/src/widgets/lib/files.ts +88 -0
- package/packages/daemon/src/widgets/lib/git.ts +88 -0
- package/packages/daemon/src/widgets/lib/grammar.ts +126 -0
- package/packages/daemon/src/widgets/lib/help-overlay.tsx +101 -0
- package/packages/daemon/src/widgets/lib/pane-comms.ts +209 -0
- package/packages/daemon/src/widgets/lib/theme.ts +194 -0
- package/packages/daemon/src/widgets/lib/watcher.ts +132 -0
- package/packages/daemon/src/widgets/preview/README.md +3 -0
- package/packages/daemon/src/widgets/preview/index.tsx +416 -0
- package/packages/daemon/src/widgets/resolve.ts +121 -0
- package/packages/daemon/src/widgets/setup/README.md +3 -0
- package/packages/daemon/src/widgets/setup/agent-naming.tsx +112 -0
- package/packages/daemon/src/widgets/setup/config-tree.tsx +246 -0
- package/packages/daemon/src/widgets/setup/detect-panel.tsx +72 -0
- package/packages/daemon/src/widgets/setup/field-editor.tsx +265 -0
- package/packages/daemon/src/widgets/setup/footer.tsx +107 -0
- package/packages/daemon/src/widgets/setup/index.tsx +341 -0
- package/packages/daemon/src/widgets/setup/layout-picker.tsx +96 -0
- package/packages/daemon/src/widgets/setup/orchestrator-panel.tsx +200 -0
- package/packages/daemon/src/widgets/setup/review-panel.tsx +140 -0
- package/packages/daemon/src/widgets/setup/setup-model.ts +188 -0
- package/packages/daemon/src/widgets/sidebar/index.tsx +527 -0
- package/packages/tmux-bridge/package.json +22 -0
- package/packages/tmux-bridge/src/errors.ts +28 -0
- package/packages/tmux-bridge/src/index.ts +31 -0
- package/packages/tmux-bridge/src/monitor.ts +77 -0
- package/packages/tmux-bridge/src/panes.ts +136 -0
- package/packages/tmux-bridge/src/runner.test.ts +501 -0
- package/packages/tmux-bridge/src/runner.ts +91 -0
- package/packages/tmux-bridge/src/sessions.ts +126 -0
- package/packages/tmux-bridge/src/targeting.test.ts +107 -0
- package/packages/tmux-bridge/src/targeting.ts +90 -0
- package/scripts/postinstall.js +26 -2
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The chrome status-bar updater — ONE background loop that keeps every adopted
|
|
3
|
+
* session's status var fresh.
|
|
4
|
+
*
|
|
5
|
+
* WHY a single updater (vs. the old per-session `#()`): the first chrome
|
|
6
|
+
* pointed each adopted session's `status-format[1]` at
|
|
7
|
+
* `#(tmux-ide statusline …)`, which tmux re-ran every `status-interval` — a
|
|
8
|
+
* full node boot + fleet scan PER adopted session PER tick (~0.35s each). And
|
|
9
|
+
* because each `#()` invocation was stateless it could never produce the
|
|
10
|
+
* cross-tick `done` status (working→idle needs history).
|
|
11
|
+
*
|
|
12
|
+
* This module computes the fleet ONCE per tick behind a PERSISTENT
|
|
13
|
+
* {@link createStatusTracker} (so working→idle surfaces as `done`), then writes
|
|
14
|
+
* a per-session `@tmux_ide_status` user option for each adopted session
|
|
15
|
+
* (per-session so each keeps its own active-highlight). `adoptSession` points
|
|
16
|
+
* `status-format[1]` at a bare `#{@tmux_ide_status}` read — near-free, no spawn.
|
|
17
|
+
*
|
|
18
|
+
* The loop is HOSTED IN TMUX: `adoptSession` spins up a hidden `_tmux-ide-chrome`
|
|
19
|
+
* session running `tmux-ide chrome-updater`, and unadopting the last session
|
|
20
|
+
* kills it. `runUpdaterTick` is factored to take injected io so it's unit-tested
|
|
21
|
+
* without a live tmux; `adoptedSessionsFrom` is a pure parser.
|
|
22
|
+
*/
|
|
23
|
+
import { hasSession, isProcessAlive, runTmux } from "@tmux-ide/tmux-bridge";
|
|
24
|
+
import { DEFAULT_THEME, getAppConfig, type AppTheme } from "../../lib/app-config.ts";
|
|
25
|
+
import {
|
|
26
|
+
maybeCheckForUpdate,
|
|
27
|
+
markUpdateNotified,
|
|
28
|
+
type UpdateStatus,
|
|
29
|
+
} from "../../lib/update-check.ts";
|
|
30
|
+
import { createStatusTracker, type AgentStatus } from "../detect/classify.ts";
|
|
31
|
+
import { listTeamProjects, type TeamProject } from "../team/projects.ts";
|
|
32
|
+
import type { PaneDetail } from "../team/sessions.ts";
|
|
33
|
+
import { paneChip } from "./chip.ts";
|
|
34
|
+
import { appendEvents, diffFleet, type AgentEventInit } from "./events.ts";
|
|
35
|
+
import {
|
|
36
|
+
decideNotifications,
|
|
37
|
+
listAttachedClients,
|
|
38
|
+
readNotificationPrefs,
|
|
39
|
+
sendSystemNotification,
|
|
40
|
+
sendToasts,
|
|
41
|
+
type AttachedClient,
|
|
42
|
+
type NotificationPrefs,
|
|
43
|
+
type NotifyEvent,
|
|
44
|
+
type ToastTarget,
|
|
45
|
+
} from "./notify.ts";
|
|
46
|
+
import {
|
|
47
|
+
collectFleetSnapshot,
|
|
48
|
+
createSnapshotter,
|
|
49
|
+
readSnapshot,
|
|
50
|
+
writeSnapshot,
|
|
51
|
+
} from "./snapshot.ts";
|
|
52
|
+
import { buildStatusline } from "./statusline.ts";
|
|
53
|
+
|
|
54
|
+
/** Per-session user option holding the pre-rendered status-bar string. */
|
|
55
|
+
export const STATUS_OPTION = "@tmux_ide_status";
|
|
56
|
+
/** Per-PANE user option holding the pre-rendered agent chip (read by pane-border-format). */
|
|
57
|
+
export const CHIP_OPTION = "@tmux_ide_chip";
|
|
58
|
+
/** Per-session marker option set on adopt so the updater can enumerate adopted sessions. */
|
|
59
|
+
export const ADOPTED_OPTION = "@tmux_ide_adopted";
|
|
60
|
+
/** The hidden internal session that hosts the updater loop. */
|
|
61
|
+
export const UPDATER_SESSION = "_tmux-ide-chrome";
|
|
62
|
+
/** Server option holding the running updater's pid (a lightweight single-owner guard). */
|
|
63
|
+
export const UPDATER_PID_OPTION = "@tmux_ide_updater_pid";
|
|
64
|
+
/** Default tick cadence — overridable via `updater.tickMs` in the app config. */
|
|
65
|
+
export const TICK_MS = 2000;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* PURE — parse `list-sessions -F '#{session_name}\t#{@tmux_ide_adopted}'` output
|
|
69
|
+
* into the list of adopted session names. A session is adopted when its marker
|
|
70
|
+
* field is exactly `"1"` (sessions without the option render an empty field —
|
|
71
|
+
* verified on tmux 3.6, where user options ARE readable in list-sessions
|
|
72
|
+
* formats).
|
|
73
|
+
*/
|
|
74
|
+
export function adoptedSessionsFrom(lines: string[]): string[] {
|
|
75
|
+
const out: string[] = [];
|
|
76
|
+
for (const line of lines) {
|
|
77
|
+
const [name = "", flag = ""] = line.split("\t");
|
|
78
|
+
if (name && flag === "1") out.push(name);
|
|
79
|
+
}
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Enumerate adopted sessions from the live tmux server. Never throws. */
|
|
84
|
+
export function listAdoptedSessions(): string[] {
|
|
85
|
+
try {
|
|
86
|
+
const raw = runTmux(["list-sessions", "-F", `#{session_name}\t#{${ADOPTED_OPTION}}`])
|
|
87
|
+
.toString()
|
|
88
|
+
.trim();
|
|
89
|
+
return raw ? adoptedSessionsFrom(raw.split("\n")) : [];
|
|
90
|
+
} catch {
|
|
91
|
+
return [];
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Write a session's pre-rendered status var. */
|
|
96
|
+
function writeSessionStatus(session: string, value: string): void {
|
|
97
|
+
runTmux(["set-option", "-t", session, STATUS_OPTION, value]);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Write a pane's pre-rendered chip var (empty string clears it → title fallback). */
|
|
101
|
+
function writePaneChip(paneId: string, value: string): void {
|
|
102
|
+
runTmux(["set-option", "-p", "-t", paneId, CHIP_OPTION, value]);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** The io a single tick needs — injectable so the orchestration is unit-tested. */
|
|
106
|
+
export interface UpdaterTickDeps {
|
|
107
|
+
listAdopted: () => string[];
|
|
108
|
+
/**
|
|
109
|
+
* Compute the fleet. The tick passes an `onPane` collector so per-pane detail
|
|
110
|
+
* (agent + status) is recovered during the SAME scan the bars are built from
|
|
111
|
+
* — the loop wires this to `listTeamProjects(tracker, { onPane })`. Callers
|
|
112
|
+
* that don't need chips (tests) may ignore the argument.
|
|
113
|
+
*/
|
|
114
|
+
computeProjects: (onPane: (pane: PaneDetail) => void) => TeamProject[];
|
|
115
|
+
writeStatus: (session: string, value: string) => void;
|
|
116
|
+
/**
|
|
117
|
+
* The shared palette threaded into {@link buildStatusline} / {@link paneChip}
|
|
118
|
+
* (default {@link DEFAULT_THEME}). The loop resolves it once from the app
|
|
119
|
+
* config so a re-theme applies on the next updater start.
|
|
120
|
+
*/
|
|
121
|
+
theme?: AppTheme;
|
|
122
|
+
/**
|
|
123
|
+
* Per-pane chip write (optional). When wired, the tick writes each ADOPTED
|
|
124
|
+
* session's panes a `@tmux_ide_chip` pane option (`agent · status`, or empty
|
|
125
|
+
* for a non-agent pane). Only CHANGED chips are written — `chipCache` holds
|
|
126
|
+
* the last value per pane and is mutated in place across ticks so the steady
|
|
127
|
+
* state costs zero set-options.
|
|
128
|
+
*/
|
|
129
|
+
writeChip?: (paneId: string, value: string) => void;
|
|
130
|
+
chipCache?: Map<string, string>;
|
|
131
|
+
/**
|
|
132
|
+
* Transition tracking (optional). When both are supplied, the tick diffs the
|
|
133
|
+
* WHOLE fleet against `prevState` and appends any transitions via
|
|
134
|
+
* `appendEvents`, mutating `prevState` in place to the fresh state. Omitted by
|
|
135
|
+
* callers/tests that only care about the status bars.
|
|
136
|
+
*/
|
|
137
|
+
prevState?: Map<string, AgentStatus>;
|
|
138
|
+
appendEvents?: (events: AgentEventInit[]) => void;
|
|
139
|
+
/**
|
|
140
|
+
* Notification dispatch (optional). When wired alongside `prevState`, the tick
|
|
141
|
+
* turns THIS tick's transitions into user pings — toasts on attached clients
|
|
142
|
+
* and/or a macOS notification — via {@link decideNotifications}, gated on
|
|
143
|
+
* `prefs`. `lastNotified` is the persistent debounce map, mutated in place.
|
|
144
|
+
* All deps-injected so the routing is unit-tested without a live tmux.
|
|
145
|
+
*/
|
|
146
|
+
listClients?: () => AttachedClient[];
|
|
147
|
+
lastNotified?: Map<string, number>;
|
|
148
|
+
now?: () => number;
|
|
149
|
+
prefs?: NotificationPrefs;
|
|
150
|
+
sendToasts?: (toasts: ToastTarget[]) => void;
|
|
151
|
+
sendSystem?: (message: string) => void;
|
|
152
|
+
/**
|
|
153
|
+
* Update-flow surfacing (optional). When wired, the tick calls this cheap,
|
|
154
|
+
* cache-backed check each tick (throttled internally to 24h; it kicks off a
|
|
155
|
+
* background registry refresh). When it reports an available update the tick
|
|
156
|
+
* threads the `⬆ v<latest>` dock segment into every adopted session's bar and,
|
|
157
|
+
* via {@link markUpdateNotified}, fires a ONE-time toast for that version. Both
|
|
158
|
+
* deps-injected so the surfacing is unit-tested without a live tmux/network.
|
|
159
|
+
*/
|
|
160
|
+
maybeCheckForUpdate?: () => UpdateStatus;
|
|
161
|
+
markUpdateNotified?: (version: string) => boolean;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* PURE — the reserved dock segment for an available update: the clickable
|
|
166
|
+
* `⬆ v<latest>` chip (accent-colored, wrapped in a `user|update` mouse range so a
|
|
167
|
+
* click floats the update popup — see {@link ./statusline.ts statusClickBindCommand}).
|
|
168
|
+
* Empty string when there's nothing to offer, so it takes no space on the bar.
|
|
169
|
+
*/
|
|
170
|
+
export function updateSegment(status: UpdateStatus, theme: AppTheme): string {
|
|
171
|
+
if (!status.updateAvailable || !status.latest) return "";
|
|
172
|
+
return `#[range=user|update]#[fg=${theme.accent}]⬆ v${status.latest}#[default]#[norange]`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Flatten the project view to a flat per-session status list for {@link diffFleet}. */
|
|
176
|
+
function fleetStatuses(projects: TeamProject[]): Array<{ name: string; status: AgentStatus }> {
|
|
177
|
+
return projects.flatMap((p) => p.sessions.map((s) => ({ name: s.name, status: s.status })));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* One tick: if any session is adopted, compute the fleet ONCE and write each
|
|
182
|
+
* adopted session its own {@link buildStatusline} (its name flagged active so
|
|
183
|
+
* the per-session highlight is correct). PURE given its deps — no tmux, no
|
|
184
|
+
* fleet scan when nothing is adopted.
|
|
185
|
+
*
|
|
186
|
+
* When `prevState`/`appendEvents` are wired, it also detects state TRANSITIONS
|
|
187
|
+
* across the whole fleet (not just adopted sessions) and appends them to the
|
|
188
|
+
* event log — the updater is the one process that sees every tick, so it's the
|
|
189
|
+
* natural place to emit history.
|
|
190
|
+
*/
|
|
191
|
+
export function runUpdaterTick(deps: UpdaterTickDeps): void {
|
|
192
|
+
const adopted = deps.listAdopted();
|
|
193
|
+
if (adopted.length === 0) return;
|
|
194
|
+
const theme = deps.theme ?? DEFAULT_THEME;
|
|
195
|
+
// Collect per-pane detail during the fleet scan so chips need no second pass.
|
|
196
|
+
const panes: PaneDetail[] = [];
|
|
197
|
+
const projects = deps.computeProjects((pane) => panes.push(pane));
|
|
198
|
+
// The dock-first update surface: a cheap cache read that also kicks off the
|
|
199
|
+
// throttled background registry refresh. Threads the "⬆ v<latest>" chip into
|
|
200
|
+
// every bar this tick when an update is pending.
|
|
201
|
+
const update = deps.maybeCheckForUpdate?.();
|
|
202
|
+
const extra = update ? updateSegment(update, theme) : "";
|
|
203
|
+
for (const session of adopted) {
|
|
204
|
+
deps.writeStatus(session, buildStatusline(projects, session, 12, theme, extra));
|
|
205
|
+
}
|
|
206
|
+
writeChips(deps, adopted, panes, theme);
|
|
207
|
+
if (update?.updateAvailable && update.latest) dispatchUpdateToast(deps, update.latest);
|
|
208
|
+
if (deps.prevState && deps.appendEvents) {
|
|
209
|
+
const { events, state } = diffFleet(deps.prevState, fleetStatuses(projects));
|
|
210
|
+
deps.prevState.clear();
|
|
211
|
+
for (const [name, status] of state) deps.prevState.set(name, status);
|
|
212
|
+
if (events.length > 0) {
|
|
213
|
+
deps.appendEvents(events);
|
|
214
|
+
dispatchNotifications(deps, events);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Write each adopted session's panes their agent chip — but only when the chip
|
|
221
|
+
* CHANGED since last tick (per-pane cache), so a steady fleet issues zero
|
|
222
|
+
* set-options. No-op unless both `writeChip` and `chipCache` are wired. Panes of
|
|
223
|
+
* non-adopted (user, un-adopted) sessions are skipped: we only paint borders on
|
|
224
|
+
* sessions we've adopted.
|
|
225
|
+
*/
|
|
226
|
+
function writeChips(
|
|
227
|
+
deps: UpdaterTickDeps,
|
|
228
|
+
adopted: string[],
|
|
229
|
+
panes: PaneDetail[],
|
|
230
|
+
theme: AppTheme,
|
|
231
|
+
): void {
|
|
232
|
+
const { writeChip, chipCache } = deps;
|
|
233
|
+
if (!writeChip || !chipCache) return;
|
|
234
|
+
const adoptedSet = new Set(adopted);
|
|
235
|
+
for (const pane of panes) {
|
|
236
|
+
if (!adoptedSet.has(pane.sessionName)) continue;
|
|
237
|
+
const chip = paneChip(pane.agent, pane.status, theme);
|
|
238
|
+
if (chipCache.get(pane.paneId) === chip) continue;
|
|
239
|
+
chipCache.set(pane.paneId, chip);
|
|
240
|
+
writeChip(pane.paneId, chip);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Ping the user about who needs them from this tick's transitions. Only runs
|
|
246
|
+
* when the notification deps are wired AND at least one channel is enabled;
|
|
247
|
+
* `lastNotified` is mutated in place so the debounce persists across ticks.
|
|
248
|
+
*/
|
|
249
|
+
function dispatchNotifications(deps: UpdaterTickDeps, events: NotifyEvent[]): void {
|
|
250
|
+
const { listClients, lastNotified, now, prefs, sendToasts: toast, sendSystem } = deps;
|
|
251
|
+
if (!listClients || !lastNotified || !now || !prefs) return;
|
|
252
|
+
if (!prefs.toast && !prefs.macos) return;
|
|
253
|
+
const decision = decideNotifications(events, listClients(), lastNotified, now());
|
|
254
|
+
lastNotified.clear();
|
|
255
|
+
for (const [key, ts] of decision.nextLastNotified) lastNotified.set(key, ts);
|
|
256
|
+
if (prefs.toast && toast) toast(decision.toasts);
|
|
257
|
+
if (prefs.macos && sendSystem) {
|
|
258
|
+
for (const { message } of decision.system) sendSystem(message);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Toast every attached client ONCE that an update is out — "run: tmux-ide
|
|
264
|
+
* update". The one-time guarantee lives in {@link markUpdateNotified} (persisted
|
|
265
|
+
* in the update cache, so it survives updater restarts, unlike the fleet
|
|
266
|
+
* notification's in-memory debounce). Honors the `toast` prefs kill-switch and
|
|
267
|
+
* no-ops unless the toast deps are wired.
|
|
268
|
+
*/
|
|
269
|
+
function dispatchUpdateToast(deps: UpdaterTickDeps, version: string): void {
|
|
270
|
+
const { markUpdateNotified: mark, listClients, sendToasts: toast, prefs } = deps;
|
|
271
|
+
if (!mark || !listClients || !toast) return;
|
|
272
|
+
if (prefs && !prefs.toast) return;
|
|
273
|
+
if (!mark(version)) return; // already toasted this version
|
|
274
|
+
const message = `⬆ tmux-ide v${version} available — run: tmux-ide update`;
|
|
275
|
+
toast(listClients().map((c) => ({ client: c.client, message })));
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Seed a single session's status var NOW (a one-off fleet scan). Called by
|
|
280
|
+
* `adoptSession` so a freshly-adopted bar is never blank while it waits for the
|
|
281
|
+
* background loop's next tick. Best-effort — a failure just defers to the loop.
|
|
282
|
+
*/
|
|
283
|
+
export function seedSessionStatus(session: string): void {
|
|
284
|
+
try {
|
|
285
|
+
const projects = listTeamProjects(createStatusTracker());
|
|
286
|
+
writeSessionStatus(session, buildStatusline(projects, session, 12, getAppConfig().theme));
|
|
287
|
+
} catch {
|
|
288
|
+
// leave it to the updater's next tick
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** Whether the updater session is already up. */
|
|
293
|
+
export function updaterRunning(): boolean {
|
|
294
|
+
try {
|
|
295
|
+
return hasSession(UPDATER_SESSION);
|
|
296
|
+
} catch {
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Ensure the background updater is running: if the `_tmux-ide-chrome` session
|
|
303
|
+
* isn't up, start it detached running `tmux-ide chrome-updater`. `exec` replaces
|
|
304
|
+
* the shell so the pane IS the loop; killing the session stops it. `_`-internal
|
|
305
|
+
* so it's hidden from the bar/switcher. Best-effort — a chrome failure must
|
|
306
|
+
* never break adopt/launch.
|
|
307
|
+
*/
|
|
308
|
+
export function startUpdaterIfNeeded(): void {
|
|
309
|
+
try {
|
|
310
|
+
if (updaterRunning()) return;
|
|
311
|
+
runTmux(["new-session", "-d", "-s", UPDATER_SESSION, "exec tmux-ide chrome-updater"]);
|
|
312
|
+
} catch {
|
|
313
|
+
// best-effort — the bar still works via the last-written var
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** Kill the updater session (called when the last adopted session is unadopted). */
|
|
318
|
+
export function stopUpdater(): void {
|
|
319
|
+
try {
|
|
320
|
+
if (updaterRunning()) runTmux(["kill-session", "-t", UPDATER_SESSION]);
|
|
321
|
+
} catch {
|
|
322
|
+
// already gone — nothing to stop
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** Read the pid the current updater owner recorded, or null when unset/garbage. */
|
|
327
|
+
function readUpdaterPid(): number | null {
|
|
328
|
+
try {
|
|
329
|
+
const raw = runTmux(["show-option", "-s", "-v", UPDATER_PID_OPTION]).toString().trim();
|
|
330
|
+
const pid = Number(raw);
|
|
331
|
+
return raw && Number.isInteger(pid) ? pid : null;
|
|
332
|
+
} catch {
|
|
333
|
+
// option never set (unset server user-options error out) — no owner
|
|
334
|
+
return null;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Claim single-ownership of the loop. Returns false when another LIVE updater
|
|
340
|
+
* already holds the pid option (so a stray manual `chrome-updater` exits
|
|
341
|
+
* cleanly instead of double-writing). A dead/stale pid is reclaimed.
|
|
342
|
+
*/
|
|
343
|
+
function claimUpdater(): boolean {
|
|
344
|
+
const existing = readUpdaterPid();
|
|
345
|
+
if (existing !== null && existing !== process.pid && isProcessAlive(existing)) return false;
|
|
346
|
+
try {
|
|
347
|
+
runTmux(["set-option", "-s", UPDATER_PID_OPTION, String(process.pid)]);
|
|
348
|
+
} catch {
|
|
349
|
+
// if we can't record the pid, still run — the session-level guard suffices
|
|
350
|
+
}
|
|
351
|
+
return true;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** Release ownership on shutdown (only if we still hold it). */
|
|
355
|
+
function releaseUpdater(): void {
|
|
356
|
+
try {
|
|
357
|
+
if (readUpdaterPid() === process.pid) runTmux(["set-option", "-s", "-u", UPDATER_PID_OPTION]);
|
|
358
|
+
} catch {
|
|
359
|
+
// best-effort
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Run the updater loop forever (the body of `tmux-ide chrome-updater`). Claims
|
|
365
|
+
* single-ownership, then rewrites every adopted session's bar immediately and
|
|
366
|
+
* every {@link TICK_MS} thereafter behind ONE persistent tracker (so `done`
|
|
367
|
+
* transitions surface). Blocks — the interval keeps the event loop alive.
|
|
368
|
+
*/
|
|
369
|
+
export function runUpdaterLoop(): void {
|
|
370
|
+
if (!claimUpdater()) return;
|
|
371
|
+
// Resolve the config once for the loop's lifetime — cadence + palette. A
|
|
372
|
+
// config change (theme/keys/cadence) takes effect on the next updater start
|
|
373
|
+
// (which a re-adopt triggers).
|
|
374
|
+
const config = getAppConfig();
|
|
375
|
+
const tracker = createStatusTracker();
|
|
376
|
+
// Persistent across ticks so `diffFleet` can spot working→done etc.
|
|
377
|
+
const prevState = new Map<string, AgentStatus>();
|
|
378
|
+
// Persistent so the notification debounce survives across ticks.
|
|
379
|
+
const lastNotified = new Map<string, number>();
|
|
380
|
+
// Persistent per-pane chip cache so we only rewrite a chip when it changed.
|
|
381
|
+
const chipCache = new Map<string, string>();
|
|
382
|
+
// The fleet snapshotter — pulsed each tick, self-throttled, writes only on a
|
|
383
|
+
// structural change so the fleet can be rebuilt after a tmux-server death.
|
|
384
|
+
const snapshotter = createSnapshotter({
|
|
385
|
+
collect: () => collectFleetSnapshot(),
|
|
386
|
+
read: readSnapshot,
|
|
387
|
+
write: writeSnapshot,
|
|
388
|
+
every: config.updater.snapshotEvery,
|
|
389
|
+
});
|
|
390
|
+
const tick = () => {
|
|
391
|
+
try {
|
|
392
|
+
runUpdaterTick({
|
|
393
|
+
listAdopted: listAdoptedSessions,
|
|
394
|
+
computeProjects: (onPane) => listTeamProjects(tracker, { onPane }),
|
|
395
|
+
writeStatus: writeSessionStatus,
|
|
396
|
+
theme: config.theme,
|
|
397
|
+
writeChip: writePaneChip,
|
|
398
|
+
chipCache,
|
|
399
|
+
prevState,
|
|
400
|
+
appendEvents,
|
|
401
|
+
listClients: listAttachedClients,
|
|
402
|
+
lastNotified,
|
|
403
|
+
now: () => Date.now(),
|
|
404
|
+
prefs: readNotificationPrefs(),
|
|
405
|
+
sendToasts,
|
|
406
|
+
sendSystem: sendSystemNotification,
|
|
407
|
+
maybeCheckForUpdate: () => maybeCheckForUpdate({ enabled: config.updates.check }),
|
|
408
|
+
markUpdateNotified,
|
|
409
|
+
});
|
|
410
|
+
} catch {
|
|
411
|
+
// never let one bad tick kill the loop
|
|
412
|
+
}
|
|
413
|
+
try {
|
|
414
|
+
snapshotter.onTick();
|
|
415
|
+
} catch {
|
|
416
|
+
// a failed snapshot just means staler disaster-recovery state
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
tick();
|
|
420
|
+
const timer = setInterval(tick, config.updater.tickMs);
|
|
421
|
+
const shutdown = () => {
|
|
422
|
+
clearInterval(timer);
|
|
423
|
+
releaseUpdater();
|
|
424
|
+
process.exit(0);
|
|
425
|
+
};
|
|
426
|
+
process.on("SIGTERM", shutdown);
|
|
427
|
+
process.on("SIGINT", shutdown);
|
|
428
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The first-run WELCOME card — shown ONCE, the moment tmux-ide first adopts a
|
|
3
|
+
* session.
|
|
4
|
+
*
|
|
5
|
+
* The discovery problem: once a session is adopted, the whole TUI is one
|
|
6
|
+
* keystroke away (the home cockpit, the switcher, the actions menu, the cheat
|
|
7
|
+
* sheet), but a brand-new user has no way to know those keys exist. The welcome
|
|
8
|
+
* card is the pointer: a tiny hero that names the four "unlock" keys and then
|
|
9
|
+
* gets out of the way forever.
|
|
10
|
+
*
|
|
11
|
+
* "Once" is enforced by a marker file (`~/.tmux-ide/welcomed`, overridable via
|
|
12
|
+
* `TMUX_IDE_HOME` so tests — and the dev box — never see it unexpectedly). The
|
|
13
|
+
* card is ALSO gated by config (`welcome.show`), so it can be suppressed without
|
|
14
|
+
* touching the marker.
|
|
15
|
+
*
|
|
16
|
+
* {@link buildWelcomeText} is PURE (tested); {@link maybeShowWelcomePopup} and
|
|
17
|
+
* the marker helpers wire the io. The CLI `welcome` command prints the card and
|
|
18
|
+
* waits for any key (see bin/cli.ts).
|
|
19
|
+
*/
|
|
20
|
+
import { spawn } from "node:child_process";
|
|
21
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
22
|
+
import { homedir } from "node:os";
|
|
23
|
+
import { dirname, join } from "node:path";
|
|
24
|
+
import { DEFAULT_KEYS, getAppConfig, type AppKeys } from "../../lib/app-config.ts";
|
|
25
|
+
|
|
26
|
+
// --- ANSI styling — the CLI's bold/dim/cyan pattern (matches ./cheatsheet.ts). ---
|
|
27
|
+
const bold = (s: string) => `\x1b[1m${s}\x1b[22m`;
|
|
28
|
+
const dim = (s: string) => `\x1b[2m${s}\x1b[22m`;
|
|
29
|
+
const head = (s: string) => `\x1b[1;36m${s}\x1b[0m`;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Render a tmux key name for humans: `M-` → `⌥`, `C-` → `^`, `S-` → `⇧`. Keeps
|
|
33
|
+
* the card's key hints sourced from the real `M-…` config values (same helper as
|
|
34
|
+
* the cheat sheet).
|
|
35
|
+
*/
|
|
36
|
+
function renderKey(tmuxKey: string): string {
|
|
37
|
+
return tmuxKey.replace(/M-/g, "⌥").replace(/C-/g, "^").replace(/S-/g, "⇧");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Absolute path to the "already welcomed" marker: `<home>/welcomed`, where
|
|
42
|
+
* `<home>` is `TMUX_IDE_HOME` when set (tests / per-run overrides), else
|
|
43
|
+
* `~/.tmux-ide`. The env override lets a live test point the marker at a scratch
|
|
44
|
+
* dir so it never touches — or is confused by — the real user's marker.
|
|
45
|
+
*/
|
|
46
|
+
export function welcomeMarkerPath(): string {
|
|
47
|
+
const home = process.env.TMUX_IDE_HOME ?? join(homedir(), ".tmux-ide");
|
|
48
|
+
return join(home, "welcomed");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Whether the first-run welcome should show: the marker file is ABSENT and the
|
|
53
|
+
* config hasn't disabled it (`welcome.show !== false`). A missing/garbage config
|
|
54
|
+
* defaults `welcome.show` to true (see app-config), so a fresh install shows it.
|
|
55
|
+
*/
|
|
56
|
+
export function shouldShowWelcome(): boolean {
|
|
57
|
+
return !existsSync(welcomeMarkerPath()) && getAppConfig().welcome.show;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Create the marker file so the welcome shows only once. Best-effort — a marker
|
|
62
|
+
* we can't write means the card may show again, but it must never crash adopt.
|
|
63
|
+
*/
|
|
64
|
+
export function markWelcomed(): void {
|
|
65
|
+
const path = welcomeMarkerPath();
|
|
66
|
+
try {
|
|
67
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
68
|
+
writeFileSync(path, new Date().toISOString());
|
|
69
|
+
} catch {
|
|
70
|
+
// can't write the marker — degrade to "may show again", never throw
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* PURE — the welcome card text (ANSI-styled), sized for a small ~60×12 popup. A
|
|
76
|
+
* tiny hero naming the FOUR keys that unlock the whole TUI, sourced from the live
|
|
77
|
+
* key config so a rebind relabels the card. Ends with the "shows once" note so
|
|
78
|
+
* the user knows it won't nag.
|
|
79
|
+
*/
|
|
80
|
+
export function buildWelcomeText(keys: AppKeys = DEFAULT_KEYS): string {
|
|
81
|
+
const lines = [
|
|
82
|
+
head(" You're in tmux-ide"),
|
|
83
|
+
dim(" your terminal, now a fleet you can see and steer."),
|
|
84
|
+
"",
|
|
85
|
+
" Four keys unlock everything:",
|
|
86
|
+
` ${bold("right-click")} the actions menu — anywhere`,
|
|
87
|
+
` ${bold(renderKey(keys.home).padEnd(11))} the home cockpit`,
|
|
88
|
+
` ${bold(renderKey(keys.popup).padEnd(11))} switch session`,
|
|
89
|
+
` ${bold(renderKey(keys.cheatsheet).padEnd(11))} all keys (the cheat sheet)`,
|
|
90
|
+
"",
|
|
91
|
+
dim(" This card shows once — press any key to close."),
|
|
92
|
+
];
|
|
93
|
+
return lines.join("\n");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* io — float the one-time welcome card on the CURRENT tmux client, best-effort.
|
|
98
|
+
*
|
|
99
|
+
* Called at the end of {@link ../chrome/statusline.ts adoptSession}. Gated by
|
|
100
|
+
* {@link shouldShowWelcome} (marker + config) AND by being inside a tmux client
|
|
101
|
+
* — outside tmux there's nowhere to float the popup, so we neither show it nor
|
|
102
|
+
* burn the one-shot (the marker stays, so the next in-tmux adopt still shows it).
|
|
103
|
+
*
|
|
104
|
+
* The popup is spawned DETACHED and unref'd so it never blocks the adopt: a
|
|
105
|
+
* `display-popup -E` would otherwise keep the tmux CLI alive until the user
|
|
106
|
+
* pressed a key. `spawn` inherits `$TMUX`, so the popup lands on the invoking
|
|
107
|
+
* client. The marker is written immediately after the spawn attempt so a rapid
|
|
108
|
+
* `adopt --all` shows the card exactly once, not once per session.
|
|
109
|
+
*/
|
|
110
|
+
export function maybeShowWelcomePopup(): void {
|
|
111
|
+
if (!shouldShowWelcome()) return;
|
|
112
|
+
if (!process.env.TMUX) return;
|
|
113
|
+
try {
|
|
114
|
+
const child = spawn(
|
|
115
|
+
"tmux",
|
|
116
|
+
["display-popup", "-E", "-w", "60", "-h", "12", "tmux-ide welcome"],
|
|
117
|
+
{ stdio: "ignore", detached: true },
|
|
118
|
+
);
|
|
119
|
+
child.unref();
|
|
120
|
+
} catch {
|
|
121
|
+
// tmux missing / no client — best-effort, still mark so we don't retry forever
|
|
122
|
+
}
|
|
123
|
+
markWelcomed();
|
|
124
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolution for the TUI surfaces across BOTH distribution modes.
|
|
3
|
+
*
|
|
4
|
+
* Dev checkout: the surfaces are OpenTUI/Solid `.tsx` spawned by `bun` (the
|
|
5
|
+
* bunfig preload supplies the JSX transform). Installed via npm/pnpm/bun: there
|
|
6
|
+
* is no checkout and no `bun`, so we fall back to the compiled `tmux-ide-tui`
|
|
7
|
+
* binary (see scripts/build-tui.mjs) which bundles every surface behind a
|
|
8
|
+
* `<surface> [flags]` argv dispatcher and needs no runtime.
|
|
9
|
+
*
|
|
10
|
+
* Order is "checkout first, binary second": a dev machine that happens to have
|
|
11
|
+
* built the binary still uses its live `.tsx` sources. The binary is consulted
|
|
12
|
+
* only when the checkout sources or `bun` are missing.
|
|
13
|
+
*
|
|
14
|
+
* {@link resolveTuiLaunch} is a PURE decision (unit-tested); {@link findCompiledTui}
|
|
15
|
+
* and {@link isBunAvailable} are the thin io probes that feed it.
|
|
16
|
+
*/
|
|
17
|
+
import { existsSync } from "node:fs";
|
|
18
|
+
import { dirname, resolve } from "node:path";
|
|
19
|
+
import { fileURLToPath } from "node:url";
|
|
20
|
+
import { execFileSync } from "node:child_process";
|
|
21
|
+
|
|
22
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
23
|
+
|
|
24
|
+
export type TuiLaunch =
|
|
25
|
+
| { mode: "bun"; bin: "bun"; argv: string[] }
|
|
26
|
+
| { mode: "binary"; bin: string; argv: string[] }
|
|
27
|
+
| { mode: "unavailable"; reasons: string[] };
|
|
28
|
+
|
|
29
|
+
export interface TuiResolveInput {
|
|
30
|
+
/** Dispatcher surface token: team | explorer | changes | preview | config | setup | sidebar. */
|
|
31
|
+
surface: string;
|
|
32
|
+
/** The checkout `.tsx` entry for this surface. */
|
|
33
|
+
scriptPath: string;
|
|
34
|
+
/** Surface flags (`--session=…`, `--dir=…`, `--theme=…`, …). */
|
|
35
|
+
args: string[];
|
|
36
|
+
/** Whether {@link scriptPath} exists (checkout present). */
|
|
37
|
+
checkoutExists: boolean;
|
|
38
|
+
/** Whether the `bun` runtime is on PATH. */
|
|
39
|
+
bunAvailable: boolean;
|
|
40
|
+
/** Absolute path to the compiled `tmux-ide-tui`, or null if not found. */
|
|
41
|
+
compiledBinary: string | null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* PURE — decide how to launch a surface. Bun-from-checkout wins when both are
|
|
46
|
+
* present (dev); the compiled binary is the installed fallback; otherwise the
|
|
47
|
+
* caller surfaces an actionable message built from `reasons`.
|
|
48
|
+
*/
|
|
49
|
+
export function resolveTuiLaunch(input: TuiResolveInput): TuiLaunch {
|
|
50
|
+
if (input.checkoutExists && input.bunAvailable) {
|
|
51
|
+
return { mode: "bun", bin: "bun", argv: [input.scriptPath, ...input.args] };
|
|
52
|
+
}
|
|
53
|
+
if (input.compiledBinary) {
|
|
54
|
+
return { mode: "binary", bin: input.compiledBinary, argv: [input.surface, ...input.args] };
|
|
55
|
+
}
|
|
56
|
+
const reasons: string[] = [];
|
|
57
|
+
if (!input.checkoutExists) {
|
|
58
|
+
reasons.push(
|
|
59
|
+
"the TUI widget sources are absent (reinstall tmux-ide — releases since v2.6.1 ship them)",
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
if (!input.bunAvailable) {
|
|
63
|
+
reasons.push("the `bun` runtime is not installed (https://bun.sh)");
|
|
64
|
+
}
|
|
65
|
+
reasons.push(
|
|
66
|
+
"no compiled `tmux-ide-tui` binary was found (build one with `pnpm build:tui`, or reinstall a release that ships it)",
|
|
67
|
+
);
|
|
68
|
+
return { mode: "unavailable", reasons };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Candidate locations for the compiled binary, relative to an anchor dir. The
|
|
72
|
+
// installed tarball ships it at packages/daemon/dist/tui/tmux-ide-tui and the
|
|
73
|
+
// bin is bin/cli.js, so a bin-anchored `../packages/daemon/dist/...` hits it;
|
|
74
|
+
// the other rels cover unbundled daemon layouts and a co-located binary.
|
|
75
|
+
const BINARY_RELS = [
|
|
76
|
+
"../packages/daemon/dist/tui/tmux-ide-tui",
|
|
77
|
+
"../../dist/tui/tmux-ide-tui",
|
|
78
|
+
"../dist/tui/tmux-ide-tui",
|
|
79
|
+
"dist/tui/tmux-ide-tui",
|
|
80
|
+
"tmux-ide-tui",
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* io — locate the compiled `tmux-ide-tui`. Honors `TMUX_IDE_TUI_BIN` (absolute
|
|
85
|
+
* override, e.g. for tests / custom installs), then probes bin- and
|
|
86
|
+
* module-relative candidates. Returns null when none exists.
|
|
87
|
+
*/
|
|
88
|
+
export function findCompiledTui(): string | null {
|
|
89
|
+
const override = process.env.TMUX_IDE_TUI_BIN;
|
|
90
|
+
if (override) return existsSync(override) ? override : null;
|
|
91
|
+
|
|
92
|
+
const anchors: string[] = [];
|
|
93
|
+
if (process.argv[1]) anchors.push(dirname(process.argv[1]));
|
|
94
|
+
anchors.push(__dirname);
|
|
95
|
+
|
|
96
|
+
for (const anchor of anchors) {
|
|
97
|
+
for (const rel of BINARY_RELS) {
|
|
98
|
+
const candidate = resolve(anchor, rel);
|
|
99
|
+
if (existsSync(candidate)) return candidate;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** io — is the `bun` runtime callable? */
|
|
106
|
+
export function isBunAvailable(): boolean {
|
|
107
|
+
try {
|
|
108
|
+
execFileSync("bun", ["--version"], { stdio: "ignore" });
|
|
109
|
+
return true;
|
|
110
|
+
} catch {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
}
|