@push.rocks/smartdaemon 2.22.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,7 @@
1
1
  import * as plugins from './smartdaemon.plugins.js';
2
2
  import * as paths from './smartdaemon.paths.js';
3
3
  import { SmartDaemon } from './smartdaemon.classes.smartdaemon.js';
4
+ import { SmartDaemonCommandError } from './smartdaemon.classes.commanderror.js';
4
5
  import { SmartDaemonService, } from './smartdaemon.classes.service.js';
5
6
  export class SmartDaemonSystemdManager {
6
7
  // STATIC
@@ -16,6 +17,7 @@ export class SmartDaemonSystemdManager {
16
17
  }
17
18
  constructor(smartdaemonRefArg, sudoPasswordArg) {
18
19
  this.shouldExecute = false;
20
+ /** Whether this process runs as root; set by the first privileged operation's check. */
19
21
  this.isRoot = false;
20
22
  this.smartdaemonRef = smartdaemonRefArg;
21
23
  this.smartshellInstance = new plugins.smartshell.Smartshell({
@@ -23,18 +25,27 @@ export class SmartDaemonSystemdManager {
23
25
  });
24
26
  this.smartFs = new plugins.smartfs.SmartFs(new plugins.smartfs.SmartFsProviderNode());
25
27
  this.sudoPassword = sudoPasswordArg;
26
- // Check if we're running as root
27
- this.checkIsRoot().then(isRoot => {
28
- this.isRoot = isRoot;
29
- if (!isRoot) {
30
- console.log('Not running as root. Sudo will be used for privileged operations.');
31
- }
32
- });
33
28
  }
34
29
  async checkIsRoot() {
35
30
  const result = await this.smartshellInstance.exec('id -u');
36
31
  return result.stdout.trim() === '0';
37
32
  }
33
+ /** Checks once whether this process runs as root and sets `isRoot`; later calls reuse that check. */
34
+ ensureRootStatus() {
35
+ if (!this.rootStatus) {
36
+ this.rootStatus = this.checkIsRoot().then((isRoot) => {
37
+ this.isRoot = isRoot;
38
+ if (!isRoot) {
39
+ console.log('Not running as root. Sudo will be used for privileged operations.');
40
+ }
41
+ });
42
+ // A failed check is not memoized, so the next privileged operation asks again.
43
+ this.rootStatus.catch(() => {
44
+ this.rootStatus = undefined;
45
+ });
46
+ }
47
+ return this.rootStatus;
48
+ }
38
49
  async checkElegibility() {
39
50
  if (plugins.os.platform() === 'linux') {
40
51
  this.shouldExecute = true;
@@ -45,30 +56,50 @@ export class SmartDaemonSystemdManager {
45
56
  }
46
57
  return this.shouldExecute;
47
58
  }
59
+ /**
60
+ * Runs a command, through sudo when this process is not root and the command does not start with
61
+ * `sudo` already. With a `sudoPassword`, sudo reads it from standard input (`sudo -k -S -p ''`):
62
+ * it never reaches argv, the environment or a terminal. Rejects with `SmartDaemonCommandError`
63
+ * when the command does not exit 0.
64
+ */
48
65
  async execute(commandArg) {
49
- if (await this.checkElegibility()) {
50
- // Only use sudo if we're not root and command doesn't already have sudo
51
- if (!this.isRoot && !commandArg.startsWith('sudo')) {
52
- commandArg = `sudo ${commandArg}`;
53
- if (this.sudoPassword) {
54
- // Use interactive PTY mode for password input
55
- const interactiveExec = await this.smartshellInstance.execInteractiveControlPty(commandArg);
56
- await interactiveExec.sendLine(this.sudoPassword);
57
- const result = await interactiveExec.finalPromise;
58
- return result;
59
- }
60
- }
61
- // Execute command (with or without sudo)
62
- try {
63
- await this.smartshellInstance.exec(commandArg);
64
- }
65
- catch (error) {
66
- if (!this.isRoot && error instanceof Error && error.message.includes('authentication')) {
67
- throw new Error('Sudo authentication failed. Please configure passwordless sudo or provide a sudo password.');
68
- }
69
- throw error;
70
- }
66
+ if (!(await this.checkElegibility())) {
67
+ return undefined;
68
+ }
69
+ await this.ensureRootStatus();
70
+ if (this.isRoot || commandArg.startsWith('sudo')) {
71
+ return await this.runChecked(commandArg);
72
+ }
73
+ if (!this.sudoPassword) {
74
+ return await this.runChecked(`sudo ${commandArg}`);
75
+ }
76
+ // -k ignores cached credentials, so sudo reads the password line whenever its policy asks for
77
+ // one; ending stdin right after it makes a rejected password fail instead of waiting for a retry.
78
+ const sudoCommand = `sudo -k -S -p '' -- ${commandArg}`;
79
+ const interactiveExec = await this.smartshellInstance.execInteractiveControl(sudoCommand);
80
+ let writeError;
81
+ try {
82
+ await interactiveExec.sendLine(this.sudoPassword);
83
+ }
84
+ catch (error) {
85
+ // sudo can exit before reading stdin (EPIPE); its exit status below decides the outcome.
86
+ writeError = error;
87
+ }
88
+ interactiveExec.endInput();
89
+ const result = await interactiveExec.finalPromise;
90
+ this.assertSucceeded(sudoCommand, result, writeError);
91
+ return result;
92
+ }
93
+ async runChecked(commandArg) {
94
+ const result = await this.smartshellInstance.exec(commandArg);
95
+ this.assertSucceeded(commandArg, result);
96
+ return result;
97
+ }
98
+ assertSucceeded(commandArg, resultArg, causeArg) {
99
+ if (resultArg.exitCode === 0 && !resultArg.signal) {
100
+ return;
71
101
  }
102
+ throw new SmartDaemonCommandError(commandArg, resultArg.exitCode, resultArg.signal, resultArg.stderr, causeArg === undefined ? undefined : { cause: causeArg });
72
103
  }
73
104
  /**
74
105
  * gets all services that are already present
@@ -101,6 +132,7 @@ export class SmartDaemonSystemdManager {
101
132
  if (await this.checkElegibility()) {
102
133
  const content = this.smartdaemonRef.templateManager.generateUnitFileForService(serviceArg);
103
134
  const targetPath = SmartDaemonSystemdManager.createFilePathFromServiceName(serviceArg.name);
135
+ await this.ensureRootStatus();
104
136
  if (this.isRoot) {
105
137
  // Direct write when running as root
106
138
  await this.smartFs.file(targetPath).encoding('utf8').mode(0o644).write(content);
@@ -117,6 +149,7 @@ export class SmartDaemonSystemdManager {
117
149
  async deleteService(serviceArg) {
118
150
  if (await this.checkElegibility()) {
119
151
  const filePath = SmartDaemonSystemdManager.createFilePathFromServiceName(serviceArg.name);
152
+ await this.ensureRootStatus();
120
153
  if (this.isRoot) {
121
154
  await this.smartFs.file(filePath).delete();
122
155
  }
@@ -145,4 +178,4 @@ export class SmartDaemonSystemdManager {
145
178
  }
146
179
  }
147
180
  }
148
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic21hcnRkYWVtb24uY2xhc3Nlcy5zeXN0ZW1kbWFuYWdlci5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3RzL3NtYXJ0ZGFlbW9uLmNsYXNzZXMuc3lzdGVtZG1hbmFnZXIudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxLQUFLLE9BQU8sTUFBTSwwQkFBMEIsQ0FBQztBQUNwRCxPQUFPLEtBQUssS0FBSyxNQUFNLHdCQUF3QixDQUFDO0FBQ2hELE9BQU8sRUFBRSxXQUFXLEVBQUUsTUFBTSxzQ0FBc0MsQ0FBQztBQUNuRSxPQUFPLEVBRUwsa0JBQWtCLEdBQ25CLE1BQU0sa0NBQWtDLENBQUM7QUFFMUMsTUFBTSxPQUFPLHlCQUF5QjtJQUNwQyxTQUFTO2FBQ00seUJBQW9CLEdBQUcsYUFBYSxBQUFoQixDQUFpQjtJQUU3QyxNQUFNLENBQUMsZ0NBQWdDLENBQUMsY0FBc0I7UUFDbkUsT0FBTyxHQUFHLHlCQUF5QixDQUFDLG9CQUFvQixJQUFJLGNBQWMsRUFBRSxDQUFDO0lBQy9FLENBQUM7SUFFTSxNQUFNLENBQUMsNkJBQTZCLENBQUMsY0FBc0I7UUFDaEUsT0FBTyxHQUFHLHlCQUF5QixDQUFDLGdDQUFnQyxDQUFDLGNBQWMsQ0FBQyxVQUFVLENBQUM7SUFDakcsQ0FBQztJQUVNLE1BQU0sQ0FBQyw2QkFBNkIsQ0FBQyxjQUFzQjtRQUNoRSxPQUFPLE9BQU8sQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUN0QixLQUFLLENBQUMsVUFBVSxFQUNoQix5QkFBeUIsQ0FBQyw2QkFBNkIsQ0FBQyxjQUFjLENBQUMsQ0FDeEUsQ0FBQztJQUNKLENBQUM7SUFXRCxZQUFZLGlCQUE4QixFQUFFLGVBQXdCO1FBSjdELGtCQUFhLEdBQVksS0FBSyxDQUFDO1FBQy9CLFdBQU0sR0FBWSxLQUFLLENBQUM7UUFJN0IsSUFBSSxDQUFDLGNBQWMsR0FBRyxpQkFBaUIsQ0FBQztRQUN4QyxJQUFJLENBQUMsa0JBQWtCLEdBQUcsSUFBSSxPQUFP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
181
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic21hcnRkYWVtb24uY2xhc3Nlcy5zeXN0ZW1kbWFuYWdlci5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3RzL3NtYXJ0ZGFlbW9uLmNsYXNzZXMuc3lzdGVtZG1hbmFnZXIudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxLQUFLLE9BQU8sTUFBTSwwQkFBMEIsQ0FBQztBQUNwRCxPQUFPLEtBQUssS0FBSyxNQUFNLHdCQUF3QixDQUFDO0FBQ2hELE9BQU8sRUFBRSxXQUFXLEVBQUUsTUFBTSxzQ0FBc0MsQ0FBQztBQUNuRSxPQUFPLEVBQUUsdUJBQXVCLEVBQUUsTUFBTSx1Q0FBdUMsQ0FBQztBQUNoRixPQUFPLEVBRUwsa0JBQWtCLEdBQ25CLE1BQU0sa0NBQWtDLENBQUM7QUFFMUMsTUFBTSxPQUFPLHlCQUF5QjtJQUNwQyxTQUFTO2FBQ00seUJBQW9CLEdBQUcsYUFBYSxBQUFoQixDQUFpQjtJQUU3QyxNQUFNLENBQUMsZ0NBQWdDLENBQUMsY0FBc0I7UUFDbkUsT0FBTyxHQUFHLHlCQUF5QixDQUFDLG9CQUFvQixJQUFJLGNBQWMsRUFBRSxDQUFDO0lBQy9FLENBQUM7SUFFTSxNQUFNLENBQUMsNkJBQTZCLENBQUMsY0FBc0I7UUFDaEUsT0FBTyxHQUFHLHlCQUF5QixDQUFDLGdDQUFnQyxDQUFDLGNBQWMsQ0FBQyxVQUFVLENBQUM7SUFDakcsQ0FBQztJQUVNLE1BQU0sQ0FBQyw2QkFBNkIsQ0FBQyxjQUFzQjtRQUNoRSxPQUFPLE9BQU8sQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUN0QixLQUFLLENBQUMsVUFBVSxFQUNoQix5QkFBeUIsQ0FBQyw2QkFBNkIsQ0FBQyxjQUFjLENBQUMsQ0FDeEUsQ0FBQztJQUNKLENBQUM7SUFhRCxZQUFZLGlCQUE4QixFQUFFLGVBQXdCO1FBTjdELGtCQUFhLEdBQVksS0FBSyxDQUFDO1FBQ3RDLHdGQUF3RjtRQUNqRixXQUFNLEdBQVksS0FBSyxDQUFDO1FBSzdCLElBQUksQ0FBQyxj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
@@ -5,13 +5,12 @@ import { constants as fsConstants } from 'node:fs';
5
5
  import * as crypto from 'node:crypto';
6
6
  import * as util from 'node:util';
7
7
  import * as os from 'node:os';
8
+ import * as timers from 'node:timers/promises';
8
9
  export type { Stats } from 'node:fs';
9
- export { path, childProcess, fs, fsConstants, crypto, util, os };
10
+ export { path, childProcess, fs, fsConstants, crypto, util, os, timers };
10
11
  import * as lik from '@push.rocks/lik';
11
12
  import * as smartfs from '@push.rocks/smartfs';
12
- import * as smartlog from '@push.rocks/smartlog';
13
- import * as smartlogDestinationLocal from '@push.rocks/smartlog/destination-local';
14
13
  import * as smartmarkdown from '@push.rocks/smartmarkdown/iso';
15
14
  import * as smartpath from '@push.rocks/smartpath';
16
15
  import * as smartshell from '@push.rocks/smartshell';
17
- export { lik, smartfs, smartlog, smartlogDestinationLocal, smartmarkdown, smartpath, smartshell, };
16
+ export { lik, smartfs, smartmarkdown, smartpath, smartshell, };
@@ -6,15 +6,14 @@ import { constants as fsConstants } from 'node:fs';
6
6
  import * as crypto from 'node:crypto';
7
7
  import * as util from 'node:util';
8
8
  import * as os from 'node:os';
9
- export { path, childProcess, fs, fsConstants, crypto, util, os };
9
+ import * as timers from 'node:timers/promises';
10
+ export { path, childProcess, fs, fsConstants, crypto, util, os, timers };
10
11
  // @pushrocks scope
11
12
  import * as lik from '@push.rocks/lik';
12
13
  import * as smartfs from '@push.rocks/smartfs';
13
- import * as smartlog from '@push.rocks/smartlog';
14
- import * as smartlogDestinationLocal from '@push.rocks/smartlog/destination-local';
15
14
  import * as smartmarkdown from '@push.rocks/smartmarkdown/iso';
16
15
  import * as smartpath from '@push.rocks/smartpath';
17
16
  import * as smartshell from '@push.rocks/smartshell';
18
- export { lik, smartfs, smartlog, smartlogDestinationLocal, smartmarkdown, smartpath, smartshell, };
17
+ export { lik, smartfs, smartmarkdown, smartpath, smartshell, };
19
18
  // third party
20
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic21hcnRkYWVtb24ucGx1Z2lucy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3RzL3NtYXJ0ZGFlbW9uLnBsdWdpbnMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsb0JBQW9CO0FBQ3BCLE9BQU8sS0FBSyxJQUFJLE1BQU0sTUFBTSxDQUFDO0FBQzdCLE9BQU8sS0FBSyxZQUFZLE1BQU0sb0JBQW9CLENBQUM7QUFDbkQsT0FBTyxLQUFLLEVBQUUsTUFBTSxrQkFBa0IsQ0FBQztBQUN2QyxPQUFPLEVBQUUsU0FBUyxJQUFJLFdBQVcsRUFBRSxNQUFNLFNBQVMsQ0FBQztBQUNuRCxPQUFPLEtBQUssTUFBTSxNQUFNLGFBQWEsQ0FBQztBQUN0QyxPQUFPLEtBQUssSUFBSSxNQUFNLFdBQVcsQ0FBQztBQUNsQyxPQUFPLEtBQUssRUFBRSxNQUFNLFNBQVMsQ0FBQztBQUc5QixPQUFPLEVBQUUsSUFBSSxFQUFFLFlBQVksRUFBRSxFQUFFLEVBQUUsV0FBVyxFQUFFLE1BQU0sRUFBRSxJQUFJLEVBQUUsRUFBRSxFQUFFLENBQUM7QUFFakUsbUJBQW1CO0FBQ25CLE9BQU8sS0FBSyxHQUFHLE1BQU0saUJBQWlCLENBQUM7QUFDdkMsT0FBTyxLQUFLLE9BQU8sTUFBTSxxQkFBcUIsQ0FBQztBQUMvQyxPQUFPLEtBQUssUUFBUSxNQUFNLHNCQUFzQixDQUFDO0FBQ2pELE9BQU8sS0FBSyx3QkFBd0IsTUFBTSx3Q0FBd0MsQ0FBQztBQUNuRixPQUFPLEtBQUssYUFBYSxNQUFNLCtCQUErQixDQUFDO0FBQy9ELE9BQU8sS0FBSyxTQUFTLE1BQU0sdUJBQXVCLENBQUM7QUFDbkQsT0FBTyxLQUFLLFVBQVUsTUFBTSx3QkFBd0IsQ0FBQztBQUVyRCxPQUFPLEVBQ0wsR0FBRyxFQUNILE9BQU8sRUFDUCxRQUFRLEVBQ1Isd0JBQXdCLEVBQ3hCLGFBQWEsRUFDYixTQUFTLEVBQ1QsVUFBVSxHQUNYLENBQUM7QUFFRixjQUFjIn0=
19
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic21hcnRkYWVtb24ucGx1Z2lucy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3RzL3NtYXJ0ZGFlbW9uLnBsdWdpbnMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsb0JBQW9CO0FBQ3BCLE9BQU8sS0FBSyxJQUFJLE1BQU0sTUFBTSxDQUFDO0FBQzdCLE9BQU8sS0FBSyxZQUFZLE1BQU0sb0JBQW9CLENBQUM7QUFDbkQsT0FBTyxLQUFLLEVBQUUsTUFBTSxrQkFBa0IsQ0FBQztBQUN2QyxPQUFPLEVBQUUsU0FBUyxJQUFJLFdBQVcsRUFBRSxNQUFNLFNBQVMsQ0FBQztBQUNuRCxPQUFPLEtBQUssTUFBTSxNQUFNLGFBQWEsQ0FBQztBQUN0QyxPQUFPLEtBQUssSUFBSSxNQUFNLFdBQVcsQ0FBQztBQUNsQyxPQUFPLEtBQUssRUFBRSxNQUFNLFNBQVMsQ0FBQztBQUM5QixPQUFPLEtBQUssTUFBTSxNQUFNLHNCQUFzQixDQUFDO0FBRy9DLE9BQU8sRUFBRSxJQUFJLEVBQUUsWUFBWSxFQUFFLEVBQUUsRUFBRSxXQUFXLEVBQUUsTUFBTSxFQUFFLElBQUksRUFBRSxFQUFFLEVBQUUsTUFBTSxFQUFFLENBQUM7QUFFekUsbUJBQW1CO0FBQ25CLE9BQU8sS0FBSyxHQUFHLE1BQU0saUJBQWlCLENBQUM7QUFDdkMsT0FBTyxLQUFLLE9BQU8sTUFBTSxxQkFBcUIsQ0FBQztBQUMvQyxPQUFPLEtBQUssYUFBYSxNQUFNLCtCQUErQixDQUFDO0FBQy9ELE9BQU8sS0FBSyxTQUFTLE1BQU0sdUJBQXVCLENBQUM7QUFDbkQsT0FBTyxLQUFLLFVBQVUsTUFBTSx3QkFBd0IsQ0FBQztBQUVyRCxPQUFPLEVBQ0wsR0FBRyxFQUNILE9BQU8sRUFDUCxhQUFhLEVBQ2IsU0FBUyxFQUNULFVBQVUsR0FDWCxDQUFDO0FBRUYsY0FBYyJ9
@@ -15,6 +15,8 @@ export declare const fileDescriptorStorePreserveMajor = 254;
15
15
  export declare const outputAppendMajor = 240;
16
16
  /** Internal: the systemd major whose `systemctl clean` first releases a file descriptor store (`--what=fdstore`). */
17
17
  export declare const fileDescriptorStoreCleanMajor = 254;
18
+ /** Internal: the systemd major whose `systemctl kill` first takes `--kill-whom=` (formerly `--kill-who=`). */
19
+ export declare const killWhomMajor = 252;
18
20
  /**
19
21
  * Internal: the systemd major whose `systemd-run` first takes `--expand-environment` and reports a
20
22
  * started scope as `runInTransientScope()` reads it.
@@ -4,6 +4,8 @@ export const fileDescriptorStorePreserveMajor = 254;
4
4
  export const outputAppendMajor = 240;
5
5
  /** Internal: the systemd major whose `systemctl clean` first releases a file descriptor store (`--what=fdstore`). */
6
6
  export const fileDescriptorStoreCleanMajor = 254;
7
+ /** Internal: the systemd major whose `systemctl kill` first takes `--kill-whom=` (formerly `--kill-who=`). */
8
+ export const killWhomMajor = 252;
7
9
  /**
8
10
  * Internal: the systemd major whose `systemd-run` first takes `--expand-environment` and reports a
9
11
  * started scope as `runInTransientScope()` reads it.
@@ -22,4 +24,4 @@ export const parseManagerVersion = (stdout) => {
22
24
  const match = /^Version=(([1-9][0-9]{0,3})(?:[.~+^_-][\x21-\x7e]{0,251})?)\n$/.exec(stdout);
23
25
  return match ? Object.freeze({ major: Number(match[2]), version: match[1] }) : undefined;
24
26
  };
25
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic3lzdGVtZC5tYW5hZ2VydmVyc2lvbi5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3RzL3N5c3RlbWQubWFuYWdlcnZlcnNpb24udHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBWUEsa0dBQWtHO0FBQ2xHLE1BQU0sQ0FBQyxNQUFNLGdDQUFnQyxHQUFHLEdBQUcsQ0FBQztBQUVwRCx3R0FBd0c7QUFDeEcsTUFBTSxDQUFDLE1BQU0saUJBQWlCLEdBQUcsR0FBRyxDQUFDO0FBRXJDLHFIQUFxSDtBQUNySCxNQUFNLENBQUMsTUFBTSw2QkFBNkIsR0FBRyxHQUFHLENBQUM7QUFFakQ7OztHQUdHO0FBQ0gsTUFBTSxDQUFDLE1BQU0sbUJBQW1CLEdBQUcsR0FBRyxDQUFDO0FBRXZDLCtGQUErRjtBQUMvRixNQUFNLENBQUMsTUFBTSx1QkFBdUIsR0FBc0IsQ0FBQyxNQUFNLEVBQUUsb0JBQW9CLENBQUMsQ0FBQztBQUV6Rjs7Ozs7O0dBTUc7QUFDSCxNQUFNLENBQUMsTUFBTSxtQkFBbUIsR0FBRyxDQUFDLE1BQWMsRUFBc0MsRUFBRTtJQUN4RixNQUFNLEtBQUssR0FBRyxnRUFBZ0UsQ0FBQyxJQUFJLENBQUMsTUFBTSxDQUFDLENBQUM7SUFDNUYsT0FBTyxLQUFLLENBQUMsQ0FBQyxDQUFDLE1BQU0sQ0FBQyxNQUFNLENBQUMsRUFBRSxLQUFLLEVBQUUsTUFBTSxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUMsQ0FBQyxFQUFFLE9BQU8sRUFBRSxLQUFLLENBQUMsQ0FBQyxDQUFDLEVBQUUsQ0FBQyxDQUFDLENBQUMsQ0FBQyxTQUFTLENBQUM7QUFDM0YsQ0FBQyxDQUFDIn0=
27
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic3lzdGVtZC5tYW5hZ2VydmVyc2lvbi5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3RzL3N5c3RlbWQubWFuYWdlcnZlcnNpb24udHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBWUEsa0dBQWtHO0FBQ2xHLE1BQU0sQ0FBQyxNQUFNLGdDQUFnQyxHQUFHLEdBQUcsQ0FBQztBQUVwRCx3R0FBd0c7QUFDeEcsTUFBTSxDQUFDLE1BQU0saUJBQWlCLEdBQUcsR0FBRyxDQUFDO0FBRXJDLHFIQUFxSDtBQUNySCxNQUFNLENBQUMsTUFBTSw2QkFBNkIsR0FBRyxHQUFHLENBQUM7QUFFakQsOEdBQThHO0FBQzlHLE1BQU0sQ0FBQyxNQUFNLGFBQWEsR0FBRyxHQUFHLENBQUM7QUFFakM7OztHQUdHO0FBQ0gsTUFBTSxDQUFDLE1BQU0sbUJBQW1CLEdBQUcsR0FBRyxDQUFDO0FBRXZDLCtGQUErRjtBQUMvRixNQUFNLENBQUMsTUFBTSx1QkFBdUIsR0FBc0IsQ0FBQyxNQUFNLEVBQUUsb0JBQW9CLENBQUMsQ0FBQztBQUV6Rjs7Ozs7O0dBTUc7QUFDSCxNQUFNLENBQUMsTUFBTSxtQkFBbUIsR0FBRyxDQUFDLE1BQWMsRUFBc0MsRUFBRTtJQUN4RixNQUFNLEtBQUssR0FBRyxnRUFBZ0UsQ0FBQyxJQUFJLENBQUMsTUFBTSxDQUFDLENBQUM7SUFDNUYsT0FBTyxLQUFLLENBQUMsQ0FBQyxDQUFDLE1BQU0sQ0FBQyxNQUFNLENBQUMsRUFBRSxLQUFLLEVBQUUsTUFBTSxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUMsQ0FBQyxFQUFFLE9BQU8sRUFBRSxLQUFLLENBQUMsQ0FBQyxDQUFDLEVBQUUsQ0FBQyxDQUFDLENBQUMsQ0FBQyxTQUFTLENBQUM7QUFDM0YsQ0FBQyxDQUFDIn0=
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@push.rocks/smartdaemon",
3
- "version": "2.22.0",
3
+ "version": "3.1.0",
4
4
  "private": false,
5
5
  "description": "Start scripts as long running daemons and manage them.",
6
6
  "main": "dist_ts/index.js",
@@ -37,18 +37,15 @@
37
37
  },
38
38
  "homepage": "https://code.foss.global/push.rocks/smartdaemon",
39
39
  "devDependencies": {
40
- "@git.zone/tsbuild": "^6.0.2",
41
- "@git.zone/tsrun": "^3.0.3",
42
- "@git.zone/tstest": "^7.2.1",
43
- "@types/node": "^26.6.4"
40
+ "@git.zone/tsrepo": "^1.1.1",
41
+ "@types/node": "^26.6.5"
44
42
  },
45
43
  "dependencies": {
46
44
  "@push.rocks/lik": "^6.4.4",
47
45
  "@push.rocks/smartfs": "^1.5.1",
48
- "@push.rocks/smartlog": "^4.0.4",
49
46
  "@push.rocks/smartmarkdown": "^3.3.0",
50
47
  "@push.rocks/smartpath": "^6.0.0",
51
- "@push.rocks/smartshell": "^4.2.1"
48
+ "@push.rocks/smartshell": "^5.0.0"
52
49
  },
53
50
  "files": [
54
51
  "ts/**/*",
@@ -69,9 +66,9 @@
69
66
  "last 1 chrome versions"
70
67
  ],
71
68
  "scripts": {
72
- "test": "tstest test/ --verbose --timeout 60",
73
- "build": "tsbuild tsfolders --web",
69
+ "test": "tsrepo test test/ --verbose --timeout 60",
70
+ "build": "tsrepo build tsfolders --web",
74
71
  "format": "gitzone format",
75
- "buildDocs": "tsdoc"
72
+ "buildDocs": "tsrepo docs"
76
73
  }
77
74
  }
package/readme.md CHANGED
@@ -21,7 +21,7 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
21
21
  - 🔄 **Systemd Integration**: Full Linux systemd support for production-grade service management
22
22
  - 🛡️ **Auto-Recovery**: Built-in service restart on failure
23
23
  - 📝 **Declarative Service Definitions**: Simple, clear service configuration
24
- - 🔌 **Interactive & Non-Interactive Modes**: Support for both passwordless sudo and interactive authentication
24
+ - 🔌 **Passwordless or Password-Based Sudo**: Works with passwordless sudo, or hands a configured sudo password to sudo on standard input (no terminal or PTY needed)
25
25
  - 🎛️ **Complete Lifecycle Management**: Start, stop, enable, disable, and reload services with ease
26
26
 
27
27
  ## 📦 Installation
@@ -115,6 +115,20 @@ const service = await daemon.addService({
115
115
  await service.enable(); // Automatically uses sudo with password
116
116
  ```
117
117
 
118
+ Each privileged command runs as `sudo -k -S -p '' -- <command>`: sudo reads the password from
119
+ standard input, which is closed right after the password line, so a rejected password fails at
120
+ once instead of waiting for a retry. The password is never placed in argv, the environment or the
121
+ output, and no PTY (`node-pty`) is involved. `-k` makes sudo ask for the password every time and
122
+ leaves no cached credentials behind. If a sudoers rule lets the command run without a password
123
+ (`NOPASSWD`), sudo does not read standard input and the password line reaches the command's
124
+ standard input instead (`systemctl`, `mv`, `chmod` and `rm` never read it); leave `sudoPassword`
125
+ unset when sudo is passwordless.
126
+
127
+ A command that does not exit 0 rejects with `SmartDaemonCommandError` (`command`, `exitCode`,
128
+ `signal`, `stderr`), with or without a sudo password: a rejected password, a missing `sudo`
129
+ (exit code 127) or a failing `systemctl` call all surface this way. Besides `systemctl`, saving
130
+ and deleting a unit file as a non-root user runs `mv`, `chmod` and `rm` through sudo.
131
+
118
132
  ### Passwordless Sudo (Recommended for Production)
119
133
 
120
134
  For production environments, configure passwordless sudo for specific systemctl commands:
@@ -279,6 +293,49 @@ store needs systemd 254 or later: the call reads the manager's version first and
279
293
  manager with `SystemdUnitManagerTooOldError` (`manager_too_old`), an unreadable version with
280
294
  `version_unknown` and an unanswered read with `command_failed`, each without running the clean.
281
295
 
296
+ `kill(signal?, options?)` ends a unit's processes without a stop job: it sends `SIGKILL` (the
297
+ default) or `SIGTERM` to every process of the unit at once, its main and control process and every
298
+ other process in its control group (`systemctl kill --signal=<signal> --kill-whom=all`), then reads
299
+ the unit every 100 ms until it is at rest or about to restart, for at most `timeoutMs` (integer
300
+ milliseconds from 1 to 600000, default 30000). Use it where a `stop()` would wait out the unit's
301
+ `TimeoutStopSec=`, such as a unit whose stop already failed or overran; it does not run `ExecStop=`,
302
+ does not cancel or await a pending job, and never starts, enables, unmasks or resets the unit. It
303
+ resolves a frozen `{ outcome, state }`, where `state` is the snapshot that decided the outcome (for
304
+ `timeout`, the last one read):
305
+
306
+ | `outcome` | Meaning |
307
+ | --- | --- |
308
+ | `'stopped'`, `'failed'` | `systemdUnitQuiescence()`'s `rest`: no main or control process and no control group. A unit whose main process is gone while its control group still holds processes resolves only once the group is gone. A unit already at rest is signalled all the same (nothing receives it) and resolves at the first read |
309
+ | `'auto-restart'` | SubState `auto-restart`, or `auto-restart-queued` (systemd 254), without a main or control process: the unit waits for its `Restart=` timer or its queued restart. Its control group is not read here: with `RestartMode=direct` the manager keeps the emptied group |
310
+ | `'restarted'` | the manager began a new invocation (`InvocationID`) before a read saw the unit at rest: a restart delay shorter than the poll interval, or another client's start |
311
+ | `'timeout'` | none of these within `timeoutMs`; a read begun before the deadline is still awaited, each bounded by `commandTimeoutMs` |
312
+
313
+ A unit whose `LoadState` is not `loaded` in the read before the signal (`not-found`, `masked`,
314
+ `bad-setting`, `error`, ...) is refused with `command_failed` before any signal: that read makes the
315
+ manager load the unit object, and the manager may signal an improperly loaded unit, so such a unit
316
+ would otherwise resolve `stopped`. A refused signal and a `systemctl kill` that exceeded
317
+ `commandTimeoutMs` reject with `command_failed` too; after a timed-out call the signal may have been
318
+ sent, so inspect the unit. Once the manager accepted the signal, a failed read rejects with
319
+ `signalled_uninspected` and that read's error as `cause`. `signalled_uninspected` is a new member of
320
+ `SystemdUnitError['code']`: a consumer with an exhaustive `never` switch over the code must add a
321
+ case for it. `kill()` needs systemd 252 or later, the first whose `systemctl` takes
322
+ `--kill-whom=`: it reads the manager's version first and refuses an older manager with
323
+ `SystemdUnitManagerTooOldError`, an unreadable version with `version_unknown` and an unanswered read
324
+ with `command_failed`, each before any signal. A signal other than `'SIGKILL'` or `'SIGTERM'` and an
325
+ option other than `timeoutMs` reject with `invalid_options` before any command runs.
326
+
327
+ ```typescript
328
+ const killed = await unit.kill('SIGKILL', { timeoutMs: 30_000 });
329
+ if (killed.outcome === 'timeout') {
330
+ console.log('still running', killed.state.activeState, killed.state.controlGroup);
331
+ } else if (killed.outcome !== 'restarted') {
332
+ await unit.start();
333
+ }
334
+ ```
335
+
336
+ After an `auto-restart` outcome the start job stays queued until the unit's `RestartSec=` elapses,
337
+ so `start()` waits for it; a long `RestartSec=` can exceed `commandTimeoutMs`.
338
+
282
339
  `inspectConfiguration()` reports the selected `fragmentPath`, opaque `dropInPaths`
283
340
  list and `needsReload` flag. `reloadConfiguration()` explicitly reloads the manager's
284
341
  unit definitions globally and returns a fresh configuration snapshot. Neither
@@ -1398,7 +1455,8 @@ WantedBy=multi-user.target
1398
1455
 
1399
1456
  ### Permission Denied Errors
1400
1457
 
1401
- If you encounter "Interactive authentication required" errors:
1458
+ If an operation rejects with a `SmartDaemonCommandError` whose `stderr` reports that sudo needs a
1459
+ password or a terminal (for example "a terminal is required to read the password"):
1402
1460
 
1403
1461
  1. **Option 1**: Run your application with sudo
1404
1462
  ```bash
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@push.rocks/smartdaemon',
6
- version: '2.22.0',
6
+ version: '3.1.0',
7
7
  description: 'Start scripts as long running daemons and manage them.'
8
8
  }
@@ -12,6 +12,7 @@ import {
12
12
  type ISystemdManagerVersion,
13
13
  fileDescriptorStoreCleanMajor,
14
14
  fileDescriptorStorePreserveMajor,
15
+ killWhomMajor,
15
16
  managerVersionArguments,
16
17
  parseManagerVersion,
17
18
  } from './systemd.managerversion.js';
@@ -127,6 +128,49 @@ export interface ISystemdUnitJobOptions {
127
128
  wait?: boolean;
128
129
  }
129
130
 
131
+ /** The signals `SystemdUnit.kill()` sends. */
132
+ export type TSystemdUnitKillSignal = 'SIGKILL' | 'SIGTERM';
133
+
134
+ /** `SystemdUnit.kill()` options. */
135
+ export interface ISystemdUnitKillOptions {
136
+ /**
137
+ * How long to wait, after the manager accepted the signal, for the unit to come to rest: integer
138
+ * milliseconds from 1 to 600000. Defaults to 30000. A read begun before the deadline is still
139
+ * awaited; each read is bounded by `commandTimeoutMs`.
140
+ */
141
+ timeoutMs?: number;
142
+ }
143
+
144
+ /**
145
+ * How the unit stood when `SystemdUnit.kill()` stopped waiting. `stopped` and `failed` are
146
+ * `systemdUnitQuiescence()`'s `rest`: no main or control process and no control group.
147
+ * `auto-restart`: SubState `auto-restart` or `auto-restart-queued` (systemd 254) without a main or
148
+ * control process, waiting for its `Restart=` timer or its queued restart job. `restarted`: the
149
+ * manager started a new invocation of the unit (a restart delay shorter than the poll interval, or
150
+ * another client's start) before a read saw it at rest. `timeout`: none of these within `timeoutMs`.
151
+ */
152
+ export type TSystemdUnitKillOutcome = 'stopped' | 'failed' | 'auto-restart' | 'restarted' | 'timeout';
153
+
154
+ export interface ISystemdUnitKillResult {
155
+ readonly outcome: TSystemdUnitKillOutcome;
156
+ /** The snapshot that decided `outcome`; for `timeout`, the last one read before the deadline. */
157
+ readonly state: ISystemdUnitState;
158
+ }
159
+
160
+ /** Interval between the snapshots `kill()` reads while it waits. */
161
+ const killPollIntervalMs = 100;
162
+
163
+ /** Reads the outcome a kill has reached from one snapshot, or null while it has reached none. */
164
+ const killOutcome = (state: ISystemdUnitState, newInvocation: boolean): TSystemdUnitKillOutcome | null => {
165
+ const { rest } = systemdUnitQuiescence(state);
166
+ if (rest !== null) return rest;
167
+ // In auto-restart the manager may keep the unit's (empty) control group: with RestartMode=direct
168
+ // the unit never passes the inactive state that releases it, so ControlGroup is not read here.
169
+ if (state.mainPid === 0 && state.controlPid === 0 &&
170
+ (state.subState === 'auto-restart' || state.subState === 'auto-restart-queued')) return 'auto-restart';
171
+ return newInvocation ? 'restarted' : null;
172
+ };
173
+
130
174
  export interface ISystemdUnitConfigurationState {
131
175
  readonly unitName: string;
132
176
  readonly fragmentPath: string;
@@ -232,12 +276,15 @@ export class SystemdUnitError extends Error {
232
276
  * `version_unknown`: the manager answered, but its `Version` property is not one this package
233
277
  * reads, so a call that needs a minimum manager version refuses.
234
278
  * `manager_too_old`: always a `SystemdUnitManagerTooOldError`, naming both versions.
279
+ * `signalled_uninspected`: only `kill()`. The manager accepted the signal, and a snapshot read
280
+ * while waiting failed; `cause` is that inspection's error.
235
281
  */
236
282
  public readonly code:
237
283
  | 'invalid_options'
238
284
  | 'command_failed'
239
285
  | 'invalid_state'
240
286
  | 'released_uninspected'
287
+ | 'signalled_uninspected'
241
288
  | 'version_unknown'
242
289
  | 'manager_too_old',
243
290
  options?: ErrorOptions,
@@ -480,10 +527,18 @@ export class SystemdUnit {
480
527
 
481
528
  /** Snapshot only. Callers own application-specific admission and restart policy. */
482
529
  public async inspect(): Promise<ISystemdUnitState> {
530
+ return (await this.readState(properties)).state;
531
+ }
532
+
533
+ /**
534
+ * Reads one snapshot with `keys`: the snapshot properties, optionally followed by `InvocationID`,
535
+ * which is empty for a unit never started and otherwise 32 lowercase hex digits.
536
+ */
537
+ private async readState(keys: readonly string[]): Promise<{ state: ISystemdUnitState; invocationId: string }> {
483
538
  const stdout = await this.command([
484
539
  'show',
485
540
  this.unitName,
486
- `--property=${properties.join(',')}`,
541
+ `--property=${keys.join(',')}`,
487
542
  '--all',
488
543
  ]);
489
544
  const fields = new Map<string, string>();
@@ -491,13 +546,14 @@ export class SystemdUnit {
491
546
  for (const line of (stdout.endsWith('\n') ? stdout.slice(0, -1) : stdout).split('\n')) {
492
547
  const separator = line.indexOf('=');
493
548
  const key = line.slice(0, separator);
494
- if (separator < 1 || !(properties as readonly string[]).includes(key) || fields.has(key)) {
549
+ if (separator < 1 || !keys.includes(key) || fields.has(key)) {
495
550
  throw new SystemdUnitError('invalid_state');
496
551
  }
497
552
  fields.set(key, line.slice(separator + 1));
498
553
  }
499
554
  if (
500
- fields.size !== properties.length || fields.get('Id') !== this.unitName ||
555
+ fields.size !== keys.length || fields.get('Id') !== this.unitName ||
556
+ (keys.includes('InvocationID') && !/^(?:[0-9a-f]{32})?$/.test(fields.get('InvocationID') ?? '')) ||
501
557
  ['LoadState', 'ActiveState', 'SubState'].some((key) => !fields.get(key)) ||
502
558
  !/^[a-z][a-z-]{0,31}$/.test(fields.get('Result')!) ||
503
559
  ['MainPID', 'ControlPID', 'NFileDescriptorStore'].some((key) =>
@@ -506,7 +562,7 @@ export class SystemdUnit {
506
562
  // systemd prints the store's unsigned 32-bit count in decimal.
507
563
  Number(fields.get('NFileDescriptorStore')) > 4294967295
508
564
  ) throw new SystemdUnitError('invalid_state');
509
- return Object.freeze({
565
+ const state: ISystemdUnitState = Object.freeze({
510
566
  unitName: this.unitName,
511
567
  loadState: fields.get('LoadState')!,
512
568
  activeState: fields.get('ActiveState')!,
@@ -521,6 +577,7 @@ export class SystemdUnit {
521
577
  result: fields.get('Result')!,
522
578
  fileDescriptorStoreCount: Number(fields.get('NFileDescriptorStore')),
523
579
  });
580
+ return { state, invocationId: fields.get('InvocationID') ?? '' };
524
581
  }
525
582
 
526
583
  /**
@@ -585,6 +642,65 @@ export class SystemdUnit {
585
642
  return this.inspect();
586
643
  }
587
644
 
645
+ /**
646
+ * Sends `signal` (default `SIGKILL`) to every process of the unit at once, its main and control
647
+ * process and every other process in its control group (`systemctl kill --signal=<signal>
648
+ * --kill-whom=all`), then waits, bounded by `timeoutMs`, until the unit is at rest or about to
649
+ * restart, and resolves with the outcome and the snapshot that decided it. It does not queue a
650
+ * stop job and does not wait out `TimeoutStopSec=`; a pending job is neither cancelled nor
651
+ * awaited. It never starts, enables, unmasks or resets the unit.
652
+ *
653
+ * A unit already at rest is signalled all the same (nothing receives it) and resolves at the first
654
+ * read. A unit whose main process is gone while its control group still holds processes resolves
655
+ * only once the group is gone. A unit whose `LoadState` in the read before the signal is not
656
+ * `loaded` (`not-found`, `masked`, `bad-setting`, `error`, ...) is refused with `command_failed`
657
+ * before any signal, as are a refused signal and a `systemctl` call that exceeded
658
+ * `commandTimeoutMs`; after a timed-out call the signal may have been sent, so inspect the unit. Once the manager accepted the signal, a failed read
659
+ * rejects with `signalled_uninspected` and that read's error as `cause`.
660
+ *
661
+ * Needs systemd 252 or later, the first whose `systemctl` takes `--kill-whom=`: the manager's
662
+ * version is read first, and an older manager is refused with `SystemdUnitManagerTooOldError`, an
663
+ * unreadable version with `version_unknown` and an unanswered read with `command_failed`, each
664
+ * before any signal. A malformed signal or option rejects with `invalid_options` before any command.
665
+ */
666
+ public async kill(
667
+ signalArg: TSystemdUnitKillSignal = 'SIGKILL',
668
+ optionsArg?: ISystemdUnitKillOptions,
669
+ ): Promise<ISystemdUnitKillResult> {
670
+ if (
671
+ (signalArg !== 'SIGKILL' && signalArg !== 'SIGTERM') ||
672
+ (optionsArg !== undefined && (
673
+ typeof optionsArg !== 'object' || optionsArg === null ||
674
+ Object.keys(optionsArg).some((key) => key !== 'timeoutMs') ||
675
+ (optionsArg.timeoutMs !== undefined && (!Number.isSafeInteger(optionsArg.timeoutMs) ||
676
+ optionsArg.timeoutMs < 1 || optionsArg.timeoutMs > 600000))
677
+ ))
678
+ ) throw new SystemdUnitError('invalid_options');
679
+ const timeoutMs = optionsArg?.timeoutMs ?? 30000;
680
+ await this.requireManagerVersion(killWhomMajor);
681
+ const killKeys = [...properties, 'InvocationID'];
682
+ // The invocation before the signal, so a restart the wait could not observe at rest is reported.
683
+ const { state: before, invocationId } = await this.readState(killKeys);
684
+ // That read makes the manager load the unit object, and the manager may signal a unit it could
685
+ // not load properly; such a unit has no processes of its own to end, so it is refused here.
686
+ if (before.loadState !== 'loaded') throw new SystemdUnitError('command_failed');
687
+ await this.command(['kill', `--signal=${signalArg}`, '--kill-whom=all', this.unitName]);
688
+ const deadline = performance.now() + timeoutMs;
689
+ for (;;) {
690
+ let read: { state: ISystemdUnitState; invocationId: string };
691
+ try {
692
+ read = await this.readState(killKeys);
693
+ } catch (error) {
694
+ throw new SystemdUnitError('signalled_uninspected', { cause: error });
695
+ }
696
+ const outcome = killOutcome(read.state, read.invocationId !== invocationId);
697
+ if (outcome !== null) return Object.freeze({ outcome, state: read.state });
698
+ const remaining = deadline - performance.now();
699
+ if (remaining <= 0) return Object.freeze({ outcome: 'timeout', state: read.state });
700
+ await plugins.timers.setTimeout(Math.min(killPollIntervalMs, remaining));
701
+ }
702
+ }
703
+
588
704
  /** Reports the manager's selected definition without reading or changing its contents. */
589
705
  public async inspectConfiguration(): Promise<ISystemdUnitConfigurationState> {
590
706
  const keys = ['Id', 'FragmentPath', 'DropInPaths', 'NeedDaemonReload'];
package/ts/index.ts CHANGED
@@ -4,3 +4,4 @@ export * from './classes.systemdunit.js';
4
4
  export * from './classes.systemdservicedefinition.js';
5
5
  export * from './classes.systemdunitfile.js';
6
6
  export * from './classes.systemdtransientscope.js';
7
+ export * from './smartdaemon.classes.commanderror.js';
@@ -0,0 +1,23 @@
1
+ /**
2
+ * A privileged command run by `SmartDaemon` (through `systemdManager.execute()`) did not exit 0.
3
+ * The exit status alone decides failure; `stderr` carries what the command or sudo reported.
4
+ */
5
+ export class SmartDaemonCommandError extends Error {
6
+ constructor(
7
+ /** The command as run, including any `sudo` prefix. It never contains the sudo password. */
8
+ public readonly command: string,
9
+ /** The exit code; `1` when a signal ended the command. */
10
+ public readonly exitCode: number,
11
+ /** The signal that ended the command, if one did. */
12
+ public readonly signal: NodeJS.Signals | undefined,
13
+ /** The command's standard error, as captured. */
14
+ public readonly stderr: string,
15
+ /** `cause`: the stdin write error when sudo exited before reading the password. */
16
+ options?: ErrorOptions,
17
+ ) {
18
+ const reason = signal ? `was terminated by signal ${signal}` : `exited with code ${exitCode}`;
19
+ const detail = stderr.trim();
20
+ super(`Command "${command}" ${reason}${detail ? `: ${detail}` : '.'}`, options);
21
+ this.name = 'SmartDaemonCommandError';
22
+ }
23
+ }