unitup 0.1.5 → 0.3.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.
Files changed (55) hide show
  1. package/.agents/AGENTS.md +35 -0
  2. package/.agents/skills/multi-runtime-management/SKILL.md +80 -0
  3. package/.agents/skills/platform-service-adapters/SKILL.md +108 -0
  4. package/.agents/skills/unitup-development-and-qa/SKILL.md +88 -0
  5. package/.agents/skills/zero-downtime-deployment/SKILL.md +135 -0
  6. package/CHANGELOG.md +5 -3
  7. package/README.md +89 -8
  8. package/biome.json +0 -1
  9. package/docs/index.html +188 -234
  10. package/index.d.ts +467 -440
  11. package/package.json +9 -3
  12. package/src/cli.js +561 -103
  13. package/src/core/drain-manager.js +70 -0
  14. package/src/core/generation-manager.js +414 -0
  15. package/src/core/index.js +8 -0
  16. package/src/core/ipc.js +236 -0
  17. package/src/core/lock.js +133 -0
  18. package/src/core/process-manager.js +313 -0
  19. package/src/core/readiness.js +243 -0
  20. package/src/core/router.js +390 -0
  21. package/src/core/supervisor.js +222 -0
  22. package/src/deploy/deployment-manager.js +407 -0
  23. package/src/deploy/index.js +9 -0
  24. package/src/deploy/rollback-manager.js +186 -0
  25. package/src/doctor.js +123 -56
  26. package/src/errors.js +131 -0
  27. package/src/index.js +186 -59
  28. package/src/logs.js +148 -0
  29. package/src/platform/base.js +211 -0
  30. package/src/platform/index.js +43 -0
  31. package/src/platform/linux.js +155 -0
  32. package/src/platform/macos.js +726 -0
  33. package/src/platform/windows-host.js +246 -0
  34. package/src/platform/windows.js +498 -0
  35. package/src/runtimes/bun.js +1 -1
  36. package/src/runtimes/common.js +24 -6
  37. package/src/runtimes/deno.js +1 -1
  38. package/src/runtimes/elixir.js +1 -1
  39. package/src/runtimes/go.js +1 -1
  40. package/src/runtimes/index.js +7 -7
  41. package/src/runtimes/native.js +2 -4
  42. package/src/runtimes/node.js +1 -1
  43. package/src/runtimes/php.js +1 -1
  44. package/src/runtimes/python.js +1 -1
  45. package/src/runtimes/ruby.js +1 -1
  46. package/src/runtimes/shell.js +1 -1
  47. package/src/schedule.js +31 -24
  48. package/src/service/config.js +63 -0
  49. package/src/service/manager.js +233 -0
  50. package/src/service/normalize.js +245 -0
  51. package/src/systemd.js +84 -38
  52. package/src/unit.js +17 -19
  53. package/src/utils.js +112 -18
  54. package/tsconfig.json +1 -1
  55. package/types-test/index.ts +158 -0
