tmux-ide 2.9.2 → 2.9.4
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/LICENSE +1 -1
- package/README.md +121 -140
- package/bin/cli.js +2457 -727
- package/package.json +23 -8
- package/packages/contracts/src/desktop-host.ts +3 -0
- package/packages/contracts/src/saved-machines.ts +19 -0
- package/packages/contracts/src/terminal-replica.ts +1 -1
- package/packages/core/package.json +3 -2
- package/packages/core/src/terminal-delivery.ts +51 -3
- package/packages/core/src/terminal-fnv64-wasm.ts +4 -3
- package/packages/core/src/terminal-replica-hash-cache.ts +129 -31
- package/packages/core/src/terminal-replica-owned-row.ts +27 -0
- package/packages/core/src/terminal-replica.ts +13 -28
- package/packages/daemon/dist/command-center/daemon-fleet-facts-observer.js +96 -10
- package/packages/daemon/dist/command-center/resources/saved-machine-route.js +40 -1
- package/packages/daemon/dist/command-center/ws-events.js +5 -24
- package/packages/daemon/dist/lib/canonical-daemon-bootstrap.js +8 -0
- package/packages/daemon/dist/lib/canonical-daemon.js +28 -2
- package/packages/daemon/dist/lib/daemon-embed.js +1 -1
- package/packages/daemon/dist/lib/daemon-service-manager.js +122 -0
- package/packages/daemon/dist/lib/daemon-service-plan.js +127 -0
- package/packages/daemon/dist/lib/daemon-service.js +259 -0
- package/packages/daemon/dist/lib/install-origin.js +2 -0
- package/packages/daemon/dist/lib/local-fleet-request.js +9 -3
- package/packages/daemon/dist/lib/log.js +6 -10
- package/packages/daemon/dist/lib/managed-installer-update.js +67 -0
- package/packages/daemon/dist/lib/{tui-download-lock.js → private-operation-lock.js} +11 -11
- package/packages/daemon/dist/lib/remote-daemon-info.js +3 -2
- package/packages/daemon/dist/lib/remote-daemon-lifecycle.js +4 -4
- package/packages/daemon/dist/lib/remote-tmux-command.js +10 -0
- package/packages/daemon/dist/lib/runtime-namespace.js +28 -16
- package/packages/daemon/dist/lib/saved-machines.js +40 -2
- package/packages/daemon/dist/lib/ssh-daemon-transport.js +24 -9
- package/packages/daemon/dist/lib/tmux-server-owner.js +1 -1
- package/packages/daemon/dist/lib/tmux-server-registration.js +21 -3
- package/packages/daemon/dist/lib/tui-binary.js +2 -2
- package/packages/daemon/dist/lib/update.js +18 -0
- package/packages/daemon/dist/lib/workspace-multiplexer-verbs.js +24 -1
- package/packages/daemon/dist/machines.js +40 -4
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Info.plist +2 -2
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/MacOS/tmux-ide-notifier +0 -0
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Resources/Assets.car +0 -0
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/_CodeSignature/CodeResources +2 -2
- package/packages/daemon/dist/terminal/attachments/direct-websocket.js +4 -1
- package/packages/daemon/dist/terminal/attachments/native-runtime.js +22 -13
- package/packages/daemon/dist/terminal/mirror/mirror-service.js +101 -3
- package/packages/daemon/dist/terminal/mirror/registered-window-guard.js +62 -0
- package/packages/daemon/dist/terminal/mirror/session-channel.js +75 -4
- package/packages/daemon/dist/terminal/mirror/shared-window-index.js +45 -0
- package/packages/daemon/dist/terminal/pane-stream/pane-stream-websocket.js +11 -1
- package/packages/daemon/dist/terminal/session-runtime/multiplexer-backend.js +1 -0
- package/packages/daemon/dist/terminal/session-runtime/registry.js +56 -7
- package/packages/daemon/dist/terminal/session-runtime/semantic-mutation-executor.js +3 -1
- package/packages/daemon/dist/tui/integrations/claude-agent-teams.js +264 -0
- package/packages/daemon/dist/tui/mirror/dialog-model.js +2 -35
- package/packages/daemon/dist/tui/mirror/runtime/application-daemon-authority-owner.js +8 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-home-fleet.js +20 -2
- package/packages/daemon/dist/tui/mirror/runtime/application-machine-agents.js +7 -1
- package/packages/daemon/dist/tui/mirror/runtime/application-machine-authority.js +59 -9
- package/packages/daemon/dist/tui/mirror/runtime/application-machine-navigation.js +27 -19
- package/packages/daemon/dist/tui/mirror/runtime/application-machine-sidebar.jsx +23 -18
- package/packages/daemon/dist/tui/mirror/runtime/application-machine-startup.js +31 -2
- package/packages/daemon/dist/tui/mirror/runtime/application-palette-search-owner.js +7 -3
- package/packages/daemon/dist/tui/mirror/runtime/application-performance-log.js +3 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-terminal-input-ingress.js +146 -2
- package/packages/daemon/dist/tui/mirror/runtime/application-terminal-interaction-controller.js +201 -50
- package/packages/daemon/dist/tui/mirror/runtime/bounded-performance-record-writer.js +50 -10
- package/packages/daemon/dist/tui/mirror/runtime/terminal-fast-lane-renderer-adapter.js +6 -5
- package/packages/daemon/dist/tui/mirror/runtime/terminal-pane-input-router.js +3 -0
- package/packages/daemon/dist/tui/mirror/runtime/workspace-terminal-fast-lane.js +8 -2
- package/packages/daemon/dist/tui/mirror/terminal-text.js +35 -0
- package/packages/daemon/dist/tui/mirror/workspace/connection-feedback.js +5 -3
- package/packages/daemon/src/command-center/daemon-fleet-facts-observer.ts +100 -10
- package/packages/daemon/src/command-center/resources/saved-machine-route.ts +48 -1
- package/packages/daemon/src/command-center/ws-events.ts +5 -25
- package/packages/daemon/src/lib/canonical-daemon-bootstrap.ts +12 -0
- package/packages/daemon/src/lib/canonical-daemon.ts +44 -2
- package/packages/daemon/src/lib/daemon-embed.ts +1 -0
- package/packages/daemon/src/lib/daemon-service-manager.ts +152 -0
- package/packages/daemon/src/lib/daemon-service-plan.ts +153 -0
- package/packages/daemon/src/lib/daemon-service.ts +301 -0
- package/packages/daemon/src/lib/install-origin.ts +5 -0
- package/packages/daemon/src/lib/local-fleet-request.ts +19 -3
- package/packages/daemon/src/lib/log.ts +13 -9
- package/packages/daemon/src/lib/managed-installer-update.ts +79 -0
- package/packages/daemon/src/lib/{tui-download-lock.ts → private-operation-lock.ts} +15 -11
- package/packages/daemon/src/lib/remote-daemon-info.ts +3 -3
- package/packages/daemon/src/lib/remote-daemon-lifecycle.ts +4 -4
- package/packages/daemon/src/lib/remote-tmux-command.ts +12 -0
- package/packages/daemon/src/lib/runtime-namespace.ts +27 -18
- package/packages/daemon/src/lib/saved-machines.ts +40 -1
- package/packages/daemon/src/lib/ssh-daemon-transport.ts +25 -7
- package/packages/daemon/src/lib/tmux-server-owner.ts +1 -0
- package/packages/daemon/src/lib/tmux-server-registration.ts +21 -3
- package/packages/daemon/src/lib/tui-binary.ts +2 -2
- package/packages/daemon/src/lib/update.ts +24 -0
- package/packages/daemon/src/lib/workspace-multiplexer-verbs.ts +22 -0
- package/packages/daemon/src/machines.ts +53 -5
- package/packages/daemon/src/terminal/attachments/direct-websocket.ts +8 -2
- package/packages/daemon/src/terminal/attachments/native-runtime.ts +28 -13
- package/packages/daemon/src/terminal/mirror/mirror-service.ts +105 -3
- package/packages/daemon/src/terminal/mirror/registered-window-guard.ts +66 -0
- package/packages/daemon/src/terminal/mirror/session-channel.ts +92 -4
- package/packages/daemon/src/terminal/mirror/shared-window-index.ts +42 -0
- package/packages/daemon/src/terminal/pane-stream/pane-stream-websocket.ts +12 -1
- package/packages/daemon/src/terminal/session-runtime/multiplexer-backend.ts +2 -0
- package/packages/daemon/src/terminal/session-runtime/registry.ts +70 -8
- package/packages/daemon/src/terminal/session-runtime/semantic-mutation-executor.ts +3 -1
- package/packages/daemon/src/tui/integrations/claude-agent-teams.ts +334 -0
- package/packages/daemon/src/tui/mirror/dialog-model.ts +2 -32
- package/packages/daemon/src/tui/mirror/runtime/application-daemon-authority-owner.ts +8 -0
- package/packages/daemon/src/tui/mirror/runtime/application-home-fleet.ts +19 -2
- package/packages/daemon/src/tui/mirror/runtime/application-machine-agents.ts +6 -1
- package/packages/daemon/src/tui/mirror/runtime/application-machine-authority.ts +60 -11
- package/packages/daemon/src/tui/mirror/runtime/application-machine-navigation.ts +39 -33
- package/packages/daemon/src/tui/mirror/runtime/application-machine-sidebar.tsx +31 -23
- package/packages/daemon/src/tui/mirror/runtime/application-machine-startup.ts +38 -2
- package/packages/daemon/src/tui/mirror/runtime/application-palette-search-owner.ts +7 -4
- package/packages/daemon/src/tui/mirror/runtime/application-performance-log.ts +3 -0
- package/packages/daemon/src/tui/mirror/runtime/application-root-v2.tsx +1 -1
- package/packages/daemon/src/tui/mirror/runtime/application-shell-view.tsx +47 -46
- package/packages/daemon/src/tui/mirror/runtime/application-terminal-input-ingress.ts +165 -2
- package/packages/daemon/src/tui/mirror/runtime/application-terminal-interaction-controller.ts +207 -53
- package/packages/daemon/src/tui/mirror/runtime/bounded-performance-record-writer.ts +49 -9
- package/packages/daemon/src/tui/mirror/runtime/terminal-fast-lane-renderer-adapter.ts +6 -5
- package/packages/daemon/src/tui/mirror/runtime/terminal-pane-input-router.ts +4 -0
- package/packages/daemon/src/tui/mirror/runtime/workspace-terminal-fast-lane.ts +8 -3
- package/packages/daemon/src/tui/mirror/terminal-text.ts +32 -0
- package/packages/daemon/src/tui/mirror/workspace/connection-feedback.ts +6 -3
- package/packages/daemon-client/src/fleet-connection-status.ts +10 -1
- package/packages/daemon-client/src/workspace-client.ts +4 -1
- package/skill/SKILL.md +30 -30
- package/packages/contracts/src/__tests__/actions-contract.test.ts +0 -119
- package/packages/contracts/src/__tests__/agent-graph-overlay.test.ts +0 -428
- package/packages/contracts/src/__tests__/app-window-mutation.test.ts +0 -51
- package/packages/contracts/src/__tests__/app-window-state.test.ts +0 -34
- package/packages/contracts/src/__tests__/application-shell.test.ts +0 -645
- package/packages/contracts/src/__tests__/automation-operations.test.ts +0 -78
- package/packages/contracts/src/__tests__/causal-cell.test.ts +0 -47
- package/packages/contracts/src/__tests__/client-view-state.test.ts +0 -115
- package/packages/contracts/src/__tests__/cohesion-fixture.test.ts +0 -106
- package/packages/contracts/src/__tests__/commands.test.ts +0 -86
- package/packages/contracts/src/__tests__/control.test.ts +0 -154
- package/packages/contracts/src/__tests__/daemon-events.test.ts +0 -405
- package/packages/contracts/src/__tests__/daemon-resources.test.ts +0 -143
- package/packages/contracts/src/__tests__/daemon-wire.test.ts +0 -149
- package/packages/contracts/src/__tests__/desktop-host.test.ts +0 -269
- package/packages/contracts/src/__tests__/desktop-missions.test.ts +0 -215
- package/packages/contracts/src/__tests__/experience-conformance.test.ts +0 -153
- package/packages/contracts/src/__tests__/experience-kernel-boundary.test.ts +0 -113
- package/packages/contracts/src/__tests__/experience-shell.test.ts +0 -65
- package/packages/contracts/src/__tests__/fixtures/daemon-wire-v2.json +0 -35
- package/packages/contracts/src/__tests__/fixtures/daemon-wire-v3.json +0 -35
- package/packages/contracts/src/__tests__/fixtures/daemon-wire-v4.json +0 -35
- package/packages/contracts/src/__tests__/fixtures/scoped-receipt.ts +0 -62
- package/packages/contracts/src/__tests__/fleet-agent-graph.test.ts +0 -231
- package/packages/contracts/src/__tests__/fleet-catalog.test.ts +0 -270
- package/packages/contracts/src/__tests__/fleet-lifecycle.test.ts +0 -62
- package/packages/contracts/src/__tests__/focus-overlay.test.ts +0 -110
- package/packages/contracts/src/__tests__/ide-config.test.ts +0 -64
- package/packages/contracts/src/__tests__/interaction-evidence.test.ts +0 -288
- package/packages/contracts/src/__tests__/interaction-receipt-v2.test.ts +0 -72
- package/packages/contracts/src/__tests__/issue-error.test.ts +0 -78
- package/packages/contracts/src/__tests__/mission-projections.test.ts +0 -186
- package/packages/contracts/src/__tests__/missions.test.ts +0 -272
- package/packages/contracts/src/__tests__/multiplexer-verbs.test.ts +0 -242
- package/packages/contracts/src/__tests__/pane-appearance.test.ts +0 -146
- package/packages/contracts/src/__tests__/pane-read-result.test.ts +0 -55
- package/packages/contracts/src/__tests__/pane-stream.test.ts +0 -536
- package/packages/contracts/src/__tests__/pane-widget-marker.test.ts +0 -327
- package/packages/contracts/src/__tests__/performance-metrics.test.ts +0 -73
- package/packages/contracts/src/__tests__/performance-qualification.test.ts +0 -137
- package/packages/contracts/src/__tests__/rich-card-widget.test.ts +0 -29
- package/packages/contracts/src/__tests__/session-runtime.test.ts +0 -512
- package/packages/contracts/src/__tests__/startup-readiness.test.ts +0 -409
- package/packages/contracts/src/__tests__/terminal-attachment-stream.test.ts +0 -72
- package/packages/contracts/src/__tests__/terminal-attachments.test.ts +0 -329
- package/packages/contracts/src/__tests__/terminal-delivery.test.ts +0 -88
- package/packages/contracts/src/__tests__/terminal-runtime-inventory.test.ts +0 -68
- package/packages/contracts/src/__tests__/terminals.test.ts +0 -87
- package/packages/contracts/src/__tests__/tmux-server-scope.test.ts +0 -166
- package/packages/contracts/src/__tests__/transport-contract-loading.test.ts +0 -91
- package/packages/contracts/src/__tests__/visual-tokens.test.ts +0 -224
- package/packages/contracts/src/__tests__/window-links.test.ts +0 -243
- package/packages/contracts/src/__tests__/workspace-changes-resource.test.ts +0 -317
- package/packages/contracts/src/__tests__/workspace-changes-view.test.ts +0 -122
- package/packages/contracts/src/__tests__/workspace-config.test.ts +0 -294
- package/packages/contracts/src/__tests__/workspace-files-resource.test.ts +0 -261
- package/packages/contracts/src/__tests__/workspace-files-tree.test.ts +0 -147
- package/packages/contracts/src/__tests__/workspace-pane-creation.test.ts +0 -249
- package/packages/contracts/src/__tests__/workspace-promotion.test.ts +0 -94
- package/packages/contracts/src/__tests__/workspace-resource-identity.test.ts +0 -90
- package/packages/contracts/src/daemon-resource-request.test.ts +0 -192
- package/packages/contracts/src/native-operation-identity.test.ts +0 -23
- package/packages/contracts/src/workspace-admission.test.ts +0 -21
- package/packages/contracts/src/workspace-catalog-resource.test.ts +0 -146
- package/packages/core/src/agent-provisioning.test.ts +0 -42
- package/packages/core/src/application-shell-session.test.ts +0 -105
- package/packages/core/src/interaction-evidence.test.ts +0 -134
- package/packages/core/src/interaction-flow.test.ts +0 -66
- package/packages/core/src/interaction-receipts.test.ts +0 -671
- package/packages/core/src/navigator.test.ts +0 -34
- package/packages/core/src/optimistic-projection-conformance.test.ts +0 -75
- package/packages/core/src/optimistic-projection.test.ts +0 -118
- package/packages/core/src/performance-metrics.test.ts +0 -121
- package/packages/core/src/performance-qualification.test.ts +0 -283
- package/packages/core/src/saved-machines.test.ts +0 -80
- package/packages/core/src/terminal-compact-cold.test.ts +0 -87
- package/packages/core/src/terminal-conformance.test.ts +0 -48
- package/packages/core/src/terminal-delivery-cache-lifetime.test.ts +0 -167
- package/packages/core/src/terminal-delivery-encode-cooperative.test.ts +0 -140
- package/packages/core/src/terminal-delivery-representation-hash.test.ts +0 -77
- package/packages/core/src/terminal-delivery.test.ts +0 -1471
- package/packages/core/src/terminal-fnv64-wasm.test.ts +0 -61
- package/packages/core/src/terminal-replica-cold.test.ts +0 -31
- package/packages/core/src/terminal-replica-cooperative.test.ts +0 -170
- package/packages/core/src/terminal-replica-hash-cache.test.ts +0 -283
- package/packages/core/src/terminal-replica.test.ts +0 -347
- package/packages/core/src/workspace-selection.test.ts +0 -63
- package/packages/daemon/dist/__tests__/support.js +0 -33
- package/packages/daemon/dist/lib/__tests__/fixtures/workspace-state-writer.js +0 -58
- package/packages/daemon/dist/lib/__tests__/installed-recovery-fixture.js +0 -400
- package/packages/daemon/dist/terminal/__tests__/MockPtyAdapter.js +0 -182
- package/packages/daemon/dist/terminal/mirror/__tests__/scripted-channel.js +0 -98
- package/packages/daemon/dist/terminal/mirror/__tests__/simulated-channel.js +0 -156
- package/packages/daemon/src/tui/mirror/__snapshots__/activity-surface-renderer.test.tsx.snap +0 -42
- package/packages/daemon/src/tui/mirror/__snapshots__/changes-terminal-surface-renderer.test.tsx.snap +0 -267
- package/packages/daemon/src/tui/mirror/__snapshots__/home-files-surface-renderer.test.tsx.snap +0 -267
- package/packages/daemon/src/tui/mirror/__snapshots__/missions-surface-renderer.test.tsx.snap +0 -134
- package/packages/daemon/src/tui/mirror/__snapshots__/recipes-gallery-renderer.test.tsx.snap +0 -141
- package/packages/daemon/src/tui/mirror/__snapshots__/shell-chrome-renderer.test.tsx.snap +0 -134
- package/packages/daemon/src/tui/mirror/runtime/__snapshots__/application-shell-view-renderer.test.tsx.snap +0 -87
- package/packages/daemon/src/tui/mirror/workspace/__snapshots__/agent-terminal-canvas-renderer.test.tsx.snap +0 -348
- package/packages/daemon/src/tui/mirror/workspace/__snapshots__/application-shell-renderer.test.tsx.snap +0 -134
- package/packages/daemon/src/tui/mirror/workspace/__snapshots__/command-palette-surface-renderer.test.tsx.snap +0 -255
- package/packages/daemon/src/tui/mirror/workspace/__snapshots__/pane-frame-renderer.test.tsx.snap +0 -525
- package/packages/daemon/src/tui/mirror/workspace/__snapshots__/terminal-pane-header-polish-renderer.test.tsx.snap +0 -31
- package/packages/daemon/src/tui/mirror/workspace/__snapshots__/workbench-shell-renderer.test.tsx.snap +0 -134
- package/packages/daemon-client/src/application-shell-session.test.ts +0 -196
- package/packages/daemon-client/src/automation-client.test.ts +0 -214
- package/packages/daemon-client/src/bootstrap-coordinator.test.ts +0 -145
- package/packages/daemon-client/src/connection-supervisor.test.ts +0 -109
- package/packages/daemon-client/src/direct-application-shell-transport.test.ts +0 -941
- package/packages/daemon-client/src/first-latest-coordinator.test.ts +0 -175
- package/packages/daemon-client/src/fleet-dial-scheduler.test.ts +0 -114
- package/packages/daemon-client/src/fleet-environments.test.ts +0 -40
- package/packages/daemon-client/src/generation-bound-store-timers.test.ts +0 -74
- package/packages/daemon-client/src/owner-action-client.test.ts +0 -245
- package/packages/daemon-client/src/pane-stream-client.test.ts +0 -2841
- package/packages/daemon-client/src/pane-stream-clock-calibration.test.ts +0 -207
- package/packages/daemon-client/src/pane-stream-loopback-benchmark.test.ts +0 -51
- package/packages/daemon-client/src/push-resource-session.test.ts +0 -509
- package/packages/daemon-client/src/resource-replica.test.ts +0 -186
- package/packages/daemon-client/src/runtime-resource-ledger.test.ts +0 -18
- package/packages/daemon-client/src/scoped-tmux-server-transport.test.ts +0 -114
- package/packages/daemon-client/src/terminal-fast-lane.test.ts +0 -835
- package/packages/daemon-client/src/tmux-server-client.test.ts +0 -279
- package/packages/daemon-client/src/tmux-server-interaction-events.test.ts +0 -230
- package/packages/daemon-client/src/workspace-catalog-v2.test.ts +0 -109
- package/packages/daemon-client/src/workspace-client-boundaries.test.ts +0 -59
- package/packages/daemon-client/src/workspace-client-operations.test.ts +0 -96
- package/packages/daemon-client/src/workspace-client.test.ts +0 -2912
- package/packages/daemon-client/src/workspace-event-supervisor.test.ts +0 -1086
- package/packages/daemon-client/src/workspace-pane-client.test.ts +0 -80
- package/packages/presentation/src/presentation-contract.test.ts +0 -63
- package/packages/tmux-bridge/src/runner.test.ts +0 -614
- package/packages/tmux-bridge/src/targeting.test.ts +0 -107
|
@@ -81,12 +81,11 @@ function absolutePath(value: string, cwd: string, key: string): string {
|
|
|
81
81
|
return path;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
function
|
|
84
|
+
function pathEntryStat(path: string) {
|
|
85
85
|
try {
|
|
86
|
-
lstatSync(path);
|
|
87
|
-
return true;
|
|
86
|
+
return lstatSync(path);
|
|
88
87
|
} catch (error) {
|
|
89
|
-
if ((error as NodeJS.ErrnoException).code === "ENOENT") return
|
|
88
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
|
|
90
89
|
throw error;
|
|
91
90
|
}
|
|
92
91
|
}
|
|
@@ -94,21 +93,23 @@ function pathEntryExists(path: string): boolean {
|
|
|
94
93
|
function pathIdentity(path: string): string {
|
|
95
94
|
let cursor = resolve(path);
|
|
96
95
|
const suffix: string[] = [];
|
|
97
|
-
|
|
96
|
+
let stat = pathEntryStat(cursor);
|
|
97
|
+
while (!stat) {
|
|
98
98
|
const parent = dirname(cursor);
|
|
99
99
|
if (parent === cursor) break;
|
|
100
100
|
suffix.unshift(basename(cursor));
|
|
101
101
|
cursor = parent;
|
|
102
|
+
stat = pathEntryStat(cursor);
|
|
102
103
|
}
|
|
103
|
-
if (
|
|
104
|
+
if (stat?.isSocket()) {
|
|
104
105
|
return resolve(captureUnixSocketIdentity(cursor).path, ...suffix);
|
|
105
106
|
}
|
|
106
|
-
return resolve(
|
|
107
|
+
return resolve(stat ? realpathSync(cursor) : cursor, ...suffix);
|
|
107
108
|
}
|
|
108
109
|
|
|
109
|
-
function isInsideOrEqual(path: string, parent: string): boolean {
|
|
110
|
-
const child =
|
|
111
|
-
const root =
|
|
110
|
+
function isInsideOrEqual(path: string, parent: string, identity = pathIdentity): boolean {
|
|
111
|
+
const child = identity(path);
|
|
112
|
+
const root = identity(parent);
|
|
112
113
|
const offset = relative(root, child);
|
|
113
114
|
return offset === "" || (!offset.startsWith(`..${sep}`) && offset !== "..");
|
|
114
115
|
}
|
|
@@ -124,6 +125,17 @@ function runtimeMode(env: NodeJS.ProcessEnv): RuntimeMode {
|
|
|
124
125
|
export function resolveRuntimeNamespace(
|
|
125
126
|
options: RuntimeNamespaceResolutionOptions = {},
|
|
126
127
|
): RuntimeNamespace {
|
|
128
|
+
// Reuse a path's identity only within this synchronous resolution. Every
|
|
129
|
+
// subsequent resolution must see new ancestors and replaced symlinks.
|
|
130
|
+
let identities: Map<string, string> | undefined;
|
|
131
|
+
const identity = (path: string): string => {
|
|
132
|
+
const cached = identities?.get(path);
|
|
133
|
+
if (cached !== undefined) return cached;
|
|
134
|
+
const resolved = pathIdentity(path);
|
|
135
|
+
(identities ??= new Map()).set(path, resolved);
|
|
136
|
+
return resolved;
|
|
137
|
+
};
|
|
138
|
+
const within = (path: string, parent: string) => isInsideOrEqual(path, parent, identity);
|
|
127
139
|
const env = options.env ?? process.env;
|
|
128
140
|
const userHome = options.userHome ?? homedir();
|
|
129
141
|
const cwd = options.cwd ?? process.cwd();
|
|
@@ -205,16 +217,13 @@ export function resolveRuntimeNamespace(
|
|
|
205
217
|
if (isolated && tmuxSocket.kind === "name" && tmuxSocket.name === "default") {
|
|
206
218
|
throw new TypeError(`${mode} runtime requires a non-default ${TMUX_SOCKET_NAME_ENV}`);
|
|
207
219
|
}
|
|
208
|
-
if (isolated &&
|
|
220
|
+
if (isolated && within(stateHome, canonicalHome)) {
|
|
209
221
|
throw new TypeError(`${mode} runtime cannot use the canonical tmux-ide state home`);
|
|
210
222
|
}
|
|
211
|
-
if (
|
|
212
|
-
isolated &&
|
|
213
|
-
(isInsideOrEqual(registryDir, canonicalHome) || isInsideOrEqual(daemonInfoDir, canonicalHome))
|
|
214
|
-
) {
|
|
223
|
+
if (isolated && (within(registryDir, canonicalHome) || within(daemonInfoDir, canonicalHome))) {
|
|
215
224
|
throw new TypeError(`${mode} runtime cannot use canonical registry or daemon state`);
|
|
216
225
|
}
|
|
217
|
-
if (isolated && tmuxSocket.kind === "path" &&
|
|
226
|
+
if (isolated && tmuxSocket.kind === "path" && within(tmuxSocket.path, canonicalHome)) {
|
|
218
227
|
throw new TypeError(`${mode} runtime cannot use a tmux socket inside canonical state`);
|
|
219
228
|
}
|
|
220
229
|
if (isolated && cleanupToken === null) {
|
|
@@ -229,7 +238,7 @@ export function resolveRuntimeNamespace(
|
|
|
229
238
|
(key === "TMUX_IDE_CONFIG" || key === "TMUX_IDE_SETTINGS_DIR"
|
|
230
239
|
? env[key]
|
|
231
240
|
: nonEmpty(env, key)) ?? fallback;
|
|
232
|
-
if (development && (!isAbsolute(value) || !
|
|
241
|
+
if (development && (!isAbsolute(value) || !within(value, development.root)))
|
|
233
242
|
throw new TypeError(`development ${key} escapes instance`);
|
|
234
243
|
return value;
|
|
235
244
|
};
|
|
@@ -267,7 +276,7 @@ export function resolveRuntimeNamespace(
|
|
|
267
276
|
if (nonEmpty(env, key)) scoped(key, "");
|
|
268
277
|
if (env.TMUX) {
|
|
269
278
|
const socket = /^(.*),[0-9]+,[0-9]+$/u.exec(env.TMUX)?.[1];
|
|
270
|
-
if (!socket ||
|
|
279
|
+
if (!socket || identity(socket) !== identity(join(runtimeDir, "tmux.sock")))
|
|
271
280
|
throw new TypeError("development rejects inherited foreign TMUX authority");
|
|
272
281
|
}
|
|
273
282
|
}
|
|
@@ -9,8 +9,9 @@ import {
|
|
|
9
9
|
renameSync,
|
|
10
10
|
unlinkSync,
|
|
11
11
|
writeFileSync,
|
|
12
|
+
watch,
|
|
12
13
|
} from "node:fs";
|
|
13
|
-
import { dirname, join } from "node:path";
|
|
14
|
+
import { basename, dirname, join } from "node:path";
|
|
14
15
|
import { SavedMachineRegistrySchema, type SavedMachineRegistry } from "@tmux-ide/contracts";
|
|
15
16
|
import { changeSavedMachines, type SavedMachineChange } from "@tmux-ide/core";
|
|
16
17
|
import { resolveRuntimeNamespace } from "./runtime-namespace.ts";
|
|
@@ -38,6 +39,44 @@ export function loadSavedMachines(path = savedMachinesPath()): SavedMachineRegis
|
|
|
38
39
|
}
|
|
39
40
|
}
|
|
40
41
|
|
|
42
|
+
/** Watch the directory so atomic registry replacement does not detach the watcher. */
|
|
43
|
+
export function watchSavedMachines(
|
|
44
|
+
onChange: (registry: SavedMachineRegistry) => void,
|
|
45
|
+
onError: () => void,
|
|
46
|
+
path = savedMachinesPath(),
|
|
47
|
+
): () => void {
|
|
48
|
+
let stopped = false;
|
|
49
|
+
let pending: ReturnType<typeof setTimeout> | null = null;
|
|
50
|
+
const refresh = () => {
|
|
51
|
+
pending = null;
|
|
52
|
+
if (stopped) return;
|
|
53
|
+
try {
|
|
54
|
+
onChange(loadSavedMachines(path));
|
|
55
|
+
} catch {
|
|
56
|
+
// Keep the last valid routes while an editor is midway through a write.
|
|
57
|
+
onError();
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
// A first-run app can subscribe before the daemon creates its registry.
|
|
61
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
62
|
+
const watcher = watch(dirname(path), (_event, file) => {
|
|
63
|
+
if (stopped || (file !== null && file.toString() !== basename(path))) return;
|
|
64
|
+
if (pending) clearTimeout(pending);
|
|
65
|
+
pending = setTimeout(refresh, 50);
|
|
66
|
+
pending.unref();
|
|
67
|
+
});
|
|
68
|
+
watcher.unref();
|
|
69
|
+
watcher.on("error", onError);
|
|
70
|
+
// Close the read-before-subscribe race at startup.
|
|
71
|
+
pending = setTimeout(refresh, 0);
|
|
72
|
+
pending.unref();
|
|
73
|
+
return () => {
|
|
74
|
+
stopped = true;
|
|
75
|
+
if (pending) clearTimeout(pending);
|
|
76
|
+
watcher.close();
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
41
80
|
/** The daemon owns writes. Synchronous read/reduce/rename cannot interleave within it. */
|
|
42
81
|
export function updateSavedMachines(
|
|
43
82
|
change: SavedMachineChange,
|
|
@@ -3,6 +3,7 @@ import { spawn, type ChildProcess } from "node:child_process";
|
|
|
3
3
|
import { createServer } from "node:net";
|
|
4
4
|
import type { Readable } from "node:stream";
|
|
5
5
|
import { z } from "zod";
|
|
6
|
+
import { remoteTmuxIdeCommand } from "./remote-tmux-command.ts";
|
|
6
7
|
import { createSshDaemonRelay, type SshDaemonRelay } from "./ssh-daemon-relay.ts";
|
|
7
8
|
import {
|
|
8
9
|
CanonicalDaemonInfoSchema,
|
|
@@ -49,12 +50,30 @@ export class SshConnectionError extends Error {
|
|
|
49
50
|
super(`SSH daemon connection: ${message}`);
|
|
50
51
|
}
|
|
51
52
|
get retryable(): boolean {
|
|
52
|
-
|
|
53
|
+
// A supervisor may temporarily remove the record while replacing its daemon.
|
|
54
|
+
return this.code === "unavailable" || this.code === "daemon-missing";
|
|
53
55
|
}
|
|
54
56
|
}
|
|
55
57
|
function failure(message: string, code: FleetConnectionFailureCode = "unavailable"): Error {
|
|
56
58
|
return new SshConnectionError(message, code);
|
|
57
59
|
}
|
|
60
|
+
|
|
61
|
+
/** Classify known OpenSSH errors without publishing remote stderr or credentials. */
|
|
62
|
+
function observeSshFailure(child: SshTransportChild): () => FleetConnectionFailureCode {
|
|
63
|
+
let tail = "";
|
|
64
|
+
let code: FleetConnectionFailureCode = "unavailable";
|
|
65
|
+
child.stderr?.on("data", (chunk: Buffer) => {
|
|
66
|
+
tail = (tail + chunk.toString("utf8")).slice(-4096);
|
|
67
|
+
if (/Host key verification failed|REMOTE HOST IDENTIFICATION HAS CHANGED/i.test(tail))
|
|
68
|
+
code = "ssh-host-key";
|
|
69
|
+
else if (/Permission denied \((?:publickey|password|keyboard-interactive)[^)]*\)/i.test(tail))
|
|
70
|
+
code = "ssh-authentication";
|
|
71
|
+
});
|
|
72
|
+
child.once("close", () => {
|
|
73
|
+
tail = "";
|
|
74
|
+
});
|
|
75
|
+
return () => code;
|
|
76
|
+
}
|
|
58
77
|
const stoppedChildren = new WeakSet<SshTransportChild>();
|
|
59
78
|
function stop(child: SshTransportChild): void {
|
|
60
79
|
if (stoppedChildren.has(child) || child.exitCode !== null || child.signalCode !== null) return;
|
|
@@ -153,6 +172,7 @@ const defaults: SshDaemonTransportDependencies = {
|
|
|
153
172
|
probe: probeSshDaemonIdentity,
|
|
154
173
|
};
|
|
155
174
|
function discover(child: SshTransportChild, signal: AbortSignal): Promise<RemoteDaemon> {
|
|
175
|
+
const sshFailure = observeSshFailure(child);
|
|
156
176
|
return new Promise((resolve, reject) => {
|
|
157
177
|
const chunks: Buffer[] = [];
|
|
158
178
|
let bytes = 0;
|
|
@@ -177,8 +197,6 @@ function discover(child: SshTransportChild, signal: AbortSignal): Promise<Remote
|
|
|
177
197
|
}
|
|
178
198
|
chunks.push(Buffer.from(chunk));
|
|
179
199
|
});
|
|
180
|
-
// Drain without capturing: SSH banners/errors may contain confidential remote information.
|
|
181
|
-
child.stderr?.resume();
|
|
182
200
|
child.once("error", () => finish(failure("could not start OpenSSH")));
|
|
183
201
|
child.once("close", (code) => {
|
|
184
202
|
if (settled) return;
|
|
@@ -186,6 +204,7 @@ function discover(child: SshTransportChild, signal: AbortSignal): Promise<Remote
|
|
|
186
204
|
finish(
|
|
187
205
|
failure(
|
|
188
206
|
"discovery failed; check SSH authentication, host trust, and remote tmux-ide installation",
|
|
207
|
+
code === 127 ? "remote-cli-missing" : sshFailure(),
|
|
189
208
|
),
|
|
190
209
|
);
|
|
191
210
|
return;
|
|
@@ -266,9 +285,7 @@ export async function openSshDaemonTransport(
|
|
|
266
285
|
"ForkAfterAuthentication=no",
|
|
267
286
|
"--",
|
|
268
287
|
options.alias,
|
|
269
|
-
"
|
|
270
|
-
"remote-daemon-info",
|
|
271
|
-
"--json",
|
|
288
|
+
remoteTmuxIdeCommand("discover"),
|
|
272
289
|
]);
|
|
273
290
|
const daemon = await discover(child, signal);
|
|
274
291
|
const port = await cancellable(dependencies.allocatePort(), signal);
|
|
@@ -307,7 +324,7 @@ export async function openSshDaemonTransport(
|
|
|
307
324
|
options.alias,
|
|
308
325
|
]);
|
|
309
326
|
child.stdout?.resume();
|
|
310
|
-
child
|
|
327
|
+
const sshFailure = observeSshFailure(child);
|
|
311
328
|
child.once("exit", dispose);
|
|
312
329
|
const tunnelClosed = new Promise<void>((resolve) => {
|
|
313
330
|
child!.once("close", () => {
|
|
@@ -342,6 +359,7 @@ export async function openSshDaemonTransport(
|
|
|
342
359
|
}
|
|
343
360
|
throw failure(
|
|
344
361
|
"tunnel could not authenticate the expected daemon before cancellation or timeout",
|
|
362
|
+
sshFailure(),
|
|
345
363
|
);
|
|
346
364
|
} catch (error) {
|
|
347
365
|
dispose();
|
|
@@ -268,6 +268,7 @@ export async function createNativeTmuxServerOwner(options: NativeTmuxServerOwner
|
|
|
268
268
|
return multiplexer.mutateWindowLink(
|
|
269
269
|
{ operationId, expectedDaemonInstanceId: generation, intent },
|
|
270
270
|
(session, action) => sessionRuntimeRegistry.executeWindowLinkAction(session, action),
|
|
271
|
+
timing,
|
|
271
272
|
);
|
|
272
273
|
}
|
|
273
274
|
if (intent.verb === "workspace.pane.resize") {
|
|
@@ -77,11 +77,29 @@ export function createTmuxServerProbe(
|
|
|
77
77
|
)
|
|
78
78
|
).stdout.trimEnd();
|
|
79
79
|
try {
|
|
80
|
-
|
|
80
|
+
// A direct socket can be pinned before contacting tmux. Named servers
|
|
81
|
+
// and aliases still need discovery followed by an explicit-path proof.
|
|
82
|
+
let pinned: ReturnType<typeof captureUnixSocketIdentity> | null = null;
|
|
83
|
+
if (selector.kind === "path") {
|
|
84
|
+
try {
|
|
85
|
+
pinned = captureUnixSocketIdentity(selector.path);
|
|
86
|
+
} catch {
|
|
87
|
+
// Preserve discovery for aliases which are not direct socket paths.
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const first = await read(pinned ? ["-S", pinned.path] : args);
|
|
81
91
|
const match = /^(.*)\|([1-9][0-9]*)\|([1-9][0-9]*)$/u.exec(first);
|
|
82
92
|
if (!match) return null;
|
|
83
|
-
const
|
|
84
|
-
|
|
93
|
+
const reportedSocket = captureUnixSocketIdentity(match[1]!);
|
|
94
|
+
const socket = pinned ?? reportedSocket;
|
|
95
|
+
if (pinned) {
|
|
96
|
+
if (
|
|
97
|
+
reportedSocket.path !== pinned.path ||
|
|
98
|
+
reportedSocket.dev !== pinned.dev ||
|
|
99
|
+
reportedSocket.ino !== pinned.ino
|
|
100
|
+
)
|
|
101
|
+
return null;
|
|
102
|
+
} else if ((await read(["-S", socket.path])) !== first) return null;
|
|
85
103
|
revalidateUnixSocketIdentity(socket);
|
|
86
104
|
return {
|
|
87
105
|
nativeServerIdentity: { pid: match[2]!, startTime: match[3]! },
|
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
} from "node:fs";
|
|
29
29
|
import { dirname, join } from "node:path";
|
|
30
30
|
import { gunzipSync } from "node:zlib";
|
|
31
|
-
import {
|
|
31
|
+
import { acquirePrivateOperationLock } from "./private-operation-lock.ts";
|
|
32
32
|
import { getCurrentVersion } from "./update-check.ts";
|
|
33
33
|
|
|
34
34
|
/** The `<os>-<arch>` tags we publish a prebuilt TUI binary for. */
|
|
@@ -360,7 +360,7 @@ export async function downloadTuiBinary(
|
|
|
360
360
|
timeoutMs: opts.timeoutMs ?? TUI_DOWNLOAD_TIMEOUT_MS,
|
|
361
361
|
};
|
|
362
362
|
mkdirSync(dirname(dest), { recursive: true });
|
|
363
|
-
const releaseLock = await
|
|
363
|
+
const releaseLock = await acquirePrivateOperationLock(
|
|
364
364
|
downloadLockPath(dest),
|
|
365
365
|
limits.timeoutMs * 2 + 5_000,
|
|
366
366
|
);
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import {
|
|
2
|
+
managedInstallerPrefix,
|
|
3
|
+
runManagedInstallerUpdate,
|
|
4
|
+
MANAGED_INSTALLER_URL,
|
|
5
|
+
} from "./managed-installer-update.ts";
|
|
1
6
|
import { resolveRuntimeNamespace } from "./runtime-namespace.ts";
|
|
2
7
|
import { parseStrictSemver } from "./semver.ts";
|
|
3
8
|
/** Origin-aware update planning; unsupported origins never fall back to npm. */
|
|
@@ -28,6 +33,7 @@ export interface UpdatePlan {
|
|
|
28
33
|
args?: string[];
|
|
29
34
|
guidance?: string;
|
|
30
35
|
proposedCommand?: string;
|
|
36
|
+
installerPrefix?: string;
|
|
31
37
|
}
|
|
32
38
|
export const UPDATE_COMMANDS: Record<PackageManager, string> = {
|
|
33
39
|
npm: "npm install -g tmux-ide@latest",
|
|
@@ -62,7 +68,21 @@ export function planUpdate(input: {
|
|
|
62
68
|
reason: `global ${method} layout (${input.cliPath})`,
|
|
63
69
|
};
|
|
64
70
|
}
|
|
71
|
+
if (method === "installer") {
|
|
72
|
+
const prefix = managedInstallerPrefix(input.cliPath);
|
|
73
|
+
if (prefix) {
|
|
74
|
+
return {
|
|
75
|
+
method,
|
|
76
|
+
channel,
|
|
77
|
+
installerPrefix: prefix,
|
|
78
|
+
command: `installer ${MANAGED_INSTALLER_URL} --prefix ${JSON.stringify(prefix)} --version ${channel}`,
|
|
79
|
+
reason: `verified active managed installation (${prefix})`,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
}
|
|
65
83
|
const guidance: Record<Exclude<InstallOrigin, PackageManager>, string> = {
|
|
84
|
+
installer:
|
|
85
|
+
"Rerun https://tmux-ide.com/install.sh with the same --prefix to update atomically and retain rollback. This managed release must not be modified by npm in place.",
|
|
66
86
|
dev: "Update this checkout with git pull, then follow its build instructions.",
|
|
67
87
|
homebrew: "Update with brew upgrade tmux-ide (using the tap/formula you installed).",
|
|
68
88
|
yarn: `Update this Yarn global installation with yarn global add tmux-ide@${channel}.`,
|
|
@@ -167,6 +187,10 @@ export function runUpdate(
|
|
|
167
187
|
});
|
|
168
188
|
executed = true;
|
|
169
189
|
}
|
|
190
|
+
if (!dryRun && plan.installerPrefix && plan.channel) {
|
|
191
|
+
runManagedInstallerUpdate(plan.installerPrefix, plan.channel, json, dependencies.execute);
|
|
192
|
+
executed = true;
|
|
193
|
+
}
|
|
170
194
|
if (json) output(JSON.stringify({ ...plan, current, latest, dryRun, executed }));
|
|
171
195
|
return plan;
|
|
172
196
|
}
|
|
@@ -468,6 +468,7 @@ export class WorkspaceMultiplexerAuthority {
|
|
|
468
468
|
session: string,
|
|
469
469
|
action: AuthoritativeWindowLinkAction,
|
|
470
470
|
) => Promise<AuthoritativeWindowLinkActionResult>,
|
|
471
|
+
timing?: WorkspaceMultiplexerOperationTiming,
|
|
471
472
|
): Promise<WorkspaceMultiplexerMutationResult> {
|
|
472
473
|
if (this.#disposed) throw new WorkspaceMultiplexerError("workspace_unavailable");
|
|
473
474
|
const request = WorkspaceMultiplexerMutationRequestSchemaZ.parse(raw);
|
|
@@ -480,7 +481,26 @@ export class WorkspaceMultiplexerAuthority {
|
|
|
480
481
|
intent.verb !== "workspace.pane.select"
|
|
481
482
|
)
|
|
482
483
|
throw new TypeError("Expected a window-link selection or unlink intent");
|
|
484
|
+
const now = () => {
|
|
485
|
+
try {
|
|
486
|
+
return timing?.nowMicros() ?? null;
|
|
487
|
+
} catch {
|
|
488
|
+
return null;
|
|
489
|
+
}
|
|
490
|
+
};
|
|
491
|
+
const record = (operation: string, start: number | null) => {
|
|
492
|
+
const end = now();
|
|
493
|
+
if (start !== null && end !== null) {
|
|
494
|
+
try {
|
|
495
|
+
timing?.record(operation, start, end);
|
|
496
|
+
} catch {
|
|
497
|
+
/* Diagnostics cannot own selection. */
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
};
|
|
501
|
+
const lookupStarted = now();
|
|
483
502
|
const workspace = this.#registry.get(intent.workspaceName);
|
|
503
|
+
record("semantic-workspace-lookup", lookupStarted);
|
|
484
504
|
if (!workspace) throw new WorkspaceMultiplexerError("workspace_not_found");
|
|
485
505
|
const envelope = {
|
|
486
506
|
operationId: request.operationId,
|
|
@@ -488,6 +508,7 @@ export class WorkspaceMultiplexerAuthority {
|
|
|
488
508
|
workspaceName: intent.workspaceName,
|
|
489
509
|
};
|
|
490
510
|
try {
|
|
511
|
+
const effectStarted = now();
|
|
491
512
|
const result = await execute(
|
|
492
513
|
workspace.sessionName,
|
|
493
514
|
intent.verb === "workspace.pane.select"
|
|
@@ -501,6 +522,7 @@ export class WorkspaceMultiplexerAuthority {
|
|
|
501
522
|
target: intent.target,
|
|
502
523
|
},
|
|
503
524
|
);
|
|
525
|
+
record("window-link-effect-proof", effectStarted);
|
|
504
526
|
if (result.outcome !== "applied") {
|
|
505
527
|
throw new WorkspaceMultiplexerError(
|
|
506
528
|
result.outcome === "stale"
|
|
@@ -1,22 +1,70 @@
|
|
|
1
1
|
import { readFileSync, statSync } from "node:fs";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
SavedMachineRegistrySchema,
|
|
5
|
+
SavedMachineSchema,
|
|
6
|
+
SavedMachineMutationSchema,
|
|
7
|
+
} from "@tmux-ide/contracts/saved-machines";
|
|
4
8
|
import { loadSavedMachines, planSavedMachineMerge } from "./lib/saved-machines.ts";
|
|
5
9
|
import { loadFleetClientState } from "./lib/fleet-client-state.ts";
|
|
6
|
-
import {
|
|
10
|
+
import { changeSavedMachines } from "@tmux-ide/core";
|
|
11
|
+
import { saveMachineProfiles, mutateMachineProfile } from "./lib/local-fleet-request.ts";
|
|
12
|
+
import { remoteTmuxIdeCommand } from "./lib/remote-tmux-command.ts";
|
|
7
13
|
|
|
8
14
|
export async function machines(
|
|
9
15
|
command: string | undefined,
|
|
10
16
|
argument: string | undefined,
|
|
11
|
-
options: { write?: boolean; label?: string },
|
|
17
|
+
options: { write?: boolean; label?: string; sshTarget?: string },
|
|
12
18
|
) {
|
|
13
19
|
const current = loadSavedMachines();
|
|
20
|
+
if (
|
|
21
|
+
(command === "enable" || command === "disable" || command === "remove" || command === "edit") &&
|
|
22
|
+
argument
|
|
23
|
+
) {
|
|
24
|
+
const key = argument.normalize("NFKC").toLowerCase();
|
|
25
|
+
const match =
|
|
26
|
+
current.machines.find((machine) => machine.id === key) ??
|
|
27
|
+
current.machines.find((machine) => machine.label.normalize("NFKC").toLowerCase() === key);
|
|
28
|
+
if (!match)
|
|
29
|
+
throw new Error("Saved machine not found; use machines ls to select its ID or label.");
|
|
30
|
+
const change = SavedMachineMutationSchema.parse(
|
|
31
|
+
command === "edit"
|
|
32
|
+
? {
|
|
33
|
+
id: match.id,
|
|
34
|
+
operation: command,
|
|
35
|
+
patch: {
|
|
36
|
+
...(options.label !== undefined ? { label: options.label } : {}),
|
|
37
|
+
...(options.sshTarget !== undefined ? { sshTarget: options.sshTarget } : {}),
|
|
38
|
+
},
|
|
39
|
+
}
|
|
40
|
+
: { id: match.id, operation: command },
|
|
41
|
+
);
|
|
42
|
+
const preview = changeSavedMachines(
|
|
43
|
+
current,
|
|
44
|
+
command === "remove"
|
|
45
|
+
? { type: "remove", id: match.id }
|
|
46
|
+
: {
|
|
47
|
+
type: "update",
|
|
48
|
+
id: match.id,
|
|
49
|
+
patch: change.operation === "edit" ? change.patch : { enabled: command === "enable" },
|
|
50
|
+
},
|
|
51
|
+
);
|
|
52
|
+
return {
|
|
53
|
+
written: options.write === true,
|
|
54
|
+
registry: options.write ? await mutateMachineProfile(change) : preview,
|
|
55
|
+
...(!options.write
|
|
56
|
+
? {
|
|
57
|
+
note: "Repeat with --write to change this saved profile. Remote sessions and daemons are preserved.",
|
|
58
|
+
}
|
|
59
|
+
: {}),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
14
62
|
if (command === "start" && argument) {
|
|
15
63
|
SavedMachineSchema.shape.sshTarget.parse(argument);
|
|
16
64
|
if (!options.write)
|
|
17
65
|
return {
|
|
18
66
|
written: false,
|
|
19
|
-
command: ["ssh", "--", argument, "
|
|
67
|
+
command: ["ssh", "--", argument, remoteTmuxIdeCommand("start")],
|
|
20
68
|
note: "Repeat with --write to start the installed daemon. No package installation occurs.",
|
|
21
69
|
};
|
|
22
70
|
await (await import("./lib/remote-daemon-lifecycle.ts")).startInstalledRemoteDaemon(argument);
|
|
@@ -74,7 +122,7 @@ export async function machines(
|
|
|
74
122
|
};
|
|
75
123
|
} else
|
|
76
124
|
throw new Error(
|
|
77
|
-
"Usage: tmux-ide machines ls|export|import <file>|add <alias> [--write] [--json]",
|
|
125
|
+
"Usage: tmux-ide machines ls|export|import <file>|add <alias>|edit|enable|disable|remove <id-or-label> [--write] [--json]",
|
|
78
126
|
);
|
|
79
127
|
const preview = planSavedMachineMerge(current, incoming);
|
|
80
128
|
if (!options.write)
|
|
@@ -210,7 +210,7 @@ export interface TerminalAttachmentAdmissionCoordinatorOptions {
|
|
|
210
210
|
/** Trusted redemption boundary into the generation's SessionRuntime owner. */
|
|
211
211
|
readonly bindSessionRuntime?: (
|
|
212
212
|
descriptor: AttachmentLeaseDescriptor,
|
|
213
|
-
) => SessionRuntimeRedeemedTransportBinding
|
|
213
|
+
) => SessionRuntimeRedeemedTransportBinding | Promise<SessionRuntimeRedeemedTransportBinding>;
|
|
214
214
|
readonly maxPendingTickets?: number;
|
|
215
215
|
readonly maxPreAuthSockets?: number;
|
|
216
216
|
readonly maxLiveConnections?: number;
|
|
@@ -765,7 +765,13 @@ export class TerminalAttachmentAdmissionCoordinator {
|
|
|
765
765
|
let sessionRuntimeBinding: SessionRuntimeRedeemedTransportBinding | null = null;
|
|
766
766
|
let live: TerminalAttachmentLiveConnection;
|
|
767
767
|
try {
|
|
768
|
-
sessionRuntimeBinding = this.#bindSessionRuntime?.(activeDescriptor) ?? null;
|
|
768
|
+
sessionRuntimeBinding = (await this.#bindSessionRuntime?.(activeDescriptor)) ?? null;
|
|
769
|
+
if (!admission.isOpen()) {
|
|
770
|
+
throw new TerminalAttachmentAdmissionError(
|
|
771
|
+
"redemption-rejected",
|
|
772
|
+
"Terminal attachment redemption was rejected.",
|
|
773
|
+
);
|
|
774
|
+
}
|
|
769
775
|
live = new TerminalAttachmentLiveConnection({
|
|
770
776
|
onRetire: (connection) => this.#trackRetiringRelease(connection),
|
|
771
777
|
socket,
|
|
@@ -1307,6 +1307,7 @@ export class WorkspaceTerminalInventoryRuntime {
|
|
|
1307
1307
|
}
|
|
1308
1308
|
>();
|
|
1309
1309
|
#lifecycle: "initializing" | "ready" | "failed" | "disposed" = "initializing";
|
|
1310
|
+
#registeredWindows: { invalidate(): void; dispose(): void } | undefined;
|
|
1310
1311
|
#disposed = false;
|
|
1311
1312
|
|
|
1312
1313
|
constructor(options: WorkspaceTerminalInventoryRuntimeOptions) {
|
|
@@ -1318,6 +1319,16 @@ export class WorkspaceTerminalInventoryRuntime {
|
|
|
1318
1319
|
});
|
|
1319
1320
|
this.readRunner = pinnedReadRunner(authority, executeRead);
|
|
1320
1321
|
this.#registry = options.registry;
|
|
1322
|
+
this.#registeredWindows = options.sessionRuntimeRegistry?.setRegisteredWindowReader?.(
|
|
1323
|
+
async (signal) => {
|
|
1324
|
+
const snapshot = await discoverWorkspaceRegistryTerminalInventory(
|
|
1325
|
+
this.#registry,
|
|
1326
|
+
this.readRunner,
|
|
1327
|
+
signal,
|
|
1328
|
+
);
|
|
1329
|
+
return snapshot.panes.map((pane) => ({ session: pane.sessionName, window: pane.windowId }));
|
|
1330
|
+
},
|
|
1331
|
+
);
|
|
1321
1332
|
this.#observability = options.observability ?? DISABLED_SESSION_RUNTIME_OBSERVABILITY;
|
|
1322
1333
|
this.#resolveInteractionEndpoint = options.resolveInteractionEndpoint;
|
|
1323
1334
|
this.#nativeServerEpoch = options.nativeServerEpoch;
|
|
@@ -1441,10 +1452,12 @@ export class WorkspaceTerminalInventoryRuntime {
|
|
|
1441
1452
|
this.#observeWorkspaceSession = null;
|
|
1442
1453
|
}
|
|
1443
1454
|
this.#stopWorkspaceAddedObserver = options.registry.on("workspace.added", (workspace) => {
|
|
1455
|
+
this.#registeredWindows?.invalidate();
|
|
1444
1456
|
this.invalidate();
|
|
1445
1457
|
this.#observeWorkspaceSession?.(workspace.name, workspace.sessionName);
|
|
1446
1458
|
});
|
|
1447
1459
|
this.#stopWorkspaceRemovedObserver = options.registry.on("workspace.removed", (name) => {
|
|
1460
|
+
this.#registeredWindows?.invalidate();
|
|
1448
1461
|
this.invalidate();
|
|
1449
1462
|
if (!options.sessionRuntimeRegistry) return;
|
|
1450
1463
|
const sessionName = sessionsByWorkspace.get(name);
|
|
@@ -1749,12 +1762,12 @@ export class WorkspaceTerminalInventoryRuntime {
|
|
|
1749
1762
|
let catalogIssue: NativeTerminalInventoryCatalogIssue | null = inventory.catalog
|
|
1750
1763
|
.invalidRuntimeProof
|
|
1751
1764
|
? "invalid-runtime-proof"
|
|
1752
|
-
: inventory.catalog.
|
|
1753
|
-
? "
|
|
1754
|
-
: inventory.catalog.
|
|
1755
|
-
? "
|
|
1756
|
-
: inventory.catalog.
|
|
1757
|
-
? "duplicate-
|
|
1765
|
+
: inventory.catalog.duplicateRuntimePaneBinding
|
|
1766
|
+
? "duplicate-runtime-pane-binding"
|
|
1767
|
+
: inventory.catalog.missingSemanticStamp
|
|
1768
|
+
? "missing-semantic-stamp"
|
|
1769
|
+
: inventory.catalog.duplicateSemanticStamp
|
|
1770
|
+
? "duplicate-semantic-stamp"
|
|
1758
1771
|
: null;
|
|
1759
1772
|
if (
|
|
1760
1773
|
shouldPrewarm &&
|
|
@@ -1949,6 +1962,7 @@ export class WorkspaceTerminalInventoryRuntime {
|
|
|
1949
1962
|
if (this.#disposed) return;
|
|
1950
1963
|
this.#disposed = true;
|
|
1951
1964
|
this.#lifecycle = "disposed";
|
|
1965
|
+
this.#registeredWindows?.dispose();
|
|
1952
1966
|
this.invalidate();
|
|
1953
1967
|
this.#stopWorkspaceAddedObserver?.();
|
|
1954
1968
|
this.#stopWorkspaceRemovedObserver?.();
|
|
@@ -2128,12 +2142,13 @@ export class NativeTerminalAttachmentRuntime {
|
|
|
2128
2142
|
resolveGeometry: (descriptor, client) => geometry.resolve(descriptor, client),
|
|
2129
2143
|
...(options.sessionRuntimeRegistry
|
|
2130
2144
|
? {
|
|
2131
|
-
bindSessionRuntime: (descriptor: AttachmentLeaseDescriptor) => {
|
|
2145
|
+
bindSessionRuntime: async (descriptor: AttachmentLeaseDescriptor) => {
|
|
2132
2146
|
const workspace = options.registry.get(descriptor.target.workspaceName);
|
|
2133
2147
|
if (!workspace) throw new Error("Terminal attachment workspace is unavailable");
|
|
2134
2148
|
if (!descriptor.hostClientId) {
|
|
2135
2149
|
throw new Error("Interactive terminal attachment lacks trusted host identity");
|
|
2136
2150
|
}
|
|
2151
|
+
await options.sessionRuntimeRegistry!.verifyWindowOwnership();
|
|
2137
2152
|
return new SessionRuntimeTransportBinder(options.sessionRuntimeRegistry!).bind({
|
|
2138
2153
|
transport: "terminal-attachment",
|
|
2139
2154
|
transportLeaseId: descriptor.leaseId,
|
|
@@ -2357,12 +2372,12 @@ export class NativeTerminalAttachmentRuntime {
|
|
|
2357
2372
|
const catalogIssue: NativeTerminalInventoryCatalogIssue | null = inventory.catalog
|
|
2358
2373
|
.invalidRuntimeProof
|
|
2359
2374
|
? "invalid-runtime-proof"
|
|
2360
|
-
: inventory.catalog.
|
|
2361
|
-
? "
|
|
2362
|
-
: inventory.catalog.
|
|
2363
|
-
? "
|
|
2364
|
-
: inventory.catalog.
|
|
2365
|
-
? "duplicate-
|
|
2375
|
+
: inventory.catalog.duplicateRuntimePaneBinding
|
|
2376
|
+
? "duplicate-runtime-pane-binding"
|
|
2377
|
+
: inventory.catalog.missingSemanticStamp
|
|
2378
|
+
? "missing-semantic-stamp"
|
|
2379
|
+
: inventory.catalog.duplicateSemanticStamp
|
|
2380
|
+
? "duplicate-semantic-stamp"
|
|
2366
2381
|
: null;
|
|
2367
2382
|
// Ground-truth agent facts (authority + scrape fallback). All IO stays here;
|
|
2368
2383
|
// the resource projector composes them purely. Absent probe → no facts →
|