tmux-ide 2.9.2 → 2.9.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/bin/cli.js +2071 -625
- package/package.json +14 -5
- 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/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/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/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
|
@@ -152,16 +152,16 @@ export function getLogStreamFailures(): readonly StreamName[] {
|
|
|
152
152
|
function markStreamFailed(name: StreamName, error: unknown): void {
|
|
153
153
|
if (streamState[name].failed) return;
|
|
154
154
|
streamState[name].failed = true;
|
|
155
|
-
const detail =
|
|
155
|
+
const detail = sanitizeLogMessage(
|
|
156
|
+
error instanceof Error ? `${error.name}: ${error.message}` : String(error),
|
|
157
|
+
secrets,
|
|
158
|
+
budget,
|
|
159
|
+
);
|
|
156
160
|
const warning = `[log.ts] ${name} write failed (${detail}); further records are retained in memory only\n`;
|
|
157
161
|
const other: StreamName = name === "stdout" ? "stderr" : "stdout";
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
} catch {
|
|
162
|
-
streamState[other].failed = true;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
162
|
+
// The fallback can fail asynchronously too. Install the same error handling
|
|
163
|
+
// before writing it; failed flags prevent a warning loop if both streams fail.
|
|
164
|
+
if (!streamState[other].failed) writeToStream(other, warning);
|
|
165
165
|
}
|
|
166
166
|
|
|
167
167
|
function writeToStream(name: StreamName, line: string): void {
|
|
@@ -221,7 +221,11 @@ function writeStructuredLog(
|
|
|
221
221
|
// Avoid recursion via the logger; write directly to stderr.
|
|
222
222
|
writeToStream(
|
|
223
223
|
"stderr",
|
|
224
|
-
`[log.ts] subscriber threw: ${
|
|
224
|
+
`[log.ts] subscriber threw: ${sanitizeLogMessage(
|
|
225
|
+
err instanceof Error ? err.message : String(err),
|
|
226
|
+
secrets,
|
|
227
|
+
budget,
|
|
228
|
+
)}\n`,
|
|
225
229
|
);
|
|
226
230
|
}
|
|
227
231
|
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { mkdtempSync, readFileSync, realpathSync, rmSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { execFileSync } from "node:child_process";
|
|
5
|
+
|
|
6
|
+
export const MANAGED_INSTALLER_URL = "https://tmux-ide.com/install.sh";
|
|
7
|
+
|
|
8
|
+
/** Only the active release of a marked installation may update its prefix. */
|
|
9
|
+
export function managedInstallerPrefix(cliPath: string): string | null {
|
|
10
|
+
const match =
|
|
11
|
+
/^(.*)\/share\/tmux-ide\/releases\/(install-[A-Za-z0-9]{6})\/npm\/lib\/node_modules\/tmux-ide\/bin$/.exec(
|
|
12
|
+
cliPath,
|
|
13
|
+
);
|
|
14
|
+
if (!match) return null;
|
|
15
|
+
const prefix = match[1] || "/";
|
|
16
|
+
const root = join(prefix, "share/tmux-ide");
|
|
17
|
+
const release = join(root, "releases", match[2]!);
|
|
18
|
+
try {
|
|
19
|
+
if (realpathSync(join(root, "current")) !== release) return null;
|
|
20
|
+
if (realpathSync(release) !== release) return null;
|
|
21
|
+
if (readFileSync(join(root, "installer-v1"), "utf8") !== "1\n") return null;
|
|
22
|
+
if (readFileSync(join(release, ".installer-release-v1"), "utf8") !== "1\n") return null;
|
|
23
|
+
if (
|
|
24
|
+
!readFileSync(join(prefix, "bin/tmux-ide"), "utf8")
|
|
25
|
+
.split("\n")
|
|
26
|
+
.includes("# tmux-ide universal installer v1")
|
|
27
|
+
)
|
|
28
|
+
return null;
|
|
29
|
+
if (
|
|
30
|
+
JSON.parse(readFileSync(join(dirname(cliPath), "package.json"), "utf8")).name !== "tmux-ide"
|
|
31
|
+
)
|
|
32
|
+
return null;
|
|
33
|
+
return prefix;
|
|
34
|
+
} catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Download completely before execution; the canonical installer owns activation and rollback. */
|
|
40
|
+
export function runManagedInstallerUpdate(
|
|
41
|
+
prefix: string,
|
|
42
|
+
channel: string,
|
|
43
|
+
json: boolean,
|
|
44
|
+
execute: typeof execFileSync = execFileSync,
|
|
45
|
+
): void {
|
|
46
|
+
const temporary = mkdtempSync(join(tmpdir(), "tmux-ide-update-"));
|
|
47
|
+
const script = join(temporary, "install.sh");
|
|
48
|
+
try {
|
|
49
|
+
execute(
|
|
50
|
+
"curl",
|
|
51
|
+
[
|
|
52
|
+
"--fail",
|
|
53
|
+
"--location",
|
|
54
|
+
"--silent",
|
|
55
|
+
"--show-error",
|
|
56
|
+
"--proto",
|
|
57
|
+
"=https",
|
|
58
|
+
"--proto-redir",
|
|
59
|
+
"=https",
|
|
60
|
+
"--connect-timeout",
|
|
61
|
+
"15",
|
|
62
|
+
"--max-time",
|
|
63
|
+
"180",
|
|
64
|
+
"--output",
|
|
65
|
+
script,
|
|
66
|
+
MANAGED_INSTALLER_URL,
|
|
67
|
+
],
|
|
68
|
+
{ stdio: ["ignore", 2, 2] },
|
|
69
|
+
);
|
|
70
|
+
if (!readFileSync(script, "utf8").startsWith("#!/bin/sh\n"))
|
|
71
|
+
throw new Error("Installer download was not a shell script; installation unchanged");
|
|
72
|
+
execute("/bin/sh", ["-n", script], { stdio: ["ignore", 2, 2] });
|
|
73
|
+
execute("/bin/sh", [script, "--prefix", prefix, "--version", channel], {
|
|
74
|
+
stdio: json ? ["ignore", 2, 2] : "inherit",
|
|
75
|
+
});
|
|
76
|
+
} finally {
|
|
77
|
+
rmSync(temporary, { recursive: true, force: true });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -26,7 +26,7 @@ function trusted(path: string, directory: boolean) {
|
|
|
26
26
|
(value.mode & 0o777) !== (directory ? 0o700 : 0o600) ||
|
|
27
27
|
(!directory && value.nlink !== 1)
|
|
28
28
|
)
|
|
29
|
-
throw new Error("unsafe
|
|
29
|
+
throw new Error("unsafe private operation lock");
|
|
30
30
|
return value;
|
|
31
31
|
}
|
|
32
32
|
function readOwner(path: string): { pid: number; ino: number; dev: number } {
|
|
@@ -35,15 +35,15 @@ function readOwner(path: string): { pid: number; ino: number; dev: number } {
|
|
|
35
35
|
try {
|
|
36
36
|
const actual = fstatSync(fd);
|
|
37
37
|
if (actual.ino !== before.ino || actual.dev !== before.dev || actual.size > 32) {
|
|
38
|
-
throw new Error("unsafe
|
|
38
|
+
throw new Error("unsafe private operation lock");
|
|
39
39
|
}
|
|
40
40
|
const bytes = Buffer.alloc(33);
|
|
41
41
|
const length = readSync(fd, bytes, 0, bytes.length, 0);
|
|
42
42
|
const text = bytes.subarray(0, length).toString("utf8");
|
|
43
|
-
if (!/^[1-9][0-9]*\n$/.test(text)) throw new Error("invalid
|
|
43
|
+
if (!/^[1-9][0-9]*\n$/.test(text)) throw new Error("invalid private operation lock owner");
|
|
44
44
|
const pid = Number(text.trim());
|
|
45
45
|
if (!Number.isSafeInteger(pid) || pid > 2147483647)
|
|
46
|
-
throw new Error("invalid
|
|
46
|
+
throw new Error("invalid private operation lock owner");
|
|
47
47
|
return { pid, ino: actual.ino, dev: actual.dev };
|
|
48
48
|
} finally {
|
|
49
49
|
closeSync(fd);
|
|
@@ -88,7 +88,7 @@ function cleanStaging(
|
|
|
88
88
|
try {
|
|
89
89
|
const current = trusted(path, true);
|
|
90
90
|
if (current.ino !== expected.ino || current.dev !== expected.dev)
|
|
91
|
-
throw new Error("
|
|
91
|
+
throw new Error("private operation staging lock changed");
|
|
92
92
|
try {
|
|
93
93
|
unlinkSync(join(path, name));
|
|
94
94
|
} catch (error) {
|
|
@@ -102,7 +102,10 @@ function cleanStaging(
|
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
/** Publish a populated private directory atomically; age never proves owner death. */
|
|
105
|
-
export async function
|
|
105
|
+
export async function acquirePrivateOperationLock(
|
|
106
|
+
lock: string,
|
|
107
|
+
waitMs: number,
|
|
108
|
+
): Promise<() => void> {
|
|
106
109
|
const deadline = Date.now() + waitMs;
|
|
107
110
|
const nonce = randomUUID();
|
|
108
111
|
const name = `owner-${nonce}`;
|
|
@@ -116,7 +119,7 @@ export async function acquireTuiDownloadLock(lock: string, waitMs: number): Prom
|
|
|
116
119
|
writeFileSync(join(staging, name), `${process.pid}\n`, { flag: "wx", mode: 0o600 });
|
|
117
120
|
while (true) {
|
|
118
121
|
if (attempted && Date.now() >= deadline)
|
|
119
|
-
throw new Error("timed out waiting for another
|
|
122
|
+
throw new Error("timed out waiting for another private operation");
|
|
120
123
|
attempted = true;
|
|
121
124
|
try {
|
|
122
125
|
const existing = lstatSync(lock);
|
|
@@ -154,7 +157,7 @@ export async function acquireTuiDownloadLock(lock: string, waitMs: number): Prom
|
|
|
154
157
|
}
|
|
155
158
|
if (names.length !== 1 || !/^owner-[0-9a-f-]{36}$/.test(names[0]!)) {
|
|
156
159
|
// Empty is only a retirement gap; the next atomic rename can claim it.
|
|
157
|
-
if (names.length !== 0) throw new Error("invalid
|
|
160
|
+
if (names.length !== 0) throw new Error("invalid private operation lock inventory");
|
|
158
161
|
} else {
|
|
159
162
|
const owner = readOwner(join(lock, names[0]!));
|
|
160
163
|
if (dead(owner.pid)) {
|
|
@@ -164,7 +167,7 @@ export async function acquireTuiDownloadLock(lock: string, waitMs: number): Prom
|
|
|
164
167
|
current.dev !== owner.dev ||
|
|
165
168
|
current.pid !== owner.pid
|
|
166
169
|
) {
|
|
167
|
-
throw new Error("
|
|
170
|
+
throw new Error("private operation lock changed");
|
|
168
171
|
}
|
|
169
172
|
retire(lock, names[0]!, entry);
|
|
170
173
|
}
|
|
@@ -182,7 +185,7 @@ export async function acquireTuiDownloadLock(lock: string, waitMs: number): Prom
|
|
|
182
185
|
current.dev !== owner.dev ||
|
|
183
186
|
current.pid !== owner.pid
|
|
184
187
|
) {
|
|
185
|
-
throw new Error("
|
|
188
|
+
throw new Error("private operation lock changed");
|
|
186
189
|
}
|
|
187
190
|
try {
|
|
188
191
|
unlinkSync(lock);
|
|
@@ -194,7 +197,8 @@ export async function acquireTuiDownloadLock(lock: string, waitMs: number): Prom
|
|
|
194
197
|
} catch (error) {
|
|
195
198
|
if (code(error) !== "ENOENT") throw error;
|
|
196
199
|
}
|
|
197
|
-
if (Date.now() >= deadline)
|
|
200
|
+
if (Date.now() >= deadline)
|
|
201
|
+
throw new Error("timed out waiting for another private operation");
|
|
198
202
|
await new Promise((resolve) =>
|
|
199
203
|
setTimeout(resolve, Math.min(50, Math.max(0, deadline - Date.now()))),
|
|
200
204
|
);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type CanonicalDaemonInfo } from "@tmux-ide/contracts";
|
|
1
|
+
import { DAEMON_WIRE_PROTOCOL_VERSION, type CanonicalDaemonInfo } from "@tmux-ide/contracts";
|
|
2
2
|
import {
|
|
3
3
|
canonicalDaemonUrl,
|
|
4
4
|
isCanonicalDaemonAlive,
|
|
@@ -35,9 +35,9 @@ export async function readRemoteDaemonHandshake(
|
|
|
35
35
|
"daemon-missing",
|
|
36
36
|
);
|
|
37
37
|
const parsed = RemoteDaemonHandshakeSchema.safeParse({ version: 1, daemon: info });
|
|
38
|
-
if (!parsed.success)
|
|
38
|
+
if (!parsed.success || info.protocolVersion !== DAEMON_WIRE_PROTOCOL_VERSION)
|
|
39
39
|
throw new RemoteDaemonInfoError(
|
|
40
|
-
"The daemon does not support authenticated SSH discovery.",
|
|
40
|
+
"The daemon does not support this authenticated SSH discovery protocol.",
|
|
41
41
|
"incompatible",
|
|
42
42
|
);
|
|
43
43
|
try {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { SavedMachineSchema } from "@tmux-ide/contracts/saved-machines";
|
|
3
|
+
import { remoteTmuxIdeCommand } from "./remote-tmux-command.ts";
|
|
3
4
|
|
|
4
5
|
/** Only explicit user actions call this; catalog discovery and retries never do. */
|
|
5
6
|
export async function startInstalledRemoteDaemon(
|
|
@@ -21,12 +22,11 @@ export async function startInstalledRemoteDaemon(
|
|
|
21
22
|
"BatchMode=yes",
|
|
22
23
|
"-o",
|
|
23
24
|
"ConnectTimeout=10",
|
|
25
|
+
"-o",
|
|
26
|
+
"ForkAfterAuthentication=no",
|
|
24
27
|
"--",
|
|
25
28
|
alias,
|
|
26
|
-
"
|
|
27
|
-
"update",
|
|
28
|
-
"--daemon",
|
|
29
|
-
"--json",
|
|
29
|
+
remoteTmuxIdeCommand("start"),
|
|
30
30
|
],
|
|
31
31
|
{ stdio: ["ignore", "pipe", "pipe"] },
|
|
32
32
|
);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { shellEscape } from "./shell.ts";
|
|
2
|
+
|
|
3
|
+
/** Fixed commands only. SSH normally omits interactive shell PATH setup. */
|
|
4
|
+
export function remoteTmuxIdeCommand(operation: "discover" | "start"): string {
|
|
5
|
+
const args = operation === "discover" ? "remote-daemon-info --json" : "update --daemon --json";
|
|
6
|
+
// Respect the host's existing installation preference, then try the installer
|
|
7
|
+
// and common package-manager prefixes. Never source shell profiles.
|
|
8
|
+
const script =
|
|
9
|
+
'export PATH="${PATH:-/usr/bin:/bin}:$HOME/.local/bin:/opt/homebrew/bin:/usr/local/bin:/home/linuxbrew/.linuxbrew/bin:$HOME/.npm-global/bin"; ' +
|
|
10
|
+
`exec tmux-ide ${args}`;
|
|
11
|
+
return `/bin/sh -c ${shellEscape(script)}`;
|
|
12
|
+
}
|
|
@@ -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
|
}
|