tmux-ide 2.9.0-beta.21 → 2.9.0-beta.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.js +46791 -43589
- package/package.json +2 -2
- package/packages/contracts/src/__tests__/daemon-wire.test.ts +2 -2
- package/packages/contracts/src/__tests__/fixtures/daemon-wire-v3.json +35 -0
- package/packages/contracts/src/__tests__/issue-error.test.ts +1 -1
- package/packages/contracts/src/__tests__/multiplexer-verbs.test.ts +3 -1
- package/packages/contracts/src/__tests__/pane-stream.test.ts +17 -5
- package/packages/contracts/src/__tests__/tmux-server-scope.test.ts +166 -0
- package/packages/contracts/src/__tests__/window-links.test.ts +243 -0
- package/packages/contracts/src/actions-contract.ts +12 -0
- package/packages/contracts/src/daemon-resource-request.test.ts +1 -1
- package/packages/contracts/src/daemon-wire.ts +10 -5
- package/packages/contracts/src/fleet-client-state.ts +3 -0
- package/packages/contracts/src/fleet-lifecycle.ts +5 -0
- package/packages/contracts/src/index.ts +2 -0
- package/packages/contracts/src/interaction-receipts.ts +39 -0
- package/packages/contracts/src/issue-error.ts +1 -1
- package/packages/contracts/src/multiplexer-verbs.ts +25 -1
- package/packages/contracts/src/pane-stream.ts +51 -8
- package/packages/contracts/src/tmux-server-scope.ts +172 -0
- package/packages/contracts/src/window-links.ts +76 -0
- package/packages/contracts/src/workspace-multiplexer.ts +54 -0
- package/packages/core/src/interaction-receipts.ts +4 -0
- package/packages/daemon/dist/command-center/actions/command-definitions.js +6 -0
- package/packages/daemon/dist/command-center/actions/handlers/workspace-multiplexer.js +2 -0
- package/packages/daemon/dist/command-center/actions/registry.js +13 -1
- package/packages/daemon/dist/command-center/discovery.js +2 -9
- package/packages/daemon/dist/command-center/server.js +21 -1
- package/packages/daemon/dist/command-center/tmux-server-native-backing.js +37 -0
- package/packages/daemon/dist/command-center/tmux-servers.js +351 -0
- package/packages/daemon/dist/lib/canonical-daemon-bootstrap.js +64 -8
- package/packages/daemon/dist/lib/canonical-daemon.js +5 -2
- package/packages/daemon/dist/lib/daemon-embed.js +198 -30
- package/packages/daemon/dist/lib/embedded-tmux-server-owners.js +147 -0
- package/packages/daemon/dist/lib/fleet-lifecycle-authority.js +31 -11
- package/packages/daemon/dist/lib/headless-daemon.js +10 -1
- package/packages/daemon/dist/lib/runtime-namespace.js +17 -3
- package/packages/daemon/dist/lib/tmux-client-execution.js +59 -0
- package/packages/daemon/dist/lib/tmux-named-socket-fence.js +4 -0
- package/packages/daemon/dist/lib/tmux-server-generation-runner.js +62 -0
- package/packages/daemon/dist/lib/tmux-server-owner.js +285 -0
- package/packages/daemon/dist/lib/tmux-server-owners.js +192 -0
- package/packages/daemon/dist/lib/tmux-server-proof.js +82 -0
- package/packages/daemon/dist/lib/tmux-server-registration.js +85 -0
- package/packages/daemon/dist/lib/tmux-server-session-create.js +43 -0
- package/packages/daemon/dist/lib/tmux-server-session-open.js +101 -0
- package/packages/daemon/dist/lib/tmux-servers-cli.js +134 -0
- package/packages/daemon/dist/lib/tmux-session-mutation-fence.js +55 -0
- package/packages/daemon/dist/lib/tmux-window-link-guard.js +88 -0
- package/packages/daemon/dist/lib/workspace-config-loader.js +3 -0
- package/packages/daemon/dist/lib/workspace-multiplexer-verbs.js +82 -1
- package/packages/daemon/dist/lib/workspace-pane-creation.js +13 -57
- package/packages/daemon/dist/lib/workspace-promotion.js +91 -12
- package/packages/daemon/dist/lib/workspace-registry.js +4 -0
- package/packages/daemon/dist/server/pane-stream-upgrade.js +20 -6
- package/packages/daemon/dist/terminal/attachments/native-runtime.js +61 -17
- package/packages/daemon/dist/terminal/mirror/__tests__/scripted-channel.js +9 -1
- package/packages/daemon/dist/terminal/mirror/__tests__/simulated-channel.js +11 -2
- package/packages/daemon/dist/terminal/mirror/control-channel.js +65 -6
- package/packages/daemon/dist/terminal/mirror/mirror-service.js +44 -0
- package/packages/daemon/dist/terminal/mirror/session-channel.js +210 -50
- package/packages/daemon/dist/terminal/mirror/window-link-authority.js +160 -0
- package/packages/daemon/dist/terminal/pane-stream/lease-manager.js +3 -3
- package/packages/daemon/dist/terminal/pane-stream/pane-stream-websocket.js +40 -6
- package/packages/daemon/dist/terminal/protocol/live-session-identity.js +9 -0
- package/packages/daemon/dist/terminal/protocol/native-backing-client.js +2 -1
- package/packages/daemon/dist/terminal/session-runtime/interaction-receipt-facts.js +9 -0
- package/packages/daemon/dist/terminal/session-runtime/registry.js +6 -0
- package/packages/daemon/dist/terminal/session-runtime/semantic-mutation-executor.js +1 -1
- package/packages/daemon/dist/terminal/session-runtime/semantic-mutation-resource-changes.js +1 -0
- package/packages/daemon/dist/tui/mirror/application-shell-daemon-connection.js +7 -2
- package/packages/daemon/dist/tui/mirror/application-shell-server-connection.js +114 -0
- package/packages/daemon/dist/tui/mirror/menu-model.js +1 -1
- package/packages/daemon/dist/tui/mirror/open-tui-verified-routing.js +34 -4
- package/packages/daemon/dist/tui/mirror/open-tui-workspace-runtime-port.js +29 -8
- package/packages/daemon/dist/tui/mirror/runtime/application-guided-tour-integration.jsx +119 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-guided-tour-owner.js +132 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-home-agent-roster.jsx +21 -9
- package/packages/daemon/dist/tui/mirror/runtime/application-home-agent-transport.js +63 -1
- package/packages/daemon/dist/tui/mirror/runtime/application-home-agents-owner.js +8 -2
- package/packages/daemon/dist/tui/mirror/runtime/application-home-agents.js +3 -1
- package/packages/daemon/dist/tui/mirror/runtime/application-home-catalog-owner.js +11 -5
- package/packages/daemon/dist/tui/mirror/runtime/application-home-catalog.js +140 -75
- package/packages/daemon/dist/tui/mirror/runtime/application-home-experience.js +40 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-home-fleet.js +132 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-machine-agents.js +9 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-machine-catalog.js +12 -2
- package/packages/daemon/dist/tui/mirror/runtime/application-machine-navigation.js +211 -31
- package/packages/daemon/dist/tui/mirror/runtime/application-machine-sidebar.jsx +91 -37
- package/packages/daemon/dist/tui/mirror/runtime/application-performance-log.js +20 -7
- package/packages/daemon/dist/tui/mirror/runtime/application-root-configuration.js +9 -3
- package/packages/daemon/dist/tui/mirror/runtime/application-route-connection.js +95 -2
- package/packages/daemon/dist/tui/mirror/runtime/application-shell-home.jsx +6 -2
- package/packages/daemon/dist/tui/mirror/runtime/application-terminal-interaction-controller.js +158 -2
- package/packages/daemon/dist/tui/mirror/runtime/application-terminal-workspace-policy.js +43 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-terminal-workspace.jsx +78 -25
- package/packages/daemon/dist/tui/mirror/runtime/application-theme-repaint.js +9 -0
- package/packages/daemon/dist/tui/mirror/runtime/fleet-lifecycle-client.js +44 -1
- package/packages/daemon/dist/tui/mirror/runtime/guided-tour-coach.jsx +54 -0
- package/packages/daemon/dist/tui/mirror/runtime/guided-tour-storage.js +67 -0
- package/packages/daemon/dist/tui/mirror/runtime/guided-tour.js +113 -0
- package/packages/daemon/dist/tui/mirror/runtime/open-tui-generation-host.js +1 -1
- package/packages/daemon/dist/tui/mirror/runtime/resize-terminal-pane.js +4 -0
- package/packages/daemon/dist/tui/mirror/runtime/select-terminal-pane.js +2 -1
- package/packages/daemon/dist/tui/mirror/runtime/semantic-shell-viewport-resize.js +12 -4
- package/packages/daemon/dist/tui/mirror/runtime/terminal-pane-input-router.js +6 -3
- package/packages/daemon/dist/tui/mirror/workspace/application-command-description.js +11 -2
- package/packages/daemon/dist/tui/mirror/workspace/terminal-window-strip.jsx +2 -1
- package/packages/daemon/src/command-center/actions/command-definitions.ts +6 -0
- package/packages/daemon/src/command-center/actions/errors.ts +4 -0
- package/packages/daemon/src/command-center/actions/handlers/workspace-multiplexer.ts +2 -0
- package/packages/daemon/src/command-center/actions/registry.ts +14 -0
- package/packages/daemon/src/command-center/discovery.ts +2 -10
- package/packages/daemon/src/command-center/server.ts +29 -1
- package/packages/daemon/src/command-center/tmux-server-native-backing.ts +49 -0
- package/packages/daemon/src/command-center/tmux-servers.ts +479 -0
- package/packages/daemon/src/lib/canonical-daemon-bootstrap.ts +111 -4
- package/packages/daemon/src/lib/canonical-daemon.ts +15 -3
- package/packages/daemon/src/lib/daemon-embed.ts +241 -28
- package/packages/daemon/src/lib/embedded-tmux-server-owners.ts +190 -0
- package/packages/daemon/src/lib/fleet-lifecycle-authority.ts +33 -9
- package/packages/daemon/src/lib/headless-daemon.ts +18 -1
- package/packages/daemon/src/lib/runtime-namespace.ts +24 -3
- package/packages/daemon/src/lib/tmux-client-execution.ts +56 -0
- package/packages/daemon/src/lib/tmux-named-socket-fence.ts +5 -0
- package/packages/daemon/src/lib/tmux-server-generation-runner.ts +81 -0
- package/packages/daemon/src/lib/tmux-server-owner.ts +336 -0
- package/packages/daemon/src/lib/tmux-server-owners.ts +228 -0
- package/packages/daemon/src/lib/tmux-server-proof.ts +95 -0
- package/packages/daemon/src/lib/tmux-server-registration.ts +105 -0
- package/packages/daemon/src/lib/tmux-server-session-create.ts +52 -0
- package/packages/daemon/src/lib/tmux-server-session-open.ts +106 -0
- package/packages/daemon/src/lib/tmux-servers-cli.ts +186 -0
- package/packages/daemon/src/lib/tmux-session-mutation-fence.ts +62 -0
- package/packages/daemon/src/lib/tmux-window-link-guard.ts +112 -0
- package/packages/daemon/src/lib/workspace-config-loader.ts +3 -0
- package/packages/daemon/src/lib/workspace-multiplexer-verbs.ts +121 -1
- package/packages/daemon/src/lib/workspace-pane-creation.ts +16 -55
- package/packages/daemon/src/lib/workspace-promotion.ts +98 -12
- package/packages/daemon/src/lib/workspace-registry.ts +5 -0
- package/packages/daemon/src/server/pane-stream-upgrade.ts +25 -5
- package/packages/daemon/src/terminal/attachments/native-runtime.ts +70 -16
- package/packages/daemon/src/terminal/mirror/control-channel.ts +64 -7
- package/packages/daemon/src/terminal/mirror/events.ts +2 -0
- package/packages/daemon/src/terminal/mirror/mirror-service.ts +56 -0
- package/packages/daemon/src/terminal/mirror/session-channel.ts +245 -50
- package/packages/daemon/src/terminal/mirror/window-link-authority.ts +203 -0
- package/packages/daemon/src/terminal/pane-stream/lease-manager.ts +3 -3
- package/packages/daemon/src/terminal/pane-stream/pane-stream-websocket.ts +50 -6
- package/packages/daemon/src/terminal/protocol/live-session-identity.ts +14 -0
- package/packages/daemon/src/terminal/protocol/native-backing-client.ts +4 -1
- package/packages/daemon/src/terminal/session-runtime/interaction-receipt-facts.ts +9 -0
- package/packages/daemon/src/terminal/session-runtime/registry.ts +9 -0
- package/packages/daemon/src/terminal/session-runtime/semantic-mutation-executor.ts +2 -2
- package/packages/daemon/src/terminal/session-runtime/semantic-mutation-resource-changes.ts +1 -0
- package/packages/daemon/src/tui/mirror/application-shell-daemon-connection.ts +28 -2
- package/packages/daemon/src/tui/mirror/application-shell-server-connection.ts +154 -0
- package/packages/daemon/src/tui/mirror/menu-model.ts +1 -1
- package/packages/daemon/src/tui/mirror/open-tui-verified-routing.ts +41 -3
- package/packages/daemon/src/tui/mirror/open-tui-workspace-runtime-port.ts +47 -6
- package/packages/daemon/src/tui/mirror/runtime/application-entry.ts +2 -1
- package/packages/daemon/src/tui/mirror/runtime/application-fleet-session-actions.tsx +10 -4
- package/packages/daemon/src/tui/mirror/runtime/application-fleet-tabs.ts +2 -0
- package/packages/daemon/src/tui/mirror/runtime/application-guided-tour-integration.tsx +199 -0
- package/packages/daemon/src/tui/mirror/runtime/application-guided-tour-owner.ts +162 -0
- package/packages/daemon/src/tui/mirror/runtime/application-home-agent-roster.tsx +36 -10
- package/packages/daemon/src/tui/mirror/runtime/application-home-agent-transport.ts +69 -0
- package/packages/daemon/src/tui/mirror/runtime/application-home-agents-owner.ts +12 -2
- package/packages/daemon/src/tui/mirror/runtime/application-home-agents.ts +13 -2
- package/packages/daemon/src/tui/mirror/runtime/application-home-catalog-owner.ts +13 -3
- package/packages/daemon/src/tui/mirror/runtime/application-home-catalog.ts +172 -100
- package/packages/daemon/src/tui/mirror/runtime/application-home-experience.ts +59 -0
- package/packages/daemon/src/tui/mirror/runtime/application-home-fleet.ts +175 -0
- package/packages/daemon/src/tui/mirror/runtime/application-machine-agents.ts +9 -1
- package/packages/daemon/src/tui/mirror/runtime/application-machine-catalog.ts +19 -7
- package/packages/daemon/src/tui/mirror/runtime/application-machine-navigation.ts +285 -36
- package/packages/daemon/src/tui/mirror/runtime/application-machine-overlays.tsx +1 -1
- package/packages/daemon/src/tui/mirror/runtime/application-machine-sidebar.tsx +124 -40
- package/packages/daemon/src/tui/mirror/runtime/application-palette-preview.tsx +3 -0
- package/packages/daemon/src/tui/mirror/runtime/application-performance-log.ts +18 -6
- package/packages/daemon/src/tui/mirror/runtime/application-root-configuration.ts +12 -3
- package/packages/daemon/src/tui/mirror/runtime/application-root-v2.tsx +40 -40
- package/packages/daemon/src/tui/mirror/runtime/application-route-connection.ts +93 -1
- package/packages/daemon/src/tui/mirror/runtime/application-shell-catalog.tsx +4 -0
- package/packages/daemon/src/tui/mirror/runtime/application-shell-home.tsx +20 -1
- package/packages/daemon/src/tui/mirror/runtime/application-shell-view.tsx +10 -0
- package/packages/daemon/src/tui/mirror/runtime/application-terminal-interaction-controller.ts +177 -2
- package/packages/daemon/src/tui/mirror/runtime/application-terminal-workspace-policy.ts +62 -0
- package/packages/daemon/src/tui/mirror/runtime/application-terminal-workspace.tsx +87 -30
- package/packages/daemon/src/tui/mirror/runtime/application-theme-repaint.ts +19 -0
- package/packages/daemon/src/tui/mirror/runtime/fleet-lifecycle-client.ts +55 -0
- package/packages/daemon/src/tui/mirror/runtime/guided-tour-coach.tsx +118 -0
- package/packages/daemon/src/tui/mirror/runtime/guided-tour-storage.ts +72 -0
- package/packages/daemon/src/tui/mirror/runtime/guided-tour.ts +147 -0
- package/packages/daemon/src/tui/mirror/runtime/open-tui-generation-host.ts +1 -1
- package/packages/daemon/src/tui/mirror/runtime/resize-terminal-pane.ts +3 -0
- package/packages/daemon/src/tui/mirror/runtime/select-terminal-pane.ts +3 -0
- package/packages/daemon/src/tui/mirror/runtime/semantic-shell-viewport-resize.ts +11 -4
- package/packages/daemon/src/tui/mirror/runtime/terminal-pane-input-router.ts +15 -4
- package/packages/daemon/src/tui/mirror/workspace/application-command-description.ts +13 -2
- package/packages/daemon/src/tui/mirror/workspace/terminal-window-strip.tsx +5 -1
- package/packages/daemon-client/package.json +3 -1
- package/packages/daemon-client/src/index.ts +2 -0
- package/packages/daemon-client/src/pane-stream-client.test.ts +108 -38
- package/packages/daemon-client/src/pane-stream-client.ts +26 -0
- package/packages/daemon-client/src/scoped-tmux-server-transport.test.ts +114 -0
- package/packages/daemon-client/src/scoped-tmux-server-transport.ts +385 -0
- package/packages/daemon-client/src/tmux-server-client.test.ts +279 -0
- package/packages/daemon-client/src/tmux-server-client.ts +287 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { tmuxServerPaneStreamPath } from "@tmux-ide/contracts";
|
|
4
|
+
import { TmuxServerOwners, MAX_TMUX_SERVER_OWNERS, } from "./tmux-server-owners.js";
|
|
5
|
+
import { createTmuxServerProbe, readTmuxServerRegistrations, writeTmuxServerRegistrations, } from "./tmux-server-registration.js";
|
|
6
|
+
import { createNativeTmuxServerOwner } from "./tmux-server-owner.js";
|
|
7
|
+
import { attachPaneStreamWebSocket, } from "../server/pane-stream-upgrade.js";
|
|
8
|
+
/** Adapts the established default owner without duplicating its control clients. */
|
|
9
|
+
export async function createEmbeddedTmuxServerOwners(options) {
|
|
10
|
+
const path = join(options.stateDirectory, "tmux-servers.json");
|
|
11
|
+
const persisted = readTmuxServerRegistrations(path);
|
|
12
|
+
const probe = createTmuxServerProbe(options.defaultAuthority.executablePath);
|
|
13
|
+
const defaultObservation = await probe(options.defaultAuthority.socketSelector);
|
|
14
|
+
let established = persisted.find((entry) => JSON.stringify(entry.selector) === JSON.stringify(options.defaultAuthority.socketSelector));
|
|
15
|
+
if (!established && defaultObservation) {
|
|
16
|
+
for (const entry of persisted) {
|
|
17
|
+
const observation = await probe(entry.selector);
|
|
18
|
+
if (observation?.fingerprint === defaultObservation.fingerprint) {
|
|
19
|
+
established = entry;
|
|
20
|
+
break;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const defaultRegistration = established ?? {
|
|
25
|
+
serverId: `tmux-server.${randomUUID().replaceAll("-", "")}`,
|
|
26
|
+
label: "Default",
|
|
27
|
+
selector: options.defaultAuthority.socketSelector,
|
|
28
|
+
};
|
|
29
|
+
let httpServer = null;
|
|
30
|
+
const boundaries = new Map();
|
|
31
|
+
const pending = new Map();
|
|
32
|
+
const isCurrent = (owner) => {
|
|
33
|
+
try {
|
|
34
|
+
return (owners.current({ serverId: owner.serverId, generation: owner.generation })
|
|
35
|
+
.paneStreamRuntime === owner.paneStreamRuntime);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
const attachOwner = (route, owner) => {
|
|
42
|
+
pending.set(route, owner);
|
|
43
|
+
if (httpServer)
|
|
44
|
+
boundaries.set(route, attachPaneStreamWebSocket(httpServer, owner.paneStreamRuntime.coordinator, route, () => isCurrent(owner)));
|
|
45
|
+
};
|
|
46
|
+
let defaultRetired = false;
|
|
47
|
+
const retireDefault = async () => {
|
|
48
|
+
if (defaultRetired)
|
|
49
|
+
return;
|
|
50
|
+
defaultRetired = true;
|
|
51
|
+
await options.defaultOwner.dispose();
|
|
52
|
+
};
|
|
53
|
+
const owners = new TmuxServerOwners({
|
|
54
|
+
probe,
|
|
55
|
+
persist: (registrations) => writeTmuxServerRegistrations(path, registrations),
|
|
56
|
+
create: async (registration, scope, observation) => {
|
|
57
|
+
if (registration.serverId === defaultRegistration.serverId)
|
|
58
|
+
await retireDefault();
|
|
59
|
+
const route = tmuxServerPaneStreamPath(scope);
|
|
60
|
+
const owner = await createNativeTmuxServerOwner({
|
|
61
|
+
...scope,
|
|
62
|
+
tmuxAuthority: observation.authority,
|
|
63
|
+
nativeServerIdentity: observation.nativeServerIdentity,
|
|
64
|
+
stateDirectory: join(options.stateDirectory, "server-owners", registration.serverId),
|
|
65
|
+
webSocketUrl: new URL(route, options.webSocketBaseUrl).href,
|
|
66
|
+
});
|
|
67
|
+
attachOwner(route, owner);
|
|
68
|
+
return {
|
|
69
|
+
...owner,
|
|
70
|
+
dispose: async () => {
|
|
71
|
+
const boundary = boundaries.get(route);
|
|
72
|
+
boundaries.delete(route);
|
|
73
|
+
pending.delete(route);
|
|
74
|
+
try {
|
|
75
|
+
await boundary?.close();
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
await owner.dispose();
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
if (defaultObservation && defaultObservation.fingerprint === options.expectedDefaultProofDigest)
|
|
85
|
+
owners.adopt(defaultRegistration, { serverId: defaultRegistration.serverId, generation: options.defaultGeneration }, defaultObservation, {
|
|
86
|
+
...options.defaultOwner,
|
|
87
|
+
serverId: defaultRegistration.serverId,
|
|
88
|
+
generation: options.defaultGeneration,
|
|
89
|
+
dispose: retireDefault,
|
|
90
|
+
});
|
|
91
|
+
try {
|
|
92
|
+
if (!defaultObservation ||
|
|
93
|
+
defaultObservation.fingerprint !== options.expectedDefaultProofDigest)
|
|
94
|
+
await owners.register(defaultRegistration);
|
|
95
|
+
for (const registration of persisted) {
|
|
96
|
+
if (registration.serverId !== defaultRegistration.serverId)
|
|
97
|
+
await owners.register(registration);
|
|
98
|
+
}
|
|
99
|
+
writeTmuxServerRegistrations(path, owners.registrations());
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
await owners.dispose();
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
const refuseUnknownScope = (request, socket) => {
|
|
106
|
+
const route = (request.url ?? "").split("?", 1)[0];
|
|
107
|
+
if (route.startsWith("/v2/tmux-servers/") && !pending.has(route))
|
|
108
|
+
socket.end("HTTP/1.1 410 Gone\r\nConnection: close\r\nContent-Length: 0\r\n\r\n");
|
|
109
|
+
};
|
|
110
|
+
return {
|
|
111
|
+
owners,
|
|
112
|
+
isDefaultCurrent() {
|
|
113
|
+
if (defaultRetired)
|
|
114
|
+
return false;
|
|
115
|
+
if (!defaultObservation ||
|
|
116
|
+
defaultObservation.fingerprint !== options.expectedDefaultProofDigest)
|
|
117
|
+
return true;
|
|
118
|
+
try {
|
|
119
|
+
owners.current({
|
|
120
|
+
serverId: defaultRegistration.serverId,
|
|
121
|
+
generation: options.defaultGeneration,
|
|
122
|
+
});
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
attach(server) {
|
|
130
|
+
if (httpServer)
|
|
131
|
+
throw new Error("Server owner transport already attached");
|
|
132
|
+
httpServer = server;
|
|
133
|
+
server.on("upgrade", refuseUnknownScope);
|
|
134
|
+
server.setMaxListeners(Math.max(server.getMaxListeners(), MAX_TMUX_SERVER_OWNERS + server.listenerCount("upgrade") + 4));
|
|
135
|
+
for (const [route, owner] of pending)
|
|
136
|
+
boundaries.set(route, attachPaneStreamWebSocket(server, owner.paneStreamRuntime.coordinator, route, () => isCurrent(owner)));
|
|
137
|
+
},
|
|
138
|
+
async dispose() {
|
|
139
|
+
httpServer?.off("upgrade", refuseUnknownScope);
|
|
140
|
+
await owners.dispose();
|
|
141
|
+
},
|
|
142
|
+
/** Legacy default routes remain bound to their original authority; never retarget. */
|
|
143
|
+
get defaultRetired() {
|
|
144
|
+
return defaultRetired;
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { liveSessionIdForNativeIdentity } from "../terminal/protocol/live-session-identity.js";
|
|
1
2
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
3
|
import { isAbsolute, resolve } from "node:path";
|
|
3
4
|
import { realpath, stat } from "node:fs/promises";
|
|
@@ -22,6 +23,7 @@ export class FleetLifecycleAuthority {
|
|
|
22
23
|
#startedAt;
|
|
23
24
|
#registry;
|
|
24
25
|
#runTmux;
|
|
26
|
+
#ensureChromeUpdater;
|
|
25
27
|
#readFleet;
|
|
26
28
|
#operations = new Map();
|
|
27
29
|
#tail = Promise.resolve();
|
|
@@ -30,6 +32,7 @@ export class FleetLifecycleAuthority {
|
|
|
30
32
|
this.#productVersion = options.productVersion;
|
|
31
33
|
this.#startedAt = options.startedAt;
|
|
32
34
|
this.#registry = options.registry;
|
|
35
|
+
this.#ensureChromeUpdater = options.ensureChromeUpdater ?? true;
|
|
33
36
|
this.#runTmux = options.runTmux;
|
|
34
37
|
this.#readFleet = options.readFleet ?? (() => readAdoptedFleet(this.#registry));
|
|
35
38
|
}
|
|
@@ -77,10 +80,14 @@ export class FleetLifecycleAuthority {
|
|
|
77
80
|
}
|
|
78
81
|
}
|
|
79
82
|
let created = false;
|
|
83
|
+
let liveSessionId;
|
|
80
84
|
try {
|
|
81
|
-
this.#runTmux([
|
|
85
|
+
const createdId = this.#runTmux([
|
|
82
86
|
"new-session",
|
|
83
87
|
"-d",
|
|
88
|
+
"-P",
|
|
89
|
+
"-F",
|
|
90
|
+
"#{pid}\t#{session_id}\t#{session_created}",
|
|
84
91
|
...TMUX_TRUECOLOR_ENVIRONMENT_ARGS,
|
|
85
92
|
"-s",
|
|
86
93
|
identity.sessionName,
|
|
@@ -90,15 +97,25 @@ export class FleetLifecycleAuthority {
|
|
|
90
97
|
cwd,
|
|
91
98
|
TMUX_TRUECOLOR_INTERACTIVE_SHELL_COMMAND,
|
|
92
99
|
]);
|
|
100
|
+
const [serverPid, sessionId, sessionCreated] = createdId.trim().split("\t");
|
|
101
|
+
if (serverPid &&
|
|
102
|
+
/^\d+$/u.test(serverPid) &&
|
|
103
|
+
sessionId &&
|
|
104
|
+
/^\$\d+$/u.test(sessionId) &&
|
|
105
|
+
sessionCreated &&
|
|
106
|
+
/^\d+$/u.test(sessionCreated))
|
|
107
|
+
liveSessionId = liveSessionIdForNativeIdentity(serverPid, sessionId, sessionCreated);
|
|
93
108
|
created = true;
|
|
94
109
|
prepareTmuxTruecolorEnvironment(this.#runTmux, identity.sessionName);
|
|
95
110
|
this.#runTmux(["set-environment", "-t", identity.sessionName, "TMUX_IDE", "1"]);
|
|
96
111
|
this.#runTmux(adoptMarkArgv(identity.sessionName));
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
112
|
+
if (this.#ensureChromeUpdater) {
|
|
113
|
+
try {
|
|
114
|
+
this.#runTmux(updaterProbeArgv());
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
this.#runTmux(updaterSpawnArgv());
|
|
118
|
+
}
|
|
102
119
|
}
|
|
103
120
|
if (!existing)
|
|
104
121
|
this.#registry.add({
|
|
@@ -125,6 +142,7 @@ export class FleetLifecycleAuthority {
|
|
|
125
142
|
operationId,
|
|
126
143
|
daemonInstanceId: this.#daemonInstanceId,
|
|
127
144
|
outcome: "created",
|
|
145
|
+
...(input.includeLiveSessionId && liveSessionId ? { liveSessionId } : {}),
|
|
128
146
|
fleetSessionId: fleetSessionIdForName(identity.sessionName),
|
|
129
147
|
workspaceName: identity.workspaceName,
|
|
130
148
|
displayName: input.displayName,
|
|
@@ -268,11 +286,13 @@ export class FleetLifecycleAuthority {
|
|
|
268
286
|
this.#runTmux(["set-option", "-p", "-t", paneId, "@agent_launch", input.command]);
|
|
269
287
|
this.#runTmux(["set-option", "-p", "-t", paneId, "@agent_hint", input.harness]);
|
|
270
288
|
this.#runTmux(["select-pane", "-t", paneId, "-T", input.displayTitle]);
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
289
|
+
if (this.#ensureChromeUpdater) {
|
|
290
|
+
try {
|
|
291
|
+
this.#runTmux(updaterProbeArgv());
|
|
292
|
+
}
|
|
293
|
+
catch {
|
|
294
|
+
this.#runTmux(updaterSpawnArgv());
|
|
295
|
+
}
|
|
276
296
|
}
|
|
277
297
|
}
|
|
278
298
|
catch (error) {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { assertCanonicalDaemonServerIntent } from "./canonical-daemon-bootstrap.js";
|
|
2
|
+
import { resolveTmuxServerIntent } from "./runtime-namespace.js";
|
|
1
3
|
import { canonicalDaemonUrl, matchesCanonicalDaemonPredecessor, getCanonicalDaemonInfoPath, prepareCanonicalDaemonInfoForBootstrap, isCanonicalDaemonAlive, isCanonicalDaemonRecordOwnerProvenDead, probeCanonicalDaemonHealth, probeCanonicalDaemonIdentity, warnOnDaemonVersionSkew, } from "./canonical-daemon.js";
|
|
2
4
|
import { DAEMON_WIRE_PROTOCOL_VERSION, isDaemonWireProtocolCompatible, } from "@tmux-ide/contracts";
|
|
3
5
|
import { startEmbeddedDaemon, } from "./daemon-embed.js";
|
|
@@ -63,12 +65,15 @@ function assertIdentityMatches(info, identity) {
|
|
|
63
65
|
}
|
|
64
66
|
}
|
|
65
67
|
async function assertAttachableDaemon(deps, info, options) {
|
|
66
|
-
const identity = await deps.probeCanonicalDaemonIdentity(info);
|
|
68
|
+
const identity = await deps.probeCanonicalDaemonIdentity(info, undefined, Boolean(options.tmuxServerIntent));
|
|
67
69
|
if (!identity) {
|
|
68
70
|
throw new IdeError(`Canonical daemon PID ${info.pid} is alive but its identity endpoint is unavailable. ` +
|
|
69
71
|
"Refusing takeover.", { code: "DAEMON_IDENTITY_UNAVAILABLE", exitCode: 1 });
|
|
70
72
|
}
|
|
71
73
|
assertIdentityMatches(info, identity);
|
|
74
|
+
await assertCanonicalDaemonServerIntent(info, options.tmuxServerIntent ?? null, {
|
|
75
|
+
identity: async () => identity,
|
|
76
|
+
});
|
|
72
77
|
const health = await deps.probeCanonicalDaemonHealth(info);
|
|
73
78
|
if (!health) {
|
|
74
79
|
throw new IdeError(`Canonical daemon PID ${info.pid} is alive but its health endpoint is unavailable. ` +
|
|
@@ -180,6 +185,10 @@ async function waitForCanonicalWinner(deps, options, timeoutMs = 15_000) {
|
|
|
180
185
|
* action converge on the same idempotent EmbeddedDaemonHandle.stop() path.
|
|
181
186
|
*/
|
|
182
187
|
export async function runHeadlessDaemon(options = {}, deps = defaultDependencies) {
|
|
188
|
+
options = {
|
|
189
|
+
...options,
|
|
190
|
+
tmuxServerIntent: options.tmuxServerIntent === undefined ? resolveTmuxServerIntent() : options.tmuxServerIntent,
|
|
191
|
+
};
|
|
183
192
|
let restoreTmuxWorkspaces = false;
|
|
184
193
|
const lifecycle = {};
|
|
185
194
|
while (true) {
|
|
@@ -212,6 +212,7 @@ export function resolveRuntimeNamespace(options = {}) {
|
|
|
212
212
|
controlSocketPath: join(runtimeDir, "control.sock"),
|
|
213
213
|
eventLogPath: join(stateHome, "events.jsonl"),
|
|
214
214
|
tmuxSocket,
|
|
215
|
+
tmuxSocketExplicit: Boolean(tmuxSocketName || tmuxSocketPath),
|
|
215
216
|
cleanupToken,
|
|
216
217
|
namespaceId: development?.id ?? (isolated ? cleanupToken : "canonical"),
|
|
217
218
|
persistence: isolated && !development ? "ephemeral" : "durable",
|
|
@@ -238,9 +239,11 @@ export function runtimeNamespaceEnvironment(namespace) {
|
|
|
238
239
|
[STATE_HOME_ENV]: namespace.stateHome,
|
|
239
240
|
[REGISTRY_DIR_ENV]: namespace.registryDir,
|
|
240
241
|
[DAEMON_INFO_DIR_ENV]: namespace.daemonInfoDir,
|
|
241
|
-
...(namespace.
|
|
242
|
-
? {
|
|
243
|
-
:
|
|
242
|
+
...(namespace.tmuxSocketExplicit === false
|
|
243
|
+
? {}
|
|
244
|
+
: namespace.tmuxSocket.kind === "name"
|
|
245
|
+
? { [TMUX_SOCKET_NAME_ENV]: namespace.tmuxSocket.name }
|
|
246
|
+
: { [TMUX_SOCKET_PATH_ENV]: namespace.tmuxSocket.path }),
|
|
244
247
|
...(namespace.cleanupToken ? { [CLEANUP_TOKEN_ENV]: namespace.cleanupToken } : {}),
|
|
245
248
|
});
|
|
246
249
|
}
|
|
@@ -294,3 +297,14 @@ export function assertQualifiedDevelopmentLaunch() {
|
|
|
294
297
|
if (resolveRuntimeNamespace().development)
|
|
295
298
|
throw new Error("Development launch requires exact build artifacts (D03); installed CLI fallback is disabled");
|
|
296
299
|
}
|
|
300
|
+
/** Caller intent differs from the default used when starting a new owner. */
|
|
301
|
+
export function resolveTmuxServerIntent(options = {}) {
|
|
302
|
+
const env = options.env ?? process.env;
|
|
303
|
+
const namespace = resolveRuntimeNamespace(options);
|
|
304
|
+
if (namespace.isolated ||
|
|
305
|
+
nonEmpty(env, TMUX_SOCKET_NAME_ENV) ||
|
|
306
|
+
nonEmpty(env, TMUX_SOCKET_PATH_ENV))
|
|
307
|
+
return { source: "explicit", selector: namespace.tmuxSocket };
|
|
308
|
+
const current = /^(.*),[0-9]+,[0-9]+$/u.exec(env.TMUX ?? "")?.[1];
|
|
309
|
+
return current ? { source: "current", selector: { kind: "path", path: current } } : null;
|
|
310
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { accessSync, constants, realpathSync, statSync } from "node:fs";
|
|
2
|
+
import { delimiter, isAbsolute, join } from "node:path";
|
|
3
|
+
import { resolveRuntimeNamespace } from "./runtime-namespace.js";
|
|
4
|
+
import { resolveBundledTmux } from "./bundled-tmux.js";
|
|
5
|
+
export function resolveTmuxExecutable() {
|
|
6
|
+
if (resolveRuntimeNamespace().development)
|
|
7
|
+
return resolveBundledTmux();
|
|
8
|
+
const configured = process.env.TMUX_IDE_TMUX_BIN;
|
|
9
|
+
if (!configured) {
|
|
10
|
+
const bundled = resolveBundledTmux();
|
|
11
|
+
if (bundled)
|
|
12
|
+
return bundled;
|
|
13
|
+
}
|
|
14
|
+
const candidates = configured
|
|
15
|
+
? [configured]
|
|
16
|
+
: (process.env.PATH ?? "")
|
|
17
|
+
.split(delimiter)
|
|
18
|
+
// Empty and relative PATH entries mean the daemon/project cwd. They
|
|
19
|
+
// are never executable authority for a privileged tmux mutation.
|
|
20
|
+
.filter((entry) => entry.length > 0 && isAbsolute(entry))
|
|
21
|
+
.map((entry) => join(entry, "tmux"));
|
|
22
|
+
for (const candidate of candidates) {
|
|
23
|
+
try {
|
|
24
|
+
if (!isAbsolute(candidate))
|
|
25
|
+
continue;
|
|
26
|
+
accessSync(candidate, constants.X_OK);
|
|
27
|
+
const canonical = realpathSync(candidate);
|
|
28
|
+
if (statSync(canonical).isFile())
|
|
29
|
+
return canonical;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// Continue to the next daemon-start candidate.
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
throw new Error("tmux_executable_unavailable");
|
|
36
|
+
}
|
|
37
|
+
const SAFE_TERMINAL_VALUE = /^(?:xterm|screen|tmux|rxvt|vt100|ansi)[A-Za-z0-9+._-]{0,58}$/u;
|
|
38
|
+
const SAFE_LOCALE_VALUE = /^[A-Za-z0-9][A-Za-z0-9_.@-]{0,127}$/u;
|
|
39
|
+
/**
|
|
40
|
+
* The pinned tmux client receives only presentation metadata. Everything else
|
|
41
|
+
* is denied by omission: PATH/HOME/SHELL, TMUX/TMUX_PANE/TMUX_TMPDIR, shell
|
|
42
|
+
* startup hooks, dynamic-loader variables, Node options, and project secrets
|
|
43
|
+
* cannot redirect authority or inject code into this daemon-owned execution.
|
|
44
|
+
*/
|
|
45
|
+
export function tmuxClientEnvironment(source) {
|
|
46
|
+
const environment = {
|
|
47
|
+
TERM: SAFE_TERMINAL_VALUE.test(source.TERM ?? "") ? source.TERM : "xterm-256color",
|
|
48
|
+
// A pinned runner may be the first tmux client and therefore create the
|
|
49
|
+
// server. Never let a headless parent (`TERM=dumb`, `NO_COLOR=1`) become
|
|
50
|
+
// the global environment inherited by every later interactive child.
|
|
51
|
+
COLORTERM: "truecolor",
|
|
52
|
+
};
|
|
53
|
+
for (const name of ["LANG", "LC_ALL", "LC_CTYPE"]) {
|
|
54
|
+
const value = source[name];
|
|
55
|
+
if (value && SAFE_LOCALE_VALUE.test(value))
|
|
56
|
+
environment[name] = value;
|
|
57
|
+
}
|
|
58
|
+
return environment;
|
|
59
|
+
}
|
|
@@ -63,3 +63,7 @@ export function createNamedSocketFence(authority, executable, environment) {
|
|
|
63
63
|
isPinned: () => shared.identity !== null,
|
|
64
64
|
};
|
|
65
65
|
}
|
|
66
|
+
/** True only before this exact authority has observed any live socket. */
|
|
67
|
+
export function isNamedSocketAuthorityUnbound(authority) {
|
|
68
|
+
return authority.socketSelector.kind === "name" && !states.get(authority)?.identity;
|
|
69
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { createPinnedWorkspaceTmuxRunner, createPinnedWorkspaceTmuxAsyncRunner, } from "./workspace-pane-creation.js";
|
|
3
|
+
import { shellEscape } from "./shell.js";
|
|
4
|
+
/** Guard each command on the server which actually accepted this connection. */
|
|
5
|
+
export function fenceNativeTmuxCommand(args, expected) {
|
|
6
|
+
const { pid, startTime } = expected;
|
|
7
|
+
if (!/^[1-9][0-9]*$/u.test(pid) || !/^[1-9][0-9]*$/u.test(startTime))
|
|
8
|
+
throw new Error("Invalid native tmux server identity");
|
|
9
|
+
if (args.length === 0 || args.some((arg) => arg.includes("\0")))
|
|
10
|
+
throw new Error("Invalid tmux command");
|
|
11
|
+
const globalFlags = [];
|
|
12
|
+
let firstCommand = 0;
|
|
13
|
+
while (args[firstCommand]?.startsWith("-")) {
|
|
14
|
+
const flag = args[firstCommand];
|
|
15
|
+
if (flag !== "-N" && flag !== "-u")
|
|
16
|
+
throw new Error("Unsupported tmux global option in generation-fenced command");
|
|
17
|
+
globalFlags.push(flag);
|
|
18
|
+
firstCommand += 1;
|
|
19
|
+
}
|
|
20
|
+
if (firstCommand === args.length)
|
|
21
|
+
throw new Error("Missing tmux command");
|
|
22
|
+
const condition = `#{&&:#{==:#{pid},${pid}},#{==:#{start_time},${startTime}}}`;
|
|
23
|
+
// Preserve arbitrary literal payloads; a standalone ; is the native argv
|
|
24
|
+
// command separator already used by the existing runners.
|
|
25
|
+
const command = args
|
|
26
|
+
.slice(firstCommand)
|
|
27
|
+
.map((arg) => (arg === ";" ? ";" : shellEscape(arg)))
|
|
28
|
+
.join(" ");
|
|
29
|
+
const refusal = `tmux-server-stale.${randomUUID()}`;
|
|
30
|
+
return {
|
|
31
|
+
argv: [...globalFlags, "if-shell", "-F", condition, command, `display-message -p '${refusal}'`],
|
|
32
|
+
verify(output) {
|
|
33
|
+
if (output.trimEnd() === refusal)
|
|
34
|
+
throw new Error("Tmux server generation changed before command execution");
|
|
35
|
+
return output;
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/** Socket validation alone leaves a validation-to-connect replacement race. */
|
|
40
|
+
export function createServerGenerationFencedTmuxRunner(authority, expected) {
|
|
41
|
+
const run = createPinnedWorkspaceTmuxRunner(authority, { timeoutMs: 5_000 });
|
|
42
|
+
const [pid, startTime] = expected
|
|
43
|
+
? [expected.pid, expected.startTime]
|
|
44
|
+
: run(["display-message", "-p", "#{pid}\t#{start_time}"]).split("\t");
|
|
45
|
+
if (!pid || !startTime)
|
|
46
|
+
throw new Error("Invalid native tmux server identity");
|
|
47
|
+
const identity = { pid, startTime };
|
|
48
|
+
// Validate once before publishing an owner; each command rechecks natively.
|
|
49
|
+
fenceNativeTmuxCommand(["display-message", "-p", ""], identity);
|
|
50
|
+
return (args) => {
|
|
51
|
+
const command = fenceNativeTmuxCommand(args, identity);
|
|
52
|
+
return command.verify(run(command.argv));
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
export function createServerGenerationFencedTmuxAsyncRunner(authority, expected) {
|
|
56
|
+
const run = createPinnedWorkspaceTmuxAsyncRunner(authority);
|
|
57
|
+
fenceNativeTmuxCommand(["display-message", "-p", ""], expected);
|
|
58
|
+
return async (args, signal) => {
|
|
59
|
+
const command = fenceNativeTmuxCommand(args, expected);
|
|
60
|
+
return command.verify(await run(command.argv, signal));
|
|
61
|
+
};
|
|
62
|
+
}
|