tmux-ide 2.7.0 → 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 +22 -5
- package/bin/cli.js +3532 -1090
- package/bin/cli.ts +368 -71
- package/package.json +2 -1
- 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/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/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/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 +103 -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/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 +582 -84
- package/packages/daemon/src/tui/chrome/updater.ts +268 -62
- package/packages/daemon/src/tui/detect/classify.ts +34 -0
- package/packages/daemon/src/tui/detect/manifest-loader.ts +54 -5
- package/packages/daemon/src/tui/detect/manifest.ts +24 -3
- package/packages/daemon/src/tui/detect/manifests.ts +240 -6
- package/packages/daemon/src/tui/detect/process-tree.ts +13 -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/mirror/agent-chip.ts +40 -11
- package/packages/daemon/src/tui/mirror/agent-lifecycle.ts +437 -0
- package/packages/daemon/src/tui/mirror/agent-rows.ts +27 -5
- package/packages/daemon/src/tui/mirror/app-state.ts +171 -8
- package/packages/daemon/src/tui/mirror/app.tsx +2182 -399
- package/packages/daemon/src/tui/mirror/attention.ts +110 -0
- package/packages/daemon/src/tui/mirror/dialog-stack.ts +17 -4
- package/packages/daemon/src/tui/mirror/diff-model.ts +279 -4
- package/packages/daemon/src/tui/mirror/file-tree.ts +231 -6
- 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/layout-parse.ts +154 -0
- package/packages/daemon/src/tui/mirror/menu-model.ts +27 -4
- package/packages/daemon/src/tui/mirror/palette.ts +299 -9
- package/packages/daemon/src/tui/mirror/pane-mirror.ts +82 -4
- package/packages/daemon/src/tui/mirror/pane-surface.tsx +18 -11
- package/packages/daemon/src/tui/mirror/perf-tap.ts +29 -3
- package/packages/daemon/src/tui/mirror/selection.ts +122 -8
- package/packages/daemon/src/tui/mirror/session-mirror.ts +349 -68
- package/packages/daemon/src/tui/mirror/settings-model.ts +96 -16
- package/packages/daemon/src/tui/mirror/sidebar.tsx +218 -0
- package/packages/daemon/src/tui/mirror/size-truth.ts +53 -0
- package/packages/daemon/src/tui/mirror/theme.ts +45 -0
- package/packages/daemon/src/tui/team/fuzzy.ts +20 -0
- package/packages/daemon/src/tui/team/sessions.ts +85 -7
- package/packages/daemon/src/tui/team/wait.ts +144 -0
- package/scripts/build-macos-notifier.mjs +160 -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 +88 -2
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
classifyInstant,
|
|
16
16
|
parseAuthority,
|
|
17
17
|
parseAuthorityEpoch,
|
|
18
|
+
sanitizeAgentText,
|
|
18
19
|
type AgentStatus,
|
|
19
20
|
type StatusTracker,
|
|
20
21
|
} from "../detect/classify.ts";
|
|
@@ -78,6 +79,19 @@ export interface PaneAgentEntry {
|
|
|
78
79
|
command: string;
|
|
79
80
|
/** `pane_current_path` — the pane's working directory. */
|
|
80
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;
|
|
81
95
|
}
|
|
82
96
|
|
|
83
97
|
/**
|
|
@@ -93,6 +107,9 @@ export function buildAgentEntry(input: {
|
|
|
93
107
|
manifest: AgentManifest | undefined;
|
|
94
108
|
state: AgentStatus;
|
|
95
109
|
since: number | null;
|
|
110
|
+
/** Already-sanitized display metadata (undefined when absent/stale). */
|
|
111
|
+
statusText?: string;
|
|
112
|
+
displayName?: string;
|
|
96
113
|
}): PaneAgentEntry | null {
|
|
97
114
|
const { manifest, pane } = input;
|
|
98
115
|
if (!manifest || manifest.id === "shell") return null;
|
|
@@ -107,6 +124,29 @@ export function buildAgentEntry(input: {
|
|
|
107
124
|
title: pane.title,
|
|
108
125
|
command: pane.cmd,
|
|
109
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 } : {}),
|
|
110
150
|
};
|
|
111
151
|
}
|
|
112
152
|
|
|
@@ -138,6 +178,15 @@ export interface PaneDetail {
|
|
|
138
178
|
paneId: string;
|
|
139
179
|
agent: string | null;
|
|
140
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;
|
|
141
190
|
}
|
|
142
191
|
|
|
143
192
|
interface PaneRecord {
|
|
@@ -155,6 +204,12 @@ interface PaneRecord {
|
|
|
155
204
|
authority: string;
|
|
156
205
|
/** Raw `@agent_hint` pane option — forces a manifest when set. */
|
|
157
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;
|
|
158
213
|
/** `window_index` — the window (tab) this pane lives in. */
|
|
159
214
|
windowIndex: number;
|
|
160
215
|
/** `window_name`. */
|
|
@@ -182,12 +237,13 @@ export function excludeSidebarPanes<T extends { sidebar: boolean }>(panes: T[]):
|
|
|
182
237
|
const SEVERITY: AgentStatus[] = ["blocked", "working", "done", "idle", "unknown"];
|
|
183
238
|
|
|
184
239
|
/**
|
|
185
|
-
* Whether a session should appear in the switcher.
|
|
186
|
-
* internal plumbing (the `_tmux-ide-chrome` updater,
|
|
187
|
-
*
|
|
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.
|
|
188
244
|
*/
|
|
189
245
|
export function isListableSession(name: string): boolean {
|
|
190
|
-
return !name.startsWith("_");
|
|
246
|
+
return !name.startsWith("_") && !name.startsWith("zz-");
|
|
191
247
|
}
|
|
192
248
|
|
|
193
249
|
function tmux(args: string[]): string {
|
|
@@ -300,10 +356,23 @@ export function listTeamSessions(
|
|
|
300
356
|
paneId: pane.id,
|
|
301
357
|
agent: manifest && manifest.id !== "shell" ? manifest.id : null,
|
|
302
358
|
status,
|
|
359
|
+
windowIndex: pane.windowIndex,
|
|
360
|
+
pid: pane.pid,
|
|
361
|
+
dir: pane.dir,
|
|
362
|
+
sessionId: pane.sessionId.length > 0 ? pane.sessionId : null,
|
|
303
363
|
});
|
|
304
364
|
// Surface per-pane agent detail (same resolved manifest/status — nothing
|
|
305
|
-
// re-derived). Non-agent panes yield null and are skipped.
|
|
306
|
-
|
|
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),
|
|
375
|
+
});
|
|
307
376
|
if (entry) agents.push(entry);
|
|
308
377
|
return status;
|
|
309
378
|
});
|
|
@@ -332,7 +401,10 @@ function collectPanes(): Map<string, PaneRecord[]> {
|
|
|
332
401
|
// title stays the trailing catch-all — window names/paths don't contain tabs
|
|
333
402
|
// in practice. pane_current_path rides this SAME list-panes call (no extra
|
|
334
403
|
// tmux round-trip) so per-pane agent entries can carry a working dir.
|
|
335
|
-
|
|
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}`,
|
|
336
408
|
]);
|
|
337
409
|
const bySession = new Map<string, PaneRecord[]>();
|
|
338
410
|
for (const line of raw.split("\n").filter(Boolean)) {
|
|
@@ -343,6 +415,9 @@ function collectPanes(): Map<string, PaneRecord[]> {
|
|
|
343
415
|
cmd = "",
|
|
344
416
|
authority = "",
|
|
345
417
|
hint = "",
|
|
418
|
+
sessionId = "",
|
|
419
|
+
statusTextRaw = "",
|
|
420
|
+
displayNameRaw = "",
|
|
346
421
|
sidebar = "",
|
|
347
422
|
windowIndex = "0",
|
|
348
423
|
windowName = "",
|
|
@@ -358,6 +433,9 @@ function collectPanes(): Map<string, PaneRecord[]> {
|
|
|
358
433
|
cmd,
|
|
359
434
|
authority,
|
|
360
435
|
hint,
|
|
436
|
+
sessionId,
|
|
437
|
+
statusTextRaw,
|
|
438
|
+
displayNameRaw,
|
|
361
439
|
sidebar: sidebar === "1",
|
|
362
440
|
windowIndex: Number(windowIndex) || 0,
|
|
363
441
|
windowName,
|
|
@@ -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/postinstall.js
CHANGED
|
@@ -91,7 +91,14 @@ const nextSettings = {
|
|
|
91
91
|
},
|
|
92
92
|
};
|
|
93
93
|
|
|
94
|
-
|
|
94
|
+
try {
|
|
95
|
+
writeFileSync(settingsPath, `${JSON.stringify(nextSettings, null, 2)}\n`);
|
|
96
|
+
} catch (error) {
|
|
97
|
+
// Best-effort like everything above: sandboxed installs (e.g. a package
|
|
98
|
+
// manager building in a $HOME-restricted sandbox) may deny this write —
|
|
99
|
+
// that must never fail the install itself.
|
|
100
|
+
console.warn(`[tmux-ide] Skipping Claude settings update: ${error.message}`);
|
|
101
|
+
}
|
|
95
102
|
|
|
96
103
|
function shouldInstallClaudeIntegration() {
|
|
97
104
|
return process.env.npm_config_global === "true";
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { execFileSync } from "node:child_process";
|
|
13
|
-
import { existsSync, statSync } from "node:fs";
|
|
13
|
+
import { accessSync, constants, existsSync, statSync } from "node:fs";
|
|
14
14
|
import { join } from "node:path";
|
|
15
15
|
|
|
16
16
|
function run(command, args) {
|
|
@@ -42,3 +42,39 @@ if (cliJsMtime < cliTsMtime) {
|
|
|
42
42
|
"bin/cli.js is older than bin/cli.ts — run: pnpm build:cli && git add bin/cli.js",
|
|
43
43
|
);
|
|
44
44
|
}
|
|
45
|
+
|
|
46
|
+
// ---------------------------------------------------------------
|
|
47
|
+
// Native macOS notification sender — release.yml builds this on a macOS 26
|
|
48
|
+
// runner and injects it into packages/daemon/dist before npm publish. npm and
|
|
49
|
+
// Homebrew consume the same tarball, so a missing bundle would silently put
|
|
50
|
+
// users back on an unbranded AppleScript fallback.
|
|
51
|
+
// ---------------------------------------------------------------
|
|
52
|
+
const notifierRoot = join(
|
|
53
|
+
process.cwd(),
|
|
54
|
+
"packages",
|
|
55
|
+
"daemon",
|
|
56
|
+
"dist",
|
|
57
|
+
"native",
|
|
58
|
+
"TmuxIdeNotifier.app",
|
|
59
|
+
"Contents",
|
|
60
|
+
);
|
|
61
|
+
for (const relative of [
|
|
62
|
+
"Info.plist",
|
|
63
|
+
join("Resources", "Assets.car"),
|
|
64
|
+
join("Resources", "AppIcon.icns"),
|
|
65
|
+
]) {
|
|
66
|
+
const path = join(notifierRoot, relative);
|
|
67
|
+
if (!existsSync(path)) {
|
|
68
|
+
throw new Error(
|
|
69
|
+
`native macOS notifier is incomplete (${relative} missing) — run pnpm build:macos-notifier on macOS`,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const notifierExecutable = join(notifierRoot, "MacOS", "tmux-ide-notifier");
|
|
74
|
+
try {
|
|
75
|
+
accessSync(notifierExecutable, constants.X_OK);
|
|
76
|
+
} catch {
|
|
77
|
+
throw new Error(
|
|
78
|
+
"native macOS notifier executable is missing or not executable — run pnpm build:macos-notifier on macOS",
|
|
79
|
+
);
|
|
80
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Publish packaging/homebrew/Formula/tmux-ide.rb to the Homebrew tap repo
|
|
3
|
+
# (wavyrai/homebrew-tap), i.e. the one-time seeding the CI `bump_tap` job
|
|
4
|
+
# assumes. After this, every release keeps the tap current automatically
|
|
5
|
+
# (given the TAP_PUSH_TOKEN secret — see .github/workflows/release.yml).
|
|
6
|
+
#
|
|
7
|
+
# Usage:
|
|
8
|
+
# scripts/publish-tap.sh [path-to-tap-checkout]
|
|
9
|
+
#
|
|
10
|
+
# With no argument, clones git@github.com:wavyrai/homebrew-tap.git into a
|
|
11
|
+
# temp dir. The tap repo must already exist on GitHub (create it empty —
|
|
12
|
+
# the name MUST be exactly `homebrew-tap` for `brew install
|
|
13
|
+
# wavyrai/tap/tmux-ide` to resolve).
|
|
14
|
+
set -euo pipefail
|
|
15
|
+
|
|
16
|
+
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
17
|
+
formula="$repo_root/packaging/homebrew/Formula/tmux-ide.rb"
|
|
18
|
+
[[ -f "$formula" ]] || {
|
|
19
|
+
echo "formula not found: $formula" >&2
|
|
20
|
+
exit 1
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
tap_dir="${1:-}"
|
|
24
|
+
cleanup=""
|
|
25
|
+
if [[ -z "$tap_dir" ]]; then
|
|
26
|
+
tap_dir="$(mktemp -d)/homebrew-tap"
|
|
27
|
+
cleanup="$(dirname "$tap_dir")"
|
|
28
|
+
git clone git@github.com:wavyrai/homebrew-tap.git "$tap_dir"
|
|
29
|
+
fi
|
|
30
|
+
[[ -d "$tap_dir/.git" ]] || {
|
|
31
|
+
echo "not a git checkout: $tap_dir" >&2
|
|
32
|
+
exit 1
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
version="$(sed -nE 's#^ url ".*/tmux-ide-([0-9][^"]*)\.tgz"$#\1#p' "$formula")"
|
|
36
|
+
[[ -n "$version" ]] || {
|
|
37
|
+
echo "could not read the version from the formula url" >&2
|
|
38
|
+
exit 1
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
mkdir -p "$tap_dir/Formula"
|
|
42
|
+
cp "$formula" "$tap_dir/Formula/tmux-ide.rb"
|
|
43
|
+
|
|
44
|
+
cd "$tap_dir"
|
|
45
|
+
git add Formula/tmux-ide.rb
|
|
46
|
+
if git diff --cached --quiet; then
|
|
47
|
+
echo "tap already has this formula version (v$version) — nothing to push."
|
|
48
|
+
else
|
|
49
|
+
git commit -m "tmux-ide $version"
|
|
50
|
+
git push origin HEAD
|
|
51
|
+
echo "pushed tmux-ide v$version to $(git remote get-url origin)"
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
[[ -n "$cleanup" ]] && rm -rf "$cleanup"
|
|
55
|
+
echo "install with: brew install wavyrai/tap/tmux-ide"
|