metaharness 0.4.4 → 0.4.6

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.
@@ -21,12 +21,17 @@
21
21
  // without touching launchctl/systemctl/schtasks. That mirrors `installFromSource`
22
22
  // in meta-proxy.ts, which already takes an injected `run`.
23
23
  import { spawnSync } from 'node:child_process';
24
- import { existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
24
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
25
25
  import { homedir } from 'node:os';
26
26
  import { dirname, join } from 'node:path';
27
27
  import { metaProxyBinaryPath, metaProxyDataDir } from './meta-proxy.js';
28
28
  /** Reverse-DNS label, shared by the launchd job and the scheduled task. */
29
29
  export const SERVICE_LABEL = 'one.cognitum.meta-proxy';
30
+ function assertServiceLabel(label) {
31
+ if (!/^[A-Za-z0-9][A-Za-z0-9.-]{0,127}$/.test(label)) {
32
+ throw new Error('Invalid service label.');
33
+ }
34
+ }
30
35
  function defaultRunner(invocation) {
31
36
  const result = spawnSync(invocation.command, invocation.args, {
32
37
  encoding: 'utf8',
@@ -35,27 +40,30 @@ function defaultRunner(invocation) {
35
40
  });
36
41
  const output = `${result.stdout ?? ''}${result.stderr ?? ''}`.trim();
37
42
  if (result.error)
38
- return { ok: false, output: result.error.message };
43
+ return { ok: false, output: result.error.message, error: result.error.message };
39
44
  return { ok: result.status === 0, output };
40
45
  }
41
46
  // --- unit file locations ----------------------------------------------------
42
- export function launchAgentPath(home = homedir()) {
43
- return join(home, 'Library', 'LaunchAgents', `${SERVICE_LABEL}.plist`);
47
+ export function launchAgentPath(home = homedir(), label = SERVICE_LABEL) {
48
+ assertServiceLabel(label);
49
+ return join(home, 'Library', 'LaunchAgents', `${label}.plist`);
44
50
  }
45
51
  export function systemdUnitPath(home = homedir()) {
46
52
  return join(home, '.config', 'systemd', 'user', 'meta-proxy.service');
47
53
  }
48
54
  /** The XML definition handed to `schtasks /create /xml`. */
49
- export function scheduledTaskPath(home = homedir()) {
50
- return join(metaProxyDataDir(home), 'meta-proxy-task.xml');
55
+ export function scheduledTaskPath(home = homedir(), label = SERVICE_LABEL) {
56
+ assertServiceLabel(label);
57
+ const filename = label === SERVICE_LABEL ? 'meta-proxy-task.xml' : `${label}-task.xml`;
58
+ return join(metaProxyDataDir(home), filename);
51
59
  }
52
- export function serviceUnitPath(platform, home = homedir()) {
60
+ export function serviceUnitPath(platform, home = homedir(), label = SERVICE_LABEL) {
53
61
  if (platform === 'darwin')
54
- return launchAgentPath(home);
62
+ return launchAgentPath(home, label);
55
63
  if (platform === 'linux')
56
64
  return systemdUnitPath(home);
57
65
  if (platform === 'win32')
58
- return scheduledTaskPath(home);
66
+ return scheduledTaskPath(home, label);
59
67
  return null;
60
68
  }
61
69
  // --- pure renderers ---------------------------------------------------------
@@ -74,16 +82,17 @@ function escapeXml(value) {
74
82
  * `proxy stop` (SIGTERM, exit 0) stay stopped — a plain `KeepAlive: true` would
75
83
  * fight the user's own stop command forever.
76
84
  */
77
- export function renderLaunchAgent(binaryPath, logPath) {
85
+ export function renderLaunchAgent(binaryPath, logPath, label = SERVICE_LABEL) {
78
86
  return `<?xml version="1.0" encoding="UTF-8"?>
79
87
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
80
88
  <plist version="1.0">
81
89
  <dict>
82
90
  <key>Label</key>
83
- <string>${SERVICE_LABEL}</string>
91
+ <string>${escapeXml(label)}</string>
84
92
  <key>ProgramArguments</key>
85
93
  <array>
86
94
  <string>${escapeXml(binaryPath)}</string>
95
+ <string>--supervised</string>
87
96
  </array>
88
97
  <key>RunAtLoad</key>
89
98
  <true/>
@@ -118,7 +127,7 @@ After=network-online.target
118
127
 
119
128
  [Service]
120
129
  Type=simple
121
- ExecStart=${binaryPath}
130
+ ExecStart=${binaryPath} --supervised
122
131
  Restart=on-failure
123
132
  RestartSec=10
124
133
 
@@ -128,11 +137,19 @@ WantedBy=default.target
128
137
  }
129
138
  /** Task Scheduler definition — Windows has no launchd/systemd equivalent. */
130
139
  export function renderScheduledTask(binaryPath) {
131
- return `<?xml version="1.0" encoding="UTF-16"?>
140
+ // Task Scheduler's XML importer requires its canonical BOM-marked UTF-16
141
+ // representation; enableMetaProxyService persists this string as utf16le.
142
+ return `\uFEFF<?xml version="1.0" encoding="UTF-16"?>
132
143
  <Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
133
144
  <RegistrationInfo>
134
145
  <Description>Cognitum Meta-Proxy sidecar</Description>
135
146
  </RegistrationInfo>
147
+ <Principals>
148
+ <Principal id="Author">
149
+ <LogonType>InteractiveToken</LogonType>
150
+ <RunLevel>LeastPrivilege</RunLevel>
151
+ </Principal>
152
+ </Principals>
136
153
  <Triggers>
137
154
  <LogonTrigger>
138
155
  <Enabled>true</Enabled>
@@ -152,19 +169,20 @@ export function renderScheduledTask(binaryPath) {
152
169
  <Actions Context="Author">
153
170
  <Exec>
154
171
  <Command>${escapeXml(binaryPath)}</Command>
172
+ <Arguments>--supervised</Arguments>
155
173
  </Exec>
156
174
  </Actions>
157
175
  </Task>
158
176
  `;
159
177
  }
160
178
  // --- the OS commands, as data so they can be asserted -----------------------
161
- export function enableCommands(platform, unitPath) {
179
+ export function enableCommands(platform, unitPath, label = SERVICE_LABEL) {
162
180
  if (platform === 'darwin') {
163
181
  // `bootstrap` is the modern replacement for `load -w`; `kickstart` starts it
164
182
  // now so the user does not have to log out and back in to get a daemon.
165
183
  return [
166
184
  { command: 'launchctl', args: ['bootstrap', `gui/${process.getuid?.() ?? 0}`, unitPath] },
167
- { command: 'launchctl', args: ['kickstart', `gui/${process.getuid?.() ?? 0}/${SERVICE_LABEL}`] },
185
+ { command: 'launchctl', args: ['kickstart', `gui/${process.getuid?.() ?? 0}/${label}`] },
168
186
  ];
169
187
  }
170
188
  if (platform === 'linux') {
@@ -175,15 +193,15 @@ export function enableCommands(platform, unitPath) {
175
193
  }
176
194
  if (platform === 'win32') {
177
195
  return [
178
- { command: 'schtasks', args: ['/create', '/tn', SERVICE_LABEL, '/xml', unitPath, '/f'] },
179
- { command: 'schtasks', args: ['/run', '/tn', SERVICE_LABEL] },
196
+ { command: 'schtasks', args: ['/create', '/tn', label, '/xml', unitPath, '/f'] },
197
+ { command: 'schtasks', args: ['/run', '/tn', label] },
180
198
  ];
181
199
  }
182
200
  return [];
183
201
  }
184
- export function disableCommands(platform, unitPath) {
202
+ export function disableCommands(platform, unitPath, label = SERVICE_LABEL) {
185
203
  if (platform === 'darwin') {
186
- return [{ command: 'launchctl', args: ['bootout', `gui/${process.getuid?.() ?? 0}`, unitPath] }];
204
+ return [{ command: 'launchctl', args: ['bootout', `gui/${process.getuid?.() ?? 0}/${label}`] }];
187
205
  }
188
206
  if (platform === 'linux') {
189
207
  return [
@@ -192,22 +210,117 @@ export function disableCommands(platform, unitPath) {
192
210
  ];
193
211
  }
194
212
  if (platform === 'win32') {
195
- return [{ command: 'schtasks', args: ['/delete', '/tn', SERVICE_LABEL, '/f'] }];
213
+ return [{ command: 'schtasks', args: ['/delete', '/tn', label, '/f'] }];
196
214
  }
197
215
  return [];
198
216
  }
217
+ export function serviceStateCommand(platform, label = SERVICE_LABEL) {
218
+ if (platform === 'darwin') {
219
+ return { command: 'launchctl', args: ['print', `gui/${process.getuid?.() ?? 0}/${label}`] };
220
+ }
221
+ if (platform === 'linux') {
222
+ return {
223
+ command: 'systemctl',
224
+ args: ['--user', 'show', 'meta-proxy.service', '--property=LoadState,UnitFileState,ActiveState,MainPID'],
225
+ };
226
+ }
227
+ if (platform === 'win32') {
228
+ // schtasks renders both headings and states in the user's UI language.
229
+ // Query the ScheduledTasks API and serialize numeric enum/bool values so
230
+ // state and login enablement are authoritative and locale-neutral.
231
+ const script = `$task = Get-ScheduledTask -ErrorAction Stop | Where-Object { $_.TaskName -ceq '${label}' -and $_.TaskPath -eq '\\' } | Select-Object -First 1; if ($null -eq $task) { Write-Output 'TaskState=Missing'; exit 0 }; Write-Output ('TaskState=' + [int]$task.State)`;
232
+ return { command: 'powershell.exe', args: ['-NoProfile', '-NonInteractive', '-Command', script] };
233
+ }
234
+ return null;
235
+ }
236
+ function knownMissingService(platform, output) {
237
+ const text = output.toLowerCase();
238
+ if (platform === 'darwin') {
239
+ return text.includes('could not find service') || text.includes('service not found');
240
+ }
241
+ if (platform === 'linux') {
242
+ return text.includes('loadstate=not-found') || text.includes('unit meta-proxy.service could not be found');
243
+ }
244
+ if (platform === 'win32') {
245
+ return /^taskstate=missing$/m.test(text);
246
+ }
247
+ return false;
248
+ }
249
+ function parseManagerState(platform, outcome, definitionPresent) {
250
+ if (outcome.error)
251
+ return { managerState: 'unknown', loaded: null, enabledAtLogin: false, running: null, pid: null };
252
+ if (!outcome.ok) {
253
+ return knownMissingService(platform, outcome.output)
254
+ ? { managerState: 'disabled', loaded: false, enabledAtLogin: platform === 'darwin' && definitionPresent, running: false, pid: null }
255
+ : { managerState: 'unknown', loaded: null, enabledAtLogin: false, running: null, pid: null };
256
+ }
257
+ if (platform === 'darwin') {
258
+ const pid = outcome.output.match(/\bpid\s*=\s*(\d+)/)?.[1];
259
+ return {
260
+ managerState: 'enabled',
261
+ loaded: true,
262
+ enabledAtLogin: definitionPresent,
263
+ running: /\bstate\s*=\s*running\b/.test(outcome.output) || pid !== undefined,
264
+ pid: pid ? Number.parseInt(pid, 10) : null,
265
+ };
266
+ }
267
+ if (platform === 'linux') {
268
+ if (/^LoadState=not-found$/m.test(outcome.output)) {
269
+ return { managerState: 'disabled', loaded: false, enabledAtLogin: false, running: false, pid: null };
270
+ }
271
+ const enabled = /^UnitFileState=(?:enabled|enabled-runtime|linked|linked-runtime)$/m.test(outcome.output);
272
+ const loaded = /^LoadState=loaded$/m.test(outcome.output);
273
+ const pidText = outcome.output.match(/^MainPID=(\d+)$/m)?.[1];
274
+ const pid = pidText && pidText !== '0' ? Number.parseInt(pidText, 10) : null;
275
+ return {
276
+ managerState: loaded ? 'enabled' : 'disabled',
277
+ loaded,
278
+ enabledAtLogin: enabled,
279
+ running: /^ActiveState=active$/m.test(outcome.output),
280
+ pid,
281
+ };
282
+ }
283
+ if (platform === 'win32') {
284
+ if (/^TaskState=Missing$/m.test(outcome.output)) {
285
+ return { managerState: 'disabled', loaded: false, enabledAtLogin: false, running: false, pid: null };
286
+ }
287
+ const state = outcome.output.match(/^TaskState=(\d+)$/m)?.[1];
288
+ if (state === undefined || !['0', '1', '2', '3', '4'].includes(state)) {
289
+ return { managerState: 'unknown', loaded: null, enabledAtLogin: false, running: null, pid: null };
290
+ }
291
+ return {
292
+ managerState: 'enabled',
293
+ loaded: true,
294
+ // TASK_STATE_DISABLED is the authoritative registered-but-disabled state;
295
+ // all other known registered states remain enabled for their triggers.
296
+ enabledAtLogin: state !== '1',
297
+ running: state === '4' ? true : state === '1' || state === '3' ? false : null,
298
+ pid: null,
299
+ };
300
+ }
301
+ return { managerState: 'unknown', loaded: null, enabledAtLogin: false, running: null, pid: null };
302
+ }
199
303
  // --- public API -------------------------------------------------------------
200
- export function metaProxyServiceState(platform = process.platform, home = homedir()) {
201
- const unitPath = serviceUnitPath(platform, home);
304
+ export function metaProxyServiceState(platform = process.platform, home = homedir(), run = defaultRunner, label = SERVICE_LABEL) {
305
+ const unitPath = serviceUnitPath(platform, home, label);
202
306
  if (!unitPath)
203
- return { supported: false, installed: false, unitPath: null };
204
- return { supported: true, installed: existsSync(unitPath), unitPath };
307
+ return { supported: false, installed: false, unitPath: null, managerState: 'unknown', definitionPresent: false, loaded: null, enabledAtLogin: false, running: null, pid: null };
308
+ const definitionPresent = existsSync(unitPath);
309
+ const command = serviceStateCommand(platform, label);
310
+ const outcome = command ? run(command) : { ok: false, output: '', error: 'unsupported' };
311
+ const manager = parseManagerState(platform, outcome, definitionPresent);
312
+ return { supported: true, installed: manager.loaded === true, unitPath, definitionPresent, ...manager };
313
+ }
314
+ function defaultWait(milliseconds) {
315
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, milliseconds);
205
316
  }
206
317
  export function enableMetaProxyService(options = {}) {
207
318
  const home = options.home ?? homedir();
208
319
  const platform = options.platform ?? process.platform;
209
320
  const run = options.run ?? defaultRunner;
210
- const unitPath = serviceUnitPath(platform, home);
321
+ const label = options.label ?? SERVICE_LABEL;
322
+ const wait = options.wait ?? defaultWait;
323
+ const unitPath = serviceUnitPath(platform, home, label);
211
324
  if (!unitPath) {
212
325
  return { ok: false, message: `Start-at-login is not supported on ${platform}.` };
213
326
  }
@@ -220,17 +333,193 @@ export function enableMetaProxyService(options = {}) {
220
333
  }
221
334
  const logPath = join(metaProxyDataDir(home), 'meta-proxy.log');
222
335
  const definition = platform === 'darwin'
223
- ? renderLaunchAgent(binaryPath, logPath)
336
+ ? renderLaunchAgent(binaryPath, logPath, label)
224
337
  : platform === 'linux'
225
338
  ? renderSystemdUnit(binaryPath)
226
339
  : renderScheduledTask(binaryPath);
340
+ const before = metaProxyServiceState(platform, home, run, label);
341
+ if (before.managerState === 'unknown') {
342
+ return { ok: false, message: `Could not determine service-manager state; preserved ${unitPath}.`, unitPath };
343
+ }
344
+ if (before.loaded === true && !before.definitionPresent) {
345
+ // Something answers to our label but no owned definition exists — another
346
+ // installer's job, or a unit shadowing ours from a different path. On
347
+ // darwin/linux a failed enable from this state would be "compensated" by
348
+ // booting out that foreign service, so refuse up front on every platform.
349
+ return {
350
+ ok: false,
351
+ message: `A Meta-Proxy ${platform === 'win32' ? 'task' : 'service'} is registered without the owned definition at ${unitPath}. Inspect and remove it manually before replacement.`,
352
+ unitPath,
353
+ };
354
+ }
355
+ const persistedEncoding = platform === 'win32' ? 'utf16le' : 'utf8';
356
+ if (before.definitionPresent && readFileSync(unitPath, persistedEncoding) !== definition) {
357
+ return {
358
+ ok: false,
359
+ message: `A different Meta-Proxy service definition already exists at ${unitPath}. Disable it before replacement.`,
360
+ unitPath,
361
+ };
362
+ }
363
+ if (before.managerState === 'enabled' && before.enabledAtLogin) {
364
+ if (before.definitionPresent) {
365
+ return { ok: true, message: `Meta-Proxy is already set to start at login. Definition: ${unitPath}`, unitPath };
366
+ }
367
+ return {
368
+ ok: false,
369
+ message: `A Meta-Proxy service is already registered with a different or missing definition. Disable it before replacing ${unitPath}.`,
370
+ unitPath,
371
+ };
372
+ }
227
373
  mkdirSync(dirname(unitPath), { recursive: true, mode: 0o700 });
228
- writeFileSync(unitPath, definition, { encoding: 'utf8', mode: 0o600 });
229
- for (const invocation of enableCommands(platform, unitPath)) {
374
+ writeFileSync(unitPath, definition, { encoding: persistedEncoding, mode: 0o600 });
375
+ for (const invocation of enableCommands(platform, unitPath, label)) {
230
376
  const outcome = run(invocation);
231
377
  if (!outcome.ok) {
232
- // Leave nothing half-installed: a unit file present but never loaded
233
- // would make `proxy status` claim start-at-login is on when it is not.
378
+ const state = metaProxyServiceState(platform, home, run, label);
379
+ if (before.definitionPresent) {
380
+ // This enable started from an existing owned definition. Never erase it
381
+ // or stop a pre-existing active service while compensating a failed
382
+ // attempt to change only its login-start state.
383
+ if (platform === 'linux') {
384
+ const restore = [];
385
+ if (!before.enabledAtLogin && state.enabledAtLogin) {
386
+ // Deliberately omit --now: a service that was already active must
387
+ // remain active while only its prior login-enable bit is restored.
388
+ restore.push({ command: 'systemctl', args: ['--user', 'disable', 'meta-proxy.service'] });
389
+ }
390
+ if (before.running === false && state.running === true) {
391
+ restore.push({ command: 'systemctl', args: ['--user', 'stop', 'meta-proxy.service'] });
392
+ }
393
+ const restoreFailure = restore.map((command) => run(command)).find((result) => !result.ok);
394
+ if (restoreFailure) {
395
+ return {
396
+ ok: false,
397
+ message: `Could not enable start-at-login and restoring the prior service state failed: ${restoreFailure.output || 'command failed'}. Definition preserved at ${unitPath}.`,
398
+ unitPath,
399
+ };
400
+ }
401
+ const restored = metaProxyServiceState(platform, home, run, label);
402
+ if (restored.managerState === 'unknown'
403
+ || restored.enabledAtLogin !== before.enabledAtLogin
404
+ || restored.running !== before.running) {
405
+ return {
406
+ ok: false,
407
+ message: `Could not prove the prior service state was restored. Definition preserved at ${unitPath}.`,
408
+ unitPath,
409
+ };
410
+ }
411
+ }
412
+ if (platform === 'win32' && before.loaded === false && state.loaded === true) {
413
+ const cleanup = stopAndDeleteWindowsTask(home, unitPath, label, run, wait, false);
414
+ if (!cleanup.ok) {
415
+ return {
416
+ ok: false,
417
+ message: `Could not enable start-at-login and restoring the prior absent task failed: ${cleanup.message}`,
418
+ unitPath,
419
+ };
420
+ }
421
+ return {
422
+ ok: false,
423
+ message: `Could not enable start-at-login (${invocation.command} ${invocation.args.join(' ')}): ${outcome.output || 'command failed'}. Prior definition preserved at ${unitPath}.`,
424
+ unitPath,
425
+ };
426
+ }
427
+ if (platform === 'win32' && before.loaded === true && !before.enabledAtLogin) {
428
+ if (state.managerState === 'unknown') {
429
+ return {
430
+ ok: false,
431
+ message: `Could not enable start-at-login and manager state is unknown. Prior definition preserved at ${unitPath}.`,
432
+ unitPath,
433
+ };
434
+ }
435
+ if (state.loaded === false) {
436
+ const recreate = enableCommands('win32', unitPath, label)[0];
437
+ const recreated = recreate ? run(recreate) : { ok: false, output: 'create command unavailable' };
438
+ // `/create /f` can apply and still lose its response. Its exit
439
+ // status is not authoritative; re-read before deciding whether
440
+ // the prior task is still absent or available for restoration.
441
+ const afterRecreate = metaProxyServiceState('win32', home, run, label);
442
+ if (afterRecreate.managerState === 'unknown' || afterRecreate.loaded !== true) {
443
+ return {
444
+ ok: false,
445
+ message: `Could not enable start-at-login and could not prove the prior disabled task was recreated: ${recreated.output || 'state mismatch'}. Definition preserved at ${unitPath}.`,
446
+ unitPath,
447
+ };
448
+ }
449
+ }
450
+ const stopped = confirmWindowsTaskStopped(home, unitPath, label, run, wait);
451
+ if (!stopped.ok) {
452
+ return {
453
+ ok: false,
454
+ message: `Could not enable start-at-login and restoring the prior disabled task failed: ${stopped.message}`,
455
+ unitPath,
456
+ };
457
+ }
458
+ const disabled = run({ command: 'schtasks', args: ['/change', '/tn', label, '/disable'] });
459
+ const restored = disabled.ok ? metaProxyServiceState('win32', home, run, label) : null;
460
+ if (!disabled.ok || restored?.loaded !== true || restored.enabledAtLogin || restored.running !== false) {
461
+ return {
462
+ ok: false,
463
+ message: `Could not enable start-at-login and restoring the prior disabled task failed: ${disabled.output || 'state mismatch'}. Definition preserved at ${unitPath}.`,
464
+ unitPath,
465
+ };
466
+ }
467
+ return {
468
+ ok: false,
469
+ message: `Could not enable start-at-login (${invocation.command} ${invocation.args.join(' ')}): ${outcome.output || 'command failed'}. Prior disabled task and definition restored at ${unitPath}.`,
470
+ unitPath,
471
+ };
472
+ }
473
+ if (before.loaded === false && state.managerState === 'enabled') {
474
+ const cleanup = disableCommands(platform, unitPath, label)[0];
475
+ const cleanupOutcome = cleanup ? run(cleanup) : { ok: false, output: 'cleanup command unavailable' };
476
+ if (!cleanupOutcome.ok) {
477
+ return {
478
+ ok: false,
479
+ message: `Could not enable start-at-login and compensating cleanup failed: ${cleanupOutcome.output || 'command failed'}. Definition preserved at ${unitPath}.`,
480
+ unitPath,
481
+ };
482
+ }
483
+ }
484
+ return {
485
+ ok: false,
486
+ message: `Could not enable start-at-login (${invocation.command} ${invocation.args.join(' ')}): ${outcome.output || 'command failed'}. Definition preserved at ${unitPath}.`,
487
+ unitPath,
488
+ };
489
+ }
490
+ if (state.managerState === 'enabled') {
491
+ if (platform === 'win32') {
492
+ const cleanup = stopAndDeleteWindowsTask(home, unitPath, label, run, wait);
493
+ if (!cleanup.ok) {
494
+ return {
495
+ ok: false,
496
+ message: `Could not enable start-at-login and compensating cleanup failed: ${cleanup.message}`,
497
+ unitPath,
498
+ };
499
+ }
500
+ return {
501
+ ok: false,
502
+ message: `Could not enable start-at-login (${invocation.command} ${invocation.args.join(' ')}): ${outcome.output || 'command failed'}`,
503
+ };
504
+ }
505
+ const cleanupFailures = disableCommands(platform, unitPath, label)
506
+ .map((cleanup) => run(cleanup))
507
+ .filter((cleanup) => !cleanup.ok);
508
+ if (cleanupFailures.length > 0) {
509
+ return {
510
+ ok: false,
511
+ message: `Could not enable start-at-login and compensating cleanup failed: ${cleanupFailures.map((failure) => failure.output || 'command failed').join('; ')}. Definition preserved at ${unitPath}.`,
512
+ unitPath,
513
+ };
514
+ }
515
+ }
516
+ else if (state.managerState === 'unknown') {
517
+ return {
518
+ ok: false,
519
+ message: `Could not enable start-at-login and manager state is unknown. Definition preserved at ${unitPath}.`,
520
+ unitPath,
521
+ };
522
+ }
234
523
  rmSync(unitPath, { force: true });
235
524
  return {
236
525
  ok: false,
@@ -244,34 +533,221 @@ export function enableMetaProxyService(options = {}) {
244
533
  unitPath,
245
534
  };
246
535
  }
536
+ function startCommand(platform, label = SERVICE_LABEL) {
537
+ if (platform === 'darwin')
538
+ return { command: 'launchctl', args: ['kickstart', `gui/${process.getuid?.() ?? 0}/${label}`] };
539
+ if (platform === 'linux')
540
+ return { command: 'systemctl', args: ['--user', 'start', 'meta-proxy.service'] };
541
+ if (platform === 'win32')
542
+ return { command: 'schtasks', args: ['/run', '/tn', label] };
543
+ return null;
544
+ }
545
+ function stopCommand(platform, label = SERVICE_LABEL) {
546
+ // `launchctl kill SIGTERM` may still be classified by launchd as an
547
+ // unsuccessful termination and restarted under KeepAlive. Bootout unloads
548
+ // the current job but deliberately leaves the plist in LaunchAgents, so the
549
+ // explicit stop stays stopped now and remains configured for next login.
550
+ if (platform === 'darwin')
551
+ return { command: 'launchctl', args: ['bootout', `gui/${process.getuid?.() ?? 0}/${label}`] };
552
+ if (platform === 'linux')
553
+ return { command: 'systemctl', args: ['--user', 'stop', 'meta-proxy.service'] };
554
+ if (platform === 'win32')
555
+ return { command: 'schtasks', args: ['/end', '/tn', label] };
556
+ return null;
557
+ }
558
+ export function startMetaProxyService(options = {}) {
559
+ const home = options.home ?? homedir();
560
+ const platform = options.platform ?? process.platform;
561
+ const run = options.run ?? defaultRunner;
562
+ const label = options.label ?? SERVICE_LABEL;
563
+ const state = metaProxyServiceState(platform, home, run, label);
564
+ if (state.managerState === 'unknown')
565
+ return { ok: false, message: 'Could not determine service-manager state.' };
566
+ if (state.managerState !== 'enabled' && !(platform === 'darwin' && state.definitionPresent)) {
567
+ return { ok: false, message: 'Meta-Proxy is not enabled at login.' };
568
+ }
569
+ if (state.running)
570
+ return { ok: true, message: `Meta-Proxy is already running${state.pid ? ` (pid ${state.pid})` : ''}.` };
571
+ const commands = platform === 'darwin' && state.managerState === 'disabled' && state.unitPath
572
+ ? enableCommands(platform, state.unitPath, label)
573
+ : [startCommand(platform, label)].filter((command) => command !== null);
574
+ if (commands.length === 0)
575
+ return { ok: false, message: `Managed start is not supported on ${platform}.` };
576
+ for (const command of commands) {
577
+ const outcome = run(command);
578
+ if (!outcome.ok)
579
+ return { ok: false, message: `Could not start Meta-Proxy: ${outcome.output || 'command failed'}` };
580
+ }
581
+ return { ok: true, message: 'Meta-Proxy managed start requested.' };
582
+ }
583
+ export function stopMetaProxyService(options = {}) {
584
+ const home = options.home ?? homedir();
585
+ const platform = options.platform ?? process.platform;
586
+ const run = options.run ?? defaultRunner;
587
+ const label = options.label ?? SERVICE_LABEL;
588
+ const state = metaProxyServiceState(platform, home, run, label);
589
+ if (state.managerState === 'unknown')
590
+ return { ok: false, message: 'Could not determine service-manager state.' };
591
+ if (state.managerState !== 'enabled') {
592
+ return state.enabledAtLogin
593
+ ? { ok: true, message: 'Meta-Proxy managed service is already stopped.' }
594
+ : { ok: false, message: 'Meta-Proxy is not enabled at login.' };
595
+ }
596
+ if (state.running === false)
597
+ return { ok: true, message: 'Meta-Proxy managed service is already stopped.' };
598
+ const command = stopCommand(platform, label);
599
+ if (!command)
600
+ return { ok: false, message: `Managed stop is not supported on ${platform}.` };
601
+ const outcome = run(command);
602
+ return outcome.ok
603
+ ? { ok: true, message: 'Meta-Proxy managed stop requested.' }
604
+ : { ok: false, message: `Could not stop Meta-Proxy: ${outcome.output || 'command failed'}` };
605
+ }
247
606
  export function disableMetaProxyService(options = {}) {
248
607
  const home = options.home ?? homedir();
249
608
  const platform = options.platform ?? process.platform;
250
609
  const run = options.run ?? defaultRunner;
251
- const unitPath = serviceUnitPath(platform, home);
610
+ const label = options.label ?? SERVICE_LABEL;
611
+ const wait = options.wait ?? defaultWait;
612
+ const unitPath = serviceUnitPath(platform, home, label);
252
613
  if (!unitPath) {
253
614
  return { ok: true, message: `Start-at-login is not supported on ${platform}; nothing to disable.` };
254
615
  }
255
- if (!existsSync(unitPath)) {
256
- return { ok: true, message: 'Meta-Proxy is not set to start at login.' };
616
+ const state = metaProxyServiceState(platform, home, run, label);
617
+ if (state.managerState === 'unknown') {
618
+ return { ok: false, message: `Could not determine service-manager state; preserved ${unitPath}.`, unitPath };
257
619
  }
258
- // Best-effort: the unit file is removed even if the OS command complains,
259
- // because a stale definition left behind is the state that lies to
260
- // `proxy status`. The daemon binary is untouched — disabling supervision is
261
- // not uninstalling the proxy.
262
- const failures = [];
263
- for (const invocation of disableCommands(platform, unitPath)) {
264
- const outcome = run(invocation);
265
- if (!outcome.ok)
266
- failures.push(outcome.output || `${invocation.command} failed`);
620
+ if (state.managerState === 'disabled' && !state.definitionPresent) {
621
+ return { ok: true, message: 'Meta-Proxy is not set to start at login.' };
267
622
  }
268
- rmSync(unitPath, { force: true });
269
- if (failures.length > 0) {
623
+ if (state.loaded === true && !state.definitionPresent) {
624
+ // Same ownership rule as enable: without the owned definition on disk we
625
+ // cannot prove the registered job is ours, and unregistering by label
626
+ // alone would stop another installer's service.
270
627
  return {
271
- ok: true,
272
- message: `Removed ${unitPath}, but the service manager reported: ${failures.join('; ')}. The Meta-Proxy binary is untouched.`,
628
+ ok: false,
629
+ message: `A Meta-Proxy ${platform === 'win32' ? 'task' : 'service'} is registered without the owned definition at ${unitPath}; refusing to stop or delete it by label alone.`,
630
+ unitPath,
273
631
  };
274
632
  }
633
+ if (platform === 'win32' && state.managerState === 'enabled') {
634
+ return stopAndDeleteWindowsTask(home, unitPath, label, run, wait);
635
+ }
636
+ const unregister = state.managerState === 'enabled'
637
+ ? disableCommands(platform, unitPath, label)[0]
638
+ : undefined;
639
+ if (unregister) {
640
+ const outcome = run(unregister);
641
+ if (!outcome.ok) {
642
+ if (platform === 'linux') {
643
+ const after = metaProxyServiceState(platform, home, run, label);
644
+ if (after.managerState === 'unknown') {
645
+ return {
646
+ ok: false,
647
+ message: `Could not disable start-at-login and could not determine whether it partially changed state: ${outcome.output || `${unregister.command} failed`}. Definition preserved at ${unitPath}.`,
648
+ unitPath,
649
+ };
650
+ }
651
+ const restore = [];
652
+ if (state.enabledAtLogin !== after.enabledAtLogin) {
653
+ restore.push({ command: 'systemctl', args: ['--user', state.enabledAtLogin ? 'enable' : 'disable', 'meta-proxy.service'] });
654
+ }
655
+ if (state.running !== after.running) {
656
+ restore.push({ command: 'systemctl', args: ['--user', state.running ? 'start' : 'stop', 'meta-proxy.service'] });
657
+ }
658
+ const restoreFailure = restore.map((command) => run(command)).find((result) => !result.ok);
659
+ const restored = restoreFailure ? null : metaProxyServiceState(platform, home, run, label);
660
+ if (restoreFailure
661
+ || restored?.managerState === 'unknown'
662
+ || restored?.loaded !== state.loaded
663
+ || restored?.enabledAtLogin !== state.enabledAtLogin
664
+ || restored?.running !== state.running) {
665
+ return {
666
+ ok: false,
667
+ message: `Could not disable start-at-login and restoring its possible partial changes failed: ${restoreFailure?.output || 'state mismatch'}. Definition preserved at ${unitPath}.`,
668
+ unitPath,
669
+ };
670
+ }
671
+ return {
672
+ ok: false,
673
+ message: `Could not disable start-at-login (${outcome.output || `${unregister.command} failed`}); prior login enablement and running state were restored. Definition preserved at ${unitPath}.`,
674
+ unitPath,
675
+ };
676
+ }
677
+ return {
678
+ ok: false,
679
+ message: `Could not disable start-at-login: ${outcome.output || `${unregister.command} failed`}. Definition preserved at ${unitPath}.`,
680
+ unitPath,
681
+ };
682
+ }
683
+ }
684
+ const definition = state.definitionPresent ? readFileSync(unitPath, 'utf8') : null;
685
+ rmSync(unitPath, { force: true });
686
+ if (platform === 'linux') {
687
+ const refresh = disableCommands(platform, unitPath, label)[1];
688
+ const outcome = refresh ? run(refresh) : { ok: false, output: 'daemon-reload command unavailable' };
689
+ if (!outcome.ok) {
690
+ if (definition !== null) {
691
+ mkdirSync(dirname(unitPath), { recursive: true, mode: 0o700 });
692
+ writeFileSync(unitPath, definition, { encoding: 'utf8', mode: 0o600 });
693
+ }
694
+ const restoreReload = refresh ? run(refresh) : { ok: false, output: 'daemon-reload command unavailable' };
695
+ const restoreCommands = [];
696
+ if (state.enabledAtLogin)
697
+ restoreCommands.push({ command: 'systemctl', args: ['--user', 'enable', 'meta-proxy.service'] });
698
+ if (state.running)
699
+ restoreCommands.push({ command: 'systemctl', args: ['--user', 'start', 'meta-proxy.service'] });
700
+ const restoreFailure = !restoreReload.ok
701
+ ? restoreReload
702
+ : restoreCommands.map((command) => run(command)).find((result) => !result.ok);
703
+ const restored = restoreFailure ? null : metaProxyServiceState(platform, home, run, label);
704
+ if (restoreFailure
705
+ || restored?.managerState === 'unknown'
706
+ || restored?.enabledAtLogin !== state.enabledAtLogin
707
+ || restored?.running !== state.running) {
708
+ return {
709
+ ok: false,
710
+ message: `Disabled service and daemon-reload failed; restoring the prior manager state also failed: ${restoreFailure?.output || 'state mismatch'}. Definition preserved at ${unitPath}.`,
711
+ unitPath,
712
+ };
713
+ }
714
+ return {
715
+ ok: false,
716
+ message: `Disable rolled back after daemon-reload failed: ${outcome.output || 'command failed'}. Prior definition, login enablement, and running state restored at ${unitPath}.`,
717
+ unitPath,
718
+ };
719
+ }
720
+ }
275
721
  return { ok: true, message: `Meta-Proxy will no longer start at login. Removed ${unitPath}.` };
276
722
  }
723
+ function stopAndDeleteWindowsTask(home, unitPath, label, run, wait, removeDefinition = true) {
724
+ const stopped = confirmWindowsTaskStopped(home, unitPath, label, run, wait);
725
+ if (!stopped.ok)
726
+ return stopped;
727
+ const removeTask = disableCommands('win32', unitPath, label)[0];
728
+ const removed = removeTask ? run(removeTask) : { ok: false, output: 'delete command unavailable' };
729
+ if (!removed.ok) {
730
+ return { ok: false, message: `Could not delete the stopped Meta-Proxy task: ${removed.output || 'command failed'}. Definition preserved at ${unitPath}.`, unitPath };
731
+ }
732
+ if (removeDefinition)
733
+ rmSync(unitPath, { force: true });
734
+ return { ok: true, message: removeDefinition ? `Meta-Proxy will no longer start at login. Removed ${unitPath}.` : 'Newly-created Meta-Proxy task removed; prior definition preserved.', unitPath };
735
+ }
736
+ function confirmWindowsTaskStopped(home, unitPath, label, run, wait) {
737
+ const end = stopCommand('win32', label);
738
+ if (!end)
739
+ return { ok: false, message: 'Could not construct the Windows stop command.', unitPath };
740
+ run(end);
741
+ let stopped = metaProxyServiceState('win32', home, run, label);
742
+ let consecutiveStopped = stopped.running === false ? 1 : 0;
743
+ for (let attempt = 0; attempt < 20 && stopped.managerState !== 'unknown' && consecutiveStopped < 2; attempt++) {
744
+ wait(100);
745
+ stopped = metaProxyServiceState('win32', home, run, label);
746
+ consecutiveStopped = stopped.running === false ? consecutiveStopped + 1 : 0;
747
+ }
748
+ if (stopped.managerState === 'unknown' || consecutiveStopped < 2) {
749
+ return { ok: false, message: `Could not confirm the Meta-Proxy task stopped; preserved task and ${unitPath}.`, unitPath };
750
+ }
751
+ return { ok: true, message: 'Meta-Proxy task is stably stopped.', unitPath };
752
+ }
277
753
  //# sourceMappingURL=meta-proxy-service.js.map