@just-every/manager 0.1.12 → 0.1.13

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.
@@ -5,12 +5,15 @@ import { platform as osPlatform } from 'os';
5
5
  const args = process.argv.slice(2);
6
6
  const wantsHelp = args.includes('--help') || args.includes('-h');
7
7
  const skipLaunchEnv = process.env.JE_AGENT_SKIP_LAUNCH === '1' || process.env.JE_MANAGER_SKIP_LAUNCH === '1';
8
+ const forceForeground = args.includes('--foreground') || args.includes('--no-daemon');
9
+ const forceDaemon = args.includes('--daemon');
8
10
  const sessionType = (process.env.XDG_SESSION_TYPE || '').toLowerCase();
9
11
  const hasDisplay = Boolean(process.env.DISPLAY || process.env.WAYLAND_DISPLAY);
10
12
  const hasGuiSession = hasDisplay || (Boolean(sessionType) && sessionType !== 'tty');
11
13
  const isHeadlessLinux = osPlatform() === 'linux' && !hasGuiSession;
12
14
  const downloadOnly = args.includes('--download-only') || args.includes('--no-open') || skipLaunchEnv;
13
15
  const printPath = args.includes('--print-path');
16
+ const launchMode = forceForeground ? 'foreground' : forceDaemon ? 'daemon' : 'auto';
14
17
 
