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
package/bin/cli.ts
CHANGED
|
@@ -9,8 +9,17 @@ import { existsSync } from "node:fs";
|
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
10
|
|
|
11
11
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
// The node-runnable CLI path that spawned TUI surfaces shell back to (the app's
|
|
13
|
+
// async fleet poll + `detect --write`). When we're the published `bin/cli.js`
|
|
14
|
+
// this file IS it; under dev (`bun bin/cli.ts`) it's the built sibling
|
|
15
|
+
// `bin/cli.js`. Forwarded to surfaces as TMUX_IDE_CLI so the COMPILED TUI binary
|
|
16
|
+
// — whose own `import.meta.url` is a virtual bunfs path with no real cli.js next
|
|
17
|
+
// to it — can still find the CLI to run its subprocesses.
|
|
18
|
+
const selfPath = fileURLToPath(import.meta.url);
|
|
19
|
+
const nodeCliPath = selfPath.endsWith(".js") ? selfPath : resolve(__dirname, "cli.js");
|
|
12
20
|
import { launch } from "../packages/daemon/src/launch.ts";
|
|
13
|
-
import {
|
|
21
|
+
import { resolveEntry } from "../packages/daemon/src/tui/team/entry.ts";
|
|
22
|
+
import { loadAppConfig } from "../packages/daemon/src/lib/app-config.ts";
|
|
14
23
|
import {
|
|
15
24
|
resolveTuiLaunch,
|
|
16
25
|
findCompiledTui,
|
|
@@ -31,6 +40,16 @@ import { restore } from "../packages/daemon/src/restore.ts";
|
|
|
31
40
|
import { send } from "../packages/daemon/src/send.ts";
|
|
32
41
|
import { IdeError } from "../packages/daemon/src/lib/errors.ts";
|
|
33
42
|
import { printCommandError } from "../packages/daemon/src/lib/output.ts";
|
|
43
|
+
import {
|
|
44
|
+
wantsHostedApp,
|
|
45
|
+
hostedEnvVars,
|
|
46
|
+
hostedCommandLine,
|
|
47
|
+
hostExistsArgv,
|
|
48
|
+
hostCreateArgv,
|
|
49
|
+
hostSetupArgvs,
|
|
50
|
+
hostAttachArgv,
|
|
51
|
+
HOSTED_ENV,
|
|
52
|
+
} from "../packages/daemon/src/tui/mirror/hosted.ts";
|
|
34
53
|
|
|
35
54
|
const { positionals, values } = parseArgs({
|
|
36
55
|
allowPositionals: true,
|
|
@@ -83,6 +102,10 @@ const { positionals, values } = parseArgs({
|
|
|
83
102
|
// actions menu opens at the pointer instead of centered (see `menu` case)
|
|
84
103
|
x: { type: "string" },
|
|
85
104
|
y: { type: "string" },
|
|
105
|
+
// app: host the cockpit in the internal `_tmux-ide-app` session and attach
|
|
106
|
+
// to it (M23.2) — `--detachable` is the primary name, `--hosted` the alias
|
|
107
|
+
detachable: { type: "boolean" },
|
|
108
|
+
hosted: { type: "boolean" },
|
|
86
109
|
// worktree: base ref for a new branch, the worktree checkout dir override,
|
|
87
110
|
// skip creating a session, and force-remove a dirty worktree (see `worktree`)
|
|
88
111
|
from: { type: "string" },
|
|
@@ -110,6 +133,7 @@ const knownCommands = new Set([
|
|
|
110
133
|
"send",
|
|
111
134
|
"settings",
|
|
112
135
|
"team",
|
|
136
|
+
"app",
|
|
113
137
|
"switcher",
|
|
114
138
|
"wait",
|
|
115
139
|
"events",
|
|
@@ -127,6 +151,7 @@ const knownCommands = new Set([
|
|
|
127
151
|
"worktree",
|
|
128
152
|
"update",
|
|
129
153
|
"skill-sync",
|
|
154
|
+
"serve",
|
|
130
155
|
"command-center",
|
|
131
156
|
"server",
|
|
132
157
|
"help",
|
|
@@ -177,12 +202,15 @@ ${bold("Usage:")}
|
|
|
177
202
|
${dim("(--resume-agents revives claude conversations via claude --resume)")}
|
|
178
203
|
${cyan("tmux-ide attach")} ${dim("Reattach to a running session")}
|
|
179
204
|
${cyan("tmux-ide team")} [--json] ${dim("TUI over all tmux sessions (--json prints fleet state)")}
|
|
205
|
+
${cyan("tmux-ide app")} [session] ${dim("Unified app: fleet home + live session mirror (bare = home)")}
|
|
206
|
+
${cyan("tmux-ide app --detachable")} ${dim("Host the app in tmux and attach — survives the terminal, ^q detaches")}
|
|
180
207
|
${cyan("tmux-ide switcher")} ${dim("Compact session picker (opens in the M-p popup on adopted sessions)")}
|
|
181
208
|
${cyan("tmux-ide wait agent-status")} <session> --status <s> [--timeout <ms>]
|
|
182
209
|
${dim("Block until a session reaches a status (exit 0 match / 1 timeout)")}
|
|
183
210
|
${cyan("tmux-ide wait output")} <pane|session> --match <regex> [--timeout <ms>]
|
|
184
211
|
${dim("Block until a pane's output matches a regex (exit 0 match / 1 timeout)")}
|
|
185
|
-
${cyan("tmux-ide events")} [--follow] [--json] ${dim("Stream agent-status transitions (
|
|
212
|
+
${cyan("tmux-ide events")} [--follow] [--json] [--socket] ${dim("Stream agent-status transitions (--socket: push from a running serve)")}
|
|
213
|
+
${cyan("tmux-ide serve")} [--socket <path>] ${dim("Local control socket: NDJSON verbs + pushed events (~/.tmux-ide/control.sock)")}
|
|
186
214
|
${cyan("tmux-ide adopt")} <session> ${dim("Add the live tmux-ide status bar to a session")}
|
|
187
215
|
${cyan("tmux-ide adopt --all")} ${dim("Adopt every live (non-internal) session")}
|
|
188
216
|
${cyan("tmux-ide unadopt")} <session> ${dim("Remove the status bar")}
|
|
@@ -202,6 +230,7 @@ ${bold("Usage:")}
|
|
|
202
230
|
${cyan("tmux-ide inspect")} [--json] ${dim("Show effective config and runtime state")}
|
|
203
231
|
${cyan("tmux-ide doctor")} ${dim("Check system requirements")}
|
|
204
232
|
${cyan("tmux-ide update")} [--dry-run] ${dim("Update tmux-ide (detects dev checkout vs npm/pnpm/bun global)")}
|
|
233
|
+
${cyan("tmux-ide update --manifests")} ${dim("Fetch the latest agent-detection manifest pack (your overrides still win)")}
|
|
205
234
|
${cyan("tmux-ide skill-sync")} ${dim("Refresh the bundled Claude Code skill in ~/.claude/skills/tmux-ide")}
|
|
206
235
|
${cyan("tmux-ide validate")} [--json] ${dim("Validate ide.yml")}
|
|
207
236
|
${cyan("tmux-ide detect")} [--json] ${dim("Detect project stack")}
|
|
@@ -269,7 +298,12 @@ function execBunWidget(
|
|
|
269
298
|
);
|
|
270
299
|
}
|
|
271
300
|
|
|
272
|
-
const env = {
|
|
301
|
+
const env = {
|
|
302
|
+
...process.env,
|
|
303
|
+
TMUX_IDE_CWD: process.cwd(),
|
|
304
|
+
TMUX_IDE_CLI: nodeCliPath,
|
|
305
|
+
...extraEnv,
|
|
306
|
+
};
|
|
273
307
|
if (launch.mode === "bun") {
|
|
274
308
|
// Spawn from the repo root so bun finds `bunfig.toml` (the @opentui/solid
|
|
275
309
|
// JSX preload). Without this, running from any other cwd — e.g. bare
|
|
@@ -290,6 +324,63 @@ function execBunWidget(
|
|
|
290
324
|
execFileSync(launch.bin, launch.argv, { stdio: "inherit", env });
|
|
291
325
|
}
|
|
292
326
|
|
|
327
|
+
// The detachable cockpit (M23.2): instead of running the app in THIS terminal,
|
|
328
|
+
// ensure the internal `_tmux-ide-app` session exists running it full-screen
|
|
329
|
+
// (status off, window-size latest — see hostSetupArgvs), then attach here.
|
|
330
|
+
// Re-invocation from any terminal reattaches the SAME cockpit; ^q inside a
|
|
331
|
+
// hosted app detaches (the HOSTED_ENV marker on the pane command flips it).
|
|
332
|
+
// Inside tmux the client switch-clients instead of nesting an attach.
|
|
333
|
+
function launchHostedApp(scriptPath: string, appArgs: string[]): void {
|
|
334
|
+
const launch = resolveTuiLaunch({
|
|
335
|
+
surface: "app",
|
|
336
|
+
scriptPath,
|
|
337
|
+
args: appArgs,
|
|
338
|
+
checkoutExists: existsSync(scriptPath),
|
|
339
|
+
bunAvailable: isBunAvailable(),
|
|
340
|
+
compiledBinary: findCompiledTui(),
|
|
341
|
+
});
|
|
342
|
+
if (launch.mode === "unavailable") {
|
|
343
|
+
throw new IdeError(
|
|
344
|
+
`\`tmux-ide app --detachable\` is unavailable because ${launch.reasons.join(" and ")}.\n` +
|
|
345
|
+
`Install bun (https://bun.sh) — the TUI surfaces run on it. Sources ship with the npm package since v2.6.1.`,
|
|
346
|
+
{ code: "USAGE", exitCode: 1 },
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
let exists = true;
|
|
351
|
+
try {
|
|
352
|
+
execFileSync("tmux", hostExistsArgv(), { stdio: "ignore" });
|
|
353
|
+
} catch {
|
|
354
|
+
exists = false; // also covers "no server yet" — new-session starts one
|
|
355
|
+
}
|
|
356
|
+
if (!exists) {
|
|
357
|
+
// Same cwd rule as execBunWidget: bun needs the repo root (bunfig preload),
|
|
358
|
+
// the compiled binary must NOT run from it. The app's real env travels on
|
|
359
|
+
// the pane command line — the tmux server's environment is not ours.
|
|
360
|
+
const cwd = launch.mode === "bun" ? resolve(__dirname, "..") : process.cwd();
|
|
361
|
+
const commandLine = hostedCommandLine(
|
|
362
|
+
launch.bin,
|
|
363
|
+
launch.argv,
|
|
364
|
+
hostedEnvVars({
|
|
365
|
+
cwd: process.cwd(),
|
|
366
|
+
cli: nodeCliPath,
|
|
367
|
+
path: process.env.PATH,
|
|
368
|
+
home: process.env.TMUX_IDE_HOME,
|
|
369
|
+
config: process.env.TMUX_IDE_CONFIG,
|
|
370
|
+
tuiBin: process.env.TMUX_IDE_TUI_BIN,
|
|
371
|
+
}),
|
|
372
|
+
);
|
|
373
|
+
execFileSync("tmux", hostCreateArgv({ cwd, commandLine }), { stdio: "ignore" });
|
|
374
|
+
}
|
|
375
|
+
// Setup runs on EVERY ensure, not just create (M25.5): the list is
|
|
376
|
+
// idempotent, so this upgrades a host created by an older tmux-ide (no
|
|
377
|
+
// resize hooks yet) and re-asserts `window-size latest` on a host a stray
|
|
378
|
+
// `resize-window` flipped to manual — the measured way a cockpit gets stuck
|
|
379
|
+
// at a departed client's size.
|
|
380
|
+
for (const args of hostSetupArgvs()) execFileSync("tmux", args, { stdio: "ignore" });
|
|
381
|
+
execFileSync("tmux", hostAttachArgv(Boolean(process.env.TMUX)), { stdio: "inherit" });
|
|
382
|
+
}
|
|
383
|
+
|
|
293
384
|
// The scriptable control surface for the cockpit: print the fleet state as JSON
|
|
294
385
|
// and exit without spawning the (bun/OpenTUI) TUI. Shared by `tmux-ide team
|
|
295
386
|
// --json` and bare `tmux-ide --json` when there's no ide.yml to launch. Dynamic
|
|
@@ -301,7 +392,43 @@ async function printFleetJson(): Promise<void> {
|
|
|
301
392
|
console.log(JSON.stringify(toFleetJson(listTeamProjects(createStatusTracker())), null, 2));
|
|
302
393
|
}
|
|
303
394
|
|
|
395
|
+
// The `--socket[=path]` opt-in (undeclared in parseArgs on purpose: bare
|
|
396
|
+
// `--socket` parses to `true`, `--socket=/path` to the path — strict:false
|
|
397
|
+
// gives optional-value semantics parseArgs can't declare). `true | string |
|
|
398
|
+
// undefined`; string overrides the default socket path.
|
|
399
|
+
const socketFlag = values.socket as string | boolean | undefined;
|
|
400
|
+
|
|
401
|
+
// Run a `wait` on a live `tmux-ide serve` (connect once, the server holds the
|
|
402
|
+
// wait — no local polling). Returns null when the flag is off, no server
|
|
403
|
+
// answers, or the connection drops mid-wait — callers fall back SILENTLY to
|
|
404
|
+
// the local polling implementation (same shared logic, tui/team/wait.ts).
|
|
405
|
+
async function waitOverSocket(
|
|
406
|
+
params: Record<string, unknown>,
|
|
407
|
+
): Promise<{ timedOut: boolean; data?: unknown } | null> {
|
|
408
|
+
if (!socketFlag) return null;
|
|
409
|
+
const { connectControl, ControlRequestError } =
|
|
410
|
+
await import("../packages/daemon/src/control/client.ts");
|
|
411
|
+
let client: Awaited<ReturnType<typeof connectControl>>;
|
|
412
|
+
try {
|
|
413
|
+
client = await connectControl({
|
|
414
|
+
socketPath: typeof socketFlag === "string" ? socketFlag : undefined,
|
|
415
|
+
});
|
|
416
|
+
} catch {
|
|
417
|
+
return null; // no server listening — poll locally instead
|
|
418
|
+
}
|
|
419
|
+
try {
|
|
420
|
+
const data = await client.request("wait", params);
|
|
421
|
+
return { timedOut: false, data };
|
|
422
|
+
} catch (err) {
|
|
423
|
+
if (err instanceof ControlRequestError && err.code === "timeout") return { timedOut: true };
|
|
424
|
+
return null; // dropped/errored mid-wait — fall back to polling
|
|
425
|
+
} finally {
|
|
426
|
+
client.close();
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
304
430
|
const teamScriptPath = resolve(__dirname, "../packages/daemon/src/tui/team/index.tsx");
|
|
431
|
+
const appScriptPath = resolve(__dirname, "../packages/daemon/src/tui/mirror/app.tsx");
|
|
305
432
|
|
|
306
433
|
// `tmux-ide team` runs the standalone full-screen cockpit (the OpenTUI app owns
|
|
307
434
|
// the whole terminal). The floating switcher popup (M-p on adopted sessions)
|
|
@@ -310,6 +437,29 @@ function launchTeamCockpit(): void {
|
|
|
310
437
|
execBunWidget("team", teamScriptPath, [], "team");
|
|
311
438
|
}
|
|
312
439
|
|
|
440
|
+
// The one entry for the unified app: `--detachable`/`--hosted` (or
|
|
441
|
+
// `app.detachable` in config) route through the hosted launcher, everything
|
|
442
|
+
// else runs the app in this terminal as before. The HOSTED_ENV guard keeps the
|
|
443
|
+
// app INSIDE the host session from re-hosting itself.
|
|
444
|
+
function runApp(appArgs: string[]): void {
|
|
445
|
+
const hosted = wantsHostedApp({
|
|
446
|
+
flagDetachable: values.detachable === true,
|
|
447
|
+
flagHosted: values.hosted === true,
|
|
448
|
+
configDetachable: loadAppConfig().app.detachable,
|
|
449
|
+
hostedEnv: process.env[HOSTED_ENV] === "1",
|
|
450
|
+
});
|
|
451
|
+
if (hosted) launchHostedApp(appScriptPath, appArgs);
|
|
452
|
+
else execBunWidget("app", appScriptPath, appArgs, "app");
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// The unified app as the front door (M22.6): bare `tmux-ide` opens `tmux-ide
|
|
456
|
+
// app`'s HOME panel when `app.frontDoor` is on and there's nothing else to
|
|
457
|
+
// launch. Same entry as the explicit `app` command with no session positional
|
|
458
|
+
// — including the hosted flip when `app.detachable` is set (M23.2).
|
|
459
|
+
function launchApp(): void {
|
|
460
|
+
runApp([]);
|
|
461
|
+
}
|
|
462
|
+
|
|
313
463
|
try {
|
|
314
464
|
switch (command) {
|
|
315
465
|
case "start": {
|
|
@@ -332,13 +482,23 @@ try {
|
|
|
332
482
|
}
|
|
333
483
|
const targetDir = resolve(startTargetDir || ".");
|
|
334
484
|
const hasIdeYml = existsSync(join(targetDir, "ide.yml"));
|
|
335
|
-
|
|
336
|
-
|
|
485
|
+
// M22.6 — the front-door decision. `--team` always means the classic
|
|
486
|
+
// cockpit; a present ide.yml still auto-launches the project; otherwise
|
|
487
|
+
// `app.frontDoor` flips the default no-project entry to the unified app.
|
|
488
|
+
const entry = resolveEntry({
|
|
489
|
+
hasIdeYml,
|
|
490
|
+
teamFlag: values.team === true,
|
|
491
|
+
frontDoor: loadAppConfig().app.frontDoor,
|
|
492
|
+
});
|
|
493
|
+
if (entry !== "project") {
|
|
494
|
+
// No project to launch here. `--json` is a scripting surface — it always
|
|
495
|
+
// prints the fleet, whichever interactive front door is configured.
|
|
337
496
|
if (json) {
|
|
338
497
|
await printFleetJson();
|
|
339
498
|
break;
|
|
340
499
|
}
|
|
341
|
-
|
|
500
|
+
if (entry === "app") launchApp();
|
|
501
|
+
else launchTeamCockpit();
|
|
342
502
|
break;
|
|
343
503
|
}
|
|
344
504
|
await launch(startTargetDir, { json });
|
|
@@ -491,6 +651,20 @@ try {
|
|
|
491
651
|
break;
|
|
492
652
|
}
|
|
493
653
|
|
|
654
|
+
case "app": {
|
|
655
|
+
// The unified app (M18.1): sidebar fleet + a live tmux-session mirror.
|
|
656
|
+
// Bare `tmux-ide app` opens the HOME panel (fleet cards); an optional
|
|
657
|
+
// session positional boots straight into that session's mirror. With
|
|
658
|
+
// `--detachable` (alias `--hosted`, or `app.detachable` in config) the
|
|
659
|
+
// app runs in the internal `_tmux-ide-app` session instead and this
|
|
660
|
+
// terminal attaches to it (M23.2) — the positional only shapes the
|
|
661
|
+
// cockpit at CREATE time; a reattach finds the app exactly as left.
|
|
662
|
+
const session = positionals[1];
|
|
663
|
+
const appArgs = session ? [`--target=${session}`] : [];
|
|
664
|
+
runApp(appArgs);
|
|
665
|
+
break;
|
|
666
|
+
}
|
|
667
|
+
|
|
494
668
|
case "switcher": {
|
|
495
669
|
// Runs the team app in PICKER mode inside a tmux `display-popup` (bound to
|
|
496
670
|
// `M-p` on adopt). Picking a session `switch-client`s the invoking client
|
|
@@ -513,7 +687,7 @@ try {
|
|
|
513
687
|
const pattern = values.match;
|
|
514
688
|
if (!target || typeof pattern !== "string" || pattern.length === 0) {
|
|
515
689
|
console.error(
|
|
516
|
-
"Usage: tmux-ide wait output <pane|session> --match <regex> [--timeout <ms>]",
|
|
690
|
+
"Usage: tmux-ide wait output <pane|session> --match <regex> [--timeout <ms>] [--socket[=path]]",
|
|
517
691
|
);
|
|
518
692
|
process.exit(1);
|
|
519
693
|
}
|
|
@@ -523,46 +697,42 @@ try {
|
|
|
523
697
|
console.error(`Invalid --match regex: ${(err as Error).message}`);
|
|
524
698
|
process.exit(1);
|
|
525
699
|
}
|
|
526
|
-
const { capturePane } = await import("../packages/tmux-bridge/src/index.ts");
|
|
527
700
|
const outTimeout = Number(values.timeout ?? "60000");
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
let hit: string | null = null;
|
|
541
|
-
for (const line of lines) {
|
|
542
|
-
if (new RegExp(pattern!).test(line)) {
|
|
543
|
-
hit = line;
|
|
544
|
-
break;
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
if (hit === null && new RegExp(pattern!).test(text)) hit = lines[lines.length - 1] ?? "";
|
|
548
|
-
if (hit !== null) {
|
|
549
|
-
if (json) console.log(JSON.stringify({ matched: hit }));
|
|
550
|
-
else console.log(hit);
|
|
551
|
-
process.exit(0);
|
|
552
|
-
}
|
|
553
|
-
if (Date.now() - outStart >= outTimeout) {
|
|
701
|
+
|
|
702
|
+
// `--socket` fast path: let a running `tmux-ide serve` hold the wait
|
|
703
|
+
// (one process, no spawn-per-poll). Falls back silently when no server
|
|
704
|
+
// is listening — the polling below is exactly the same implementation.
|
|
705
|
+
const viaSocket = await waitOverSocket({
|
|
706
|
+
kind: "output",
|
|
707
|
+
target,
|
|
708
|
+
match: pattern,
|
|
709
|
+
timeoutMs: outTimeout,
|
|
710
|
+
});
|
|
711
|
+
if (viaSocket) {
|
|
712
|
+
if (viaSocket.timedOut) {
|
|
554
713
|
console.error(
|
|
555
714
|
`Timed out after ${outTimeout}ms waiting for ${target} output to match /${pattern}/`,
|
|
556
715
|
);
|
|
557
716
|
process.exit(1);
|
|
558
717
|
}
|
|
559
|
-
|
|
718
|
+
const hit = (viaSocket.data as { matched: string }).matched;
|
|
719
|
+
if (json) console.log(JSON.stringify({ matched: hit }));
|
|
720
|
+
else console.log(hit);
|
|
721
|
+
process.exit(0);
|
|
560
722
|
}
|
|
561
|
-
}
|
|
562
723
|
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
724
|
+
const { waitForOutputMatch } = await import("../packages/daemon/src/tui/team/wait.ts");
|
|
725
|
+
const result = await waitForOutputMatch(target!, pattern!, { timeoutMs: outTimeout });
|
|
726
|
+
if (!result.ok) {
|
|
727
|
+
console.error(
|
|
728
|
+
`Timed out after ${outTimeout}ms waiting for ${target} output to match /${pattern}/`,
|
|
729
|
+
);
|
|
730
|
+
process.exit(1);
|
|
731
|
+
}
|
|
732
|
+
if (json) console.log(JSON.stringify({ matched: result.matched }));
|
|
733
|
+
else console.log(result.matched);
|
|
734
|
+
process.exit(0);
|
|
735
|
+
}
|
|
566
736
|
|
|
567
737
|
const VALID = new Set(["blocked", "working", "done", "idle", "unknown"]);
|
|
568
738
|
const sessionName = positionals[2];
|
|
@@ -570,37 +740,50 @@ try {
|
|
|
570
740
|
|
|
571
741
|
if (sub !== "agent-status" || !sessionName || typeof want !== "string" || !VALID.has(want)) {
|
|
572
742
|
console.error(
|
|
573
|
-
"Usage: tmux-ide wait agent-status <session> --status <blocked|working|done|idle|unknown> [--timeout <ms>]",
|
|
743
|
+
"Usage: tmux-ide wait agent-status <session> --status <blocked|working|done|idle|unknown> [--timeout <ms>] [--socket[=path]]",
|
|
574
744
|
);
|
|
575
745
|
process.exit(1);
|
|
576
746
|
}
|
|
577
747
|
|
|
578
748
|
const timeout = Number(values.timeout ?? "60000");
|
|
579
|
-
|
|
580
|
-
const
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
if (status === want) {
|
|
589
|
-
if (json) {
|
|
590
|
-
console.log(JSON.stringify({ session: sessionName, status, ok: true }));
|
|
591
|
-
} else {
|
|
592
|
-
console.log(`${sessionName} reached status: ${status}`);
|
|
593
|
-
}
|
|
594
|
-
process.exit(0);
|
|
595
|
-
}
|
|
596
|
-
if (Date.now() - started >= timeout) {
|
|
749
|
+
|
|
750
|
+
const viaSocket = await waitOverSocket({
|
|
751
|
+
kind: "agent-status",
|
|
752
|
+
session: sessionName,
|
|
753
|
+
status: want,
|
|
754
|
+
timeoutMs: timeout,
|
|
755
|
+
});
|
|
756
|
+
if (viaSocket) {
|
|
757
|
+
if (viaSocket.timedOut) {
|
|
597
758
|
console.error(
|
|
598
|
-
`Timed out after ${timeout}ms waiting for ${sessionName} to reach status "${want}"
|
|
759
|
+
`Timed out after ${timeout}ms waiting for ${sessionName} to reach status "${want}"`,
|
|
599
760
|
);
|
|
600
761
|
process.exit(1);
|
|
601
762
|
}
|
|
602
|
-
|
|
763
|
+
if (json) console.log(JSON.stringify({ session: sessionName, status: want, ok: true }));
|
|
764
|
+
else console.log(`${sessionName} reached status: ${want}`);
|
|
765
|
+
process.exit(0);
|
|
603
766
|
}
|
|
767
|
+
|
|
768
|
+
const { waitForAgentStatus } = await import("../packages/daemon/src/tui/team/wait.ts");
|
|
769
|
+
const result = await waitForAgentStatus(
|
|
770
|
+
sessionName!,
|
|
771
|
+
want as import("../packages/daemon/src/tui/detect/classify.ts").AgentStatus,
|
|
772
|
+
{ timeoutMs: timeout },
|
|
773
|
+
);
|
|
774
|
+
if (!result.ok) {
|
|
775
|
+
console.error(
|
|
776
|
+
`Timed out after ${timeout}ms waiting for ${sessionName} to reach status "${want}" (last: ${result.status ?? "absent"})`,
|
|
777
|
+
);
|
|
778
|
+
process.exit(1);
|
|
779
|
+
}
|
|
780
|
+
if (json) {
|
|
781
|
+
console.log(JSON.stringify({ session: sessionName, status: result.status, ok: true }));
|
|
782
|
+
} else {
|
|
783
|
+
console.log(`${sessionName} reached status: ${result.status}`);
|
|
784
|
+
}
|
|
785
|
+
process.exit(0);
|
|
786
|
+
break;
|
|
604
787
|
}
|
|
605
788
|
|
|
606
789
|
case "events": {
|
|
@@ -613,10 +796,6 @@ try {
|
|
|
613
796
|
type EventLike = { ts: string; session: string; from: Status | null; to: Status };
|
|
614
797
|
|
|
615
798
|
const path = eventsPath();
|
|
616
|
-
if (!existsSync(path)) {
|
|
617
|
-
console.log("no events yet — is a session adopted? (the chrome updater writes events)");
|
|
618
|
-
break;
|
|
619
|
-
}
|
|
620
799
|
|
|
621
800
|
// Status → ANSI color, matching the chrome bar's palette in spirit.
|
|
622
801
|
const paintStatus = (status: Status | null, text: string): string => {
|
|
@@ -646,6 +825,40 @@ try {
|
|
|
646
825
|
}
|
|
647
826
|
};
|
|
648
827
|
|
|
828
|
+
// `--follow --socket` fast path: a running `tmux-ide serve` PUSHES
|
|
829
|
+
// transitions the moment its detection tick sees them — no file polling.
|
|
830
|
+
// The last-50 backlog still comes from the log (the server has no
|
|
831
|
+
// history); falls back silently to the polling path when no server is up.
|
|
832
|
+
if (values.follow && socketFlag) {
|
|
833
|
+
const { connectControl } = await import("../packages/daemon/src/control/client.ts");
|
|
834
|
+
const client = await connectControl({
|
|
835
|
+
socketPath: typeof socketFlag === "string" ? socketFlag : undefined,
|
|
836
|
+
}).catch(() => null);
|
|
837
|
+
if (client) {
|
|
838
|
+
if (existsSync(path)) {
|
|
839
|
+
const backlog = readFileSync(path, "utf8")
|
|
840
|
+
.split("\n")
|
|
841
|
+
.filter((l) => l.trim().length > 0);
|
|
842
|
+
for (const line of backlog.slice(-50)) printLine(line);
|
|
843
|
+
}
|
|
844
|
+
await client.subscribe((frame) => {
|
|
845
|
+
if (frame.event === "agent-status") printLine(JSON.stringify(frame.data));
|
|
846
|
+
});
|
|
847
|
+
process.on("SIGINT", () => {
|
|
848
|
+
client.close();
|
|
849
|
+
process.exit(0);
|
|
850
|
+
});
|
|
851
|
+
// Ends when the server shuts down (EOF) — exit cleanly then.
|
|
852
|
+
await client.done;
|
|
853
|
+
break;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
if (!existsSync(path)) {
|
|
858
|
+
console.log("no events yet — is a session adopted? (the chrome updater writes events)");
|
|
859
|
+
break;
|
|
860
|
+
}
|
|
861
|
+
|
|
649
862
|
const allLines = readFileSync(path, "utf8")
|
|
650
863
|
.split("\n")
|
|
651
864
|
.filter((l) => l.trim().length > 0);
|
|
@@ -769,18 +982,36 @@ try {
|
|
|
769
982
|
case "integration": {
|
|
770
983
|
const sub = positionals[1];
|
|
771
984
|
const agent = positionals[2];
|
|
772
|
-
// `status` and `offer` need no agent arg; install/uninstall
|
|
773
|
-
|
|
774
|
-
|
|
985
|
+
// `status` and `offer` need no agent arg; install/uninstall take a kind
|
|
986
|
+
// we ship an integration for (claude hooks, opencode plugin).
|
|
987
|
+
const needsAgent = sub === "install" || sub === "uninstall";
|
|
988
|
+
const installable = agent === "claude" || agent === "opencode";
|
|
989
|
+
if (!sub || (needsAgent && !installable)) {
|
|
775
990
|
console.error(
|
|
776
|
-
"Usage: tmux-ide integration <install|uninstall|status|offer> [claude]\n" +
|
|
777
|
-
" install hook
|
|
778
|
-
"
|
|
779
|
-
"
|
|
991
|
+
"Usage: tmux-ide integration <install|uninstall|status|offer> [claude|opencode]\n" +
|
|
992
|
+
" install claude: hook lifecycle events into tmux pane state\n" +
|
|
993
|
+
" opencode: plugin that records the session id for restore --resume-agents\n" +
|
|
994
|
+
" uninstall remove exactly the tmux-ide entries for that agent\n" +
|
|
995
|
+
" status list discovered agents + integration/capture state\n" +
|
|
780
996
|
" offer one-time first-adopt install prompt (used by the popup)",
|
|
781
997
|
);
|
|
782
998
|
process.exit(1);
|
|
783
999
|
}
|
|
1000
|
+
if (needsAgent && agent === "opencode") {
|
|
1001
|
+
const oc = await import("../packages/daemon/src/tui/integrations/opencode.ts");
|
|
1002
|
+
if (sub === "install") {
|
|
1003
|
+
const { pluginPath } = oc.installOpencodeIntegration();
|
|
1004
|
+
console.log(`plugin: ${pluginPath}`);
|
|
1005
|
+
console.log(
|
|
1006
|
+
"installed — NEW opencode sessions record their session id into the pane\n" +
|
|
1007
|
+
"(@agent_session_id), so `tmux-ide restore --resume-agents` can revive them.",
|
|
1008
|
+
);
|
|
1009
|
+
} else {
|
|
1010
|
+
const { wasInstalled } = oc.uninstallOpencodeIntegration();
|
|
1011
|
+
console.log(wasInstalled ? "uninstalled — plugin removed" : "was not installed");
|
|
1012
|
+
}
|
|
1013
|
+
break;
|
|
1014
|
+
}
|
|
784
1015
|
const mod = await import("../packages/daemon/src/tui/integrations/claude.ts");
|
|
785
1016
|
if (sub === "install") {
|
|
786
1017
|
const { scriptPath, settingsPath } = mod.installClaudeIntegration();
|
|
@@ -796,6 +1027,51 @@ try {
|
|
|
796
1027
|
"installed — NEW Claude Code sessions now report working/blocked/done " +
|
|
797
1028
|
"authoritatively into the tmux-ide chrome.",
|
|
798
1029
|
);
|
|
1030
|
+
// M25.1: this is the moment the user is wiring notifications, so offer
|
|
1031
|
+
// the macOS banner channel here — one plain y/N key, same shape as the
|
|
1032
|
+
// first-adopt offer. Skipped when already on, off-macOS, or when there
|
|
1033
|
+
// is no TTY to ask (TMUX_IDE_NOTIFY_KEY forces an answer for tests).
|
|
1034
|
+
const { getAppConfig, updateAppConfig } =
|
|
1035
|
+
await import("../packages/daemon/src/lib/app-config.ts");
|
|
1036
|
+
const forcedKey = process.env.TMUX_IDE_NOTIFY_KEY;
|
|
1037
|
+
const canAsk = forcedKey !== undefined || process.stdin.isTTY === true;
|
|
1038
|
+
if (process.platform === "darwin" && !getAppConfig().notifications.macos && canAsk) {
|
|
1039
|
+
const act = (key: string): void => {
|
|
1040
|
+
if (key === "y" || key === "Y") {
|
|
1041
|
+
updateAppConfig({ notifications: { macos: true } });
|
|
1042
|
+
console.log(
|
|
1043
|
+
"macOS notifications on — native branded banners will jump to the session when clicked.",
|
|
1044
|
+
);
|
|
1045
|
+
} else {
|
|
1046
|
+
console.log(
|
|
1047
|
+
"skipped — turn banners on anytime: notifications.macos in ~/.tmux-ide/config.json.",
|
|
1048
|
+
);
|
|
1049
|
+
}
|
|
1050
|
+
};
|
|
1051
|
+
process.stdout.write("\nAlso get a macOS notification when an agent needs you? [y/N] ");
|
|
1052
|
+
if (forcedKey !== undefined) {
|
|
1053
|
+
console.log(forcedKey);
|
|
1054
|
+
act(forcedKey);
|
|
1055
|
+
} else {
|
|
1056
|
+
const key = await new Promise<string>((resolve) => {
|
|
1057
|
+
try {
|
|
1058
|
+
process.stdin.setRawMode?.(true);
|
|
1059
|
+
process.stdin.resume();
|
|
1060
|
+
process.stdin.once("data", (data) => resolve(data.toString()));
|
|
1061
|
+
} catch {
|
|
1062
|
+
resolve(""); // no readable stdin — treat as "no"
|
|
1063
|
+
}
|
|
1064
|
+
});
|
|
1065
|
+
try {
|
|
1066
|
+
process.stdin.setRawMode?.(false);
|
|
1067
|
+
process.stdin.pause();
|
|
1068
|
+
} catch {
|
|
1069
|
+
// best-effort terminal restore
|
|
1070
|
+
}
|
|
1071
|
+
console.log(/^[ -~]$/.test(key) ? key : "");
|
|
1072
|
+
act(key);
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
799
1075
|
} else if (sub === "uninstall") {
|
|
800
1076
|
const { wasInstalled } = mod.uninstallClaudeIntegration();
|
|
801
1077
|
console.log(wasInstalled ? "uninstalled — hook entries removed" : "was not installed");
|
|
@@ -861,7 +1137,18 @@ try {
|
|
|
861
1137
|
else if (a.integration)
|
|
862
1138
|
state = a.installed ? "integration installed ✓" : "on PATH — integration not installed";
|
|
863
1139
|
else state = "detected (no integration)";
|
|
864
|
-
|
|
1140
|
+
// The resume-key story: how @agent_session_id (what `restore
|
|
1141
|
+
// --resume-agents` revives from) gets captured for this kind.
|
|
1142
|
+
let capture = "";
|
|
1143
|
+
if (a.path !== null) {
|
|
1144
|
+
if (a.capture === "probe") capture = " · session-id capture: automatic";
|
|
1145
|
+
else if (a.capture !== null)
|
|
1146
|
+
capture = a.captureActive
|
|
1147
|
+
? ` · session-id capture: ${a.capture} ✓`
|
|
1148
|
+
: ` · session-id capture: ${a.capture} (install to enable)`;
|
|
1149
|
+
else capture = " · session-id capture: none";
|
|
1150
|
+
}
|
|
1151
|
+
console.log(` ${a.id.padEnd(10)} ${state}${capture}`);
|
|
865
1152
|
}
|
|
866
1153
|
}
|
|
867
1154
|
break;
|
|
@@ -1338,6 +1625,45 @@ try {
|
|
|
1338
1625
|
}
|
|
1339
1626
|
|
|
1340
1627
|
case "update": {
|
|
1628
|
+
// `--manifests`: fetch the agent-detection manifest pack (versioned JSON,
|
|
1629
|
+
// a GitHub release asset) into ~/.tmux-ide/agent-detection/pack/ — the
|
|
1630
|
+
// loader hot-merges it under bundled<pack<user precedence. Schema-invalid
|
|
1631
|
+
// packs are rejected loudly. See lib/manifest-pack.ts for the format.
|
|
1632
|
+
if (values["manifests"] === true) {
|
|
1633
|
+
const { updateManifestPack } = await import("../packages/daemon/src/lib/manifest-pack.ts");
|
|
1634
|
+
try {
|
|
1635
|
+
const r = await updateManifestPack({ log: (m) => console.error(m) });
|
|
1636
|
+
if (json) {
|
|
1637
|
+
console.log(JSON.stringify({ ok: true, ...r }, null, 2));
|
|
1638
|
+
} else {
|
|
1639
|
+
console.log(
|
|
1640
|
+
`manifest pack ${r.packVersion} installed (${r.count} manifests): ${r.path}`,
|
|
1641
|
+
);
|
|
1642
|
+
console.log(
|
|
1643
|
+
"your own agent-detection/*.json overrides still win — the pack merges beneath them",
|
|
1644
|
+
);
|
|
1645
|
+
}
|
|
1646
|
+
} catch (err) {
|
|
1647
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1648
|
+
if (json) console.log(JSON.stringify({ ok: false, error: message }, null, 2));
|
|
1649
|
+
else console.error(`manifest pack NOT installed: ${message}`);
|
|
1650
|
+
process.exitCode = 1;
|
|
1651
|
+
}
|
|
1652
|
+
break;
|
|
1653
|
+
}
|
|
1654
|
+
// `--tui-binary`: download the per-platform TUI binary (the fallback that
|
|
1655
|
+
// lets an npm install with no bun run the full cockpit). Explicit opt-in —
|
|
1656
|
+
// never auto-fetched on install (it's ~70MB). See lib/tui-binary.ts.
|
|
1657
|
+
if (values["tui-binary"] === true) {
|
|
1658
|
+
const { downloadTuiBinary } = await import("../packages/daemon/src/lib/tui-binary.ts");
|
|
1659
|
+
const { path } = await downloadTuiBinary({ log: (m) => console.error(m) });
|
|
1660
|
+
if (json) {
|
|
1661
|
+
console.log(JSON.stringify({ ok: true, path }, null, 2));
|
|
1662
|
+
} else {
|
|
1663
|
+
console.log(`TUI binary ready: ${path}`);
|
|
1664
|
+
}
|
|
1665
|
+
break;
|
|
1666
|
+
}
|
|
1341
1667
|
// Detect how tmux-ide was installed and act on the pending update: a dev
|
|
1342
1668
|
// checkout prints the `git pull` hint; a global install prints (with
|
|
1343
1669
|
// --dry-run) or runs its package manager's update command. `__dirname` is
|
|
@@ -1381,6 +1707,34 @@ try {
|
|
|
1381
1707
|
break;
|
|
1382
1708
|
}
|
|
1383
1709
|
|
|
1710
|
+
case "serve": {
|
|
1711
|
+
// The local control socket (M23.3): NDJSON verbs + pushed agent-status
|
|
1712
|
+
// events over a 0600 Unix socket. Foreground on purpose — the process
|
|
1713
|
+
// the user (or their agent loop) started is the process they own; see
|
|
1714
|
+
// the host tradeoff in control/server.ts.
|
|
1715
|
+
const { startControlServer, defaultControlSocketPath } =
|
|
1716
|
+
await import("../packages/daemon/src/control/server.ts");
|
|
1717
|
+
const socketPath =
|
|
1718
|
+
typeof socketFlag === "string"
|
|
1719
|
+
? socketFlag
|
|
1720
|
+
: (positionals[1] ?? defaultControlSocketPath());
|
|
1721
|
+
const server = await startControlServer({
|
|
1722
|
+
socketPath,
|
|
1723
|
+
log: (m) => console.error(`[serve] ${m}`),
|
|
1724
|
+
});
|
|
1725
|
+
let closing = false;
|
|
1726
|
+
const shutdown = () => {
|
|
1727
|
+
if (closing) return;
|
|
1728
|
+
closing = true;
|
|
1729
|
+
// Unlinks the socket and EOFs every client before exiting.
|
|
1730
|
+
void server.close().then(() => process.exit(0));
|
|
1731
|
+
};
|
|
1732
|
+
process.on("SIGTERM", shutdown);
|
|
1733
|
+
process.on("SIGINT", shutdown);
|
|
1734
|
+
await new Promise(() => {}); // the server owns the process lifetime
|
|
1735
|
+
break;
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1384
1738
|
case "command-center": {
|
|
1385
1739
|
const { startCommandCenter } = await import("../packages/daemon/src/command-center/index.ts");
|
|
1386
1740
|
await startCommandCenter({ port: parseInt(values.port ?? "4000") });
|