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
|
@@ -10,12 +10,14 @@ import type {
|
|
|
10
10
|
TerminalReplicaTombstonePayload,
|
|
11
11
|
} from "@tmux-ide/contracts";
|
|
12
12
|
import { consumeCompactReplicaCapability } from "./terminal-compact-capability.ts";
|
|
13
|
+
import { freezeOwnedTerminalReplicaRow as freezeRow } from "./terminal-replica-owned-row.ts";
|
|
13
14
|
import {
|
|
14
15
|
hashCanonicalTerminalValue,
|
|
15
16
|
hashCanonicalTerminalValueCooperatively,
|
|
16
17
|
hashTerminalReplicaRowCached,
|
|
17
18
|
hashTerminalReplicaRowCooperatively,
|
|
18
19
|
isTerminalReplicaRowDeeplyFrozen,
|
|
20
|
+
TerminalReplicaRunEncodingCache,
|
|
19
21
|
} from "./terminal-replica-hash-cache.ts";
|
|
20
22
|
|
|
21
23
|
// Only rows validated and copied by this module can bypass a repeated copy.
|
|
@@ -835,27 +837,11 @@ function blankRow(cols: number): TerminalReplicaRow {
|
|
|
835
837
|
}) as unknown as TerminalReplicaRow;
|
|
836
838
|
}
|
|
837
839
|
|
|
838
|
-
function freezeRow(row: TerminalReplicaRow): TerminalReplicaRow {
|
|
839
|
-
return Object.freeze({
|
|
840
|
-
wrapped: row.wrapped,
|
|
841
|
-
cells: Object.freeze(
|
|
842
|
-
row.cells.map((cell) =>
|
|
843
|
-
Object.freeze({
|
|
844
|
-
...cell,
|
|
845
|
-
foreground: Object.freeze({ ...cell.foreground }),
|
|
846
|
-
background: Object.freeze({ ...cell.background }),
|
|
847
|
-
}),
|
|
848
|
-
),
|
|
849
|
-
),
|
|
850
|
-
}) as unknown as TerminalReplicaRow;
|
|
851
|
-
}
|
|
852
|
-
|
|
853
840
|
function validateAndFreezeRow(
|
|
854
841
|
row: TerminalReplicaRow,
|
|
855
842
|
profile?: MutableTerminalReplicaApplyProfile,
|
|
856
843
|
): TerminalReplicaRow {
|
|
857
844
|
if (VALIDATED_PATCH_ROWS.has(row)) return row;
|
|
858
|
-
const cells = new Array(row.cells.length);
|
|
859
845
|
for (let index = 0; index < row.cells.length; index += 1) {
|
|
860
846
|
const cell = row.cells[index]!;
|
|
861
847
|
if (profile) profile.counts.validatedCells += 1;
|
|
@@ -863,17 +849,11 @@ function validateAndFreezeRow(
|
|
|
863
849
|
throw new TypeError("Malformed terminal replica row");
|
|
864
850
|
if (cell.width === 0 && (index === 0 || row.cells[index - 1]?.width !== 2))
|
|
865
851
|
throw new TypeError("Malformed terminal replica row");
|
|
866
|
-
cells[index] = Object.freeze({
|
|
867
|
-
...cell,
|
|
868
|
-
foreground: Object.freeze({ ...cell.foreground }),
|
|
869
|
-
background: Object.freeze({ ...cell.background }),
|
|
870
|
-
});
|
|
871
|
-
if (profile) profile.counts.frozenCells += 1;
|
|
872
852
|
}
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
853
|
+
// Use the same copy owner as seed admission and encoding. Already owned rows
|
|
854
|
+
// retain identity; external rows still receive an immutable detached copy.
|
|
855
|
+
const frozen = freezeRow(row);
|
|
856
|
+
if (profile && frozen !== row) profile.counts.frozenCells += row.cells.length;
|
|
877
857
|
VALIDATED_PATCH_ROWS.add(frozen);
|
|
878
858
|
return frozen;
|
|
879
859
|
}
|
|
@@ -1034,11 +1014,13 @@ function hashTerminalReplicaRow(
|
|
|
1034
1014
|
row: TerminalReplicaRow,
|
|
1035
1015
|
profile?: MutableTerminalReplicaApplyProfile,
|
|
1036
1016
|
instrumentation?: TerminalReplicaApplyOptions["instrumentation"],
|
|
1017
|
+
encodingCache?: TerminalReplicaRunEncodingCache,
|
|
1037
1018
|
): string {
|
|
1038
1019
|
const started = readProfileClock(instrumentation);
|
|
1039
1020
|
const hash = hashTerminalReplicaRowCached(
|
|
1040
1021
|
row,
|
|
1041
1022
|
profile ? () => (profile.counts.rowHashMisses += 1) : undefined,
|
|
1023
|
+
encodingCache,
|
|
1042
1024
|
);
|
|
1043
1025
|
addProfileDuration(profile, "rowHash", started, instrumentation);
|
|
1044
1026
|
return hash;
|
|
@@ -1051,12 +1033,13 @@ function hashTerminalReplicaRows(
|
|
|
1051
1033
|
): string {
|
|
1052
1034
|
const cached = ROW_ARRAY_HASH_CACHE.get(rows);
|
|
1053
1035
|
if (cached) return cached.hash.toString(16).padStart(16, "0");
|
|
1036
|
+
const encodingCache = rows.length >= 64 ? new TerminalReplicaRunEncodingCache() : undefined;
|
|
1054
1037
|
let hash = 0n;
|
|
1055
1038
|
for (const row of rows)
|
|
1056
1039
|
hash = BigInt.asUintN(
|
|
1057
1040
|
64,
|
|
1058
1041
|
hash * ROW_SEQUENCE_BASE +
|
|
1059
|
-
BigInt(`0x${hashTerminalReplicaRow(row, profile, instrumentation)}`),
|
|
1042
|
+
BigInt(`0x${hashTerminalReplicaRow(row, profile, instrumentation, encodingCache)}`),
|
|
1060
1043
|
);
|
|
1061
1044
|
if (Object.isFrozen(rows) && rows.every(isTerminalReplicaRowDeeplyFrozen))
|
|
1062
1045
|
ROW_ARRAY_HASH_CACHE.set(rows, { hash, length: rows.length });
|
|
@@ -1099,10 +1082,12 @@ export function registerTerminalReplicaRowsDeltaHash(
|
|
|
1099
1082
|
);
|
|
1100
1083
|
hash = BigInt.asUintN(64, hash - contribution);
|
|
1101
1084
|
}
|
|
1085
|
+
const encodingCache = append.length >= 64 ? new TerminalReplicaRunEncodingCache() : undefined;
|
|
1102
1086
|
for (const row of append)
|
|
1103
1087
|
hash = BigInt.asUintN(
|
|
1104
1088
|
64,
|
|
1105
|
-
hash * ROW_SEQUENCE_BASE +
|
|
1089
|
+
hash * ROW_SEQUENCE_BASE +
|
|
1090
|
+
BigInt(`0x${hashTerminalReplicaRow(row, undefined, undefined, encodingCache)}`),
|
|
1106
1091
|
);
|
|
1107
1092
|
if (Object.isFrozen(next) && next.every(isTerminalReplicaRowDeeplyFrozen))
|
|
1108
1093
|
ROW_ARRAY_HASH_CACHE.set(next, { hash, length: next.length });
|
|
@@ -459,16 +459,7 @@ export function createDefaultFleetFactsReaders() {
|
|
|
459
459
|
return null;
|
|
460
460
|
}
|
|
461
461
|
};
|
|
462
|
-
return
|
|
463
|
-
readSessions: async (signal) => {
|
|
464
|
-
const raw = await execute(SESSION_COMPOSITION_TMUX_ARGS, signal);
|
|
465
|
-
return raw === null ? null : parseSessionCompositionFacts(raw);
|
|
466
|
-
},
|
|
467
|
-
readAgents: async (signal) => {
|
|
468
|
-
const raw = await execute(AGENT_STATE_TMUX_ARGS, signal);
|
|
469
|
-
return raw === null ? null : parseAgentStateFacts(raw);
|
|
470
|
-
},
|
|
471
|
-
};
|
|
462
|
+
return createSharedFleetFactsReaders(execute);
|
|
472
463
|
}
|
|
473
464
|
export const SESSION_COMPOSITION_TMUX_ARGS = [
|
|
474
465
|
"list-panes",
|
|
@@ -500,3 +491,98 @@ export const AGENT_STATE_TMUX_ARGS = [
|
|
|
500
491
|
export async function readAgentStateFacts(signal) {
|
|
501
492
|
return createDefaultFleetFactsReaders().readAgents(signal);
|
|
502
493
|
}
|
|
494
|
+
/** Agent columns are excluded from the session topology projection. */
|
|
495
|
+
export const FLEET_FACTS_TMUX_ARGS = [
|
|
496
|
+
"list-panes",
|
|
497
|
+
"-a",
|
|
498
|
+
"-F",
|
|
499
|
+
`${SESSION_COMPOSITION_TMUX_ARGS[3]}\t#{@agent_state}\t#{pane_current_command}`,
|
|
500
|
+
];
|
|
501
|
+
/** Share only an outstanding read; completed snapshots are never cached. */
|
|
502
|
+
export function createSharedFleetFactsReaders(run, isAbsentServer = () => false) {
|
|
503
|
+
let pending = null;
|
|
504
|
+
const read = async (signal) => {
|
|
505
|
+
if (signal?.aborted)
|
|
506
|
+
return null;
|
|
507
|
+
if (!pending) {
|
|
508
|
+
const controller = new AbortController();
|
|
509
|
+
const entry = {
|
|
510
|
+
controller,
|
|
511
|
+
readers: 0,
|
|
512
|
+
result: Promise.resolve().then(() => run(FLEET_FACTS_TMUX_ARGS, controller.signal)),
|
|
513
|
+
};
|
|
514
|
+
entry.result = entry.result.finally(() => {
|
|
515
|
+
if (pending === entry)
|
|
516
|
+
pending = null;
|
|
517
|
+
});
|
|
518
|
+
pending = entry;
|
|
519
|
+
}
|
|
520
|
+
const entry = pending;
|
|
521
|
+
entry.readers += 1;
|
|
522
|
+
let released = false;
|
|
523
|
+
const release = () => {
|
|
524
|
+
if (released)
|
|
525
|
+
return;
|
|
526
|
+
released = true;
|
|
527
|
+
entry.readers -= 1;
|
|
528
|
+
if (entry.readers === 0) {
|
|
529
|
+
if (pending === entry)
|
|
530
|
+
pending = null;
|
|
531
|
+
entry.controller.abort();
|
|
532
|
+
}
|
|
533
|
+
};
|
|
534
|
+
signal?.addEventListener("abort", release, { once: true });
|
|
535
|
+
try {
|
|
536
|
+
// Keep the promise outstanding for a noncooperative runner so the
|
|
537
|
+
// observer's per-kind quarantine remains effective after cancellation.
|
|
538
|
+
const raw = await entry.result;
|
|
539
|
+
return signal?.aborted ? null : raw;
|
|
540
|
+
}
|
|
541
|
+
catch (error) {
|
|
542
|
+
if (signal?.aborted)
|
|
543
|
+
return null;
|
|
544
|
+
throw error;
|
|
545
|
+
}
|
|
546
|
+
finally {
|
|
547
|
+
signal?.removeEventListener("abort", release);
|
|
548
|
+
release();
|
|
549
|
+
}
|
|
550
|
+
};
|
|
551
|
+
return {
|
|
552
|
+
readSessions: async (signal) => {
|
|
553
|
+
try {
|
|
554
|
+
const raw = await read(signal);
|
|
555
|
+
return raw === null
|
|
556
|
+
? null
|
|
557
|
+
: parseSessionCompositionFacts(raw
|
|
558
|
+
.split("\n")
|
|
559
|
+
.map((line) => line.split("\t").slice(0, 11).join("\t"))
|
|
560
|
+
.join("\n"));
|
|
561
|
+
}
|
|
562
|
+
catch (error) {
|
|
563
|
+
// Socket absence is an authoritative empty catalog, but must not
|
|
564
|
+
// manufacture agent turn completions from a failed agent observation.
|
|
565
|
+
return isAbsentServer(error) ? parseSessionCompositionFacts("") : null;
|
|
566
|
+
}
|
|
567
|
+
},
|
|
568
|
+
readAgents: async (signal) => {
|
|
569
|
+
try {
|
|
570
|
+
const raw = await read(signal);
|
|
571
|
+
return raw === null
|
|
572
|
+
? null
|
|
573
|
+
: parseAgentStateFacts(raw
|
|
574
|
+
.split("\n")
|
|
575
|
+
.map((line) => {
|
|
576
|
+
const fields = line.split("\t");
|
|
577
|
+
if (fields.length !== 13)
|
|
578
|
+
return "";
|
|
579
|
+
return [fields[0], fields[6], fields[9], fields[11], fields[12]].join("\t");
|
|
580
|
+
})
|
|
581
|
+
.join("\n"));
|
|
582
|
+
}
|
|
583
|
+
catch {
|
|
584
|
+
return null;
|
|
585
|
+
}
|
|
586
|
+
},
|
|
587
|
+
};
|
|
588
|
+
}
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import { bodyLimit } from "hono/body-limit";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { SavedMachineRegistrySchema } from "@tmux-ide/contracts";
|
|
4
|
-
import {
|
|
4
|
+
import { SavedMachineMutationSchema } from "@tmux-ide/contracts/saved-machines";
|
|
5
|
+
import { loadSavedMachines, mergeSavedMachines, updateSavedMachines, } from "../../lib/saved-machines.js";
|
|
5
6
|
import { ownerAuthorityGate } from "../owner-authority.js";
|
|
6
7
|
const Request = z.strictObject({
|
|
7
8
|
expectedInstanceId: z.uuid(),
|
|
8
9
|
registry: SavedMachineRegistrySchema,
|
|
9
10
|
});
|
|
11
|
+
const MutationRequest = z.strictObject({
|
|
12
|
+
expectedInstanceId: z.uuid(),
|
|
13
|
+
change: SavedMachineMutationSchema,
|
|
14
|
+
});
|
|
10
15
|
export function mountSavedMachineRoute(app, options) {
|
|
11
16
|
const authorize = ownerAuthorityGate(options.ownerToken, {
|
|
12
17
|
whenOwnerless: "unavailable",
|
|
@@ -47,4 +52,38 @@ export function mountSavedMachineRoute(app, options) {
|
|
|
47
52
|
}, 409);
|
|
48
53
|
}
|
|
49
54
|
});
|
|
55
|
+
app.patch("/api/resources/saved-machines", bodyLimit({ maxSize: 1024 }), async (c) => {
|
|
56
|
+
const gate = authorize(c);
|
|
57
|
+
if (gate)
|
|
58
|
+
return gate;
|
|
59
|
+
c.header("Cache-Control", "no-store");
|
|
60
|
+
let parsed;
|
|
61
|
+
try {
|
|
62
|
+
parsed = MutationRequest.safeParse(await c.req.json());
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return c.json({ error: "Invalid machine mutation" }, 400);
|
|
66
|
+
}
|
|
67
|
+
if (!parsed.success)
|
|
68
|
+
return c.json({ error: "Invalid machine mutation" }, 400);
|
|
69
|
+
if (parsed.data.expectedInstanceId !== options.daemon.instanceId)
|
|
70
|
+
return c.json({ error: "Daemon generation changed" }, 409);
|
|
71
|
+
const change = parsed.data.change;
|
|
72
|
+
const { id, operation } = change;
|
|
73
|
+
try {
|
|
74
|
+
const registry = (options.update ?? updateSavedMachines)(operation === "remove"
|
|
75
|
+
? { type: "remove", id }
|
|
76
|
+
: {
|
|
77
|
+
type: "update",
|
|
78
|
+
id,
|
|
79
|
+
patch: change.operation === "edit" ? change.patch : { enabled: operation === "enable" },
|
|
80
|
+
});
|
|
81
|
+
return c.json({ daemon: options.daemon, registry });
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return c.json({
|
|
85
|
+
error: "Machine not found, edit conflicts with another profile, or registry unavailable; nothing changed",
|
|
86
|
+
}, 409);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
50
89
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createBoundedControlWriter } from "./bounded-control-writer.js";
|
|
2
2
|
import { discoverSessions, buildOverviews, buildProjectDetail } from "./discovery.js";
|
|
3
|
-
import { DaemonFleetFactsObserver,
|
|
3
|
+
import { DaemonFleetFactsObserver, createDefaultFleetFactsReaders, createSharedFleetFactsReaders, } from "./daemon-fleet-facts-observer.js";
|
|
4
4
|
import { agentIdForPaneStamp } from "./resources/application-shell.js";
|
|
5
5
|
import { projectRegistryEmitter } from "../lib/project-registry.js";
|
|
6
6
|
import { getDefaultWorkspaceRegistry, isTmuxServerUnavailableError, } from "../lib/workspace-registry.js";
|
|
@@ -256,30 +256,11 @@ export function setFleetFactsObserverDiagnostics(diagnostics) {
|
|
|
256
256
|
/** Pin fleet invalidations to the same tmux authority as catalog HTTP reads. */
|
|
257
257
|
export function setFleetFactsTmuxRunner(runTmux) {
|
|
258
258
|
stopFleetFactsObserver();
|
|
259
|
-
|
|
260
|
-
?
|
|
261
|
-
try {
|
|
262
|
-
return parseSessionCompositionFacts(await runTmux(SESSION_COMPOSITION_TMUX_ARGS, signal));
|
|
263
|
-
}
|
|
264
|
-
catch (error) {
|
|
265
|
-
// A daemon may legitimately precede the first tmux server. Socket
|
|
266
|
-
// absence is an authoritative empty fleet baseline, not a failed
|
|
267
|
-
// observation: catalog clients must be allowed to render their
|
|
268
|
-
// actionable first-run state while the observer waits for tmux.
|
|
269
|
-
return isTmuxServerUnavailableError(error) ? parseSessionCompositionFacts("") : null;
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
: null;
|
|
273
|
-
agentStateReaderOverride = runTmux
|
|
274
|
-
? async (signal) => {
|
|
275
|
-
try {
|
|
276
|
-
return parseAgentStateFacts(await runTmux(AGENT_STATE_TMUX_ARGS, signal));
|
|
277
|
-
}
|
|
278
|
-
catch {
|
|
279
|
-
return null;
|
|
280
|
-
}
|
|
281
|
-
}
|
|
259
|
+
const readers = runTmux
|
|
260
|
+
? createSharedFleetFactsReaders(runTmux, isTmuxServerUnavailableError)
|
|
282
261
|
: null;
|
|
262
|
+
sessionCompositionReaderOverride = readers?.readSessions ?? null;
|
|
263
|
+
agentStateReaderOverride = readers?.readAgents ?? null;
|
|
283
264
|
}
|
|
284
265
|
function acquireGlobalObserver(kind) {
|
|
285
266
|
let releaseAuthority;
|
|
@@ -133,6 +133,14 @@ async function replaceOlderCanonicalDaemon(deps, info, timeoutMs, expectedProduc
|
|
|
133
133
|
reason: "identity-mismatch",
|
|
134
134
|
});
|
|
135
135
|
}
|
|
136
|
+
const supervisor = latest.info.provenance?.supervisor;
|
|
137
|
+
if (!latest.info.supervisionId && (supervisor === "systemd" || supervisor === "launchd")) {
|
|
138
|
+
throw new DaemonBootstrapError("incompatible", `The older canonical daemon is managed by ${supervisor} without a supervisor reservation. ` +
|
|
139
|
+
"Stop and disable the old service, reserve the stopped namespace with " +
|
|
140
|
+
"tmux-ide daemon reserve-supervisor <id>, then configure the service to run " +
|
|
141
|
+
"tmux-ide --headless --supervised <id> before restarting it. " +
|
|
142
|
+
"The existing daemon has been left running.", { reason: "canonical-record-invalid" });
|
|
143
|
+
}
|
|
136
144
|
await deps.shutdownOlderOwner(info);
|
|
137
145
|
const deadline = deps.now() + timeoutMs;
|
|
138
146
|
while (deps.now() < deadline) {
|
|
@@ -10,7 +10,12 @@ const MAX_DAEMON_INFO_BYTES = 64 * 1024;
|
|
|
10
10
|
const MAX_DAEMON_CLAIM_BYTES = 4 * 1024;
|
|
11
11
|
const activeClaims = new Map();
|
|
12
12
|
export function getCanonicalDaemonInfoPath() {
|
|
13
|
-
|
|
13
|
+
const namespace = resolveRuntimeNamespace();
|
|
14
|
+
const path = join(namespace.daemonInfoDir, DAEMON_INFO_FILE);
|
|
15
|
+
// Resolution already validates ordinary and isolated namespaces. Only
|
|
16
|
+
// development adds a per-path containment check; do not resolve twice on
|
|
17
|
+
// every ordinary daemon read. Each call still resolves the current namespace.
|
|
18
|
+
return namespace.development ? runtimeOwnedPath(path) : path;
|
|
14
19
|
}
|
|
15
20
|
export function getCanonicalDaemonClaimPath() {
|
|
16
21
|
return join(dirname(getCanonicalDaemonInfoPath()), DAEMON_CLAIM_DIR);
|
|
@@ -472,10 +477,31 @@ export function assertCanonicalDaemonSupervision(supervisionId) {
|
|
|
472
477
|
throw new Error("Matching supervisor reservation required before startup");
|
|
473
478
|
}
|
|
474
479
|
/** Explicit installation only; startup never calls this to recreate a reservation. */
|
|
480
|
+
/** Sanitized reservation guidance; never includes credentials or filesystem errors. */
|
|
481
|
+
export class CanonicalDaemonReservationError extends Error {
|
|
482
|
+
constructor(message) {
|
|
483
|
+
super(`Supervisor reservation refused: ${message}`);
|
|
484
|
+
this.name = "CanonicalDaemonReservationError";
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
function reservationRefusal(attempt) {
|
|
488
|
+
if (attempt.status === "busy")
|
|
489
|
+
return new CanonicalDaemonReservationError(`another lifecycle operation holds the namespace (PID ${attempt.owner.pid}). Wait for it to finish, then retry.`);
|
|
490
|
+
const state = inspectCanonicalDaemonInfo();
|
|
491
|
+
if (state.status === "valid")
|
|
492
|
+
return new CanonicalDaemonReservationError(`the recorded ${state.info.supervisionId ? "supervised" : "unsupervised"} daemon (PID ${state.info.pid}) has not been released. Inspect it with tmux-ide daemon info --json, then stop it through its owning process or service before reserving.`);
|
|
493
|
+
if (state.status === "reserved")
|
|
494
|
+
return new CanonicalDaemonReservationError(`this namespace is reserved for supervisor ${state.reservation.supervisionId}. Remove that service and release its reservation before assigning a different ID.`);
|
|
495
|
+
if (state.status === "invalid")
|
|
496
|
+
return new CanonicalDaemonReservationError(`the namespace record cannot be trusted (${state.reason}). Run tmux-ide daemon info --json for diagnosis; repair ownership or permissions before retrying.`);
|
|
497
|
+
return new CanonicalDaemonReservationError("the namespace changed or its lifecycle claim is unavailable. Run tmux-ide daemon info --json, then retry after the owning operation finishes.");
|
|
498
|
+
}
|
|
475
499
|
export function reserveCanonicalDaemonSupervision(supervisionId) {
|
|
500
|
+
if (!DaemonSupervisionIdSchema.safeParse(supervisionId).success)
|
|
501
|
+
throw new CanonicalDaemonReservationError("use a 1–128 character ID starting with a letter or digit and containing only letters, digits, periods, underscores or hyphens.");
|
|
476
502
|
const attempt = tryAcquireCanonicalDaemonClaim({ kind: "reserve", supervisionId });
|
|
477
503
|
if (attempt.status !== "acquired")
|
|
478
|
-
throw
|
|
504
|
+
throw reservationRefusal(attempt);
|
|
479
505
|
const claim = attempt.claim;
|
|
480
506
|
try {
|
|
481
507
|
const before = inspectCanonicalDaemonInfo();
|
|
@@ -989,7 +989,7 @@ async function startEmbeddedDaemonGeneration(opts) {
|
|
|
989
989
|
if (!sessionRuntimeRegistry)
|
|
990
990
|
throw new Error("Session runtime unavailable");
|
|
991
991
|
return sessionRuntimeRegistry.executeWindowLinkAction(session, action);
|
|
992
|
-
});
|
|
992
|
+
}, timing);
|
|
993
993
|
}
|
|
994
994
|
if (intent.verb === "workspace.pane.resize") {
|
|
995
995
|
return workspaceMultiplexer.mutateResize({ operationId, expectedDaemonInstanceId: instanceId, intent }, (session) => {
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
const runCommand = (file, args) => new Promise((resolve) => {
|
|
3
|
+
execFile(file, [...args], {
|
|
4
|
+
encoding: "utf8",
|
|
5
|
+
timeout: 15_000,
|
|
6
|
+
maxBuffer: 256 * 1024,
|
|
7
|
+
env: { ...process.env, LC_ALL: "C", LANG: "C" },
|
|
8
|
+
}, (error, stdout, stderr) => resolve({
|
|
9
|
+
code: error ? (typeof error.code === "number" ? error.code : null) : 0,
|
|
10
|
+
stdout,
|
|
11
|
+
stderr,
|
|
12
|
+
}));
|
|
13
|
+
});
|
|
14
|
+
export class DaemonServiceManagerError extends Error {
|
|
15
|
+
operation;
|
|
16
|
+
manager;
|
|
17
|
+
code = "DAEMON_SERVICE_MANAGER_FAILED";
|
|
18
|
+
constructor(operation, manager) {
|
|
19
|
+
super(`${manager} user service ${operation} failed; check the user manager and service logs`);
|
|
20
|
+
this.operation = operation;
|
|
21
|
+
this.manager = manager;
|
|
22
|
+
this.name = "DaemonServiceManagerError";
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/** Bounded argv-only OS boundary. Never expose manager output (it may contain environment data). */
|
|
26
|
+
export function createDaemonServiceManager(plan, run = runCommand) {
|
|
27
|
+
const launchd = plan.manager === "launchd";
|
|
28
|
+
const file = launchd ? "/bin/launchctl" : "systemctl";
|
|
29
|
+
const domain = plan.target.slice(0, plan.target.lastIndexOf("/"));
|
|
30
|
+
const command = async (operation, args) => {
|
|
31
|
+
const result = await run(file, launchd ? args : ["--user", ...args]);
|
|
32
|
+
if (result.code !== 0)
|
|
33
|
+
throw new DaemonServiceManagerError(operation, plan.manager);
|
|
34
|
+
return result;
|
|
35
|
+
};
|
|
36
|
+
return {
|
|
37
|
+
async available() {
|
|
38
|
+
await command("availability check", launchd ? ["print-disabled", domain] : ["show-environment"]);
|
|
39
|
+
},
|
|
40
|
+
async inspect() {
|
|
41
|
+
const result = await run(file, launchd
|
|
42
|
+
? ["print", plan.target]
|
|
43
|
+
: [
|
|
44
|
+
"--user",
|
|
45
|
+
"show",
|
|
46
|
+
plan.target,
|
|
47
|
+
"--property=LoadState,ActiveState,MainPID,FragmentPath",
|
|
48
|
+
]);
|
|
49
|
+
const absent = { loaded: false, active: false, pid: null, definitionPath: null };
|
|
50
|
+
if (launchd) {
|
|
51
|
+
if (result.code === 113 && result.stderr.includes("Could not find service"))
|
|
52
|
+
return absent;
|
|
53
|
+
if (result.code !== 0)
|
|
54
|
+
throw new DaemonServiceManagerError("inspection", plan.manager);
|
|
55
|
+
const field = (name) => result.stdout.match(new RegExp(`^\\t${name} = (.*)$`, "m"))?.[1];
|
|
56
|
+
const path = field("path");
|
|
57
|
+
const pidText = field("pid");
|
|
58
|
+
const state = field("state");
|
|
59
|
+
if (!path || !state || (pidText !== undefined && !/^[1-9][0-9]*$/u.test(pidText)))
|
|
60
|
+
throw new DaemonServiceManagerError("inspection format", plan.manager);
|
|
61
|
+
return {
|
|
62
|
+
loaded: true,
|
|
63
|
+
active: state === "running",
|
|
64
|
+
pid: pidText ? Number(pidText) : null,
|
|
65
|
+
definitionPath: path,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
const fields = new Map();
|
|
69
|
+
for (const line of result.stdout.trim().split("\n")) {
|
|
70
|
+
const index = line.indexOf("=");
|
|
71
|
+
if (index < 1 || fields.has(line.slice(0, index)))
|
|
72
|
+
throw new DaemonServiceManagerError("inspection format", plan.manager);
|
|
73
|
+
fields.set(line.slice(0, index), line.slice(index + 1));
|
|
74
|
+
}
|
|
75
|
+
if (fields.get("LoadState") === "not-found" && (result.code === 0 || result.code === 4))
|
|
76
|
+
return absent;
|
|
77
|
+
if (result.code !== 0 || fields.get("LoadState") !== "loaded" || !fields.has("ActiveState"))
|
|
78
|
+
throw new DaemonServiceManagerError("inspection", plan.manager);
|
|
79
|
+
const pidText = fields.get("MainPID");
|
|
80
|
+
const path = fields.get("FragmentPath");
|
|
81
|
+
if (!pidText || !/^(0|[1-9][0-9]*)$/u.test(pidText) || !path)
|
|
82
|
+
throw new DaemonServiceManagerError("inspection format", plan.manager);
|
|
83
|
+
return {
|
|
84
|
+
loaded: true,
|
|
85
|
+
active: fields.get("ActiveState") === "active",
|
|
86
|
+
pid: Number(pidText) || null,
|
|
87
|
+
definitionPath: path,
|
|
88
|
+
};
|
|
89
|
+
},
|
|
90
|
+
async install() {
|
|
91
|
+
if (launchd)
|
|
92
|
+
await command("bootstrap", ["bootstrap", domain, plan.unitPath]);
|
|
93
|
+
else {
|
|
94
|
+
await command("reload", ["daemon-reload"]);
|
|
95
|
+
// The manager may have a different XDG_CONFIG_HOME from the invoking
|
|
96
|
+
// shell. Enable the owned file explicitly so it is linked into its search path.
|
|
97
|
+
await command("enable/start", ["enable", "--now", plan.unitPath]);
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
async restart(hasProcess = true) {
|
|
101
|
+
// An explicit recovery after a failed launcher must also clear systemd's
|
|
102
|
+
// start-rate counter. The lifecycle owner has already verified this unit's
|
|
103
|
+
// private definition and reservation; never reset all user units.
|
|
104
|
+
if (!launchd && !hasProcess)
|
|
105
|
+
await command("reset failed state", ["reset-failed", plan.target]);
|
|
106
|
+
// KeepAlive relaunches after a graceful TERM. kickstart -k would force-kill
|
|
107
|
+
// the daemon instead of allowing canonical ownership to be released.
|
|
108
|
+
await command("restart", launchd
|
|
109
|
+
? hasProcess
|
|
110
|
+
? ["kill", "SIGTERM", plan.target]
|
|
111
|
+
: ["kickstart", plan.target]
|
|
112
|
+
: ["restart", plan.target]);
|
|
113
|
+
},
|
|
114
|
+
async stop() {
|
|
115
|
+
await command("stop/remove", launchd ? ["bootout", plan.target] : ["disable", "--now", plan.target]);
|
|
116
|
+
},
|
|
117
|
+
async reload() {
|
|
118
|
+
if (!launchd)
|
|
119
|
+
await command("reload", ["daemon-reload"]);
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { isAbsolute, join } from "node:path";
|
|
3
|
+
import { runtimeNamespaceEnvironment } from "./runtime-namespace.js";
|
|
4
|
+
function safeValue(value, name) {
|
|
5
|
+
if (!value || [...value].some((char) => char.charCodeAt(0) < 32 || char.charCodeAt(0) === 127))
|
|
6
|
+
throw new TypeError(`${name} must be nonempty and contain no control characters`);
|
|
7
|
+
return value;
|
|
8
|
+
}
|
|
9
|
+
function absolute(value, name) {
|
|
10
|
+
safeValue(value, name);
|
|
11
|
+
if (!isAbsolute(value))
|
|
12
|
+
throw new TypeError(`${name} must be an absolute path`);
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
15
|
+
function xml(value) {
|
|
16
|
+
return value.replace(/[&<>"']/gu, (char) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[char]);
|
|
17
|
+
}
|
|
18
|
+
// systemd applies specifier expansion to paths, environment values and ExecStart.
|
|
19
|
+
// ExecStart additionally expands dollars unless the ':' executable prefix is used.
|
|
20
|
+
function systemd(value) {
|
|
21
|
+
return `"${value.replace(/%/gu, "%%").replace(/\\/gu, "\\\\").replace(/"/gu, '\\"')}"`;
|
|
22
|
+
}
|
|
23
|
+
/** Pure rendering only. Reservation and OS-manager mutations belong to the lifecycle owner. */
|
|
24
|
+
export function planDaemonService(options) {
|
|
25
|
+
const { namespace } = options;
|
|
26
|
+
if (options.platform !== "darwin" && options.platform !== "linux")
|
|
27
|
+
throw new TypeError("Daemon services support macOS launchd and Linux systemd user managers");
|
|
28
|
+
if (!Number.isSafeInteger(options.uid) || options.uid <= 0)
|
|
29
|
+
throw new TypeError("Daemon services require a non-root user");
|
|
30
|
+
if (namespace.mode === "development")
|
|
31
|
+
throw new TypeError("Development instances own their lifecycle; use dev:instance instead");
|
|
32
|
+
const home = absolute(options.home, "Home directory");
|
|
33
|
+
const executable = absolute(options.executable, "Stable CLI executable");
|
|
34
|
+
// Keep the supplied stable launcher path. realpath would pin installer services
|
|
35
|
+
// to a retired version instead of following the next verified activation.
|
|
36
|
+
const identity = createHash("sha256")
|
|
37
|
+
.update(absolute(namespace.daemonInfoDir, "Daemon state directory"))
|
|
38
|
+
.digest("hex")
|
|
39
|
+
.slice(0, 24);
|
|
40
|
+
const supervisionId = `tmux-ide.${identity}`;
|
|
41
|
+
const stdoutPath = join(namespace.daemonInfoDir, "service.stdout.log");
|
|
42
|
+
const stderrPath = join(namespace.daemonInfoDir, "service.stderr.log");
|
|
43
|
+
const environment = {
|
|
44
|
+
HOME: home,
|
|
45
|
+
PATH: safeValue(options.path, "Service PATH"),
|
|
46
|
+
...runtimeNamespaceEnvironment(namespace),
|
|
47
|
+
TMUX_IDE_CONFIG: namespace.configPath,
|
|
48
|
+
TMUX_IDE_SETTINGS_DIR: namespace.settingsDir,
|
|
49
|
+
TMUX_IDE_CLAUDE_DIR: namespace.claudeDir,
|
|
50
|
+
TMUX_IDE_CLAUDE_SETTINGS: namespace.claudeSettingsPath,
|
|
51
|
+
TMUX_IDE_CLAUDE_HOOK_PATH: namespace.claudeHookPath,
|
|
52
|
+
TMUX_IDE_OPENCODE_DIR: namespace.opencodeDir,
|
|
53
|
+
};
|
|
54
|
+
for (const [name, value] of Object.entries(environment))
|
|
55
|
+
safeValue(value, name);
|
|
56
|
+
const args = [executable, "--headless", "--supervised", supervisionId];
|
|
57
|
+
const shared = {
|
|
58
|
+
supervisionId,
|
|
59
|
+
executable,
|
|
60
|
+
recordPath: join(namespace.daemonInfoDir, "service.json"),
|
|
61
|
+
stdoutPath,
|
|
62
|
+
stderrPath,
|
|
63
|
+
};
|
|
64
|
+
if (options.platform === "darwin") {
|
|
65
|
+
const label = `com.${supervisionId}`;
|
|
66
|
+
const string = (value) => `<string>${xml(value)}</string>`;
|
|
67
|
+
return Object.freeze({
|
|
68
|
+
...shared,
|
|
69
|
+
manager: "launchd",
|
|
70
|
+
target: `gui/${options.uid}/${label}`,
|
|
71
|
+
unitPath: join(home, "Library", "LaunchAgents", `${label}.plist`),
|
|
72
|
+
contents: `<?xml version="1.0" encoding="UTF-8"?>
|
|
73
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
74
|
+
<plist version="1.0"><dict>
|
|
75
|
+
<key>Label</key>${string(label)}
|
|
76
|
+
<key>ProgramArguments</key><array>${args.map(string).join("")}</array>
|
|
77
|
+
<key>EnvironmentVariables</key><dict>${Object.entries(environment)
|
|
78
|
+
.map(([key, value]) => `<key>${xml(key)}</key>${string(value)}`)
|
|
79
|
+
.join("")}</dict>
|
|
80
|
+
<key>WorkingDirectory</key>${string(home)}
|
|
81
|
+
<key>RunAtLoad</key><true/>
|
|
82
|
+
<key>KeepAlive</key><true/>
|
|
83
|
+
<key>ThrottleInterval</key><integer>3</integer>
|
|
84
|
+
<key>ExitTimeOut</key><integer>30</integer>
|
|
85
|
+
<key>AbandonProcessGroup</key><true/>
|
|
86
|
+
<key>StandardOutPath</key>${string(stdoutPath)}
|
|
87
|
+
<key>StandardErrorPath</key>${string(stderrPath)}
|
|
88
|
+
</dict></plist>
|
|
89
|
+
`,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
const configHome = absolute(options.configHome ?? join(home, ".config"), "Config directory");
|
|
93
|
+
const target = `${supervisionId}.service`;
|
|
94
|
+
// WorkingDirectory is a raw path, not an ExecStart-style quoted word.
|
|
95
|
+
// The equivalent '/.' suffix preserves trailing spaces/backslashes without
|
|
96
|
+
// allowing the unit parser to trim them or treat them as line continuation.
|
|
97
|
+
const workingDirectory = `${home.replace(/%/gu, "%%")}/.`;
|
|
98
|
+
return Object.freeze({
|
|
99
|
+
...shared,
|
|
100
|
+
manager: "systemd",
|
|
101
|
+
target,
|
|
102
|
+
unitPath: join(configHome, "systemd", "user", target),
|
|
103
|
+
contents: `[Unit]
|
|
104
|
+
Description=tmux-ide canonical user daemon
|
|
105
|
+
StartLimitIntervalSec=60
|
|
106
|
+
StartLimitBurst=10
|
|
107
|
+
|
|
108
|
+
[Service]
|
|
109
|
+
Type=exec
|
|
110
|
+
ExecStart=:${args.map(systemd).join(" ")}
|
|
111
|
+
WorkingDirectory=${workingDirectory}
|
|
112
|
+
${Object.entries(environment)
|
|
113
|
+
.map(([key, value]) => `Environment=${systemd(`${key}=${value}`)}`)
|
|
114
|
+
.join("\n")}
|
|
115
|
+
Restart=always
|
|
116
|
+
RestartSec=3
|
|
117
|
+
TimeoutStopSec=30
|
|
118
|
+
# tmux pane work has an independent lifetime; never kill the whole service cgroup.
|
|
119
|
+
KillMode=process
|
|
120
|
+
StandardOutput=journal
|
|
121
|
+
StandardError=journal
|
|
122
|
+
|
|
123
|
+
[Install]
|
|
124
|
+
WantedBy=default.target
|
|
125
|
+
`,
|
|
126
|
+
});
|
|
127
|
+
}
|