tmux-ide 2.6.0 → 2.7.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 +14 -9
- package/bin/cli.js +1024 -519
- package/bin/cli.ts +63 -6
- package/bunfig.toml +4 -0
- package/package.json +18 -7
- package/packages/contracts/package.json +22 -0
- package/packages/contracts/src/__tests__/ide-config.test.ts +46 -0
- package/packages/contracts/src/__tests__/terminals.test.ts +87 -0
- package/packages/contracts/src/actions-contract.ts +310 -0
- package/packages/contracts/src/actions-errors.ts +41 -0
- package/packages/contracts/src/domain.ts +36 -0
- package/packages/contracts/src/ide-config.ts +170 -0
- package/packages/contracts/src/index.ts +24 -0
- package/packages/contracts/src/lib-internal/auth.ts +13 -0
- package/packages/contracts/src/lib-internal/hq.ts +38 -0
- package/packages/contracts/src/terminals.ts +116 -0
- package/packages/contracts/src/tmux.ts +60 -0
- package/packages/contracts/src/workspace.ts +67 -0
- package/packages/daemon/dist/agent-explain.d.ts +8 -1
- package/packages/daemon/dist/agent-explain.js +19 -3
- 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/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 +298 -0
- package/packages/daemon/src/attach.ts +20 -0
- package/packages/daemon/src/bin.ts +4 -0
- package/packages/daemon/src/canonical.ts +7 -0
- package/packages/daemon/src/cli.ts +499 -0
- package/packages/daemon/src/command-center/actions/contract.ts +2 -0
- package/packages/daemon/src/command-center/actions/dispatcher.ts +137 -0
- package/packages/daemon/src/command-center/actions/errors.ts +105 -0
- package/packages/daemon/src/command-center/actions/handlers/_project-context.ts +30 -0
- package/packages/daemon/src/command-center/actions/handlers/_resolve-project.ts +78 -0
- package/packages/daemon/src/command-center/actions/handlers/app-set-remote-access.ts +118 -0
- package/packages/daemon/src/command-center/actions/handlers/config-actions.ts +113 -0
- package/packages/daemon/src/command-center/actions/handlers/daemon-shutdown.ts +38 -0
- package/packages/daemon/src/command-center/actions/handlers/project-activate.ts +30 -0
- package/packages/daemon/src/command-center/actions/handlers/project-launch.ts +70 -0
- package/packages/daemon/src/command-center/actions/handlers/project-open-terminal.ts +87 -0
- package/packages/daemon/src/command-center/actions/handlers/project-restart.ts +38 -0
- package/packages/daemon/src/command-center/actions/handlers/project-stop.ts +62 -0
- package/packages/daemon/src/command-center/actions/handlers/terminal-respawn.ts +119 -0
- package/packages/daemon/src/command-center/actions/handlers/terminal-stop.ts +35 -0
- package/packages/daemon/src/command-center/actions/registry.ts +149 -0
- package/packages/daemon/src/command-center/discovery.ts +96 -0
- package/packages/daemon/src/command-center/index.ts +31 -0
- package/packages/daemon/src/command-center/schemas.ts +85 -0
- package/packages/daemon/src/command-center/server.ts +1260 -0
- package/packages/daemon/src/command-center/ws-events.ts +316 -0
- package/packages/daemon/src/config.ts +549 -0
- package/packages/daemon/src/detect.ts +248 -0
- package/packages/daemon/src/doctor.ts +242 -0
- package/packages/daemon/src/embed.ts +5 -0
- package/packages/daemon/src/index.ts +12 -0
- package/packages/daemon/src/init.ts +211 -0
- package/packages/daemon/src/inspect.ts +178 -0
- package/packages/daemon/src/js-yaml.d.ts +10 -0
- package/packages/daemon/src/launch.ts +349 -0
- package/packages/daemon/src/lib/active-projects.ts +49 -0
- package/packages/daemon/src/lib/agent-discovery.ts +121 -0
- package/packages/daemon/src/lib/app-config.ts +427 -0
- package/packages/daemon/src/lib/app-settings.ts +53 -0
- package/packages/daemon/src/lib/auth/auth-service.ts +227 -0
- package/packages/daemon/src/lib/auth/middleware.ts +56 -0
- package/packages/daemon/src/lib/auth/types.ts +2 -0
- package/packages/daemon/src/lib/auth-token.ts +5 -0
- package/packages/daemon/src/lib/authorship.ts +280 -0
- package/packages/daemon/src/lib/canonical-daemon.ts +122 -0
- package/packages/daemon/src/lib/cli-action-bridge.ts +216 -0
- package/packages/daemon/src/lib/daemon-embed.ts +782 -0
- package/packages/daemon/src/lib/daemon-watchdog.ts +111 -0
- package/packages/daemon/src/lib/daemon.ts +79 -0
- package/packages/daemon/src/lib/dot-path.ts +17 -0
- package/packages/daemon/src/lib/errors.ts +67 -0
- package/packages/daemon/src/lib/filesystem-browser.ts +292 -0
- package/packages/daemon/src/lib/launch-plan.ts +90 -0
- package/packages/daemon/src/lib/log.ts +134 -0
- package/packages/daemon/src/lib/output.ts +76 -0
- package/packages/daemon/src/lib/project-init-runner.ts +150 -0
- package/packages/daemon/src/lib/project-inspect.ts +80 -0
- package/packages/daemon/src/lib/project-onboard.ts +149 -0
- package/packages/daemon/src/lib/project-probe.ts +92 -0
- package/packages/daemon/src/lib/project-registry.ts +296 -0
- package/packages/daemon/src/lib/session-monitor.ts +122 -0
- package/packages/daemon/src/lib/session-options.ts +100 -0
- package/packages/daemon/src/lib/shell.ts +8 -0
- package/packages/daemon/src/lib/sizes.ts +36 -0
- package/packages/daemon/src/lib/skill-sync.ts +155 -0
- package/packages/daemon/src/lib/slugify.ts +10 -0
- package/packages/daemon/src/lib/terminals-store.ts +125 -0
- package/packages/daemon/src/lib/tui-binary.ts +165 -0
- package/packages/daemon/src/lib/update-check.ts +298 -0
- package/packages/daemon/src/lib/update.ts +158 -0
- package/packages/daemon/src/lib/workspace-registry.ts +229 -0
- package/packages/daemon/src/lib/worktree.ts +289 -0
- package/packages/daemon/src/lib/yaml-io.ts +27 -0
- package/packages/daemon/src/ls.ts +40 -0
- package/packages/daemon/src/restart.ts +24 -0
- package/packages/daemon/src/restore.ts +514 -0
- package/packages/daemon/src/schemas/domain.ts +2 -0
- package/packages/daemon/src/schemas/filesystem.ts +34 -0
- package/packages/daemon/src/schemas/ide-config.ts +2 -0
- package/packages/daemon/src/schemas/index.ts +59 -0
- package/packages/daemon/src/schemas/inspect.ts +67 -0
- package/packages/daemon/src/schemas/registry.ts +55 -0
- package/packages/daemon/src/schemas/ws-events.ts +135 -0
- package/packages/daemon/src/send.ts +171 -0
- package/packages/daemon/src/server/README.md +15 -0
- package/packages/daemon/src/server/index.ts +74 -0
- package/packages/daemon/src/server/pty-bridge.ts +532 -0
- package/packages/daemon/src/server/standalone.ts +18 -0
- package/packages/daemon/src/server/ws-route.ts +483 -0
- package/packages/daemon/src/status.ts +59 -0
- package/packages/daemon/src/stop.ts +28 -0
- package/packages/daemon/src/terminal/NodePtyAdapter.ts +271 -0
- package/packages/daemon/src/terminal/PtyAdapter.ts +140 -0
- package/packages/daemon/src/terminal/README.md +92 -0
- package/packages/daemon/src/tui/chrome/cheatsheet.ts +260 -0
- package/packages/daemon/src/tui/chrome/chip.ts +30 -0
- package/packages/daemon/src/tui/chrome/events.ts +119 -0
- package/packages/daemon/src/tui/chrome/kitty-keys.ts +55 -0
- package/packages/daemon/src/tui/chrome/menu.ts +289 -0
- package/packages/daemon/src/tui/chrome/notify.ts +382 -0
- package/packages/daemon/src/tui/chrome/panels.ts +111 -0
- package/packages/daemon/src/tui/chrome/sidebar.ts +222 -0
- package/packages/daemon/src/tui/chrome/snapshot.ts +425 -0
- package/packages/daemon/src/tui/chrome/statusline.ts +595 -0
- package/packages/daemon/src/tui/chrome/updater.ts +510 -0
- package/packages/daemon/src/tui/chrome/welcome.ts +124 -0
- package/packages/daemon/src/tui/compiled.ts +121 -0
- package/packages/daemon/src/tui/detect/classify.ts +208 -0
- package/packages/daemon/src/tui/detect/manifest-loader.ts +193 -0
- package/packages/daemon/src/tui/detect/manifest.ts +199 -0
- package/packages/daemon/src/tui/detect/manifests.ts +354 -0
- package/packages/daemon/src/tui/detect/process-tree.ts +217 -0
- package/packages/daemon/src/tui/detect/snapshot.ts +70 -0
- package/packages/daemon/src/tui/integrations/claude.ts +176 -0
- package/packages/daemon/src/tui/integrations/offer.ts +145 -0
- package/packages/daemon/src/tui/main.ts +82 -0
- package/packages/daemon/src/tui/mirror/ack-writer.ts +77 -0
- package/packages/daemon/src/tui/mirror/agent-chip.ts +97 -0
- package/packages/daemon/src/tui/mirror/agent-rows.ts +133 -0
- package/packages/daemon/src/tui/mirror/app-state.ts +179 -0
- package/packages/daemon/src/tui/mirror/app.tsx +5265 -0
- package/packages/daemon/src/tui/mirror/blit.ts +186 -0
- package/packages/daemon/src/tui/mirror/control-client.ts +214 -0
- package/packages/daemon/src/tui/mirror/control.ts +97 -0
- package/packages/daemon/src/tui/mirror/dialog-model.ts +298 -0
- package/packages/daemon/src/tui/mirror/dialog-stack.ts +354 -0
- package/packages/daemon/src/tui/mirror/diff-model.ts +112 -0
- package/packages/daemon/src/tui/mirror/editor-buffer.ts +117 -0
- package/packages/daemon/src/tui/mirror/file-tree.ts +97 -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/input-coalescer.ts +105 -0
- package/packages/daemon/src/tui/mirror/menu-model.ts +187 -0
- package/packages/daemon/src/tui/mirror/palette.ts +274 -0
- package/packages/daemon/src/tui/mirror/pane-mirror.ts +561 -0
- package/packages/daemon/src/tui/mirror/pane-surface.tsx +415 -0
- package/packages/daemon/src/tui/mirror/perf-tap.ts +160 -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 +262 -0
- package/packages/daemon/src/tui/mirror/session-mirror.ts +443 -0
- package/packages/daemon/src/tui/mirror/settings-model.ts +345 -0
- package/packages/daemon/src/tui/mirror/size-truth.ts +77 -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/team/CONTROL.md +50 -0
- package/packages/daemon/src/tui/team/entry.ts +38 -0
- package/packages/daemon/src/tui/team/fuzzy.ts +133 -0
- package/packages/daemon/src/tui/team/home.ts +170 -0
- package/packages/daemon/src/tui/team/index.tsx +1521 -0
- package/packages/daemon/src/tui/team/input.ts +34 -0
- package/packages/daemon/src/tui/team/keymap.ts +127 -0
- package/packages/daemon/src/tui/team/mouse.ts +29 -0
- package/packages/daemon/src/tui/team/nav.ts +31 -0
- package/packages/daemon/src/tui/team/preview.ts +34 -0
- package/packages/daemon/src/tui/team/projects.ts +191 -0
- package/packages/daemon/src/tui/team/report.ts +83 -0
- package/packages/daemon/src/tui/team/sessions.ts +433 -0
- package/packages/daemon/src/tui/team/tree.ts +62 -0
- package/packages/daemon/src/types.ts +13 -0
- package/packages/daemon/src/ui/index.ts +32 -0
- package/packages/daemon/src/ui/terminal/index.ts +9 -0
- package/packages/daemon/src/ui/types.ts +91 -0
- package/packages/daemon/src/ui/web/base.css +80 -0
- package/packages/daemon/src/ui/web/components/Box.tsx +59 -0
- package/packages/daemon/src/ui/web/components/Input.tsx +32 -0
- package/packages/daemon/src/ui/web/components/ScrollBox.tsx +60 -0
- package/packages/daemon/src/ui/web/components/Text.tsx +28 -0
- package/packages/daemon/src/ui/web/hooks.ts +106 -0
- package/packages/daemon/src/ui/web/index.ts +27 -0
- package/packages/daemon/src/ui/web/render.ts +77 -0
- package/packages/daemon/src/ui/web/utils/color.ts +27 -0
- package/packages/daemon/src/validate.ts +217 -0
- package/packages/daemon/src/widgets/changes/README.md +3 -0
- package/packages/daemon/src/widgets/changes/index.tsx +691 -0
- package/packages/daemon/src/widgets/config/README.md +3 -0
- package/packages/daemon/src/widgets/config/index.tsx +481 -0
- package/packages/daemon/src/widgets/explorer/README.md +3 -0
- package/packages/daemon/src/widgets/explorer/breadcrumbs.tsx +77 -0
- package/packages/daemon/src/widgets/explorer/footer.tsx +20 -0
- package/packages/daemon/src/widgets/explorer/header.tsx +23 -0
- package/packages/daemon/src/widgets/explorer/index.tsx +456 -0
- package/packages/daemon/src/widgets/explorer/tree-model.ts +103 -0
- package/packages/daemon/src/widgets/explorer/tree.tsx +165 -0
- package/packages/daemon/src/widgets/lib/config-model.ts +116 -0
- package/packages/daemon/src/widgets/lib/files.ts +88 -0
- package/packages/daemon/src/widgets/lib/git.ts +88 -0
- package/packages/daemon/src/widgets/lib/grammar.ts +126 -0
- package/packages/daemon/src/widgets/lib/help-overlay.tsx +101 -0
- package/packages/daemon/src/widgets/lib/pane-comms.ts +209 -0
- package/packages/daemon/src/widgets/lib/theme.ts +194 -0
- package/packages/daemon/src/widgets/lib/watcher.ts +132 -0
- package/packages/daemon/src/widgets/preview/README.md +3 -0
- package/packages/daemon/src/widgets/preview/index.tsx +416 -0
- package/packages/daemon/src/widgets/resolve.ts +121 -0
- package/packages/daemon/src/widgets/setup/README.md +3 -0
- package/packages/daemon/src/widgets/setup/agent-naming.tsx +112 -0
- package/packages/daemon/src/widgets/setup/config-tree.tsx +246 -0
- package/packages/daemon/src/widgets/setup/detect-panel.tsx +72 -0
- package/packages/daemon/src/widgets/setup/field-editor.tsx +265 -0
- package/packages/daemon/src/widgets/setup/footer.tsx +107 -0
- package/packages/daemon/src/widgets/setup/index.tsx +341 -0
- package/packages/daemon/src/widgets/setup/layout-picker.tsx +96 -0
- package/packages/daemon/src/widgets/setup/orchestrator-panel.tsx +200 -0
- package/packages/daemon/src/widgets/setup/review-panel.tsx +140 -0
- package/packages/daemon/src/widgets/setup/setup-model.ts +188 -0
- package/packages/daemon/src/widgets/sidebar/index.tsx +527 -0
- package/packages/tmux-bridge/package.json +22 -0
- package/packages/tmux-bridge/src/errors.ts +28 -0
- package/packages/tmux-bridge/src/index.ts +31 -0
- package/packages/tmux-bridge/src/monitor.ts +77 -0
- package/packages/tmux-bridge/src/panes.ts +136 -0
- package/packages/tmux-bridge/src/runner.test.ts +501 -0
- package/packages/tmux-bridge/src/runner.ts +91 -0
- package/packages/tmux-bridge/src/sessions.ts +126 -0
- package/packages/tmux-bridge/src/targeting.test.ts +107 -0
- package/packages/tmux-bridge/src/targeting.ts +90 -0
- package/scripts/build-tui.mjs +11 -4
- package/scripts/perf-mirror.mjs +313 -0
- package/scripts/postinstall.js +26 -2
- package/skill/SKILL.md +22 -0
- 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
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NodePtyAdapter — concrete `PtyAdapter` backed by the `node-pty` native
|
|
3
|
+
* module (T087). The only place in the daemon that imports `node-pty`.
|
|
4
|
+
*
|
|
5
|
+
* One-time setup:
|
|
6
|
+
* - macOS / Linux: node-pty ships a `spawn-helper` binary. The
|
|
7
|
+
* `chmod +x` is occasionally lost during install (npm tarball
|
|
8
|
+
* metadata edge cases). Borrowing t3's
|
|
9
|
+
* `ensureNodePtySpawnHelperExecutable`, we walk the published candidate
|
|
10
|
+
* paths and chmod 0755 once per process. Failure is best-effort —
|
|
11
|
+
* spawning will surface a real error if it actually matters.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { chmodSync, existsSync, statSync, type Stats } from "node:fs";
|
|
15
|
+
import { dirname, join } from "node:path";
|
|
16
|
+
import { createRequire } from "node:module";
|
|
17
|
+
import * as pty from "node-pty";
|
|
18
|
+
import {
|
|
19
|
+
PtySpawnError,
|
|
20
|
+
type PtyAdapter,
|
|
21
|
+
type PtyExitEvent,
|
|
22
|
+
type PtyProcess,
|
|
23
|
+
type PtySpawnInput,
|
|
24
|
+
} from "./PtyAdapter.ts";
|
|
25
|
+
|
|
26
|
+
const ADAPTER_ID = "node-pty";
|
|
27
|
+
let helperEnsured = false;
|
|
28
|
+
|
|
29
|
+
function candidateSpawnHelperPaths(): string[] {
|
|
30
|
+
const requireForNodePty = createRequire(import.meta.url);
|
|
31
|
+
let pkgJsonPath: string;
|
|
32
|
+
try {
|
|
33
|
+
pkgJsonPath = requireForNodePty.resolve("node-pty/package.json");
|
|
34
|
+
} catch {
|
|
35
|
+
// node-pty package missing — `pty.spawn` will throw a richer error.
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
const pkgDir = dirname(pkgJsonPath);
|
|
39
|
+
return [
|
|
40
|
+
join(pkgDir, "build", "Release", "spawn-helper"),
|
|
41
|
+
join(pkgDir, "build", "Debug", "spawn-helper"),
|
|
42
|
+
join(pkgDir, "prebuilds", `${process.platform}-${process.arch}`, "spawn-helper"),
|
|
43
|
+
];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Chmod node-pty's `spawn-helper` to 0755 the first time we touch it.
|
|
48
|
+
* No-op on Windows (no helper binary). Idempotent across calls.
|
|
49
|
+
* Exported for the unit test that asserts the chmod side effect.
|
|
50
|
+
*/
|
|
51
|
+
export function ensureNodePtySpawnHelperExecutable(
|
|
52
|
+
options: { explicitPath?: string; force?: boolean } = {},
|
|
53
|
+
): void {
|
|
54
|
+
if (process.platform === "win32") return;
|
|
55
|
+
if (!options.force && !options.explicitPath && helperEnsured) return;
|
|
56
|
+
|
|
57
|
+
const candidates = options.explicitPath ? [options.explicitPath] : candidateSpawnHelperPaths();
|
|
58
|
+
|
|
59
|
+
for (const candidate of candidates) {
|
|
60
|
+
if (!existsSync(candidate)) continue;
|
|
61
|
+
try {
|
|
62
|
+
chmodSync(candidate, 0o755);
|
|
63
|
+
} catch {
|
|
64
|
+
// Best-effort: a hardened filesystem may forbid chmod even when the
|
|
65
|
+
// bit is already set. Continue to the next candidate so we never
|
|
66
|
+
// fail spawning just because the helper was already executable.
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!options.explicitPath) helperEnsured = true;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function assertValidCwd(cwd: string, statFn: (cwd: string) => Stats): void {
|
|
74
|
+
let stats: Stats;
|
|
75
|
+
try {
|
|
76
|
+
stats = statFn(cwd);
|
|
77
|
+
} catch (err) {
|
|
78
|
+
throw new PtySpawnError({
|
|
79
|
+
adapter: ADAPTER_ID,
|
|
80
|
+
code: "cwd_invalid",
|
|
81
|
+
message: `cwd does not exist or cannot be stat'd: ${cwd}`,
|
|
82
|
+
cause: err,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
if (!stats.isDirectory()) {
|
|
86
|
+
throw new PtySpawnError({
|
|
87
|
+
adapter: ADAPTER_ID,
|
|
88
|
+
code: "cwd_invalid",
|
|
89
|
+
message: `cwd is not a directory: ${cwd}`,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Wrap a live `node-pty` IPty with the `PtyProcess` interface. Disposes are
|
|
96
|
+
* idempotent; `kill` emits a synthetic exit if node-pty doesn't fire one
|
|
97
|
+
* (e.g. when the child has already gone away under us).
|
|
98
|
+
*/
|
|
99
|
+
class NodePtyProcess implements PtyProcess {
|
|
100
|
+
private exited = false;
|
|
101
|
+
private readonly child: pty.IPty;
|
|
102
|
+
private readonly dataListeners = new Set<(data: Buffer) => void>();
|
|
103
|
+
private readonly exitListeners = new Set<(event: PtyExitEvent) => void>();
|
|
104
|
+
|
|
105
|
+
constructor(child: pty.IPty) {
|
|
106
|
+
this.child = child;
|
|
107
|
+
// node-pty hands us strings by default; we want raw buffers for
|
|
108
|
+
// byte-accurate WS bridging. The encoding:null on spawn opts gives us
|
|
109
|
+
// buffers in the typings of node-pty@1.2.0-beta.12.
|
|
110
|
+
this.child.onData((data) => {
|
|
111
|
+
const buf = Buffer.isBuffer(data) ? data : Buffer.from(String(data), "utf8");
|
|
112
|
+
for (const listener of this.dataListeners) listener(buf);
|
|
113
|
+
});
|
|
114
|
+
this.child.onExit((evt) => {
|
|
115
|
+
this.exited = true;
|
|
116
|
+
const event: PtyExitEvent = {
|
|
117
|
+
exitCode: evt.exitCode ?? 0,
|
|
118
|
+
signal: typeof evt.signal === "number" ? evt.signal : null,
|
|
119
|
+
};
|
|
120
|
+
for (const listener of this.exitListeners) listener(event);
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
get pid(): number {
|
|
125
|
+
return this.child.pid;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
write(data: string | Uint8Array): void {
|
|
129
|
+
if (this.exited) return;
|
|
130
|
+
if (typeof data === "string") this.child.write(data);
|
|
131
|
+
else this.child.write(Buffer.from(data).toString("binary"));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
resize(cols: number, rows: number): void {
|
|
135
|
+
if (this.exited) return;
|
|
136
|
+
if (!Number.isInteger(cols) || cols <= 0)
|
|
137
|
+
throw new RangeError("cols must be a positive integer");
|
|
138
|
+
if (!Number.isInteger(rows) || rows <= 0)
|
|
139
|
+
throw new RangeError("rows must be a positive integer");
|
|
140
|
+
try {
|
|
141
|
+
this.child.resize(cols, rows);
|
|
142
|
+
} catch {
|
|
143
|
+
// node-pty throws if the underlying fd is gone — treat as no-op so
|
|
144
|
+
// bridge resize handlers don't have to special-case.
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
kill(signal?: NodeJS.Signals | number): void {
|
|
149
|
+
if (this.exited) return;
|
|
150
|
+
try {
|
|
151
|
+
this.child.kill(typeof signal === "number" ? String(signal) : signal);
|
|
152
|
+
} catch {
|
|
153
|
+
// Already gone. Synthesize an exit so listeners detach cleanly.
|
|
154
|
+
this.exited = true;
|
|
155
|
+
for (const listener of this.exitListeners) listener({ exitCode: 0, signal: null });
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
onData(callback: (data: Buffer) => void): () => void {
|
|
160
|
+
this.dataListeners.add(callback);
|
|
161
|
+
return () => {
|
|
162
|
+
this.dataListeners.delete(callback);
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
onExit(callback: (event: PtyExitEvent) => void): () => void {
|
|
167
|
+
if (this.exited) {
|
|
168
|
+
// Adapters MUST guarantee at most one terminal onExit — late
|
|
169
|
+
// subscribers get an inert disposer.
|
|
170
|
+
return () => undefined;
|
|
171
|
+
}
|
|
172
|
+
this.exitListeners.add(callback);
|
|
173
|
+
return () => {
|
|
174
|
+
this.exitListeners.delete(callback);
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export interface NodePtyAdapterOptions {
|
|
180
|
+
/** Override node-pty's `spawn` for tests. */
|
|
181
|
+
spawnPty?: typeof pty.spawn;
|
|
182
|
+
/** Override `fs.statSync` for cwd validation in tests. */
|
|
183
|
+
statCwd?: (cwd: string) => Stats;
|
|
184
|
+
/** Skip the spawn-helper chmod (test isolation). */
|
|
185
|
+
skipHelperEnsure?: boolean;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export class NodePtyAdapter implements PtyAdapter {
|
|
189
|
+
readonly id = ADAPTER_ID;
|
|
190
|
+
private readonly spawnPty: typeof pty.spawn;
|
|
191
|
+
private readonly statCwd: (cwd: string) => Stats;
|
|
192
|
+
private readonly skipHelperEnsure: boolean;
|
|
193
|
+
|
|
194
|
+
constructor(options: NodePtyAdapterOptions = {}) {
|
|
195
|
+
this.spawnPty = options.spawnPty ?? pty.spawn;
|
|
196
|
+
this.statCwd = options.statCwd ?? statSync;
|
|
197
|
+
this.skipHelperEnsure = options.skipHelperEnsure ?? false;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async spawn(input: PtySpawnInput): Promise<PtyProcess> {
|
|
201
|
+
if (!this.skipHelperEnsure) ensureNodePtySpawnHelperExecutable();
|
|
202
|
+
return this.spawnSyncInternal(input);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
spawnSync(input: PtySpawnInput): PtyProcess {
|
|
206
|
+
if (!this.skipHelperEnsure) ensureNodePtySpawnHelperExecutable();
|
|
207
|
+
return this.spawnSyncInternal(input);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private spawnSyncInternal(input: PtySpawnInput): PtyProcess {
|
|
211
|
+
assertValidCwd(input.cwd, this.statCwd);
|
|
212
|
+
if (!Number.isInteger(input.cols) || input.cols <= 0) {
|
|
213
|
+
throw new PtySpawnError({
|
|
214
|
+
adapter: ADAPTER_ID,
|
|
215
|
+
code: "unknown",
|
|
216
|
+
message: `cols must be a positive integer (got ${input.cols})`,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
if (!Number.isInteger(input.rows) || input.rows <= 0) {
|
|
220
|
+
throw new PtySpawnError({
|
|
221
|
+
adapter: ADAPTER_ID,
|
|
222
|
+
code: "unknown",
|
|
223
|
+
message: `rows must be a positive integer (got ${input.rows})`,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
// node-pty wants `Record<string, string>` for env — strip undefined values.
|
|
227
|
+
const env: Record<string, string> = {};
|
|
228
|
+
for (const [key, value] of Object.entries(input.env)) {
|
|
229
|
+
if (typeof value === "string") env[key] = value;
|
|
230
|
+
}
|
|
231
|
+
let child: pty.IPty;
|
|
232
|
+
try {
|
|
233
|
+
child = this.spawnPty(input.shell, [...(input.args ?? [])], {
|
|
234
|
+
name: input.name ?? "xterm-256color",
|
|
235
|
+
cols: input.cols,
|
|
236
|
+
rows: input.rows,
|
|
237
|
+
cwd: input.cwd,
|
|
238
|
+
env,
|
|
239
|
+
encoding: input.encoding === "utf8" ? "utf8" : (null as unknown as undefined),
|
|
240
|
+
});
|
|
241
|
+
} catch (err) {
|
|
242
|
+
const errno = (err as NodeJS.ErrnoException | undefined)?.code;
|
|
243
|
+
if (errno === "ENOENT") {
|
|
244
|
+
throw new PtySpawnError({
|
|
245
|
+
adapter: ADAPTER_ID,
|
|
246
|
+
code: "shell_not_found",
|
|
247
|
+
message: `shell not found in PATH: ${input.shell}`,
|
|
248
|
+
cause: err,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
if (errno === "EACCES" || errno === "EPERM") {
|
|
252
|
+
throw new PtySpawnError({
|
|
253
|
+
adapter: ADAPTER_ID,
|
|
254
|
+
code: "permission_denied",
|
|
255
|
+
message: `permission denied spawning ${input.shell}`,
|
|
256
|
+
cause: err,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
throw new PtySpawnError({
|
|
260
|
+
adapter: ADAPTER_ID,
|
|
261
|
+
code: "unknown",
|
|
262
|
+
message: `node-pty spawn failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
263
|
+
cause: err,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
return new NodePtyProcess(child);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** Convenience singleton — most call sites just want `defaultNodePtyAdapter`. */
|
|
271
|
+
export const defaultNodePtyAdapter: PtyAdapter = new NodePtyAdapter();
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PtyAdapter — terminal PTY adapter service contract (T087).
|
|
3
|
+
*
|
|
4
|
+
* Defines the process primitives required by terminal session management
|
|
5
|
+
* without binding to a specific PTY implementation. Mirrors t3's
|
|
6
|
+
* `apps/server/src/terminal/Services/PTY.ts` shape minus the Effect runtime
|
|
7
|
+
* (Effect adoption is G14-T07 territory; we stay on plain TS Promises here).
|
|
8
|
+
*
|
|
9
|
+
* Why:
|
|
10
|
+
* - `node-pty` is a native module whose `onData` callback never fires
|
|
11
|
+
* under Bun (T085 burned half a day on the diagnosis). Routing every
|
|
12
|
+
* daemon-side PTY spawn through this interface lets us swap runtimes
|
|
13
|
+
* without touching consumers and lets tests stub the entire layer with
|
|
14
|
+
* a `MockPtyAdapter` that returns scripted output.
|
|
15
|
+
* - The Electron preload + future remote-PTY transports will eventually
|
|
16
|
+
* ship their own adapters. Keeping the seam thin makes that drop-in.
|
|
17
|
+
*
|
|
18
|
+
* Synchronous vs async:
|
|
19
|
+
* - `spawn(input): Promise<PtyProcess>` is the canonical entry point.
|
|
20
|
+
* Async because some adapters do real setup (e.g. NodePtyAdapter chmods
|
|
21
|
+
* node-pty's `spawn-helper` binary the first time, t3 does the same).
|
|
22
|
+
* - `spawnSync(input): PtyProcess` is a sibling for legacy synchronous
|
|
23
|
+
* callers (today: `PtyBridge.spawn` in `server/pty-bridge.ts`). It MUST
|
|
24
|
+
* have the same observable semantics as `spawn` once any one-shot setup
|
|
25
|
+
* has been done. Adapters that genuinely need async work should throw
|
|
26
|
+
* `PtySpawnError` ("sync_unsupported") from `spawnSync` so the caller
|
|
27
|
+
* can fall back to the async path.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
export interface PtySpawnInput {
|
|
31
|
+
/** Executable to run. Defaults are adapter-defined when omitted. */
|
|
32
|
+
shell: string;
|
|
33
|
+
/** Argv for the executable (without argv[0]). Optional; adapters may apply defaults. */
|
|
34
|
+
args?: ReadonlyArray<string>;
|
|
35
|
+
/** Working directory for the spawned process. Must exist + be a directory. */
|
|
36
|
+
cwd: string;
|
|
37
|
+
/** Initial cols. Must be a positive integer. */
|
|
38
|
+
cols: number;
|
|
39
|
+
/** Initial rows. Must be a positive integer. */
|
|
40
|
+
rows: number;
|
|
41
|
+
/** Environment vars handed to the child. */
|
|
42
|
+
env: NodeJS.ProcessEnv;
|
|
43
|
+
/**
|
|
44
|
+
* Terminal name (`$TERM`) handed to the child. Defaults to
|
|
45
|
+
* `xterm-256color` when omitted. Some adapters honour it, some pin it.
|
|
46
|
+
*/
|
|
47
|
+
name?: string;
|
|
48
|
+
/**
|
|
49
|
+
* Output encoding. `null` (default) yields raw `Buffer` data — required
|
|
50
|
+
* for byte-accurate WebSocket bridging.
|
|
51
|
+
*/
|
|
52
|
+
encoding?: "utf8" | null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface PtyExitEvent {
|
|
56
|
+
/** Process exit code. `0` is clean shutdown. */
|
|
57
|
+
exitCode: number;
|
|
58
|
+
/** Terminating signal (numeric), or `null` when the process exited cleanly. */
|
|
59
|
+
signal: number | null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Handle to a live PTY child returned by `PtyAdapter.spawn`. Modelled on
|
|
64
|
+
* t3's `PtyProcess` and `node-pty`'s `IPty` so wrapping is trivial.
|
|
65
|
+
*
|
|
66
|
+
* Listeners returned by `onData`/`onExit` are disposers — call them to
|
|
67
|
+
* detach. After `kill()` the adapter is expected to emit a single
|
|
68
|
+
* synthetic `onExit` event so the bridge layer can converge on shutdown.
|
|
69
|
+
*/
|
|
70
|
+
export interface PtyProcess {
|
|
71
|
+
/** Underlying OS PID (or a synthetic positive integer for mocks). */
|
|
72
|
+
readonly pid: number;
|
|
73
|
+
/** Write raw bytes (string is interpreted as UTF-8) into the child stdin. */
|
|
74
|
+
write(data: string | Uint8Array): void;
|
|
75
|
+
/** Resize the controlling terminal. */
|
|
76
|
+
resize(cols: number, rows: number): void;
|
|
77
|
+
/**
|
|
78
|
+
* Send a signal to the child. Defaults to `SIGTERM` so adapters that
|
|
79
|
+
* can't deliver arbitrary signals (mocks, sandboxed remotes) still
|
|
80
|
+
* honour the common case.
|
|
81
|
+
*/
|
|
82
|
+
kill(signal?: NodeJS.Signals | number): void;
|
|
83
|
+
/**
|
|
84
|
+
* Subscribe to data emissions. Returns a disposer; calling it detaches
|
|
85
|
+
* the listener without affecting any sibling subscribers.
|
|
86
|
+
*/
|
|
87
|
+
onData(callback: (data: Buffer) => void): () => void;
|
|
88
|
+
/**
|
|
89
|
+
* Subscribe to exit. Returns a disposer. Adapters MUST guarantee at
|
|
90
|
+
* most one terminal `onExit` per process; resubscribing after exit is
|
|
91
|
+
* a no-op that returns an inert disposer.
|
|
92
|
+
*/
|
|
93
|
+
onExit(callback: (event: PtyExitEvent) => void): () => void;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Discriminator codes for `PtySpawnError`. */
|
|
97
|
+
export type PtySpawnErrorCode =
|
|
98
|
+
| "cwd_invalid"
|
|
99
|
+
| "shell_not_found"
|
|
100
|
+
| "permission_denied"
|
|
101
|
+
| "sync_unsupported"
|
|
102
|
+
| "unknown";
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Typed failure for `PtyAdapter.spawn` / `spawnSync`. Adapters MUST throw
|
|
106
|
+
* (or reject with) this class so the WS bridge can translate to a
|
|
107
|
+
* structured error frame without sniffing native error message strings.
|
|
108
|
+
*/
|
|
109
|
+
export class PtySpawnError extends Error {
|
|
110
|
+
readonly adapter: string;
|
|
111
|
+
readonly code: PtySpawnErrorCode;
|
|
112
|
+
constructor(args: {
|
|
113
|
+
adapter: string;
|
|
114
|
+
code: PtySpawnErrorCode;
|
|
115
|
+
message: string;
|
|
116
|
+
cause?: unknown;
|
|
117
|
+
}) {
|
|
118
|
+
super(args.message, args.cause !== undefined ? { cause: args.cause } : undefined);
|
|
119
|
+
this.name = "PtySpawnError";
|
|
120
|
+
this.adapter = args.adapter;
|
|
121
|
+
this.code = args.code;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* The adapter contract every PTY backend must satisfy. Async is the
|
|
127
|
+
* canonical surface; `spawnSync` is provided for the synchronous callers
|
|
128
|
+
* that exist today (PtyBridge / ws-route). New code should prefer `spawn`.
|
|
129
|
+
*/
|
|
130
|
+
export interface PtyAdapter {
|
|
131
|
+
/** Human-readable adapter id, surfaced in `PtySpawnError.adapter`. */
|
|
132
|
+
readonly id: string;
|
|
133
|
+
/** Canonical async spawn. Resolves once the child PID is known. */
|
|
134
|
+
spawn(input: PtySpawnInput): Promise<PtyProcess>;
|
|
135
|
+
/**
|
|
136
|
+
* Synchronous spawn. Adapters that can't satisfy this (because they
|
|
137
|
+
* need async setup) MUST throw `PtySpawnError({ code: "sync_unsupported" })`.
|
|
138
|
+
*/
|
|
139
|
+
spawnSync(input: PtySpawnInput): PtyProcess;
|
|
140
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# `terminal/` — PTY adapter layer (T087)
|
|
2
|
+
|
|
3
|
+
The daemon's PTY surface is layered to keep `node-pty` (a native module
|
|
4
|
+
with sharp runtime edges) confined to one file. Everything above this
|
|
5
|
+
folder talks to the abstract `PtyAdapter` interface and never knows
|
|
6
|
+
whether the bytes came from a real OS PTY, a mock, or — eventually —
|
|
7
|
+
a remote transport.
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
+------------------+ +------------------------+ +-------------+
|
|
11
|
+
| server/pty-bridge | ---> | PtyAdapter (iface) | ---> | node-pty |
|
|
12
|
+
| (ring buffer, | | | | (NodePty- |
|
|
13
|
+
| replay, WS | | spawn spawnSync | | Adapter) |
|
|
14
|
+
| bridge) | | PtyProcess primitives | +-------------+
|
|
15
|
+
+------------------+ +-------------+-----------+
|
|
16
|
+
|
|
|
17
|
+
v
|
|
18
|
+
+----------------+
|
|
19
|
+
| MockPtyAdapter |
|
|
20
|
+
| (tests) |
|
|
21
|
+
+----------------+
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Files
|
|
25
|
+
|
|
26
|
+
- `PtyAdapter.ts` — the contract: `PtySpawnInput`, `PtyExitEvent`,
|
|
27
|
+
`PtyProcess`, `PtyAdapter`, and the `PtySpawnError` taxonomy.
|
|
28
|
+
- `NodePtyAdapter.ts` — concrete implementation backed by `node-pty`.
|
|
29
|
+
Ports t3's `ensureNodePtySpawnHelperExecutable` chmod-on-helper trick
|
|
30
|
+
so a fresh install always boots.
|
|
31
|
+
- `__tests__/MockPtyAdapter.ts` — scripted PTY for the test suite. Lives
|
|
32
|
+
under `__tests__/` so production bundling never picks it up.
|
|
33
|
+
- `__tests__/*.test.ts` — contract tests parameterised over every
|
|
34
|
+
adapter, plus adapter-specific units.
|
|
35
|
+
|
|
36
|
+
## Why this exists
|
|
37
|
+
|
|
38
|
+
`node-pty` is a native module compiled against an underlying runtime ABI.
|
|
39
|
+
T085 burned half a day finding out that under Bun, `node-pty`'s `onData`
|
|
40
|
+
callback never fires — the PTY spawns, the child exits, and zero bytes
|
|
41
|
+
flow back. The fix had to live in two places:
|
|
42
|
+
|
|
43
|
+
1. **Runtime pinning** (T087 PART 1): every daemon spawn site uses
|
|
44
|
+
`node`/`tsx`, never `bun`. See `daemon-watchdog.ts`,
|
|
45
|
+
`tmux-bridge/src/monitor.ts`, `src/lib/tmux.ts`. The
|
|
46
|
+
`packages/daemon/package.json` `dev`/`start` scripts mirror t3's
|
|
47
|
+
`apps/server/package.json` (`tsx --watch` in dev, `node dist/...` in
|
|
48
|
+
prod).
|
|
49
|
+
2. **Adapter abstraction** (T087 PART 2): a thin interface so the future
|
|
50
|
+
doesn't paint us into the same corner. New runtimes get their own
|
|
51
|
+
adapter; tests use `MockPtyAdapter` so we never accidentally spawn a
|
|
52
|
+
real PTY in CI.
|
|
53
|
+
|
|
54
|
+
## When to add a new adapter
|
|
55
|
+
|
|
56
|
+
Anything that produces PTY-shaped bytes is fair game. Concrete examples
|
|
57
|
+
we've considered:
|
|
58
|
+
|
|
59
|
+
- A **remote PTY** adapter that proxies over WebSocket to a daemon
|
|
60
|
+
running on another host. Drops in here without touching `pty-bridge`.
|
|
61
|
+
- A **mock SSH** adapter for integration tests that need a deterministic
|
|
62
|
+
remote shell. Built on `MockPtyAdapter` semantics.
|
|
63
|
+
- An **xterm replay** adapter that pipes a captured `.cast` file back
|
|
64
|
+
through `onData` for demos. Useful for marketing GIFs.
|
|
65
|
+
|
|
66
|
+
Each new adapter MUST satisfy `PtyAdapter.contract.test.ts` — the
|
|
67
|
+
parameterised suite runs every contract assertion against every adapter
|
|
68
|
+
we register there.
|
|
69
|
+
|
|
70
|
+
## Constraints we honour
|
|
71
|
+
|
|
72
|
+
- `spawn` is async; `spawnSync` exists for legacy callers (today:
|
|
73
|
+
`server/pty-bridge.ts`). Adapters that can't satisfy `spawnSync` throw
|
|
74
|
+
`PtySpawnError({ code: "sync_unsupported" })`.
|
|
75
|
+
- Errors are typed (`PtySpawnError` with a discriminator) so the WS
|
|
76
|
+
bridge can surface a structured error frame instead of grepping native
|
|
77
|
+
messages.
|
|
78
|
+
- `kill()` is idempotent. After it the adapter MUST guarantee at most
|
|
79
|
+
one terminal `onExit` event — subsequent `onExit(...)` subscribers
|
|
80
|
+
receive an inert disposer.
|
|
81
|
+
- No production code outside `NodePtyAdapter.ts` imports `node-pty`.
|
|
82
|
+
This is enforced by `pty-bridge.ts` now consuming `PtyAdapter` and is
|
|
83
|
+
the gate G14-T10 will tighten further.
|
|
84
|
+
|
|
85
|
+
## Roadmap notes
|
|
86
|
+
|
|
87
|
+
- **G14-T07** introduces Effect runtime. When that lands, this contract
|
|
88
|
+
grows an `Effect.Effect<PtyProcess, PtySpawnError>` shape; the plain-
|
|
89
|
+
TS interface here stays so adapters can implement either side.
|
|
90
|
+
- **G14-T10** moves chat reactor → Effect Stream. PTY bytes will flow
|
|
91
|
+
through the same Stream pipeline; today's `EventEmitter`-based
|
|
92
|
+
`pty-bridge` becomes the bottom of that Stream.
|