@phreshos/cli 0.1.12 → 0.1.13
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/README.md +13 -11
- package/dist/intake-address.js +14 -0
- package/dist/program-intake.js +8 -7
- package/dist/system/installation.js +3 -2
- package/dist/system/npm.js +8 -0
- package/dist/system/paths.js +2 -1
- package/dist/system/service/index.js +3 -0
- package/dist/system/service/macos.js +1 -2
- package/dist/system/service/windows-runner.js +45 -0
- package/dist/system/service/windows.js +238 -0
- package/dist/template/package.json +1 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -57,20 +57,22 @@ native per-user service. The selected release and the service entry therefore
|
|
|
57
57
|
cannot become two competing sources of truth if installation is interrupted.
|
|
58
58
|
It never reads a source checkout and never requires Bun or TypeScript.
|
|
59
59
|
|
|
60
|
-
The System runs under `launchd` on macOS
|
|
61
|
-
Linux
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
60
|
+
The System runs under `launchd` on macOS, a real `systemd --user` manager on
|
|
61
|
+
Linux, and a least-privilege per-user scheduled task on Windows. In Linux
|
|
62
|
+
containers with no init manager, it runs as a detached user-owned background
|
|
63
|
+
process that survives the terminal but ends with the container. Automatic
|
|
64
|
+
startup is unavailable there rather than being reported as enabled. `start`
|
|
65
|
+
and `stop` change current execution only; where a native manager exists,
|
|
66
|
+
`enable` and `disable` change automatic startup only. Successful installation
|
|
67
|
+
and startup show the desktop address. `status` reports that same address with
|
|
68
|
+
the installed version, service readiness, and automatic startup without
|
|
69
|
+
changing them; `version` reports only the installed System release.
|
|
69
70
|
|
|
70
71
|
Installation files and persistent System state have separate homes. Removing
|
|
71
72
|
the System unregisters its service and removes its release files while keeping
|
|
72
|
-
`~/.phreshos`, including Programs and owner data.
|
|
73
|
-
|
|
73
|
+
`~/.phreshos`, including Programs and owner data. Local Program intake uses an
|
|
74
|
+
owner-only socket file on POSIX and an owner-created duplex named pipe on
|
|
75
|
+
Windows; neither becomes a network endpoint or introduces a bearer secret.
|
|
74
76
|
|
|
75
77
|
## create
|
|
76
78
|
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
/** Address the same owner-local intake without turning it into a network port. */
|
|
4
|
+
export default function intakeAddress(storage, platform = process.platform) {
|
|
5
|
+
if (platform !== "win32")
|
|
6
|
+
return join(storage, "intake.sock");
|
|
7
|
+
// Windows pipe names share one machine-wide namespace. The storage root
|
|
8
|
+
// separates users and isolated instances while case folding follows the
|
|
9
|
+
// filesystem they came from. Access remains the pipe creator's default
|
|
10
|
+
// duplex ACL; the name is identity, not a secret or an authorization.
|
|
11
|
+
const owner = storage.replaceAll("\\", "/").replace(/\/+$/, "").toLowerCase();
|
|
12
|
+
const identity = createHash("sha256").update(owner).digest("hex").slice(0, 32);
|
|
13
|
+
return `\\\\.\\pipe\\phreshos-${identity}-intake`;
|
|
14
|
+
}
|
package/dist/program-intake.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { connect } from "node:net";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { isAbsolute, join } from "node:path";
|
|
4
|
+
import intakeAddress from "./intake-address.js";
|
|
4
5
|
/**
|
|
5
6
|
* The local Program intake, from the CLI's side.
|
|
6
7
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* is the proof.
|
|
8
|
+
* An owner-local IPC address rather than a port: a mode-0600 socket file
|
|
9
|
+
* on POSIX and an owner-created duplex named pipe on Windows. Only the
|
|
10
|
+
* account that owns this machine can complete the channel, and that account
|
|
11
|
+
* is exactly who may run and install programs on it. Nothing is sent to
|
|
12
|
+
* prove anything, because being able to connect is the proof.
|
|
12
13
|
*
|
|
13
14
|
* A message is a line. Closing our own side to mark the end of a
|
|
14
15
|
* question would make lifetime ambiguous. A line delimiter keeps the
|
|
@@ -23,10 +24,10 @@ import { isAbsolute, join } from "node:path";
|
|
|
23
24
|
export function programIntakePath(environment = process.env, userHome = homedir()) {
|
|
24
25
|
const instanceHome = environment.PHRESHOS_HOME;
|
|
25
26
|
if (instanceHome === undefined)
|
|
26
|
-
return join(userHome, ".phreshos"
|
|
27
|
+
return intakeAddress(join(userHome, ".phreshos"));
|
|
27
28
|
if (!isAbsolute(instanceHome))
|
|
28
29
|
throw new Error("PHRESHOS_HOME must be an absolute filesystem path");
|
|
29
|
-
return
|
|
30
|
+
return intakeAddress(instanceHome);
|
|
30
31
|
}
|
|
31
32
|
export const socketPath = programIntakePath();
|
|
32
33
|
export default function speak(question, heard, path = socketPath, signal) {
|
|
@@ -3,6 +3,7 @@ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
|
3
3
|
import { mkdir, mkdtemp, open, readFile, readdir, readlink, rename, rm, symlink, writeFile } from "node:fs/promises";
|
|
4
4
|
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
5
5
|
import { requireSuccess } from "./process.js";
|
|
6
|
+
import npmInvocation from "./npm.js";
|
|
6
7
|
import { minimumSystemNodeVersion } from "./node.js";
|
|
7
8
|
import AdmZip from "adm-zip";
|
|
8
9
|
/** Owns immutable release directories and the one atomic active record. */
|
|
@@ -207,8 +208,8 @@ function requireDirectory(path) {
|
|
|
207
208
|
mkdirSync(path, { recursive: true, mode: 0o700 });
|
|
208
209
|
}
|
|
209
210
|
async function installProductionDependencies(directory) {
|
|
210
|
-
const npm =
|
|
211
|
-
await requireSuccess(npm,
|
|
211
|
+
const npm = npmInvocation(["install", "--omit=dev", "--no-audit", "--no-fund", "--package-lock=false"]);
|
|
212
|
+
await requireSuccess(npm.command, npm.args, { cwd: directory });
|
|
212
213
|
}
|
|
213
214
|
function releaseRecord(value) {
|
|
214
215
|
return record(value)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { win32 } from "node:path";
|
|
2
|
+
/** Invoke npm without asking a platform command shell to interpret its shim. */
|
|
3
|
+
export default function npmInvocation(args, platform = process.platform, executable = process.execPath) {
|
|
4
|
+
if (platform !== "win32")
|
|
5
|
+
return { command: "npm", args };
|
|
6
|
+
const npm = win32.join(win32.dirname(executable), "node_modules", "npm", "bin", "npm-cli.js");
|
|
7
|
+
return { command: executable, args: [npm, ...args] };
|
|
8
|
+
}
|
package/dist/system/paths.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import intakeAddress from "../intake-address.js";
|
|
1
2
|
import { homedir } from "node:os";
|
|
2
3
|
import { isAbsolute, join } from "node:path";
|
|
3
4
|
/** The installation is separate from the persistent state it operates on. */
|
|
@@ -15,7 +16,7 @@ export default function systemPaths(platform = process.platform, userHome = home
|
|
|
15
16
|
releases: join(root, "releases"),
|
|
16
17
|
current: join(root, "current"),
|
|
17
18
|
storage,
|
|
18
|
-
intake:
|
|
19
|
+
intake: intakeAddress(storage, platform),
|
|
19
20
|
log: join(storage, "service.log")
|
|
20
21
|
};
|
|
21
22
|
}
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { homedir } from "node:os";
|
|
2
2
|
import LinuxSystemService from "./linux.js";
|
|
3
3
|
import MacOSSystemService from "./macos.js";
|
|
4
|
+
import WindowsSystemService from "./windows.js";
|
|
4
5
|
/** Select the per-user service implementation available in this environment. */
|
|
5
6
|
export default function systemService(platform = process.platform, userHome = homedir()) {
|
|
6
7
|
if (platform === "darwin")
|
|
7
8
|
return new MacOSSystemService(userHome);
|
|
8
9
|
if (platform === "linux")
|
|
9
10
|
return new LinuxSystemService(userHome);
|
|
11
|
+
if (platform === "win32")
|
|
12
|
+
return new WindowsSystemService(userHome);
|
|
10
13
|
throw new Error(`PhreshOS System services are not supported on ${platform}`);
|
|
11
14
|
}
|
|
@@ -11,10 +11,9 @@ export default class MacOSSystemService {
|
|
|
11
11
|
plist;
|
|
12
12
|
domain;
|
|
13
13
|
target;
|
|
14
|
-
constructor(userHome, run = execute, label = defaultLabel) {
|
|
14
|
+
constructor(userHome, run = execute, label = defaultLabel, uid = process.getuid?.()) {
|
|
15
15
|
this.run = run;
|
|
16
16
|
this.label = label;
|
|
17
|
-
const uid = process.getuid?.();
|
|
18
17
|
if (uid === undefined)
|
|
19
18
|
throw new Error("The current macOS user could not be identified");
|
|
20
19
|
this.plist = join(userHome, "Library", "LaunchAgents", `${this.label}.plist`);
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { open, writeFile } from "node:fs/promises";
|
|
3
|
+
const payload = decode(process.argv[2]);
|
|
4
|
+
const output = await open(payload.definition.output, "a", 0o600);
|
|
5
|
+
try {
|
|
6
|
+
const child = spawn(payload.definition.executable, [payload.definition.entry], {
|
|
7
|
+
cwd: payload.definition.directory,
|
|
8
|
+
stdio: ["ignore", output.fd, output.fd]
|
|
9
|
+
});
|
|
10
|
+
await new Promise(function (settle, refuse) {
|
|
11
|
+
child.once("spawn", settle);
|
|
12
|
+
child.once("error", refuse);
|
|
13
|
+
});
|
|
14
|
+
if (child.pid === undefined)
|
|
15
|
+
throw new Error("The PhreshOS System process has no pid");
|
|
16
|
+
await writeFile(payload.state, JSON.stringify({ runner: process.pid, child: child.pid }), { mode: 0o600 });
|
|
17
|
+
const result = await new Promise(settle => {
|
|
18
|
+
child.once("close", (code, signal) => settle({ code, signal }));
|
|
19
|
+
});
|
|
20
|
+
process.exitCode = result.signal ? 1 : result.code ?? 1;
|
|
21
|
+
}
|
|
22
|
+
finally {
|
|
23
|
+
await output.close();
|
|
24
|
+
}
|
|
25
|
+
function decode(encoded) {
|
|
26
|
+
let value;
|
|
27
|
+
try {
|
|
28
|
+
value = JSON.parse(Buffer.from(encoded ?? "", "base64url").toString("utf8"));
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
throw new Error("The PhreshOS System task payload is invalid");
|
|
32
|
+
}
|
|
33
|
+
if (!value || typeof value !== "object")
|
|
34
|
+
throw new Error("The PhreshOS System task payload is invalid");
|
|
35
|
+
const candidate = value;
|
|
36
|
+
if (typeof candidate.state !== "string" || !definition(candidate.definition))
|
|
37
|
+
throw new Error("The PhreshOS System task payload is invalid");
|
|
38
|
+
return { state: candidate.state, definition: candidate.definition };
|
|
39
|
+
}
|
|
40
|
+
function definition(value) {
|
|
41
|
+
if (!value || typeof value !== "object")
|
|
42
|
+
return false;
|
|
43
|
+
const candidate = value;
|
|
44
|
+
return ["executable", "entry", "directory", "output"].every(name => typeof candidate[name] === "string" && candidate[name] !== "");
|
|
45
|
+
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { execute } from "../process.js";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
const scheduler = "schtasks.exe";
|
|
7
|
+
const powershell = "powershell.exe";
|
|
8
|
+
const defaultTask = "PhreshOS System";
|
|
9
|
+
const runner = fileURLToPath(new URL("./windows-runner.js", import.meta.url));
|
|
10
|
+
/** A per-user Windows Task Scheduler service requiring no administrator rights. */
|
|
11
|
+
export default class WindowsSystemService {
|
|
12
|
+
userHome;
|
|
13
|
+
run;
|
|
14
|
+
task;
|
|
15
|
+
constructor(userHome, run = execute, task = defaultTask) {
|
|
16
|
+
this.userHome = userHome;
|
|
17
|
+
this.run = run;
|
|
18
|
+
this.task = task;
|
|
19
|
+
this.state = join(userHome, ".phreshos", "system-service.json");
|
|
20
|
+
}
|
|
21
|
+
state;
|
|
22
|
+
async inspect() {
|
|
23
|
+
const result = await this.run(powershell, powershellArguments(inspectScript(this.task)));
|
|
24
|
+
if (result.code === 3)
|
|
25
|
+
return absent();
|
|
26
|
+
if (result.code !== 0)
|
|
27
|
+
throw failure(powershell, result);
|
|
28
|
+
const match = /^([0-4]),([01])$/.exec(result.stdout.trim());
|
|
29
|
+
if (!match)
|
|
30
|
+
throw new Error("The PhreshOS System scheduled task returned an invalid state");
|
|
31
|
+
const pid = match[1] === "4" ? (await this.readState())?.child : undefined;
|
|
32
|
+
return {
|
|
33
|
+
registered: true,
|
|
34
|
+
automaticStartup: true,
|
|
35
|
+
enabled: match[2] === "1",
|
|
36
|
+
running: match[1] === "4",
|
|
37
|
+
...(pid && alive(pid) ? { pid } : {})
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
async register(definition) {
|
|
41
|
+
await this.stop();
|
|
42
|
+
await mkdir(dirname(definition.output), { recursive: true });
|
|
43
|
+
await mkdir(dirname(this.state), { recursive: true });
|
|
44
|
+
await rm(this.state, { force: true });
|
|
45
|
+
const sid = await this.userSid();
|
|
46
|
+
const temporary = join(this.userHome, `.phreshos-system-${randomUUID()}.xml`);
|
|
47
|
+
try {
|
|
48
|
+
await writeFile(temporary, utf16(task(this.task, sid, definition, this.state)));
|
|
49
|
+
await this.require(scheduler, ["/Create", "/TN", this.task, "/XML", temporary, "/F"]);
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
await rm(temporary, { force: true });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
async unregister() {
|
|
56
|
+
const state = await this.inspect();
|
|
57
|
+
if (!state.registered) {
|
|
58
|
+
await rm(this.state, { force: true });
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (state.running)
|
|
62
|
+
await this.stop();
|
|
63
|
+
await this.require(scheduler, ["/Delete", "/TN", this.task, "/F"]);
|
|
64
|
+
await rm(this.state, { force: true });
|
|
65
|
+
}
|
|
66
|
+
async start() {
|
|
67
|
+
const state = await this.inspect();
|
|
68
|
+
if (!state.registered)
|
|
69
|
+
throw new Error("The PhreshOS System service is not registered");
|
|
70
|
+
if (state.running)
|
|
71
|
+
return;
|
|
72
|
+
// Task Scheduler refuses an explicit run while a task is disabled.
|
|
73
|
+
// Enablement names future logons, not current execution, so borrow it
|
|
74
|
+
// only for the launch and restore the persisted choice immediately.
|
|
75
|
+
if (!state.enabled)
|
|
76
|
+
await this.change("/Enable");
|
|
77
|
+
try {
|
|
78
|
+
await this.require(scheduler, ["/Run", "/TN", this.task]);
|
|
79
|
+
}
|
|
80
|
+
finally {
|
|
81
|
+
if (!state.enabled)
|
|
82
|
+
await this.change("/Disable");
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
async stop() {
|
|
86
|
+
const state = await this.inspect();
|
|
87
|
+
if (!state.registered || !state.running)
|
|
88
|
+
return;
|
|
89
|
+
await this.require(scheduler, ["/End", "/TN", this.task]);
|
|
90
|
+
const until = Date.now() + 5_000;
|
|
91
|
+
while (Date.now() < until) {
|
|
92
|
+
const processes = await this.readState();
|
|
93
|
+
if (!(await this.inspect()).running && !running(processes))
|
|
94
|
+
break;
|
|
95
|
+
await new Promise(settle => setTimeout(settle, 50));
|
|
96
|
+
}
|
|
97
|
+
const processes = await this.readState();
|
|
98
|
+
if (running(processes)) {
|
|
99
|
+
if (processes?.child && alive(processes.child))
|
|
100
|
+
process.kill(processes.child);
|
|
101
|
+
await settle(processes?.child);
|
|
102
|
+
if (processes?.runner && alive(processes.runner))
|
|
103
|
+
process.kill(processes.runner);
|
|
104
|
+
await settle(processes?.runner);
|
|
105
|
+
}
|
|
106
|
+
if ((await this.inspect()).running || running(await this.readState()))
|
|
107
|
+
throw new Error("The PhreshOS System scheduled task did not stop");
|
|
108
|
+
await rm(this.state, { force: true });
|
|
109
|
+
}
|
|
110
|
+
async enable() {
|
|
111
|
+
await this.requireRegistered();
|
|
112
|
+
await this.change("/Enable");
|
|
113
|
+
}
|
|
114
|
+
async disable() {
|
|
115
|
+
await this.requireRegistered();
|
|
116
|
+
await this.change("/Disable");
|
|
117
|
+
}
|
|
118
|
+
async userSid() {
|
|
119
|
+
const result = await this.run(powershell, powershellArguments("[Console]::Out.Write([Security.Principal.WindowsIdentity]::GetCurrent().User.Value)"));
|
|
120
|
+
if (result.code !== 0)
|
|
121
|
+
throw failure(powershell, result);
|
|
122
|
+
const sid = result.stdout.trim();
|
|
123
|
+
if (!/^S-[0-9]+(?:-[0-9]+)+$/.test(sid))
|
|
124
|
+
throw new Error("The current Windows user could not be identified");
|
|
125
|
+
return sid;
|
|
126
|
+
}
|
|
127
|
+
async readState() {
|
|
128
|
+
try {
|
|
129
|
+
const value = JSON.parse(await readFile(this.state, "utf8"));
|
|
130
|
+
if (integer(value.runner) && integer(value.child))
|
|
131
|
+
return { runner: value.runner, child: value.child };
|
|
132
|
+
}
|
|
133
|
+
catch { }
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
async requireRegistered() {
|
|
137
|
+
if (!(await this.inspect()).registered)
|
|
138
|
+
throw new Error("The PhreshOS System service is not registered");
|
|
139
|
+
}
|
|
140
|
+
async change(action) {
|
|
141
|
+
await this.require(scheduler, ["/Change", "/TN", this.task, action]);
|
|
142
|
+
}
|
|
143
|
+
async require(command, args) {
|
|
144
|
+
const result = await this.run(command, args);
|
|
145
|
+
if (result.code !== 0)
|
|
146
|
+
throw failure(command, result);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function absent() {
|
|
150
|
+
return { registered: false, automaticStartup: true, enabled: false, running: false };
|
|
151
|
+
}
|
|
152
|
+
function inspectScript(name) {
|
|
153
|
+
const selected = quotePowerShell(name);
|
|
154
|
+
return `$task = Get-ScheduledTask -TaskName ${selected} -TaskPath '\\' -ErrorAction SilentlyContinue; if ($null -eq $task) { exit 3 }; [Console]::Out.Write(("{0},{1}" -f [int]$task.State, [int]$task.Settings.Enabled))`;
|
|
155
|
+
}
|
|
156
|
+
function task(name, sid, definition, state) {
|
|
157
|
+
const payload = Buffer.from(JSON.stringify({ definition, state })).toString("base64url");
|
|
158
|
+
const argumentsValue = `"${runner}" ${payload}`;
|
|
159
|
+
return `<?xml version="1.0" encoding="UTF-16"?>
|
|
160
|
+
<Task version="1.4" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
|
|
161
|
+
<RegistrationInfo>
|
|
162
|
+
<Description>PhreshOS System</Description>
|
|
163
|
+
<URI>\\${xml(name)}</URI>
|
|
164
|
+
</RegistrationInfo>
|
|
165
|
+
<Triggers>
|
|
166
|
+
<LogonTrigger>
|
|
167
|
+
<Enabled>true</Enabled>
|
|
168
|
+
<UserId>${xml(sid)}</UserId>
|
|
169
|
+
</LogonTrigger>
|
|
170
|
+
</Triggers>
|
|
171
|
+
<Principals>
|
|
172
|
+
<Principal id="User">
|
|
173
|
+
<UserId>${xml(sid)}</UserId>
|
|
174
|
+
<LogonType>InteractiveToken</LogonType>
|
|
175
|
+
<RunLevel>LeastPrivilege</RunLevel>
|
|
176
|
+
</Principal>
|
|
177
|
+
</Principals>
|
|
178
|
+
<Settings>
|
|
179
|
+
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
|
|
180
|
+
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
|
|
181
|
+
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
|
|
182
|
+
<AllowHardTerminate>true</AllowHardTerminate>
|
|
183
|
+
<StartWhenAvailable>true</StartWhenAvailable>
|
|
184
|
+
<AllowStartOnDemand>true</AllowStartOnDemand>
|
|
185
|
+
<Enabled>true</Enabled>
|
|
186
|
+
<ExecutionTimeLimit>PT0S</ExecutionTimeLimit>
|
|
187
|
+
<RestartOnFailure>
|
|
188
|
+
<Interval>PT1M</Interval>
|
|
189
|
+
<Count>999</Count>
|
|
190
|
+
</RestartOnFailure>
|
|
191
|
+
</Settings>
|
|
192
|
+
<Actions Context="User">
|
|
193
|
+
<Exec>
|
|
194
|
+
<Command>${xml(definition.executable)}</Command>
|
|
195
|
+
<Arguments>${xml(argumentsValue)}</Arguments>
|
|
196
|
+
<WorkingDirectory>${xml(definition.directory)}</WorkingDirectory>
|
|
197
|
+
</Exec>
|
|
198
|
+
</Actions>
|
|
199
|
+
</Task>
|
|
200
|
+
`;
|
|
201
|
+
}
|
|
202
|
+
function powershellArguments(script) {
|
|
203
|
+
return ["-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", script];
|
|
204
|
+
}
|
|
205
|
+
function quotePowerShell(value) {
|
|
206
|
+
return `'${value.replaceAll("'", "''")}'`;
|
|
207
|
+
}
|
|
208
|
+
function xml(value) {
|
|
209
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
210
|
+
}
|
|
211
|
+
function utf16(value) {
|
|
212
|
+
return Buffer.concat([Buffer.from([0xff, 0xfe]), Buffer.from(value, "utf16le")]);
|
|
213
|
+
}
|
|
214
|
+
function failure(command, result) {
|
|
215
|
+
return new Error(result.stderr.trim() || result.stdout.trim() || `${command} exited with code ${result.code}`);
|
|
216
|
+
}
|
|
217
|
+
function integer(value) {
|
|
218
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
|
|
219
|
+
}
|
|
220
|
+
function alive(pid) {
|
|
221
|
+
try {
|
|
222
|
+
process.kill(pid, 0);
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
function running(value) {
|
|
230
|
+
return Boolean(value && (alive(value.runner) || alive(value.child)));
|
|
231
|
+
}
|
|
232
|
+
async function settle(pid) {
|
|
233
|
+
if (!pid)
|
|
234
|
+
return;
|
|
235
|
+
const until = Date.now() + 1_000;
|
|
236
|
+
while (Date.now() < until && alive(pid))
|
|
237
|
+
await new Promise(resolve => setTimeout(resolve, 25));
|
|
238
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phreshos/cli",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.13",
|
|
5
5
|
"description": "The Phresh command-line interface for Program projects and system management.",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": ">=20.10"
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"verify:system-release": "node --run compile && node scripts/verify-system-release.mjs",
|
|
16
16
|
"verify:linux-service": "node --run compile && node scripts/verify-linux-service.mjs",
|
|
17
17
|
"verify:macos-service": "node --run compile && node scripts/verify-macos-service.mjs",
|
|
18
|
+
"verify:windows-service": "node --run compile && node scripts/verify-windows-service.mjs",
|
|
18
19
|
"prepack": "node --run build"
|
|
19
20
|
},
|
|
20
21
|
"bin": {
|