@@ -0,0 +1,186 @@
1
+ import { DrainManager } from '../core/drain-manager.js';
2
+ import { GenerationManager } from '../core/generation-manager.js';
3
+ import { IPCClient } from '../core/ipc.js';
4
+ import { DeploymentLock } from '../core/lock.js';
5
+ import { ProcessManager } from '../core/process-manager.js';
6
+ import { ReadinessChecker } from '../core/readiness.js';
7
+ import { Router } from '../core/router.js';
8
+ import { readAppMetadata } from '../utils.js';
9
+
10
+ /**
11
+ * RollbackManager provides orchestration for reverting traffic to a previous generation.
12
+ * It strictly composes the Unitup core primitives.
13
+ */
14
+ export class RollbackManager {
15
+ /**
16
+ * @param {object} [options]
17
+ * @param {import('../core/generation-manager.js').GenerationManager} [options.generationManager]
18
+ * @param {import('../core/router.js').Router} [options.router]
19
+ * @param {import('../core/readiness.js').ReadinessChecker} [options.readiness]
20
+ * @param {import('../core/drain-manager.js').DrainManager} [options.drainManager]
21
+ * @param {import('../core/process-manager.js').ProcessManager} [options.processManager]
22
+ * @param {import('../core/lock.js').DeploymentLock} [options.lock]
23
+ */
24
+ constructor(options = {}) {
25
+ this.generationManager = options.generationManager;
26
+ this.router = options.router;
27
+ this.readiness = options.readiness;
28
+ this.drainManager = options.drainManager;
29
+ this.processManager = options.processManager;
30
+ this.lock = options.lock;
31
+ }
32
+
33
+ _ensurePrimitives(serviceName, options = {}) {
34
+ if (!this.processManager) {
35
+ this.processManager = new ProcessManager();
36
+ }
37
+ if (!this.generationManager) {
38
+ this.generationManager = new GenerationManager({
39
+ processManager: this.processManager,
40
+ stateDir: options.stateDir
41
+ });
42
+ }
43
+ if (!this.router) {
44
+ this.router = new Router();
45
+ }
46
+ if (!this.readiness) {
47
+ this.readiness = new ReadinessChecker({ processManager: this.processManager });
48
+ }
49
+ if (!this.drainManager) {
50
+ this.drainManager = new DrainManager({
51
+ router: this.router,
52
+ processManager: this.processManager
53
+ });
54
+ }
55
+ if (!this.lock) {
56
+ this.lock = new DeploymentLock({
57
+ lockDir: options.lockDir,
58
+ processManager: this.processManager
59
+ });
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Rolls back a service to its previous generation.
65
+ *
66
+ * @param {string} serviceName
67
+ * @param {object} [serviceConfig]
68
+ * @param {object} [options]
69
+ * @param {string} [options.readyPath]
70
+ * @param {number} [options.drainTimeout]
71
+ * @param {function(object): void} [options.onProgress]
72
+ * @returns {Promise<object>} Rollback summary
73
+ */
74
+ async rollback(serviceName, serviceConfig = {}, options = {}) {
75
+ if (!serviceName) {
76
+ throw new Error('Service name is required for rollback.');
77
+ }
78
+
79
+ // Check if a supervisor daemon is running for this service via IPC
80
+ const ipcClient = new IPCClient(serviceName, options.ipcDir);
81
+ const isRemote = await ipcClient.isAlive();
82
+ if (isRemote) {
83
+ return ipcClient.send(
84
+ {
85
+ action: 'rollback',
86
+ options: { ...options, ...serviceConfig }
87
+ },
88
+ options.onProgress
89
+ );
90
+ }
91
+
92
+ this._ensurePrimitives(serviceName, options);
93
+
94
+ const config =
95
+ serviceConfig && (serviceConfig.command || serviceConfig.script)
96
+ ? serviceConfig
97
+ : readAppMetadata(serviceName) || serviceConfig || {};
98
+
99
+ await this.lock.acquire(serviceName);
100
+
101
+ try {
102
+ // Check if a canary generation is running: if so, abort canary and preserve active
103
+ const canary = this.generationManager.getCanary(serviceName);
104
+ if (canary) {
105
+ if (options.onProgress) {
106
+ options.onProgress({ state: 'ABORTING_CANARY', canaryGeneration: canary.id });
107
+ }
108
+ this.router.clearCanary(serviceName);
109
+ const drainTimeout = options.drainTimeout || config.deploy?.drainTimeout || 5000;
110
+ await this.drainManager.drain(canary, { timeout: drainTimeout, stopProcess: true });
111
+ this.generationManager.remove(canary, false);
112
+
113
+ const active = this.generationManager.getActive(serviceName);
114
+ return {
115
+ service: serviceName,
116
+ abortedCanary: canary.id,
117
+ activeGeneration: active ? active.id : null,
118
+ status: 'canary_aborted'
119
+ };
120
+ }
121
+
122
+ const current = this.generationManager.getActive(serviceName);
123
+ const previous = this.generationManager.getPrevious(serviceName);
124
+
125
+ if (!previous) {
126
+ throw new Error(`No previous generation available to roll back to for service "${serviceName}".`);
127
+ }
128
+
129
+ if (options.onProgress) {
130
+ options.onProgress({ state: 'CHECKING_PREVIOUS', previous: previous.id });
131
+ }
132
+
133
+ // Check if previous is alive
134
+ if (!this.processManager.isAlive(previous.pid)) {
135
+ throw new Error(
136
+ `Previous generation #${previous.id} (PID ${previous.pid}) is no longer running and cannot be rolled back to.`
137
+ );
138
+ }
139
+
140
+ // Ensure previous generation is still ready
141
+ const isReady = await this.readiness.ensure(previous, {
142
+ path: options.readyPath || config.deploy?.ready
143
+ });
144
+
145
+ if (!isReady) {
146
+ throw new Error(`Previous generation #${previous.id} failed readiness verification.`);
147
+ }
148
+
149
+ if (options.onProgress) {
150
+ options.onProgress({ state: 'SWITCHING', targetGeneration: previous.id });
151
+ }
152
+
153
+ // Switch router to previous generation
154
+ this.router.switchBackend(serviceName, previous);
155
+ this.generationManager.markActive(previous);
156
+
157
+ // Drain and stop the rolled-back current generation
158
+ if (current && current.id !== previous.id) {
159
+ if (options.onProgress) {
160
+ options.onProgress({ state: 'DRAINING_CURRENT', generation: current.id });
161
+ }
162
+ this.generationManager.markDraining(current);
163
+ const drainTimeout = options.drainTimeout || config.deploy?.drainTimeout || 10000;
164
+ await this.drainManager.drain(current, { timeout: drainTimeout, stopProcess: true });
165
+ this.generationManager.remove(current, false);
166
+ }
167
+
168
+ const result = {
169
+ service: serviceName,
170
+ rolledBackFrom: current?.id ?? null,
171
+ activeGeneration: previous.id,
172
+ status: 'rolled_back'
173
+ };
174
+
175
+ if (options.onProgress) {
176
+ options.onProgress({ state: 'COMPLETE', ...result });
177
+ }
178
+
179
+ return result;
180
+ } finally {
181
+ await this.lock.release(serviceName);
182
+ }
183
+ }
184
+ }
185
+
186
+ export const defaultRollbackManager = new RollbackManager();
package/src/doctor.js CHANGED
@@ -1,15 +1,18 @@
1
+ import fs from 'node:fs';
1
2
  import os from 'node:os';
3
+ import { findRuntimeExecutable } from './runtimes/common.js';
2
4
  import {
5
+ checkNodeDiagnostics,
6
+ checkUserLinger,
3
7
  isLinux,
4
8
  isSystemctlAvailable,
5
9
  isSystemdPID1,
6
10
  isUserSystemdAvailable,
7
11
  isUserUnitDirWritable,
8
- checkUserLinger,
9
- checkNodeDiagnostics
12
+ runCommand
10
13
  } from './systemd.js';
11
14
  import { getUserUnitDir } from './unit.js';
12
- import { findRuntimeExecutable } from './runtimes/common.js';
15
+ import { getUnitupDir } from './utils.js';
13
16
 
14
17
  /**
15
18
  * Runs system diagnostic checks and returns structured data.
@@ -17,6 +20,7 @@ import { findRuntimeExecutable } from './runtimes/common.js';
17
20
  * @returns {Promise<Object>}
18
21
  */
19
22
  export async function getDoctorInfo() {
23
+ const platform = process.platform;
20
24
  const linux = isLinux();
21
25
  const systemctl = await isSystemctlAvailable();
22
26
  const pid1 = await isSystemdPID1();
@@ -25,6 +29,32 @@ export async function getDoctorInfo() {
25
29
  const lingering = await checkUserLinger();
26
30
  const nodeDiag = await checkNodeDiagnostics();
27
31
 
32
+ // macOS specific checks
33
+ let launchctlAvailable = false;
34
+ if (platform === 'darwin') {
35
+ try {
36
+ const res = await runCommand('launchctl', ['version']);
37
+ launchctlAvailable = res.code === 0 || (res.stdout && res.stdout.length > 0) || !res.stderr.includes('not found');
38
+ } catch {
39
+ launchctlAvailable = false;
40
+ }
41
+ }
42
+
43
+ // Windows specific checks
44
+ let windowsScmAvailable = false;
45
+ let isAdmin = false;
46
+ if (platform === 'win32') {
47
+ try {
48
+ const res = await runCommand('sc.exe', ['query']);
49
+ windowsScmAvailable = res.code === 0 || !res.stderr.includes('not found');
50
+ // Simple admin check: net session or whoami /priv
51
+ const adminRes = await runCommand('net', ['session']);
52
+ isAdmin = adminRes.code === 0;
53
+ } catch {
54
+ windowsScmAvailable = false;
55
+ }
56
+ }
57
+
28
58
  const nodePath = nodeDiag.execPath || (await findRuntimeExecutable(['node']));
29
59
  const pythonPath = await findRuntimeExecutable(['python3', 'python']);
30
60
  const rubyPath = await findRuntimeExecutable(['ruby']);
@@ -36,13 +66,13 @@ export async function getDoctorInfo() {
36
66
 
37
67
  const runtimes = {
38
68
  'Node.js': nodePath,
39
- 'Python': pythonPath,
40
- 'Ruby': rubyPath,
41
- 'PHP': phpPath,
42
- 'Bun': bunPath,
43
- 'Deno': denoPath,
44
- 'Go': goPath,
45
- 'Elixir': elixirPath
69
+ Python: pythonPath,
70
+ Ruby: rubyPath,
71
+ PHP: phpPath,
72
+ Bun: bunPath,
73
+ Deno: denoPath,
74
+ Go: goPath,
75
+ Elixir: elixirPath
46
76
  };
47
77
 
48
78
  let cgroupV2 = false;
@@ -60,11 +90,20 @@ export async function getDoctorInfo() {
60
90
  const memoryMaxSupported = linux && (cgroupV2 || userSystemd);
61
91
  const memorySwapMaxSupported = linux && cgroupV2 && memoryController;
62
92
 
93
+ let serviceManager = 'systemd';
94
+ if (platform === 'darwin') serviceManager = 'launchd';
95
+ else if (platform === 'win32') serviceManager = 'Windows Service Manager';
96
+
63
97
  return {
98
+ platform,
99
+ serviceManager,
64
100
  linux,
65
101
  systemctl,
66
102
  systemdRunning: pid1,
67
103
  userSystemdAvailable: userSystemd,
104
+ launchctlAvailable,
105
+ windowsScmAvailable,
106
+ isAdmin,
68
107
  cgroupV2,
69
108
  memoryController,
70
109
  memoryMaxSupported,
@@ -74,6 +113,7 @@ export async function getDoctorInfo() {
74
113
  nodeVersion: nodeDiag.version,
75
114
  runtimes,
76
115
  unitDir: getUserUnitDir(),
116
+ unitupDataDir: getUnitupDir(),
77
117
  unitDirWritable,
78
118
  lingering,
79
119
  username: os.userInfo().username
@@ -89,27 +129,49 @@ export async function runDoctor() {
89
129
  const info = await getDoctorInfo();
90
130
 
91
131
  if (info.linux) {
132
+ console.log('Platform: Linux');
133
+ console.log('Service manager: systemd');
134
+ console.log(`Node: ${info.nodePath} ${info.nodeVersion ? `(${info.nodeVersion})` : ''}`);
135
+ console.log(`Unitup data: ${info.unitupDataDir}`);
92
136
  console.log('✓ Linux detected');
93
- } else {
94
- console.log('✗ Non-Linux OS detected (systemd requires Linux)');
95
- }
96
137
 
97
- if (info.systemctl) {
98
- console.log('✓ systemctl available');
99
- } else {
100
- console.log('✗ systemctl command not found');
101
- }
138
+ if (info.systemctl) {
139
+ console.log('✓ systemctl available');
140
+ } else {
141
+ console.log('✗ systemctl command not found');
142
+ }
102
143
 
103
- if (info.systemdRunning) {
104
- console.log('✓ systemd is running');
105
- } else {
106
- console.log('! systemd is not running as PID 1');
107
- }
144
+ if (info.systemdRunning) {
145
+ console.log('✓ systemd is running');
146
+ } else {
147
+ console.log('! systemd is not running as PID 1');
148
+ }
108
149
 
109
- if (info.userSystemdAvailable) {
110
- console.log('✓ systemd user services available');
150
+ if (info.userSystemdAvailable) {
151
+ console.log('✓ systemd user services available');
152
+ } else {
153
+ console.log('! systemd user services unavailable');
154
+ }
155
+ } else if (info.platform === 'darwin') {
156
+ console.log('Platform: macOS');
157
+ console.log('Service manager: launchd');
158
+ console.log(`Node: ${info.nodePath} ${info.nodeVersion ? `(${info.nodeVersion})` : ''}`);
159
+ console.log(`Unitup data: ${info.unitupDataDir}`);
160
+ console.log('! Non-Linux OS detected (macOS uses launchd)');
161
+ console.log('✓ launchctl: available');
162
+ console.log('✓ Permissions: user services supported');
163
+ } else if (info.platform === 'win32') {
164
+ console.log('Platform: Windows');
165
+ console.log('Service manager: Windows Service Manager');
166
+ console.log(`Node: ${info.nodePath} ${info.nodeVersion ? `(${info.nodeVersion})` : ''}`);
167
+ console.log(`Unitup data: ${info.unitupDataDir}`);
168
+ console.log('! Non-Linux OS detected (Windows uses Windows Services)');
169
+ console.log(`Administrator: ${info.isAdmin ? 'yes' : 'no'}`);
170
+ if (!info.isAdmin) {
171
+ console.log('! Service installation requires Administrator privileges');
172
+ }
111
173
  } else {
112
- console.log('! systemd user services unavailable');
174
+ console.log(`✗ Non-Linux OS detected (${info.platform})`);
113
175
  }
114
176
 
115
177
  console.log('\nDetected runtimes:');
@@ -121,48 +183,53 @@ export async function runDoctor() {
121
183
  }
122
184
  }
123
185
 
124
- console.log('\nMemory Controller Diagnostics:');
125
- if (info.cgroupV2) {
126
- console.log('✓ cgroup v2 detected');
127
- } else {
128
- console.log('- cgroup v2 unavailable');
129
- }
186
+ if (info.linux) {
187
+ console.log('\nMemory Controller Diagnostics:');
188
+ if (info.cgroupV2) {
189
+ console.log('✓ cgroup v2 detected');
190
+ } else {
191
+ console.log('- cgroup v2 unavailable');
192
+ }
130
193
 
131
- if (info.memoryController) {
132
- console.log('✓ Memory controller available');
133
- } else {
134
- console.log('- Memory controller unavailable');
135
- }
194
+ if (info.memoryController) {
195
+ console.log('✓ Memory controller available');
196
+ } else {
197
+ console.log('- Memory controller unavailable');
198
+ }
136
199
 
137
- if (info.memoryMaxSupported) {
138
- console.log('✓ MemoryMax supported');
139
- } else {
140
- console.log('- MemoryMax unavailable');
141
- }
200
+ if (info.memoryMaxSupported) {
201
+ console.log('✓ MemoryMax supported');
202
+ } else {
203
+ console.log('- MemoryMax unavailable');
204
+ }
142
205
 
143
- if (info.memorySwapMaxSupported) {
144
- console.log('✓ MemorySwapMax supported');
145
- } else {
146
- console.log('- MemorySwapMax unavailable');
147
- }
206
+ if (info.memorySwapMaxSupported) {
207
+ console.log('✓ MemorySwapMax supported');
208
+ } else {
209
+ console.log('- MemorySwapMax unavailable');
210
+ }
148
211
 
149
- console.log('');
212
+ console.log('');
150
213
 
151
- if (info.unitDirWritable) {
152
- console.log('✓ Unit directory writable');
153
- } else {
154
- console.log(`! Unit directory not writable (${info.unitDir})`);
214
+ if (info.unitDirWritable) {
215
+ console.log('✓ Unit directory writable');
216
+ } else {
217
+ console.log(`! Unit directory not writable (${info.unitDir})`);
218
+ }
155
219
  }
156
220
 
157
221
  if (info.lingering) {
158
222
  console.log('✓ User lingering is enabled');
159
223
  } else {
160
224
  console.log('! User lingering is disabled');
161
- console.log('\nUser lingering is disabled.');
162
- console.log('The service may stop after logout.');
163
- console.log('\nEnable it manually:');
164
- console.log(` loginctl enable-linger ${info.username}\n`);
225
+ if (info.linux) {
226
+ console.log('\nUser lingering is disabled.');
227
+ console.log('The service may stop after logout.');
228
+ console.log('\nEnable it manually:');
229
+ console.log(` loginctl enable-linger ${info.username}\n`);
230
+ }
165
231
  }
166
232
 
233
+ console.log('\n✓ Unitup can manage services on this machine');
167
234
  return info;
168
235
  }
package/src/errors.js ADDED
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Base error class for all Unitup errors.
3
+ */
4
+ export class UnitupError extends Error {
5
+ /**
6
+ * @param {string} message
7
+ * @param {string} [code]
8
+ */
9
+ constructor(message, code = 'UNITUP_ERROR') {
10
+ super(message);
11
+ this.name = this.constructor.name;
12
+ this.code = code;
13
+ if (Error.captureStackTrace) {
14
+ Error.captureStackTrace(this, this.constructor);
15
+ }
16
+ }
17
+ }
18
+
19
+ /**
20
+ * Thrown when an unsupported platform is encountered.
21
+ */
22
+ export class UnsupportedPlatformError extends UnitupError {
23
+ /**
24
+ * @param {string} [platform]
25
+ */
26
+ constructor(platform = process.platform) {
27
+ super(`Platform "${platform}" is not supported by Unitup.`, 'ERR_UNSUPPORTED_PLATFORM');
28
+ this.platform = platform;
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Thrown when a specified service is not found.
34
+ */
35
+ export class ServiceNotFoundError extends UnitupError {
36
+ /**
37
+ * @param {string} name
38
+ */
39
+ constructor(name) {
40
+ super(`Service "${name}" does not exist.`, 'ERR_SERVICE_NOT_FOUND');
41
+ this.serviceName = name;
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Thrown when attempting to create a service that already exists without overwrite/force.
47
+ */
48
+ export class ServiceAlreadyExistsError extends UnitupError {
49
+ /**
50
+ * @param {string} name
51
+ */
52
+ constructor(name) {
53
+ super(`Service "${name}" already exists. Use --force to overwrite.`, 'ERR_SERVICE_ALREADY_EXISTS');
54
+ this.serviceName = name;
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Thrown when elevated administrator or root privileges are required for an action.
60
+ */
61
+ export class PermissionRequiredError extends UnitupError {
62
+ /**
63
+ * @param {string} message
64
+ * @param {string} [action]
65
+ */
66
+ constructor(message = 'Administrator/root privileges are required to perform this action.', action) {
67
+ super(message, 'ERR_PERMISSION_REQUIRED');
68
+ this.action = action;
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Thrown when a service fails to start.
74
+ */
75
+ export class ServiceStartError extends UnitupError {
76
+ /**
77
+ * @param {string} name
78
+ * @param {string} [reason]
79
+ */
80
+ constructor(name, reason) {
81
+ super(`Failed to start service "${name}"${reason ? `: ${reason}` : ''}`, 'ERR_SERVICE_START');
82
+ this.serviceName = name;
83
+ this.reason = reason;
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Thrown when a service fails to stop.
89
+ */
90
+ export class ServiceStopError extends UnitupError {
91
+ /**
92
+ * @param {string} name
93
+ * @param {string} [reason]
94
+ */
95
+ constructor(name, reason) {
96
+ super(`Failed to stop service "${name}"${reason ? `: ${reason}` : ''}`, 'ERR_SERVICE_STOP');
97
+ this.serviceName = name;
98
+ this.reason = reason;
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Thrown when service configuration is invalid.
104
+ */
105
+ export class InvalidServiceConfigError extends UnitupError {
106
+ /**
107
+ * @param {string} message
108
+ * @param {string} [field]
109
+ */
110
+ constructor(message, field) {
111
+ super(message, 'ERR_INVALID_CONFIG');
112
+ this.field = field;
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Thrown when a required binary executable cannot be resolved.
118
+ */
119
+ export class ExecutableNotFoundError extends UnitupError {
120
+ /**
121
+ * @param {string} executable
122
+ * @param {string} [message]
123
+ */
124
+ constructor(executable, message) {
125
+ super(
126
+ message || `Executable "${executable}" could not be found in PATH or at specified path.`,
127
+ 'ERR_EXECUTABLE_NOT_FOUND'
128
+ );
129
+ this.executable = executable;
130
+ }
131
+ }