@phreshos/cli 0.1.12 → 0.1.14

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 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 and a real `systemd --user` manager on
61
- Linux. In Linux containers with no init manager, it runs as a detached
62
- user-owned background process that survives the terminal but ends with the
63
- container. Automatic startup is unavailable there rather than being reported
64
- as enabled. `start` and `stop` change current execution only; where a native
65
- manager exists, `enable` and `disable` change automatic startup only. Successful
66
- installation and startup show the desktop address. `status` reports that same
67
- address with the installed version, service readiness, and automatic startup
68
- without changing them; `version` reports only the installed System release.
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. Windows remains unsupported
73
- until the System has an equally strong local-intake authorization model there.
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
+ }
@@ -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
- * A socket file rather than a port, because the file's permissions are
8
- * the authorization: only the account that owns this machine can open
9
- * it, and that account is exactly who may run and install programs on
10
- * it. Nothing is sent to prove anything, because being able to connect
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", "intake.sock");
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 join(instanceHome, "intake.sock");
30
+ return intakeAddress(instanceHome);
30
31
  }
31
32
  export const socketPath = programIntakePath();
32
33
  export default function speak(question, heard, path = socketPath, signal) {
@@ -4,16 +4,6 @@ import { mkdtemp, readFile, rm } from "node:fs/promises";
4
4
  import { tmpdir } from "node:os";
5
5
  import { dirname, isAbsolute, join, relative, resolve } from "node:path";
6
6
  import AdmZip from "adm-zip";
7
- const officialPrograms = {
8
- phresh: {
9
- identity: "phresh-program",
10
- repository: "PhreshOS/phresh-program"
11
- },
12
- setup: {
13
- identity: "setup",
14
- repository: "PhreshOS/setup-program"
15
- }
16
- };
17
7
  /** Resolve, verify, and unpack one official production Program release. */
18
8
  export async function prepareOfficialProgram(name, fetcher = fetch) {
19
9
  const release = await resolveOfficialProgramRelease(name, fetcher);
@@ -34,10 +24,8 @@ export async function prepareOfficialProgram(name, fetcher = fetch) {
34
24
  }
35
25
  }
36
26
  export async function resolveOfficialProgramRelease(name, fetcher = fetch) {
37
- const official = officialPrograms[name];
38
- if (!official)
39
- throw new Error(`No official Program is named "${name}"`);
40
- const response = await fetcher(`https://api.github.com/repos/${official.repository}/releases?per_page=100`, {
27
+ const requested = programName(name);
28
+ const response = await fetcher(`https://api.github.com/repos/PhreshOS/${requested}-program/releases?per_page=100`, {
41
29
  headers: {
42
30
  Accept: "application/vnd.github+json",
43
31
  "User-Agent": "@phreshos/cli"
@@ -46,26 +34,24 @@ export async function resolveOfficialProgramRelease(name, fetcher = fetch) {
46
34
  });
47
35
  if (!response.ok)
48
36
  throw new Error(`The ${name} release list could not be read (${response.status} ${response.statusText})`);
49
- return selectProgramRelease(official.identity, await response.json());
37
+ return selectProgramRelease(await response.json());
50
38
  }
51
- export function selectProgramRelease(identity, value) {
39
+ export function selectProgramRelease(value) {
52
40
  if (!Array.isArray(value))
53
- throw new Error(`The ${identity} release list is invalid`);
41
+ throw new Error("The Program release list is invalid");
54
42
  const releases = value.flatMap(function (item) {
55
43
  if (!record(item) || item.draft === true || item.prerelease === true || typeof item.tag_name !== "string" || !Array.isArray(item.assets))
56
44
  return [];
57
45
  const version = parseVersion(item.tag_name);
58
46
  if (!version)
59
47
  return [];
60
- const archiveName = `${identity}@${version}.zip`;
61
- const archive = asset(item.assets, archiveName);
62
- const checksum = asset(item.assets, `${archiveName}.sha256`);
63
- return archive && checksum ? [{ identity, version, archive, checksum }] : [];
48
+ const files = programAssets(item.assets, version);
49
+ return files ? [{ version, ...files }] : [];
64
50
  });
65
51
  releases.sort((left, right) => compare(right.version, left.version));
66
52
  const selected = releases[0];
67
53
  if (!selected)
68
- throw new Error(`No stable ${identity} Program release is available`);
54
+ throw new Error("No stable Program release is available");
69
55
  return selected;
70
56
  }
71
57
  export async function downloadProgramRelease(release, fetcher = fetch) {
@@ -150,6 +136,27 @@ function asset(assets, name) {
150
136
  const found = assets.find(item => record(item) && item.name === name && typeof item.browser_download_url === "string");
151
137
  return record(found) && typeof found.browser_download_url === "string" ? found.browser_download_url : undefined;
152
138
  }
139
+ function programAssets(assets, version) {
140
+ const suffix = `@${version}.zip`;
141
+ const archives = assets.flatMap(function (item) {
142
+ if (!record(item) || typeof item.name !== "string" || typeof item.browser_download_url !== "string" || !item.name.endsWith(suffix))
143
+ return [];
144
+ const identity = item.name.slice(0, -suffix.length);
145
+ if (!validProgramName(identity))
146
+ return [];
147
+ const checksum = asset(assets, `${item.name}.sha256`);
148
+ return checksum ? [{ identity, archive: item.browser_download_url, checksum }] : [];
149
+ });
150
+ return archives.length === 1 ? archives[0] : undefined;
151
+ }
152
+ function programName(name) {
153
+ if (!validProgramName(name))
154
+ throw new Error(`The official Program name "${name}" is invalid`);
155
+ return name;
156
+ }
157
+ function validProgramName(name) {
158
+ return name.length <= 64 && /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name);
159
+ }
153
160
  function parseVersion(tag) {
154
161
  const match = /^v(\d+)\.(\d+)\.(\d+)$/.exec(tag);
155
162
  return match ? tag.slice(1) : undefined;
package/dist/style.js CHANGED
@@ -19,7 +19,6 @@ export function column(label) {
19
19
  return label.length < 12 ? label.padEnd(12) : `${label} `;
20
20
  }
21
21
  export function heading(title, note) {
22
- console.log("");
23
22
  section(title, note);
24
23
  console.log("");
25
24
  }
@@ -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 = process.platform === "win32" ? "npm.cmd" : "npm";
211
- await requireSuccess(npm, ["install", "--omit=dev", "--no-audit", "--no-fund", "--package-lock=false"], { cwd: directory });
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
+ }
@@ -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: join(storage, "intake.sock"),
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("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;");
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
+ }
@@ -16,7 +16,7 @@
16
16
  "react-dom": "^19.2.8"
17
17
  },
18
18
  "devDependencies": {
19
- "@phreshos/cli": "^0.1.12",
19
+ "@phreshos/cli": "^0.1.14",
20
20
  "@types/node": "^26.2.0",
21
21
  "@types/react": "^19.2.18",
22
22
  "@types/react-dom": "^19.2.4",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@phreshos/cli",
3
3
  "type": "module",
4
- "version": "0.1.12",
4
+ "version": "0.1.14",
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": {