15
18
  if (wantsHelp) {
16
19
  console.log(`Every Manager installer helper
@@ -21,6 +24,8 @@ Usage:
21
24
  Options:
22
25
  --download-only Download the installer but do not launch it.
23
26
  --print-path Print the installer path after download.
27
+ --foreground Run the Linux headless daemon in the foreground.
28
+ --daemon Force the Linux headless daemon to run in the background.
24
29
  -h, --help Show this help message.
25
30
 
26
31
  Environment:
@@ -40,7 +45,7 @@ try {
40
45
  console.log(result.path);
41
46
  }
42
47
  if (!downloadOnly) {
43
- await launchInstaller(result.path);
48
+ await launchInstaller(result.path, { mode: launchMode });
44
49
  } else if (isHeadlessLinux) {
45
50
  console.log('No desktop session detected. Run the installer on a machine with a GUI.');
46
51
  }
package/lib/installer.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  chmodSync,
3
+ copyFileSync,
3
4
  createWriteStream,
4
5
  existsSync,
5
6
  mkdirSync,
@@ -7,12 +8,13 @@ import {
7
8
  renameSync,
8
9
  statSync,
9
10
  unlinkSync,
11
+ writeFileSync,
10
12
  } from 'fs';
11
13
  import { dirname, join, resolve } from 'path';
12
14
  import { fileURLToPath } from 'url';
13
15
  import { arch as osArch, homedir, platform as osPlatform } from 'os';
14
16
  import { get } from 'https';
15
- import { spawn } from 'child_process';
17
+ import { spawn, spawnSync } from 'child_process';
16
18
 
17
19
  const OWNER_REPO = 'just-every/manager';
18
20
  const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
@@ -52,6 +54,7 @@ const EXTENSIONS = {
52
54
  };
53
55
 
54
56
  const FILE_PREFIXES = ['Every.Manager', 'JustEvery.Agent'];
57
+ const SYSTEMD_SERVICE_NAME = 'justevery-manager';
55
58
 
56
59
  function readPackageVersion() {
57
60
  const raw = readFileSync(PACKAGE_JSON, 'utf8');
@@ -80,6 +83,29 @@ function getCacheDir(version) {
80
83
  return dir;
81
84
  }
82
85
 
86
+ function getDataDir() {
87
+ const platform = osPlatform();
88
+ const home = homedir();
89
+ if (platform === 'win32') {
90
+ return process.env.LOCALAPPDATA || join(home, 'AppData', 'Local');
91
+ }
92
+ if (platform === 'darwin') {
93
+ return join(home, 'Library', 'Application Support');
94
+ }
95
+ return process.env.XDG_DATA_HOME || join(home, '.local', 'share');
96
+ }
97
+
98
+ function getServicePaths() {
99
+ const home = homedir();
100
+ const dataDir = join(getDataDir(), 'justevery', 'manager');
101
+ const binPath = join(dataDir, 'managerd');
102
+ const configHome = process.env.XDG_CONFIG_HOME || join(home, '.config');
103
+ const systemdDir = join(configHome, 'systemd', 'user');
104
+ const unitPath = join(systemdDir, `${SYSTEMD_SERVICE_NAME}.service`);
105
+ const logPath = join(dataDir, 'managerd.log');
106
+ return { dataDir, binPath, systemdDir, unitPath, logPath };
107
+ }
108
+
83
109
  async function fetchLatestVersion() {
84
110
  const url = `https://api.github.com/repos/${OWNER_REPO}/releases/latest`;
85
111
  const response = await downloadJson(url);
@@ -325,6 +351,113 @@ function ensureExecutable(path) {
325
351
  } catch {}
326
352
  }
327
353
 
354
+ function escapeSystemdValue(value) {
355
+ return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
356
+ }
357
+
358
+ function systemctlAvailable() {
359
+ const result = spawnSync('systemctl', ['--user', '--version'], { stdio: 'ignore' });
360
+ return result.status === 0;
361
+ }
362
+
363
+ function systemctlUser(args) {
364
+ return spawnSync('systemctl', ['--user', ...args], { stdio: 'ignore' });
365
+ }
366
+
367
+ function loginctlEnableLinger() {
368
+ const user = process.env.USER;
369
+ if (!user) return false;
370
+ const result = spawnSync('loginctl', ['enable-linger', user], { stdio: 'ignore' });
371
+ return result.status === 0;
372
+ }
373
+
374
+ function buildSystemdUnit(binaryPath, envVars) {
375
+ const lines = [
376
+ '[Unit]',
377
+ 'Description=Every Manager ingestion service',
378
+ 'After=network-online.target',
379
+ 'Wants=network-online.target',
380
+ '',
381
+ '[Service]',
382
+ 'Type=simple',
383
+ `ExecStart=${binaryPath} run`,
384
+ 'Restart=on-failure',
385
+ 'RestartSec=5',
386
+ ];
387
+ if (envVars) {
388
+ if (envVars.apiBase) {
389
+ lines.push(`Environment=JE_MANAGER_API_BASE=${escapeSystemdValue(envVars.apiBase)}`);
390
+ }
391
+ if (envVars.deviceName) {
392
+ lines.push(`Environment=JE_MANAGER_DEVICE_NAME=${escapeSystemdValue(envVars.deviceName)}`);
393
+ }
394
+ }
395
+ lines.push('', '[Install]', 'WantedBy=default.target', '');
396
+ return lines.join('\n');
397
+ }
398
+
399
+ function ensureServiceBinary(installerPath) {
400
+ const { dataDir, binPath } = getServicePaths();
401
+ if (!existsSync(dataDir)) {
402
+ mkdirSync(dataDir, { recursive: true });
403
+ }
404
+ try {
405
+ copyFileSync(installerPath, binPath);
406
+ } catch {
407
+ return null;
408
+ }
409
+ ensureExecutable(binPath);
410
+ return binPath;
411
+ }
412
+
413
+ function installSystemdService(binaryPath) {
414
+ if (!systemctlAvailable()) return { ok: false, reason: 'systemctl_unavailable' };
415
+ const { systemdDir, unitPath } = getServicePaths();
416
+ if (!existsSync(systemdDir)) {
417
+ mkdirSync(systemdDir, { recursive: true });
418
+ }
419
+ const envVars = {
420
+ apiBase: process.env.JE_MANAGER_API_BASE || process.env.MANAGER_API_BASE || '',
421
+ deviceName: process.env.JE_MANAGER_DEVICE_NAME || '',
422
+ };
423
+ const unitContents = buildSystemdUnit(binaryPath, envVars);
424
+ writeFileSync(unitPath, unitContents, 'utf8');
425
+ const reload = systemctlUser(['daemon-reload']);
426
+ if (reload.status !== 0) {
427
+ return { ok: false, reason: 'daemon-reload' };
428
+ }
429
+ const enable = systemctlUser(['enable', '--now', `${SYSTEMD_SERVICE_NAME}.service`]);
430
+ if (enable.status !== 0) {
431
+ return { ok: false, reason: 'enable' };
432
+ }
433
+ return { ok: true, needsLinger: true };
434
+ }
435
+
436
+ function ensureCronEntry(binaryPath) {
437
+ const { logPath, dataDir } = getServicePaths();
438
+ if (!existsSync(dataDir)) {
439
+ mkdirSync(dataDir, { recursive: true });
440
+ }
441
+ const cronLine = `@reboot ${binaryPath} run >> "${logPath}" 2>&1`;
442
+ const current = spawnSync('crontab', ['-l'], { encoding: 'utf8' });
443
+ const existing = current.status === 0 ? current.stdout : '';
444
+ if (existing.includes(binaryPath)) {
445
+ return true;
446
+ }
447
+ const next = `${existing.trimEnd()}\n${cronLine}\n`;
448
+ const update = spawnSync('crontab', ['-'], { input: next, encoding: 'utf8' });
449
+ return update.status === 0;
450
+ }
451
+
452
+ function spawnDetached(binaryPath) {
453
+ const child = spawn(binaryPath, ['run'], {
454
+ detached: true,
455
+ stdio: 'ignore',
456
+ env: { ...process.env, JE_MANAGER_DAEMONIZED: '1' },
457
+ });
458
+ child.unref();
459
+ }
460
+
328
461
  function shouldSkipLaunch() {
329
462
  if (process.env.JE_AGENT_SKIP_LAUNCH === '1' || process.env.JE_MANAGER_SKIP_LAUNCH === '1') {
330
463
  return true;
@@ -384,10 +517,30 @@ export async function ensureInstaller({ allowDownload = true } = {}) {
384
517
  throw new Error(`Unable to locate a compatible installer for ${osPlatform()} ${osArch()}.${authHint}`);
385
518
  }
386
519
 
387
- export function launchInstaller(installerPath) {
520
+ export function launchInstaller(installerPath, { mode = 'auto' } = {}) {
388
521
  const platform = osPlatform();
389
522
  if (platform === 'linux' && installerPath.includes('_headless')) {
390
- return runCommand(installerPath, []);
523
+ if (mode === 'foreground') {
524
+ return runCommand(installerPath, []);
525
+ }
526
+ const binaryPath = ensureServiceBinary(installerPath) || installerPath;
527
+ const service = installSystemdService(binaryPath);
528
+ if (service.ok) {
529
+ if (service.needsLinger) {
530
+ const lingered = loginctlEnableLinger();
531
+ if (!lingered && !ensureCronEntry(binaryPath)) {
532
+ console.log('Every Manager is running, but enable-linger failed; it may not start on reboot.');
533
+ console.log('Run: sudo loginctl enable-linger $USER');
534
+ }
535
+ }
536
+ console.log('Every Manager is running in the background (systemd user service).');
537
+ return Promise.resolve();
538
+ }
539
+ spawnDetached(binaryPath);
540
+ if (!ensureCronEntry(binaryPath)) {
541
+ console.log('Every Manager is running in the background, but auto-start on reboot may not be configured.');
542
+ }
543
+ return Promise.resolve();
391
544
  }
392
545
  if (shouldSkipLaunch()) {
393
546
  console.log('Every Manager installer downloaded. Launch skipped in this environment.');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@just-every/manager",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
4
4
  "description": "Installer wrapper for Every Manager",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",