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,498 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import {
6
+ PermissionRequiredError,
7
+ ServiceAlreadyExistsError,
8
+ ServiceNotFoundError,
9
+ ServiceStartError,
10
+ ServiceStopError
11
+ } from '../errors.js';
12
+ import { readServiceLogs } from '../logs.js';
13
+ import {
14
+ deleteAppMetadata,
15
+ formatRelativeTime,
16
+ getAppsDir,
17
+ getUnitupBinPath,
18
+ getUnitupDir,
19
+ readAppMetadata,
20
+ sanitizeServiceName,
21
+ saveAppMetadata
22
+ } from '../utils.js';
23
+ import { ServiceAdapter } from './base.js';
24
+
25
+ const __filename = fileURLToPath(import.meta.url);
26
+ const __dirname = path.dirname(__filename);
27
+ const WINDOWS_HOST_PATH = path.resolve(__dirname, 'windows-host.js');
28
+
29
+ /**
30
+ * Windows Service Adapter using Windows SCM (sc.exe) and Unitup Windows Service Host.
31
+ */
32
+ export class WindowsAdapter extends ServiceAdapter {
33
+ get name() {
34
+ return 'windows';
35
+ }
36
+
37
+ get capabilities() {
38
+ return {
39
+ serviceManager: 'windows',
40
+ supports: {
41
+ install: true,
42
+ uninstall: true,
43
+ start: true,
44
+ stop: true,
45
+ restart: true,
46
+ enable: true,
47
+ disable: true,
48
+ status: true,
49
+ logs: true,
50
+ restartPolicy: true,
51
+ userServices: false,
52
+ systemServices: true,
53
+ memoryLimits: false,
54
+ schedule: false
55
+ }
56
+ };
57
+ }
58
+
59
+ /**
60
+ * Returns the Windows service name.
61
+ * @param {string} name
62
+ * @returns {string}
63
+ */
64
+ getServiceName(name) {
65
+ const safeName = sanitizeServiceName(name);
66
+ return `unitup-${safeName}`;
67
+ }
68
+
69
+ /**
70
+ * Generates Windows service creation command parameters.
71
+ *
72
+ * @param {object} config - NormalizedServiceConfig
73
+ * @returns {object}
74
+ */
75
+ generateService(config) {
76
+ const safeName = sanitizeServiceName(config.name);
77
+ const serviceName = this.getServiceName(safeName);
78
+ const nodeExec = process.execPath;
79
+ const hostScript = config.deploy?.zeroDowntime ? getUnitupBinPath() : WINDOWS_HOST_PATH;
80
+ const hostArg = config.deploy?.zeroDowntime ? `supervisor "${safeName}"` : `"${safeName}"`;
81
+ const binPath = `"${nodeExec}" "${hostScript}" ${hostArg}`;
82
+ const startType = config.autostart !== false ? 'auto' : 'demand';
83
+
84
+ return {
85
+ serviceName,
86
+ displayName: config.displayName && config.displayName !== safeName ? config.displayName : `Unitup - ${safeName}`,
87
+ description: config.description || `unitup service: ${safeName}`,
88
+ binPath,
89
+ startType,
90
+ command: config.command,
91
+ args: config.args,
92
+ cwd: config.cwd,
93
+ env: config.env
94
+ };
95
+ }
96
+
97
+ /**
98
+ * Installs a Windows Service.
99
+ *
100
+ * @param {object} config - NormalizedServiceConfig
101
+ * @param {object} [options]
102
+ * @returns {Promise<object>}
103
+ */
104
+ async install(config, options = {}) {
105
+ const safeName = sanitizeServiceName(config.name);
106
+ const serviceName = this.getServiceName(safeName);
107
+ const generated = this.generateService(config);
108
+
109
+ const isExisting = await this.isInstalled(safeName);
110
+ let wasRunning = false;
111
+
112
+ if (isExisting) {
113
+ const currentStatus = await this.status(safeName);
114
+ if (currentStatus.state === 'running') {
115
+ wasRunning = true;
116
+ if (!options.force && !config.raw?.force) {
117
+ throw new ServiceAlreadyExistsError(safeName);
118
+ }
119
+ await this.stop(safeName);
120
+ }
121
+ await this.uninstall(safeName, { force: true });
122
+ }
123
+
124
+ // Save app metadata before creating service
125
+ saveAppMetadata({
126
+ ...config,
127
+ name: safeName,
128
+ serviceName,
129
+ hostPath: WINDOWS_HOST_PATH,
130
+ installedAt: new Date().toISOString()
131
+ });
132
+
133
+ const scArgs = [
134
+ 'create',
135
+ serviceName,
136
+ `binPath= ${generated.binPath}`,
137
+ `displayName= ${generated.displayName}`,
138
+ `start= ${generated.startType}`
139
+ ];
140
+
141
+ const res = await this.run('sc.exe', scArgs);
142
+ if (res.code !== 0) {
143
+ if (res.stderr.includes('Access is denied') || res.stdout.includes('Access is denied') || res.code === 5) {
144
+ throw new PermissionRequiredError(
145
+ `Installing a Windows service requires Administrator privileges.\n\nPlease open PowerShell or Command Prompt as Administrator and run:\n unitup install ${config.name}`,
146
+ 'install'
147
+ );
148
+ }
149
+ throw new Error(`Failed to create Windows service "${serviceName}": ${res.stderr || res.stdout}`);
150
+ }
151
+
152
+ // Set description if supported
153
+ try {
154
+ await this.run('sc.exe', ['description', serviceName, generated.description]);
155
+ } catch {
156
+ // ignore
157
+ }
158
+
159
+ if (options.start || config.raw?.start || wasRunning) {
160
+ await this.start(safeName);
161
+ }
162
+
163
+ return {
164
+ name: safeName,
165
+ serviceName,
166
+ displayName: generated.displayName,
167
+ unitPath: generated.binPath
168
+ };
169
+ }
170
+
171
+ /**
172
+ * Uninstalls a Windows Service.
173
+ *
174
+ * @param {string} name
175
+ * @param {object} [options]
176
+ * @returns {Promise<boolean>}
177
+ */
178
+ async uninstall(name, options = {}) {
179
+ const safeName = sanitizeServiceName(name);
180
+ const serviceName = this.getServiceName(safeName);
181
+
182
+ const isExisting = await this.isInstalled(safeName);
183
+ if (!isExisting) {
184
+ throw new ServiceNotFoundError(safeName);
185
+ }
186
+
187
+ const currentStatus = await this.status(safeName);
188
+ if (currentStatus.state === 'running' && !options.force) {
189
+ throw new Error(
190
+ `Service "${safeName}" is currently running.\n` +
191
+ `Use --force (-f) to remove running services, or stop it first:\n` +
192
+ ` unitup stop ${safeName}`
193
+ );
194
+ }
195
+
196
+ if (currentStatus.state === 'running') {
197
+ try {
198
+ await this.stop(safeName);
199
+ } catch {
200
+ // ignore
201
+ }
202
+ }
203
+
204
+ const res = await this.run('sc.exe', ['delete', serviceName]);
205
+ if (res.code !== 0) {
206
+ if (res.stderr.includes('Access is denied') || res.stdout.includes('Access is denied') || res.code === 5) {
207
+ throw new PermissionRequiredError(
208
+ `Uninstalling a Windows service requires Administrator privileges.`,
209
+ 'uninstall'
210
+ );
211
+ }
212
+ throw new Error(`Failed to delete Windows service "${serviceName}": ${res.stderr || res.stdout}`);
213
+ }
214
+
215
+ deleteAppMetadata(safeName);
216
+ return true;
217
+ }
218
+
219
+ /**
220
+ * Starts a Windows Service.
221
+ *
222
+ * @param {string} name
223
+ * @param {object} [options]
224
+ * @returns {Promise<boolean>}
225
+ */
226
+ async start(name, options = {}) {
227
+ const safeName = sanitizeServiceName(name);
228
+ const serviceName = this.getServiceName(safeName);
229
+
230
+ const res = await this.run('sc.exe', ['start', serviceName]);
231
+ if (res.code !== 0) {
232
+ if (res.stdout.includes('already running') || res.stdout.includes('1056')) {
233
+ return true;
234
+ }
235
+ if (res.stderr.includes('Access is denied') || res.stdout.includes('Access is denied')) {
236
+ throw new PermissionRequiredError('Starting a Windows service requires Administrator privileges.', 'start');
237
+ }
238
+ throw new ServiceStartError(safeName, res.stderr || res.stdout);
239
+ }
240
+ return true;
241
+ }
242
+
243
+ /**
244
+ * Stops a Windows Service.
245
+ *
246
+ * @param {string} name
247
+ * @param {object} [options]
248
+ * @returns {Promise<boolean>}
249
+ */
250
+ async stop(name, options = {}) {
251
+ const safeName = sanitizeServiceName(name);
252
+ const serviceName = this.getServiceName(safeName);
253
+
254
+ const res = await this.run('sc.exe', ['stop', serviceName]);
255
+ if (res.code !== 0) {
256
+ if (res.stdout.includes('not started') || res.stdout.includes('1062')) {
257
+ return true;
258
+ }
259
+ if (res.stderr.includes('Access is denied') || res.stdout.includes('Access is denied')) {
260
+ throw new PermissionRequiredError('Stopping a Windows service requires Administrator privileges.', 'stop');
261
+ }
262
+ throw new ServiceStopError(safeName, res.stderr || res.stdout);
263
+ }
264
+ return true;
265
+ }
266
+
267
+ /**
268
+ * Restarts a Windows Service.
269
+ *
270
+ * @param {string} name
271
+ * @param {object} [options]
272
+ * @returns {Promise<boolean>}
273
+ */
274
+ async restart(name, options = {}) {
275
+ await this.stop(name, options);
276
+ // Give SCM a brief pause
277
+ await new Promise((r) => setTimeout(r, 1000));
278
+ return this.start(name, options);
279
+ }
280
+
281
+ /**
282
+ * Enables automatic startup for a Windows service.
283
+ *
284
+ * @param {string} name
285
+ * @param {object} [options]
286
+ * @returns {Promise<boolean>}
287
+ */
288
+ async enable(name, options = {}) {
289
+ const safeName = sanitizeServiceName(name);
290
+ const serviceName = this.getServiceName(safeName);
291
+
292
+ const res = await this.run('sc.exe', ['config', serviceName, 'start=', 'auto']);
293
+ return res.code === 0;
294
+ }
295
+
296
+ /**
297
+ * Disables automatic startup for a Windows service.
298
+ *
299
+ * @param {string} name
300
+ * @param {object} [options]
301
+ * @returns {Promise<boolean>}
302
+ */
303
+ async disable(name, options = {}) {
304
+ const safeName = sanitizeServiceName(name);
305
+ const serviceName = this.getServiceName(safeName);
306
+
307
+ const res = await this.run('sc.exe', ['config', serviceName, 'start=', 'disabled']);
308
+ return res.code === 0;
309
+ }
310
+
311
+ /**
312
+ * Gets normalized status for a Windows service.
313
+ *
314
+ * @param {string} name
315
+ * @param {object} [options]
316
+ * @returns {Promise<object>}
317
+ */
318
+ async status(name, options = {}) {
319
+ const safeName = sanitizeServiceName(name);
320
+ const serviceName = this.getServiceName(safeName);
321
+ const meta = readAppMetadata(safeName) || {};
322
+
323
+ let state = 'stopped';
324
+ const pid = meta.pid || '-';
325
+ let installed = false;
326
+ const details = {};
327
+
328
+ const res = await this.run('sc.exe', ['query', serviceName]);
329
+ if (res.code === 0 && res.stdout) {
330
+ installed = true;
331
+ details.raw = res.stdout;
332
+
333
+ if (res.stdout.includes('RUNNING')) {
334
+ state = 'running';
335
+ } else if (res.stdout.includes('STOP_PENDING')) {
336
+ state = 'stopping';
337
+ } else if (res.stdout.includes('START_PENDING')) {
338
+ state = 'starting';
339
+ } else if (res.stdout.includes('STOPPED')) {
340
+ state = 'stopped';
341
+ }
342
+ } else {
343
+ // Check if metadata exists even if sc query failed
344
+ if (meta && Object.keys(meta).length > 0) {
345
+ installed = true;
346
+ state = 'stopped';
347
+ }
348
+ }
349
+
350
+ const command = meta.command || process.execPath;
351
+ const argsList = meta.args || (meta.script ? [meta.script] : []);
352
+
353
+ return {
354
+ name: safeName,
355
+ installed,
356
+ state,
357
+ status: state,
358
+ enabled: installed,
359
+ pid,
360
+ restarts: meta.restarts || '0',
361
+ started: meta.started ? formatRelativeTime(meta.started) : state === 'running' ? 'active' : 'never',
362
+ startedRaw: meta.started || null,
363
+ command,
364
+ arguments: argsList.join(' '),
365
+ args: argsList,
366
+ script: meta.script || argsList[0] || command,
367
+ cwd: meta.cwd || process.cwd(),
368
+ unitFile: serviceName,
369
+ unitPath: serviceName,
370
+ platform: 'win32',
371
+ manager: 'windows',
372
+ details
373
+ };
374
+ }
375
+
376
+ /**
377
+ * Inspects detailed configuration and runtime info for a service.
378
+ *
379
+ * @param {string} name
380
+ * @param {object} [options]
381
+ * @returns {Promise<object>}
382
+ */
383
+ async inspect(name, options = {}) {
384
+ const statusObj = await this.status(name, options);
385
+ const meta = readAppMetadata(name) || {};
386
+ return {
387
+ ...statusObj,
388
+ runtime: meta.runtime || 'node',
389
+ group: meta.group || 'default',
390
+ node: statusObj.command
391
+ };
392
+ }
393
+
394
+ /**
395
+ * Retrieves logs for a Windows service.
396
+ *
397
+ * @param {string} name
398
+ * @param {object} [options]
399
+ * @returns {Promise<any>}
400
+ */
401
+ async logs(name, options = {}) {
402
+ const safeName = sanitizeServiceName(name);
403
+ const meta = readAppMetadata(safeName);
404
+ const defaultStdout = path.join(getUnitupDir(), 'logs', `${safeName}.log`);
405
+ const defaultStderr = path.join(getUnitupDir(), 'logs', `${safeName}-error.log`);
406
+
407
+ const stdoutPath = meta?.logs?.stdout || defaultStdout;
408
+ const stderrPath = meta?.logs?.stderr || defaultStderr;
409
+
410
+ let paths = [stdoutPath, stderrPath];
411
+ if (options.output === 'stdout' || options.stdout) {
412
+ paths = [stdoutPath];
413
+ } else if (options.output === 'stderr' || options.stderr) {
414
+ paths = [stderrPath];
415
+ }
416
+
417
+ return readServiceLogs(paths, options);
418
+ }
419
+
420
+ /**
421
+ * Lists all unitup Windows services.
422
+ *
423
+ * @param {object} [options]
424
+ * @returns {Promise<Array<object>>}
425
+ */
426
+ async list(options = {}) {
427
+ const appsDir = getAppsDir();
428
+ if (!fs.existsSync(appsDir)) return [];
429
+
430
+ const files = fs.readdirSync(appsDir).filter((f) => f.endsWith('.json'));
431
+ const result = [];
432
+ const targetGroup = options.group
433
+ ? (options.group.startsWith('@') ? options.group.slice(1) : options.group).toLowerCase()
434
+ : null;
435
+
436
+ for (const file of files) {
437
+ const serviceName = file.replace(/\.json$/, '');
438
+ const meta = readAppMetadata(serviceName);
439
+ if (!meta) continue;
440
+
441
+ const group = meta.group || 'default';
442
+ if (targetGroup && group.toLowerCase() !== targetGroup) {
443
+ continue;
444
+ }
445
+
446
+ try {
447
+ const stat = await this.status(serviceName);
448
+ const cmdBase = path.basename(stat.command || 'node');
449
+ const argsStr = (stat.args || []).map((a) => path.basename(a)).join(' ');
450
+ const commandSummary = (cmdBase + (argsStr ? ` ${argsStr}` : '')).trim();
451
+
452
+ result.push({
453
+ name: serviceName,
454
+ runtime: meta.runtime || 'node',
455
+ group,
456
+ status: stat.state,
457
+ enabled: stat.enabled ? 'yes' : 'no',
458
+ pid: stat.pid,
459
+ command: commandSummary,
460
+ uptime: stat.started,
461
+ restarts: stat.restarts || '0',
462
+ platform: 'win32',
463
+ manager: 'windows'
464
+ });
465
+ } catch {
466
+ result.push({
467
+ name: serviceName,
468
+ runtime: meta.runtime || 'node',
469
+ group,
470
+ status: 'unknown',
471
+ enabled: 'unknown',
472
+ pid: '-',
473
+ command: 'unknown',
474
+ uptime: 'never',
475
+ restarts: '0',
476
+ platform: 'win32',
477
+ manager: 'windows'
478
+ });
479
+ }
480
+ }
481
+
482
+ return result;
483
+ }
484
+
485
+ async isInstalled(name, options = {}) {
486
+ const safeName = sanitizeServiceName(name);
487
+ const serviceName = this.getServiceName(safeName);
488
+ const res = await this.run('sc.exe', ['query', serviceName]);
489
+ if (res.code === 0) return true;
490
+ const meta = readAppMetadata(safeName);
491
+ return !!meta;
492
+ }
493
+
494
+ async failures(options = {}) {
495
+ const all = await this.list(options);
496
+ return all.filter((i) => i.status === 'failed');
497
+ }
498
+ }
@@ -1,5 +1,5 @@
1
- import path from 'node:path';
2
1
  import fs from 'node:fs';
