tmux-ide 2.9.2 → 2.9.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/bin/cli.js +2071 -625
- package/package.json +14 -5
- package/packages/contracts/src/desktop-host.ts +3 -0
- package/packages/contracts/src/saved-machines.ts +19 -0
- package/packages/contracts/src/terminal-replica.ts +1 -1
- package/packages/core/package.json +3 -2
- package/packages/core/src/terminal-delivery.ts +51 -3
- package/packages/core/src/terminal-fnv64-wasm.ts +4 -3
- package/packages/core/src/terminal-replica-hash-cache.ts +129 -31
- package/packages/core/src/terminal-replica-owned-row.ts +27 -0
- package/packages/core/src/terminal-replica.ts +13 -28
- package/packages/daemon/dist/command-center/daemon-fleet-facts-observer.js +96 -10
- package/packages/daemon/dist/command-center/resources/saved-machine-route.js +40 -1
- package/packages/daemon/dist/command-center/ws-events.js +5 -24
- package/packages/daemon/dist/lib/canonical-daemon-bootstrap.js +8 -0
- package/packages/daemon/dist/lib/canonical-daemon.js +28 -2
- package/packages/daemon/dist/lib/daemon-embed.js +1 -1
- package/packages/daemon/dist/lib/daemon-service-manager.js +122 -0
- package/packages/daemon/dist/lib/daemon-service-plan.js +127 -0
- package/packages/daemon/dist/lib/daemon-service.js +259 -0
- package/packages/daemon/dist/lib/install-origin.js +2 -0
- package/packages/daemon/dist/lib/local-fleet-request.js +9 -3
- package/packages/daemon/dist/lib/log.js +6 -10
- package/packages/daemon/dist/lib/managed-installer-update.js +67 -0
- package/packages/daemon/dist/lib/{tui-download-lock.js → private-operation-lock.js} +11 -11
- package/packages/daemon/dist/lib/remote-daemon-info.js +3 -2
- package/packages/daemon/dist/lib/remote-daemon-lifecycle.js +4 -4
- package/packages/daemon/dist/lib/remote-tmux-command.js +10 -0
- package/packages/daemon/dist/lib/runtime-namespace.js +28 -16
- package/packages/daemon/dist/lib/saved-machines.js +40 -2
- package/packages/daemon/dist/lib/ssh-daemon-transport.js +24 -9
- package/packages/daemon/dist/lib/tmux-server-owner.js +1 -1
- package/packages/daemon/dist/lib/tmux-server-registration.js +21 -3
- package/packages/daemon/dist/lib/tui-binary.js +2 -2
- package/packages/daemon/dist/lib/update.js +18 -0
- package/packages/daemon/dist/lib/workspace-multiplexer-verbs.js +24 -1
- package/packages/daemon/dist/machines.js +40 -4
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Info.plist +2 -2
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/MacOS/tmux-ide-notifier +0 -0
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Resources/Assets.car +0 -0
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/_CodeSignature/CodeResources +2 -2
- package/packages/daemon/dist/terminal/attachments/direct-websocket.js +4 -1
- package/packages/daemon/dist/terminal/attachments/native-runtime.js +22 -13
- package/packages/daemon/dist/terminal/mirror/mirror-service.js +101 -3
- package/packages/daemon/dist/terminal/mirror/registered-window-guard.js +62 -0
- package/packages/daemon/dist/terminal/mirror/session-channel.js +75 -4
- package/packages/daemon/dist/terminal/mirror/shared-window-index.js +45 -0
- package/packages/daemon/dist/terminal/pane-stream/pane-stream-websocket.js +11 -1
- package/packages/daemon/dist/terminal/session-runtime/multiplexer-backend.js +1 -0
- package/packages/daemon/dist/terminal/session-runtime/registry.js +56 -7
- package/packages/daemon/dist/terminal/session-runtime/semantic-mutation-executor.js +3 -1
- package/packages/daemon/dist/tui/mirror/dialog-model.js +2 -35
- package/packages/daemon/dist/tui/mirror/runtime/application-daemon-authority-owner.js +8 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-home-fleet.js +20 -2
- package/packages/daemon/dist/tui/mirror/runtime/application-machine-agents.js +7 -1
- package/packages/daemon/dist/tui/mirror/runtime/application-machine-authority.js +59 -9
- package/packages/daemon/dist/tui/mirror/runtime/application-machine-navigation.js +27 -19
- package/packages/daemon/dist/tui/mirror/runtime/application-machine-sidebar.jsx +23 -18
- package/packages/daemon/dist/tui/mirror/runtime/application-machine-startup.js +31 -2
- package/packages/daemon/dist/tui/mirror/runtime/application-palette-search-owner.js +7 -3
- package/packages/daemon/dist/tui/mirror/runtime/application-performance-log.js +3 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-terminal-input-ingress.js +146 -2
- package/packages/daemon/dist/tui/mirror/runtime/application-terminal-interaction-controller.js +201 -50
- package/packages/daemon/dist/tui/mirror/runtime/bounded-performance-record-writer.js +50 -10
- package/packages/daemon/dist/tui/mirror/runtime/terminal-fast-lane-renderer-adapter.js +6 -5
- package/packages/daemon/dist/tui/mirror/runtime/terminal-pane-input-router.js +3 -0
- package/packages/daemon/dist/tui/mirror/runtime/workspace-terminal-fast-lane.js +8 -2
- package/packages/daemon/dist/tui/mirror/terminal-text.js +35 -0
- package/packages/daemon/dist/tui/mirror/workspace/connection-feedback.js +5 -3
- package/packages/daemon/src/command-center/daemon-fleet-facts-observer.ts +100 -10
- package/packages/daemon/src/command-center/resources/saved-machine-route.ts +48 -1
- package/packages/daemon/src/command-center/ws-events.ts +5 -25
- package/packages/daemon/src/lib/canonical-daemon-bootstrap.ts +12 -0
- package/packages/daemon/src/lib/canonical-daemon.ts +44 -2
- package/packages/daemon/src/lib/daemon-embed.ts +1 -0
- package/packages/daemon/src/lib/daemon-service-manager.ts +152 -0
- package/packages/daemon/src/lib/daemon-service-plan.ts +153 -0
- package/packages/daemon/src/lib/daemon-service.ts +301 -0
- package/packages/daemon/src/lib/install-origin.ts +5 -0
- package/packages/daemon/src/lib/local-fleet-request.ts +19 -3
- package/packages/daemon/src/lib/log.ts +13 -9
- package/packages/daemon/src/lib/managed-installer-update.ts +79 -0
- package/packages/daemon/src/lib/{tui-download-lock.ts → private-operation-lock.ts} +15 -11
- package/packages/daemon/src/lib/remote-daemon-info.ts +3 -3
- package/packages/daemon/src/lib/remote-daemon-lifecycle.ts +4 -4
- package/packages/daemon/src/lib/remote-tmux-command.ts +12 -0
- package/packages/daemon/src/lib/runtime-namespace.ts +27 -18
- package/packages/daemon/src/lib/saved-machines.ts +40 -1
- package/packages/daemon/src/lib/ssh-daemon-transport.ts +25 -7
- package/packages/daemon/src/lib/tmux-server-owner.ts +1 -0
- package/packages/daemon/src/lib/tmux-server-registration.ts +21 -3
- package/packages/daemon/src/lib/tui-binary.ts +2 -2
- package/packages/daemon/src/lib/update.ts +24 -0
- package/packages/daemon/src/lib/workspace-multiplexer-verbs.ts +22 -0
- package/packages/daemon/src/machines.ts +53 -5
- package/packages/daemon/src/terminal/attachments/direct-websocket.ts +8 -2
- package/packages/daemon/src/terminal/attachments/native-runtime.ts +28 -13
- package/packages/daemon/src/terminal/mirror/mirror-service.ts +105 -3
- package/packages/daemon/src/terminal/mirror/registered-window-guard.ts +66 -0
- package/packages/daemon/src/terminal/mirror/session-channel.ts +92 -4
- package/packages/daemon/src/terminal/mirror/shared-window-index.ts +42 -0
- package/packages/daemon/src/terminal/pane-stream/pane-stream-websocket.ts +12 -1
- package/packages/daemon/src/terminal/session-runtime/multiplexer-backend.ts +2 -0
- package/packages/daemon/src/terminal/session-runtime/registry.ts +70 -8
- package/packages/daemon/src/terminal/session-runtime/semantic-mutation-executor.ts +3 -1
- package/packages/daemon/src/tui/mirror/dialog-model.ts +2 -32
- package/packages/daemon/src/tui/mirror/runtime/application-daemon-authority-owner.ts +8 -0
- package/packages/daemon/src/tui/mirror/runtime/application-home-fleet.ts +19 -2
- package/packages/daemon/src/tui/mirror/runtime/application-machine-agents.ts +6 -1
- package/packages/daemon/src/tui/mirror/runtime/application-machine-authority.ts +60 -11
- package/packages/daemon/src/tui/mirror/runtime/application-machine-navigation.ts +39 -33
- package/packages/daemon/src/tui/mirror/runtime/application-machine-sidebar.tsx +31 -23
- package/packages/daemon/src/tui/mirror/runtime/application-machine-startup.ts +38 -2
- package/packages/daemon/src/tui/mirror/runtime/application-palette-search-owner.ts +7 -4
- package/packages/daemon/src/tui/mirror/runtime/application-performance-log.ts +3 -0
- package/packages/daemon/src/tui/mirror/runtime/application-root-v2.tsx +1 -1
- package/packages/daemon/src/tui/mirror/runtime/application-shell-view.tsx +47 -46
- package/packages/daemon/src/tui/mirror/runtime/application-terminal-input-ingress.ts +165 -2
- package/packages/daemon/src/tui/mirror/runtime/application-terminal-interaction-controller.ts +207 -53
- package/packages/daemon/src/tui/mirror/runtime/bounded-performance-record-writer.ts +49 -9
- package/packages/daemon/src/tui/mirror/runtime/terminal-fast-lane-renderer-adapter.ts +6 -5
- package/packages/daemon/src/tui/mirror/runtime/terminal-pane-input-router.ts +4 -0
- package/packages/daemon/src/tui/mirror/runtime/workspace-terminal-fast-lane.ts +8 -3
- package/packages/daemon/src/tui/mirror/terminal-text.ts +32 -0
- package/packages/daemon/src/tui/mirror/workspace/connection-feedback.ts +6 -3
- package/packages/daemon-client/src/fleet-connection-status.ts +10 -1
- package/packages/daemon-client/src/workspace-client.ts +4 -1
- package/packages/contracts/src/__tests__/actions-contract.test.ts +0 -119
- package/packages/contracts/src/__tests__/agent-graph-overlay.test.ts +0 -428
- package/packages/contracts/src/__tests__/app-window-mutation.test.ts +0 -51
- package/packages/contracts/src/__tests__/app-window-state.test.ts +0 -34
- package/packages/contracts/src/__tests__/application-shell.test.ts +0 -645
- package/packages/contracts/src/__tests__/automation-operations.test.ts +0 -78
- package/packages/contracts/src/__tests__/causal-cell.test.ts +0 -47
- package/packages/contracts/src/__tests__/client-view-state.test.ts +0 -115
- package/packages/contracts/src/__tests__/cohesion-fixture.test.ts +0 -106
- package/packages/contracts/src/__tests__/commands.test.ts +0 -86
- package/packages/contracts/src/__tests__/control.test.ts +0 -154
- package/packages/contracts/src/__tests__/daemon-events.test.ts +0 -405
- package/packages/contracts/src/__tests__/daemon-resources.test.ts +0 -143
- package/packages/contracts/src/__tests__/daemon-wire.test.ts +0 -149
- package/packages/contracts/src/__tests__/desktop-host.test.ts +0 -269
- package/packages/contracts/src/__tests__/desktop-missions.test.ts +0 -215
- package/packages/contracts/src/__tests__/experience-conformance.test.ts +0 -153
- package/packages/contracts/src/__tests__/experience-kernel-boundary.test.ts +0 -113
- package/packages/contracts/src/__tests__/experience-shell.test.ts +0 -65
- package/packages/contracts/src/__tests__/fixtures/daemon-wire-v2.json +0 -35
- package/packages/contracts/src/__tests__/fixtures/daemon-wire-v3.json +0 -35
- package/packages/contracts/src/__tests__/fixtures/daemon-wire-v4.json +0 -35
- package/packages/contracts/src/__tests__/fixtures/scoped-receipt.ts +0 -62
- package/packages/contracts/src/__tests__/fleet-agent-graph.test.ts +0 -231
- package/packages/contracts/src/__tests__/fleet-catalog.test.ts +0 -270
- package/packages/contracts/src/__tests__/fleet-lifecycle.test.ts +0 -62
- package/packages/contracts/src/__tests__/focus-overlay.test.ts +0 -110
- package/packages/contracts/src/__tests__/ide-config.test.ts +0 -64
- package/packages/contracts/src/__tests__/interaction-evidence.test.ts +0 -288
- package/packages/contracts/src/__tests__/interaction-receipt-v2.test.ts +0 -72
- package/packages/contracts/src/__tests__/issue-error.test.ts +0 -78
- package/packages/contracts/src/__tests__/mission-projections.test.ts +0 -186
- package/packages/contracts/src/__tests__/missions.test.ts +0 -272
- package/packages/contracts/src/__tests__/multiplexer-verbs.test.ts +0 -242
- package/packages/contracts/src/__tests__/pane-appearance.test.ts +0 -146
- package/packages/contracts/src/__tests__/pane-read-result.test.ts +0 -55
- package/packages/contracts/src/__tests__/pane-stream.test.ts +0 -536
- package/packages/contracts/src/__tests__/pane-widget-marker.test.ts +0 -327
- package/packages/contracts/src/__tests__/performance-metrics.test.ts +0 -73
- package/packages/contracts/src/__tests__/performance-qualification.test.ts +0 -137
- package/packages/contracts/src/__tests__/rich-card-widget.test.ts +0 -29
- package/packages/contracts/src/__tests__/session-runtime.test.ts +0 -512
- package/packages/contracts/src/__tests__/startup-readiness.test.ts +0 -409
- package/packages/contracts/src/__tests__/terminal-attachment-stream.test.ts +0 -72
- package/packages/contracts/src/__tests__/terminal-attachments.test.ts +0 -329
- package/packages/contracts/src/__tests__/terminal-delivery.test.ts +0 -88
- package/packages/contracts/src/__tests__/terminal-runtime-inventory.test.ts +0 -68
- package/packages/contracts/src/__tests__/terminals.test.ts +0 -87
- package/packages/contracts/src/__tests__/tmux-server-scope.test.ts +0 -166
- package/packages/contracts/src/__tests__/transport-contract-loading.test.ts +0 -91
- package/packages/contracts/src/__tests__/visual-tokens.test.ts +0 -224
- package/packages/contracts/src/__tests__/window-links.test.ts +0 -243
- package/packages/contracts/src/__tests__/workspace-changes-resource.test.ts +0 -317
- package/packages/contracts/src/__tests__/workspace-changes-view.test.ts +0 -122
- package/packages/contracts/src/__tests__/workspace-config.test.ts +0 -294
- package/packages/contracts/src/__tests__/workspace-files-resource.test.ts +0 -261
- package/packages/contracts/src/__tests__/workspace-files-tree.test.ts +0 -147
- package/packages/contracts/src/__tests__/workspace-pane-creation.test.ts +0 -249
- package/packages/contracts/src/__tests__/workspace-promotion.test.ts +0 -94
- package/packages/contracts/src/__tests__/workspace-resource-identity.test.ts +0 -90
- package/packages/contracts/src/daemon-resource-request.test.ts +0 -192
- package/packages/contracts/src/native-operation-identity.test.ts +0 -23
- package/packages/contracts/src/workspace-admission.test.ts +0 -21
- package/packages/contracts/src/workspace-catalog-resource.test.ts +0 -146
- package/packages/core/src/agent-provisioning.test.ts +0 -42
- package/packages/core/src/application-shell-session.test.ts +0 -105
- package/packages/core/src/interaction-evidence.test.ts +0 -134
- package/packages/core/src/interaction-flow.test.ts +0 -66
- package/packages/core/src/interaction-receipts.test.ts +0 -671
- package/packages/core/src/navigator.test.ts +0 -34
- package/packages/core/src/optimistic-projection-conformance.test.ts +0 -75
- package/packages/core/src/optimistic-projection.test.ts +0 -118
- package/packages/core/src/performance-metrics.test.ts +0 -121
- package/packages/core/src/performance-qualification.test.ts +0 -283
- package/packages/core/src/saved-machines.test.ts +0 -80
- package/packages/core/src/terminal-compact-cold.test.ts +0 -87
- package/packages/core/src/terminal-conformance.test.ts +0 -48
- package/packages/core/src/terminal-delivery-cache-lifetime.test.ts +0 -167
- package/packages/core/src/terminal-delivery-encode-cooperative.test.ts +0 -140
- package/packages/core/src/terminal-delivery-representation-hash.test.ts +0 -77
- package/packages/core/src/terminal-delivery.test.ts +0 -1471
- package/packages/core/src/terminal-fnv64-wasm.test.ts +0 -61
- package/packages/core/src/terminal-replica-cold.test.ts +0 -31
- package/packages/core/src/terminal-replica-cooperative.test.ts +0 -170
- package/packages/core/src/terminal-replica-hash-cache.test.ts +0 -283
- package/packages/core/src/terminal-replica.test.ts +0 -347
- package/packages/core/src/workspace-selection.test.ts +0 -63
- package/packages/daemon/dist/__tests__/support.js +0 -33
- package/packages/daemon/dist/lib/__tests__/fixtures/workspace-state-writer.js +0 -58
- package/packages/daemon/dist/lib/__tests__/installed-recovery-fixture.js +0 -400
- package/packages/daemon/dist/terminal/__tests__/MockPtyAdapter.js +0 -182
- package/packages/daemon/dist/terminal/mirror/__tests__/scripted-channel.js +0 -98
- package/packages/daemon/dist/terminal/mirror/__tests__/simulated-channel.js +0 -156
- package/packages/daemon/src/tui/mirror/__snapshots__/activity-surface-renderer.test.tsx.snap +0 -42
- package/packages/daemon/src/tui/mirror/__snapshots__/changes-terminal-surface-renderer.test.tsx.snap +0 -267
- package/packages/daemon/src/tui/mirror/__snapshots__/home-files-surface-renderer.test.tsx.snap +0 -267
- package/packages/daemon/src/tui/mirror/__snapshots__/missions-surface-renderer.test.tsx.snap +0 -134
- package/packages/daemon/src/tui/mirror/__snapshots__/recipes-gallery-renderer.test.tsx.snap +0 -141
- package/packages/daemon/src/tui/mirror/__snapshots__/shell-chrome-renderer.test.tsx.snap +0 -134
- package/packages/daemon/src/tui/mirror/runtime/__snapshots__/application-shell-view-renderer.test.tsx.snap +0 -87
- package/packages/daemon/src/tui/mirror/workspace/__snapshots__/agent-terminal-canvas-renderer.test.tsx.snap +0 -348
- package/packages/daemon/src/tui/mirror/workspace/__snapshots__/application-shell-renderer.test.tsx.snap +0 -134
- package/packages/daemon/src/tui/mirror/workspace/__snapshots__/command-palette-surface-renderer.test.tsx.snap +0 -255
- package/packages/daemon/src/tui/mirror/workspace/__snapshots__/pane-frame-renderer.test.tsx.snap +0 -525
- package/packages/daemon/src/tui/mirror/workspace/__snapshots__/terminal-pane-header-polish-renderer.test.tsx.snap +0 -31
- package/packages/daemon/src/tui/mirror/workspace/__snapshots__/workbench-shell-renderer.test.tsx.snap +0 -134
- package/packages/daemon-client/src/application-shell-session.test.ts +0 -196
- package/packages/daemon-client/src/automation-client.test.ts +0 -214
- package/packages/daemon-client/src/bootstrap-coordinator.test.ts +0 -145
- package/packages/daemon-client/src/connection-supervisor.test.ts +0 -109
- package/packages/daemon-client/src/direct-application-shell-transport.test.ts +0 -941
- package/packages/daemon-client/src/first-latest-coordinator.test.ts +0 -175
- package/packages/daemon-client/src/fleet-dial-scheduler.test.ts +0 -114
- package/packages/daemon-client/src/fleet-environments.test.ts +0 -40
- package/packages/daemon-client/src/generation-bound-store-timers.test.ts +0 -74
- package/packages/daemon-client/src/owner-action-client.test.ts +0 -245
- package/packages/daemon-client/src/pane-stream-client.test.ts +0 -2841
- package/packages/daemon-client/src/pane-stream-clock-calibration.test.ts +0 -207
- package/packages/daemon-client/src/pane-stream-loopback-benchmark.test.ts +0 -51
- package/packages/daemon-client/src/push-resource-session.test.ts +0 -509
- package/packages/daemon-client/src/resource-replica.test.ts +0 -186
- package/packages/daemon-client/src/runtime-resource-ledger.test.ts +0 -18
- package/packages/daemon-client/src/scoped-tmux-server-transport.test.ts +0 -114
- package/packages/daemon-client/src/terminal-fast-lane.test.ts +0 -835
- package/packages/daemon-client/src/tmux-server-client.test.ts +0 -279
- package/packages/daemon-client/src/tmux-server-interaction-events.test.ts +0 -230
- package/packages/daemon-client/src/workspace-catalog-v2.test.ts +0 -109
- package/packages/daemon-client/src/workspace-client-boundaries.test.ts +0 -59
- package/packages/daemon-client/src/workspace-client-operations.test.ts +0 -96
- package/packages/daemon-client/src/workspace-client.test.ts +0 -2912
- package/packages/daemon-client/src/workspace-event-supervisor.test.ts +0 -1086
- package/packages/daemon-client/src/workspace-pane-client.test.ts +0 -80
- package/packages/presentation/src/presentation-contract.test.ts +0 -63
- package/packages/tmux-bridge/src/runner.test.ts +0 -614
- package/packages/tmux-bridge/src/targeting.test.ts +0 -107
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import type { DaemonServicePlan } from "./daemon-service-plan.ts";
|
|
3
|
+
|
|
4
|
+
export interface DaemonServiceState {
|
|
5
|
+
readonly loaded: boolean;
|
|
6
|
+
readonly active: boolean;
|
|
7
|
+
readonly pid: number | null;
|
|
8
|
+
readonly definitionPath: string | null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type ServiceCommand = (
|
|
12
|
+
file: string,
|
|
13
|
+
args: readonly string[],
|
|
14
|
+
) => Promise<{ code: number | null; stdout: string; stderr: string }>;
|
|
15
|
+
|
|
16
|
+
const runCommand: ServiceCommand = (file, args) =>
|
|
17
|
+
new Promise((resolve) => {
|
|
18
|
+
execFile(
|
|
19
|
+
file,
|
|
20
|
+
[...args],
|
|
21
|
+
{
|
|
22
|
+
encoding: "utf8",
|
|
23
|
+
timeout: 15_000,
|
|
24
|
+
maxBuffer: 256 * 1024,
|
|
25
|
+
env: { ...process.env, LC_ALL: "C", LANG: "C" },
|
|
26
|
+
},
|
|
27
|
+
(error, stdout, stderr) =>
|
|
28
|
+
resolve({
|
|
29
|
+
code: error ? (typeof error.code === "number" ? error.code : null) : 0,
|
|
30
|
+
stdout,
|
|
31
|
+
stderr,
|
|
32
|
+
}),
|
|
33
|
+
);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
export class DaemonServiceManagerError extends Error {
|
|
37
|
+
readonly code = "DAEMON_SERVICE_MANAGER_FAILED";
|
|
38
|
+
constructor(
|
|
39
|
+
readonly operation: string,
|
|
40
|
+
readonly manager: string,
|
|
41
|
+
) {
|
|
42
|
+
super(`${manager} user service ${operation} failed; check the user manager and service logs`);
|
|
43
|
+
this.name = "DaemonServiceManagerError";
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Bounded argv-only OS boundary. Never expose manager output (it may contain environment data). */
|
|
48
|
+
export function createDaemonServiceManager(plan: DaemonServicePlan, run = runCommand) {
|
|
49
|
+
const launchd = plan.manager === "launchd";
|
|
50
|
+
const file = launchd ? "/bin/launchctl" : "systemctl";
|
|
51
|
+
const domain = plan.target.slice(0, plan.target.lastIndexOf("/"));
|
|
52
|
+
const command = async (operation: string, args: readonly string[]) => {
|
|
53
|
+
const result = await run(file, launchd ? args : ["--user", ...args]);
|
|
54
|
+
if (result.code !== 0) throw new DaemonServiceManagerError(operation, plan.manager);
|
|
55
|
+
return result;
|
|
56
|
+
};
|
|
57
|
+
return {
|
|
58
|
+
async available(): Promise<void> {
|
|
59
|
+
await command(
|
|
60
|
+
"availability check",
|
|
61
|
+
launchd ? ["print-disabled", domain] : ["show-environment"],
|
|
62
|
+
);
|
|
63
|
+
},
|
|
64
|
+
async inspect(): Promise<DaemonServiceState> {
|
|
65
|
+
const result = await run(
|
|
66
|
+
file,
|
|
67
|
+
launchd
|
|
68
|
+
? ["print", plan.target]
|
|
69
|
+
: [
|
|
70
|
+
"--user",
|
|
71
|
+
"show",
|
|
72
|
+
plan.target,
|
|
73
|
+
"--property=LoadState,ActiveState,MainPID,FragmentPath",
|
|
74
|
+
],
|
|
75
|
+
);
|
|
76
|
+
const absent = { loaded: false, active: false, pid: null, definitionPath: null } as const;
|
|
77
|
+
if (launchd) {
|
|
78
|
+
if (result.code === 113 && result.stderr.includes("Could not find service")) return absent;
|
|
79
|
+
if (result.code !== 0) throw new DaemonServiceManagerError("inspection", plan.manager);
|
|
80
|
+
const field = (name: string) =>
|
|
81
|
+
result.stdout.match(new RegExp(`^\\t${name} = (.*)$`, "m"))?.[1];
|
|
82
|
+
const path = field("path");
|
|
83
|
+
const pidText = field("pid");
|
|
84
|
+
const state = field("state");
|
|
85
|
+
if (!path || !state || (pidText !== undefined && !/^[1-9][0-9]*$/u.test(pidText)))
|
|
86
|
+
throw new DaemonServiceManagerError("inspection format", plan.manager);
|
|
87
|
+
return {
|
|
88
|
+
loaded: true,
|
|
89
|
+
active: state === "running",
|
|
90
|
+
pid: pidText ? Number(pidText) : null,
|
|
91
|
+
definitionPath: path,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
const fields = new Map<string, string>();
|
|
95
|
+
for (const line of result.stdout.trim().split("\n")) {
|
|
96
|
+
const index = line.indexOf("=");
|
|
97
|
+
if (index < 1 || fields.has(line.slice(0, index)))
|
|
98
|
+
throw new DaemonServiceManagerError("inspection format", plan.manager);
|
|
99
|
+
fields.set(line.slice(0, index), line.slice(index + 1));
|
|
100
|
+
}
|
|
101
|
+
if (fields.get("LoadState") === "not-found" && (result.code === 0 || result.code === 4))
|
|
102
|
+
return absent;
|
|
103
|
+
if (result.code !== 0 || fields.get("LoadState") !== "loaded" || !fields.has("ActiveState"))
|
|
104
|
+
throw new DaemonServiceManagerError("inspection", plan.manager);
|
|
105
|
+
const pidText = fields.get("MainPID");
|
|
106
|
+
const path = fields.get("FragmentPath");
|
|
107
|
+
if (!pidText || !/^(0|[1-9][0-9]*)$/u.test(pidText) || !path)
|
|
108
|
+
throw new DaemonServiceManagerError("inspection format", plan.manager);
|
|
109
|
+
return {
|
|
110
|
+
loaded: true,
|
|
111
|
+
active: fields.get("ActiveState") === "active",
|
|
112
|
+
pid: Number(pidText) || null,
|
|
113
|
+
definitionPath: path,
|
|
114
|
+
};
|
|
115
|
+
},
|
|
116
|
+
async install(): Promise<void> {
|
|
117
|
+
if (launchd) await command("bootstrap", ["bootstrap", domain, plan.unitPath]);
|
|
118
|
+
else {
|
|
119
|
+
await command("reload", ["daemon-reload"]);
|
|
120
|
+
// The manager may have a different XDG_CONFIG_HOME from the invoking
|
|
121
|
+
// shell. Enable the owned file explicitly so it is linked into its search path.
|
|
122
|
+
await command("enable/start", ["enable", "--now", plan.unitPath]);
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
async restart(hasProcess = true): Promise<void> {
|
|
126
|
+
// An explicit recovery after a failed launcher must also clear systemd's
|
|
127
|
+
// start-rate counter. The lifecycle owner has already verified this unit's
|
|
128
|
+
// private definition and reservation; never reset all user units.
|
|
129
|
+
if (!launchd && !hasProcess)
|
|
130
|
+
await command("reset failed state", ["reset-failed", plan.target]);
|
|
131
|
+
// KeepAlive relaunches after a graceful TERM. kickstart -k would force-kill
|
|
132
|
+
// the daemon instead of allowing canonical ownership to be released.
|
|
133
|
+
await command(
|
|
134
|
+
"restart",
|
|
135
|
+
launchd
|
|
136
|
+
? hasProcess
|
|
137
|
+
? ["kill", "SIGTERM", plan.target]
|
|
138
|
+
: ["kickstart", plan.target]
|
|
139
|
+
: ["restart", plan.target],
|
|
140
|
+
);
|
|
141
|
+
},
|
|
142
|
+
async stop(): Promise<void> {
|
|
143
|
+
await command(
|
|
144
|
+
"stop/remove",
|
|
145
|
+
launchd ? ["bootout", plan.target] : ["disable", "--now", plan.target],
|
|
146
|
+
);
|
|
147
|
+
},
|
|
148
|
+
async reload(): Promise<void> {
|
|
149
|
+
if (!launchd) await command("reload", ["daemon-reload"]);
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { isAbsolute, join } from "node:path";
|
|
3
|
+
import { runtimeNamespaceEnvironment, type RuntimeNamespace } from "./runtime-namespace.ts";
|
|
4
|
+
|
|
5
|
+
export interface DaemonServicePlan {
|
|
6
|
+
readonly manager: "launchd" | "systemd";
|
|
7
|
+
readonly supervisionId: string;
|
|
8
|
+
readonly target: string;
|
|
9
|
+
readonly unitPath: string;
|
|
10
|
+
readonly recordPath: string;
|
|
11
|
+
readonly executable: string;
|
|
12
|
+
readonly contents: string;
|
|
13
|
+
readonly stdoutPath: string;
|
|
14
|
+
readonly stderrPath: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function safeValue(value: string, name: string): string {
|
|
18
|
+
if (!value || [...value].some((char) => char.charCodeAt(0) < 32 || char.charCodeAt(0) === 127))
|
|
19
|
+
throw new TypeError(`${name} must be nonempty and contain no control characters`);
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function absolute(value: string, name: string): string {
|
|
24
|
+
safeValue(value, name);
|
|
25
|
+
if (!isAbsolute(value)) throw new TypeError(`${name} must be an absolute path`);
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function xml(value: string): string {
|
|
30
|
+
return value.replace(
|
|
31
|
+
/[&<>"']/gu,
|
|
32
|
+
(char) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[char]!,
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// systemd applies specifier expansion to paths, environment values and ExecStart.
|
|
37
|
+
// ExecStart additionally expands dollars unless the ':' executable prefix is used.
|
|
38
|
+
function systemd(value: string): string {
|
|
39
|
+
return `"${value.replace(/%/gu, "%%").replace(/\\/gu, "\\\\").replace(/"/gu, '\\"')}"`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Pure rendering only. Reservation and OS-manager mutations belong to the lifecycle owner. */
|
|
43
|
+
export function planDaemonService(options: {
|
|
44
|
+
platform: string;
|
|
45
|
+
uid: number;
|
|
46
|
+
home: string;
|
|
47
|
+
configHome?: string;
|
|
48
|
+
executable: string;
|
|
49
|
+
path: string;
|
|
50
|
+
namespace: RuntimeNamespace;
|
|
51
|
+
}): DaemonServicePlan {
|
|
52
|
+
const { namespace } = options;
|
|
53
|
+
if (options.platform !== "darwin" && options.platform !== "linux")
|
|
54
|
+
throw new TypeError("Daemon services support macOS launchd and Linux systemd user managers");
|
|
55
|
+
if (!Number.isSafeInteger(options.uid) || options.uid <= 0)
|
|
56
|
+
throw new TypeError("Daemon services require a non-root user");
|
|
57
|
+
if (namespace.mode === "development")
|
|
58
|
+
throw new TypeError("Development instances own their lifecycle; use dev:instance instead");
|
|
59
|
+
const home = absolute(options.home, "Home directory");
|
|
60
|
+
const executable = absolute(options.executable, "Stable CLI executable");
|
|
61
|
+
// Keep the supplied stable launcher path. realpath would pin installer services
|
|
62
|
+
// to a retired version instead of following the next verified activation.
|
|
63
|
+
const identity = createHash("sha256")
|
|
64
|
+
.update(absolute(namespace.daemonInfoDir, "Daemon state directory"))
|
|
65
|
+
.digest("hex")
|
|
66
|
+
.slice(0, 24);
|
|
67
|
+
const supervisionId = `tmux-ide.${identity}`;
|
|
68
|
+
const stdoutPath = join(namespace.daemonInfoDir, "service.stdout.log");
|
|
69
|
+
const stderrPath = join(namespace.daemonInfoDir, "service.stderr.log");
|
|
70
|
+
const environment = {
|
|
71
|
+
HOME: home,
|
|
72
|
+
PATH: safeValue(options.path, "Service PATH"),
|
|
73
|
+
...runtimeNamespaceEnvironment(namespace),
|
|
74
|
+
TMUX_IDE_CONFIG: namespace.configPath,
|
|
75
|
+
TMUX_IDE_SETTINGS_DIR: namespace.settingsDir,
|
|
76
|
+
TMUX_IDE_CLAUDE_DIR: namespace.claudeDir,
|
|
77
|
+
TMUX_IDE_CLAUDE_SETTINGS: namespace.claudeSettingsPath,
|
|
78
|
+
TMUX_IDE_CLAUDE_HOOK_PATH: namespace.claudeHookPath,
|
|
79
|
+
TMUX_IDE_OPENCODE_DIR: namespace.opencodeDir,
|
|
80
|
+
};
|
|
81
|
+
for (const [name, value] of Object.entries(environment)) safeValue(value, name);
|
|
82
|
+
const args = [executable, "--headless", "--supervised", supervisionId];
|
|
83
|
+
const shared = {
|
|
84
|
+
supervisionId,
|
|
85
|
+
executable,
|
|
86
|
+
recordPath: join(namespace.daemonInfoDir, "service.json"),
|
|
87
|
+
stdoutPath,
|
|
88
|
+
stderrPath,
|
|
89
|
+
};
|
|
90
|
+
if (options.platform === "darwin") {
|
|
91
|
+
const label = `com.${supervisionId}`;
|
|
92
|
+
const string = (value: string) => `<string>${xml(value)}</string>`;
|
|
93
|
+
return Object.freeze({
|
|
94
|
+
...shared,
|
|
95
|
+
manager: "launchd",
|
|
96
|
+
target: `gui/${options.uid}/${label}`,
|
|
97
|
+
unitPath: join(home, "Library", "LaunchAgents", `${label}.plist`),
|
|
98
|
+
contents: `<?xml version="1.0" encoding="UTF-8"?>
|
|
99
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
100
|
+
<plist version="1.0"><dict>
|
|
101
|
+
<key>Label</key>${string(label)}
|
|
102
|
+
<key>ProgramArguments</key><array>${args.map(string).join("")}</array>
|
|
103
|
+
<key>EnvironmentVariables</key><dict>${Object.entries(environment)
|
|
104
|
+
.map(([key, value]) => `<key>${xml(key)}</key>${string(value)}`)
|
|
105
|
+
.join("")}</dict>
|
|
106
|
+
<key>WorkingDirectory</key>${string(home)}
|
|
107
|
+
<key>RunAtLoad</key><true/>
|
|
108
|
+
<key>KeepAlive</key><true/>
|
|
109
|
+
<key>ThrottleInterval</key><integer>3</integer>
|
|
110
|
+
<key>ExitTimeOut</key><integer>30</integer>
|
|
111
|
+
<key>AbandonProcessGroup</key><true/>
|
|
112
|
+
<key>StandardOutPath</key>${string(stdoutPath)}
|
|
113
|
+
<key>StandardErrorPath</key>${string(stderrPath)}
|
|
114
|
+
</dict></plist>
|
|
115
|
+
`,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
const configHome = absolute(options.configHome ?? join(home, ".config"), "Config directory");
|
|
119
|
+
const target = `${supervisionId}.service`;
|
|
120
|
+
// WorkingDirectory is a raw path, not an ExecStart-style quoted word.
|
|
121
|
+
// The equivalent '/.' suffix preserves trailing spaces/backslashes without
|
|
122
|
+
// allowing the unit parser to trim them or treat them as line continuation.
|
|
123
|
+
const workingDirectory = `${home.replace(/%/gu, "%%")}/.`;
|
|
124
|
+
return Object.freeze({
|
|
125
|
+
...shared,
|
|
126
|
+
manager: "systemd",
|
|
127
|
+
target,
|
|
128
|
+
unitPath: join(configHome, "systemd", "user", target),
|
|
129
|
+
contents: `[Unit]
|
|
130
|
+
Description=tmux-ide canonical user daemon
|
|
131
|
+
StartLimitIntervalSec=60
|
|
132
|
+
StartLimitBurst=10
|
|
133
|
+
|
|
134
|
+
[Service]
|
|
135
|
+
Type=exec
|
|
136
|
+
ExecStart=:${args.map(systemd).join(" ")}
|
|
137
|
+
WorkingDirectory=${workingDirectory}
|
|
138
|
+
${Object.entries(environment)
|
|
139
|
+
.map(([key, value]) => `Environment=${systemd(`${key}=${value}`)}`)
|
|
140
|
+
.join("\n")}
|
|
141
|
+
Restart=always
|
|
142
|
+
RestartSec=3
|
|
143
|
+
TimeoutStopSec=30
|
|
144
|
+
# tmux pane work has an independent lifetime; never kill the whole service cgroup.
|
|
145
|
+
KillMode=process
|
|
146
|
+
StandardOutput=journal
|
|
147
|
+
StandardError=journal
|
|
148
|
+
|
|
149
|
+
[Install]
|
|
150
|
+
WantedBy=default.target
|
|
151
|
+
`,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
@@ -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) {
|