tmux-ide 2.9.0-beta.21 → 2.9.0-beta.22
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 +52253 -49070
- package/package.json +1 -1
- 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/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 +345 -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 +16 -10
- 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-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-machine-catalog.js +12 -2
- package/packages/daemon/dist/tui/mirror/runtime/application-machine-navigation.js +190 -31
- package/packages/daemon/dist/tui/mirror/runtime/application-machine-sidebar.jsx +91 -37
- 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-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/fleet-lifecycle-client.js +42 -1
- package/packages/daemon/dist/tui/mirror/runtime/open-tui-generation-host.js +1 -1
- package/packages/daemon/dist/tui/mirror/runtime/select-terminal-pane.js +2 -1
- 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 +468 -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 +16 -8
- 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-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-machine-catalog.ts +19 -7
- package/packages/daemon/src/tui/mirror/runtime/application-machine-navigation.ts +263 -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-root-configuration.ts +12 -3
- package/packages/daemon/src/tui/mirror/runtime/application-root-v2.tsx +24 -24
- package/packages/daemon/src/tui/mirror/runtime/application-route-connection.ts +93 -1
- package/packages/daemon/src/tui/mirror/runtime/application-shell-view.tsx +4 -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/fleet-lifecycle-client.ts +52 -0
- package/packages/daemon/src/tui/mirror/runtime/open-tui-generation-host.ts +1 -1
- package/packages/daemon/src/tui/mirror/runtime/select-terminal-pane.ts +3 -0
- 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,192 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { TmuxServerIdSchemaZ, TmuxServerRegistrationRequestSchemaZ, TmuxServerScopeSchemaZ, } from "@tmux-ide/contracts";
|
|
3
|
+
// Admission limits, not performance claims. Discovery is explicit/on-demand.
|
|
4
|
+
export const MAX_TMUX_SERVER_OWNERS = 16;
|
|
5
|
+
export const TmuxServerSelectorSchemaZ = TmuxServerRegistrationRequestSchemaZ.shape.selector;
|
|
6
|
+
export const TmuxServerRegistrationSchemaZ = TmuxServerRegistrationRequestSchemaZ.extend({
|
|
7
|
+
serverId: TmuxServerIdSchemaZ,
|
|
8
|
+
}).strict();
|
|
9
|
+
export class TmuxServerScopeError extends Error {
|
|
10
|
+
code;
|
|
11
|
+
constructor(code) {
|
|
12
|
+
super(`tmux server ${code}`);
|
|
13
|
+
this.code = code;
|
|
14
|
+
this.name = "TmuxServerScopeError";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/** One process; no shared mutable tmux authority, runtime cache or retry ledger. */
|
|
18
|
+
export class TmuxServerOwners {
|
|
19
|
+
options;
|
|
20
|
+
#entries = new Map();
|
|
21
|
+
#tail = Promise.resolve();
|
|
22
|
+
#disposed = false;
|
|
23
|
+
#disposePromise = null;
|
|
24
|
+
constructor(options) {
|
|
25
|
+
this.options = options;
|
|
26
|
+
}
|
|
27
|
+
#serial(work) {
|
|
28
|
+
const run = this.#tail.then(() => {
|
|
29
|
+
if (this.#disposed)
|
|
30
|
+
throw new TmuxServerScopeError("disposed");
|
|
31
|
+
return work();
|
|
32
|
+
});
|
|
33
|
+
this.#tail = run.catch(() => undefined);
|
|
34
|
+
return run;
|
|
35
|
+
}
|
|
36
|
+
#descriptor(entry) {
|
|
37
|
+
return entry.scope
|
|
38
|
+
? { ...entry.scope, label: entry.registration.label, state: "online" }
|
|
39
|
+
: {
|
|
40
|
+
serverId: entry.registration.serverId,
|
|
41
|
+
generation: null,
|
|
42
|
+
label: entry.registration.label,
|
|
43
|
+
state: "offline",
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
list() {
|
|
47
|
+
return [...this.#entries.values()].map((entry) => this.#descriptor(entry));
|
|
48
|
+
}
|
|
49
|
+
registrations() {
|
|
50
|
+
return [...this.#entries.values()].map((entry) => structuredClone(entry.registration));
|
|
51
|
+
}
|
|
52
|
+
#save() {
|
|
53
|
+
this.options.persist?.(this.registrations());
|
|
54
|
+
}
|
|
55
|
+
async #retire(entry) {
|
|
56
|
+
const owner = entry.owner;
|
|
57
|
+
entry.owner = null;
|
|
58
|
+
entry.scope = null;
|
|
59
|
+
entry.observation = null;
|
|
60
|
+
if (owner)
|
|
61
|
+
await owner.dispose();
|
|
62
|
+
}
|
|
63
|
+
async #refresh(entry, observed) {
|
|
64
|
+
const next = observed === undefined ? await this.options.probe(entry.registration.selector) : observed;
|
|
65
|
+
if (next && entry.observation?.fingerprint === next.fingerprint && entry.observation.valid())
|
|
66
|
+
return;
|
|
67
|
+
// Absence revokes command/stream leases without claiming native server death.
|
|
68
|
+
// Socket recreation also needs a fresh connection authority, even if PID survived.
|
|
69
|
+
await this.#retire(entry);
|
|
70
|
+
if (this.#disposed || !next || !next.valid())
|
|
71
|
+
return;
|
|
72
|
+
const duplicate = [...this.#entries.values()].find((other) => other !== entry &&
|
|
73
|
+
other.observation?.fingerprint === next.fingerprint &&
|
|
74
|
+
other.observation.valid());
|
|
75
|
+
if (duplicate)
|
|
76
|
+
return; // Never construct a second owner for a proven live alias.
|
|
77
|
+
const scope = { serverId: entry.registration.serverId, generation: randomUUID() };
|
|
78
|
+
let owner;
|
|
79
|
+
try {
|
|
80
|
+
owner = await this.options.create(entry.registration, scope, next);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return;
|
|
84
|
+
} // Registration remains visible/offline; explicit refresh retries.
|
|
85
|
+
if (this.#disposed || !next.valid()) {
|
|
86
|
+
await owner.dispose();
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
entry.observation = next;
|
|
90
|
+
entry.scope = scope;
|
|
91
|
+
entry.owner = owner;
|
|
92
|
+
}
|
|
93
|
+
register(input) {
|
|
94
|
+
return this.#serial(async () => {
|
|
95
|
+
const registration = TmuxServerRegistrationSchemaZ.parse({
|
|
96
|
+
...input,
|
|
97
|
+
serverId: input.serverId ?? `tmux-server.${randomUUID().replaceAll("-", "")}`,
|
|
98
|
+
});
|
|
99
|
+
const sameSelector = [...this.#entries.values()].find((entry) => JSON.stringify(entry.registration.selector) === JSON.stringify(registration.selector));
|
|
100
|
+
if (sameSelector) {
|
|
101
|
+
await this.#refresh(sameSelector);
|
|
102
|
+
return this.#descriptor(sameSelector);
|
|
103
|
+
}
|
|
104
|
+
if (this.#entries.has(registration.serverId))
|
|
105
|
+
throw new TypeError("Server registration already exists");
|
|
106
|
+
const observation = await this.options.probe(registration.selector);
|
|
107
|
+
if (this.#disposed)
|
|
108
|
+
throw new TmuxServerScopeError("disposed");
|
|
109
|
+
const alias = observation &&
|
|
110
|
+
[...this.#entries.values()].find((entry) => entry.observation?.fingerprint === observation.fingerprint && entry.observation.valid());
|
|
111
|
+
if (alias)
|
|
112
|
+
return this.#descriptor(alias);
|
|
113
|
+
if (this.#entries.size >= MAX_TMUX_SERVER_OWNERS)
|
|
114
|
+
throw new TmuxServerScopeError("capacity");
|
|
115
|
+
const entry = { registration, observation: null, scope: null, owner: null };
|
|
116
|
+
this.#entries.set(registration.serverId, entry);
|
|
117
|
+
try {
|
|
118
|
+
this.#save();
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
this.#entries.delete(registration.serverId);
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
await this.#refresh(entry, observation);
|
|
125
|
+
return this.#descriptor(entry);
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
/** Adopt the existing default owner rather than creating a duplicate mirror. */
|
|
129
|
+
adopt(registrationInput, scopeInput, observation, owner) {
|
|
130
|
+
const registration = TmuxServerRegistrationSchemaZ.parse(registrationInput);
|
|
131
|
+
const scope = TmuxServerScopeSchemaZ.parse(scopeInput);
|
|
132
|
+
if (scope.serverId !== registration.serverId || this.#entries.size || this.#disposed)
|
|
133
|
+
throw new TypeError("Invalid default owner adoption");
|
|
134
|
+
this.#entries.set(scope.serverId, { registration, scope, observation, owner });
|
|
135
|
+
}
|
|
136
|
+
refresh() {
|
|
137
|
+
return this.#serial(async () => {
|
|
138
|
+
// Sequential admission bounds subprocess work and avoids alias races.
|
|
139
|
+
for (const entry of this.#entries.values())
|
|
140
|
+
await this.#refresh(entry);
|
|
141
|
+
return this.list();
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
current(scopeInput) {
|
|
145
|
+
const scope = TmuxServerScopeSchemaZ.parse(scopeInput);
|
|
146
|
+
if (this.#disposed)
|
|
147
|
+
throw new TmuxServerScopeError("disposed");
|
|
148
|
+
const entry = this.#entries.get(scope.serverId);
|
|
149
|
+
if (!entry)
|
|
150
|
+
throw new TmuxServerScopeError("not-found");
|
|
151
|
+
if (!entry.scope || !entry.owner)
|
|
152
|
+
throw new TmuxServerScopeError("offline");
|
|
153
|
+
if (entry.scope.generation !== scope.generation || !entry.observation?.valid())
|
|
154
|
+
throw new TmuxServerScopeError("stale-generation");
|
|
155
|
+
return entry.owner;
|
|
156
|
+
}
|
|
157
|
+
async withOwner(scope, work) {
|
|
158
|
+
const owner = this.current(scope);
|
|
159
|
+
const result = await work(owner);
|
|
160
|
+
if (this.current(scope) !== owner)
|
|
161
|
+
throw new TmuxServerScopeError("stale-generation");
|
|
162
|
+
return result;
|
|
163
|
+
}
|
|
164
|
+
remove(serverId) {
|
|
165
|
+
return this.#serial(async () => {
|
|
166
|
+
const entry = this.#entries.get(TmuxServerIdSchemaZ.parse(serverId));
|
|
167
|
+
if (!entry)
|
|
168
|
+
throw new TmuxServerScopeError("not-found");
|
|
169
|
+
this.#entries.delete(serverId);
|
|
170
|
+
try {
|
|
171
|
+
this.#save();
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
this.#entries.set(serverId, entry);
|
|
175
|
+
throw error;
|
|
176
|
+
}
|
|
177
|
+
await this.#retire(entry); // never kill-server or kill-session
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
dispose() {
|
|
181
|
+
if (this.#disposePromise)
|
|
182
|
+
return this.#disposePromise;
|
|
183
|
+
this.#disposed = true;
|
|
184
|
+
this.#disposePromise = (async () => {
|
|
185
|
+
await this.#tail;
|
|
186
|
+
const entries = [...this.#entries.values()];
|
|
187
|
+
this.#entries.clear();
|
|
188
|
+
await Promise.allSettled(entries.map((entry) => this.#retire(entry)));
|
|
189
|
+
})();
|
|
190
|
+
return this.#disposePromise;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { realpathSync, statSync } from "node:fs";
|
|
2
|
+
import { isNamedSocketAuthorityUnbound } from "./tmux-named-socket-fence.js";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { captureUnixSocketIdentity, revalidateUnixSocketIdentity, } from "./unix-socket-authority.js";
|
|
5
|
+
/** Two read-only, no-start queries fence the socket and server incarnation. */
|
|
6
|
+
export function captureTmuxServerProof(run) {
|
|
7
|
+
const args = ["-N", "display-message", "-p", "#{socket_path}|#{pid}|#{start_time}"];
|
|
8
|
+
try {
|
|
9
|
+
const raw = run(args).trimEnd();
|
|
10
|
+
const match = /^(.*)\|([1-9][0-9]*)\|([1-9][0-9]*)$/u.exec(raw);
|
|
11
|
+
if (!match || raw.length > 8192)
|
|
12
|
+
return null;
|
|
13
|
+
const socket = captureUnixSocketIdentity(match[1]);
|
|
14
|
+
if (run(args).trimEnd() !== raw)
|
|
15
|
+
return null;
|
|
16
|
+
revalidateUnixSocketIdentity(socket);
|
|
17
|
+
return {
|
|
18
|
+
version: 1,
|
|
19
|
+
kind: "live",
|
|
20
|
+
digest: createHash("sha256")
|
|
21
|
+
.update(JSON.stringify([socket.path, match[2], match[3]]))
|
|
22
|
+
.digest("hex"),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export async function captureTmuxServerProofAsync(run) {
|
|
30
|
+
const args = ["-N", "display-message", "-p", "#{socket_path}|#{pid}|#{start_time}"];
|
|
31
|
+
try {
|
|
32
|
+
const raw = (await run(args)).trimEnd();
|
|
33
|
+
const match = /^(.*)\|([1-9][0-9]*)\|([1-9][0-9]*)$/u.exec(raw);
|
|
34
|
+
if (!match || raw.length > 8192)
|
|
35
|
+
return null;
|
|
36
|
+
const socket = captureUnixSocketIdentity(match[1]);
|
|
37
|
+
if ((await run(args)).trimEnd() !== raw)
|
|
38
|
+
return null;
|
|
39
|
+
revalidateUnixSocketIdentity(socket);
|
|
40
|
+
return {
|
|
41
|
+
version: 1,
|
|
42
|
+
kind: "live",
|
|
43
|
+
digest: createHash("sha256")
|
|
44
|
+
.update(JSON.stringify([socket.path, match[2], match[3]]))
|
|
45
|
+
.digest("hex"),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/** A selector commitment is not evidence of an absent or live server. */
|
|
53
|
+
export function captureUnboundTmuxSelectorProof(authority) {
|
|
54
|
+
if (!isNamedSocketAuthorityUnbound(authority) || authority.socketSelector.kind !== "name")
|
|
55
|
+
return null;
|
|
56
|
+
try {
|
|
57
|
+
const executable = realpathSync(authority.executablePath);
|
|
58
|
+
const stat = statSync(executable, { bigint: true });
|
|
59
|
+
const uid = process.getuid?.();
|
|
60
|
+
if (uid === undefined || !stat.isFile())
|
|
61
|
+
return null;
|
|
62
|
+
return {
|
|
63
|
+
version: 1,
|
|
64
|
+
kind: "unbound-name",
|
|
65
|
+
digest: createHash("sha256")
|
|
66
|
+
.update(JSON.stringify([
|
|
67
|
+
"sanitized-tmux-environment-v1",
|
|
68
|
+
executable,
|
|
69
|
+
String(stat.dev),
|
|
70
|
+
String(stat.ino),
|
|
71
|
+
String(stat.mtimeNs),
|
|
72
|
+
String(stat.size),
|
|
73
|
+
uid,
|
|
74
|
+
authority.socketSelector.name,
|
|
75
|
+
]))
|
|
76
|
+
.digest("hex"),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
4
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync, realpathSync, unlinkSync, } from "node:fs";
|
|
5
|
+
import { dirname } from "node:path";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { tmuxClientEnvironment } from "./tmux-client-execution.js";
|
|
8
|
+
import { captureUnixSocketIdentity, revalidateUnixSocketIdentity, } from "./unix-socket-authority.js";
|
|
9
|
+
import { MAX_TMUX_SERVER_OWNERS, TmuxServerRegistrationSchemaZ, } from "./tmux-server-owners.js";
|
|
10
|
+
const exec = promisify(execFile);
|
|
11
|
+
const FileSchema = z
|
|
12
|
+
.object({
|
|
13
|
+
version: z.literal(1),
|
|
14
|
+
servers: z.array(TmuxServerRegistrationSchemaZ).max(MAX_TMUX_SERVER_OWNERS),
|
|
15
|
+
})
|
|
16
|
+
.strict()
|
|
17
|
+
.superRefine((file, ctx) => {
|
|
18
|
+
if (new Set(file.servers.map((server) => server.serverId)).size !== file.servers.length)
|
|
19
|
+
ctx.addIssue({ code: "custom", message: "Duplicate server registration" });
|
|
20
|
+
});
|
|
21
|
+
export function readTmuxServerRegistrations(path) {
|
|
22
|
+
try {
|
|
23
|
+
return FileSchema.parse(JSON.parse(readFileSync(path, "utf8"))).servers;
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
if (error.code === "ENOENT")
|
|
27
|
+
return [];
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export function writeTmuxServerRegistrations(path, registrations) {
|
|
32
|
+
const file = FileSchema.parse({ version: 1, servers: registrations });
|
|
33
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
34
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
35
|
+
try {
|
|
36
|
+
writeFileSync(temporary, `${JSON.stringify(file, null, 2)}\n`, { mode: 0o600, flag: "wx" });
|
|
37
|
+
renameSync(temporary, path);
|
|
38
|
+
}
|
|
39
|
+
finally {
|
|
40
|
+
try {
|
|
41
|
+
unlinkSync(temporary);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
/* rename already removed it */
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/** Discovery never creates a server, loads configuration, or attaches a client. */
|
|
49
|
+
export function createTmuxServerProbe(executable) {
|
|
50
|
+
const executablePath = realpathSync(executable);
|
|
51
|
+
const env = tmuxClientEnvironment(process.env);
|
|
52
|
+
return async (selector) => {
|
|
53
|
+
const args = selector.kind === "path" ? ["-S", selector.path] : ["-L", selector.name];
|
|
54
|
+
const read = async (selection) => (await exec(executablePath, [...selection, "-N", "display-message", "-p", "#{socket_path}|#{pid}|#{start_time}"], { env, encoding: "utf8", timeout: 500, maxBuffer: 8192 })).stdout.trimEnd();
|
|
55
|
+
try {
|
|
56
|
+
const first = await read(args);
|
|
57
|
+
const match = /^(.*)\|([1-9][0-9]*)\|([1-9][0-9]*)$/u.exec(first);
|
|
58
|
+
if (!match)
|
|
59
|
+
return null;
|
|
60
|
+
const socket = captureUnixSocketIdentity(match[1]);
|
|
61
|
+
if ((await read(["-S", socket.path])) !== first)
|
|
62
|
+
return null;
|
|
63
|
+
revalidateUnixSocketIdentity(socket);
|
|
64
|
+
return {
|
|
65
|
+
nativeServerIdentity: { pid: match[2], startTime: match[3] },
|
|
66
|
+
fingerprint: createHash("sha256")
|
|
67
|
+
.update(JSON.stringify([socket.path, match[2], match[3]]))
|
|
68
|
+
.digest("hex"),
|
|
69
|
+
authority: { executablePath, socketSelector: { kind: "path", path: socket.path } },
|
|
70
|
+
valid: () => {
|
|
71
|
+
try {
|
|
72
|
+
revalidateUnixSocketIdentity(socket);
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { WorkspaceSessionCreateArgumentsSchemaZ, } from "@tmux-ide/contracts";
|
|
2
|
+
import { FleetLifecycleAuthority } from "./fleet-lifecycle-authority.js";
|
|
3
|
+
/** Bounded online-server creation. The existing lifecycle owns idempotency and workspace naming. */
|
|
4
|
+
export function createNativeTmuxSessionCreator(options) {
|
|
5
|
+
let disposed = false;
|
|
6
|
+
const pending = new Set();
|
|
7
|
+
const assertOpen = () => {
|
|
8
|
+
options.assertOpen();
|
|
9
|
+
if (disposed)
|
|
10
|
+
throw new Error("Session creation is retired");
|
|
11
|
+
};
|
|
12
|
+
const authority = new FleetLifecycleAuthority({
|
|
13
|
+
daemonInstanceId: options.generation,
|
|
14
|
+
productVersion: "scoped-owner",
|
|
15
|
+
startedAt: new Date().toISOString(),
|
|
16
|
+
registry: options.registry,
|
|
17
|
+
runTmux(args) {
|
|
18
|
+
assertOpen();
|
|
19
|
+
return options.run(args);
|
|
20
|
+
},
|
|
21
|
+
readFleet: () => [],
|
|
22
|
+
ensureChromeUpdater: false,
|
|
23
|
+
});
|
|
24
|
+
return {
|
|
25
|
+
createSession(operationId, input) {
|
|
26
|
+
assertOpen();
|
|
27
|
+
const parsed = WorkspaceSessionCreateArgumentsSchemaZ.parse(input);
|
|
28
|
+
if (parsed.expectedDaemonInstanceId && parsed.expectedDaemonInstanceId !== options.generation)
|
|
29
|
+
throw new Error("Selected tmux server generation changed");
|
|
30
|
+
if (pending.size >= 16)
|
|
31
|
+
throw new Error("Session creation capacity reached");
|
|
32
|
+
const operation = authority
|
|
33
|
+
.createSession(operationId, options.generation, parsed)
|
|
34
|
+
.finally(() => pending.delete(operation));
|
|
35
|
+
pending.add(operation);
|
|
36
|
+
return operation;
|
|
37
|
+
},
|
|
38
|
+
async dispose() {
|
|
39
|
+
disposed = true;
|
|
40
|
+
await Promise.allSettled([...pending]);
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { WorkspaceCatalogLiveSessionIdSchemaZ } from "@tmux-ide/contracts";
|
|
3
|
+
import { liveSessionIdForNativeIdentity } from "../terminal/protocol/live-session-identity.js";
|
|
4
|
+
import { fleetSessionIdForName } from "../command-center/resources/fleet-catalog.js";
|
|
5
|
+
import { isVisibleFleetSession } from "../command-center/discovery.js";
|
|
6
|
+
import { WorkspacePromotionAuthority } from "./workspace-promotion.js";
|
|
7
|
+
import { shellEscape } from "./shell.js";
|
|
8
|
+
/** Owner-local bounded admission; all native IO must already fence the server generation. */
|
|
9
|
+
export function createNativeTmuxSessionOpener(options) {
|
|
10
|
+
let disposed = false;
|
|
11
|
+
let tail = Promise.resolve();
|
|
12
|
+
const pending = new Map();
|
|
13
|
+
const assertOpen = () => {
|
|
14
|
+
options.assertOpen();
|
|
15
|
+
if (disposed)
|
|
16
|
+
throw new Error("Session admission is retired");
|
|
17
|
+
};
|
|
18
|
+
const open = async (liveSessionId) => {
|
|
19
|
+
assertOpen();
|
|
20
|
+
const records = await options.run([
|
|
21
|
+
"list-sessions",
|
|
22
|
+
"-F",
|
|
23
|
+
"#{pid}\t#{session_id}\t#{session_created}\t#{session_name}",
|
|
24
|
+
]);
|
|
25
|
+
const matches = records
|
|
26
|
+
.trim()
|
|
27
|
+
.split("\n")
|
|
28
|
+
.map((line) => line.split("\t"))
|
|
29
|
+
.filter(([pid, id, created, name]) => pid &&
|
|
30
|
+
/^\d+$/.test(pid) &&
|
|
31
|
+
id &&
|
|
32
|
+
/^\$\d+$/.test(id) &&
|
|
33
|
+
created &&
|
|
34
|
+
/^\d+$/.test(created) &&
|
|
35
|
+
name &&
|
|
36
|
+
isVisibleFleetSession(name) &&
|
|
37
|
+
liveSessionIdForNativeIdentity(pid, id, created) === liveSessionId);
|
|
38
|
+
if (matches.length !== 1)
|
|
39
|
+
throw new Error("Selected live tmux session is no longer available");
|
|
40
|
+
const [, nativeSessionId, created, name] = matches[0];
|
|
41
|
+
// Each command checks the exact native session on the server accepting it.
|
|
42
|
+
// A name-only validation followed by stamping would cross a session replacement.
|
|
43
|
+
const run = async (args) => {
|
|
44
|
+
assertOpen();
|
|
45
|
+
const sentinel = `tmux-session-stale.${randomUUID()}`;
|
|
46
|
+
const condition = `#{&&:#{==:#{session_id},${nativeSessionId}},#{==:#{session_created},${created}}}`;
|
|
47
|
+
const command = args.map((arg) => (arg === ";" ? ";" : shellEscape(arg))).join(" ");
|
|
48
|
+
const output = await options.run([
|
|
49
|
+
"if-shell",
|
|
50
|
+
"-t",
|
|
51
|
+
nativeSessionId,
|
|
52
|
+
"-F",
|
|
53
|
+
condition,
|
|
54
|
+
command,
|
|
55
|
+
`display-message -p '${sentinel}'`,
|
|
56
|
+
]);
|
|
57
|
+
assertOpen();
|
|
58
|
+
if (output.trim() === sentinel)
|
|
59
|
+
throw new Error("Selected live tmux session changed during admission");
|
|
60
|
+
return output;
|
|
61
|
+
};
|
|
62
|
+
const promotion = new WorkspacePromotionAuthority({
|
|
63
|
+
daemonInstanceId: options.generation,
|
|
64
|
+
registry: options.registry,
|
|
65
|
+
io: { runTmux: run },
|
|
66
|
+
});
|
|
67
|
+
try {
|
|
68
|
+
const result = await promotion.promote({
|
|
69
|
+
operationId: randomUUID(),
|
|
70
|
+
expectedDaemonInstanceId: options.generation,
|
|
71
|
+
intent: { sessionId: fleetSessionIdForName(name) },
|
|
72
|
+
});
|
|
73
|
+
await run(["display-message", "-p", "-t", nativeSessionId, "#{session_id}"]);
|
|
74
|
+
return { workspaceName: result.resource.workspaceName, liveSessionId };
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
await promotion.dispose();
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
return {
|
|
81
|
+
openSession(liveSessionId) {
|
|
82
|
+
WorkspaceCatalogLiveSessionIdSchemaZ.parse(liveSessionId);
|
|
83
|
+
assertOpen();
|
|
84
|
+
const previous = pending.get(liveSessionId);
|
|
85
|
+
if (previous)
|
|
86
|
+
return previous;
|
|
87
|
+
if (pending.size >= 16)
|
|
88
|
+
throw new Error("Session admission capacity reached");
|
|
89
|
+
const operation = tail
|
|
90
|
+
.then(() => open(liveSessionId))
|
|
91
|
+
.finally(() => pending.delete(liveSessionId));
|
|
92
|
+
pending.set(liveSessionId, operation);
|
|
93
|
+
tail = operation.catch(() => { });
|
|
94
|
+
return operation;
|
|
95
|
+
},
|
|
96
|
+
async dispose() {
|
|
97
|
+
disposed = true;
|
|
98
|
+
await tail;
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { SavedMachineSchema, TmuxServerIdSchemaZ, TmuxServerRegistrationRequestSchemaZ, WorkspaceSessionCreateArgumentsSchemaZ, } from "@tmux-ide/contracts";
|
|
3
|
+
import { createTmuxServerClient, listTmuxServers, registerTmuxServer, removeTmuxServer, TmuxServerClientError, } from "@tmux-ide/daemon-client/tmux-server-client";
|
|
4
|
+
import { canonicalDaemonUrl } from "./canonical-daemon.js";
|
|
5
|
+
import { ensureCanonicalDaemon } from "./canonical-daemon-bootstrap.js";
|
|
6
|
+
import { openSshDaemonTransport } from "./ssh-daemon-transport.js";
|
|
7
|
+
import { loadSavedMachines } from "./saved-machines.js";
|
|
8
|
+
/** Parse before acquiring a transport; invalid input cannot start a daemon. */
|
|
9
|
+
export function parseTmuxServersOperation(options) {
|
|
10
|
+
const command = options.command ?? "list";
|
|
11
|
+
if (options.ssh !== undefined)
|
|
12
|
+
SavedMachineSchema.shape.sshTarget.parse(options.ssh);
|
|
13
|
+
if (command === "create") {
|
|
14
|
+
if (options.socketName !== undefined ||
|
|
15
|
+
options.socketPath !== undefined ||
|
|
16
|
+
options.label !== undefined)
|
|
17
|
+
throw new Error("servers create requires a registered server ID and --session-name NAME");
|
|
18
|
+
return {
|
|
19
|
+
command,
|
|
20
|
+
serverId: TmuxServerIdSchemaZ.parse(options.serverId),
|
|
21
|
+
input: WorkspaceSessionCreateArgumentsSchemaZ.parse({
|
|
22
|
+
displayName: options.sessionName,
|
|
23
|
+
...(options.cwd ? { cwd: options.cwd } : {}),
|
|
24
|
+
}),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
if (options.sessionName !== undefined || options.cwd !== undefined)
|
|
28
|
+
throw new Error("--session-name and --dir are only supported by servers create");
|
|
29
|
+
if (command === "add") {
|
|
30
|
+
if (options.serverId ||
|
|
31
|
+
(options.socketName === undefined) === (options.socketPath === undefined))
|
|
32
|
+
throw new Error("servers add requires exactly one of --socket-name NAME or --socket-path /PATH");
|
|
33
|
+
return {
|
|
34
|
+
command,
|
|
35
|
+
registration: TmuxServerRegistrationRequestSchemaZ.parse({
|
|
36
|
+
label: options.label ?? options.socketName ?? "Custom server",
|
|
37
|
+
selector: options.socketName !== undefined
|
|
38
|
+
? { kind: "name", name: options.socketName }
|
|
39
|
+
: { kind: "path", path: options.socketPath },
|
|
40
|
+
}),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
if (options.socketName !== undefined ||
|
|
44
|
+
options.socketPath !== undefined ||
|
|
45
|
+
options.label !== undefined)
|
|
46
|
+
throw new Error("Socket selectors and --name are only supported by servers add");
|
|
47
|
+
if (command === "list" && options.serverId === undefined)
|
|
48
|
+
return { command };
|
|
49
|
+
if (command === "remove" || command === "sessions")
|
|
50
|
+
return { command, serverId: TmuxServerIdSchemaZ.parse(options.serverId) };
|
|
51
|
+
throw new Error("Usage: tmux-ide servers list|add|remove <server-id>|sessions <server-id>|create <server-id> --session-name NAME [--ssh HOST] [--json]");
|
|
52
|
+
}
|
|
53
|
+
export async function connectTmuxServersCli(options) {
|
|
54
|
+
const hostClientId = randomUUID();
|
|
55
|
+
if (options.ssh !== undefined) {
|
|
56
|
+
const transport = await openSshDaemonTransport({ alias: options.ssh });
|
|
57
|
+
try {
|
|
58
|
+
const expected = loadSavedMachines().machines.find((machine) => machine.sshTarget === options.ssh)?.expectedEnvironmentId;
|
|
59
|
+
if (expected && transport.daemon.environmentId !== expected)
|
|
60
|
+
throw new Error("SSH route reached a different environment than the saved profile");
|
|
61
|
+
return {
|
|
62
|
+
client: {
|
|
63
|
+
baseUrl: transport.baseUrl,
|
|
64
|
+
ownerToken: transport.daemon.authToken,
|
|
65
|
+
hostClientId,
|
|
66
|
+
origin: transport.baseUrl,
|
|
67
|
+
},
|
|
68
|
+
dispose: () => transport.dispose(),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
transport.dispose();
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// Server registration addresses the daemon, not the invoking shell's tmux server.
|
|
77
|
+
const { candidate } = await ensureCanonicalDaemon({
|
|
78
|
+
entryPath: options.entryPath,
|
|
79
|
+
expectedProductVersion: options.expectedProductVersion,
|
|
80
|
+
tmuxServerIntent: null,
|
|
81
|
+
});
|
|
82
|
+
if (!candidate.authToken)
|
|
83
|
+
throw new Error("Daemon owner authority is unavailable; update the local daemon");
|
|
84
|
+
const baseUrl = canonicalDaemonUrl("http", candidate.bindHostname, candidate.port);
|
|
85
|
+
return {
|
|
86
|
+
client: { baseUrl, ownerToken: candidate.authToken, hostClientId, origin: baseUrl },
|
|
87
|
+
dispose() { },
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
export async function runTmuxServersCli(options, connect) {
|
|
91
|
+
const operation = parseTmuxServersOperation(options);
|
|
92
|
+
const connection = await connect();
|
|
93
|
+
try {
|
|
94
|
+
// Probe the versioned resource before writes. An older daemon is unsupported,
|
|
95
|
+
// never a reason to execute a raw command against a default server.
|
|
96
|
+
let catalog;
|
|
97
|
+
try {
|
|
98
|
+
catalog = await listTmuxServers(connection.client);
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
if (error instanceof TmuxServerClientError && [404, 405, 426].includes(error.status ?? 0))
|
|
102
|
+
throw new Error("This daemon does not support tmux server selection; update the selected machine's daemon", { cause: error });
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
if (operation.command === "list")
|
|
106
|
+
return catalog;
|
|
107
|
+
if (operation.command === "add")
|
|
108
|
+
return await registerTmuxServer(connection.client, operation.registration);
|
|
109
|
+
const server = catalog.servers.find((candidate) => candidate.serverId === operation.serverId);
|
|
110
|
+
if (!server)
|
|
111
|
+
throw new Error("Selected tmux server is not registered; run tmux-ide servers list");
|
|
112
|
+
if (operation.command === "remove") {
|
|
113
|
+
await removeTmuxServer(connection.client, server.serverId);
|
|
114
|
+
return { removed: true, serverId: server.serverId };
|
|
115
|
+
}
|
|
116
|
+
if (server.state !== "online")
|
|
117
|
+
throw new Error("Selected tmux server is offline; no session operation was attempted");
|
|
118
|
+
const client = createTmuxServerClient(connection.client, {
|
|
119
|
+
serverId: server.serverId,
|
|
120
|
+
generation: server.generation,
|
|
121
|
+
});
|
|
122
|
+
try {
|
|
123
|
+
return operation.command === "create"
|
|
124
|
+
? await client.createSession(randomUUID(), operation.input)
|
|
125
|
+
: await client.sessions();
|
|
126
|
+
}
|
|
127
|
+
finally {
|
|
128
|
+
client.dispose();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
finally {
|
|
132
|
+
connection.dispose();
|
|
133
|
+
}
|
|
134
|
+
}
|