unitup 0.1.3 → 0.1.5
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/CHANGELOG.md +37 -0
- package/README.md +34 -0
- package/biome.json +45 -0
- package/index.d.ts +25 -0
- package/package.json +6 -2
- package/src/cli.js +113 -17
- package/src/index.js +6 -0
- package/src/runtimes/bun.js +3 -2
- package/src/runtimes/deno.js +3 -2
- package/src/runtimes/elixir.js +3 -2
- package/src/runtimes/go.js +3 -2
- package/src/runtimes/index.js +7 -4
- package/src/runtimes/native.js +2 -1
- package/src/runtimes/node.js +3 -2
- package/src/runtimes/php.js +3 -2
- package/src/runtimes/python.js +3 -2
- package/src/runtimes/ruby.js +3 -2
- package/src/runtimes/shell.js +3 -2
- package/src/systemd.js +52 -13
- package/src/utils.js +64 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to the `unitup` project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.1.4] - 2026-07-31
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Local project configuration file support (`unitup.config.json` and `.unitup.json`).
|
|
14
|
+
- `unitup init` CLI command to create and initialize `unitup.config.json` in any directory with custom script, runtime, name, env, group, memory limits, and runtime arguments.
|
|
15
|
+
- `--config <path>` flag for specifying custom configuration files in `unitup add` and `unitup init`.
|
|
16
|
+
- Biome (`@biomejs/biome`) developer tool integration for code linting and formatting (`npm run check`, `npm run format`, `npm run lint`).
|
|
17
|
+
- `CHANGELOG.md` tracking project release history and changes.
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
- Fixed script argument resolution in all runtime adapters (`node`, `python`, `ruby`, `php`, `bun`, `deno`, `shell`, `go`, `elixir`, `native`) to resolve relative script paths against `opts.cwd` instead of `process.cwd()`.
|
|
21
|
+
|
|
22
|
+
## [0.1.3] - 2026-07-31
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
- Native systemd timer-based task scheduler (`unitup schedule`).
|
|
26
|
+
- Systemd-native memory limits per service (`--memory-high`, `--memory-max`, `--memory-swap-max`, `--default-memory`).
|
|
27
|
+
- System memory overview command (`unitup memory` / `unitup top`).
|
|
28
|
+
- Extended multi-runtime support for Node.js, Python, Ruby, PHP, Bun, Deno, Go, Elixir, Shell scripts, and compiled native executables.
|
|
29
|
+
- Advanced `journald` log streaming filters and maintenance commands (`disk-usage`, `rotate`, `vacuum`).
|
|
30
|
+
- Readiness diagnostics CLI tool (`unitup doctor`).
|
|
31
|
+
- Service grouping support (`@group`).
|
|
32
|
+
|
|
33
|
+
## [0.1.0] - 2026-07-01
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
- Initial release of `unitup` — minimal, zero-dependency systemd user service wrapper.
|
|
37
|
+
- Basic CLI commands: `add`, `start`, `stop`, `restart`, `status`, `logs`, `remove`, `list`.
|
package/README.md
CHANGED
|
@@ -92,6 +92,40 @@ At its core, `unitup` generates systemd service unit files directly from a gener
|
|
|
92
92
|
|
|
93
93
|
## CLI Usage
|
|
94
94
|
|
|
95
|
+
### Project Configuration (`unitup.config.json`)
|
|
96
|
+
|
|
97
|
+
You can create a local configuration file in any project directory to define default service settings:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
# Initialize a unitup.config.json file in the current folder
|
|
101
|
+
unitup init app.js --name my-app --memory-max 512M --env PORT=3000
|
|
102
|
+
|
|
103
|
+
# Add service using settings from unitup.config.json
|
|
104
|
+
unitup add
|
|
105
|
+
|
|
106
|
+
# Explicit CLI flags override settings in unitup.config.json
|
|
107
|
+
unitup add --memory-max 1G
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Example `unitup.config.json`:
|
|
111
|
+
```json
|
|
112
|
+
{
|
|
113
|
+
"name": "my-app",
|
|
114
|
+
"group": "backend",
|
|
115
|
+
"script": "app.js",
|
|
116
|
+
"runtime": "node",
|
|
117
|
+
"env": {
|
|
118
|
+
"NODE_ENV": "production",
|
|
119
|
+
"PORT": "3000"
|
|
120
|
+
},
|
|
121
|
+
"resources": {
|
|
122
|
+
"memoryMax": "512M",
|
|
123
|
+
"memoryHigh": "400M"
|
|
124
|
+
},
|
|
125
|
+
"restart": "on-failure"
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
95
129
|
### Memory Limits
|
|
96
130
|
|
|
97
131
|
`unitup` allows you to set native systemd memory limits for your services without running background monitoring daemons:
|
package/biome.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
|
|
3
|
+
"vcs": {
|
|
4
|
+
"enabled": true,
|
|
5
|
+
"clientKind": "git",
|
|
6
|
+
"useIgnoreFile": true
|
|
7
|
+
},
|
|
8
|
+
"files": {
|
|
9
|
+
"ignoreUnknown": true,
|
|
10
|
+
"ignore": ["node_modules", "dist", "coverage"]
|
|
11
|
+
},
|
|
12
|
+
"formatter": {
|
|
13
|
+
"enabled": true,
|
|
14
|
+
"indentStyle": "space",
|
|
15
|
+
"indentWidth": 2,
|
|
16
|
+
"lineWidth": 120
|
|
17
|
+
},
|
|
18
|
+
"organizeImports": {
|
|
19
|
+
"enabled": true
|
|
20
|
+
},
|
|
21
|
+
"linter": {
|
|
22
|
+
"enabled": true,
|
|
23
|
+
"rules": {
|
|
24
|
+
"recommended": true,
|
|
25
|
+
"style": {
|
|
26
|
+
"useConstStruct": "off",
|
|
27
|
+
"noUnusedTemplateLiteral": "off"
|
|
28
|
+
},
|
|
29
|
+
"complexity": {
|
|
30
|
+
"noForEach": "off"
|
|
31
|
+
},
|
|
32
|
+
"suspicious": {
|
|
33
|
+
"noEmptyBlockStatements": "off"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"javascript": {
|
|
38
|
+
"formatter": {
|
|
39
|
+
"quoteStyle": "single",
|
|
40
|
+
"jsxQuoteStyle": "double",
|
|
41
|
+
"trailingCommas": "none",
|
|
42
|
+
"semicolons": "always"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
package/index.d.ts
CHANGED
|
@@ -482,6 +482,31 @@ export function checkNodeDiagnostics(customNodePath?: string): Promise<NodeDiagn
|
|
|
482
482
|
export function findNodeExecutable(customPath?: string): Promise<string | null>;
|
|
483
483
|
export function getDoctorInfo(): Promise<DoctorInfo>;
|
|
484
484
|
|
|
485
|
+
export interface ProjectConfig {
|
|
486
|
+
name?: string;
|
|
487
|
+
group?: string;
|
|
488
|
+
script?: string;
|
|
489
|
+
command?: string;
|
|
490
|
+
runtime?: string;
|
|
491
|
+
runtimeArgs?: string[];
|
|
492
|
+
args?: string[];
|
|
493
|
+
env?: Record<string, string>;
|
|
494
|
+
envFile?: string;
|
|
495
|
+
restart?: string;
|
|
496
|
+
resources?: {
|
|
497
|
+
memoryHigh?: string;
|
|
498
|
+
memoryMax?: string;
|
|
499
|
+
memorySwapMax?: string;
|
|
500
|
+
};
|
|
501
|
+
memoryHigh?: string;
|
|
502
|
+
memoryMax?: string;
|
|
503
|
+
memorySwapMax?: string;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
export function findProjectConfig(dirPath?: string): string | null;
|
|
507
|
+
export function readProjectConfig(filePathOrDir?: string): ProjectConfig | null;
|
|
508
|
+
export function saveProjectConfig(dirPathOrFile?: string, config?: ProjectConfig): string;
|
|
509
|
+
|
|
485
510
|
// Path & Unit Helpers
|
|
486
511
|
export function getUserUnitDir(): string;
|
|
487
512
|
export function getUnitPath(name: string): string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "unitup",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "Minimal, zero-dependency systemd-native service manager for any runtime, script, or executable",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"types": "./index.d.ts",
|
|
@@ -9,9 +9,13 @@
|
|
|
9
9
|
"unitup": "bin/unitup.js"
|
|
10
10
|
},
|
|
11
11
|
"scripts": {
|
|
12
|
-
"test": "node --test test/*.test.js"
|
|
12
|
+
"test": "node --test test/*.test.js",
|
|
13
|
+
"lint": "biome check .",
|
|
14
|
+
"format": "biome format --write .",
|
|
15
|
+
"check": "biome check ."
|
|
13
16
|
},
|
|
14
17
|
"devDependencies": {
|
|
18
|
+
"@biomejs/biome": "^1.9.4",
|
|
15
19
|
"typescript": "^5.7.3"
|
|
16
20
|
},
|
|
17
21
|
"keywords": [
|
package/src/cli.js
CHANGED
|
@@ -32,7 +32,16 @@ import {
|
|
|
32
32
|
disableSchedule,
|
|
33
33
|
removeSchedule
|
|
34
34
|
} from './schedule.js';
|
|
35
|
-
import {
|
|
35
|
+
import {
|
|
36
|
+
sanitizeServiceName,
|
|
37
|
+
formatTable,
|
|
38
|
+
readGlobalConfig,
|
|
39
|
+
saveGlobalConfig,
|
|
40
|
+
validateMemorySize,
|
|
41
|
+
findProjectConfig,
|
|
42
|
+
readProjectConfig,
|
|
43
|
+
saveProjectConfig
|
|
44
|
+
} from './utils.js';
|
|
36
45
|
|
|
37
46
|
/**
|
|
38
47
|
* Custom light CLI argument parser without runtime dependencies.
|
|
@@ -51,6 +60,7 @@ export function parseArgs(argv) {
|
|
|
51
60
|
command: '',
|
|
52
61
|
node: '',
|
|
53
62
|
cwd: '',
|
|
63
|
+
config: '',
|
|
54
64
|
env: [],
|
|
55
65
|
envFile: '',
|
|
56
66
|
restart: 'on-failure',
|
|
@@ -147,6 +157,12 @@ export function parseArgs(argv) {
|
|
|
147
157
|
} else if (arg.startsWith('--cwd=')) {
|
|
148
158
|
result.flags.cwd = arg.slice(6);
|
|
149
159
|
i++;
|
|
160
|
+
} else if (arg === '--config') {
|
|
161
|
+
result.flags.config = argv[i + 1] || '';
|
|
162
|
+
i += 2;
|
|
163
|
+
} else if (arg.startsWith('--config=')) {
|
|
164
|
+
result.flags.config = arg.slice(9);
|
|
165
|
+
i++;
|
|
150
166
|
} else if (arg === '--env') {
|
|
151
167
|
if (argv[i + 1]) result.flags.env.push(argv[i + 1]);
|
|
152
168
|
i += 2;
|
|
@@ -350,6 +366,7 @@ unitup - Minimal systemd user service wrapper for any executable & runtime
|
|
|
350
366
|
|
|
351
367
|
Usage:
|
|
352
368
|
unitup doctor Run system readiness and runtime check
|
|
369
|
+
unitup init [script] Create local project config file (unitup.config.json)
|
|
353
370
|
unitup add <script> [options] Add script or executable as systemd user service
|
|
354
371
|
unitup start <name|@group> Start a service (--enable to enable on boot)
|
|
355
372
|
unitup stop <name|@group> Stop a service
|
|
@@ -363,6 +380,9 @@ Usage:
|
|
|
363
380
|
unitup memory / unitup top Show memory usage for all apps or a specific app
|
|
364
381
|
unitup config Manage global configuration (--default-memory 1G)
|
|
365
382
|
|
|
383
|
+
Config Options:
|
|
384
|
+
--config <path> Path to custom project config file (defaults to unitup.config.json)
|
|
385
|
+
|
|
366
386
|
Schedule Commands:
|
|
367
387
|
unitup schedule <script> [options] Create a systemd timer schedule
|
|
368
388
|
unitup schedules / unitup timers List all schedule timers
|
|
@@ -440,10 +460,86 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
440
460
|
break;
|
|
441
461
|
}
|
|
442
462
|
|
|
443
|
-
case '
|
|
463
|
+
case 'init': {
|
|
464
|
+
const targetCwd = flags.cwd ? path.resolve(process.cwd(), flags.cwd) : process.cwd();
|
|
465
|
+
const configFilename = flags.config || 'unitup.config.json';
|
|
466
|
+
const targetConfigPath = path.resolve(targetCwd, configFilename);
|
|
467
|
+
|
|
468
|
+
if (fs.existsSync(targetConfigPath) && !flags.force) {
|
|
469
|
+
throw new Error(
|
|
470
|
+
`Configuration file "${path.basename(targetConfigPath)}" already exists at ${targetConfigPath}.\n` +
|
|
471
|
+
`Use --force (-f) to overwrite.`
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
|
|
444
475
|
const scriptArg = positionals[0];
|
|
476
|
+
let scriptPath = scriptArg || '';
|
|
445
477
|
let name = flags.name;
|
|
446
|
-
|
|
478
|
+
if (!name) {
|
|
479
|
+
if (scriptPath) {
|
|
480
|
+
const baseName = path.basename(scriptPath);
|
|
481
|
+
const ext = path.extname(baseName);
|
|
482
|
+
name = ext ? baseName.slice(0, -ext.length) : baseName;
|
|
483
|
+
} else {
|
|
484
|
+
name = path.basename(targetCwd);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const envObj = {};
|
|
489
|
+
for (const e of flags.env) {
|
|
490
|
+
const idx = e.indexOf('=');
|
|
491
|
+
if (idx !== -1) {
|
|
492
|
+
envObj[e.slice(0, idx)] = e.slice(idx + 1);
|
|
493
|
+
} else {
|
|
494
|
+
envObj[e] = '';
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const projectCfg = {
|
|
499
|
+
name,
|
|
500
|
+
...(scriptPath ? { script: scriptPath } : {}),
|
|
501
|
+
...(flags.command ? { command: flags.command } : {}),
|
|
502
|
+
...(flags.runtime ? { runtime: flags.runtime } : {}),
|
|
503
|
+
...(flags.runtimeArgs && flags.runtimeArgs.length > 0 ? { runtimeArgs: flags.runtimeArgs } : {}),
|
|
504
|
+
group: flags.group || 'default',
|
|
505
|
+
...(Object.keys(envObj).length > 0 ? { env: envObj } : {}),
|
|
506
|
+
...(flags.envFile ? { envFile: flags.envFile } : {}),
|
|
507
|
+
...(flags.restart ? { restart: flags.restart } : { restart: 'on-failure' }),
|
|
508
|
+
...(flags.args && flags.args.length > 0 ? { args: flags.args } : {})
|
|
509
|
+
};
|
|
510
|
+
|
|
511
|
+
if (flags.memoryMax || flags.memoryHigh || flags.memorySwapMax) {
|
|
512
|
+
projectCfg.resources = {};
|
|
513
|
+
if (flags.memoryHigh) projectCfg.resources.memoryHigh = flags.memoryHigh;
|
|
514
|
+
if (flags.memoryMax) projectCfg.resources.memoryMax = flags.memoryMax;
|
|
515
|
+
if (flags.memorySwapMax) projectCfg.resources.memorySwapMax = flags.memorySwapMax;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const writtenPath = saveProjectConfig(targetConfigPath, projectCfg);
|
|
519
|
+
console.log(`✓ Project configuration created at ${writtenPath}`);
|
|
520
|
+
console.log(`Run "unitup add" to register service from configuration.`);
|
|
521
|
+
break;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
case 'add': {
|
|
525
|
+
const targetCwd = flags.cwd ? path.resolve(process.cwd(), flags.cwd) : process.cwd();
|
|
526
|
+
const configPath = flags.config ? path.resolve(targetCwd, flags.config) : findProjectConfig(targetCwd);
|
|
527
|
+
const projectCfg = configPath ? readProjectConfig(configPath) : null;
|
|
528
|
+
|
|
529
|
+
const scriptArg = positionals[0] || (projectCfg ? projectCfg.script : undefined);
|
|
530
|
+
let name = flags.name || (projectCfg ? projectCfg.name : undefined);
|
|
531
|
+
let cmdFlag = flags.command || (projectCfg ? projectCfg.command : undefined);
|
|
532
|
+
let runtimeFlag = flags.runtime || (projectCfg ? projectCfg.runtime : undefined);
|
|
533
|
+
let groupFlag = flags.group || (projectCfg ? projectCfg.group : undefined) || 'default';
|
|
534
|
+
let restartFlag = flags.restart !== 'on-failure' ? flags.restart : ((projectCfg && projectCfg.restart) || 'on-failure');
|
|
535
|
+
let envFileFlag = flags.envFile || (projectCfg ? projectCfg.envFile : undefined);
|
|
536
|
+
let argsFlag = (flags.args && flags.args.length > 0) ? flags.args : ((projectCfg && projectCfg.args) || []);
|
|
537
|
+
let runtimeArgsFlag = (flags.runtimeArgs && flags.runtimeArgs.length > 0) ? flags.runtimeArgs : ((projectCfg && projectCfg.runtimeArgs) || []);
|
|
538
|
+
let memoryHighFlag = flags.memoryHigh || (projectCfg?.resources?.memoryHigh || projectCfg?.memoryHigh || '');
|
|
539
|
+
let memoryMaxFlag = flags.memoryMax || (projectCfg?.resources?.memoryMax || projectCfg?.memoryMax || '');
|
|
540
|
+
let memorySwapMaxFlag = flags.memorySwapMax || (projectCfg?.resources?.memorySwapMax || projectCfg?.memorySwapMax || '');
|
|
541
|
+
|
|
542
|
+
let absScriptPath = scriptArg ? path.resolve(targetCwd, scriptArg) : undefined;
|
|
447
543
|
|
|
448
544
|
if (flags.node) {
|
|
449
545
|
const resolvedNode = await findNodeExecutable(flags.node);
|
|
@@ -452,14 +548,14 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
452
548
|
}
|
|
453
549
|
}
|
|
454
550
|
|
|
455
|
-
if (
|
|
551
|
+
if (cmdFlag) {
|
|
456
552
|
if (!name) {
|
|
457
553
|
if (scriptArg) {
|
|
458
554
|
const baseName = path.basename(scriptArg);
|
|
459
555
|
const ext = path.extname(baseName);
|
|
460
556
|
name = ext ? baseName.slice(0, -ext.length) : baseName;
|
|
461
557
|
} else {
|
|
462
|
-
const baseCmd = path.basename(
|
|
558
|
+
const baseCmd = path.basename(cmdFlag);
|
|
463
559
|
name = baseCmd;
|
|
464
560
|
}
|
|
465
561
|
}
|
|
@@ -477,7 +573,7 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
477
573
|
}
|
|
478
574
|
}
|
|
479
575
|
|
|
480
|
-
const envObj = {};
|
|
576
|
+
const envObj = (projectCfg && projectCfg.env && typeof projectCfg.env === 'object') ? { ...projectCfg.env } : {};
|
|
481
577
|
for (const e of flags.env) {
|
|
482
578
|
const idx = e.indexOf('=');
|
|
483
579
|
if (idx !== -1) {
|
|
@@ -489,21 +585,21 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
489
585
|
|
|
490
586
|
const res = await addService({
|
|
491
587
|
name,
|
|
492
|
-
group:
|
|
588
|
+
group: groupFlag,
|
|
493
589
|
script: absScriptPath,
|
|
494
|
-
command:
|
|
495
|
-
runtime:
|
|
496
|
-
runtimeArgs:
|
|
590
|
+
command: cmdFlag,
|
|
591
|
+
runtime: runtimeFlag,
|
|
592
|
+
runtimeArgs: runtimeArgsFlag,
|
|
497
593
|
nodePath: flags.node,
|
|
498
|
-
cwd:
|
|
594
|
+
cwd: targetCwd,
|
|
499
595
|
env: envObj,
|
|
500
|
-
envFile:
|
|
501
|
-
restart:
|
|
502
|
-
args:
|
|
596
|
+
envFile: envFileFlag ? path.resolve(targetCwd, envFileFlag) : undefined,
|
|
597
|
+
restart: restartFlag,
|
|
598
|
+
args: argsFlag,
|
|
503
599
|
start: flags.start,
|
|
504
|
-
memoryHigh:
|
|
505
|
-
memoryMax:
|
|
506
|
-
memorySwapMax:
|
|
600
|
+
memoryHigh: memoryHighFlag,
|
|
601
|
+
memoryMax: memoryMaxFlag,
|
|
602
|
+
memorySwapMax: memorySwapMaxFlag,
|
|
507
603
|
defaultMemory: flags.defaultMemory,
|
|
508
604
|
force: flags.force
|
|
509
605
|
});
|
package/src/index.js
CHANGED
|
@@ -58,6 +58,9 @@ import {
|
|
|
58
58
|
readGlobalConfig,
|
|
59
59
|
saveGlobalConfig,
|
|
60
60
|
getConfigFilepath,
|
|
61
|
+
findProjectConfig,
|
|
62
|
+
readProjectConfig,
|
|
63
|
+
saveProjectConfig,
|
|
61
64
|
resolveEffectiveMemoryLimits,
|
|
62
65
|
getSchedulesDir,
|
|
63
66
|
getAppsDir,
|
|
@@ -115,6 +118,9 @@ export {
|
|
|
115
118
|
readGlobalConfig,
|
|
116
119
|
saveGlobalConfig,
|
|
117
120
|
getConfigFilepath,
|
|
121
|
+
findProjectConfig,
|
|
122
|
+
readProjectConfig,
|
|
123
|
+
saveProjectConfig,
|
|
118
124
|
resolveEffectiveMemoryLimits,
|
|
119
125
|
getSchedulesDir,
|
|
120
126
|
getAppsDir,
|
package/src/runtimes/bun.js
CHANGED
|
@@ -3,11 +3,12 @@ import fs from 'node:fs';
|
|
|
3
3
|
import { findRuntimeExecutable, getBinaryVersion } from './common.js';
|
|
4
4
|
|
|
5
5
|
export async function createBunAdapter(opts = {}) {
|
|
6
|
+
const baseDir = opts.cwd || process.cwd();
|
|
6
7
|
const customPath = opts.command;
|
|
7
8
|
let execPath = null;
|
|
8
9
|
|
|
9
10
|
if (customPath) {
|
|
10
|
-
const absPath = path.resolve(
|
|
11
|
+
const absPath = path.resolve(baseDir, customPath);
|
|
11
12
|
if (fs.existsSync(absPath)) {
|
|
12
13
|
try {
|
|
13
14
|
fs.accessSync(absPath, fs.constants.X_OK);
|
|
@@ -27,7 +28,7 @@ export async function createBunAdapter(opts = {}) {
|
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
const version = await getBinaryVersion(execPath, '-v');
|
|
30
|
-
const scriptPath = opts.script ? path.resolve(
|
|
31
|
+
const scriptPath = opts.script ? path.resolve(baseDir, opts.script) : null;
|
|
31
32
|
const runtimeArgs = Array.isArray(opts.runtimeArgs) ? opts.runtimeArgs : [];
|
|
32
33
|
const scriptArgs = Array.isArray(opts.args) ? opts.args : [];
|
|
33
34
|
|
package/src/runtimes/deno.js
CHANGED
|
@@ -3,11 +3,12 @@ import fs from 'node:fs';
|
|
|
3
3
|
import { findRuntimeExecutable, getBinaryVersion } from './common.js';
|
|
4
4
|
|
|
5
5
|
export async function createDenoAdapter(opts = {}) {
|
|
6
|
+
const baseDir = opts.cwd || process.cwd();
|
|
6
7
|
const customPath = opts.command;
|
|
7
8
|
let execPath = null;
|
|
8
9
|
|
|
9
10
|
if (customPath) {
|
|
10
|
-
const absPath = path.resolve(
|
|
11
|
+
const absPath = path.resolve(baseDir, customPath);
|
|
11
12
|
if (fs.existsSync(absPath)) {
|
|
12
13
|
try {
|
|
13
14
|
fs.accessSync(absPath, fs.constants.X_OK);
|
|
@@ -27,7 +28,7 @@ export async function createDenoAdapter(opts = {}) {
|
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
const version = await getBinaryVersion(execPath, '--version');
|
|
30
|
-
const scriptPath = opts.script ? path.resolve(
|
|
31
|
+
const scriptPath = opts.script ? path.resolve(baseDir, opts.script) : null;
|
|
31
32
|
const runtimeArgs = Array.isArray(opts.runtimeArgs) ? opts.runtimeArgs : [];
|
|
32
33
|
const scriptArgs = Array.isArray(opts.args) ? opts.args : [];
|
|
33
34
|
|
package/src/runtimes/elixir.js
CHANGED
|
@@ -3,11 +3,12 @@ import fs from 'node:fs';
|
|
|
3
3
|
import { findRuntimeExecutable, getBinaryVersion } from './common.js';
|
|
4
4
|
|
|
5
5
|
export async function createElixirAdapter(opts = {}) {
|
|
6
|
+
const baseDir = opts.cwd || process.cwd();
|
|
6
7
|
const customPath = opts.command;
|
|
7
8
|
let execPath = null;
|
|
8
9
|
|
|
9
10
|
if (customPath) {
|
|
10
|
-
const absPath = path.resolve(
|
|
11
|
+
const absPath = path.resolve(baseDir, customPath);
|
|
11
12
|
if (fs.existsSync(absPath)) {
|
|
12
13
|
try {
|
|
13
14
|
fs.accessSync(absPath, fs.constants.X_OK);
|
|
@@ -27,7 +28,7 @@ export async function createElixirAdapter(opts = {}) {
|
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
const version = await getBinaryVersion(execPath, '-v');
|
|
30
|
-
const scriptPath = opts.script ? path.resolve(
|
|
31
|
+
const scriptPath = opts.script ? path.resolve(baseDir, opts.script) : null;
|
|
31
32
|
const runtimeArgs = Array.isArray(opts.runtimeArgs) ? opts.runtimeArgs : [];
|
|
32
33
|
const scriptArgs = Array.isArray(opts.args) ? opts.args : [];
|
|
33
34
|
|
package/src/runtimes/go.js
CHANGED
|
@@ -3,11 +3,12 @@ import fs from 'node:fs';
|
|
|
3
3
|
import { findRuntimeExecutable, getBinaryVersion } from './common.js';
|
|
4
4
|
|
|
5
5
|
export async function createGoAdapter(opts = {}) {
|
|
6
|
+
const baseDir = opts.cwd || process.cwd();
|
|
6
7
|
const customPath = opts.command;
|
|
7
8
|
let execPath = null;
|
|
8
9
|
|
|
9
10
|
if (customPath) {
|
|
10
|
-
const absPath = path.resolve(
|
|
11
|
+
const absPath = path.resolve(baseDir, customPath);
|
|
11
12
|
if (fs.existsSync(absPath)) {
|
|
12
13
|
try {
|
|
13
14
|
fs.accessSync(absPath, fs.constants.X_OK);
|
|
@@ -27,7 +28,7 @@ export async function createGoAdapter(opts = {}) {
|
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
const version = await getBinaryVersion(execPath, 'version');
|
|
30
|
-
const scriptPath = opts.script ? path.resolve(
|
|
31
|
+
const scriptPath = opts.script ? path.resolve(baseDir, opts.script) : null;
|
|
31
32
|
const runtimeArgs = Array.isArray(opts.runtimeArgs) ? opts.runtimeArgs : [];
|
|
32
33
|
const scriptArgs = Array.isArray(opts.args) ? opts.args : [];
|
|
33
34
|
|
package/src/runtimes/index.js
CHANGED
|
@@ -11,7 +11,7 @@ import { createNativeAdapter } from './native.js';
|
|
|
11
11
|
import { createGoAdapter } from './go.js';
|
|
12
12
|
import { createElixirAdapter } from './elixir.js';
|
|
13
13
|
|
|
14
|
-
export function detectRuntime(filepath) {
|
|
14
|
+
export function detectRuntime(filepath, opts = {}) {
|
|
15
15
|
if (!filepath) {
|
|
16
16
|
throw new Error('Script path or command is required for runtime detection.');
|
|
17
17
|
}
|
|
@@ -42,7 +42,8 @@ export function detectRuntime(filepath) {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
// Inspect shebang header if file exists
|
|
45
|
-
const
|
|
45
|
+
const baseDir = opts.cwd || process.cwd();
|
|
46
|
+
const absPath = path.resolve(baseDir, filepath);
|
|
46
47
|
if (fs.existsSync(absPath)) {
|
|
47
48
|
try {
|
|
48
49
|
const fd = fs.openSync(absPath, 'r');
|
|
@@ -85,9 +86,11 @@ export function detectRuntime(filepath) {
|
|
|
85
86
|
}
|
|
86
87
|
|
|
87
88
|
export async function resolveRuntimeConfig(opts = {}) {
|
|
89
|
+
const baseDir = opts.cwd || process.cwd();
|
|
90
|
+
|
|
88
91
|
// 1. If explicit --command is provided, bypass runtime auto-detection
|
|
89
92
|
if (opts.command) {
|
|
90
|
-
const absCommand = path.resolve(
|
|
93
|
+
const absCommand = path.resolve(baseDir, opts.command);
|
|
91
94
|
if (!fs.existsSync(absCommand)) {
|
|
92
95
|
// If binary doesn't exist as exact file path, check if it's in PATH or native executable
|
|
93
96
|
const { findRuntimeExecutable } = await import('./common.js');
|
|
@@ -116,7 +119,7 @@ export async function resolveRuntimeConfig(opts = {}) {
|
|
|
116
119
|
// 2. Resolve target runtime name
|
|
117
120
|
let runtimeName = opts.runtime;
|
|
118
121
|
if (!runtimeName) {
|
|
119
|
-
runtimeName = detectRuntime(opts.script);
|
|
122
|
+
runtimeName = detectRuntime(opts.script, opts);
|
|
120
123
|
}
|
|
121
124
|
|
|
122
125
|
const cleanRuntime = runtimeName.toLowerCase();
|
package/src/runtimes/native.js
CHANGED
|
@@ -2,13 +2,14 @@ import path from 'node:path';
|
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
|
|
4
4
|
export async function createNativeAdapter(opts = {}) {
|
|
5
|
+
const baseDir = opts.cwd || process.cwd();
|
|
5
6
|
const targetPath = opts.script || opts.command;
|
|
6
7
|
|
|
7
8
|
if (!targetPath) {
|
|
8
9
|
throw new Error('Native executable path is required.');
|
|
9
10
|
}
|
|
10
11
|
|
|
11
|
-
const absPath = path.resolve(
|
|
12
|
+
const absPath = path.resolve(baseDir, targetPath);
|
|
12
13
|
|
|
13
14
|
if (!fs.existsSync(absPath)) {
|
|
14
15
|
throw new Error(`Executable file does not exist: ${absPath}`);
|
package/src/runtimes/node.js
CHANGED
|
@@ -3,11 +3,12 @@ import fs from 'node:fs';
|
|
|
3
3
|
import { findRuntimeExecutable, getBinaryVersion } from './common.js';
|
|
4
4
|
|
|
5
5
|
export async function createNodeAdapter(opts = {}) {
|
|
6
|
+
const baseDir = opts.cwd || process.cwd();
|
|
6
7
|
const customPath = opts.command || opts.nodePath;
|
|
7
8
|
let execPath = null;
|
|
8
9
|
|
|
9
10
|
if (customPath) {
|
|
10
|
-
const absPath = path.resolve(
|
|
11
|
+
const absPath = path.resolve(baseDir, customPath);
|
|
11
12
|
if (fs.existsSync(absPath)) {
|
|
12
13
|
try {
|
|
13
14
|
fs.accessSync(absPath, fs.constants.X_OK);
|
|
@@ -38,7 +39,7 @@ export async function createNodeAdapter(opts = {}) {
|
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
const version = await getBinaryVersion(execPath, '-v');
|
|
41
|
-
const scriptPath = opts.script ? path.resolve(
|
|
42
|
+
const scriptPath = opts.script ? path.resolve(baseDir, opts.script) : null;
|
|
42
43
|
const runtimeArgs = Array.isArray(opts.runtimeArgs) ? opts.runtimeArgs : [];
|
|
43
44
|
const scriptArgs = Array.isArray(opts.args) ? opts.args : [];
|
|
44
45
|
|
package/src/runtimes/php.js
CHANGED
|
@@ -3,11 +3,12 @@ import fs from 'node:fs';
|
|
|
3
3
|
import { findRuntimeExecutable, getBinaryVersion } from './common.js';
|
|
4
4
|
|
|
5
5
|
export async function createPhpAdapter(opts = {}) {
|
|
6
|
+
const baseDir = opts.cwd || process.cwd();
|
|
6
7
|
const customPath = opts.command;
|
|
7
8
|
let execPath = null;
|
|
8
9
|
|
|
9
10
|
if (customPath) {
|
|
10
|
-
const absPath = path.resolve(
|
|
11
|
+
const absPath = path.resolve(baseDir, customPath);
|
|
11
12
|
if (fs.existsSync(absPath)) {
|
|
12
13
|
try {
|
|
13
14
|
fs.accessSync(absPath, fs.constants.X_OK);
|
|
@@ -27,7 +28,7 @@ export async function createPhpAdapter(opts = {}) {
|
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
const version = await getBinaryVersion(execPath, '-v');
|
|
30
|
-
const scriptPath = opts.script ? path.resolve(
|
|
31
|
+
const scriptPath = opts.script ? path.resolve(baseDir, opts.script) : null;
|
|
31
32
|
const runtimeArgs = Array.isArray(opts.runtimeArgs) ? opts.runtimeArgs : [];
|
|
32
33
|
const scriptArgs = Array.isArray(opts.args) ? opts.args : [];
|
|
33
34
|
|
package/src/runtimes/python.js
CHANGED
|
@@ -3,11 +3,12 @@ import fs from 'node:fs';
|
|
|
3
3
|
import { findRuntimeExecutable, getBinaryVersion } from './common.js';
|
|
4
4
|
|
|
5
5
|
export async function createPythonAdapter(opts = {}) {
|
|
6
|
+
const baseDir = opts.cwd || process.cwd();
|
|
6
7
|
const customPath = opts.command;
|
|
7
8
|
let execPath = null;
|
|
8
9
|
|
|
9
10
|
if (customPath) {
|
|
10
|
-
const absPath = path.resolve(
|
|
11
|
+
const absPath = path.resolve(baseDir, customPath);
|
|
11
12
|
if (fs.existsSync(absPath)) {
|
|
12
13
|
try {
|
|
13
14
|
fs.accessSync(absPath, fs.constants.X_OK);
|
|
@@ -27,7 +28,7 @@ export async function createPythonAdapter(opts = {}) {
|
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
const version = await getBinaryVersion(execPath, '--version');
|
|
30
|
-
const scriptPath = opts.script ? path.resolve(
|
|
31
|
+
const scriptPath = opts.script ? path.resolve(baseDir, opts.script) : null;
|
|
31
32
|
const runtimeArgs = Array.isArray(opts.runtimeArgs) ? opts.runtimeArgs : [];
|
|
32
33
|
const scriptArgs = Array.isArray(opts.args) ? opts.args : [];
|
|
33
34
|
|
package/src/runtimes/ruby.js
CHANGED
|
@@ -3,11 +3,12 @@ import fs from 'node:fs';
|
|
|
3
3
|
import { findRuntimeExecutable, getBinaryVersion } from './common.js';
|
|
4
4
|
|
|
5
5
|
export async function createRubyAdapter(opts = {}) {
|
|
6
|
+
const baseDir = opts.cwd || process.cwd();
|
|
6
7
|
const customPath = opts.command;
|
|
7
8
|
let execPath = null;
|
|
8
9
|
|
|
9
10
|
if (customPath) {
|
|
10
|
-
const absPath = path.resolve(
|
|
11
|
+
const absPath = path.resolve(baseDir, customPath);
|
|
11
12
|
if (fs.existsSync(absPath)) {
|
|
12
13
|
try {
|
|
13
14
|
fs.accessSync(absPath, fs.constants.X_OK);
|
|
@@ -27,7 +28,7 @@ export async function createRubyAdapter(opts = {}) {
|
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
const version = await getBinaryVersion(execPath, '-v');
|
|
30
|
-
const scriptPath = opts.script ? path.resolve(
|
|
31
|
+
const scriptPath = opts.script ? path.resolve(baseDir, opts.script) : null;
|
|
31
32
|
const runtimeArgs = Array.isArray(opts.runtimeArgs) ? opts.runtimeArgs : [];
|
|
32
33
|
const scriptArgs = Array.isArray(opts.args) ? opts.args : [];
|
|
33
34
|
|
package/src/runtimes/shell.js
CHANGED
|
@@ -3,11 +3,12 @@ import fs from 'node:fs';
|
|
|
3
3
|
import { findRuntimeExecutable, getBinaryVersion } from './common.js';
|
|
4
4
|
|
|
5
5
|
export async function createShellAdapter(opts = {}) {
|
|
6
|
+
const baseDir = opts.cwd || process.cwd();
|
|
6
7
|
const customPath = opts.command;
|
|
7
8
|
let execPath = null;
|
|
8
9
|
|
|
9
10
|
if (customPath) {
|
|
10
|
-
const absPath = path.resolve(
|
|
11
|
+
const absPath = path.resolve(baseDir, customPath);
|
|
11
12
|
if (fs.existsSync(absPath)) {
|
|
12
13
|
try {
|
|
13
14
|
fs.accessSync(absPath, fs.constants.X_OK);
|
|
@@ -27,7 +28,7 @@ export async function createShellAdapter(opts = {}) {
|
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
const version = await getBinaryVersion(execPath, '--version');
|
|
30
|
-
const scriptPath = opts.script ? path.resolve(
|
|
31
|
+
const scriptPath = opts.script ? path.resolve(baseDir, opts.script) : null;
|
|
31
32
|
const runtimeArgs = Array.isArray(opts.runtimeArgs) ? opts.runtimeArgs : [];
|
|
32
33
|
const scriptArgs = Array.isArray(opts.args) ? opts.args : [];
|
|
33
34
|
|
package/src/systemd.js
CHANGED
|
@@ -5,8 +5,10 @@ import path from 'node:path';
|
|
|
5
5
|
import {
|
|
6
6
|
sanitizeServiceName,
|
|
7
7
|
getUnitFilename,
|
|
8
|
+
getTimerFilename,
|
|
8
9
|
formatRelativeTime,
|
|
9
|
-
readAppMetadata
|
|
10
|
+
readAppMetadata,
|
|
11
|
+
readScheduleMetadata
|
|
10
12
|
} from './utils.js';
|
|
11
13
|
import {
|
|
12
14
|
getUserUnitDir,
|
|
@@ -516,7 +518,7 @@ export async function getServiceStatus(name) {
|
|
|
516
518
|
? formatRelativeTime(startedRaw)
|
|
517
519
|
: 'never';
|
|
518
520
|
|
|
519
|
-
const meta = readAppMetadata(safeName);
|
|
521
|
+
const meta = readAppMetadata(safeName) || readScheduleMetadata(safeName);
|
|
520
522
|
const command = meta?.command || parsed.command || meta?.node || process.execPath;
|
|
521
523
|
const argsList = meta?.args || (parsed.script ? [parsed.script] : []);
|
|
522
524
|
|
|
@@ -555,7 +557,7 @@ export async function getServicesByGroup(groupName) {
|
|
|
555
557
|
const units = listUnitFiles();
|
|
556
558
|
const matched = [];
|
|
557
559
|
for (const unit of units) {
|
|
558
|
-
const meta = readAppMetadata(unit.name);
|
|
560
|
+
const meta = readAppMetadata(unit.name) || readScheduleMetadata(unit.name);
|
|
559
561
|
const itemGroup = meta?.group || 'default';
|
|
560
562
|
if (itemGroup.toLowerCase() === cleanGroup.toLowerCase()) {
|
|
561
563
|
matched.push(unit.name);
|
|
@@ -572,7 +574,7 @@ export async function listServices(filterOpts = {}) {
|
|
|
572
574
|
: null;
|
|
573
575
|
|
|
574
576
|
for (const unit of units) {
|
|
575
|
-
const meta = readAppMetadata(unit.name);
|
|
577
|
+
const meta = readAppMetadata(unit.name) || readScheduleMetadata(unit.name);
|
|
576
578
|
const group = meta?.group || 'default';
|
|
577
579
|
const runtime = meta?.runtime || 'node';
|
|
578
580
|
|
|
@@ -604,14 +606,35 @@ export async function listServices(filterOpts = {}) {
|
|
|
604
606
|
const unitFileState = show.UnitFileState || 'unknown';
|
|
605
607
|
|
|
606
608
|
let status = 'stopped';
|
|
607
|
-
if (
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
609
|
+
if (meta?.type === 'timer' || meta?.schedule) {
|
|
610
|
+
if (activeState === 'active' && subState === 'running') {
|
|
611
|
+
status = 'running';
|
|
612
|
+
} else if (activeState === 'failed') {
|
|
613
|
+
status = 'failed';
|
|
614
|
+
} else {
|
|
615
|
+
try {
|
|
616
|
+
const timerShow = await getServiceShow(getTimerFilename(unit.name));
|
|
617
|
+
if (timerShow.ActiveState === 'active') {
|
|
618
|
+
status = 'scheduled';
|
|
619
|
+
} else if (timerShow.UnitFileState === 'disabled' || timerShow.ActiveState === 'inactive') {
|
|
620
|
+
status = 'disabled';
|
|
621
|
+
} else {
|
|
622
|
+
status = 'scheduled';
|
|
623
|
+
}
|
|
624
|
+
} catch {
|
|
625
|
+
status = 'scheduled';
|
|
626
|
+
}
|
|
627
|
+
}
|
|
613
628
|
} else {
|
|
614
|
-
|
|
629
|
+
if (activeState === 'active' && subState === 'running') {
|
|
630
|
+
status = 'running';
|
|
631
|
+
} else if (activeState === 'failed') {
|
|
632
|
+
status = 'failed';
|
|
633
|
+
} else if (activeState === 'active') {
|
|
634
|
+
status = subState;
|
|
635
|
+
} else {
|
|
636
|
+
status = 'stopped';
|
|
637
|
+
}
|
|
615
638
|
}
|
|
616
639
|
|
|
617
640
|
const enabled = unitFileState.startsWith('enabled') ? 'yes' : 'no';
|
|
@@ -657,16 +680,32 @@ export async function inspectService(name) {
|
|
|
657
680
|
throw new Error(`Service "${safeName}" does not exist.`);
|
|
658
681
|
}
|
|
659
682
|
|
|
660
|
-
const meta = readAppMetadata(safeName);
|
|
683
|
+
const meta = readAppMetadata(safeName) || readScheduleMetadata(safeName);
|
|
661
684
|
const statusObj = await getServiceStatus(safeName);
|
|
662
685
|
|
|
686
|
+
let inspectStatus = statusObj.status;
|
|
687
|
+
if ((meta?.type === 'timer' || meta?.schedule) && inspectStatus === 'stopped') {
|
|
688
|
+
try {
|
|
689
|
+
const timerShow = await getServiceShow(getTimerFilename(safeName));
|
|
690
|
+
if (timerShow.ActiveState === 'active') {
|
|
691
|
+
inspectStatus = 'scheduled';
|
|
692
|
+
} else if (timerShow.UnitFileState === 'disabled' || timerShow.ActiveState === 'inactive') {
|
|
693
|
+
inspectStatus = 'disabled';
|
|
694
|
+
} else {
|
|
695
|
+
inspectStatus = 'scheduled';
|
|
696
|
+
}
|
|
697
|
+
} catch {
|
|
698
|
+
inspectStatus = 'scheduled';
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
663
702
|
const command = meta?.command || statusObj.command || statusObj.node || process.execPath;
|
|
664
703
|
const argsList = meta?.args || (statusObj.script ? [statusObj.script] : []);
|
|
665
704
|
|
|
666
705
|
return {
|
|
667
706
|
name: safeName,
|
|
668
707
|
runtime: meta?.runtime || 'node',
|
|
669
|
-
status:
|
|
708
|
+
status: inspectStatus,
|
|
670
709
|
activeState: statusObj.activeState,
|
|
671
710
|
subState: statusObj.subState,
|
|
672
711
|
command,
|
package/src/utils.js
CHANGED
|
@@ -174,7 +174,10 @@ export function saveAppMetadata(meta) {
|
|
|
174
174
|
*/
|
|
175
175
|
export function readAppMetadata(name) {
|
|
176
176
|
try {
|
|
177
|
-
|
|
177
|
+
let filePath = getAppMetadataPath(name);
|
|
178
|
+
if (!fs.existsSync(filePath)) {
|
|
179
|
+
filePath = getScheduleMetadataPath(name);
|
|
180
|
+
}
|
|
178
181
|
if (!fs.existsSync(filePath)) return null;
|
|
179
182
|
const content = fs.readFileSync(filePath, 'utf8');
|
|
180
183
|
const data = JSON.parse(content);
|
|
@@ -696,4 +699,64 @@ export function resolveEffectiveMemoryLimits(opts = {}) {
|
|
|
696
699
|
return {};
|
|
697
700
|
}
|
|
698
701
|
|
|
702
|
+
/**
|
|
703
|
+
* Finds the project configuration file path if it exists in the target directory.
|
|
704
|
+
* Standard names checked: unitup.config.json, .unitup.json
|
|
705
|
+
*
|
|
706
|
+
* @param {string} [dirPath=process.cwd()]
|
|
707
|
+
* @returns {string|null}
|
|
708
|
+
*/
|
|
709
|
+
export function findProjectConfig(dirPath = process.cwd()) {
|
|
710
|
+
const dir = path.resolve(dirPath);
|
|
711
|
+
const candidates = ['unitup.config.json', '.unitup.json'];
|
|
712
|
+
for (const file of candidates) {
|
|
713
|
+
const full = path.join(dir, file);
|
|
714
|
+
if (fs.existsSync(full)) {
|
|
715
|
+
return full;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
return null;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
/**
|
|
722
|
+
* Reads a project configuration file.
|
|
723
|
+
*
|
|
724
|
+
* @param {string} [filePathOrDir=process.cwd()]
|
|
725
|
+
* @returns {object|null}
|
|
726
|
+
*/
|
|
727
|
+
export function readProjectConfig(filePathOrDir = process.cwd()) {
|
|
728
|
+
try {
|
|
729
|
+
let filePath = filePathOrDir;
|
|
730
|
+
if (fs.existsSync(filePath) && fs.statSync(filePath).isDirectory()) {
|
|
731
|
+
filePath = findProjectConfig(filePath);
|
|
732
|
+
}
|
|
733
|
+
if (!filePath || !fs.existsSync(filePath)) return null;
|
|
734
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
735
|
+
return JSON.parse(content) || null;
|
|
736
|
+
} catch {
|
|
737
|
+
return null;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
/**
|
|
742
|
+
* Saves a project configuration file (`unitup.config.json`).
|
|
743
|
+
*
|
|
744
|
+
* @param {string} [dirPathOrFile=process.cwd()]
|
|
745
|
+
* @param {object} config
|
|
746
|
+
* @returns {string} File path written to
|
|
747
|
+
*/
|
|
748
|
+
export function saveProjectConfig(dirPathOrFile = process.cwd(), config = {}) {
|
|
749
|
+
let filePath = dirPathOrFile;
|
|
750
|
+
if (!filePath.endsWith('.json')) {
|
|
751
|
+
filePath = path.join(filePath, 'unitup.config.json');
|
|
752
|
+
}
|
|
753
|
+
const dir = path.dirname(filePath);
|
|
754
|
+
if (!fs.existsSync(dir)) {
|
|
755
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
756
|
+
}
|
|
757
|
+
fs.writeFileSync(filePath, JSON.stringify(config, null, 2), 'utf8');
|
|
758
|
+
return filePath;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
|
|
699
762
|
|