2
+ import path from 'node:path';
3
3
  import { findRuntimeExecutable, getBinaryVersion } from './common.js';
4
4
 
5
5
  export async function createBunAdapter(opts = {}) {
@@ -1,22 +1,40 @@
1
- import path from 'node:path';
2
1
  import fs from 'node:fs';
2
+ import path from 'node:path';
3
3
  import { runCommand } from '../systemd.js';
4
4
 
5
5
  export function findExecutableInPath(binaryName) {
6
6
  // 1. Check standard PATH env
7
7
  const envPath = process.env.PATH || '';
8
8
  const dirs = envPath.split(path.delimiter).filter(Boolean);
9
-
9
+ const isWin = process.platform === 'win32';
10
+ const pathext = isWin ? (process.env.PATHEXT || '.EXE;.CMD;.BAT;.COM').split(';').filter(Boolean) : [''];
11
+
10
12
  for (const dir of dirs) {
11
- const full = path.join(dir, binaryName);
13
+ const directFull = path.join(dir, binaryName);
12
14
  try {
13
- if (fs.existsSync(full)) {
14
- fs.accessSync(full, fs.constants.X_OK);
15
- return full;
15
+ if (fs.existsSync(directFull)) {
16
+ if (!isWin) fs.accessSync(directFull, fs.constants.X_OK);
17
+ return directFull;
16
18
  }
17
19
  } catch {
18
20
  // ignore
19
21
  }
22
+
23
+ if (isWin) {
24
+ for (const ext of pathext) {
25
+ const withExt = path.join(
26
+ dir,
27
+ binaryName.toLowerCase().endsWith(ext.toLowerCase()) ? binaryName : `${binaryName}${ext.toLowerCase()}`
28
+ );
29
+ try {
30
+ if (fs.existsSync(withExt)) {
31
+ return withExt;
32
+ }
33
+ } catch {
34
+ // ignore
35
+ }
36
+ }
37
+ }
20
38
  }
21
39
  return null;
22
40
  }
@@ -1,5 +1,5 @@
1
- import path from 'node:path';
2
1
  import fs from 'node:fs';
2
+ import path from 'node:path';
3
3
  import { findRuntimeExecutable, getBinaryVersion } from './common.js';
4
4
 
5
5
  export async function createDenoAdapter(opts = {}) {
@@ -1,5 +1,5 @@
1
- import path from 'node:path';
2
1
  import fs from 'node:fs';
2
+ import path from 'node:path';
3
3
  import { findRuntimeExecutable, getBinaryVersion } from './common.js';
4
4
 
5
5
  export async function createElixirAdapter(opts = {}) {
@@ -1,5 +1,5 @@
1
- import path from 'node:path';
2
1
  import fs from 'node:fs';
2
+ import path from 'node:path';
3
3
  import { findRuntimeExecutable, getBinaryVersion } from './common.js';
4
4
 
5
5
  export async function createGoAdapter(opts = {}) {
@@ -1,15 +1,15 @@
1
- import path from 'node:path';
2
1
  import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { createBunAdapter } from './bun.js';
4
+ import { createDenoAdapter } from './deno.js';
5
+ import { createElixirAdapter } from './elixir.js';
6
+ import { createGoAdapter } from './go.js';
7
+ import { createNativeAdapter } from './native.js';
3
8
  import { createNodeAdapter } from './node.js';
9
+ import { createPhpAdapter } from './php.js';
4
10
  import { createPythonAdapter } from './python.js';
5
11
  import { createRubyAdapter } from './ruby.js';
6
- import { createPhpAdapter } from './php.js';
7
- import { createBunAdapter } from './bun.js';
8
- import { createDenoAdapter } from './deno.js';
9
12
  import { createShellAdapter } from './shell.js';
10
- import { createNativeAdapter } from './native.js';
11
- import { createGoAdapter } from './go.js';
12
- import { createElixirAdapter } from './elixir.js';
13
13
 
14
14
  export function detectRuntime(filepath, opts = {}) {
15
15
  if (!filepath) {
@@ -1,5 +1,5 @@
1
- import path from 'node:path';
2
1
  import fs from 'node:fs';
2
+ import path from 'node:path';
3
3
 
4
4
  export async function createNativeAdapter(opts = {}) {
5
5
  const baseDir = opts.cwd || process.cwd();
@@ -23,9 +23,7 @@ export async function createNativeAdapter(opts = {}) {
23
23
  try {
24
24
  fs.accessSync(absPath, fs.constants.X_OK);
25
25
  } catch {
26
- throw new Error(
27
- `The executable is not runnable.\n\nRun:\n chmod +x ${absPath}`
28
- );
26
+ throw new Error(`The executable is not runnable.\n\nRun:\n chmod +x ${absPath}`);
29
27
  }
30
28
 
31
29
  const scriptArgs = Array.isArray(opts.args) ? opts.args : [];
@@ -1,5 +1,5 @@
1
- import path from 'node:path';
2
1
  import fs from 'node:fs';
2
+ import path from 'node:path';
3
3
  import { findRuntimeExecutable, getBinaryVersion } from './common.js';
4
4
 
5
5
  export async function createNodeAdapter(opts = {}) {
@@ -1,5 +1,5 @@
1
- import path from 'node:path';
2
1
  import fs from 'node:fs';
2
+ import path from 'node:path';
3
3
  import { findRuntimeExecutable, getBinaryVersion } from './common.js';
4
4
 
5
5
  export async function createPhpAdapter(opts = {}) {
@@ -1,5 +1,5 @@
1
- import path from 'node:path';
2
1
  import fs from 'node:fs';
2
+ import path from 'node:path';
3
3
  import { findRuntimeExecutable, getBinaryVersion } from './common.js';
4
4
 
5
5
  export async function createPythonAdapter(opts = {}) {
@@ -1,5 +1,5 @@
1
- import path from 'node:path';
2
1
  import fs from 'node:fs';
2
+ import path from 'node:path';
3
3
  import { findRuntimeExecutable, getBinaryVersion } from './common.js';
4
4
 
5
5
  export async function createRubyAdapter(opts = {}) {
@@ -1,5 +1,5 @@
1
- import path from 'node:path';
2
1
  import fs from 'node:fs';
2
+ import path from 'node:path';
3
3
  import { findRuntimeExecutable, getBinaryVersion } from './common.js';
4
4
 
5
5
  export async function createShellAdapter(opts = {}) {