@phreshos/cli 0.1.5 → 0.1.7
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/LICENSE +21 -0
- package/README.md +45 -12
- package/dist/cli.js +16 -3
- package/dist/create.js +4 -19
- package/dist/project-dependency.js +3 -52
- package/dist/prompts.js +18 -8
- package/dist/style.js +10 -0
- package/dist/system/command.js +96 -0
- package/dist/system/installation.js +241 -0
- package/dist/system/lifecycle.js +176 -0
- package/dist/system/node.js +13 -0
- package/dist/system/paths.js +28 -0
- package/dist/system/process.js +23 -0
- package/dist/system/readiness.js +29 -0
- package/dist/system/release.js +86 -0
- package/dist/system/service/index.js +11 -0
- package/dist/system/service/linux.js +99 -0
- package/dist/system/service/macos.js +123 -0
- package/dist/system/types.js +0 -0
- package/dist/template/package.json +1 -1
- package/package.json +30 -1
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { execute } from "../process.js";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { mkdir, rename, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { randomUUID } from "node:crypto";
|
|
6
|
+
const command = "systemctl";
|
|
7
|
+
const unit = "phreshos.service";
|
|
8
|
+
export default class LinuxSystemService {
|
|
9
|
+
run;
|
|
10
|
+
file;
|
|
11
|
+
constructor(userHome, run = execute) {
|
|
12
|
+
this.run = run;
|
|
13
|
+
this.file = join(userHome, ".config", "systemd", "user", unit);
|
|
14
|
+
}
|
|
15
|
+
async inspect() {
|
|
16
|
+
const registered = existsSync(this.file);
|
|
17
|
+
const active = await this.run(command, ["--user", "is-active", "--quiet", unit]);
|
|
18
|
+
const enabled = await this.run(command, ["--user", "is-enabled", "--quiet", unit]);
|
|
19
|
+
const pid = active.code === 0 ? await this.run(command, ["--user", "show", unit, "--property", "MainPID", "--value"]) : undefined;
|
|
20
|
+
const value = pid && /^[0-9]+$/.test(pid.stdout.trim()) ? Number(pid.stdout.trim()) : undefined;
|
|
21
|
+
return {
|
|
22
|
+
registered,
|
|
23
|
+
enabled: registered && enabled.code === 0,
|
|
24
|
+
running: registered && active.code === 0,
|
|
25
|
+
...(value ? { pid: value } : {})
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
async register(definition) {
|
|
29
|
+
await this.stop();
|
|
30
|
+
await mkdir(dirname(this.file), { recursive: true });
|
|
31
|
+
await mkdir(dirname(definition.output), { recursive: true });
|
|
32
|
+
const temporary = `${this.file}.${randomUUID()}.tmp`;
|
|
33
|
+
try {
|
|
34
|
+
await writeFile(temporary, service(definition), { mode: 0o600 });
|
|
35
|
+
await rename(temporary, this.file);
|
|
36
|
+
}
|
|
37
|
+
finally {
|
|
38
|
+
await rm(temporary, { force: true });
|
|
39
|
+
}
|
|
40
|
+
await this.require(["--user", "daemon-reload"]);
|
|
41
|
+
}
|
|
42
|
+
async unregister() {
|
|
43
|
+
await this.stop();
|
|
44
|
+
await this.run(command, ["--user", "disable", unit]);
|
|
45
|
+
await rm(this.file, { force: true });
|
|
46
|
+
await this.require(["--user", "daemon-reload"]);
|
|
47
|
+
await this.run(command, ["--user", "reset-failed", unit]);
|
|
48
|
+
}
|
|
49
|
+
async start() {
|
|
50
|
+
if (!existsSync(this.file))
|
|
51
|
+
throw new Error("The PhreshOS System service is not registered");
|
|
52
|
+
await this.require(["--user", "start", unit]);
|
|
53
|
+
}
|
|
54
|
+
async stop() {
|
|
55
|
+
const state = await this.run(command, ["--user", "is-active", "--quiet", unit]);
|
|
56
|
+
if (state.code === 0)
|
|
57
|
+
await this.require(["--user", "stop", unit]);
|
|
58
|
+
}
|
|
59
|
+
async enable() {
|
|
60
|
+
if (!existsSync(this.file))
|
|
61
|
+
throw new Error("The PhreshOS System service is not registered");
|
|
62
|
+
await this.require(["--user", "enable", unit]);
|
|
63
|
+
}
|
|
64
|
+
async disable() {
|
|
65
|
+
if (!existsSync(this.file))
|
|
66
|
+
throw new Error("The PhreshOS System service is not registered");
|
|
67
|
+
await this.require(["--user", "disable", unit]);
|
|
68
|
+
}
|
|
69
|
+
async require(args) {
|
|
70
|
+
const result = await this.run(command, args);
|
|
71
|
+
if (result.code !== 0)
|
|
72
|
+
throw new Error(result.stderr.trim() || result.stdout.trim() || `${command} exited with code ${result.code}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function service(definition) {
|
|
76
|
+
return `[Unit]
|
|
77
|
+
Description=PhreshOS System
|
|
78
|
+
|
|
79
|
+
[Service]
|
|
80
|
+
Type=simple
|
|
81
|
+
ExecStart=${quote(definition.executable)} ${quote(definition.entry)}
|
|
82
|
+
WorkingDirectory=${setting(definition.directory)}
|
|
83
|
+
Restart=on-failure
|
|
84
|
+
RestartSec=2
|
|
85
|
+
StandardOutput=append:${setting(definition.output)}
|
|
86
|
+
StandardError=append:${setting(definition.output)}
|
|
87
|
+
|
|
88
|
+
[Install]
|
|
89
|
+
WantedBy=default.target
|
|
90
|
+
`;
|
|
91
|
+
}
|
|
92
|
+
function quote(value) {
|
|
93
|
+
return JSON.stringify(value);
|
|
94
|
+
}
|
|
95
|
+
function setting(value) {
|
|
96
|
+
if (value.includes("\n") || value.includes("\r"))
|
|
97
|
+
throw new Error("A systemd service path cannot contain a line break");
|
|
98
|
+
return value.replaceAll("%", "%%");
|
|
99
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { execute } from "../process.js";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { mkdir, rename, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { randomUUID } from "node:crypto";
|
|
6
|
+
const command = "/bin/launchctl";
|
|
7
|
+
const defaultLabel = "com.phreshos.system";
|
|
8
|
+
export default class MacOSSystemService {
|
|
9
|
+
run;
|
|
10
|
+
label;
|
|
11
|
+
plist;
|
|
12
|
+
domain;
|
|
13
|
+
target;
|
|
14
|
+
constructor(userHome, run = execute, label = defaultLabel) {
|
|
15
|
+
this.run = run;
|
|
16
|
+
this.label = label;
|
|
17
|
+
const uid = process.getuid?.();
|
|
18
|
+
if (uid === undefined)
|
|
19
|
+
throw new Error("The current macOS user could not be identified");
|
|
20
|
+
this.plist = join(userHome, "Library", "LaunchAgents", `${this.label}.plist`);
|
|
21
|
+
this.domain = `gui/${uid}`;
|
|
22
|
+
this.target = `${this.domain}/${this.label}`;
|
|
23
|
+
}
|
|
24
|
+
async inspect() {
|
|
25
|
+
const registered = existsSync(this.plist);
|
|
26
|
+
const service = await this.run(command, ["print", this.target]);
|
|
27
|
+
const disabled = await this.run(command, ["print-disabled", this.domain]);
|
|
28
|
+
const explicitlyDisabled = new RegExp(`"${escapePattern(this.label)}"\\s*=>\\s*(?:true|disabled)`).test(disabled.stdout);
|
|
29
|
+
const pid = /\bpid\s*=\s*(\d+)/.exec(service.stdout)?.[1];
|
|
30
|
+
return {
|
|
31
|
+
registered,
|
|
32
|
+
enabled: registered && !explicitlyDisabled,
|
|
33
|
+
running: service.code === 0 && /\bstate\s*=\s*running\b/.test(service.stdout),
|
|
34
|
+
...(pid ? { pid: Number(pid) } : {})
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
async register(definition) {
|
|
38
|
+
await this.stop();
|
|
39
|
+
await mkdir(dirname(this.plist), { recursive: true });
|
|
40
|
+
await mkdir(dirname(definition.output), { recursive: true });
|
|
41
|
+
const temporary = `${this.plist}.${randomUUID()}.tmp`;
|
|
42
|
+
try {
|
|
43
|
+
await writeFile(temporary, plist(this.label, definition), { mode: 0o600 });
|
|
44
|
+
await rename(temporary, this.plist);
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
await rm(temporary, { force: true });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
async unregister() {
|
|
51
|
+
await this.stop();
|
|
52
|
+
await rm(this.plist, { force: true });
|
|
53
|
+
}
|
|
54
|
+
async start() {
|
|
55
|
+
if (!existsSync(this.plist))
|
|
56
|
+
throw new Error("The PhreshOS System service is not registered");
|
|
57
|
+
const state = await this.inspect();
|
|
58
|
+
if (state.running)
|
|
59
|
+
return;
|
|
60
|
+
const loaded = await this.run(command, ["print", this.target]);
|
|
61
|
+
if (loaded.code === 0)
|
|
62
|
+
await this.require(["kickstart", "-k", this.target]);
|
|
63
|
+
else
|
|
64
|
+
await this.require(["bootstrap", this.domain, this.plist]);
|
|
65
|
+
}
|
|
66
|
+
async stop() {
|
|
67
|
+
const loaded = await this.run(command, ["print", this.target]);
|
|
68
|
+
if (loaded.code === 0)
|
|
69
|
+
await this.require(["bootout", this.target]);
|
|
70
|
+
}
|
|
71
|
+
async enable() {
|
|
72
|
+
if (!existsSync(this.plist))
|
|
73
|
+
throw new Error("The PhreshOS System service is not registered");
|
|
74
|
+
await this.require(["enable", this.target]);
|
|
75
|
+
}
|
|
76
|
+
async disable() {
|
|
77
|
+
if (!existsSync(this.plist))
|
|
78
|
+
throw new Error("The PhreshOS System service is not registered");
|
|
79
|
+
await this.require(["disable", this.target]);
|
|
80
|
+
}
|
|
81
|
+
async require(args) {
|
|
82
|
+
const result = await this.run(command, args);
|
|
83
|
+
if (result.code !== 0)
|
|
84
|
+
throw new Error(result.stderr.trim() || result.stdout.trim() || `${command} exited with code ${result.code}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function plist(label, definition) {
|
|
88
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
89
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
90
|
+
<plist version="1.0">
|
|
91
|
+
<dict>
|
|
92
|
+
<key>Label</key>
|
|
93
|
+
<string>${xml(label)}</string>
|
|
94
|
+
<key>ProgramArguments</key>
|
|
95
|
+
<array>
|
|
96
|
+
<string>${xml(definition.executable)}</string>
|
|
97
|
+
<string>${xml(definition.entry)}</string>
|
|
98
|
+
</array>
|
|
99
|
+
<key>WorkingDirectory</key>
|
|
100
|
+
<string>${xml(definition.directory)}</string>
|
|
101
|
+
<key>RunAtLoad</key>
|
|
102
|
+
<true/>
|
|
103
|
+
<key>KeepAlive</key>
|
|
104
|
+
<dict>
|
|
105
|
+
<key>SuccessfulExit</key>
|
|
106
|
+
<false/>
|
|
107
|
+
</dict>
|
|
108
|
+
<key>ThrottleInterval</key>
|
|
109
|
+
<integer>2</integer>
|
|
110
|
+
<key>StandardOutPath</key>
|
|
111
|
+
<string>${xml(definition.output)}</string>
|
|
112
|
+
<key>StandardErrorPath</key>
|
|
113
|
+
<string>${xml(definition.output)}</string>
|
|
114
|
+
</dict>
|
|
115
|
+
</plist>
|
|
116
|
+
`;
|
|
117
|
+
}
|
|
118
|
+
function xml(value) {
|
|
119
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
|
|
120
|
+
}
|
|
121
|
+
function escapePattern(value) {
|
|
122
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
123
|
+
}
|
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phreshos/cli",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.7",
|
|
5
5
|
"description": "The Phresh command-line interface for Program projects and system management.",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=20.10"
|
|
8
|
+
},
|
|
6
9
|
"scripts": {
|
|
7
10
|
"clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
|
|
8
11
|
"compile": "tsc --noEmit false --outDir dist --rootDir source --rewriteRelativeImportExtensions true --allowImportingTsExtensions true",
|
|
9
12
|
"build": "node --run clean && node --run compile && node dist/build-template.js",
|
|
13
|
+
"test": "node --run compile && node --test tests/*.test.mjs",
|
|
14
|
+
"verify": "node --run build && node --run test && node scripts/verify-package.mjs",
|
|
15
|
+
"verify:system-release": "node --run compile && node scripts/verify-system-release.mjs",
|
|
16
|
+
"verify:linux-service": "node --run compile && node scripts/verify-linux-service.mjs",
|
|
17
|
+
"verify:macos-service": "node --run compile && node scripts/verify-macos-service.mjs",
|
|
10
18
|
"prepack": "node --run build"
|
|
11
19
|
},
|
|
12
20
|
"bin": {
|
|
@@ -14,8 +22,29 @@
|
|
|
14
22
|
},
|
|
15
23
|
"files": [
|
|
16
24
|
"dist",
|
|
25
|
+
"LICENSE",
|
|
17
26
|
"README.md"
|
|
18
27
|
],
|
|
28
|
+
"author": "Zohayr SLILEH",
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "git+https://github.com/PhreshOS/cli.git"
|
|
33
|
+
},
|
|
34
|
+
"bugs": {
|
|
35
|
+
"url": "https://github.com/PhreshOS/cli/issues"
|
|
36
|
+
},
|
|
37
|
+
"homepage": "https://github.com/PhreshOS/cli#readme",
|
|
38
|
+
"keywords": [
|
|
39
|
+
"phreshos",
|
|
40
|
+
"cli",
|
|
41
|
+
"program"
|
|
42
|
+
],
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"access": "public",
|
|
45
|
+
"provenance": true
|
|
46
|
+
},
|
|
47
|
+
"packageManager": "bun@1.3.14",
|
|
19
48
|
"dependencies": {
|
|
20
49
|
"@clack/prompts": "^1.7.0",
|
|
21
50
|
"@phreshos/core": "^0.1.1",
|