tmux-ide 2.6.1 → 2.8.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 +36 -14
- package/bin/cli.js +4174 -1235
- package/bin/cli.ts +426 -72
- package/package.json +10 -6
- package/packages/contracts/src/__tests__/control.test.ts +154 -0
- package/packages/contracts/src/control.ts +217 -0
- package/packages/contracts/src/index.ts +1 -0
- package/packages/daemon/dist/agent-explain.d.ts +8 -1
- package/packages/daemon/dist/agent-explain.js +19 -3
- package/packages/daemon/dist/control/client.d.ts +23 -0
- package/packages/daemon/dist/control/client.js +105 -0
- package/packages/daemon/dist/control/dispatch.d.ts +34 -0
- package/packages/daemon/dist/control/dispatch.js +83 -0
- package/packages/daemon/dist/control/fanout.d.ts +19 -0
- package/packages/daemon/dist/control/fanout.js +37 -0
- package/packages/daemon/dist/control/frames.d.ts +23 -0
- package/packages/daemon/dist/control/frames.js +37 -0
- package/packages/daemon/dist/control/lifecycle.d.ts +45 -0
- package/packages/daemon/dist/control/lifecycle.js +114 -0
- package/packages/daemon/dist/control/server.d.ts +16 -0
- package/packages/daemon/dist/control/server.js +214 -0
- package/packages/daemon/dist/control/verbs.d.ts +11 -0
- package/packages/daemon/dist/control/verbs.js +91 -0
- package/packages/daemon/dist/doctor.d.ts +18 -0
- package/packages/daemon/dist/doctor.js +105 -15
- package/packages/daemon/dist/lib/agent-discovery.d.ts +27 -2
- package/packages/daemon/dist/lib/agent-discovery.js +29 -14
- package/packages/daemon/dist/lib/app-config.d.ts +106 -0
- package/packages/daemon/dist/lib/app-config.js +104 -5
- package/packages/daemon/dist/lib/manifest-pack.d.ts +79 -0
- package/packages/daemon/dist/lib/manifest-pack.js +232 -0
- package/packages/daemon/dist/lib/state-home.d.ts +2 -0
- package/packages/daemon/dist/lib/state-home.js +12 -0
- package/packages/daemon/dist/lib/tui-binary.d.ts +57 -0
- package/packages/daemon/dist/lib/tui-binary.js +130 -0
- package/packages/daemon/dist/lib/update-check.js +5 -0
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Info.plist +34 -0
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/MacOS/tmux-ide-notifier +0 -0
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/PkgInfo +1 -0
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Resources/AppIcon.icns +0 -0
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Resources/Assets.car +0 -0
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/_CodeSignature/CodeResources +139 -0
- package/packages/daemon/dist/restore.d.ts +35 -8
- package/packages/daemon/dist/restore.js +52 -15
- package/packages/daemon/dist/send.d.ts +33 -1
- package/packages/daemon/dist/send.js +32 -19
- package/packages/daemon/dist/widgets/explorer/breadcrumbs.d.ts +1 -1
- package/packages/daemon/dist/widgets/explorer/footer.d.ts +1 -1
- package/packages/daemon/dist/widgets/explorer/tree.d.ts +1 -1
- package/packages/daemon/dist/widgets/lib/help-overlay.d.ts +1 -1
- package/packages/daemon/dist/widgets/setup/agent-naming.d.ts +1 -1
- package/packages/daemon/dist/widgets/setup/config-tree.d.ts +1 -1
- package/packages/daemon/dist/widgets/setup/detect-panel.d.ts +1 -1
- package/packages/daemon/dist/widgets/setup/field-editor.d.ts +1 -1
- package/packages/daemon/dist/widgets/setup/footer.d.ts +1 -1
- package/packages/daemon/dist/widgets/setup/layout-picker.d.ts +1 -1
- package/packages/daemon/src/agent-explain.ts +34 -6
- package/packages/daemon/src/control/client.ts +128 -0
- package/packages/daemon/src/control/dispatch.ts +107 -0
- package/packages/daemon/src/control/fanout.ts +44 -0
- package/packages/daemon/src/control/frames.ts +40 -0
- package/packages/daemon/src/control/lifecycle.ts +151 -0
- package/packages/daemon/src/control/server.ts +237 -0
- package/packages/daemon/src/control/verbs.ts +118 -0
- package/packages/daemon/src/doctor.ts +113 -28
- package/packages/daemon/src/lib/agent-discovery.ts +53 -13
- package/packages/daemon/src/lib/app-config.ts +194 -5
- package/packages/daemon/src/lib/manifest-pack.ts +255 -0
- package/packages/daemon/src/lib/state-home.ts +13 -0
- package/packages/daemon/src/lib/tui-binary.ts +165 -0
- package/packages/daemon/src/lib/update-check.ts +5 -0
- package/packages/daemon/src/restore.ts +53 -15
- package/packages/daemon/src/send.ts +55 -21
- package/packages/daemon/src/tui/chrome/events.ts +4 -4
- package/packages/daemon/src/tui/chrome/front-door.ts +39 -0
- package/packages/daemon/src/tui/chrome/notify-prefs.ts +58 -0
- package/packages/daemon/src/tui/chrome/notify-state.ts +76 -0
- package/packages/daemon/src/tui/chrome/notify.ts +737 -52
- package/packages/daemon/src/tui/chrome/updater.ts +318 -30
- package/packages/daemon/src/tui/compiled.ts +11 -3
- package/packages/daemon/src/tui/detect/classify.ts +49 -0
- package/packages/daemon/src/tui/detect/manifest-loader.ts +56 -6
- package/packages/daemon/src/tui/detect/manifest.ts +39 -3
- package/packages/daemon/src/tui/detect/manifests.ts +395 -33
- package/packages/daemon/src/tui/detect/process-tree.ts +33 -3
- package/packages/daemon/src/tui/detect/session-id.ts +503 -0
- package/packages/daemon/src/tui/integrations/opencode.ts +121 -0
- package/packages/daemon/src/tui/main.ts +13 -1
- package/packages/daemon/src/tui/mirror/ack-writer.ts +77 -0
- package/packages/daemon/src/tui/mirror/agent-chip.ts +126 -0
- package/packages/daemon/src/tui/mirror/agent-lifecycle.ts +437 -0
- package/packages/daemon/src/tui/mirror/agent-rows.ts +155 -0
- package/packages/daemon/src/tui/mirror/app-state.ts +342 -0
- package/packages/daemon/src/tui/mirror/app.tsx +7048 -0
- package/packages/daemon/src/tui/mirror/attention.ts +110 -0
- package/packages/daemon/src/tui/mirror/blit.ts +186 -0
- package/packages/daemon/src/tui/mirror/control-client.ts +80 -9
- package/packages/daemon/src/tui/mirror/dialog-model.ts +298 -0
- package/packages/daemon/src/tui/mirror/dialog-stack.ts +367 -0
- package/packages/daemon/src/tui/mirror/diff-model.ts +387 -0
- package/packages/daemon/src/tui/mirror/editor-buffer.ts +117 -0
- package/packages/daemon/src/tui/mirror/file-tree.ts +322 -0
- package/packages/daemon/src/tui/mirror/focus-border.ts +57 -0
- package/packages/daemon/src/tui/mirror/folder-picker.ts +124 -0
- package/packages/daemon/src/tui/mirror/home-model.ts +174 -0
- package/packages/daemon/src/tui/mirror/host-terminal.ts +49 -0
- package/packages/daemon/src/tui/mirror/hosted.ts +205 -0
- package/packages/daemon/src/tui/mirror/input-coalescer.ts +105 -0
- package/packages/daemon/src/tui/mirror/layout-parse.ts +154 -0
- package/packages/daemon/src/tui/mirror/menu-model.ts +210 -0
- package/packages/daemon/src/tui/mirror/palette.ts +564 -0
- package/packages/daemon/src/tui/mirror/pane-mirror.ts +578 -20
- package/packages/daemon/src/tui/mirror/pane-surface.tsx +422 -0
- package/packages/daemon/src/tui/mirror/perf-tap.ts +186 -0
- package/packages/daemon/src/tui/mirror/resize-model.ts +85 -0
- package/packages/daemon/src/tui/mirror/scrollbar-model.ts +88 -0
- package/packages/daemon/src/tui/mirror/search-model.ts +70 -0
- package/packages/daemon/src/tui/mirror/selection.ts +376 -0
- package/packages/daemon/src/tui/mirror/session-mirror.ts +724 -0
- package/packages/daemon/src/tui/mirror/settings-model.ts +425 -0
- package/packages/daemon/src/tui/mirror/sidebar.tsx +218 -0
- package/packages/daemon/src/tui/mirror/size-truth.ts +130 -0
- package/packages/daemon/src/tui/mirror/spans.ts +46 -0
- package/packages/daemon/src/tui/mirror/status-grammar.ts +32 -0
- package/packages/daemon/src/tui/mirror/theme.ts +45 -0
- package/packages/daemon/src/tui/team/entry.ts +34 -7
- package/packages/daemon/src/tui/team/fuzzy.ts +20 -0
- package/packages/daemon/src/tui/team/report.ts +11 -1
- package/packages/daemon/src/tui/team/sessions.ts +184 -17
- package/packages/daemon/src/tui/team/wait.ts +144 -0
- package/scripts/build-macos-notifier.mjs +160 -0
- package/scripts/build-tui.mjs +11 -4
- package/scripts/perf-mirror.mjs +313 -0
- package/scripts/postinstall.js +8 -1
- package/scripts/prepublish-check.mjs +37 -1
- package/scripts/publish-tap.sh +55 -0
- package/skill/SKILL.md +110 -2
- package/templates/AGENTS.md +14 -7
- package/templates/agent-team-monorepo.yml +8 -0
- package/templates/agent-team-nextjs.yml +8 -0
- package/templates/agent-team.yml +10 -0
- package/templates/convex.yml +2 -0
- package/templates/default.yml +11 -5
- package/templates/go.yml +4 -0
- package/templates/missions.yml +6 -0
- package/templates/nextjs.yml +4 -0
- package/templates/python.yml +4 -0
- package/templates/skills/backend.md +5 -12
- package/templates/skills/frontend.md +5 -12
- package/templates/skills/general-worker.md +5 -12
- package/templates/skills/researcher.md +7 -12
- package/templates/skills/reviewer.md +7 -16
- package/templates/vite.yml +4 -0
- package/packages/daemon/src/tui/mirror/viewer.tsx +0 -166
|
@@ -14,10 +14,12 @@ import { execFileSync } from "node:child_process";
|
|
|
14
14
|
import {
|
|
15
15
|
classifyInstant,
|
|
16
16
|
parseAuthority,
|
|
17
|
+
parseAuthorityEpoch,
|
|
18
|
+
sanitizeAgentText,
|
|
17
19
|
type AgentStatus,
|
|
18
20
|
type StatusTracker,
|
|
19
21
|
} from "../detect/classify.ts";
|
|
20
|
-
import type { AgentManifest } from "../detect/manifest.ts";
|
|
22
|
+
import type { AgentManifest, ManifestConfidence } from "../detect/manifest.ts";
|
|
21
23
|
import { readProcessTable, resolveAgentCommand } from "../detect/process-tree.ts";
|
|
22
24
|
import { readPaneSnapshot } from "../detect/snapshot.ts";
|
|
23
25
|
|
|
@@ -33,6 +35,119 @@ export interface TeamSession {
|
|
|
33
35
|
* this is the richer breakdown the switcher/cockpit navigate.
|
|
34
36
|
*/
|
|
35
37
|
windowList: TeamWindow[];
|
|
38
|
+
/**
|
|
39
|
+
* Per-pane agent detail — one entry for every pane the two-layer detection
|
|
40
|
+
* classifies as an agent (a manifest resolved and it isn't the `shell`
|
|
41
|
+
* catch-all). A FLAT list across the session's windows; each entry carries
|
|
42
|
+
* its own `windowIndex`, so a consumer that wants a per-window view groups by
|
|
43
|
+
* it. ADDITIVE: the rollup fields above (`status`, `windowList`, `panes`) are
|
|
44
|
+
* unchanged — this only SURFACES per-pane truth the rollup already computes.
|
|
45
|
+
* Optional so pre-existing constructors stay valid; {@link listTeamSessions}
|
|
46
|
+
* always populates it (possibly empty).
|
|
47
|
+
*/
|
|
48
|
+
agents?: PaneAgentEntry[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Per-pane agent detail surfaced alongside a session's rollup. Emitted only for
|
|
53
|
+
* panes that resolve to a real agent (a manifest, excluding the `shell`
|
|
54
|
+
* catch-all). Everything here is already computed during the status rollup —
|
|
55
|
+
* this record just keeps it instead of discarding it.
|
|
56
|
+
*/
|
|
57
|
+
export interface PaneAgentEntry {
|
|
58
|
+
/** tmux pane id, e.g. `%5`. */
|
|
59
|
+
paneId: string;
|
|
60
|
+
/** `window_index` of the window (tab) this pane lives in. */
|
|
61
|
+
windowIndex: number;
|
|
62
|
+
/** Owning session name (repeated per entry so a flattened list stays keyed). */
|
|
63
|
+
session: string;
|
|
64
|
+
/** The resolved agent kind — the manifest id (`claude`, `codex`, …). */
|
|
65
|
+
kind: string;
|
|
66
|
+
/** Final per-pane status (authority when fresh, else scraped/tracked). */
|
|
67
|
+
state: AgentStatus;
|
|
68
|
+
/** The manifest's evidence confidence (`conservative` when the manifest omits it). */
|
|
69
|
+
confidence: ManifestConfidence;
|
|
70
|
+
/**
|
|
71
|
+
* The authority state's epoch stamp when the AUTHORITY layer provided the
|
|
72
|
+
* state (`@agent_state` = `"<state>:<epoch>"`); null for a scraped/tracked
|
|
73
|
+
* pane, which has no authoritative timestamp.
|
|
74
|
+
*/
|
|
75
|
+
since: number | null;
|
|
76
|
+
/** `pane_title`. */
|
|
77
|
+
title: string;
|
|
78
|
+
/** `pane_current_command` — the pane's immediate process (often node/bun/sh). */
|
|
79
|
+
command: string;
|
|
80
|
+
/** `pane_current_path` — the pane's working directory. */
|
|
81
|
+
dir: string;
|
|
82
|
+
/**
|
|
83
|
+
* Self-reported one-liner (`@agent_status_text`, sanitized + clamped to 32
|
|
84
|
+
* chars) — what the agent says it is doing ("refactoring auth"). ADDITIVE and
|
|
85
|
+
* present only while the pane's `@agent_state` stamp is FRESH (the metadata
|
|
86
|
+
* follows the same authority-staleness rules; stale/absent authority drops it).
|
|
87
|
+
*/
|
|
88
|
+
statusText?: string;
|
|
89
|
+
/**
|
|
90
|
+
* Self-reported display name (`@agent_display_name`, sanitized) — label
|
|
91
|
+
* precedence over `kind` in the sidebar/chips. Same freshness gate as
|
|
92
|
+
* {@link statusText}. ADDITIVE.
|
|
93
|
+
*/
|
|
94
|
+
displayName?: string;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Build a {@link PaneAgentEntry} from a classified pane — PURE. Returns null
|
|
99
|
+
* for a NON-agent pane (no manifest resolved, or the `shell` catch-all), which
|
|
100
|
+
* gets no entry. `since` is threaded from the caller (the authority epoch when
|
|
101
|
+
* the authority layer supplied the state, else null). No tmux/`ps` access —
|
|
102
|
+
* every input is already resolved by the rollup.
|
|
103
|
+
*/
|
|
104
|
+
export function buildAgentEntry(input: {
|
|
105
|
+
sessionName: string;
|
|
106
|
+
pane: Pick<PaneRecord, "id" | "windowIndex" | "title" | "cmd" | "dir">;
|
|
107
|
+
manifest: AgentManifest | undefined;
|
|
108
|
+
state: AgentStatus;
|
|
109
|
+
since: number | null;
|
|
110
|
+
/** Already-sanitized display metadata (undefined when absent/stale). */
|
|
111
|
+
statusText?: string;
|
|
112
|
+
displayName?: string;
|
|
113
|
+
}): PaneAgentEntry | null {
|
|
114
|
+
const { manifest, pane } = input;
|
|
115
|
+
if (!manifest || manifest.id === "shell") return null;
|
|
116
|
+
return {
|
|
117
|
+
paneId: pane.id,
|
|
118
|
+
windowIndex: pane.windowIndex,
|
|
119
|
+
session: input.sessionName,
|
|
120
|
+
kind: manifest.id,
|
|
121
|
+
state: input.state,
|
|
122
|
+
confidence: manifest.confidence ?? "conservative",
|
|
123
|
+
since: input.since,
|
|
124
|
+
title: pane.title,
|
|
125
|
+
command: pane.cmd,
|
|
126
|
+
dir: pane.dir,
|
|
127
|
+
...(input.statusText !== undefined ? { statusText: input.statusText } : {}),
|
|
128
|
+
...(input.displayName !== undefined ? { displayName: input.displayName } : {}),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* PURE — the display metadata a pane's agent entry surfaces: the sanitized
|
|
134
|
+
* `@agent_status_text` / `@agent_display_name` options, gated on the SAME
|
|
135
|
+
* authority-freshness verdict as the state itself. `authorityFresh` is
|
|
136
|
+
* `parseAuthority(...) !== null` — when the pane's `@agent_state` stamp is
|
|
137
|
+
* absent or stale (a dead hook mid-turn), the metadata is dropped with it
|
|
138
|
+
* rather than lying alongside a scraped state.
|
|
139
|
+
*/
|
|
140
|
+
export function agentMetadataFor(
|
|
141
|
+
pane: Pick<PaneRecord, "statusTextRaw" | "displayNameRaw">,
|
|
142
|
+
authorityFresh: boolean,
|
|
143
|
+
): { statusText?: string; displayName?: string } {
|
|
144
|
+
if (!authorityFresh) return {};
|
|
145
|
+
const statusText = sanitizeAgentText(pane.statusTextRaw);
|
|
146
|
+
const displayName = sanitizeAgentText(pane.displayNameRaw);
|
|
147
|
+
return {
|
|
148
|
+
...(statusText !== undefined ? { statusText } : {}),
|
|
149
|
+
...(displayName !== undefined ? { displayName } : {}),
|
|
150
|
+
};
|
|
36
151
|
}
|
|
37
152
|
|
|
38
153
|
/**
|
|
@@ -63,6 +178,15 @@ export interface PaneDetail {
|
|
|
63
178
|
paneId: string;
|
|
64
179
|
agent: string | null;
|
|
65
180
|
status: AgentStatus;
|
|
181
|
+
/** `window_index` of the pane's window — the notification path suppresses
|
|
182
|
+
* toasts window-granularly, so the transition must know its window. */
|
|
183
|
+
windowIndex: number;
|
|
184
|
+
/** `pane_pid` — root of the pane's process tree (session-id capture probes from it). */
|
|
185
|
+
pid: number;
|
|
186
|
+
/** `pane_current_path` — the pane's working directory. */
|
|
187
|
+
dir: string;
|
|
188
|
+
/** Existing `@agent_session_id` stamp, or null (capture only fills empty ones). */
|
|
189
|
+
sessionId: string | null;
|
|
66
190
|
}
|
|
67
191
|
|
|
68
192
|
interface PaneRecord {
|
|
@@ -74,10 +198,18 @@ interface PaneRecord {
|
|
|
74
198
|
cmd: string;
|
|
75
199
|
/** `pane_title`. */
|
|
76
200
|
title: string;
|
|
201
|
+
/** `pane_current_path` — the pane's working directory. */
|
|
202
|
+
dir: string;
|
|
77
203
|
/** Raw `@agent_state` pane option (authority layer), if set. */
|
|
78
204
|
authority: string;
|
|
79
205
|
/** Raw `@agent_hint` pane option — forces a manifest when set. */
|
|
80
206
|
hint: string;
|
|
207
|
+
/** Raw `@agent_session_id` pane option — the agent's own session id, if recorded. */
|
|
208
|
+
sessionId: string;
|
|
209
|
+
/** Raw `@agent_status_text` pane option — sanitized by {@link agentMetadataFor}. */
|
|
210
|
+
statusTextRaw: string;
|
|
211
|
+
/** Raw `@agent_display_name` pane option — sanitized by {@link agentMetadataFor}. */
|
|
212
|
+
displayNameRaw: string;
|
|
81
213
|
/** `window_index` — the window (tab) this pane lives in. */
|
|
82
214
|
windowIndex: number;
|
|
83
215
|
/** `window_name`. */
|
|
@@ -105,12 +237,13 @@ export function excludeSidebarPanes<T extends { sidebar: boolean }>(panes: T[]):
|
|
|
105
237
|
const SEVERITY: AgentStatus[] = ["blocked", "working", "done", "idle", "unknown"];
|
|
106
238
|
|
|
107
239
|
/**
|
|
108
|
-
* Whether a session should appear in the switcher.
|
|
109
|
-
* internal plumbing (the `_tmux-ide-chrome` updater,
|
|
110
|
-
*
|
|
240
|
+
* Whether a session should appear in the switcher. `_`-prefixed sessions are
|
|
241
|
+
* internal plumbing (the `_tmux-ide-chrome` updater, the `_tmux-ide-app` host)
|
|
242
|
+
* and `zz-`-prefixed sessions are development scratch sessions — both are
|
|
243
|
+
* filtered out so the cockpit never lists — or navigates into — infrastructure.
|
|
111
244
|
*/
|
|
112
245
|
export function isListableSession(name: string): boolean {
|
|
113
|
-
return !name.startsWith("_");
|
|
246
|
+
return !name.startsWith("_") && !name.startsWith("zz-");
|
|
114
247
|
}
|
|
115
248
|
|
|
116
249
|
function tmux(args: string[]): string {
|
|
@@ -177,16 +310,21 @@ export function listTeamSessions(
|
|
|
177
310
|
const seen = opts.viewed === name;
|
|
178
311
|
|
|
179
312
|
const nowSec = Math.floor(Date.now() / 1000);
|
|
180
|
-
const
|
|
313
|
+
const agents: PaneAgentEntry[] = [];
|
|
181
314
|
const statuses = panes.map((pane) => {
|
|
182
315
|
// AUTHORITY first: a fresh hook-reported state outranks scraping.
|
|
183
316
|
const authority = parseAuthority(pane.authority, nowSec);
|
|
184
317
|
let status: AgentStatus;
|
|
185
|
-
// The
|
|
186
|
-
//
|
|
187
|
-
//
|
|
318
|
+
// The agent kind is needed for BOTH the chip (onPane) and the per-pane
|
|
319
|
+
// agent entry, so the manifest is resolved for every pane. It's cheap —
|
|
320
|
+
// the process table is already loaded, and authority panes take NO
|
|
321
|
+
// capture-pane round-trip.
|
|
188
322
|
let manifest: AgentManifest | undefined;
|
|
323
|
+
// The authority state's own timestamp (its "since"); only fresh
|
|
324
|
+
// authority states carry one — scraped panes have no stamp.
|
|
325
|
+
let since: number | null = null;
|
|
189
326
|
if (authority !== null) {
|
|
327
|
+
since = parseAuthorityEpoch(pane.authority);
|
|
190
328
|
if (authority === "done" && seen) {
|
|
191
329
|
// Viewing acknowledges a finished agent — persist the ack so the
|
|
192
330
|
// pane doesn't flip back to done on the next tick.
|
|
@@ -195,11 +333,9 @@ export function listTeamSessions(
|
|
|
195
333
|
} else {
|
|
196
334
|
status = authority;
|
|
197
335
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
}).manifest;
|
|
202
|
-
}
|
|
336
|
+
manifest = resolveAgentCommand(pane.cmd, pane.pid, processTable, {
|
|
337
|
+
hint: pane.hint,
|
|
338
|
+
}).manifest;
|
|
203
339
|
} else {
|
|
204
340
|
// FALLBACK: snapshot scraping. Resolve the real agent from the pane's
|
|
205
341
|
// process tree (pane_current_command alone is usually just node/bun/sh).
|
|
@@ -220,7 +356,24 @@ export function listTeamSessions(
|
|
|
220
356
|
paneId: pane.id,
|
|
221
357
|
agent: manifest && manifest.id !== "shell" ? manifest.id : null,
|
|
222
358
|
status,
|
|
359
|
+
windowIndex: pane.windowIndex,
|
|
360
|
+
pid: pane.pid,
|
|
361
|
+
dir: pane.dir,
|
|
362
|
+
sessionId: pane.sessionId.length > 0 ? pane.sessionId : null,
|
|
363
|
+
});
|
|
364
|
+
// Surface per-pane agent detail (same resolved manifest/status — nothing
|
|
365
|
+
// re-derived). Non-agent panes yield null and are skipped. Display
|
|
366
|
+
// metadata (@agent_status_text/@agent_display_name) is gated on the SAME
|
|
367
|
+
// freshness verdict as the authority state — stale/absent stamp drops it.
|
|
368
|
+
const entry = buildAgentEntry({
|
|
369
|
+
sessionName: name,
|
|
370
|
+
pane,
|
|
371
|
+
manifest,
|
|
372
|
+
state: status,
|
|
373
|
+
since,
|
|
374
|
+
...agentMetadataFor(pane, authority !== null),
|
|
223
375
|
});
|
|
376
|
+
if (entry) agents.push(entry);
|
|
224
377
|
return status;
|
|
225
378
|
});
|
|
226
379
|
|
|
@@ -233,6 +386,7 @@ export function listTeamSessions(
|
|
|
233
386
|
// `panes` and `statuses` are parallel (statuses = panes.map(...)), so
|
|
234
387
|
// the pure rollup can group each pane's window with its resolved status.
|
|
235
388
|
windowList: rollupWindows(panes, statuses),
|
|
389
|
+
agents,
|
|
236
390
|
};
|
|
237
391
|
});
|
|
238
392
|
}
|
|
@@ -243,9 +397,14 @@ function collectPanes(): Map<string, PaneRecord[]> {
|
|
|
243
397
|
"list-panes",
|
|
244
398
|
"-a",
|
|
245
399
|
"-F",
|
|
246
|
-
// Window fields sit before pane_title so the (tab-safe)
|
|
247
|
-
// trailing catch-all — window names don't contain tabs
|
|
248
|
-
|
|
400
|
+
// Window fields + pane_current_path sit before pane_title so the (tab-safe)
|
|
401
|
+
// title stays the trailing catch-all — window names/paths don't contain tabs
|
|
402
|
+
// in practice. pane_current_path rides this SAME list-panes call (no extra
|
|
403
|
+
// tmux round-trip) so per-pane agent entries can carry a working dir.
|
|
404
|
+
// @agent_status_text/@agent_display_name ride the same caveat: the contract
|
|
405
|
+
// (skill/SKILL.md) says plain text; a stamped tab would shift this one
|
|
406
|
+
// pane's fields (sanitizeAgentText strips control chars AFTER the split).
|
|
407
|
+
`#{session_name}\t#{pane_id}\t#{pane_pid}\t#{pane_current_command}\t#{@agent_state}\t#{@agent_hint}\t#{@agent_session_id}\t#{@agent_status_text}\t#{@agent_display_name}\t#{${SIDEBAR_PANE_OPTION}}\t#{window_index}\t#{window_name}\t#{window_active}\t#{pane_current_path}\t#{pane_title}`,
|
|
249
408
|
]);
|
|
250
409
|
const bySession = new Map<string, PaneRecord[]>();
|
|
251
410
|
for (const line of raw.split("\n").filter(Boolean)) {
|
|
@@ -256,10 +415,14 @@ function collectPanes(): Map<string, PaneRecord[]> {
|
|
|
256
415
|
cmd = "",
|
|
257
416
|
authority = "",
|
|
258
417
|
hint = "",
|
|
418
|
+
sessionId = "",
|
|
419
|
+
statusTextRaw = "",
|
|
420
|
+
displayNameRaw = "",
|
|
259
421
|
sidebar = "",
|
|
260
422
|
windowIndex = "0",
|
|
261
423
|
windowName = "",
|
|
262
424
|
windowActive = "0",
|
|
425
|
+
dir = "",
|
|
263
426
|
...titleParts
|
|
264
427
|
] = line.split("\t");
|
|
265
428
|
if (!session) continue;
|
|
@@ -270,10 +433,14 @@ function collectPanes(): Map<string, PaneRecord[]> {
|
|
|
270
433
|
cmd,
|
|
271
434
|
authority,
|
|
272
435
|
hint,
|
|
436
|
+
sessionId,
|
|
437
|
+
statusTextRaw,
|
|
438
|
+
displayNameRaw,
|
|
273
439
|
sidebar: sidebar === "1",
|
|
274
440
|
windowIndex: Number(windowIndex) || 0,
|
|
275
441
|
windowName,
|
|
276
442
|
windowActive: windowActive === "1",
|
|
443
|
+
dir,
|
|
277
444
|
title: titleParts.join("\t"),
|
|
278
445
|
});
|
|
279
446
|
bySession.set(session, list);
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `wait` conditions — the SHARED implementation behind `tmux-ide wait
|
|
3
|
+
* agent-status` / `tmux-ide wait output` (bin/cli.ts) and the control
|
|
4
|
+
* socket's `wait` verb (src/control/). Extracted from the CLI case so the
|
|
5
|
+
* socket is a transport over the same logic, not a second implementation.
|
|
6
|
+
*
|
|
7
|
+
* Both loops are deps-injected (fleet lister / pane capture, clock, sleep)
|
|
8
|
+
* so the polling logic unit-tests without tmux; the exported defaults wire
|
|
9
|
+
* the real io.
|
|
10
|
+
*/
|
|
11
|
+
import { capturePane } from "@tmux-ide/tmux-bridge";
|
|
12
|
+
import type { AgentStatus, StatusTracker } from "../detect/classify.ts";
|
|
13
|
+
import { createStatusTracker } from "../detect/classify.ts";
|
|
14
|
+
import { findSessionStatus } from "./report.ts";
|
|
15
|
+
import { listTeamSessions, type TeamSession } from "./sessions.ts";
|
|
16
|
+
|
|
17
|
+
/** Default overall timeout (matches the CLI's historical default). */
|
|
18
|
+
export const WAIT_DEFAULT_TIMEOUT_MS = 60_000;
|
|
19
|
+
/** Poll cadence for the agent-status wait. */
|
|
20
|
+
export const WAIT_STATUS_POLL_MS = 750;
|
|
21
|
+
/** Poll cadence for the output-match wait. */
|
|
22
|
+
export const WAIT_OUTPUT_POLL_MS = 500;
|
|
23
|
+
|
|
24
|
+
const sleepMs = (ms: number) => new Promise<void>((r) => setTimeout(r, ms));
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* PURE — test `text` (a pane capture) against `pattern`, line by line, and
|
|
28
|
+
* report the first matching LINE; falls back to whole-text matching (with the
|
|
29
|
+
* last line reported) so multi-line patterns still hit. A fresh RegExp per
|
|
30
|
+
* test so a user-supplied /g flag can't carry `lastIndex` between calls.
|
|
31
|
+
* Returns null when nothing matches.
|
|
32
|
+
*/
|
|
33
|
+
export function matchOutput(text: string, pattern: string): string | null {
|
|
34
|
+
const lines = text.split("\n");
|
|
35
|
+
for (const line of lines) {
|
|
36
|
+
if (new RegExp(pattern).test(line)) return line;
|
|
37
|
+
}
|
|
38
|
+
if (new RegExp(pattern).test(text)) return lines[lines.length - 1] ?? "";
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface WaitAgentStatusResult {
|
|
43
|
+
ok: boolean;
|
|
44
|
+
session: string;
|
|
45
|
+
want: AgentStatus;
|
|
46
|
+
/** The session's last observed status (null = session absent). */
|
|
47
|
+
status: AgentStatus | null;
|
|
48
|
+
/** Set when `ok` is false: how long we waited. */
|
|
49
|
+
timedOutAfterMs?: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface WaitAgentStatusOpts {
|
|
53
|
+
timeoutMs?: number;
|
|
54
|
+
pollMs?: number;
|
|
55
|
+
/**
|
|
56
|
+
* The tracker threaded across polls (one PERSISTS per wait so the
|
|
57
|
+
* cross-tick working→idle `done` transition can be observed). Injected by
|
|
58
|
+
* tests; defaults to a fresh tracker.
|
|
59
|
+
*/
|
|
60
|
+
tracker?: StatusTracker;
|
|
61
|
+
listSessions?: (tracker: StatusTracker) => TeamSession[];
|
|
62
|
+
now?: () => number;
|
|
63
|
+
sleep?: (ms: number) => Promise<void>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Block until `session` reaches `want`, or time out. Never throws. */
|
|
67
|
+
export async function waitForAgentStatus(
|
|
68
|
+
session: string,
|
|
69
|
+
want: AgentStatus,
|
|
70
|
+
opts: WaitAgentStatusOpts = {},
|
|
71
|
+
): Promise<WaitAgentStatusResult> {
|
|
72
|
+
const timeoutMs = opts.timeoutMs ?? WAIT_DEFAULT_TIMEOUT_MS;
|
|
73
|
+
const pollMs = opts.pollMs ?? WAIT_STATUS_POLL_MS;
|
|
74
|
+
const tracker = opts.tracker ?? createStatusTracker();
|
|
75
|
+
const list = opts.listSessions ?? listTeamSessions;
|
|
76
|
+
const now = opts.now ?? Date.now;
|
|
77
|
+
const sleep = opts.sleep ?? sleepMs;
|
|
78
|
+
const started = now();
|
|
79
|
+
|
|
80
|
+
for (;;) {
|
|
81
|
+
const status = findSessionStatus(list(tracker), session);
|
|
82
|
+
if (status === want) return { ok: true, session, want, status };
|
|
83
|
+
if (now() - started >= timeoutMs) {
|
|
84
|
+
return { ok: false, session, want, status, timedOutAfterMs: timeoutMs };
|
|
85
|
+
}
|
|
86
|
+
await sleep(pollMs);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface WaitOutputResult {
|
|
91
|
+
ok: boolean;
|
|
92
|
+
target: string;
|
|
93
|
+
pattern: string;
|
|
94
|
+
/** The matching line when `ok`; null on timeout. */
|
|
95
|
+
matched: string | null;
|
|
96
|
+
timedOutAfterMs?: number;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface WaitOutputOpts {
|
|
100
|
+
timeoutMs?: number;
|
|
101
|
+
pollMs?: number;
|
|
102
|
+
/** Pane capture (defaults to tmux-bridge's `capturePane`, 200 lines). */
|
|
103
|
+
capture?: (target: string) => string;
|
|
104
|
+
now?: () => number;
|
|
105
|
+
sleep?: (ms: number) => Promise<void>;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Block until `target`'s captured output matches `pattern`, or time out.
|
|
110
|
+
* A capture failure (pane/session not (yet) available) keeps polling until
|
|
111
|
+
* the timeout. Throws only on an invalid regex — validate before looping.
|
|
112
|
+
*/
|
|
113
|
+
export async function waitForOutputMatch(
|
|
114
|
+
target: string,
|
|
115
|
+
pattern: string,
|
|
116
|
+
opts: WaitOutputOpts = {},
|
|
117
|
+
): Promise<WaitOutputResult> {
|
|
118
|
+
new RegExp(pattern); // an invalid pattern is a usage error, surfaced up front
|
|
119
|
+
const timeoutMs = opts.timeoutMs ?? WAIT_DEFAULT_TIMEOUT_MS;
|
|
120
|
+
const pollMs = opts.pollMs ?? WAIT_OUTPUT_POLL_MS;
|
|
121
|
+
const capture = opts.capture ?? defaultCapture;
|
|
122
|
+
const now = opts.now ?? Date.now;
|
|
123
|
+
const sleep = opts.sleep ?? sleepMs;
|
|
124
|
+
const started = now();
|
|
125
|
+
|
|
126
|
+
for (;;) {
|
|
127
|
+
let text = "";
|
|
128
|
+
try {
|
|
129
|
+
text = capture(target);
|
|
130
|
+
} catch {
|
|
131
|
+
// not capturable yet — keep polling
|
|
132
|
+
}
|
|
133
|
+
const matched = matchOutput(text, pattern);
|
|
134
|
+
if (matched !== null) return { ok: true, target, pattern, matched };
|
|
135
|
+
if (now() - started >= timeoutMs) {
|
|
136
|
+
return { ok: false, target, pattern, matched: null, timedOutAfterMs: timeoutMs };
|
|
137
|
+
}
|
|
138
|
+
await sleep(pollMs);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function defaultCapture(target: string): string {
|
|
143
|
+
return capturePane(target, { lines: 200 });
|
|
144
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/** Build the universal, appearance-aware native macOS notification helper. */
|
|
3
|
+
import { execFileSync } from "node:child_process";
|
|
4
|
+
import {
|
|
5
|
+
chmodSync,
|
|
6
|
+
cpSync,
|
|
7
|
+
mkdtempSync,
|
|
8
|
+
mkdirSync,
|
|
9
|
+
readFileSync,
|
|
10
|
+
rmSync,
|
|
11
|
+
writeFileSync,
|
|
12
|
+
} from "node:fs";
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
14
|
+
import { dirname, join, resolve } from "node:path";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
16
|
+
|
|
17
|
+
if (process.platform !== "darwin") {
|
|
18
|
+
throw new Error("build-macos-notifier must run on macOS with Xcode 26 or newer");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
22
|
+
const root = resolve(here, "..");
|
|
23
|
+
const sourceDir = join(root, "native", "macos", "notifier");
|
|
24
|
+
const outputFlag = process.argv.indexOf("--output");
|
|
25
|
+
const versionFlag = process.argv.indexOf("--version");
|
|
26
|
+
const appPath = resolve(
|
|
27
|
+
outputFlag === -1
|
|
28
|
+
? join(root, "packages", "daemon", "dist", "native", "TmuxIdeNotifier.app")
|
|
29
|
+
: process.argv[outputFlag + 1],
|
|
30
|
+
);
|
|
31
|
+
const packageJson = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
|
|
32
|
+
const version = String(
|
|
33
|
+
versionFlag === -1 ? packageJson.version || "1.0.0" : process.argv[versionFlag + 1],
|
|
34
|
+
);
|
|
35
|
+
const buildVersion = /^\d+(?:\.\d+){0,2}/.exec(version)?.[0] ?? "1";
|
|
36
|
+
const signingIdentity = process.env.TMUX_IDE_CODESIGN_IDENTITY?.trim() || "-";
|
|
37
|
+
const scratch = mkdtempSync(join(tmpdir(), "tmux-ide-notifier-"));
|
|
38
|
+
|
|
39
|
+
function run(command, args, options = {}) {
|
|
40
|
+
execFileSync(command, args, { stdio: "inherit", ...options });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function output(command, args) {
|
|
44
|
+
return execFileSync(command, args, { encoding: "utf8" }).trim();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const contents = join(appPath, "Contents");
|
|
49
|
+
const macosDir = join(contents, "MacOS");
|
|
50
|
+
const resourcesDir = join(contents, "Resources");
|
|
51
|
+
const stagedIcon = join(scratch, "AppIcon.icon");
|
|
52
|
+
const armBinary = join(scratch, "tmux-ide-notifier-arm64");
|
|
53
|
+
const x64Binary = join(scratch, "tmux-ide-notifier-x86_64");
|
|
54
|
+
const universalBinary = join(macosDir, "tmux-ide-notifier");
|
|
55
|
+
|
|
56
|
+
rmSync(appPath, { recursive: true, force: true });
|
|
57
|
+
mkdirSync(macosDir, { recursive: true });
|
|
58
|
+
mkdirSync(resourcesDir, { recursive: true });
|
|
59
|
+
cpSync(join(sourceDir, "AppIcon.icon"), stagedIcon, { recursive: true });
|
|
60
|
+
|
|
61
|
+
const swiftArgs = (target, output) => [
|
|
62
|
+
"swiftc",
|
|
63
|
+
"-O",
|
|
64
|
+
"-parse-as-library",
|
|
65
|
+
"-swift-version",
|
|
66
|
+
"5",
|
|
67
|
+
"-target",
|
|
68
|
+
target,
|
|
69
|
+
"-framework",
|
|
70
|
+
"AppKit",
|
|
71
|
+
"-framework",
|
|
72
|
+
"UserNotifications",
|
|
73
|
+
"-framework",
|
|
74
|
+
"Security",
|
|
75
|
+
join(sourceDir, "TmuxIdeNotifier.swift"),
|
|
76
|
+
"-o",
|
|
77
|
+
output,
|
|
78
|
+
];
|
|
79
|
+
run("xcrun", swiftArgs("arm64-apple-macos11.0", armBinary));
|
|
80
|
+
run("xcrun", swiftArgs("x86_64-apple-macos11.0", x64Binary));
|
|
81
|
+
run("xcrun", ["lipo", "-create", armBinary, x64Binary, "-output", universalBinary]);
|
|
82
|
+
chmodSync(universalBinary, 0o755);
|
|
83
|
+
|
|
84
|
+
run("xcrun", [
|
|
85
|
+
"actool",
|
|
86
|
+
stagedIcon,
|
|
87
|
+
"--compile",
|
|
88
|
+
resourcesDir,
|
|
89
|
+
"--platform",
|
|
90
|
+
"macosx",
|
|
91
|
+
"--minimum-deployment-target",
|
|
92
|
+
"11.0",
|
|
93
|
+
"--target-device",
|
|
94
|
+
"mac",
|
|
95
|
+
"--app-icon",
|
|
96
|
+
"AppIcon",
|
|
97
|
+
"--standalone-icon-behavior",
|
|
98
|
+
"all",
|
|
99
|
+
"--output-partial-info-plist",
|
|
100
|
+
join(scratch, "icon-partial.plist"),
|
|
101
|
+
"--warnings",
|
|
102
|
+
"--notices",
|
|
103
|
+
"--errors",
|
|
104
|
+
"--output-format",
|
|
105
|
+
"human-readable-text",
|
|
106
|
+
]);
|
|
107
|
+
|
|
108
|
+
const plist = readFileSync(join(sourceDir, "Info.plist"), "utf8")
|
|
109
|
+
// Apple bundle versions are numeric even when the npm release carries a
|
|
110
|
+
// prerelease suffix (for example 2.8.0-beta.1).
|
|
111
|
+
.replaceAll("__VERSION__", buildVersion)
|
|
112
|
+
.replaceAll("__BUILD_VERSION__", buildVersion);
|
|
113
|
+
writeFileSync(join(contents, "Info.plist"), plist);
|
|
114
|
+
writeFileSync(join(contents, "PkgInfo"), "APPL????");
|
|
115
|
+
|
|
116
|
+
// Portable release artifacts use an ad-hoc signature plus the helper's
|
|
117
|
+
// native compatibility delivery. A release environment can provide a
|
|
118
|
+
// Developer ID identity as part of a signed + notarized distribution; that
|
|
119
|
+
// makes the modern UNUserNotificationCenter path eligible at runtime.
|
|
120
|
+
const signArgs =
|
|
121
|
+
signingIdentity === "-"
|
|
122
|
+
? ["--force", "--deep", "--sign", "-", "--timestamp=none", appPath]
|
|
123
|
+
: [
|
|
124
|
+
"--force",
|
|
125
|
+
"--deep",
|
|
126
|
+
"--options",
|
|
127
|
+
"runtime",
|
|
128
|
+
"--sign",
|
|
129
|
+
signingIdentity,
|
|
130
|
+
"--timestamp",
|
|
131
|
+
appPath,
|
|
132
|
+
];
|
|
133
|
+
run("codesign", signArgs);
|
|
134
|
+
run("codesign", ["--verify", "--deep", "--strict", appPath]);
|
|
135
|
+
|
|
136
|
+
const architectures = new Set(output("xcrun", ["lipo", "-archs", universalBinary]).split(/\s+/));
|
|
137
|
+
for (const architecture of ["arm64", "x86_64"]) {
|
|
138
|
+
if (!architectures.has(architecture)) {
|
|
139
|
+
throw new Error(`native notifier is missing ${architecture}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const assetInfo = JSON.parse(
|
|
144
|
+
output("xcrun", ["assetutil", "--info", join(resourcesDir, "Assets.car")]),
|
|
145
|
+
);
|
|
146
|
+
const iconAppearances = new Set(
|
|
147
|
+
assetInfo.filter((entry) => entry.AssetType === "IconGroup").map((entry) => entry.Appearance),
|
|
148
|
+
);
|
|
149
|
+
for (const appearance of ["NSAppearanceNameAqua", "NSAppearanceNameDarkAqua"]) {
|
|
150
|
+
if (!iconAppearances.has(appearance)) {
|
|
151
|
+
throw new Error(`native notifier icon is missing ${appearance}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
console.log(
|
|
156
|
+
`[build-macos-notifier] wrote ${appPath} (${[...architectures].join("+")}; Aqua+DarkAqua)`,
|
|
157
|
+
);
|
|
158
|
+
} finally {
|
|
159
|
+
rmSync(scratch, { recursive: true, force: true });
|
|
160
|
+
}
|
package/scripts/build-tui.mjs
CHANGED
|
@@ -16,7 +16,9 @@
|
|
|
16
16
|
* node-only for CI) — run `pnpm build:tui` on a machine with bun.
|
|
17
17
|
*
|
|
18
18
|
* Cross-compile: pass `--target bun-<os>-<arch>` (e.g. bun-linux-x64) to build
|
|
19
|
-
* for another platform. Defaults to the host target.
|
|
19
|
+
* for another platform. Defaults to the host target. Pass `--outfile <path>` to
|
|
20
|
+
* write somewhere other than the default dist path (the release workflow uses
|
|
21
|
+
* this to emit per-platform artifacts side by side).
|
|
20
22
|
*/
|
|
21
23
|
|
|
22
24
|
import { createSolidTransformPlugin } from "@opentui/solid/bun-plugin";
|
|
@@ -27,8 +29,7 @@ import { fileURLToPath } from "node:url";
|
|
|
27
29
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
28
30
|
const repoRoot = resolve(here, "..");
|
|
29
31
|
const entry = resolve(repoRoot, "packages/daemon/src/tui/main.ts");
|
|
30
|
-
const
|
|
31
|
-
const outfile = resolve(outDir, "tmux-ide-tui");
|
|
32
|
+
const defaultOutDir = resolve(repoRoot, "packages/daemon/dist/tui");
|
|
32
33
|
|
|
33
34
|
if (!existsSync(entry)) {
|
|
34
35
|
throw new Error(`[build-tui] entry not found: ${entry}`);
|
|
@@ -38,7 +39,13 @@ const targetArg = process.argv.indexOf("--target");
|
|
|
38
39
|
const target =
|
|
39
40
|
targetArg !== -1 ? process.argv[targetArg + 1] : `bun-${process.platform}-${process.arch}`;
|
|
40
41
|
|
|
41
|
-
|
|
42
|
+
const outfileArg = process.argv.indexOf("--outfile");
|
|
43
|
+
const outfile =
|
|
44
|
+
outfileArg !== -1
|
|
45
|
+
? resolve(process.argv[outfileArg + 1])
|
|
46
|
+
: resolve(defaultOutDir, "tmux-ide-tui");
|
|
47
|
+
|
|
48
|
+
mkdirSync(dirname(outfile), { recursive: true });
|
|
42
49
|
|
|
43
50
|
const start = Date.now();
|
|
44
51
|
const result = await Bun.build({
|