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
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import {
|
|
2
|
+
accessSync,
|
|
3
|
+
constants,
|
|
4
|
+
openSync,
|
|
5
|
+
closeSync,
|
|
6
|
+
fstatSync,
|
|
7
|
+
mkdirSync,
|
|
8
|
+
readFileSync,
|
|
9
|
+
unlinkSync,
|
|
10
|
+
writeFileSync,
|
|
11
|
+
renameSync,
|
|
12
|
+
rmSync,
|
|
13
|
+
realpathSync,
|
|
14
|
+
linkSync,
|
|
15
|
+
} from "node:fs";
|
|
16
|
+
import { randomUUID } from "node:crypto";
|
|
17
|
+
import { dirname, join } from "node:path";
|
|
18
|
+
import { homedir } from "node:os";
|
|
19
|
+
import { z } from "zod";
|
|
20
|
+
import {
|
|
21
|
+
getCanonicalDaemonInfoPath,
|
|
22
|
+
inspectCanonicalDaemonInfo,
|
|
23
|
+
probeCanonicalDaemonIdentity,
|
|
24
|
+
releaseCanonicalDaemonSupervision,
|
|
25
|
+
reserveCanonicalDaemonSupervision,
|
|
26
|
+
type CanonicalDaemonInfoState,
|
|
27
|
+
} from "./canonical-daemon.ts";
|
|
28
|
+
import { createDaemonServiceManager } from "./daemon-service-manager.ts";
|
|
29
|
+
import { planDaemonService, type DaemonServicePlan } from "./daemon-service-plan.ts";
|
|
30
|
+
import { resolveRuntimeNamespace } from "./runtime-namespace.ts";
|
|
31
|
+
import { IdeError } from "./errors.ts";
|
|
32
|
+
import { acquirePrivateOperationLock } from "./private-operation-lock.ts";
|
|
33
|
+
|
|
34
|
+
const RecordSchema = z.strictObject({
|
|
35
|
+
version: z.literal(1),
|
|
36
|
+
removing: z.boolean().optional(),
|
|
37
|
+
executable: z.string().min(1),
|
|
38
|
+
contents: z
|
|
39
|
+
.string()
|
|
40
|
+
.min(1)
|
|
41
|
+
.max(64 * 1024),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
export interface DaemonServiceDependencies {
|
|
45
|
+
plan(executable: string): DaemonServicePlan;
|
|
46
|
+
manager: typeof createDaemonServiceManager;
|
|
47
|
+
inspect: typeof inspectCanonicalDaemonInfo;
|
|
48
|
+
reserve: typeof reserveCanonicalDaemonSupervision;
|
|
49
|
+
release: typeof releaseCanonicalDaemonSupervision;
|
|
50
|
+
probe: typeof probeCanonicalDaemonIdentity;
|
|
51
|
+
recordPath: string;
|
|
52
|
+
waitMs: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function defaults(): DaemonServiceDependencies {
|
|
56
|
+
const namespace = resolveRuntimeNamespace();
|
|
57
|
+
return {
|
|
58
|
+
plan: (executable) =>
|
|
59
|
+
planDaemonService({
|
|
60
|
+
platform: process.platform,
|
|
61
|
+
uid: process.getuid?.() ?? 0,
|
|
62
|
+
home: homedir(),
|
|
63
|
+
configHome: process.env.XDG_CONFIG_HOME || undefined,
|
|
64
|
+
executable,
|
|
65
|
+
path: process.env.PATH || "/usr/bin:/bin",
|
|
66
|
+
namespace,
|
|
67
|
+
}),
|
|
68
|
+
manager: createDaemonServiceManager,
|
|
69
|
+
inspect: inspectCanonicalDaemonInfo,
|
|
70
|
+
reserve: reserveCanonicalDaemonSupervision,
|
|
71
|
+
release: releaseCanonicalDaemonSupervision,
|
|
72
|
+
probe: probeCanonicalDaemonIdentity,
|
|
73
|
+
recordPath: join(dirname(getCanonicalDaemonInfoPath()), "service.json"),
|
|
74
|
+
waitMs: 30_000,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function refused(message: string): never {
|
|
79
|
+
throw new IdeError(message, { code: "DAEMON_SERVICE_REFUSED" });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function ownedFile(path: string): string | null {
|
|
83
|
+
let descriptor: number | undefined;
|
|
84
|
+
try {
|
|
85
|
+
descriptor = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
86
|
+
const stat = fstatSync(descriptor);
|
|
87
|
+
if (
|
|
88
|
+
!stat.isFile() ||
|
|
89
|
+
stat.uid !== process.getuid?.() ||
|
|
90
|
+
(stat.mode & 0o022) !== 0 ||
|
|
91
|
+
stat.size > 128 * 1024
|
|
92
|
+
)
|
|
93
|
+
refused(`Service file is not a bounded, user-owned regular file: ${path}`);
|
|
94
|
+
return readFileSync(descriptor, "utf8");
|
|
95
|
+
} catch (error) {
|
|
96
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
|
|
97
|
+
throw error;
|
|
98
|
+
} finally {
|
|
99
|
+
if (descriptor !== undefined) closeSync(descriptor);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function boundTo(state: CanonicalDaemonInfoState, id: string): boolean {
|
|
104
|
+
return state.status === "reserved"
|
|
105
|
+
? state.reservation.supervisionId === id
|
|
106
|
+
: state.status === "valid" && state.info.supervisionId === id;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Explicit OS supervision. Never starts a detached daemon or signals a guessed PID. */
|
|
110
|
+
export async function manageDaemonService(
|
|
111
|
+
action: "install" | "status" | "restart" | "remove",
|
|
112
|
+
executable?: string,
|
|
113
|
+
deps: DaemonServiceDependencies = defaults(),
|
|
114
|
+
) {
|
|
115
|
+
const previousRecord = ownedFile(deps.recordPath);
|
|
116
|
+
const record = previousRecord === null ? null : RecordSchema.parse(JSON.parse(previousRecord));
|
|
117
|
+
if (record?.removing && action !== "remove" && action !== "status")
|
|
118
|
+
refused("Service removal is incomplete; finish remove before installing or restarting");
|
|
119
|
+
if (!record && action !== "install") {
|
|
120
|
+
if (action === "status") return { status: "not-installed" as const };
|
|
121
|
+
refused("No managed service is installed in this daemon namespace");
|
|
122
|
+
}
|
|
123
|
+
if (action === "install" && !executable)
|
|
124
|
+
refused("Service installation requires an absolute stable CLI launcher path");
|
|
125
|
+
if (record && executable && record.executable !== executable)
|
|
126
|
+
refused(
|
|
127
|
+
"A different launcher is already registered; remove the stopped service before changing it",
|
|
128
|
+
);
|
|
129
|
+
const plan = deps.plan(record?.executable ?? executable!);
|
|
130
|
+
if (plan.recordPath !== deps.recordPath) refused("Service namespace changed during resolution");
|
|
131
|
+
const contents = record?.contents ?? plan.contents;
|
|
132
|
+
const manager = deps.manager(plan);
|
|
133
|
+
const assertFiles = (allowMissingUnit = false) => {
|
|
134
|
+
const unit = ownedFile(plan.unitPath);
|
|
135
|
+
if (unit !== contents && !((record?.removing || allowMissingUnit) && unit === null))
|
|
136
|
+
refused("Service definition is missing or modified; refusing to control it");
|
|
137
|
+
if (record && ownedFile(plan.recordPath) !== previousRecord)
|
|
138
|
+
refused("Service ownership record changed");
|
|
139
|
+
};
|
|
140
|
+
const inspectManager = async () => {
|
|
141
|
+
const state = await manager.inspect();
|
|
142
|
+
// systemd reports its generated search-path symlink for units enabled by
|
|
143
|
+
// absolute path. Resolve that link to the owned definition; the definition
|
|
144
|
+
// itself still has to pass our no-follow private-file and content checks.
|
|
145
|
+
if (
|
|
146
|
+
state.loaded &&
|
|
147
|
+
(!state.definitionPath || realpathSync(state.definitionPath) !== realpathSync(plan.unitPath))
|
|
148
|
+
)
|
|
149
|
+
refused("The service manager loaded a different definition; refusing to control it");
|
|
150
|
+
return state;
|
|
151
|
+
};
|
|
152
|
+
const ready = async (previousInstance?: string) => {
|
|
153
|
+
const deadline = Date.now() + deps.waitMs;
|
|
154
|
+
do {
|
|
155
|
+
const state = deps.inspect();
|
|
156
|
+
if (!boundTo(state, plan.supervisionId))
|
|
157
|
+
refused("Service reservation changed during startup");
|
|
158
|
+
if (state.status === "valid" && state.info.instanceId !== previousInstance) {
|
|
159
|
+
const identity = await deps.probe(state.info);
|
|
160
|
+
const service = await inspectManager();
|
|
161
|
+
if (
|
|
162
|
+
identity &&
|
|
163
|
+
service.active &&
|
|
164
|
+
service.pid === state.info.pid &&
|
|
165
|
+
identity.pid === state.info.pid &&
|
|
166
|
+
identity.instanceId === state.info.instanceId &&
|
|
167
|
+
identity.startedAt === state.info.startedAt &&
|
|
168
|
+
identity.protocolVersion === state.info.protocolVersion
|
|
169
|
+
)
|
|
170
|
+
return {
|
|
171
|
+
status: "running" as const,
|
|
172
|
+
manager: plan.manager,
|
|
173
|
+
target: plan.target,
|
|
174
|
+
pid: state.info.pid,
|
|
175
|
+
instanceId: state.info.instanceId,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
179
|
+
} while (Date.now() < deadline);
|
|
180
|
+
refused(
|
|
181
|
+
"Service did not publish a verified daemon before the deadline; inspect service logs, then retry restart or remove",
|
|
182
|
+
);
|
|
183
|
+
};
|
|
184
|
+
await manager.available();
|
|
185
|
+
const service = await inspectManager();
|
|
186
|
+
const allowMissingUnit = action === "remove" && !service.loaded;
|
|
187
|
+
if (record) assertFiles(allowMissingUnit);
|
|
188
|
+
if (action === "status") {
|
|
189
|
+
const state = deps.inspect();
|
|
190
|
+
const identity =
|
|
191
|
+
service.active && state.status === "valid" && boundTo(state, plan.supervisionId)
|
|
192
|
+
? await deps.probe(state.info)
|
|
193
|
+
: null;
|
|
194
|
+
const verified =
|
|
195
|
+
identity !== null &&
|
|
196
|
+
state.status === "valid" &&
|
|
197
|
+
service.pid === state.info.pid &&
|
|
198
|
+
identity.pid === state.info.pid &&
|
|
199
|
+
identity.instanceId === state.info.instanceId &&
|
|
200
|
+
identity.startedAt === state.info.startedAt &&
|
|
201
|
+
identity.protocolVersion === state.info.protocolVersion;
|
|
202
|
+
return {
|
|
203
|
+
status: verified
|
|
204
|
+
? ("running" as const)
|
|
205
|
+
: service.active
|
|
206
|
+
? ("active-unverified" as const)
|
|
207
|
+
: ("stopped" as const),
|
|
208
|
+
manager: plan.manager,
|
|
209
|
+
target: plan.target,
|
|
210
|
+
pid: service.pid,
|
|
211
|
+
reservationMatches: boundTo(state, plan.supervisionId),
|
|
212
|
+
unitPath: plan.unitPath,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
if (!record && (service.loaded || ownedFile(plan.unitPath) !== null))
|
|
216
|
+
refused("A service definition already exists without this namespace's ownership record");
|
|
217
|
+
if (
|
|
218
|
+
record &&
|
|
219
|
+
!boundTo(deps.inspect(), plan.supervisionId) &&
|
|
220
|
+
!(record.removing && action === "remove" && deps.inspect().status === "missing")
|
|
221
|
+
)
|
|
222
|
+
refused("Service reservation is missing or belongs to another owner");
|
|
223
|
+
if (!record) {
|
|
224
|
+
accessSync(plan.executable, constants.X_OK);
|
|
225
|
+
// Canonical reservation admission rejects live, uncertain and foreign owners.
|
|
226
|
+
deps.reserve(plan.supervisionId);
|
|
227
|
+
}
|
|
228
|
+
const lock = `${plan.recordPath}.lock`;
|
|
229
|
+
const releaseLock = await acquirePrivateOperationLock(lock, 1000);
|
|
230
|
+
let wroteRecord = false;
|
|
231
|
+
let wroteUnit = false;
|
|
232
|
+
let activationAttempted = false;
|
|
233
|
+
try {
|
|
234
|
+
if (record) assertFiles(allowMissingUnit);
|
|
235
|
+
if (action === "install" && !record) {
|
|
236
|
+
writeFileSync(
|
|
237
|
+
plan.recordPath,
|
|
238
|
+
JSON.stringify({ version: 1, executable: plan.executable, contents }) + "\n",
|
|
239
|
+
{ flag: "wx", mode: 0o600 },
|
|
240
|
+
);
|
|
241
|
+
wroteRecord = true;
|
|
242
|
+
mkdirSync(dirname(plan.unitPath), { recursive: true, mode: 0o700 });
|
|
243
|
+
const unitStage = `${plan.unitPath}.${randomUUID()}.tmp`;
|
|
244
|
+
writeFileSync(unitStage, contents, { flag: "wx", mode: 0o600 });
|
|
245
|
+
try {
|
|
246
|
+
linkSync(unitStage, plan.unitPath);
|
|
247
|
+
} finally {
|
|
248
|
+
unlinkSync(unitStage);
|
|
249
|
+
}
|
|
250
|
+
wroteUnit = true;
|
|
251
|
+
activationAttempted = true;
|
|
252
|
+
await manager.install();
|
|
253
|
+
return await ready();
|
|
254
|
+
}
|
|
255
|
+
if (action === "remove") {
|
|
256
|
+
if (service.loaded) await manager.stop();
|
|
257
|
+
let stopped = await inspectManager();
|
|
258
|
+
const stopDeadline = Date.now() + deps.waitMs;
|
|
259
|
+
while ((stopped.active || stopped.pid !== null) && Date.now() < stopDeadline) {
|
|
260
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
261
|
+
stopped = await inspectManager();
|
|
262
|
+
}
|
|
263
|
+
if (stopped.active || stopped.pid !== null)
|
|
264
|
+
refused("Service manager has not confirmed the daemon stopped; reservation retained");
|
|
265
|
+
assertFiles(allowMissingUnit);
|
|
266
|
+
const removalRecord = JSON.stringify({ ...record!, removing: true }) + "\n";
|
|
267
|
+
const temporaryRecord = `${plan.recordPath}.${randomUUID()}.tmp`;
|
|
268
|
+
writeFileSync(temporaryRecord, removalRecord, { flag: "wx", mode: 0o600 });
|
|
269
|
+
try {
|
|
270
|
+
if (ownedFile(plan.recordPath) !== previousRecord)
|
|
271
|
+
refused("Service ownership record changed during removal");
|
|
272
|
+
renameSync(temporaryRecord, plan.recordPath);
|
|
273
|
+
} finally {
|
|
274
|
+
rmSync(temporaryRecord, { force: true });
|
|
275
|
+
}
|
|
276
|
+
if (ownedFile(plan.unitPath) !== null) unlinkSync(plan.unitPath);
|
|
277
|
+
await manager.reload();
|
|
278
|
+
if ((await inspectManager()).loaded)
|
|
279
|
+
refused("Service remains loaded after removal; reservation retained");
|
|
280
|
+
if (deps.inspect().status !== "missing") deps.release(plan.supervisionId);
|
|
281
|
+
unlinkSync(plan.recordPath);
|
|
282
|
+
return { status: "removed" as const, target: plan.target };
|
|
283
|
+
}
|
|
284
|
+
const before = deps.inspect();
|
|
285
|
+
if (action === "install" && service.active) return await ready();
|
|
286
|
+
if (service.loaded) await manager.restart(service.pid !== null);
|
|
287
|
+
else await manager.install();
|
|
288
|
+
return await ready(before.status === "valid" ? before.info.instanceId : undefined);
|
|
289
|
+
} catch (error) {
|
|
290
|
+
// Never discard ownership after an uncertain OS activation. The retained
|
|
291
|
+
// record/reservation gives the explicit remove/restart commands a recovery path.
|
|
292
|
+
if (!activationAttempted && wroteRecord) {
|
|
293
|
+
if (wroteUnit && ownedFile(plan.unitPath) === contents) unlinkSync(plan.unitPath);
|
|
294
|
+
unlinkSync(plan.recordPath);
|
|
295
|
+
deps.release(plan.supervisionId);
|
|
296
|
+
}
|
|
297
|
+
throw error;
|
|
298
|
+
} finally {
|
|
299
|
+
releaseLock();
|
|
300
|
+
}
|
|
301
|
+
}
|
|
@@ -2,6 +2,7 @@ import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
export type InstallOrigin =
|
|
5
|
+
| "installer"
|
|
5
6
|
| "npm"
|
|
6
7
|
| "pnpm"
|
|
7
8
|
| "bun"
|
|
@@ -12,6 +13,10 @@ export type InstallOrigin =
|
|
|
12
13
|
| "unknown";
|
|
13
14
|
/** Conservative layout evidence; an arbitrary node_modules directory is not a global install. */
|
|
14
15
|
export function detectPackageManager(path: string): Exclude<InstallOrigin, "dev"> {
|
|
16
|
+
if (
|
|
17
|
+
/\/share\/tmux-ide\/releases\/install-[^/]+\/npm\/lib\/node_modules\/tmux-ide\/bin$/.test(path)
|
|
18
|
+
)
|
|
19
|
+
return "installer";
|
|
15
20
|
if (/\/(?:Cellar|Caskroom)\//.test(path)) return "homebrew";
|
|
16
21
|
if (/\/_npx\//.test(path)) return "npx";
|
|
17
22
|
if (/\/(?:\.yarn|yarn)\//.test(path)) return "yarn";
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
SavedMachineRegistrySchema,
|
|
3
|
+
type SavedMachine,
|
|
4
|
+
SavedMachineMutationSchema,
|
|
5
|
+
type SavedMachineMutation,
|
|
6
|
+
} from "@tmux-ide/contracts/saved-machines";
|
|
2
7
|
import {
|
|
3
8
|
canonicalDaemonUrl,
|
|
4
9
|
isCanonicalDaemonAlive,
|
|
@@ -8,6 +13,17 @@ import {
|
|
|
8
13
|
/** Intentional local registry mutation. Never uses selected-machine authority. */
|
|
9
14
|
export async function saveMachineProfiles(machines: readonly SavedMachine[]) {
|
|
10
15
|
const registry = SavedMachineRegistrySchema.parse({ version: 1, machines });
|
|
16
|
+
return writeLocalMachineRegistry("POST", { registry });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function mutateMachineProfile(change: SavedMachineMutation) {
|
|
20
|
+
return writeLocalMachineRegistry("PATCH", { change: SavedMachineMutationSchema.parse(change) });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function writeLocalMachineRegistry(
|
|
24
|
+
method: "POST" | "PATCH",
|
|
25
|
+
payload: Record<string, unknown>,
|
|
26
|
+
) {
|
|
11
27
|
const daemon = readCanonicalDaemonInfo();
|
|
12
28
|
if (!daemon?.authToken || !(await isCanonicalDaemonAlive(daemon)))
|
|
13
29
|
throw new Error("Start a local tmux-ide daemon to save machine profiles.");
|
|
@@ -16,14 +32,14 @@ export async function saveMachineProfiles(machines: readonly SavedMachine[]) {
|
|
|
16
32
|
canonicalDaemonUrl("http", daemon.bindHostname, daemon.port) +
|
|
17
33
|
"/api/resources/saved-machines",
|
|
18
34
|
{
|
|
19
|
-
method
|
|
35
|
+
method,
|
|
20
36
|
redirect: "error",
|
|
21
37
|
signal: AbortSignal.timeout(5000),
|
|
22
38
|
headers: {
|
|
23
39
|
Authorization: `Bearer ${daemon.authToken}`,
|
|
24
40
|
"Content-Type": "application/json",
|
|
25
41
|
},
|
|
26
|
-
body: JSON.stringify({ expectedInstanceId: daemon.instanceId,
|
|
42
|
+
body: JSON.stringify({ expectedInstanceId: daemon.instanceId, ...payload }),
|
|
27
43
|
},
|
|
28
44
|
);
|
|
29
45
|
if (!response.ok) {
|
|
@@ -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
|
+
}
|