@deepdream314/remodex 1.3.10 → 1.3.11

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/remodex.js CHANGED
@@ -1,19 +1,26 @@
1
1
  #!/usr/bin/env node
2
2
  // FILE: remodex.js
3
- // Purpose: CLI surface for foreground bridge runs, pairing reset, thread resume, and macOS service control.
3
+ // Purpose: CLI surface for foreground bridge runs, pairing reset, thread resume, and local service control.
4
4
  // Layer: CLI binary
5
5
  // Exports: none
6
6
  // Depends on: ../src
7
7
 
8
8
  const {
9
+ getLinuxBridgeServiceStatus,
9
10
  getMacOSBridgeServiceStatus,
11
+ printLinuxBridgePairingQr,
10
12
  printMacOSBridgePairingQr,
13
+ printLinuxBridgeServiceStatus,
11
14
  printMacOSBridgeServiceStatus,
12
15
  readBridgeConfig,
16
+ resetLinuxBridgePairing,
13
17
  resetMacOSBridgePairing,
18
+ runLinuxBridgeService,
14
19
  runMacOSBridgeService,
15
20
  startBridge,
21
+ startLinuxBridgeService,
16
22
  startMacOSBridgeService,
23
+ stopLinuxBridgeService,
17
24
  stopMacOSBridgeService,
18
25
  resetBridgePairing,
19
26
  openLastActiveThread,
@@ -22,14 +29,21 @@ const {
22
29
  const { version } = require("../package.json");
23
30
 
24
31
  const defaultDeps = {
32
+ getLinuxBridgeServiceStatus,
25
33
  getMacOSBridgeServiceStatus,
34
+ printLinuxBridgePairingQr,
26
35
  printMacOSBridgePairingQr,
36
+ printLinuxBridgeServiceStatus,
27
37
  printMacOSBridgeServiceStatus,
28
38
  readBridgeConfig,
39
+ resetLinuxBridgePairing,
29
40
  resetMacOSBridgePairing,
41
+ runLinuxBridgeService,
30
42
  runMacOSBridgeService,
31
43
  startBridge,
44
+ startLinuxBridgeService,
32
45
  startMacOSBridgeService,
46
+ stopLinuxBridgeService,
33
47
  stopMacOSBridgeService,
34
48
  resetBridgePairing,
35
49
  openLastActiveThread,
@@ -67,6 +81,16 @@ async function main({
67
81
  return;
68
82
  }
69
83
 
84
+ if (platform === "linux") {
85
+ const result = await deps.startLinuxBridgeService({
86
+ waitForPairing: true,
87
+ });
88
+ deps.printLinuxBridgePairingQr({
89
+ pairingSession: result.pairingSession,
90
+ });
91
+ return;
92
+ }
93
+
70
94
  deps.startBridge();
71
95
  return;
72
96
  }
@@ -77,28 +101,41 @@ async function main({
77
101
  }
78
102
 
79
103
  if (command === "run-service") {
80
- deps.runMacOSBridgeService();
104
+ if (platform === "darwin") {
105
+ deps.runMacOSBridgeService();
106
+ return;
107
+ }
108
+
109
+ if (platform === "linux") {
110
+ deps.runLinuxBridgeService();
111
+ return;
112
+ }
113
+
114
+ deps.startBridge();
81
115
  return;
82
116
  }
83
117
 
84
118
  if (command === "start") {
85
- assertMacOSCommand(command, {
119
+ deps.readBridgeConfig();
120
+ const result = await startManagedBridgeService({
121
+ commandName: command,
86
122
  platform,
123
+ deps,
87
124
  consoleImpl,
88
125
  exitImpl,
89
126
  });
90
- deps.readBridgeConfig();
91
- const result = await deps.startMacOSBridgeService({
92
- waitForPairing: false,
93
- });
94
127
  emitResult({
95
128
  payload: {
96
129
  ok: true,
97
130
  currentVersion: version,
131
+ platform,
98
132
  plistPath: result?.plistPath,
133
+ unitPath: result?.unitPath,
99
134
  pairingSession: result?.pairingSession,
100
135
  },
101
- message: "[remodex] macOS bridge service is running.",
136
+ message: platform === "darwin"
137
+ ? "[remodex] macOS bridge service is running."
138
+ : "[remodex] Linux bridge service is running.",
102
139
  jsonOutput,
103
140
  consoleImpl,
104
141
  });
@@ -106,23 +143,26 @@ async function main({
106
143
  }
107
144
 
108
145
  if (command === "restart") {
109
- assertMacOSCommand(command, {
146
+ deps.readBridgeConfig();
147
+ const result = await startManagedBridgeService({
148
+ commandName: command,
110
149
  platform,
150
+ deps,
111
151
  consoleImpl,
112
152
  exitImpl,
113
153
  });
114
- deps.readBridgeConfig();
115
- const result = await deps.startMacOSBridgeService({
116
- waitForPairing: false,
117
- });
118
154
  emitResult({
119
155
  payload: {
120
156
  ok: true,
121
157
  currentVersion: version,
158
+ platform,
122
159
  plistPath: result?.plistPath,
160
+ unitPath: result?.unitPath,
123
161
  pairingSession: result?.pairingSession,
124
162
  },
125
- message: "[remodex] macOS bridge service restarted.",
163
+ message: platform === "darwin"
164
+ ? "[remodex] macOS bridge service restarted."
165
+ : "[remodex] Linux bridge service restarted.",
126
166
  jsonOutput,
127
167
  consoleImpl,
128
168
  });
@@ -130,18 +170,21 @@ async function main({
130
170
  }
131
171
 
132
172
  if (command === "stop") {
133
- assertMacOSCommand(command, {
173
+ stopManagedBridgeService({
134
174
  platform,
175
+ deps,
135
176
  consoleImpl,
136
177
  exitImpl,
137
178
  });
138
- deps.stopMacOSBridgeService();
139
179
  emitResult({
140
180
  payload: {
141
181
  ok: true,
142
182
  currentVersion: version,
183
+ platform,
143
184
  },
144
- message: "[remodex] macOS bridge service stopped.",
185
+ message: platform === "darwin"
186
+ ? "[remodex] macOS bridge service stopped."
187
+ : "[remodex] Linux bridge service stopped.",
145
188
  jsonOutput,
146
189
  consoleImpl,
147
190
  });
@@ -149,19 +192,25 @@ async function main({
149
192
  }
150
193
 
151
194
  if (command === "status") {
152
- assertMacOSCommand(command, {
195
+ const status = getManagedBridgeServiceStatus({
153
196
  platform,
197
+ deps,
154
198
  consoleImpl,
155
199
  exitImpl,
156
200
  });
157
201
  if (jsonOutput) {
158
202
  emitJson({
159
- ...deps.getMacOSBridgeServiceStatus(),
203
+ ...status,
160
204
  currentVersion: version,
161
205
  });
162
206
  return;
163
207
  }
164
- deps.printMacOSBridgeServiceStatus();
208
+ printManagedBridgeServiceStatus({
209
+ platform,
210
+ deps,
211
+ consoleImpl,
212
+ exitImpl,
213
+ });
165
214
  return;
166
215
  }
167
216
 
@@ -179,6 +228,18 @@ async function main({
179
228
  jsonOutput,
180
229
  consoleImpl,
181
230
  });
231
+ } else if (platform === "linux") {
232
+ deps.resetLinuxBridgePairing();
233
+ emitResult({
234
+ payload: {
235
+ ok: true,
236
+ currentVersion: version,
237
+ platform,
238
+ },
239
+ message: "[remodex] Stopped the Linux bridge service and cleared the saved pairing state. Run `remodex up` to pair again.",
240
+ jsonOutput,
241
+ consoleImpl,
242
+ });
182
243
  } else {
183
244
  deps.resetBridgePairing();
184
245
  emitResult({
@@ -296,14 +357,89 @@ function assertMacOSCommand(name, {
296
357
  consoleImpl = console,
297
358
  exitImpl = process.exit,
298
359
  } = {}) {
299
- if (platform === "darwin") {
360
+ if (platform === "darwin" || platform === "linux") {
300
361
  return;
301
362
  }
302
363
 
303
- consoleImpl.error(`[remodex] \`${name}\` is only available on macOS. Use \`remodex up\` or \`remodex run\` for the foreground bridge on this OS.`);
364
+ consoleImpl.error(`[remodex] \`${name}\` is only available on macOS and Linux. Use \`remodex up\` or \`remodex run\` for the foreground bridge on this OS.`);
304
365
  exitImpl(1);
305
366
  }
306
367
 
368
+ async function startManagedBridgeService({
369
+ commandName = "start",
370
+ platform = process.platform,
371
+ deps = defaultDeps,
372
+ consoleImpl = console,
373
+ exitImpl = process.exit,
374
+ } = {}) {
375
+ assertMacOSCommand(commandName, {
376
+ platform,
377
+ consoleImpl,
378
+ exitImpl,
379
+ });
380
+ if (platform === "darwin") {
381
+ return deps.startMacOSBridgeService({
382
+ waitForPairing: false,
383
+ });
384
+ }
385
+ return deps.startLinuxBridgeService({
386
+ waitForPairing: false,
387
+ });
388
+ }
389
+
390
+ function stopManagedBridgeService({
391
+ platform = process.platform,
392
+ deps = defaultDeps,
393
+ consoleImpl = console,
394
+ exitImpl = process.exit,
395
+ } = {}) {
396
+ assertMacOSCommand("stop", {
397
+ platform,
398
+ consoleImpl,
399
+ exitImpl,
400
+ });
401
+ if (platform === "darwin") {
402
+ deps.stopMacOSBridgeService();
403
+ return;
404
+ }
405
+ deps.stopLinuxBridgeService();
406
+ }
407
+
408
+ function getManagedBridgeServiceStatus({
409
+ platform = process.platform,
410
+ deps = defaultDeps,
411
+ consoleImpl = console,
412
+ exitImpl = process.exit,
413
+ } = {}) {
414
+ assertMacOSCommand("status", {
415
+ platform,
416
+ consoleImpl,
417
+ exitImpl,
418
+ });
419
+ if (platform === "darwin") {
420
+ return deps.getMacOSBridgeServiceStatus();
421
+ }
422
+ return deps.getLinuxBridgeServiceStatus();
423
+ }
424
+
425
+ function printManagedBridgeServiceStatus({
426
+ platform = process.platform,
427
+ deps = defaultDeps,
428
+ consoleImpl = console,
429
+ exitImpl = process.exit,
430
+ } = {}) {
431
+ assertMacOSCommand("status", {
432
+ platform,
433
+ consoleImpl,
434
+ exitImpl,
435
+ });
436
+ if (platform === "darwin") {
437
+ deps.printMacOSBridgeServiceStatus();
438
+ return;
439
+ }
440
+ deps.printLinuxBridgeServiceStatus();
441
+ }
442
+
307
443
  function isVersionCommand(value) {
308
444
  return value === "-v" || value === "--v" || value === "-V" || value === "--version" || value === "version";
309
445
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deepdream314/remodex",
3
- "version": "1.3.10",
3
+ "version": "1.3.11",
4
4
  "description": "Local bridge between Codex and the Remodex mobile app. Run `remodex up` to start.",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -1,5 +1,5 @@
1
1
  // FILE: daemon-state.js
2
- // Purpose: Persists macOS service config/runtime state outside the repo for the launchd bridge flow.
2
+ // Purpose: Persists bridge service config/runtime state outside the repo for local daemon flows.
3
3
  // Layer: CLI helper
4
4
  // Exports: path resolvers plus read/write helpers for daemon config, pairing payloads, and service status.
5
5
  // Depends on: fs, os, path
package/src/index.js CHANGED
@@ -1,14 +1,23 @@
1
1
  // FILE: index.js
2
2
  // Purpose: Small entrypoint wrapper for bridge lifecycle commands.
3
3
  // Layer: CLI entry
4
- // Exports: bridge lifecycle, pairing reset, thread resume/watch, and macOS service helpers.
5
- // Depends on: ./bridge, ./secure-device-state, ./session-state, ./rollout-watch, ./macos-launch-agent
4
+ // Exports: bridge lifecycle, pairing reset, thread resume/watch, and platform service helpers.
5
+ // Depends on: ./bridge, ./secure-device-state, ./session-state, ./rollout-watch, ./linux-systemd, ./macos-launch-agent
6
6
 
7
7
  const { startBridge } = require("./bridge");
8
8
  const { resetBridgeDeviceState } = require("./secure-device-state");
9
9
  const { openLastActiveThread } = require("./session-state");
10
10
  const { watchThreadRollout } = require("./rollout-watch");
11
11
  const { readBridgeConfig } = require("./codex-desktop-refresher");
12
+ const {
13
+ getLinuxBridgeServiceStatus,
14
+ printLinuxBridgePairingQr,
15
+ printLinuxBridgeServiceStatus,
16
+ resetLinuxBridgePairing,
17
+ runLinuxBridgeService,
18
+ startLinuxBridgeService,
19
+ stopLinuxBridgeService,
20
+ } = require("./linux-systemd");
12
21
  const {
13
22
  getMacOSBridgeServiceStatus,
14
23
  printMacOSBridgePairingQr,
@@ -20,14 +29,21 @@ const {
20
29
  } = require("./macos-launch-agent");
21
30
 
22
31
  module.exports = {
32
+ getLinuxBridgeServiceStatus,
23
33
  getMacOSBridgeServiceStatus,
34
+ printLinuxBridgePairingQr,
24
35
  printMacOSBridgePairingQr,
36
+ printLinuxBridgeServiceStatus,
25
37
  printMacOSBridgeServiceStatus,
26
38
  readBridgeConfig,
39
+ resetLinuxBridgePairing,
27
40
  resetMacOSBridgePairing,
41
+ runLinuxBridgeService,
28
42
  startBridge,
29
43
  runMacOSBridgeService,
44
+ startLinuxBridgeService,
30
45
  startMacOSBridgeService,
46
+ stopLinuxBridgeService,
31
47
  stopMacOSBridgeService,
32
48
  resetBridgePairing: resetBridgeDeviceState,
33
49
  openLastActiveThread,
@@ -0,0 +1,435 @@
1
+ // FILE: linux-systemd.js
2
+ // Purpose: Owns Linux-only systemd user-service install/start/stop/status helpers for the background Remodex bridge.
3
+ // Layer: CLI helper
4
+ // Exports: start/stop/status helpers plus the systemd service runner used by `remodex up`.
5
+ // Depends on: child_process, fs, os, path, ./bridge, ./daemon-state, ./codex-desktop-refresher, ./qr, ./secure-device-state
6
+
7
+ const { execFileSync } = require("child_process");
8
+ const fs = require("fs");
9
+ const os = require("os");
10
+ const path = require("path");
11
+ const { startBridge } = require("./bridge");
12
+ const { readBridgeConfig } = require("./codex-desktop-refresher");
13
+ const { printQR } = require("./qr");
14
+ const { resetBridgeDeviceState } = require("./secure-device-state");
15
+ const {
16
+ clearBridgeStatus,
17
+ clearPairingSession,
18
+ ensureRemodexLogsDir,
19
+ ensureRemodexStateDir,
20
+ readBridgeStatus,
21
+ readDaemonConfig,
22
+ readPairingSession,
23
+ resolveBridgeStderrLogPath,
24
+ resolveBridgeStdoutLogPath,
25
+ resolveRemodexStateDir,
26
+ writeBridgeStatus,
27
+ writeDaemonConfig,
28
+ writePairingSession,
29
+ } = require("./daemon-state");
30
+
31
+ const SERVICE_NAME = "com.remodex.bridge.service";
32
+ const DEFAULT_PAIRING_WAIT_TIMEOUT_MS = 10_000;
33
+ const DEFAULT_PAIRING_WAIT_INTERVAL_MS = 200;
34
+
35
+ function runLinuxBridgeService({ env = process.env, platform = process.platform } = {}) {
36
+ assertLinuxPlatform(platform);
37
+ const config = readDaemonConfig({ env });
38
+ if (!config?.relayUrl) {
39
+ const message = "No relay URL configured for the Linux bridge service.";
40
+ clearPairingSession({ env });
41
+ writeBridgeStatus({
42
+ state: "error",
43
+ connectionStatus: "error",
44
+ pid: process.pid,
45
+ lastError: message,
46
+ }, { env });
47
+ console.error(`[remodex] ${message}`);
48
+ return;
49
+ }
50
+
51
+ startBridge({
52
+ config,
53
+ printPairingQr: false,
54
+ onPairingPayload(pairingPayload) {
55
+ writePairingSession(pairingPayload, { env });
56
+ },
57
+ onBridgeStatus(status) {
58
+ writeBridgeStatus(status, { env });
59
+ },
60
+ });
61
+ }
62
+
63
+ async function startLinuxBridgeService({
64
+ env = process.env,
65
+ platform = process.platform,
66
+ fsImpl = fs,
67
+ execFileSyncImpl = execFileSync,
68
+ osImpl = os,
69
+ nodePath = process.execPath,
70
+ cliPath = path.resolve(__dirname, "..", "bin", "remodex.js"),
71
+ waitForPairing = false,
72
+ pairingTimeoutMs = DEFAULT_PAIRING_WAIT_TIMEOUT_MS,
73
+ pairingPollIntervalMs = DEFAULT_PAIRING_WAIT_INTERVAL_MS,
74
+ } = {}) {
75
+ assertLinuxPlatform(platform);
76
+ const config = readBridgeConfig({ env });
77
+ assertRelayConfigured(config);
78
+ const startedAt = Date.now();
79
+
80
+ writeDaemonConfig(config, { env, fsImpl });
81
+ clearPairingSession({ env, fsImpl });
82
+ clearBridgeStatus({ env, fsImpl });
83
+ ensureRemodexStateDir({ env, fsImpl, osImpl });
84
+ ensureRemodexLogsDir({ env, fsImpl, osImpl });
85
+
86
+ const unitPath = writeUserServiceUnit({
87
+ env,
88
+ fsImpl,
89
+ osImpl,
90
+ nodePath,
91
+ cliPath,
92
+ });
93
+
94
+ runSystemctl(["--user", "daemon-reload"], { execFileSyncImpl });
95
+ runSystemctl(["--user", "enable", SERVICE_NAME], { execFileSyncImpl });
96
+ runSystemctl(["--user", "restart", SERVICE_NAME], { execFileSyncImpl });
97
+
98
+ if (!waitForPairing) {
99
+ return {
100
+ unitPath,
101
+ pairingSession: null,
102
+ };
103
+ }
104
+
105
+ const pairingSession = await waitForFreshPairingSession({
106
+ env,
107
+ fsImpl,
108
+ startedAt,
109
+ timeoutMs: pairingTimeoutMs,
110
+ intervalMs: pairingPollIntervalMs,
111
+ });
112
+ return {
113
+ unitPath,
114
+ pairingSession,
115
+ };
116
+ }
117
+
118
+ function stopLinuxBridgeService({
119
+ env = process.env,
120
+ platform = process.platform,
121
+ execFileSyncImpl = execFileSync,
122
+ fsImpl = fs,
123
+ } = {}) {
124
+ assertLinuxPlatform(platform);
125
+ try {
126
+ runSystemctl(["--user", "disable", "--now", SERVICE_NAME], {
127
+ execFileSyncImpl,
128
+ });
129
+ } catch (error) {
130
+ if (!isMissingSystemdUnitError(error)) {
131
+ throw error;
132
+ }
133
+ }
134
+ clearPairingSession({ env, fsImpl });
135
+ clearBridgeStatus({ env, fsImpl });
136
+ }
137
+
138
+ function resetLinuxBridgePairing({
139
+ env = process.env,
140
+ platform = process.platform,
141
+ execFileSyncImpl = execFileSync,
142
+ fsImpl = fs,
143
+ resetBridgePairingImpl = resetBridgeDeviceState,
144
+ } = {}) {
145
+ assertLinuxPlatform(platform);
146
+ stopLinuxBridgeService({
147
+ env,
148
+ platform,
149
+ execFileSyncImpl,
150
+ fsImpl,
151
+ });
152
+ return resetBridgePairingImpl();
153
+ }
154
+
155
+ function getLinuxBridgeServiceStatus({
156
+ env = process.env,
157
+ platform = process.platform,
158
+ execFileSyncImpl = execFileSync,
159
+ fsImpl = fs,
160
+ osImpl = os,
161
+ } = {}) {
162
+ assertLinuxPlatform(platform);
163
+ const unitPath = resolveUserServiceUnitPath({ env, osImpl });
164
+ const serviceState = readSystemdServiceState({
165
+ execFileSyncImpl,
166
+ missingAsStopped: true,
167
+ });
168
+ return {
169
+ label: SERVICE_NAME,
170
+ manager: "systemd-user",
171
+ platform: "linux",
172
+ installed: fsImpl.existsSync(unitPath),
173
+ unitPath,
174
+ unitLoaded: serviceState.loadState === "loaded",
175
+ activeState: serviceState.activeState,
176
+ subState: serviceState.subState,
177
+ servicePid: serviceState.execMainPid,
178
+ unitFileState: serviceState.unitFileState,
179
+ daemonConfig: readDaemonConfig({ env, fsImpl }),
180
+ bridgeStatus: readBridgeStatus({ env, fsImpl }),
181
+ pairingSession: readPairingSession({ env, fsImpl }),
182
+ stdoutLogPath: resolveBridgeStdoutLogPath({ env }),
183
+ stderrLogPath: resolveBridgeStderrLogPath({ env }),
184
+ };
185
+ }
186
+
187
+ function printLinuxBridgeServiceStatus(options = {}) {
188
+ const status = getLinuxBridgeServiceStatus(options);
189
+ const bridgeState = status.bridgeStatus?.state || "unknown";
190
+ const connectionStatus = status.bridgeStatus?.connectionStatus || "unknown";
191
+ const pairingCreatedAt = status.pairingSession?.createdAt || "none";
192
+ console.log(`[remodex] Service label: ${status.label}`);
193
+ console.log("[remodex] Service manager: systemd --user");
194
+ console.log(`[remodex] Installed: ${status.installed ? "yes" : "no"}`);
195
+ console.log(`[remodex] Loaded: ${status.unitLoaded ? "yes" : "no"}`);
196
+ console.log(`[remodex] Active state: ${status.activeState}`);
197
+ console.log(`[remodex] Sub-state: ${status.subState}`);
198
+ console.log(`[remodex] PID: ${status.servicePid || status.bridgeStatus?.pid || "unknown"}`);
199
+ console.log(`[remodex] Bridge state: ${bridgeState}`);
200
+ console.log(`[remodex] Connection: ${connectionStatus}`);
201
+ console.log(`[remodex] Pairing payload: ${pairingCreatedAt}`);
202
+ console.log(`[remodex] Unit file: ${status.unitPath}`);
203
+ console.log(`[remodex] Stdout log: ${status.stdoutLogPath}`);
204
+ console.log(`[remodex] Stderr log: ${status.stderrLogPath}`);
205
+ }
206
+
207
+ function printLinuxBridgePairingQr({ pairingSession = null, env = process.env, fsImpl = fs } = {}) {
208
+ const nextPairingSession = pairingSession || readPairingSession({ env, fsImpl });
209
+ const pairingPayload = nextPairingSession?.pairingPayload;
210
+ if (!pairingPayload) {
211
+ throw new Error("The Linux bridge service did not publish a pairing payload yet.");
212
+ }
213
+
214
+ printQR(pairingPayload);
215
+ }
216
+
217
+ function writeUserServiceUnit({
218
+ env = process.env,
219
+ fsImpl = fs,
220
+ osImpl = os,
221
+ nodePath = process.execPath,
222
+ cliPath = path.resolve(__dirname, "..", "bin", "remodex.js"),
223
+ } = {}) {
224
+ const unitPath = resolveUserServiceUnitPath({ env, osImpl });
225
+ const stateDir = resolveRemodexStateDir({ env, osImpl });
226
+ const stdoutLogPath = resolveBridgeStdoutLogPath({ env, osImpl });
227
+ const stderrLogPath = resolveBridgeStderrLogPath({ env, osImpl });
228
+ const homeDir = env.HOME || osImpl.homedir();
229
+ const workingDirectory = path.resolve(__dirname, "..");
230
+ const serialized = buildUserServiceUnit({
231
+ homeDir,
232
+ pathEnv: env.PATH || "",
233
+ stateDir,
234
+ stdoutLogPath,
235
+ stderrLogPath,
236
+ workingDirectory,
237
+ nodePath,
238
+ cliPath,
239
+ });
240
+
241
+ fsImpl.mkdirSync(path.dirname(unitPath), { recursive: true });
242
+ fsImpl.writeFileSync(unitPath, serialized, "utf8");
243
+ return unitPath;
244
+ }
245
+
246
+ function buildUserServiceUnit({
247
+ homeDir,
248
+ pathEnv,
249
+ stateDir,
250
+ stdoutLogPath,
251
+ stderrLogPath,
252
+ workingDirectory,
253
+ nodePath,
254
+ cliPath,
255
+ }) {
256
+ return `[Unit]
257
+ Description=Remodex bridge
258
+ After=default.target
259
+
260
+ [Service]
261
+ Type=simple
262
+ WorkingDirectory=${quoteSystemdPath(workingDirectory)}
263
+ ExecStart=${quoteSystemdExec(nodePath, cliPath, "run-service")}
264
+ Restart=on-failure
265
+ RestartSec=2
266
+ Environment=${quoteSystemdEnv("HOME", homeDir)}
267
+ Environment=${quoteSystemdEnv("PATH", pathEnv)}
268
+ Environment=${quoteSystemdEnv("REMODEX_DEVICE_STATE_DIR", stateDir)}
269
+ StandardOutput=append:${quoteSystemdPath(stdoutLogPath)}
270
+ StandardError=append:${quoteSystemdPath(stderrLogPath)}
271
+
272
+ [Install]
273
+ WantedBy=default.target
274
+ `;
275
+ }
276
+
277
+ function resolveUserServiceUnitPath({ env = process.env, osImpl = os } = {}) {
278
+ const configHome = normalizeNonEmptyString(env.XDG_CONFIG_HOME)
279
+ || path.join(env.HOME || osImpl.homedir(), ".config");
280
+ return path.join(configHome, "systemd", "user", SERVICE_NAME);
281
+ }
282
+
283
+ function readSystemdServiceState({
284
+ execFileSyncImpl = execFileSync,
285
+ missingAsStopped = false,
286
+ } = {}) {
287
+ try {
288
+ const output = runSystemctl([
289
+ "--user",
290
+ "show",
291
+ "--property=LoadState,ActiveState,SubState,ExecMainPID,UnitFileState",
292
+ SERVICE_NAME,
293
+ ], {
294
+ execFileSyncImpl,
295
+ encoding: "utf8",
296
+ });
297
+ return parseSystemdShowOutput(output);
298
+ } catch (error) {
299
+ if (missingAsStopped && isMissingSystemdUnitError(error)) {
300
+ return {
301
+ loadState: "not-found",
302
+ activeState: "inactive",
303
+ subState: "dead",
304
+ execMainPid: null,
305
+ unitFileState: "disabled",
306
+ };
307
+ }
308
+ throw error;
309
+ }
310
+ }
311
+
312
+ function runSystemctl(args, {
313
+ execFileSyncImpl = execFileSync,
314
+ encoding = "utf8",
315
+ } = {}) {
316
+ try {
317
+ return execFileSyncImpl("systemctl", args, {
318
+ encoding,
319
+ stdio: ["ignore", "pipe", "pipe"],
320
+ });
321
+ } catch (error) {
322
+ if (error?.code === "ENOENT") {
323
+ throw new Error("`systemctl --user` is required for Linux bridge service management.");
324
+ }
325
+ throw error;
326
+ }
327
+ }
328
+
329
+ function parseSystemdShowOutput(output) {
330
+ const values = Object.create(null);
331
+ for (const line of String(output || "").split(/\r?\n/)) {
332
+ if (!line || !line.includes("=")) {
333
+ continue;
334
+ }
335
+ const separatorIndex = line.indexOf("=");
336
+ const key = line.slice(0, separatorIndex);
337
+ const value = line.slice(separatorIndex + 1);
338
+ values[key] = value;
339
+ }
340
+
341
+ const pidValue = Number.parseInt(values.ExecMainPID || "", 10);
342
+ return {
343
+ loadState: values.LoadState || "unknown",
344
+ activeState: values.ActiveState || "unknown",
345
+ subState: values.SubState || "unknown",
346
+ execMainPid: Number.isFinite(pidValue) && pidValue > 0 ? pidValue : null,
347
+ unitFileState: values.UnitFileState || "unknown",
348
+ };
349
+ }
350
+
351
+ async function waitForFreshPairingSession({
352
+ env = process.env,
353
+ fsImpl = fs,
354
+ startedAt,
355
+ timeoutMs,
356
+ intervalMs,
357
+ } = {}) {
358
+ const deadline = Date.now() + timeoutMs;
359
+ while (Date.now() < deadline) {
360
+ const pairingSession = readPairingSession({ env, fsImpl });
361
+ const createdAt = Date.parse(pairingSession?.createdAt || "");
362
+ if (pairingSession?.pairingPayload && Number.isFinite(createdAt) && createdAt >= startedAt) {
363
+ return pairingSession;
364
+ }
365
+ await sleep(intervalMs);
366
+ }
367
+
368
+ throw new Error(
369
+ `Timed out waiting for the Linux bridge service to publish a pairing QR. `
370
+ + `Check ${resolveBridgeStdoutLogPath({ env })} and ${resolveBridgeStderrLogPath({ env })}.`
371
+ );
372
+ }
373
+
374
+ function isMissingSystemdUnitError(error) {
375
+ const combined = [
376
+ error?.message,
377
+ error?.stderr?.toString?.("utf8"),
378
+ error?.stdout?.toString?.("utf8"),
379
+ ].filter(Boolean).join("\n").toLowerCase();
380
+ return combined.includes("unit com.remodex.bridge.service could not be found")
381
+ || combined.includes("unit com.remodex.bridge.service not found")
382
+ || combined.includes("not loaded");
383
+ }
384
+
385
+ function assertLinuxPlatform(platform = process.platform) {
386
+ if (platform !== "linux") {
387
+ throw new Error("Linux bridge service management is only available on Linux.");
388
+ }
389
+ }
390
+
391
+ function assertRelayConfigured(config) {
392
+ if (typeof config?.relayUrl === "string" && config.relayUrl.trim()) {
393
+ return;
394
+ }
395
+ throw new Error("No relay URL configured. Run ./run-local-remodex.sh or set REMODEX_RELAY before enabling the Linux bridge service.");
396
+ }
397
+
398
+ function quoteSystemdValue(value) {
399
+ return `"${String(value).replaceAll("\\", "\\\\").replaceAll("\"", "\\\"")}"`;
400
+ }
401
+
402
+ function quoteSystemdPath(value) {
403
+ return String(value)
404
+ .replaceAll("\\", "\\\\")
405
+ .replaceAll(" ", "\\x20")
406
+ .replaceAll(":", "\\:");
407
+ }
408
+
409
+ function quoteSystemdEnv(name, value) {
410
+ return `"${String(name)}=${String(value).replaceAll("\\", "\\\\").replaceAll("\"", "\\\"")}"`;
411
+ }
412
+
413
+ function quoteSystemdExec(...parts) {
414
+ return parts.map(quoteSystemdValue).join(" ");
415
+ }
416
+
417
+ function normalizeNonEmptyString(value) {
418
+ return typeof value === "string" && value.trim() ? value.trim() : "";
419
+ }
420
+
421
+ function sleep(ms) {
422
+ return new Promise((resolve) => setTimeout(resolve, ms));
423
+ }
424
+
425
+ module.exports = {
426
+ buildUserServiceUnit,
427
+ getLinuxBridgeServiceStatus,
428
+ printLinuxBridgePairingQr,
429
+ printLinuxBridgeServiceStatus,
430
+ resetLinuxBridgePairing,
431
+ resolveUserServiceUnitPath,
432
+ runLinuxBridgeService,
433
+ startLinuxBridgeService,
434
+ stopLinuxBridgeService,
435
+ };
@@ -33,8 +33,8 @@ const DEFAULT_PAIRING_WAIT_TIMEOUT_MS = 10_000;
33
33
  const DEFAULT_PAIRING_WAIT_INTERVAL_MS = 200;
34
34
 
35
35
  // Runs the bridge inside launchd while keeping QR rendering in the foreground CLI command.
36
- function runMacOSBridgeService({ env = process.env } = {}) {
37
- assertDarwinPlatform();
36
+ function runMacOSBridgeService({ env = process.env, platform = process.platform } = {}) {
37
+ assertDarwinPlatform(platform);
38
38
  const config = readDaemonConfig({ env });
39
39
  if (!config?.relayUrl) {
40
40
  const message = "No relay URL configured for the macOS bridge service.";