unitup 0.1.2 → 0.1.4
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 +34 -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 +121 -19
- package/src/index.js +6 -0
- package/src/systemd.js +52 -13
- package/src/utils.js +85 -5
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
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
|
+
## [0.1.3] - 2026-07-31
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
- Native systemd timer-based task scheduler (`unitup schedule`).
|
|
23
|
+
- Systemd-native memory limits per service (`--memory-high`, `--memory-max`, `--memory-swap-max`, `--default-memory`).
|
|
24
|
+
- System memory overview command (`unitup memory` / `unitup top`).
|
|
25
|
+
- Extended multi-runtime support for Node.js, Python, Ruby, PHP, Bun, Deno, Go, Elixir, Shell scripts, and compiled native executables.
|
|
26
|
+
- Advanced `journald` log streaming filters and maintenance commands (`disk-usage`, `rotate`, `vacuum`).
|
|
27
|
+
- Readiness diagnostics CLI tool (`unitup doctor`).
|
|
28
|
+
- Service grouping support (`@group`).
|
|
29
|
+
|
|
30
|
+
## [0.1.0] - 2026-07-01
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
- Initial release of `unitup` — minimal, zero-dependency systemd user service wrapper.
|
|
34
|
+
- 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.4",
|
|
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',
|
|
@@ -108,12 +118,15 @@ export function parseArgs(argv) {
|
|
|
108
118
|
} else if (arg.startsWith('--runtime=')) {
|
|
109
119
|
result.flags.runtime = arg.slice(10);
|
|
110
120
|
i++;
|
|
111
|
-
} else if (arg === '--runtime-arg') {
|
|
121
|
+
} else if (arg === '--runtime-arg' || arg === '--runtime-args') {
|
|
112
122
|
if (argv[i + 1]) result.flags.runtimeArgs.push(argv[i + 1]);
|
|
113
123
|
i += 2;
|
|
114
124
|
} else if (arg.startsWith('--runtime-arg=')) {
|
|
115
125
|
result.flags.runtimeArgs.push(arg.slice(14));
|
|
116
126
|
i++;
|
|
127
|
+
} else if (arg.startsWith('--runtime-args=')) {
|
|
128
|
+
result.flags.runtimeArgs.push(arg.slice(15));
|
|
129
|
+
i++;
|
|
117
130
|
} else if (arg === '--command') {
|
|
118
131
|
result.flags.command = argv[i + 1] || '';
|
|
119
132
|
i += 2;
|
|
@@ -144,6 +157,12 @@ export function parseArgs(argv) {
|
|
|
144
157
|
} else if (arg.startsWith('--cwd=')) {
|
|
145
158
|
result.flags.cwd = arg.slice(6);
|
|
146
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++;
|
|
147
166
|
} else if (arg === '--env') {
|
|
148
167
|
if (argv[i + 1]) result.flags.env.push(argv[i + 1]);
|
|
149
168
|
i += 2;
|
|
@@ -162,12 +181,15 @@ export function parseArgs(argv) {
|
|
|
162
181
|
} else if (arg.startsWith('--restart=')) {
|
|
163
182
|
result.flags.restart = arg.slice(10);
|
|
164
183
|
i++;
|
|
165
|
-
} else if (arg === '--arg') {
|
|
184
|
+
} else if (arg === '--arg' || arg === '--args') {
|
|
166
185
|
if (argv[i + 1]) result.flags.args.push(argv[i + 1]);
|
|
167
186
|
i += 2;
|
|
168
187
|
} else if (arg.startsWith('--arg=')) {
|
|
169
188
|
result.flags.args.push(arg.slice(6));
|
|
170
189
|
i++;
|
|
190
|
+
} else if (arg.startsWith('--args=')) {
|
|
191
|
+
result.flags.args.push(arg.slice(7));
|
|
192
|
+
i++;
|
|
171
193
|
} else if (arg === '--lines' || arg === '-n') {
|
|
172
194
|
result.flags.lines = parseInt(argv[i + 1] || '100', 10);
|
|
173
195
|
i += 2;
|
|
@@ -344,6 +366,7 @@ unitup - Minimal systemd user service wrapper for any executable & runtime
|
|
|
344
366
|
|
|
345
367
|
Usage:
|
|
346
368
|
unitup doctor Run system readiness and runtime check
|
|
369
|
+
unitup init [script] Create local project config file (unitup.config.json)
|
|
347
370
|
unitup add <script> [options] Add script or executable as systemd user service
|
|
348
371
|
unitup start <name|@group> Start a service (--enable to enable on boot)
|
|
349
372
|
unitup stop <name|@group> Stop a service
|
|
@@ -357,6 +380,9 @@ Usage:
|
|
|
357
380
|
unitup memory / unitup top Show memory usage for all apps or a specific app
|
|
358
381
|
unitup config Manage global configuration (--default-memory 1G)
|
|
359
382
|
|
|
383
|
+
Config Options:
|
|
384
|
+
--config <path> Path to custom project config file (defaults to unitup.config.json)
|
|
385
|
+
|
|
360
386
|
Schedule Commands:
|
|
361
387
|
unitup schedule <script> [options] Create a systemd timer schedule
|
|
362
388
|
unitup schedules / unitup timers List all schedule timers
|
|
@@ -434,10 +460,86 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
434
460
|
break;
|
|
435
461
|
}
|
|
436
462
|
|
|
437
|
-
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
|
+
|
|
438
475
|
const scriptArg = positionals[0];
|
|
476
|
+
let scriptPath = scriptArg || '';
|
|
439
477
|
let name = flags.name;
|
|
440
|
-
|
|
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;
|
|
441
543
|
|
|
442
544
|
if (flags.node) {
|
|
443
545
|
const resolvedNode = await findNodeExecutable(flags.node);
|
|
@@ -446,14 +548,14 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
446
548
|
}
|
|
447
549
|
}
|
|
448
550
|
|
|
449
|
-
if (
|
|
551
|
+
if (cmdFlag) {
|
|
450
552
|
if (!name) {
|
|
451
553
|
if (scriptArg) {
|
|
452
554
|
const baseName = path.basename(scriptArg);
|
|
453
555
|
const ext = path.extname(baseName);
|
|
454
556
|
name = ext ? baseName.slice(0, -ext.length) : baseName;
|
|
455
557
|
} else {
|
|
456
|
-
const baseCmd = path.basename(
|
|
558
|
+
const baseCmd = path.basename(cmdFlag);
|
|
457
559
|
name = baseCmd;
|
|
458
560
|
}
|
|
459
561
|
}
|
|
@@ -471,7 +573,7 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
471
573
|
}
|
|
472
574
|
}
|
|
473
575
|
|
|
474
|
-
const envObj = {};
|
|
576
|
+
const envObj = (projectCfg && projectCfg.env && typeof projectCfg.env === 'object') ? { ...projectCfg.env } : {};
|
|
475
577
|
for (const e of flags.env) {
|
|
476
578
|
const idx = e.indexOf('=');
|
|
477
579
|
if (idx !== -1) {
|
|
@@ -483,21 +585,21 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
483
585
|
|
|
484
586
|
const res = await addService({
|
|
485
587
|
name,
|
|
486
|
-
group:
|
|
588
|
+
group: groupFlag,
|
|
487
589
|
script: absScriptPath,
|
|
488
|
-
command:
|
|
489
|
-
runtime:
|
|
490
|
-
runtimeArgs:
|
|
590
|
+
command: cmdFlag,
|
|
591
|
+
runtime: runtimeFlag,
|
|
592
|
+
runtimeArgs: runtimeArgsFlag,
|
|
491
593
|
nodePath: flags.node,
|
|
492
|
-
cwd:
|
|
594
|
+
cwd: targetCwd,
|
|
493
595
|
env: envObj,
|
|
494
|
-
envFile:
|
|
495
|
-
restart:
|
|
496
|
-
args:
|
|
596
|
+
envFile: envFileFlag ? path.resolve(targetCwd, envFileFlag) : undefined,
|
|
597
|
+
restart: restartFlag,
|
|
598
|
+
args: argsFlag,
|
|
497
599
|
start: flags.start,
|
|
498
|
-
memoryHigh:
|
|
499
|
-
memoryMax:
|
|
500
|
-
memorySwapMax:
|
|
600
|
+
memoryHigh: memoryHighFlag,
|
|
601
|
+
memoryMax: memoryMaxFlag,
|
|
602
|
+
memorySwapMax: memorySwapMaxFlag,
|
|
501
603
|
defaultMemory: flags.defaultMemory,
|
|
502
604
|
force: flags.force
|
|
503
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/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,16 +174,26 @@ 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);
|
|
181
184
|
if (!data) return null;
|
|
182
185
|
|
|
183
|
-
// Normalization for legacy metadata format
|
|
184
186
|
const command = data.command || data.node || process.execPath;
|
|
185
187
|
const args = data.args || (data.script ? [data.script] : []);
|
|
186
|
-
|
|
188
|
+
let runtime = data.runtime;
|
|
189
|
+
if (!runtime || (runtime === 'node' && data.command)) {
|
|
190
|
+
const baseCmd = path.basename(command).toLowerCase();
|
|
191
|
+
if (baseCmd !== 'node' && baseCmd !== 'nodejs') {
|
|
192
|
+
runtime = 'custom';
|
|
193
|
+
} else {
|
|
194
|
+
runtime = 'node';
|
|
195
|
+
}
|
|
196
|
+
}
|
|
187
197
|
const script = data.script || args[0] || command;
|
|
188
198
|
const cwd = data.cwd || (script ? path.dirname(script) : process.cwd());
|
|
189
199
|
|
|
@@ -472,7 +482,7 @@ export function saveScheduleMetadata(meta) {
|
|
|
472
482
|
name: safeName,
|
|
473
483
|
group: meta.group || 'default',
|
|
474
484
|
type: 'timer',
|
|
475
|
-
runtime: meta.runtime || 'node',
|
|
485
|
+
runtime: meta.runtime || (meta.command ? 'custom' : 'node'),
|
|
476
486
|
command,
|
|
477
487
|
args,
|
|
478
488
|
cwd: meta.cwd ? resolveAbsolutePath(meta.cwd) : process.cwd(),
|
|
@@ -502,7 +512,17 @@ export function readScheduleMetadata(name) {
|
|
|
502
512
|
const filePath = getScheduleMetadataPath(name);
|
|
503
513
|
if (!fs.existsSync(filePath)) return null;
|
|
504
514
|
const content = fs.readFileSync(filePath, 'utf8');
|
|
505
|
-
|
|
515
|
+
const meta = JSON.parse(content);
|
|
516
|
+
if (!meta) return null;
|
|
517
|
+
|
|
518
|
+
// Normalization for legacy metadata format where runtime defaulted to 'node' for custom commands
|
|
519
|
+
if (meta.runtime === 'node' && meta.command) {
|
|
520
|
+
const baseCmd = path.basename(meta.command).toLowerCase();
|
|
521
|
+
if (baseCmd !== 'node' && baseCmd !== 'nodejs') {
|
|
522
|
+
meta.runtime = 'custom';
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
return meta;
|
|
506
526
|
} catch {
|
|
507
527
|
return null;
|
|
508
528
|
}
|
|
@@ -679,4 +699,64 @@ export function resolveEffectiveMemoryLimits(opts = {}) {
|
|
|
679
699
|
return {};
|
|
680
700
|
}
|
|
681
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
|
+
|
|
682
762
|
|