@karmaniverous/jeeves 0.6.0-6 → 0.6.0-7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -0
- package/dist/cli/jeeves/index.js +2 -2
- package/dist/cli/service/index.js +355 -115
- package/dist/index.d.ts +29 -6
- package/dist/index.js +361 -120
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -201,6 +201,13 @@ Pre-defined marker sets: `SOUL_MARKERS`, `AGENTS_MARKERS`, and `LEGACY_TOOLS_MAR
|
|
|
201
201
|
|
|
202
202
|
- **`getServiceUrl(serviceName, consumerName?)`** — resolves a service URL via: consumer config → core config → default port constants.
|
|
203
203
|
|
|
204
|
+
## Service Management
|
|
205
|
+
|
|
206
|
+
`createServiceManager(descriptor)` backs each component's `service` CLI and `{name}_service` tool (install, uninstall, start, stop, restart, status). It uses NSSM on Windows and a launchd agent on macOS. On Linux it first looks for a system unit named `<service>.service` (for example `/etc/systemd/system/jeeves-watcher.service`, the way jeeves-tools provisions managed instances):
|
|
207
|
+
|
|
208
|
+
- **System unit exists:** `install` changes nothing and reports the unit (`{ existing: true, message }`). `status` reads the system unit (`systemctl is-active`). `start`, `stop` and `restart` run `sudo -n systemctl <verb> <unit>` (no sudo as root), which needs a passwordless sudoers rule for `/usr/bin/systemctl <verb> *`. When sudo refuses, the error names that rule. `uninstall` refuses, because core never removes, reloads or enables a system unit, and never creates a user unit next to one.
|
|
209
|
+
- **No system unit:** core manages a user unit in `~/.config/systemd/user` with `systemctl --user`. First it checks that a user bus exists. When `XDG_RUNTIME_DIR` is unset, or `systemctl --user` cannot connect, the error gives the cause and both fixes: provision a system unit, or run `sudo loginctl enable-linger <user>`. You don't get the raw "Failed to connect to bus" error.
|
|
210
|
+
|
|
204
211
|
## Prerequisites
|
|
205
212
|
|
|
206
213
|
- **Node.js >= 22** — the CLI enforces this at startup.
|
package/dist/cli/jeeves/index.js
CHANGED
|
@@ -140,14 +140,14 @@ const DEFAULT_PORTS = {
|
|
|
140
140
|
* Core library version, inlined at build time.
|
|
141
141
|
*
|
|
142
142
|
* @remarks
|
|
143
|
-
* The `0.6.0-
|
|
143
|
+
* The `0.6.0-7` placeholder is replaced by
|
|
144
144
|
* `@rollup/plugin-replace` during the build with the actual version
|
|
145
145
|
* from `package.json`. This ensures the correct version survives
|
|
146
146
|
* when consumers bundle core into their own dist (where runtime
|
|
147
147
|
* `import.meta.url`-based resolution would find the wrong package.json).
|
|
148
148
|
*/
|
|
149
149
|
/** The core library version from package.json (inlined at build time). */
|
|
150
|
-
const CORE_VERSION = '0.6.0-
|
|
150
|
+
const CORE_VERSION = '0.6.0-7';
|
|
151
151
|
|
|
152
152
|
/**
|
|
153
153
|
* Shared internal utility functions.
|
|
@@ -215,12 +215,130 @@ async function postJson(url, body) {
|
|
|
215
215
|
});
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
+
/**
|
|
219
|
+
* Linux systemd unit detection and state queries.
|
|
220
|
+
*
|
|
221
|
+
* @remarks
|
|
222
|
+
* A service can be provisioned either as a system unit
|
|
223
|
+
* (`/etc/systemd/system/<name>.service`, as jeeves-tools does on managed
|
|
224
|
+
* instances) or as a user unit (`~/.config/systemd/user`, as
|
|
225
|
+
* `createServiceManager` does elsewhere). An existing system unit always
|
|
226
|
+
* wins: status reads it and core never creates a competing user unit.
|
|
227
|
+
*
|
|
228
|
+
* Every query here is unprivileged. Commands run through an injectable
|
|
229
|
+
* `CommandExec` so the logic can be unit tested without systemd.
|
|
230
|
+
*/
|
|
231
|
+
/** Default `CommandExec`: `execSync` with piped stdio and a timeout. */
|
|
232
|
+
const defaultExec = (cmd) => execSync(cmd, {
|
|
233
|
+
encoding: 'utf-8',
|
|
234
|
+
timeout: 30_000,
|
|
235
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
236
|
+
}).trim();
|
|
237
|
+
/**
|
|
238
|
+
* Extract the most useful text from a failed command.
|
|
239
|
+
*
|
|
240
|
+
* @param err - Error thrown by a `CommandExec`.
|
|
241
|
+
* @returns The command's stderr when present, else the error message.
|
|
242
|
+
*/
|
|
243
|
+
function execErrorDetail(err) {
|
|
244
|
+
if (typeof err === 'object' && err !== null) {
|
|
245
|
+
if ('stderr' in err) {
|
|
246
|
+
const stderr = String(err.stderr).trim();
|
|
247
|
+
if (stderr)
|
|
248
|
+
return stderr;
|
|
249
|
+
}
|
|
250
|
+
if (err instanceof Error)
|
|
251
|
+
return err.message;
|
|
252
|
+
}
|
|
253
|
+
return String(err);
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* The systemd unit name for a service.
|
|
257
|
+
*
|
|
258
|
+
* @param serviceName - Service name, e.g. `jeeves-watcher`.
|
|
259
|
+
* @returns The unit name, e.g. `jeeves-watcher.service`.
|
|
260
|
+
*/
|
|
261
|
+
function systemdUnitName(serviceName) {
|
|
262
|
+
return `${serviceName}.service`;
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* LoadStates meaning a system unit exists. A masked unit still counts: an
|
|
266
|
+
* administrator disabled it, and a user unit must not replace it.
|
|
267
|
+
*/
|
|
268
|
+
const SYSTEM_UNIT_PRESENT = new Set(['loaded', 'masked']);
|
|
269
|
+
/**
|
|
270
|
+
* Whether a system-level systemd unit exists for the service.
|
|
271
|
+
*
|
|
272
|
+
* @remarks
|
|
273
|
+
* Queries the system manager (no `--user`, no sudo):
|
|
274
|
+
* `systemctl show <unit> --property=LoadState --value` prints `loaded`
|
|
275
|
+
* (or `masked`) for an existing unit and `not-found` otherwise. Any failure (no
|
|
276
|
+
* systemd, no system bus) means no usable system unit.
|
|
277
|
+
*
|
|
278
|
+
* @param serviceName - Service name.
|
|
279
|
+
* @param exec - Command runner.
|
|
280
|
+
* @returns True when a system unit is loaded.
|
|
281
|
+
*/
|
|
282
|
+
function hasSystemUnit(serviceName, exec = defaultExec) {
|
|
283
|
+
try {
|
|
284
|
+
const loadState = exec(`systemctl show ${systemdUnitName(serviceName)} --property=LoadState --value`);
|
|
285
|
+
return SYSTEM_UNIT_PRESENT.has(loadState.trim());
|
|
286
|
+
}
|
|
287
|
+
catch {
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Read a unit's active state and map it to a `ServiceState`.
|
|
293
|
+
*
|
|
294
|
+
* @param scopeFlag - `''` for the system manager, `'--user '` for the user manager.
|
|
295
|
+
* @param unit - Unit name.
|
|
296
|
+
* @param exec - Command runner.
|
|
297
|
+
* @returns `running` when active, else `stopped`.
|
|
298
|
+
*/
|
|
299
|
+
function activeState(scopeFlag, unit, exec) {
|
|
300
|
+
try {
|
|
301
|
+
return exec(`systemctl ${scopeFlag}is-active ${unit}`) === 'active'
|
|
302
|
+
? 'running'
|
|
303
|
+
: 'stopped';
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
// is-active exits non-zero for inactive/failed units.
|
|
307
|
+
return 'stopped';
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Detect the state of a Linux systemd service.
|
|
312
|
+
*
|
|
313
|
+
* @remarks
|
|
314
|
+
* System unit first (`systemctl is-active <unit>`), then the user unit
|
|
315
|
+
* (`systemctl --user is-enabled` / `is-active`).
|
|
316
|
+
*
|
|
317
|
+
* @param serviceName - Service name.
|
|
318
|
+
* @param exec - Command runner.
|
|
319
|
+
* @returns The detected service state.
|
|
320
|
+
*/
|
|
321
|
+
function getSystemdServiceState(serviceName, exec = defaultExec) {
|
|
322
|
+
const unit = systemdUnitName(serviceName);
|
|
323
|
+
if (hasSystemUnit(serviceName, exec))
|
|
324
|
+
return activeState('', unit, exec);
|
|
325
|
+
try {
|
|
326
|
+
exec(`systemctl --user is-enabled ${unit}`);
|
|
327
|
+
}
|
|
328
|
+
catch {
|
|
329
|
+
return 'not_installed';
|
|
330
|
+
}
|
|
331
|
+
return activeState('--user ', unit, exec);
|
|
332
|
+
}
|
|
333
|
+
|
|
218
334
|
/**
|
|
219
335
|
* Platform-aware service state detection.
|
|
220
336
|
*
|
|
221
337
|
* @remarks
|
|
222
338
|
* Detects whether a system service is installed and running.
|
|
223
339
|
* Delegates to NSSM (Windows), systemd (Linux), or launchd (macOS).
|
|
340
|
+
* On Linux an existing system unit takes precedence over a user unit
|
|
341
|
+
* (see `getSystemdServiceState`).
|
|
224
342
|
*/
|
|
225
343
|
/**
|
|
226
344
|
* Detect the state of a system service by name.
|
|
@@ -235,7 +353,7 @@ function getServiceState(serviceName) {
|
|
|
235
353
|
case 'darwin':
|
|
236
354
|
return getServiceStateMacOS(serviceName);
|
|
237
355
|
default:
|
|
238
|
-
return
|
|
356
|
+
return getSystemdServiceState(serviceName);
|
|
239
357
|
}
|
|
240
358
|
}
|
|
241
359
|
/**
|
|
@@ -263,36 +381,6 @@ function getServiceStateWindows(serviceName) {
|
|
|
263
381
|
return 'not_installed';
|
|
264
382
|
}
|
|
265
383
|
}
|
|
266
|
-
/**
|
|
267
|
-
* Linux: detect via systemd user services.
|
|
268
|
-
* - `systemctl --user is-enabled {name}.service` exits non-zero = not installed
|
|
269
|
-
* - `systemctl --user is-active {name}.service` returns "active" = running
|
|
270
|
-
*/
|
|
271
|
-
function getServiceStateLinux(serviceName) {
|
|
272
|
-
try {
|
|
273
|
-
execSync(`systemctl --user is-enabled ${serviceName}.service`, {
|
|
274
|
-
encoding: 'utf-8',
|
|
275
|
-
timeout: 5000,
|
|
276
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
277
|
-
});
|
|
278
|
-
}
|
|
279
|
-
catch {
|
|
280
|
-
return 'not_installed';
|
|
281
|
-
}
|
|
282
|
-
try {
|
|
283
|
-
const output = execSync(`systemctl --user is-active ${serviceName}.service`, {
|
|
284
|
-
encoding: 'utf-8',
|
|
285
|
-
timeout: 5000,
|
|
286
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
287
|
-
}).trim();
|
|
288
|
-
if (output === 'active')
|
|
289
|
-
return 'running';
|
|
290
|
-
return 'stopped';
|
|
291
|
-
}
|
|
292
|
-
catch {
|
|
293
|
-
return 'stopped';
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
384
|
/**
|
|
297
385
|
* macOS: detect via launchctl.
|
|
298
386
|
* - `launchctl list {name}` exits non-zero = not installed
|
|
@@ -336,35 +424,12 @@ function isExecError(err) {
|
|
|
336
424
|
}
|
|
337
425
|
|
|
338
426
|
/**
|
|
339
|
-
*
|
|
427
|
+
* Service manager contract and shared option resolution.
|
|
340
428
|
*
|
|
341
429
|
* @remarks
|
|
342
|
-
*
|
|
343
|
-
*
|
|
344
|
-
* (Windows), systemd (Linux), or launchd (macOS) based on platform.
|
|
430
|
+
* Shared by the per-platform managers built in `createServiceManager`
|
|
431
|
+
* and `createLinuxManager`.
|
|
345
432
|
*/
|
|
346
|
-
/** Exec helper that returns stdout. */
|
|
347
|
-
function run(cmd) {
|
|
348
|
-
return execSync(cmd, {
|
|
349
|
-
encoding: 'utf-8',
|
|
350
|
-
timeout: 30_000,
|
|
351
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
352
|
-
}).trim();
|
|
353
|
-
}
|
|
354
|
-
/** Exec helper that suppresses errors and returns success boolean. */
|
|
355
|
-
function runQuiet(cmd) {
|
|
356
|
-
try {
|
|
357
|
-
execSync(cmd, {
|
|
358
|
-
encoding: 'utf-8',
|
|
359
|
-
timeout: 30_000,
|
|
360
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
361
|
-
});
|
|
362
|
-
return true;
|
|
363
|
-
}
|
|
364
|
-
catch {
|
|
365
|
-
return false;
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
433
|
/**
|
|
369
434
|
* Resolve the effective service name from options and descriptor.
|
|
370
435
|
*
|
|
@@ -388,47 +453,101 @@ function resolveConfigFilePath(descriptor, options) {
|
|
|
388
453
|
const configDir = getComponentConfigDir(descriptor.name);
|
|
389
454
|
return join(configDir, descriptor.configFileName);
|
|
390
455
|
}
|
|
391
|
-
/**
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
}
|
|
430
|
-
|
|
456
|
+
/**
|
|
457
|
+
* The default install result for a freshly installed service.
|
|
458
|
+
*
|
|
459
|
+
* @param svcName - Service name.
|
|
460
|
+
* @returns A non-existing install result.
|
|
461
|
+
*/
|
|
462
|
+
function installedResult(svcName) {
|
|
463
|
+
return { existing: false, message: `Service "${svcName}" installed.` };
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Guarded access to the systemd user and system managers.
|
|
468
|
+
*
|
|
469
|
+
* @remarks
|
|
470
|
+
* - User scope: fail early with an actionable message when there is no user
|
|
471
|
+
* bus (typical for `useradd --system` accounts without linger), instead of
|
|
472
|
+
* surfacing systemctl's raw "Failed to connect to bus" error.
|
|
473
|
+
* - System scope: run only `systemctl start|stop|restart <unit>` through
|
|
474
|
+
* non-interactive sudo, which is exactly what the jeeves-tools sudoers rule
|
|
475
|
+
* for the `jeeves` user allows (`/usr/bin/systemctl stop|start|restart|status *`).
|
|
476
|
+
* Core never writes system units, reloads or enables them.
|
|
477
|
+
*/
|
|
478
|
+
/** Output patterns sudo prints when a non-interactive command is refused. */
|
|
479
|
+
const SUDO_REFUSED = /password is required|not allowed to execute|may not run sudo|not in the sudoers/i;
|
|
480
|
+
function userLabel(env) {
|
|
481
|
+
return env.USER ?? env.LOGNAME ?? 'this user';
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* Ensure a systemd user bus is reachable before any `systemctl --user` call.
|
|
485
|
+
*
|
|
486
|
+
* @param unit - Unit being managed (for the message).
|
|
487
|
+
* @param deps - Host dependencies.
|
|
488
|
+
* @throws Error with the cause and remedies when no user bus is available.
|
|
489
|
+
*/
|
|
490
|
+
function assertUserBus(unit, deps) {
|
|
491
|
+
const user = userLabel(deps.env);
|
|
492
|
+
const noBus = (reason, cause) => new Error([
|
|
493
|
+
`Cannot manage ${unit} as a systemd user unit: ${reason}.`,
|
|
494
|
+
`No system unit named ${unit} exists either.`,
|
|
495
|
+
'Fix one of:',
|
|
496
|
+
`(1) have an administrator provision ${unit} as a system unit in /etc/systemd/system; it is then detected and managed with "sudo -n systemctl";`,
|
|
497
|
+
`(2) enable a user manager with "sudo loginctl enable-linger ${user}" and retry from a session where XDG_RUNTIME_DIR is set.`,
|
|
498
|
+
].join(' '), { cause });
|
|
499
|
+
if (!deps.env.XDG_RUNTIME_DIR && !deps.env.DBUS_SESSION_BUS_ADDRESS) {
|
|
500
|
+
throw noBus(`there is no systemd user bus for "${user}" (XDG_RUNTIME_DIR is not set)`);
|
|
501
|
+
}
|
|
502
|
+
try {
|
|
503
|
+
deps.exec('systemctl --user show-environment');
|
|
504
|
+
}
|
|
505
|
+
catch (err) {
|
|
506
|
+
throw noBus(`the systemd user bus for "${user}" is unreachable (${execErrorDetail(err)})`, err);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* Run a lifecycle verb against an existing system unit.
|
|
511
|
+
*
|
|
512
|
+
* @remarks
|
|
513
|
+
* Uses `sudo -n systemctl <verb> <unit>` (plain `systemctl` when root).
|
|
514
|
+
* `-n` makes sudo fail instead of prompting for a password.
|
|
515
|
+
*
|
|
516
|
+
* @param verb - `start`, `stop` or `restart`.
|
|
517
|
+
* @param unit - System unit name.
|
|
518
|
+
* @param deps - Host dependencies.
|
|
519
|
+
* @throws Error naming the missing sudoers rule when sudo refuses.
|
|
520
|
+
*/
|
|
521
|
+
function runSystemVerb(verb, unit, deps) {
|
|
522
|
+
const root = deps.isRoot();
|
|
523
|
+
const cmd = root
|
|
524
|
+
? `systemctl ${verb} ${unit}`
|
|
525
|
+
: `sudo -n systemctl ${verb} ${unit}`;
|
|
526
|
+
try {
|
|
527
|
+
deps.exec(cmd);
|
|
528
|
+
}
|
|
529
|
+
catch (err) {
|
|
530
|
+
const detail = execErrorDetail(err);
|
|
531
|
+
if (!root && SUDO_REFUSED.test(detail)) {
|
|
532
|
+
throw new Error(`${unit} is a system unit and "${cmd}" was refused: passwordless sudo for "/usr/bin/systemctl ${verb} *" is not granted to "${userLabel(deps.env)}" (${detail}). Ask an administrator to run "sudo systemctl ${verb} ${unit}" or to grant that sudoers rule.`, { cause: err });
|
|
533
|
+
}
|
|
534
|
+
throw new Error(`"${cmd}" failed: ${detail}`, { cause: err });
|
|
535
|
+
}
|
|
431
536
|
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Linux systemd service manager.
|
|
540
|
+
*
|
|
541
|
+
* @remarks
|
|
542
|
+
* Two scopes:
|
|
543
|
+
* - **System unit present** (e.g. `/etc/systemd/system/jeeves-watcher.service`
|
|
544
|
+
* provisioned by jeeves-tools): install is a no-op that reports the unit,
|
|
545
|
+
* uninstall refuses, status reads the system unit, and start/stop/restart
|
|
546
|
+
* run `sudo -n systemctl <verb> <unit>`. Core never creates a competing
|
|
547
|
+
* user unit.
|
|
548
|
+
* - **Otherwise**: a user unit in `~/.config/systemd/user` managed with
|
|
549
|
+
* `systemctl --user`, after checking that a user bus exists.
|
|
550
|
+
*/
|
|
432
551
|
/**
|
|
433
552
|
* Generate a systemd user unit file.
|
|
434
553
|
*
|
|
@@ -437,7 +556,6 @@ function createWindowsManager(descriptor) {
|
|
|
437
556
|
* @returns Unit file content.
|
|
438
557
|
*/
|
|
439
558
|
function buildSystemdUnit(svcName, cmdArgs) {
|
|
440
|
-
const execStart = cmdArgs.join(' ');
|
|
441
559
|
return [
|
|
442
560
|
'[Unit]',
|
|
443
561
|
`Description=${svcName}`,
|
|
@@ -445,7 +563,7 @@ function buildSystemdUnit(svcName, cmdArgs) {
|
|
|
445
563
|
'',
|
|
446
564
|
'[Service]',
|
|
447
565
|
'Type=simple',
|
|
448
|
-
`ExecStart=${
|
|
566
|
+
`ExecStart=${cmdArgs.join(' ')}`,
|
|
449
567
|
'Restart=on-failure',
|
|
450
568
|
'RestartSec=5',
|
|
451
569
|
'',
|
|
@@ -453,42 +571,160 @@ function buildSystemdUnit(svcName, cmdArgs) {
|
|
|
453
571
|
'WantedBy=default.target',
|
|
454
572
|
].join('\n');
|
|
455
573
|
}
|
|
456
|
-
/**
|
|
457
|
-
function
|
|
458
|
-
|
|
574
|
+
/** Default host dependencies. */
|
|
575
|
+
function defaultDeps() {
|
|
576
|
+
return {
|
|
577
|
+
exec: defaultExec,
|
|
578
|
+
env: process.env,
|
|
579
|
+
isRoot: () => process.getuid?.() === 0,
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Build a Linux systemd service manager.
|
|
584
|
+
*
|
|
585
|
+
* @param descriptor - Component descriptor.
|
|
586
|
+
* @param deps - Host dependencies (defaults to the real host).
|
|
587
|
+
* @param unitDir - User unit directory (defaults to `~/.config/systemd/user`).
|
|
588
|
+
* @returns A `ServiceManager` for Linux.
|
|
589
|
+
*/
|
|
590
|
+
function createLinuxManager(descriptor, deps = defaultDeps(), unitDir = join(homedir(), '.config', 'systemd', 'user')) {
|
|
591
|
+
const exec = deps.exec;
|
|
459
592
|
function unitPath(svcName) {
|
|
460
|
-
return join(unitDir,
|
|
593
|
+
return join(unitDir, systemdUnitName(svcName));
|
|
594
|
+
}
|
|
595
|
+
/** Run a lifecycle verb in whichever scope owns the unit. */
|
|
596
|
+
function lifecycle(verb, options) {
|
|
597
|
+
const svcName = resolveServiceName(descriptor, options);
|
|
598
|
+
const unit = systemdUnitName(svcName);
|
|
599
|
+
if (hasSystemUnit(svcName, exec)) {
|
|
600
|
+
runSystemVerb(verb, unit, deps);
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
assertUserBus(unit, deps);
|
|
604
|
+
exec(`systemctl --user ${verb} ${unit}`);
|
|
461
605
|
}
|
|
462
606
|
return {
|
|
463
607
|
install(options) {
|
|
464
608
|
const svcName = resolveServiceName(descriptor, options);
|
|
465
|
-
const
|
|
466
|
-
|
|
609
|
+
const unit = systemdUnitName(svcName);
|
|
610
|
+
if (hasSystemUnit(svcName, exec)) {
|
|
611
|
+
return {
|
|
612
|
+
existing: true,
|
|
613
|
+
message: `Service "${svcName}" is already installed as system unit ${unit}; nothing to do. Manage it with start/stop/restart (sudo -n systemctl).`,
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
assertUserBus(unit, deps);
|
|
617
|
+
const cmdArgs = descriptor.startCommand(resolveConfigFilePath(descriptor, options));
|
|
467
618
|
mkdirSync(unitDir, { recursive: true });
|
|
468
619
|
writeFileSync(unitPath(svcName), buildSystemdUnit(svcName, cmdArgs));
|
|
469
|
-
|
|
470
|
-
|
|
620
|
+
exec('systemctl --user daemon-reload');
|
|
621
|
+
exec(`systemctl --user enable ${unit}`);
|
|
622
|
+
return installedResult(svcName);
|
|
471
623
|
},
|
|
472
624
|
uninstall(options) {
|
|
473
625
|
const svcName = resolveServiceName(descriptor, options);
|
|
474
|
-
|
|
475
|
-
|
|
626
|
+
const unit = systemdUnitName(svcName);
|
|
627
|
+
if (hasSystemUnit(svcName, exec)) {
|
|
628
|
+
throw new Error(`${unit} is a system unit provisioned outside core; core will not remove it. Ask an administrator to disable and delete it.`);
|
|
629
|
+
}
|
|
630
|
+
assertUserBus(unit, deps);
|
|
631
|
+
const quiet = (cmd) => {
|
|
632
|
+
try {
|
|
633
|
+
exec(cmd);
|
|
634
|
+
}
|
|
635
|
+
catch {
|
|
636
|
+
// Best effort: the unit may already be stopped or disabled.
|
|
637
|
+
}
|
|
638
|
+
};
|
|
639
|
+
quiet(`systemctl --user stop ${unit}`);
|
|
640
|
+
quiet(`systemctl --user disable ${unit}`);
|
|
476
641
|
const path = unitPath(svcName);
|
|
477
642
|
if (existsSync(path))
|
|
478
643
|
unlinkSync(path);
|
|
479
|
-
|
|
644
|
+
quiet('systemctl --user daemon-reload');
|
|
480
645
|
},
|
|
481
646
|
start(options) {
|
|
647
|
+
lifecycle('start', options);
|
|
648
|
+
},
|
|
649
|
+
stop(options) {
|
|
650
|
+
lifecycle('stop', options);
|
|
651
|
+
},
|
|
652
|
+
restart(options) {
|
|
653
|
+
lifecycle('restart', options);
|
|
654
|
+
},
|
|
655
|
+
status(options) {
|
|
656
|
+
return getSystemdServiceState(resolveServiceName(descriptor, options), exec);
|
|
657
|
+
},
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* Factory for platform-aware service lifecycle management.
|
|
663
|
+
*
|
|
664
|
+
* @remarks
|
|
665
|
+
* Produces a `ServiceManager` that handles install, uninstall, start,
|
|
666
|
+
* stop, restart, and status for system services. Delegates to NSSM
|
|
667
|
+
* (Windows), systemd (Linux), or launchd (macOS) based on platform.
|
|
668
|
+
* On Linux an existing system unit is detected and managed in place
|
|
669
|
+
* (see `createLinuxManager`).
|
|
670
|
+
*/
|
|
671
|
+
/** Exec helper that returns stdout. */
|
|
672
|
+
function run(cmd) {
|
|
673
|
+
return execSync(cmd, {
|
|
674
|
+
encoding: 'utf-8',
|
|
675
|
+
timeout: 30_000,
|
|
676
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
677
|
+
}).trim();
|
|
678
|
+
}
|
|
679
|
+
/** Exec helper that suppresses errors and returns success boolean. */
|
|
680
|
+
function runQuiet(cmd) {
|
|
681
|
+
try {
|
|
682
|
+
execSync(cmd, {
|
|
683
|
+
encoding: 'utf-8',
|
|
684
|
+
timeout: 30_000,
|
|
685
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
686
|
+
});
|
|
687
|
+
return true;
|
|
688
|
+
}
|
|
689
|
+
catch {
|
|
690
|
+
return false;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
/** Build a Windows NSSM service manager. */
|
|
694
|
+
function createWindowsManager(descriptor) {
|
|
695
|
+
return {
|
|
696
|
+
install(options) {
|
|
482
697
|
const svcName = resolveServiceName(descriptor, options);
|
|
483
|
-
|
|
698
|
+
const cfgPath = resolveConfigFilePath(descriptor, options);
|
|
699
|
+
const cmdArgs = descriptor.startCommand(cfgPath);
|
|
700
|
+
const appPath = cmdArgs[0];
|
|
701
|
+
const appArgs = cmdArgs.slice(1).join(' ');
|
|
702
|
+
run(`nssm install ${svcName} ${appPath}`);
|
|
703
|
+
if (appArgs) {
|
|
704
|
+
run(`nssm set ${svcName} AppParameters ${appArgs}`);
|
|
705
|
+
}
|
|
706
|
+
run(`nssm set ${svcName} AppStdout ${join(homedir(), `${svcName}.log`)}`);
|
|
707
|
+
run(`nssm set ${svcName} AppStderr ${join(homedir(), `${svcName}.log`)}`);
|
|
708
|
+
run(`nssm set ${svcName} AppRotateFiles 1`);
|
|
709
|
+
run(`nssm set ${svcName} AppRotateBytes 1048576`);
|
|
710
|
+
return installedResult(svcName);
|
|
711
|
+
},
|
|
712
|
+
uninstall(options) {
|
|
713
|
+
const svcName = resolveServiceName(descriptor, options);
|
|
714
|
+
runQuiet(`nssm stop ${svcName}`);
|
|
715
|
+
run(`nssm remove ${svcName} confirm`);
|
|
716
|
+
},
|
|
717
|
+
start(options) {
|
|
718
|
+
const svcName = resolveServiceName(descriptor, options);
|
|
719
|
+
run(`nssm start ${svcName}`);
|
|
484
720
|
},
|
|
485
721
|
stop(options) {
|
|
486
722
|
const svcName = resolveServiceName(descriptor, options);
|
|
487
|
-
run(`
|
|
723
|
+
run(`nssm stop ${svcName}`);
|
|
488
724
|
},
|
|
489
725
|
restart(options) {
|
|
490
726
|
const svcName = resolveServiceName(descriptor, options);
|
|
491
|
-
run(`
|
|
727
|
+
run(`nssm restart ${svcName}`);
|
|
492
728
|
},
|
|
493
729
|
status(options) {
|
|
494
730
|
const svcName = resolveServiceName(descriptor, options);
|
|
@@ -542,6 +778,7 @@ function createMacOSManager(descriptor) {
|
|
|
542
778
|
const cmdArgs = descriptor.startCommand(cfgPath);
|
|
543
779
|
mkdirSync(agentsDir, { recursive: true });
|
|
544
780
|
writeFileSync(plistPath(svcName), buildLaunchdPlist(svcName, cmdArgs));
|
|
781
|
+
return installedResult(svcName);
|
|
545
782
|
},
|
|
546
783
|
uninstall(options) {
|
|
547
784
|
const svcName = resolveServiceName(descriptor, options);
|
|
@@ -848,8 +1085,11 @@ function createServiceCli(descriptor) {
|
|
|
848
1085
|
.option('-n, --name <name>', 'Service name', defaultServiceName)
|
|
849
1086
|
.action((opts) => {
|
|
850
1087
|
try {
|
|
851
|
-
svcManager.install({
|
|
852
|
-
|
|
1088
|
+
const result = svcManager.install({
|
|
1089
|
+
name: opts.name,
|
|
1090
|
+
configPath: opts.config,
|
|
1091
|
+
});
|
|
1092
|
+
console.log(result.message);
|
|
853
1093
|
}
|
|
854
1094
|
catch (err) {
|
|
855
1095
|
handleCommandError('Install', err);
|
package/dist/index.d.ts
CHANGED
|
@@ -741,6 +741,8 @@ declare function getBindAddress(componentName?: string): string;
|
|
|
741
741
|
* @remarks
|
|
742
742
|
* Detects whether a system service is installed and running.
|
|
743
743
|
* Delegates to NSSM (Windows), systemd (Linux), or launchd (macOS).
|
|
744
|
+
* On Linux an existing system unit takes precedence over a user unit
|
|
745
|
+
* (see `getSystemdServiceState`).
|
|
744
746
|
*/
|
|
745
747
|
/** Service states returned by getServiceState. */
|
|
746
748
|
type ServiceState = 'not_installed' | 'stopped' | 'running';
|
|
@@ -1582,12 +1584,11 @@ declare function saveCache(): void;
|
|
|
1582
1584
|
declare function getChannelWorkspace(channelId: string, token: string, options: SlackWorkspaceOptions): Promise<string>;
|
|
1583
1585
|
|
|
1584
1586
|
/**
|
|
1585
|
-
*
|
|
1587
|
+
* Service manager contract and shared option resolution.
|
|
1586
1588
|
*
|
|
1587
1589
|
* @remarks
|
|
1588
|
-
*
|
|
1589
|
-
*
|
|
1590
|
-
* (Windows), systemd (Linux), or launchd (macOS) based on platform.
|
|
1590
|
+
* Shared by the per-platform managers built in `createServiceManager`
|
|
1591
|
+
* and `createLinuxManager`.
|
|
1591
1592
|
*/
|
|
1592
1593
|
|
|
1593
1594
|
/** Options for service manager commands that accept a service name override. */
|
|
@@ -1597,10 +1598,20 @@ interface ServiceManagerOptions {
|
|
|
1597
1598
|
/** Override config path for install. */
|
|
1598
1599
|
configPath?: string;
|
|
1599
1600
|
}
|
|
1601
|
+
/** Outcome of {@link ServiceManager.install}. */
|
|
1602
|
+
interface ServiceInstallResult {
|
|
1603
|
+
/**
|
|
1604
|
+
* True when the service was already provisioned outside core (a Linux
|
|
1605
|
+
* system unit) and install was a no-op.
|
|
1606
|
+
*/
|
|
1607
|
+
existing: boolean;
|
|
1608
|
+
/** Human-readable summary of what install did. */
|
|
1609
|
+
message: string;
|
|
1610
|
+
}
|
|
1600
1611
|
/** Service lifecycle manager produced by the factory. */
|
|
1601
1612
|
interface ServiceManager {
|
|
1602
1613
|
/** Install the service with the system service manager. */
|
|
1603
|
-
install(options?: ServiceManagerOptions):
|
|
1614
|
+
install(options?: ServiceManagerOptions): ServiceInstallResult;
|
|
1604
1615
|
/** Uninstall the service from the system service manager. */
|
|
1605
1616
|
uninstall(options?: ServiceManagerOptions): void;
|
|
1606
1617
|
/** Start the service. */
|
|
@@ -1612,6 +1623,18 @@ interface ServiceManager {
|
|
|
1612
1623
|
/** Query the service state. */
|
|
1613
1624
|
status(options?: ServiceManagerOptions): ServiceState;
|
|
1614
1625
|
}
|
|
1626
|
+
|
|
1627
|
+
/**
|
|
1628
|
+
* Factory for platform-aware service lifecycle management.
|
|
1629
|
+
*
|
|
1630
|
+
* @remarks
|
|
1631
|
+
* Produces a `ServiceManager` that handles install, uninstall, start,
|
|
1632
|
+
* stop, restart, and status for system services. Delegates to NSSM
|
|
1633
|
+
* (Windows), systemd (Linux), or launchd (macOS) based on platform.
|
|
1634
|
+
* On Linux an existing system unit is detected and managed in place
|
|
1635
|
+
* (see `createLinuxManager`).
|
|
1636
|
+
*/
|
|
1637
|
+
|
|
1615
1638
|
/**
|
|
1616
1639
|
* Create a platform-aware service manager from a component descriptor.
|
|
1617
1640
|
*
|
|
@@ -1646,4 +1669,4 @@ declare function getErrorMessage(err: unknown): string;
|
|
|
1646
1669
|
declare function isTransientError(err: unknown): boolean;
|
|
1647
1670
|
|
|
1648
1671
|
export { AGENTS_MARKERS, COMPONENT_CONFIG_PREFIX, CONFIG_FILE, CONVERSATION_HOOK_NAMES, CORE_CONFIG_DIR, CORE_VERSION, DEFAULT_BIND_ADDRESS, DEFAULT_PORTS, LEGACY_TOOLS_MARKERS, META_PORT, PLATFORM_COMPONENTS, RUNNER_PORT, SERVER_PORT, SOUL_MARKERS, STALE_LOCK_MS, VERSION_STAMP_PATTERN, WATCHER_PORT, WORKSPACE_CONFIG_DEFAULTS, WORKSPACE_CONFIG_FILE, WORKSPACE_FILES, analyzeMemory, appendJsonl, atomicWrite, buildEffectiveConfig, checkNodeVersion, connectionFail, coreConfigSchema, createConfigApplyHandler, createConfigQueryHandler, createGoogleAuth, createPluginToolset, createServiceCli, createServiceManager, createStatusHandler, ensureDir, fail, fetchJson, fetchWithTimeout, formatBeginMarker, formatEndMarker, generateJsonSchema, generateWorkspaceJsonSchema, getArg, getBindAddress, getChannelWorkspace, getComponentConfigDir, getComponentConfigPath, getConfigRoot, getCoreConfigDir, getCoreConfigFile, getEffectiveServiceName, getErrorMessage, getPackageRoot, getPackageVersion, getServiceState, getServiceUrl, getWorkspacePath, init, isTransientError, jeevesComponentDescriptorSchema, loadEnvFile, loadWorkspaceConfig, nowIso, ok, onPluginDispose, parseArgs, parseManaged, pluginToolsetOptionsSchema, postJson, promptContextOptionsSchema, readJson, readJsonl, recordRegisteredHooks, registerComponentConfigPath, registerPromptContext, rejectWindowsDrivePath, removeManagedBlock, renderManagedBlock, resetInit, resolveConfigValue, resolveOptionalPluginSetting, resolvePluginSetting, resolveWorkspacePath, run, runScript, runWithRetry, saveCache, sleepAsync, sleepMs, substituteEnvVars, upsertManagedBlock, uuid, validateConversationHooks, validateSkillFrontmatter, withFileLock, workspaceConfigSchema, writeJsonAtomic, writeJsonl };
|
|
1649
|
-
export type { AccountConfig, ConfigApplyHandler, ConfigApplyRequest, ConfigApplyResult, ConfigProvenance, ConfigQueryHandler, ConfigQueryResponse, CoreConfig, CreateStatusHandlerOptions, GoogleAuthOptions, HookRegistrationOptions, InitOptions, JeevesComponentDescriptor, ManagedBlockStampOptions, ManagedMarkers, MemoryHygieneOptions, MemoryHygieneResult, ParseManagedResult, PlatformComponent, PluginApi, PluginApiUrlResolver, PluginLifecycleApi, PluginToolsetOptions, PromptBuildContext, PromptBuildEvent, PromptBuildHandler, PromptBuildResult, PromptContextOptions, PromptContextProvider, ResolvedCliConfig, ResolvedValue, RetryOptions, RunOptions, ServiceAccountFileConfig, ServiceManager, ServiceManagerOptions, ServiceState, SkillFrontmatter, SlackWorkspaceOptions, StatusHandler, StatusHandlerResult, StatusResponse, ToolDescriptor, ToolRegistrationOptions, ToolResult, VersionStamp, WorkspaceConfig, WorkspaceOptions };
|
|
1672
|
+
export type { AccountConfig, ConfigApplyHandler, ConfigApplyRequest, ConfigApplyResult, ConfigProvenance, ConfigQueryHandler, ConfigQueryResponse, CoreConfig, CreateStatusHandlerOptions, GoogleAuthOptions, HookRegistrationOptions, InitOptions, JeevesComponentDescriptor, ManagedBlockStampOptions, ManagedMarkers, MemoryHygieneOptions, MemoryHygieneResult, ParseManagedResult, PlatformComponent, PluginApi, PluginApiUrlResolver, PluginLifecycleApi, PluginToolsetOptions, PromptBuildContext, PromptBuildEvent, PromptBuildHandler, PromptBuildResult, PromptContextOptions, PromptContextProvider, ResolvedCliConfig, ResolvedValue, RetryOptions, RunOptions, ServiceAccountFileConfig, ServiceInstallResult, ServiceManager, ServiceManagerOptions, ServiceState, SkillFrontmatter, SlackWorkspaceOptions, StatusHandler, StatusHandlerResult, StatusResponse, ToolDescriptor, ToolRegistrationOptions, ToolResult, VersionStamp, WorkspaceConfig, WorkspaceOptions };
|
package/dist/index.js
CHANGED
|
@@ -142,14 +142,14 @@ const DEFAULT_PORTS = {
|
|
|
142
142
|
* Core library version, inlined at build time.
|
|
143
143
|
*
|
|
144
144
|
* @remarks
|
|
145
|
-
* The `0.6.0-
|
|
145
|
+
* The `0.6.0-7` placeholder is replaced by
|
|
146
146
|
* `@rollup/plugin-replace` during the build with the actual version
|
|
147
147
|
* from `package.json`. This ensures the correct version survives
|
|
148
148
|
* when consumers bundle core into their own dist (where runtime
|
|
149
149
|
* `import.meta.url`-based resolution would find the wrong package.json).
|
|
150
150
|
*/
|
|
151
151
|
/** The core library version from package.json (inlined at build time). */
|
|
152
|
-
const CORE_VERSION = '0.6.0-
|
|
152
|
+
const CORE_VERSION = '0.6.0-7';
|
|
153
153
|
|
|
154
154
|
/**
|
|
155
155
|
* Workspace and config root initialization.
|
|
@@ -1081,12 +1081,130 @@ async function postJson(url, body) {
|
|
|
1081
1081
|
});
|
|
1082
1082
|
}
|
|
1083
1083
|
|
|
1084
|
+
/**
|
|
1085
|
+
* Linux systemd unit detection and state queries.
|
|
1086
|
+
*
|
|
1087
|
+
* @remarks
|
|
1088
|
+
* A service can be provisioned either as a system unit
|
|
1089
|
+
* (`/etc/systemd/system/<name>.service`, as jeeves-tools does on managed
|
|
1090
|
+
* instances) or as a user unit (`~/.config/systemd/user`, as
|
|
1091
|
+
* `createServiceManager` does elsewhere). An existing system unit always
|
|
1092
|
+
* wins: status reads it and core never creates a competing user unit.
|
|
1093
|
+
*
|
|
1094
|
+
* Every query here is unprivileged. Commands run through an injectable
|
|
1095
|
+
* `CommandExec` so the logic can be unit tested without systemd.
|
|
1096
|
+
*/
|
|
1097
|
+
/** Default `CommandExec`: `execSync` with piped stdio and a timeout. */
|
|
1098
|
+
const defaultExec = (cmd) => execSync(cmd, {
|
|
1099
|
+
encoding: 'utf-8',
|
|
1100
|
+
timeout: 30_000,
|
|
1101
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1102
|
+
}).trim();
|
|
1103
|
+
/**
|
|
1104
|
+
* Extract the most useful text from a failed command.
|
|
1105
|
+
*
|
|
1106
|
+
* @param err - Error thrown by a `CommandExec`.
|
|
1107
|
+
* @returns The command's stderr when present, else the error message.
|
|
1108
|
+
*/
|
|
1109
|
+
function execErrorDetail(err) {
|
|
1110
|
+
if (typeof err === 'object' && err !== null) {
|
|
1111
|
+
if ('stderr' in err) {
|
|
1112
|
+
const stderr = String(err.stderr).trim();
|
|
1113
|
+
if (stderr)
|
|
1114
|
+
return stderr;
|
|
1115
|
+
}
|
|
1116
|
+
if (err instanceof Error)
|
|
1117
|
+
return err.message;
|
|
1118
|
+
}
|
|
1119
|
+
return String(err);
|
|
1120
|
+
}
|
|
1121
|
+
/**
|
|
1122
|
+
* The systemd unit name for a service.
|
|
1123
|
+
*
|
|
1124
|
+
* @param serviceName - Service name, e.g. `jeeves-watcher`.
|
|
1125
|
+
* @returns The unit name, e.g. `jeeves-watcher.service`.
|
|
1126
|
+
*/
|
|
1127
|
+
function systemdUnitName(serviceName) {
|
|
1128
|
+
return `${serviceName}.service`;
|
|
1129
|
+
}
|
|
1130
|
+
/**
|
|
1131
|
+
* LoadStates meaning a system unit exists. A masked unit still counts: an
|
|
1132
|
+
* administrator disabled it, and a user unit must not replace it.
|
|
1133
|
+
*/
|
|
1134
|
+
const SYSTEM_UNIT_PRESENT = new Set(['loaded', 'masked']);
|
|
1135
|
+
/**
|
|
1136
|
+
* Whether a system-level systemd unit exists for the service.
|
|
1137
|
+
*
|
|
1138
|
+
* @remarks
|
|
1139
|
+
* Queries the system manager (no `--user`, no sudo):
|
|
1140
|
+
* `systemctl show <unit> --property=LoadState --value` prints `loaded`
|
|
1141
|
+
* (or `masked`) for an existing unit and `not-found` otherwise. Any failure (no
|
|
1142
|
+
* systemd, no system bus) means no usable system unit.
|
|
1143
|
+
*
|
|
1144
|
+
* @param serviceName - Service name.
|
|
1145
|
+
* @param exec - Command runner.
|
|
1146
|
+
* @returns True when a system unit is loaded.
|
|
1147
|
+
*/
|
|
1148
|
+
function hasSystemUnit(serviceName, exec = defaultExec) {
|
|
1149
|
+
try {
|
|
1150
|
+
const loadState = exec(`systemctl show ${systemdUnitName(serviceName)} --property=LoadState --value`);
|
|
1151
|
+
return SYSTEM_UNIT_PRESENT.has(loadState.trim());
|
|
1152
|
+
}
|
|
1153
|
+
catch {
|
|
1154
|
+
return false;
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
/**
|
|
1158
|
+
* Read a unit's active state and map it to a `ServiceState`.
|
|
1159
|
+
*
|
|
1160
|
+
* @param scopeFlag - `''` for the system manager, `'--user '` for the user manager.
|
|
1161
|
+
* @param unit - Unit name.
|
|
1162
|
+
* @param exec - Command runner.
|
|
1163
|
+
* @returns `running` when active, else `stopped`.
|
|
1164
|
+
*/
|
|
1165
|
+
function activeState(scopeFlag, unit, exec) {
|
|
1166
|
+
try {
|
|
1167
|
+
return exec(`systemctl ${scopeFlag}is-active ${unit}`) === 'active'
|
|
1168
|
+
? 'running'
|
|
1169
|
+
: 'stopped';
|
|
1170
|
+
}
|
|
1171
|
+
catch {
|
|
1172
|
+
// is-active exits non-zero for inactive/failed units.
|
|
1173
|
+
return 'stopped';
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
/**
|
|
1177
|
+
* Detect the state of a Linux systemd service.
|
|
1178
|
+
*
|
|
1179
|
+
* @remarks
|
|
1180
|
+
* System unit first (`systemctl is-active <unit>`), then the user unit
|
|
1181
|
+
* (`systemctl --user is-enabled` / `is-active`).
|
|
1182
|
+
*
|
|
1183
|
+
* @param serviceName - Service name.
|
|
1184
|
+
* @param exec - Command runner.
|
|
1185
|
+
* @returns The detected service state.
|
|
1186
|
+
*/
|
|
1187
|
+
function getSystemdServiceState(serviceName, exec = defaultExec) {
|
|
1188
|
+
const unit = systemdUnitName(serviceName);
|
|
1189
|
+
if (hasSystemUnit(serviceName, exec))
|
|
1190
|
+
return activeState('', unit, exec);
|
|
1191
|
+
try {
|
|
1192
|
+
exec(`systemctl --user is-enabled ${unit}`);
|
|
1193
|
+
}
|
|
1194
|
+
catch {
|
|
1195
|
+
return 'not_installed';
|
|
1196
|
+
}
|
|
1197
|
+
return activeState('--user ', unit, exec);
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1084
1200
|
/**
|
|
1085
1201
|
* Platform-aware service state detection.
|
|
1086
1202
|
*
|
|
1087
1203
|
* @remarks
|
|
1088
1204
|
* Detects whether a system service is installed and running.
|
|
1089
1205
|
* Delegates to NSSM (Windows), systemd (Linux), or launchd (macOS).
|
|
1206
|
+
* On Linux an existing system unit takes precedence over a user unit
|
|
1207
|
+
* (see `getSystemdServiceState`).
|
|
1090
1208
|
*/
|
|
1091
1209
|
/**
|
|
1092
1210
|
* Detect the state of a system service by name.
|
|
@@ -1101,7 +1219,7 @@ function getServiceState(serviceName) {
|
|
|
1101
1219
|
case 'darwin':
|
|
1102
1220
|
return getServiceStateMacOS(serviceName);
|
|
1103
1221
|
default:
|
|
1104
|
-
return
|
|
1222
|
+
return getSystemdServiceState(serviceName);
|
|
1105
1223
|
}
|
|
1106
1224
|
}
|
|
1107
1225
|
/**
|
|
@@ -1129,36 +1247,6 @@ function getServiceStateWindows(serviceName) {
|
|
|
1129
1247
|
return 'not_installed';
|
|
1130
1248
|
}
|
|
1131
1249
|
}
|
|
1132
|
-
/**
|
|
1133
|
-
* Linux: detect via systemd user services.
|
|
1134
|
-
* - `systemctl --user is-enabled {name}.service` exits non-zero = not installed
|
|
1135
|
-
* - `systemctl --user is-active {name}.service` returns "active" = running
|
|
1136
|
-
*/
|
|
1137
|
-
function getServiceStateLinux(serviceName) {
|
|
1138
|
-
try {
|
|
1139
|
-
execSync(`systemctl --user is-enabled ${serviceName}.service`, {
|
|
1140
|
-
encoding: 'utf-8',
|
|
1141
|
-
timeout: 5000,
|
|
1142
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1143
|
-
});
|
|
1144
|
-
}
|
|
1145
|
-
catch {
|
|
1146
|
-
return 'not_installed';
|
|
1147
|
-
}
|
|
1148
|
-
try {
|
|
1149
|
-
const output = execSync(`systemctl --user is-active ${serviceName}.service`, {
|
|
1150
|
-
encoding: 'utf-8',
|
|
1151
|
-
timeout: 5000,
|
|
1152
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1153
|
-
}).trim();
|
|
1154
|
-
if (output === 'active')
|
|
1155
|
-
return 'running';
|
|
1156
|
-
return 'stopped';
|
|
1157
|
-
}
|
|
1158
|
-
catch {
|
|
1159
|
-
return 'stopped';
|
|
1160
|
-
}
|
|
1161
|
-
}
|
|
1162
1250
|
/**
|
|
1163
1251
|
* macOS: detect via launchctl.
|
|
1164
1252
|
* - `launchctl list {name}` exits non-zero = not installed
|
|
@@ -1202,35 +1290,12 @@ function isExecError(err) {
|
|
|
1202
1290
|
}
|
|
1203
1291
|
|
|
1204
1292
|
/**
|
|
1205
|
-
*
|
|
1293
|
+
* Service manager contract and shared option resolution.
|
|
1206
1294
|
*
|
|
1207
1295
|
* @remarks
|
|
1208
|
-
*
|
|
1209
|
-
*
|
|
1210
|
-
* (Windows), systemd (Linux), or launchd (macOS) based on platform.
|
|
1296
|
+
* Shared by the per-platform managers built in `createServiceManager`
|
|
1297
|
+
* and `createLinuxManager`.
|
|
1211
1298
|
*/
|
|
1212
|
-
/** Exec helper that returns stdout. */
|
|
1213
|
-
function run$1(cmd) {
|
|
1214
|
-
return execSync(cmd, {
|
|
1215
|
-
encoding: 'utf-8',
|
|
1216
|
-
timeout: 30_000,
|
|
1217
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1218
|
-
}).trim();
|
|
1219
|
-
}
|
|
1220
|
-
/** Exec helper that suppresses errors and returns success boolean. */
|
|
1221
|
-
function runQuiet(cmd) {
|
|
1222
|
-
try {
|
|
1223
|
-
execSync(cmd, {
|
|
1224
|
-
encoding: 'utf-8',
|
|
1225
|
-
timeout: 30_000,
|
|
1226
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1227
|
-
});
|
|
1228
|
-
return true;
|
|
1229
|
-
}
|
|
1230
|
-
catch {
|
|
1231
|
-
return false;
|
|
1232
|
-
}
|
|
1233
|
-
}
|
|
1234
1299
|
/**
|
|
1235
1300
|
* Resolve the effective service name from options and descriptor.
|
|
1236
1301
|
*
|
|
@@ -1254,47 +1319,101 @@ function resolveConfigFilePath(descriptor, options) {
|
|
|
1254
1319
|
const configDir = getComponentConfigDir(descriptor.name);
|
|
1255
1320
|
return join(configDir, descriptor.configFileName);
|
|
1256
1321
|
}
|
|
1257
|
-
/**
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
}
|
|
1296
|
-
|
|
1322
|
+
/**
|
|
1323
|
+
* The default install result for a freshly installed service.
|
|
1324
|
+
*
|
|
1325
|
+
* @param svcName - Service name.
|
|
1326
|
+
* @returns A non-existing install result.
|
|
1327
|
+
*/
|
|
1328
|
+
function installedResult(svcName) {
|
|
1329
|
+
return { existing: false, message: `Service "${svcName}" installed.` };
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
/**
|
|
1333
|
+
* Guarded access to the systemd user and system managers.
|
|
1334
|
+
*
|
|
1335
|
+
* @remarks
|
|
1336
|
+
* - User scope: fail early with an actionable message when there is no user
|
|
1337
|
+
* bus (typical for `useradd --system` accounts without linger), instead of
|
|
1338
|
+
* surfacing systemctl's raw "Failed to connect to bus" error.
|
|
1339
|
+
* - System scope: run only `systemctl start|stop|restart <unit>` through
|
|
1340
|
+
* non-interactive sudo, which is exactly what the jeeves-tools sudoers rule
|
|
1341
|
+
* for the `jeeves` user allows (`/usr/bin/systemctl stop|start|restart|status *`).
|
|
1342
|
+
* Core never writes system units, reloads or enables them.
|
|
1343
|
+
*/
|
|
1344
|
+
/** Output patterns sudo prints when a non-interactive command is refused. */
|
|
1345
|
+
const SUDO_REFUSED = /password is required|not allowed to execute|may not run sudo|not in the sudoers/i;
|
|
1346
|
+
function userLabel(env) {
|
|
1347
|
+
return env.USER ?? env.LOGNAME ?? 'this user';
|
|
1348
|
+
}
|
|
1349
|
+
/**
|
|
1350
|
+
* Ensure a systemd user bus is reachable before any `systemctl --user` call.
|
|
1351
|
+
*
|
|
1352
|
+
* @param unit - Unit being managed (for the message).
|
|
1353
|
+
* @param deps - Host dependencies.
|
|
1354
|
+
* @throws Error with the cause and remedies when no user bus is available.
|
|
1355
|
+
*/
|
|
1356
|
+
function assertUserBus(unit, deps) {
|
|
1357
|
+
const user = userLabel(deps.env);
|
|
1358
|
+
const noBus = (reason, cause) => new Error([
|
|
1359
|
+
`Cannot manage ${unit} as a systemd user unit: ${reason}.`,
|
|
1360
|
+
`No system unit named ${unit} exists either.`,
|
|
1361
|
+
'Fix one of:',
|
|
1362
|
+
`(1) have an administrator provision ${unit} as a system unit in /etc/systemd/system; it is then detected and managed with "sudo -n systemctl";`,
|
|
1363
|
+
`(2) enable a user manager with "sudo loginctl enable-linger ${user}" and retry from a session where XDG_RUNTIME_DIR is set.`,
|
|
1364
|
+
].join(' '), { cause });
|
|
1365
|
+
if (!deps.env.XDG_RUNTIME_DIR && !deps.env.DBUS_SESSION_BUS_ADDRESS) {
|
|
1366
|
+
throw noBus(`there is no systemd user bus for "${user}" (XDG_RUNTIME_DIR is not set)`);
|
|
1367
|
+
}
|
|
1368
|
+
try {
|
|
1369
|
+
deps.exec('systemctl --user show-environment');
|
|
1370
|
+
}
|
|
1371
|
+
catch (err) {
|
|
1372
|
+
throw noBus(`the systemd user bus for "${user}" is unreachable (${execErrorDetail(err)})`, err);
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
/**
|
|
1376
|
+
* Run a lifecycle verb against an existing system unit.
|
|
1377
|
+
*
|
|
1378
|
+
* @remarks
|
|
1379
|
+
* Uses `sudo -n systemctl <verb> <unit>` (plain `systemctl` when root).
|
|
1380
|
+
* `-n` makes sudo fail instead of prompting for a password.
|
|
1381
|
+
*
|
|
1382
|
+
* @param verb - `start`, `stop` or `restart`.
|
|
1383
|
+
* @param unit - System unit name.
|
|
1384
|
+
* @param deps - Host dependencies.
|
|
1385
|
+
* @throws Error naming the missing sudoers rule when sudo refuses.
|
|
1386
|
+
*/
|
|
1387
|
+
function runSystemVerb(verb, unit, deps) {
|
|
1388
|
+
const root = deps.isRoot();
|
|
1389
|
+
const cmd = root
|
|
1390
|
+
? `systemctl ${verb} ${unit}`
|
|
1391
|
+
: `sudo -n systemctl ${verb} ${unit}`;
|
|
1392
|
+
try {
|
|
1393
|
+
deps.exec(cmd);
|
|
1394
|
+
}
|
|
1395
|
+
catch (err) {
|
|
1396
|
+
const detail = execErrorDetail(err);
|
|
1397
|
+
if (!root && SUDO_REFUSED.test(detail)) {
|
|
1398
|
+
throw new Error(`${unit} is a system unit and "${cmd}" was refused: passwordless sudo for "/usr/bin/systemctl ${verb} *" is not granted to "${userLabel(deps.env)}" (${detail}). Ask an administrator to run "sudo systemctl ${verb} ${unit}" or to grant that sudoers rule.`, { cause: err });
|
|
1399
|
+
}
|
|
1400
|
+
throw new Error(`"${cmd}" failed: ${detail}`, { cause: err });
|
|
1401
|
+
}
|
|
1297
1402
|
}
|
|
1403
|
+
|
|
1404
|
+
/**
|
|
1405
|
+
* Linux systemd service manager.
|
|
1406
|
+
*
|
|
1407
|
+
* @remarks
|
|
1408
|
+
* Two scopes:
|
|
1409
|
+
* - **System unit present** (e.g. `/etc/systemd/system/jeeves-watcher.service`
|
|
1410
|
+
* provisioned by jeeves-tools): install is a no-op that reports the unit,
|
|
1411
|
+
* uninstall refuses, status reads the system unit, and start/stop/restart
|
|
1412
|
+
* run `sudo -n systemctl <verb> <unit>`. Core never creates a competing
|
|
1413
|
+
* user unit.
|
|
1414
|
+
* - **Otherwise**: a user unit in `~/.config/systemd/user` managed with
|
|
1415
|
+
* `systemctl --user`, after checking that a user bus exists.
|
|
1416
|
+
*/
|
|
1298
1417
|
/**
|
|
1299
1418
|
* Generate a systemd user unit file.
|
|
1300
1419
|
*
|
|
@@ -1303,7 +1422,6 @@ function createWindowsManager(descriptor) {
|
|
|
1303
1422
|
* @returns Unit file content.
|
|
1304
1423
|
*/
|
|
1305
1424
|
function buildSystemdUnit(svcName, cmdArgs) {
|
|
1306
|
-
const execStart = cmdArgs.join(' ');
|
|
1307
1425
|
return [
|
|
1308
1426
|
'[Unit]',
|
|
1309
1427
|
`Description=${svcName}`,
|
|
@@ -1311,7 +1429,7 @@ function buildSystemdUnit(svcName, cmdArgs) {
|
|
|
1311
1429
|
'',
|
|
1312
1430
|
'[Service]',
|
|
1313
1431
|
'Type=simple',
|
|
1314
|
-
`ExecStart=${
|
|
1432
|
+
`ExecStart=${cmdArgs.join(' ')}`,
|
|
1315
1433
|
'Restart=on-failure',
|
|
1316
1434
|
'RestartSec=5',
|
|
1317
1435
|
'',
|
|
@@ -1319,42 +1437,160 @@ function buildSystemdUnit(svcName, cmdArgs) {
|
|
|
1319
1437
|
'WantedBy=default.target',
|
|
1320
1438
|
].join('\n');
|
|
1321
1439
|
}
|
|
1322
|
-
/**
|
|
1323
|
-
function
|
|
1324
|
-
|
|
1440
|
+
/** Default host dependencies. */
|
|
1441
|
+
function defaultDeps() {
|
|
1442
|
+
return {
|
|
1443
|
+
exec: defaultExec,
|
|
1444
|
+
env: process.env,
|
|
1445
|
+
isRoot: () => process.getuid?.() === 0,
|
|
1446
|
+
};
|
|
1447
|
+
}
|
|
1448
|
+
/**
|
|
1449
|
+
* Build a Linux systemd service manager.
|
|
1450
|
+
*
|
|
1451
|
+
* @param descriptor - Component descriptor.
|
|
1452
|
+
* @param deps - Host dependencies (defaults to the real host).
|
|
1453
|
+
* @param unitDir - User unit directory (defaults to `~/.config/systemd/user`).
|
|
1454
|
+
* @returns A `ServiceManager` for Linux.
|
|
1455
|
+
*/
|
|
1456
|
+
function createLinuxManager(descriptor, deps = defaultDeps(), unitDir = join(homedir(), '.config', 'systemd', 'user')) {
|
|
1457
|
+
const exec = deps.exec;
|
|
1325
1458
|
function unitPath(svcName) {
|
|
1326
|
-
return join(unitDir,
|
|
1459
|
+
return join(unitDir, systemdUnitName(svcName));
|
|
1460
|
+
}
|
|
1461
|
+
/** Run a lifecycle verb in whichever scope owns the unit. */
|
|
1462
|
+
function lifecycle(verb, options) {
|
|
1463
|
+
const svcName = resolveServiceName(descriptor, options);
|
|
1464
|
+
const unit = systemdUnitName(svcName);
|
|
1465
|
+
if (hasSystemUnit(svcName, exec)) {
|
|
1466
|
+
runSystemVerb(verb, unit, deps);
|
|
1467
|
+
return;
|
|
1468
|
+
}
|
|
1469
|
+
assertUserBus(unit, deps);
|
|
1470
|
+
exec(`systemctl --user ${verb} ${unit}`);
|
|
1327
1471
|
}
|
|
1328
1472
|
return {
|
|
1329
1473
|
install(options) {
|
|
1330
1474
|
const svcName = resolveServiceName(descriptor, options);
|
|
1331
|
-
const
|
|
1332
|
-
|
|
1475
|
+
const unit = systemdUnitName(svcName);
|
|
1476
|
+
if (hasSystemUnit(svcName, exec)) {
|
|
1477
|
+
return {
|
|
1478
|
+
existing: true,
|
|
1479
|
+
message: `Service "${svcName}" is already installed as system unit ${unit}; nothing to do. Manage it with start/stop/restart (sudo -n systemctl).`,
|
|
1480
|
+
};
|
|
1481
|
+
}
|
|
1482
|
+
assertUserBus(unit, deps);
|
|
1483
|
+
const cmdArgs = descriptor.startCommand(resolveConfigFilePath(descriptor, options));
|
|
1333
1484
|
mkdirSync(unitDir, { recursive: true });
|
|
1334
1485
|
writeFileSync(unitPath(svcName), buildSystemdUnit(svcName, cmdArgs));
|
|
1335
|
-
|
|
1336
|
-
|
|
1486
|
+
exec('systemctl --user daemon-reload');
|
|
1487
|
+
exec(`systemctl --user enable ${unit}`);
|
|
1488
|
+
return installedResult(svcName);
|
|
1337
1489
|
},
|
|
1338
1490
|
uninstall(options) {
|
|
1339
1491
|
const svcName = resolveServiceName(descriptor, options);
|
|
1340
|
-
|
|
1341
|
-
|
|
1492
|
+
const unit = systemdUnitName(svcName);
|
|
1493
|
+
if (hasSystemUnit(svcName, exec)) {
|
|
1494
|
+
throw new Error(`${unit} is a system unit provisioned outside core; core will not remove it. Ask an administrator to disable and delete it.`);
|
|
1495
|
+
}
|
|
1496
|
+
assertUserBus(unit, deps);
|
|
1497
|
+
const quiet = (cmd) => {
|
|
1498
|
+
try {
|
|
1499
|
+
exec(cmd);
|
|
1500
|
+
}
|
|
1501
|
+
catch {
|
|
1502
|
+
// Best effort: the unit may already be stopped or disabled.
|
|
1503
|
+
}
|
|
1504
|
+
};
|
|
1505
|
+
quiet(`systemctl --user stop ${unit}`);
|
|
1506
|
+
quiet(`systemctl --user disable ${unit}`);
|
|
1342
1507
|
const path = unitPath(svcName);
|
|
1343
1508
|
if (existsSync(path))
|
|
1344
1509
|
unlinkSync(path);
|
|
1345
|
-
|
|
1510
|
+
quiet('systemctl --user daemon-reload');
|
|
1511
|
+
},
|
|
1512
|
+
start(options) {
|
|
1513
|
+
lifecycle('start', options);
|
|
1514
|
+
},
|
|
1515
|
+
stop(options) {
|
|
1516
|
+
lifecycle('stop', options);
|
|
1517
|
+
},
|
|
1518
|
+
restart(options) {
|
|
1519
|
+
lifecycle('restart', options);
|
|
1520
|
+
},
|
|
1521
|
+
status(options) {
|
|
1522
|
+
return getSystemdServiceState(resolveServiceName(descriptor, options), exec);
|
|
1523
|
+
},
|
|
1524
|
+
};
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
/**
|
|
1528
|
+
* Factory for platform-aware service lifecycle management.
|
|
1529
|
+
*
|
|
1530
|
+
* @remarks
|
|
1531
|
+
* Produces a `ServiceManager` that handles install, uninstall, start,
|
|
1532
|
+
* stop, restart, and status for system services. Delegates to NSSM
|
|
1533
|
+
* (Windows), systemd (Linux), or launchd (macOS) based on platform.
|
|
1534
|
+
* On Linux an existing system unit is detected and managed in place
|
|
1535
|
+
* (see `createLinuxManager`).
|
|
1536
|
+
*/
|
|
1537
|
+
/** Exec helper that returns stdout. */
|
|
1538
|
+
function run$1(cmd) {
|
|
1539
|
+
return execSync(cmd, {
|
|
1540
|
+
encoding: 'utf-8',
|
|
1541
|
+
timeout: 30_000,
|
|
1542
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1543
|
+
}).trim();
|
|
1544
|
+
}
|
|
1545
|
+
/** Exec helper that suppresses errors and returns success boolean. */
|
|
1546
|
+
function runQuiet(cmd) {
|
|
1547
|
+
try {
|
|
1548
|
+
execSync(cmd, {
|
|
1549
|
+
encoding: 'utf-8',
|
|
1550
|
+
timeout: 30_000,
|
|
1551
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1552
|
+
});
|
|
1553
|
+
return true;
|
|
1554
|
+
}
|
|
1555
|
+
catch {
|
|
1556
|
+
return false;
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
/** Build a Windows NSSM service manager. */
|
|
1560
|
+
function createWindowsManager(descriptor) {
|
|
1561
|
+
return {
|
|
1562
|
+
install(options) {
|
|
1563
|
+
const svcName = resolveServiceName(descriptor, options);
|
|
1564
|
+
const cfgPath = resolveConfigFilePath(descriptor, options);
|
|
1565
|
+
const cmdArgs = descriptor.startCommand(cfgPath);
|
|
1566
|
+
const appPath = cmdArgs[0];
|
|
1567
|
+
const appArgs = cmdArgs.slice(1).join(' ');
|
|
1568
|
+
run$1(`nssm install ${svcName} ${appPath}`);
|
|
1569
|
+
if (appArgs) {
|
|
1570
|
+
run$1(`nssm set ${svcName} AppParameters ${appArgs}`);
|
|
1571
|
+
}
|
|
1572
|
+
run$1(`nssm set ${svcName} AppStdout ${join(homedir(), `${svcName}.log`)}`);
|
|
1573
|
+
run$1(`nssm set ${svcName} AppStderr ${join(homedir(), `${svcName}.log`)}`);
|
|
1574
|
+
run$1(`nssm set ${svcName} AppRotateFiles 1`);
|
|
1575
|
+
run$1(`nssm set ${svcName} AppRotateBytes 1048576`);
|
|
1576
|
+
return installedResult(svcName);
|
|
1577
|
+
},
|
|
1578
|
+
uninstall(options) {
|
|
1579
|
+
const svcName = resolveServiceName(descriptor, options);
|
|
1580
|
+
runQuiet(`nssm stop ${svcName}`);
|
|
1581
|
+
run$1(`nssm remove ${svcName} confirm`);
|
|
1346
1582
|
},
|
|
1347
1583
|
start(options) {
|
|
1348
1584
|
const svcName = resolveServiceName(descriptor, options);
|
|
1349
|
-
run$1(`
|
|
1585
|
+
run$1(`nssm start ${svcName}`);
|
|
1350
1586
|
},
|
|
1351
1587
|
stop(options) {
|
|
1352
1588
|
const svcName = resolveServiceName(descriptor, options);
|
|
1353
|
-
run$1(`
|
|
1589
|
+
run$1(`nssm stop ${svcName}`);
|
|
1354
1590
|
},
|
|
1355
1591
|
restart(options) {
|
|
1356
1592
|
const svcName = resolveServiceName(descriptor, options);
|
|
1357
|
-
run$1(`
|
|
1593
|
+
run$1(`nssm restart ${svcName}`);
|
|
1358
1594
|
},
|
|
1359
1595
|
status(options) {
|
|
1360
1596
|
const svcName = resolveServiceName(descriptor, options);
|
|
@@ -1408,6 +1644,7 @@ function createMacOSManager(descriptor) {
|
|
|
1408
1644
|
const cmdArgs = descriptor.startCommand(cfgPath);
|
|
1409
1645
|
mkdirSync(agentsDir, { recursive: true });
|
|
1410
1646
|
writeFileSync(plistPath(svcName), buildLaunchdPlist(svcName, cmdArgs));
|
|
1647
|
+
return installedResult(svcName);
|
|
1411
1648
|
},
|
|
1412
1649
|
uninstall(options) {
|
|
1413
1650
|
const svcName = resolveServiceName(descriptor, options);
|
|
@@ -1624,8 +1861,11 @@ function createServiceCli(descriptor) {
|
|
|
1624
1861
|
.option('-n, --name <name>', 'Service name', defaultServiceName)
|
|
1625
1862
|
.action((opts) => {
|
|
1626
1863
|
try {
|
|
1627
|
-
svcManager.install({
|
|
1628
|
-
|
|
1864
|
+
const result = svcManager.install({
|
|
1865
|
+
name: opts.name,
|
|
1866
|
+
configPath: opts.config,
|
|
1867
|
+
});
|
|
1868
|
+
console.log(result.message);
|
|
1629
1869
|
}
|
|
1630
1870
|
catch (err) {
|
|
1631
1871
|
handleCommandError('Install', err);
|
|
@@ -2595,11 +2835,12 @@ function createPluginToolset(descriptor, options) {
|
|
|
2595
2835
|
const state = svcManager.status();
|
|
2596
2836
|
return Promise.resolve(ok({ service: name, state }));
|
|
2597
2837
|
}
|
|
2838
|
+
if (action === 'install') {
|
|
2839
|
+
const { existing, message } = svcManager.install();
|
|
2840
|
+
return Promise.resolve(ok({ service: name, action, success: true, existing, message }));
|
|
2841
|
+
}
|
|
2598
2842
|
// Call the appropriate method
|
|
2599
2843
|
const methodMap = {
|
|
2600
|
-
install: () => {
|
|
2601
|
-
svcManager.install();
|
|
2602
|
-
},
|
|
2603
2844
|
uninstall: () => {
|
|
2604
2845
|
svcManager.uninstall();
|
|
2605
2846
|
},
|
package/package.json
CHANGED
|
@@ -96,8 +96,7 @@
|
|
|
96
96
|
"after:init": [
|
|
97
97
|
"npm run lint",
|
|
98
98
|
"npm run test",
|
|
99
|
-
"npm run knip"
|
|
100
|
-
"npm run build"
|
|
99
|
+
"npm run knip"
|
|
101
100
|
],
|
|
102
101
|
"after:release": [
|
|
103
102
|
"git switch -c release/${version}",
|
|
@@ -105,6 +104,7 @@
|
|
|
105
104
|
"git switch ${branchName}"
|
|
106
105
|
],
|
|
107
106
|
"after:bump": [
|
|
107
|
+
"npm run build",
|
|
108
108
|
"npx git-cliff -o CHANGELOG.md",
|
|
109
109
|
"npm run docs",
|
|
110
110
|
"git add CHANGELOG.md"
|
|
@@ -133,7 +133,7 @@
|
|
|
133
133
|
},
|
|
134
134
|
"type": "module",
|
|
135
135
|
"types": "dist/index.d.ts",
|
|
136
|
-
"version": "0.6.0-
|
|
136
|
+
"version": "0.6.0-7",
|
|
137
137
|
"allowScripts": {
|
|
138
138
|
"lefthook": true
|
|
139
139
|
}
|