@hellcoder/companion 0.96.0
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/bin/cli.ts +168 -0
- package/bin/ctl.ts +528 -0
- package/bin/generate-token.ts +28 -0
- package/dist/apple-touch-icon.png +0 -0
- package/dist/assets/AgentsPage-DCFhrJ28.js +13 -0
- package/dist/assets/CronManager-EGwLJONv.js +1 -0
- package/dist/assets/IntegrationsPage-CTMRnbQS.js +1 -0
- package/dist/assets/LinearOAuthSettingsPage-CgQFMIgr.js +1 -0
- package/dist/assets/LinearSettingsPage-C9nok1qi.js +1 -0
- package/dist/assets/Playground-BV3k0RbV.js +109 -0
- package/dist/assets/PromptsPage-CFojqNKP.js +4 -0
- package/dist/assets/RunsPage-DUJ1QUSa.js +1 -0
- package/dist/assets/SandboxManager-CrVQ-VU_.js +8 -0
- package/dist/assets/SettingsPage-D1fPCL19.js +1 -0
- package/dist/assets/TailscalePage-D06cyvyC.js +1 -0
- package/dist/assets/index-BhUa1e6X.css +1 -0
- package/dist/assets/index-DkqeP-R9.js +134 -0
- package/dist/assets/sw-register-BibwRdvC.js +1 -0
- package/dist/assets/workbox-window.prod.es5-BIl4cyR9.js +2 -0
- package/dist/favicon.svg +8 -0
- package/dist/fonts/MesloLGSNerdFontMono-Bold.woff2 +0 -0
- package/dist/fonts/MesloLGSNerdFontMono-Regular.woff2 +0 -0
- package/dist/icon-192.png +0 -0
- package/dist/icon-512.png +0 -0
- package/dist/index.html +20 -0
- package/dist/logo-codex.svg +14 -0
- package/dist/logo-docker.svg +4 -0
- package/dist/logo.svg +14 -0
- package/dist/manifest.json +24 -0
- package/dist/sw.js +2 -0
- package/package.json +104 -0
- package/server/agent-cron-migrator.test.ts +610 -0
- package/server/agent-cron-migrator.ts +85 -0
- package/server/agent-executor.test.ts +1108 -0
- package/server/agent-executor.ts +346 -0
- package/server/agent-store.test.ts +588 -0
- package/server/agent-store.ts +185 -0
- package/server/agent-types.ts +138 -0
- package/server/ai-validation-settings.test.ts +128 -0
- package/server/ai-validation-settings.ts +35 -0
- package/server/ai-validator.test.ts +387 -0
- package/server/ai-validator.ts +271 -0
- package/server/auth-manager.test.ts +83 -0
- package/server/auth-manager.ts +150 -0
- package/server/auto-namer.test.ts +252 -0
- package/server/auto-namer.ts +78 -0
- package/server/backend-adapter.test.ts +38 -0
- package/server/backend-adapter.ts +54 -0
- package/server/cache-headers.test.ts +98 -0
- package/server/cache-headers.ts +61 -0
- package/server/claude-adapter.test.ts +1363 -0
- package/server/claude-adapter.ts +889 -0
- package/server/claude-container-auth.test.ts +44 -0
- package/server/claude-container-auth.ts +30 -0
- package/server/claude-protocol-contract.test.ts +71 -0
- package/server/claude-protocol-drift.test.ts +78 -0
- package/server/claude-session-discovery.test.ts +132 -0
- package/server/claude-session-discovery.ts +157 -0
- package/server/claude-session-history.test.ts +158 -0
- package/server/claude-session-history.ts +410 -0
- package/server/cli-launcher.test.ts +1343 -0
- package/server/cli-launcher.ts +1298 -0
- package/server/cli.test.ts +16 -0
- package/server/codex-adapter.test.ts +5545 -0
- package/server/codex-adapter.ts +3062 -0
- package/server/codex-container-auth.test.ts +50 -0
- package/server/codex-container-auth.ts +24 -0
- package/server/codex-home.test.ts +61 -0
- package/server/codex-home.ts +26 -0
- package/server/codex-protocol-contract.test.ts +96 -0
- package/server/codex-protocol-drift.test.ts +123 -0
- package/server/codex-ws-proxy.cjs +226 -0
- package/server/commands-discovery.test.ts +179 -0
- package/server/commands-discovery.ts +81 -0
- package/server/constants.ts +7 -0
- package/server/container-manager.test.ts +1211 -0
- package/server/container-manager.ts +1053 -0
- package/server/cron-scheduler.test.ts +957 -0
- package/server/cron-scheduler.ts +243 -0
- package/server/cron-store.test.ts +422 -0
- package/server/cron-store.ts +148 -0
- package/server/cron-types.ts +63 -0
- package/server/env-manager.test.ts +268 -0
- package/server/env-manager.ts +161 -0
- package/server/event-bus-types.ts +64 -0
- package/server/event-bus.test.ts +244 -0
- package/server/event-bus.ts +124 -0
- package/server/execution-store.test.ts +307 -0
- package/server/execution-store.ts +170 -0
- package/server/fs-utils.ts +15 -0
- package/server/git-utils.test.ts +938 -0
- package/server/git-utils.ts +421 -0
- package/server/github-pr.test.ts +498 -0
- package/server/github-pr.ts +379 -0
- package/server/image-pull-manager.test.ts +303 -0
- package/server/image-pull-manager.ts +279 -0
- package/server/index.ts +396 -0
- package/server/linear-agent-bridge.test.ts +1157 -0
- package/server/linear-agent-bridge.ts +629 -0
- package/server/linear-agent.test.ts +473 -0
- package/server/linear-agent.ts +479 -0
- package/server/linear-cache.test.ts +136 -0
- package/server/linear-cache.ts +113 -0
- package/server/linear-connections.test.ts +350 -0
- package/server/linear-connections.ts +231 -0
- package/server/linear-credential-migration.test.ts +337 -0
- package/server/linear-credential-migration.ts +63 -0
- package/server/linear-oauth-connections-migration.test.ts +268 -0
- package/server/linear-oauth-connections.test.ts +365 -0
- package/server/linear-oauth-connections.ts +294 -0
- package/server/linear-project-manager.test.ts +162 -0
- package/server/linear-project-manager.ts +111 -0
- package/server/linear-prompt-builder.test.ts +74 -0
- package/server/linear-prompt-builder.ts +61 -0
- package/server/linear-staging.test.ts +276 -0
- package/server/linear-staging.ts +142 -0
- package/server/logger.test.ts +393 -0
- package/server/logger.ts +259 -0
- package/server/metrics-collector.test.ts +413 -0
- package/server/metrics-collector.ts +350 -0
- package/server/metrics-types.ts +108 -0
- package/server/middleware/managed-auth.test.ts +264 -0
- package/server/middleware/managed-auth.ts +195 -0
- package/server/novnc-proxy.test.ts +333 -0
- package/server/novnc-proxy.ts +99 -0
- package/server/path-resolver.test.ts +552 -0
- package/server/path-resolver.ts +186 -0
- package/server/paths.test.ts +31 -0
- package/server/paths.ts +11 -0
- package/server/pr-poller.test.ts +191 -0
- package/server/pr-poller.ts +162 -0
- package/server/prompt-manager.test.ts +211 -0
- package/server/prompt-manager.ts +211 -0
- package/server/protocol/claude-upstream/README.md +19 -0
- package/server/protocol/claude-upstream/sdk.d.ts.txt +1943 -0
- package/server/protocol/codex-upstream/ClientNotification.ts.txt +5 -0
- package/server/protocol/codex-upstream/ClientRequest.ts.txt +60 -0
- package/server/protocol/codex-upstream/README.md +18 -0
- package/server/protocol/codex-upstream/ServerNotification.ts.txt +41 -0
- package/server/protocol/codex-upstream/ServerRequest.ts.txt +16 -0
- package/server/protocol/codex-upstream/v2/DynamicToolCallParams.ts.txt +6 -0
- package/server/protocol/codex-upstream/v2/DynamicToolCallResponse.ts.txt +6 -0
- package/server/protocol-monitor.ts +50 -0
- package/server/recorder.test.ts +454 -0
- package/server/recorder.ts +374 -0
- package/server/recording-hub/compat-validator.test.ts +150 -0
- package/server/recording-hub/compat-validator.ts +284 -0
- package/server/recording-hub/diagnostics.test.ts +140 -0
- package/server/recording-hub/diagnostics.ts +299 -0
- package/server/recording-hub/hub-config.test.ts +44 -0
- package/server/recording-hub/hub-config.ts +19 -0
- package/server/recording-hub/hub-routes.test.ts +417 -0
- package/server/recording-hub/hub-routes.ts +236 -0
- package/server/recording-hub/hub-store.test.ts +262 -0
- package/server/recording-hub/hub-store.ts +265 -0
- package/server/recording-hub/replay-adapter.test.ts +294 -0
- package/server/recording-hub/replay-adapter.ts +207 -0
- package/server/relay-client.test.ts +337 -0
- package/server/relay-client.ts +320 -0
- package/server/replay.test.ts +200 -0
- package/server/replay.ts +78 -0
- package/server/routes/agent-routes.test.ts +1400 -0
- package/server/routes/agent-routes.ts +409 -0
- package/server/routes/cron-routes.test.ts +881 -0
- package/server/routes/cron-routes.ts +103 -0
- package/server/routes/env-routes.test.ts +383 -0
- package/server/routes/env-routes.ts +95 -0
- package/server/routes/fs-routes.test.ts +1198 -0
- package/server/routes/fs-routes.ts +605 -0
- package/server/routes/git-routes.test.ts +813 -0
- package/server/routes/git-routes.ts +97 -0
- package/server/routes/linear-agent-routes.test.ts +721 -0
- package/server/routes/linear-agent-routes.ts +304 -0
- package/server/routes/linear-connection-routes.test.ts +927 -0
- package/server/routes/linear-connection-routes.ts +244 -0
- package/server/routes/linear-oauth-connection-routes.test.ts +406 -0
- package/server/routes/linear-oauth-connection-routes.ts +129 -0
- package/server/routes/linear-routes.test.ts +1510 -0
- package/server/routes/linear-routes.ts +953 -0
- package/server/routes/metrics-routes.test.ts +103 -0
- package/server/routes/metrics-routes.ts +13 -0
- package/server/routes/prompt-routes.ts +67 -0
- package/server/routes/sandbox-routes.test.ts +513 -0
- package/server/routes/sandbox-routes.ts +127 -0
- package/server/routes/settings-routes.ts +270 -0
- package/server/routes/skills-routes.test.ts +690 -0
- package/server/routes/skills-routes.ts +100 -0
- package/server/routes/system-routes.test.ts +637 -0
- package/server/routes/system-routes.ts +228 -0
- package/server/routes/tailscale-routes.test.ts +176 -0
- package/server/routes/tailscale-routes.ts +22 -0
- package/server/routes.test.ts +4655 -0
- package/server/routes.ts +1277 -0
- package/server/sandbox-manager.test.ts +378 -0
- package/server/sandbox-manager.ts +168 -0
- package/server/service.test.ts +1419 -0
- package/server/service.ts +718 -0
- package/server/session-creation-service.test.ts +661 -0
- package/server/session-creation-service.ts +473 -0
- package/server/session-git-info.ts +104 -0
- package/server/session-linear-issues.test.ts +118 -0
- package/server/session-linear-issues.ts +88 -0
- package/server/session-names.test.ts +94 -0
- package/server/session-names.ts +67 -0
- package/server/session-orchestrator.test.ts +1784 -0
- package/server/session-orchestrator.ts +973 -0
- package/server/session-state-machine.test.ts +606 -0
- package/server/session-state-machine.ts +207 -0
- package/server/session-store.test.ts +290 -0
- package/server/session-store.ts +146 -0
- package/server/session-types.ts +509 -0
- package/server/settings-manager.test.ts +275 -0
- package/server/settings-manager.ts +173 -0
- package/server/tailscale-manager.test.ts +553 -0
- package/server/tailscale-manager.ts +451 -0
- package/server/terminal-manager.ts +240 -0
- package/server/update-checker.test.ts +306 -0
- package/server/update-checker.ts +197 -0
- package/server/usage-limits.test.ts +536 -0
- package/server/usage-limits.ts +225 -0
- package/server/worktree-tracker.test.ts +243 -0
- package/server/worktree-tracker.ts +84 -0
- package/server/ws-auth.test.ts +59 -0
- package/server/ws-auth.ts +41 -0
- package/server/ws-bridge-browser-ingest.test.ts +272 -0
- package/server/ws-bridge-browser-ingest.ts +72 -0
- package/server/ws-bridge-browser.ts +112 -0
- package/server/ws-bridge-cli-ingest.test.ts +302 -0
- package/server/ws-bridge-cli-ingest.ts +81 -0
- package/server/ws-bridge-codex.test.ts +1837 -0
- package/server/ws-bridge-codex.ts +266 -0
- package/server/ws-bridge-controls.test.ts +124 -0
- package/server/ws-bridge-controls.ts +20 -0
- package/server/ws-bridge-persist.test.ts +296 -0
- package/server/ws-bridge-persist.ts +66 -0
- package/server/ws-bridge-publish.test.ts +234 -0
- package/server/ws-bridge-publish.ts +79 -0
- package/server/ws-bridge-replay.test.ts +44 -0
- package/server/ws-bridge-replay.ts +61 -0
- package/server/ws-bridge-types.ts +106 -0
- package/server/ws-bridge.test.ts +4777 -0
- package/server/ws-bridge.ts +1279 -0
|
@@ -0,0 +1,718 @@
|
|
|
1
|
+
import {
|
|
2
|
+
mkdirSync,
|
|
3
|
+
writeFileSync,
|
|
4
|
+
unlinkSync,
|
|
5
|
+
existsSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
} from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { homedir } from "node:os";
|
|
10
|
+
import { execSync } from "node:child_process";
|
|
11
|
+
import { DEFAULT_PORT_PROD } from "./constants.js";
|
|
12
|
+
import { getServicePath } from "./path-resolver.js";
|
|
13
|
+
import { COMPANION_HOME } from "./paths.js";
|
|
14
|
+
|
|
15
|
+
// ─── Shared Constants ───────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
const LOG_DIR = join(COMPANION_HOME, "logs");
|
|
18
|
+
const STDOUT_LOG = join(LOG_DIR, "companion.log");
|
|
19
|
+
const STDERR_LOG = join(LOG_DIR, "companion.error.log");
|
|
20
|
+
|
|
21
|
+
// ─── macOS (launchd) Constants ──────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
const LABEL = "sh.thecompanion.app";
|
|
24
|
+
const OLD_LABEL = "co.thevibecompany.companion";
|
|
25
|
+
const PLIST_DIR = join(homedir(), "Library", "LaunchAgents");
|
|
26
|
+
const PLIST_PATH = join(PLIST_DIR, `${LABEL}.plist`);
|
|
27
|
+
const OLD_PLIST_PATH = join(PLIST_DIR, `${OLD_LABEL}.plist`);
|
|
28
|
+
|
|
29
|
+
// ─── Linux (systemd) Constants ──────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
const SYSTEMD_DIR = join(homedir(), ".config", "systemd", "user");
|
|
32
|
+
const UNIT_NAME = "the-companion.service";
|
|
33
|
+
const UNIT_PATH = join(SYSTEMD_DIR, UNIT_NAME);
|
|
34
|
+
|
|
35
|
+
// ─── Platform check ─────────────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
function ensureSupportedPlatform(): void {
|
|
38
|
+
if (process.platform !== "darwin" && process.platform !== "linux") {
|
|
39
|
+
console.error(
|
|
40
|
+
"Service management is only supported on macOS (launchd) and Linux (systemd).",
|
|
41
|
+
);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function isDarwin(): boolean {
|
|
47
|
+
return process.platform === "darwin";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function isLinux(): boolean {
|
|
51
|
+
return process.platform === "linux";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ─── Plist generation (macOS) ───────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
interface PlistOptions {
|
|
57
|
+
binPath: string;
|
|
58
|
+
port?: number;
|
|
59
|
+
path?: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function generatePlist(opts: PlistOptions): string {
|
|
63
|
+
const port = opts.port ?? DEFAULT_PORT_PROD;
|
|
64
|
+
const home = homedir();
|
|
65
|
+
|
|
66
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
67
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
68
|
+
<plist version="1.0">
|
|
69
|
+
<dict>
|
|
70
|
+
<key>Label</key>
|
|
71
|
+
<string>${LABEL}</string>
|
|
72
|
+
|
|
73
|
+
<key>ProgramArguments</key>
|
|
74
|
+
<array>
|
|
75
|
+
<string>${opts.binPath}</string>
|
|
76
|
+
<string>start</string>
|
|
77
|
+
<string>--foreground</string>
|
|
78
|
+
</array>
|
|
79
|
+
|
|
80
|
+
<key>WorkingDirectory</key>
|
|
81
|
+
<string>${home}</string>
|
|
82
|
+
|
|
83
|
+
<key>RunAtLoad</key>
|
|
84
|
+
<true/>
|
|
85
|
+
|
|
86
|
+
<key>KeepAlive</key>
|
|
87
|
+
<dict>
|
|
88
|
+
<key>SuccessfulExit</key>
|
|
89
|
+
<false/>
|
|
90
|
+
</dict>
|
|
91
|
+
|
|
92
|
+
<key>StandardOutPath</key>
|
|
93
|
+
<string>${STDOUT_LOG}</string>
|
|
94
|
+
|
|
95
|
+
<key>StandardErrorPath</key>
|
|
96
|
+
<string>${STDERR_LOG}</string>
|
|
97
|
+
|
|
98
|
+
<key>EnvironmentVariables</key>
|
|
99
|
+
<dict>
|
|
100
|
+
<key>NODE_ENV</key>
|
|
101
|
+
<string>production</string>
|
|
102
|
+
<key>PORT</key>
|
|
103
|
+
<string>${port}</string>
|
|
104
|
+
<key>HOME</key>
|
|
105
|
+
<string>${home}</string>
|
|
106
|
+
<key>PATH</key>
|
|
107
|
+
<string>${opts.path || getServicePath()}</string>
|
|
108
|
+
</dict>
|
|
109
|
+
|
|
110
|
+
<key>ProcessType</key>
|
|
111
|
+
<string>Interactive</string>
|
|
112
|
+
|
|
113
|
+
<key>ThrottleInterval</key>
|
|
114
|
+
<integer>5</integer>
|
|
115
|
+
</dict>
|
|
116
|
+
</plist>`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ─── Systemd unit generation (Linux) ────────────────────────────────────────────
|
|
120
|
+
|
|
121
|
+
interface UnitOptions {
|
|
122
|
+
binPath: string;
|
|
123
|
+
port?: number;
|
|
124
|
+
path?: string;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function generateSystemdUnit(opts: UnitOptions): string {
|
|
128
|
+
const port = opts.port ?? DEFAULT_PORT_PROD;
|
|
129
|
+
const home = homedir();
|
|
130
|
+
|
|
131
|
+
return `[Unit]
|
|
132
|
+
Description=The Companion - Web UI for Claude Code
|
|
133
|
+
After=network.target
|
|
134
|
+
|
|
135
|
+
[Service]
|
|
136
|
+
Type=simple
|
|
137
|
+
ExecStart=${opts.binPath} start --foreground
|
|
138
|
+
WorkingDirectory=${home}
|
|
139
|
+
Restart=always
|
|
140
|
+
RestartSec=5
|
|
141
|
+
SuccessExitStatus=42
|
|
142
|
+
StandardOutput=append:${STDOUT_LOG}
|
|
143
|
+
StandardError=append:${STDERR_LOG}
|
|
144
|
+
Environment=NODE_ENV=production
|
|
145
|
+
Environment=PORT=${port}
|
|
146
|
+
Environment=HOME=${home}
|
|
147
|
+
Environment=PATH=${opts.path || getServicePath()}
|
|
148
|
+
|
|
149
|
+
[Install]
|
|
150
|
+
WantedBy=default.target
|
|
151
|
+
`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ─── Binary resolution ──────────────────────────────────────────────────────────
|
|
155
|
+
|
|
156
|
+
function resolveBinPath(): string {
|
|
157
|
+
try {
|
|
158
|
+
const binPath = execSync("which the-companion", { encoding: "utf-8" }).trim();
|
|
159
|
+
if (binPath) return binPath;
|
|
160
|
+
} catch {
|
|
161
|
+
// not found globally
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
console.error("the-companion must be installed globally for service mode.");
|
|
165
|
+
console.error("");
|
|
166
|
+
console.error(" bun install -g the-companion");
|
|
167
|
+
console.error("");
|
|
168
|
+
console.error("Then retry:");
|
|
169
|
+
console.error("");
|
|
170
|
+
console.error(" the-companion install");
|
|
171
|
+
process.exit(1);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ─── macOS helpers ──────────────────────────────────────────────────────────────
|
|
175
|
+
|
|
176
|
+
function unloadLaunchdService(plistPath: string): void {
|
|
177
|
+
try {
|
|
178
|
+
execSync(`launchctl unload -w "${plistPath}"`, { stdio: "pipe" });
|
|
179
|
+
} catch {
|
|
180
|
+
// Service may already be unloaded — that's fine
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function removePlist(plistPath: string): void {
|
|
185
|
+
try {
|
|
186
|
+
unlinkSync(plistPath);
|
|
187
|
+
} catch {
|
|
188
|
+
// Already gone
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function migrateLegacyInstallIfNeeded(): void {
|
|
193
|
+
if (!existsSync(OLD_PLIST_PATH)) return;
|
|
194
|
+
|
|
195
|
+
console.log("Found legacy The Vibe Companion service. Migrating...");
|
|
196
|
+
unloadLaunchdService(OLD_PLIST_PATH);
|
|
197
|
+
removePlist(OLD_PLIST_PATH);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function getInstalledLaunchdService():
|
|
201
|
+
| { label: string; plistPath: string }
|
|
202
|
+
| undefined {
|
|
203
|
+
if (existsSync(PLIST_PATH)) return { label: LABEL, plistPath: PLIST_PATH };
|
|
204
|
+
if (existsSync(OLD_PLIST_PATH)) {
|
|
205
|
+
return { label: OLD_LABEL, plistPath: OLD_PLIST_PATH };
|
|
206
|
+
}
|
|
207
|
+
return undefined;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ─── Linux helpers ──────────────────────────────────────────────────────────────
|
|
211
|
+
|
|
212
|
+
function isSystemdUnitInstalled(): boolean {
|
|
213
|
+
return existsSync(UNIT_PATH);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function systemctlUser(cmd: string): string {
|
|
217
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : 1000;
|
|
218
|
+
return execSync(`systemctl --user ${cmd}`, {
|
|
219
|
+
encoding: "utf-8",
|
|
220
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
221
|
+
env: {
|
|
222
|
+
...process.env,
|
|
223
|
+
XDG_RUNTIME_DIR: process.env.XDG_RUNTIME_DIR || `/run/user/${uid}`,
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ─── Install ────────────────────────────────────────────────────────────────────
|
|
229
|
+
|
|
230
|
+
export async function install(opts?: { port?: number }): Promise<void> {
|
|
231
|
+
ensureSupportedPlatform();
|
|
232
|
+
|
|
233
|
+
if (isDarwin()) {
|
|
234
|
+
return installDarwin(opts);
|
|
235
|
+
}
|
|
236
|
+
return installLinux(opts);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async function installDarwin(opts?: { port?: number }): Promise<void> {
|
|
240
|
+
migrateLegacyInstallIfNeeded();
|
|
241
|
+
|
|
242
|
+
if (existsSync(PLIST_PATH)) {
|
|
243
|
+
console.error("The Companion is already installed as a service.");
|
|
244
|
+
console.error("Run 'the-companion uninstall' first to reinstall.");
|
|
245
|
+
process.exit(1);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const binPath = resolveBinPath();
|
|
249
|
+
const port = opts?.port ?? DEFAULT_PORT_PROD;
|
|
250
|
+
|
|
251
|
+
// Create log directory
|
|
252
|
+
mkdirSync(LOG_DIR, { recursive: true });
|
|
253
|
+
|
|
254
|
+
// Generate and write plist (capture user's shell PATH at install time)
|
|
255
|
+
const path = getServicePath();
|
|
256
|
+
const plist = generatePlist({ binPath, port, path });
|
|
257
|
+
mkdirSync(PLIST_DIR, { recursive: true });
|
|
258
|
+
writeFileSync(PLIST_PATH, plist, "utf-8");
|
|
259
|
+
|
|
260
|
+
// Load the service
|
|
261
|
+
try {
|
|
262
|
+
execSync(`launchctl load -w "${PLIST_PATH}"`, { stdio: "pipe" });
|
|
263
|
+
} catch (err: unknown) {
|
|
264
|
+
console.error("Failed to load the service with launchctl:");
|
|
265
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
266
|
+
// Clean up the plist on failure
|
|
267
|
+
try { unlinkSync(PLIST_PATH); } catch { /* ok */ }
|
|
268
|
+
process.exit(1);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
console.log("The Companion has been installed as a background service.");
|
|
272
|
+
console.log("");
|
|
273
|
+
console.log(` URL: http://localhost:${port}`);
|
|
274
|
+
console.log(` Logs: ${LOG_DIR}`);
|
|
275
|
+
console.log(` Plist: ${PLIST_PATH}`);
|
|
276
|
+
console.log("");
|
|
277
|
+
console.log("The service will start automatically on login.");
|
|
278
|
+
console.log("Use 'the-companion status' to check if it's running.");
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async function installLinux(opts?: { port?: number }): Promise<void> {
|
|
282
|
+
if (isSystemdUnitInstalled()) {
|
|
283
|
+
console.error("The Companion is already installed as a service.");
|
|
284
|
+
console.error("Run 'the-companion uninstall' first to reinstall.");
|
|
285
|
+
process.exit(1);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const binPath = resolveBinPath();
|
|
289
|
+
const port = opts?.port ?? DEFAULT_PORT_PROD;
|
|
290
|
+
|
|
291
|
+
// Create log directory
|
|
292
|
+
mkdirSync(LOG_DIR, { recursive: true });
|
|
293
|
+
|
|
294
|
+
// Generate and write systemd unit (capture user's shell PATH at install time)
|
|
295
|
+
const path = getServicePath();
|
|
296
|
+
const unit = generateSystemdUnit({ binPath, port, path });
|
|
297
|
+
mkdirSync(SYSTEMD_DIR, { recursive: true });
|
|
298
|
+
writeFileSync(UNIT_PATH, unit, "utf-8");
|
|
299
|
+
|
|
300
|
+
// Reload systemd and enable + start the service
|
|
301
|
+
try {
|
|
302
|
+
systemctlUser("daemon-reload");
|
|
303
|
+
systemctlUser(`enable --now ${UNIT_NAME}`);
|
|
304
|
+
} catch (err: unknown) {
|
|
305
|
+
console.error("Failed to enable the service with systemctl:");
|
|
306
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
307
|
+
// Clean up the unit file on failure
|
|
308
|
+
try { unlinkSync(UNIT_PATH); } catch { /* ok */ }
|
|
309
|
+
process.exit(1);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Enable linger so user services survive logout
|
|
313
|
+
try {
|
|
314
|
+
execSync("loginctl enable-linger", { stdio: ["pipe", "pipe", "pipe"] });
|
|
315
|
+
} catch {
|
|
316
|
+
console.warn(
|
|
317
|
+
"Warning: Could not enable linger. The service may stop when you log out.",
|
|
318
|
+
);
|
|
319
|
+
console.warn(" sudo loginctl enable-linger $(whoami)");
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
console.log("The Companion has been installed as a background service.");
|
|
323
|
+
console.log("");
|
|
324
|
+
console.log(` URL: http://localhost:${port}`);
|
|
325
|
+
console.log(` Logs: ${LOG_DIR}`);
|
|
326
|
+
console.log(` Unit: ${UNIT_PATH}`);
|
|
327
|
+
console.log("");
|
|
328
|
+
console.log("The service will start automatically on login.");
|
|
329
|
+
console.log("Use 'the-companion status' to check if it's running.");
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// ─── Uninstall ──────────────────────────────────────────────────────────────────
|
|
333
|
+
|
|
334
|
+
export async function uninstall(): Promise<void> {
|
|
335
|
+
ensureSupportedPlatform();
|
|
336
|
+
|
|
337
|
+
if (isDarwin()) {
|
|
338
|
+
return uninstallDarwin();
|
|
339
|
+
}
|
|
340
|
+
return uninstallLinux();
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
async function uninstallDarwin(): Promise<void> {
|
|
344
|
+
const installedService = getInstalledLaunchdService();
|
|
345
|
+
if (!installedService) {
|
|
346
|
+
console.log("The Companion is not installed as a service.");
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
unloadLaunchdService(installedService.plistPath);
|
|
351
|
+
removePlist(installedService.plistPath);
|
|
352
|
+
|
|
353
|
+
console.log("The Companion service has been removed.");
|
|
354
|
+
console.log(`Logs are preserved at ${LOG_DIR}`);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
async function uninstallLinux(): Promise<void> {
|
|
358
|
+
if (!isSystemdUnitInstalled()) {
|
|
359
|
+
console.log("The Companion is not installed as a service.");
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
try {
|
|
364
|
+
systemctlUser(`disable --now ${UNIT_NAME}`);
|
|
365
|
+
} catch {
|
|
366
|
+
// Service may already be stopped — that's fine
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
try {
|
|
370
|
+
unlinkSync(UNIT_PATH);
|
|
371
|
+
} catch {
|
|
372
|
+
// Already gone
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
try {
|
|
376
|
+
systemctlUser("daemon-reload");
|
|
377
|
+
} catch {
|
|
378
|
+
// Best-effort reload
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
console.log("The Companion service has been removed.");
|
|
382
|
+
console.log(`Logs are preserved at ${LOG_DIR}`);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// ─── Stop / Restart ────────────────────────────────────────────────────────────
|
|
386
|
+
|
|
387
|
+
export async function start(): Promise<void> {
|
|
388
|
+
ensureSupportedPlatform();
|
|
389
|
+
|
|
390
|
+
if (isDarwin()) {
|
|
391
|
+
return startDarwin();
|
|
392
|
+
}
|
|
393
|
+
return startLinux();
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
async function startDarwin(): Promise<void> {
|
|
397
|
+
const installedService = getInstalledLaunchdService();
|
|
398
|
+
if (!installedService) {
|
|
399
|
+
console.log("The Companion is not installed as a service.");
|
|
400
|
+
console.log("Run 'the-companion install' first.");
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : undefined;
|
|
405
|
+
const domain = uid !== undefined ? `gui/${uid}` : "gui";
|
|
406
|
+
const domainTarget = uid !== undefined
|
|
407
|
+
? `gui/${uid}/${installedService.label}`
|
|
408
|
+
: installedService.label;
|
|
409
|
+
|
|
410
|
+
try {
|
|
411
|
+
execSync(`launchctl kickstart -k "${domainTarget}"`, { stdio: "pipe" });
|
|
412
|
+
} catch {
|
|
413
|
+
try {
|
|
414
|
+
execSync(`launchctl bootstrap "${domain}" "${installedService.plistPath}"`, { stdio: "pipe" });
|
|
415
|
+
} catch {
|
|
416
|
+
try {
|
|
417
|
+
execSync(`launchctl load -w "${installedService.plistPath}"`, { stdio: "pipe" });
|
|
418
|
+
} catch (err: unknown) {
|
|
419
|
+
console.error("Failed to start the service with launchctl:");
|
|
420
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
421
|
+
process.exit(1);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
console.log("The Companion service has been started.");
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
async function startLinux(): Promise<void> {
|
|
430
|
+
if (!isSystemdUnitInstalled()) {
|
|
431
|
+
console.log("Service not yet installed. Installing now...");
|
|
432
|
+
await installLinux();
|
|
433
|
+
return; // installLinux uses enable --now which starts the service
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Ensure the installed unit file matches the latest template (e.g.
|
|
437
|
+
// SuccessExitStatus=42, Restart=always) so that stale definitions from
|
|
438
|
+
// older versions don't cause restart loops after an auto-update.
|
|
439
|
+
refreshServiceDefinition();
|
|
440
|
+
|
|
441
|
+
try {
|
|
442
|
+
systemctlUser(`start ${UNIT_NAME}`);
|
|
443
|
+
} catch (err: unknown) {
|
|
444
|
+
console.error("Failed to start the service with systemctl:");
|
|
445
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
446
|
+
process.exit(1);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
console.log("The Companion service has been started.");
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
export async function stop(): Promise<void> {
|
|
453
|
+
ensureSupportedPlatform();
|
|
454
|
+
|
|
455
|
+
if (isDarwin()) {
|
|
456
|
+
return stopDarwin();
|
|
457
|
+
}
|
|
458
|
+
return stopLinux();
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
async function stopDarwin(): Promise<void> {
|
|
462
|
+
const installedService = getInstalledLaunchdService();
|
|
463
|
+
if (!installedService) {
|
|
464
|
+
console.log("The Companion is not installed as a service.");
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
try {
|
|
469
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : undefined;
|
|
470
|
+
const domainTarget = uid !== undefined
|
|
471
|
+
? `gui/${uid}/${installedService.label}`
|
|
472
|
+
: installedService.label;
|
|
473
|
+
// `stop` is not enough with KeepAlive=true: launchd can immediately restart it.
|
|
474
|
+
// Booting out unloads the job from launchd while keeping the plist installed.
|
|
475
|
+
execSync(`launchctl bootout "${domainTarget}"`, { stdio: "pipe" });
|
|
476
|
+
} catch {
|
|
477
|
+
// Fallback for environments where bootout/domain targeting is unavailable.
|
|
478
|
+
unloadLaunchdService(installedService.plistPath);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
console.log("The Companion service has been stopped.");
|
|
482
|
+
console.log("Run 'the-companion restart' to start it again.");
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
async function stopLinux(): Promise<void> {
|
|
486
|
+
if (!isSystemdUnitInstalled()) {
|
|
487
|
+
console.log("The Companion is not installed as a service.");
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
try {
|
|
492
|
+
systemctlUser(`stop ${UNIT_NAME}`);
|
|
493
|
+
} catch (err: unknown) {
|
|
494
|
+
console.error("Failed to stop the service with systemctl:");
|
|
495
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
496
|
+
process.exit(1);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
console.log("The Companion service has been stopped.");
|
|
500
|
+
console.log("Run 'the-companion restart' to start it again.");
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
export async function restart(): Promise<void> {
|
|
504
|
+
ensureSupportedPlatform();
|
|
505
|
+
|
|
506
|
+
if (isDarwin()) {
|
|
507
|
+
return restartDarwin();
|
|
508
|
+
}
|
|
509
|
+
return restartLinux();
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
async function restartDarwin(): Promise<void> {
|
|
513
|
+
const installedService = getInstalledLaunchdService();
|
|
514
|
+
if (!installedService) {
|
|
515
|
+
console.log("The Companion is not installed as a service.");
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : undefined;
|
|
520
|
+
const domainTarget = uid !== undefined
|
|
521
|
+
? `gui/${uid}/${installedService.label}`
|
|
522
|
+
: installedService.label;
|
|
523
|
+
|
|
524
|
+
try {
|
|
525
|
+
execSync(`launchctl kickstart -k "${domainTarget}"`, { stdio: "pipe" });
|
|
526
|
+
} catch {
|
|
527
|
+
// Fallback for environments where kickstart/domain targeting is unavailable.
|
|
528
|
+
unloadLaunchdService(installedService.plistPath);
|
|
529
|
+
try {
|
|
530
|
+
execSync(`launchctl load -w "${installedService.plistPath}"`, { stdio: "pipe" });
|
|
531
|
+
} catch (err: unknown) {
|
|
532
|
+
console.error("Failed to restart the service with launchctl:");
|
|
533
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
534
|
+
process.exit(1);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
console.log("The Companion service has been restarted.");
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
async function restartLinux(): Promise<void> {
|
|
542
|
+
if (!isSystemdUnitInstalled()) {
|
|
543
|
+
console.log("The Companion is not installed as a service.");
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// Keep the unit file in sync with the latest template before restarting.
|
|
548
|
+
refreshServiceDefinition();
|
|
549
|
+
|
|
550
|
+
try {
|
|
551
|
+
systemctlUser(`restart ${UNIT_NAME}`);
|
|
552
|
+
} catch (err: unknown) {
|
|
553
|
+
console.error("Failed to restart the service with systemctl:");
|
|
554
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
555
|
+
process.exit(1);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
console.log("The Companion service has been restarted.");
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// ─── Status ─────────────────────────────────────────────────────────────────────
|
|
562
|
+
|
|
563
|
+
export interface ServiceStatus {
|
|
564
|
+
installed: boolean;
|
|
565
|
+
running: boolean;
|
|
566
|
+
pid?: number;
|
|
567
|
+
port?: number;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Safe check for whether the current process is running as a managed service.
|
|
572
|
+
* Unlike status(), this never calls process.exit() and works on all platforms.
|
|
573
|
+
*/
|
|
574
|
+
export function isRunningAsService(): boolean {
|
|
575
|
+
if (isDarwin()) {
|
|
576
|
+
const installedService = getInstalledLaunchdService();
|
|
577
|
+
if (!installedService) return false;
|
|
578
|
+
try {
|
|
579
|
+
const output = execSync(`launchctl list "${installedService.label}"`, {
|
|
580
|
+
encoding: "utf-8",
|
|
581
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
582
|
+
});
|
|
583
|
+
return /"PID"\s*=\s*\d+/.test(output);
|
|
584
|
+
} catch {
|
|
585
|
+
return false;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
if (isLinux()) {
|
|
590
|
+
if (!isSystemdUnitInstalled()) return false;
|
|
591
|
+
try {
|
|
592
|
+
const output = systemctlUser(`is-active ${UNIT_NAME}`);
|
|
593
|
+
return output.trim() === "active";
|
|
594
|
+
} catch {
|
|
595
|
+
return false;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
return false;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* Re-write the service definition (plist or systemd unit) using the current
|
|
604
|
+
* binary path and the latest template, preserving the user's custom port.
|
|
605
|
+
* On Linux this also calls daemon-reload so systemd picks up the changes.
|
|
606
|
+
*/
|
|
607
|
+
export function refreshServiceDefinition(): void {
|
|
608
|
+
if (isDarwin()) {
|
|
609
|
+
const installedService = getInstalledLaunchdService();
|
|
610
|
+
if (!installedService) return;
|
|
611
|
+
|
|
612
|
+
let port = DEFAULT_PORT_PROD;
|
|
613
|
+
try {
|
|
614
|
+
const content = readFileSync(installedService.plistPath, "utf-8");
|
|
615
|
+
const portMatch = content.match(/<key>PORT<\/key>\s*<string>(\d+)<\/string>/);
|
|
616
|
+
if (portMatch) port = Number(portMatch[1]);
|
|
617
|
+
} catch { /* use default */ }
|
|
618
|
+
|
|
619
|
+
const binPath = resolveBinPath();
|
|
620
|
+
const path = getServicePath();
|
|
621
|
+
const plist = generatePlist({ binPath, port, path });
|
|
622
|
+
writeFileSync(installedService.plistPath, plist, "utf-8");
|
|
623
|
+
} else if (isLinux()) {
|
|
624
|
+
if (!isSystemdUnitInstalled()) return;
|
|
625
|
+
|
|
626
|
+
let port = DEFAULT_PORT_PROD;
|
|
627
|
+
try {
|
|
628
|
+
const content = readFileSync(UNIT_PATH, "utf-8");
|
|
629
|
+
const portMatch = content.match(/Environment=PORT=(\d+)/);
|
|
630
|
+
if (portMatch) port = Number(portMatch[1]);
|
|
631
|
+
} catch { /* use default */ }
|
|
632
|
+
|
|
633
|
+
const binPath = resolveBinPath();
|
|
634
|
+
const path = getServicePath();
|
|
635
|
+
const unit = generateSystemdUnit({ binPath, port, path });
|
|
636
|
+
writeFileSync(UNIT_PATH, unit, "utf-8");
|
|
637
|
+
|
|
638
|
+
try {
|
|
639
|
+
systemctlUser("daemon-reload");
|
|
640
|
+
} catch { /* best effort */ }
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
export async function status(): Promise<ServiceStatus> {
|
|
645
|
+
ensureSupportedPlatform();
|
|
646
|
+
|
|
647
|
+
if (isDarwin()) {
|
|
648
|
+
return statusDarwin();
|
|
649
|
+
}
|
|
650
|
+
return statusLinux();
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
async function statusDarwin(): Promise<ServiceStatus> {
|
|
654
|
+
const installedService = getInstalledLaunchdService();
|
|
655
|
+
if (!installedService) {
|
|
656
|
+
return { installed: false, running: false };
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// Read port from the plist
|
|
660
|
+
let port = DEFAULT_PORT_PROD;
|
|
661
|
+
try {
|
|
662
|
+
const plistContent = readFileSync(installedService.plistPath, "utf-8");
|
|
663
|
+
const portMatch = plistContent.match(/<key>PORT<\/key>\s*<string>(\d+)<\/string>/);
|
|
664
|
+
if (portMatch) port = Number(portMatch[1]);
|
|
665
|
+
} catch { /* use default */ }
|
|
666
|
+
|
|
667
|
+
// Check if service is running via launchctl
|
|
668
|
+
try {
|
|
669
|
+
const output = execSync(`launchctl list "${installedService.label}"`, {
|
|
670
|
+
encoding: "utf-8",
|
|
671
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
// Parse PID from the launchctl list output
|
|
675
|
+
const pidMatch = output.match(/"PID"\s*=\s*(\d+)/);
|
|
676
|
+
if (pidMatch) {
|
|
677
|
+
return { installed: true, running: true, pid: Number(pidMatch[1]), port };
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// Service is loaded but not running (no PID)
|
|
681
|
+
return { installed: true, running: false, port };
|
|
682
|
+
} catch {
|
|
683
|
+
// launchctl list fails if service is not loaded
|
|
684
|
+
return { installed: true, running: false, port };
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
async function statusLinux(): Promise<ServiceStatus> {
|
|
689
|
+
if (!isSystemdUnitInstalled()) {
|
|
690
|
+
return { installed: false, running: false };
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// Read port from the unit file
|
|
694
|
+
let port = DEFAULT_PORT_PROD;
|
|
695
|
+
try {
|
|
696
|
+
const unitContent = readFileSync(UNIT_PATH, "utf-8");
|
|
697
|
+
const portMatch = unitContent.match(/Environment=PORT=(\d+)/);
|
|
698
|
+
if (portMatch) port = Number(portMatch[1]);
|
|
699
|
+
} catch { /* use default */ }
|
|
700
|
+
|
|
701
|
+
// Check if service is running via systemctl
|
|
702
|
+
try {
|
|
703
|
+
const output = systemctlUser(`show ${UNIT_NAME} --property=ActiveState,MainPID --no-pager`);
|
|
704
|
+
const activeMatch = output.match(/ActiveState=(\w+)/);
|
|
705
|
+
const pidMatch = output.match(/MainPID=(\d+)/);
|
|
706
|
+
|
|
707
|
+
const isActive = activeMatch?.[1] === "active";
|
|
708
|
+
const pid = pidMatch ? Number(pidMatch[1]) : undefined;
|
|
709
|
+
|
|
710
|
+
if (isActive && pid && pid > 0) {
|
|
711
|
+
return { installed: true, running: true, pid, port };
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
return { installed: true, running: false, port };
|
|
715
|
+
} catch {
|
|
716
|
+
return { installed: true, running: false, port };
|
|
717
|
+
}
|
|
718
|
+
}
|