@phreshos/cli 0.1.5 → 0.1.6

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Zohayr SLILEH
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # @phreshos/cli
2
2
 
3
- The `phresh` command for creating, running, packaging, installing, and
4
- uninstalling Programs.
3
+ The `phresh` command for creating and operating Programs, and for installing
4
+ and managing the PhreshOS System on the current machine.
5
5
 
6
6
  ## Package status
7
7
 
@@ -21,6 +21,7 @@ phresh install # lay this program out on this machine
21
21
  phresh uninstall # remove its installed form
22
22
  phresh start # run what your build left, and stay with it
23
23
  phresh dev # run from source, and stay with it
24
+ phresh system status # inspect the local System and its background service
24
25
  ```
25
26
 
26
27
  `phresh --help` lists them, `phresh <command> --help` explains one, and
@@ -28,11 +29,44 @@ phresh dev # run from source, and stay with it
28
29
  unknown command, an unknown flag and a malformed option are each refused
29
30
  and named.
30
31
 
31
- **This tool acts on the current project, and the list ends there.** It is
32
- not the machine's control panel it accepts no arbitrary program identity
33
- and has no word for a process, a window, a store or a setting. The system's
34
- local intake accepts exactly the Program this project declares, and so does
35
- this tool.
32
+ Every top-level Program command acts on the current project, and that list ends
33
+ there. It accepts no arbitrary Program identity and has no word for a Process,
34
+ Window, store, or setting. Machine lifecycle is isolated under `phresh system`;
35
+ it manages the System installation and native service, not the state inside the
36
+ System.
37
+
38
+ ## System lifecycle
39
+
40
+ ```bash
41
+ phresh system install
42
+ phresh system uninstall
43
+ phresh system status
44
+ phresh system start
45
+ phresh system stop
46
+ phresh system enable
47
+ phresh system disable
48
+ ```
49
+
50
+ `install` resolves the newest compatible stable release from the official
51
+ [`PhreshOS/system`](https://github.com/PhreshOS/system) GitHub Releases. It
52
+ downloads the production archive and adjacent checksum, verifies every byte,
53
+ installs production dependencies into a staged version directory, atomically
54
+ points the stable `current` path at it, then registers, enables, and starts the
55
+ native per-user service. The selected release and the service entry therefore
56
+ cannot become two competing sources of truth if installation is interrupted.
57
+ It never reads a source checkout and never requires Bun or TypeScript.
58
+
59
+ The System runs under `launchd` on macOS and `systemd --user` on Linux. The
60
+ native manager owns it after the CLI exits and restarts a failed active
61
+ service. `start` and `stop` change current execution only; `enable` and
62
+ `disable` change automatic startup only. `status` reads installation,
63
+ registration, startup, process, and local-intake readiness without changing
64
+ any of them.
65
+
66
+ Installation files and persistent System state have separate homes. Removing
67
+ the System unregisters its service and removes its release files while keeping
68
+ `~/.phreshos`, including Programs and owner data. Windows remains unsupported
69
+ until the System has an equally strong local-intake authorization model there.
36
70
 
37
71
  ## create
38
72
 
@@ -55,11 +89,9 @@ phresh create status-board \
55
89
  ```
56
90
 
57
91
  Dependencies are installed by default. `--no-install` creates the same valid
58
- project and leaves installation as the first reported next step. A project
59
- created inside this repository's workspace uses its sibling dev-kits through
60
- the workspace protocol. A standalone project, including one created by a
61
- distributed CLI, uses the published versions embedded in the template. Both
62
- choices pass through the same dependency resolver used by `init`.
92
+ project and leaves installation as the first reported next step. Generated and
93
+ initialized Programs always use the published package ranges embedded in the
94
+ CLI; repository layout never changes dependency meaning.
63
95
 
64
96
  ## Saying something to a program you start
65
97
 
package/dist/cli.js CHANGED
@@ -8,12 +8,13 @@ import launch from "./launch.js";
8
8
  import init from "./init.js";
9
9
  import pack from "./pack.js";
10
10
  import uninstall from "./uninstall.js";
11
+ import systemCommands from "./system/command.js";
11
12
  const { version } = metadata;
12
13
  const coreRange = metadata.dependencies["@phreshos/core"];
13
14
  const runOptionPrefix = "--run-option-";
14
15
  const program = new Command()
15
16
  .name("phresh")
16
- .description("Create and manage Programs")
17
+ .description("Create Programs and manage PhreshOS")
17
18
  .version(version, "-v, --version")
18
19
  .showHelpAfterError()
19
20
  .showSuggestionAfterError();
@@ -106,6 +107,7 @@ attached("dev", "run the development Program without installing", [
106
107
  "Runs the same attached lifecycle as start using the development",
107
108
  "declarations. A Client URL must respond within 15 seconds before launch."
108
109
  ], "development");
110
+ systemCommands(program);
109
111
  // Every command begins with the same breathing room. Keep this at the entry
110
112
  // point so individual commands never need to manufacture their own opening.
111
113
  console.log("");
package/dist/create.js CHANGED
@@ -1,4 +1,4 @@
1
- import { projectDependency, installProjectDependencies, projectPackageManager, projectScript } from "./project-dependency.js";
1
+ import { installProjectDependencies, projectPackageManager, projectScript } from "./project-dependency.js";
2
2
  import prompts from "./prompts.js";
3
3
  import { accent, bold } from "./style.js";
4
4
  import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
@@ -36,7 +36,7 @@ export default async function create(options = {}, directory = process.cwd()) {
36
36
  try {
37
37
  cpSync(bundled.directory, staging, { recursive: true });
38
38
  renameSync(resolve(staging, "gitignore"), resolve(staging, ".gitignore"));
39
- customize(staging, target, identity, name, manager);
39
+ customize(staging, identity, name, manager);
40
40
  renameSync(staging, target);
41
41
  placed = true;
42
42
  // A project inside this repository becomes a real workspace member
@@ -79,7 +79,7 @@ function template() {
79
79
  }
80
80
  throw new Error("The CLI template has not been built — run its build command and try again");
81
81
  }
82
- function customize(directory, finalDirectory, identity, name, manager) {
82
+ function customize(directory, identity, name, manager) {
83
83
  for (const path of textFiles(directory)) {
84
84
  let content = readFileSync(path, "utf-8");
85
85
  content = content.replaceAll("phresh-program", identity).replaceAll("Phresh Program", name);
@@ -93,15 +93,6 @@ function customize(directory, finalDirectory, identity, name, manager) {
93
93
  const manifestPath = resolve(directory, "package.json");
94
94
  const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
95
95
  manifest.name = identity;
96
- for (const dependencies of [manifest.dependencies, manifest.devDependencies]) {
97
- if (!dependencies)
98
- continue;
99
- for (const [dependency, range] of Object.entries(dependencies)) {
100
- if (!isProjectPackage(dependency))
101
- continue;
102
- dependencies[dependency] = projectDependency(dependency, range, finalDirectory).manifestSpecifier;
103
- }
104
- }
105
96
  writeFileSync(manifestPath, JSON.stringify(manifest, null, 4) + "\n");
106
97
  }
107
98
  function textFiles(directory) {
@@ -120,13 +111,6 @@ function packageManager(value) {
120
111
  return value;
121
112
  throw new Error(`The package manager must be bun, npm, pnpm, or yarn; received "${value}"`);
122
113
  }
123
- function isProjectPackage(value) {
124
- return value === "@phreshos/core"
125
- || value === "@phreshos/client"
126
- || value === "@phreshos/server"
127
- || value === "@phreshos/react"
128
- || value === "@phreshos/cli";
129
- }
130
114
  function title(identity) {
131
115
  return identity.split("-").map(word => word[0].toUpperCase() + word.slice(1)).join(" ");
132
116
  }
@@ -1,37 +1,11 @@
1
1
  import { line } from "./style.js";
2
2
  import { spawn } from "node:child_process";
3
3
  import { existsSync, readFileSync } from "node:fs";
4
- import { isAbsolute, relative, resolve } from "node:path";
4
+ import { resolve } from "node:path";
5
5
  /** Returns the package-manager command that runs one project script. */
6
6
  export function projectScript(directory, declared, script) {
7
7
  return `${projectPackageManager(directory, declared).name} run ${script}`;
8
8
  }
9
- /**
10
- * Resolves an SDK through the one source policy shared by `init` and `create`.
11
- *
12
- * A Program inside this repository's workspace uses its sibling dev-kit. A
13
- * standalone project or distributed CLI keeps the published range embedded
14
- * in the generated template. Local packages therefore never leak their own
15
- * workspace-only development graph into an unrelated project.
16
- */
17
- export function projectDependency(name, range, directory) {
18
- const workspaceDirectory = resolve(import.meta.dirname, "..", "..", "..");
19
- const workspace = manifestAt(workspaceDirectory);
20
- const sourceDirectory = resolve(import.meta.dirname, "..", "..", localDirectories[name]);
21
- const manifest = manifestAt(sourceDirectory);
22
- if (workspace?.name === "@phreshos/workspace" && workspaceIncludes(workspaceDirectory, directory, workspace.workspaces) && manifest?.name === name && manifest.version && accepts(range, manifest.version)) {
23
- return {
24
- installSpecifier: `${name}@workspace:*`,
25
- manifestSpecifier: "workspace:*",
26
- local: true
27
- };
28
- }
29
- return {
30
- installSpecifier: `${name}@${range}`,
31
- manifestSpecifier: range,
32
- local: false
33
- };
34
- }
35
9
  /** Detects the package manager declared by, present in, or invoking a project. */
36
10
  export function projectPackageManager(directory, declared, preferred) {
37
11
  const named = preferred ?? packageManagerName(declared) ?? packageManagerFromLocks(directory) ?? packageManagerFromInvocation();
@@ -57,9 +31,8 @@ export default async function ensureProjectDependency(name, range, directory = p
57
31
  if (manifest.dependencies?.[name] || manifest.devDependencies?.[name])
58
32
  return;
59
33
  const manager = projectPackageManager(directory, manifest.packageManager);
60
- const source = projectDependency(name, range, directory);
61
- line("dependency", name, `${manager.name}, ${source.local ? "local dev-kit" : range}`);
62
- await run(manager.name, [...manager.addArgs(section), source.installSpecifier], directory);
34
+ line("dependency", name, `${manager.name}, ${range}`);
35
+ await run(manager.name, [...manager.addArgs(section), `${name}@${range}`], directory);
63
36
  console.log("");
64
37
  }
65
38
  function run(command, args, directory, output = "inherit") {
@@ -85,21 +58,6 @@ function run(command, args, directory, output = "inherit") {
85
58
  });
86
59
  });
87
60
  }
88
- function accepts(range, version) {
89
- return range === "workspace:*" || range === version || range === `^${version}` || range === `~${version}`;
90
- }
91
- function workspaceIncludes(root, directory, patterns) {
92
- const local = relative(root, resolve(directory)).replaceAll("\\", "/");
93
- if (!local || local === ".." || local.startsWith("../") || isAbsolute(local))
94
- return false;
95
- return patterns?.some(function (pattern) {
96
- if (!pattern.endsWith("/*"))
97
- return local === pattern;
98
- const prefix = pattern.slice(0, -1);
99
- const child = local.startsWith(prefix) ? local.slice(prefix.length) : "";
100
- return Boolean(child) && !child.includes("/");
101
- }) ?? false;
102
- }
103
61
  function manifestAt(directory) {
104
62
  const path = resolve(directory, "package.json");
105
63
  if (!existsSync(path))
@@ -132,13 +90,6 @@ function packageManagerFromInvocation() {
132
90
  function isPackageManager(value) {
133
91
  return value === "bun" || value === "npm" || value === "pnpm" || value === "yarn";
134
92
  }
135
- const localDirectories = {
136
- "@phreshos/core": "core-sdk",
137
- "@phreshos/client": "client-sdk",
138
- "@phreshos/server": "server-sdk",
139
- "@phreshos/react": "react-sdk",
140
- "@phreshos/cli": "cli"
141
- };
142
93
  const managers = {
143
94
  bun: {
144
95
  name: "bun",
@@ -0,0 +1,70 @@
1
+ import SystemLifecycle from "./lifecycle.js";
2
+ import prompts from "../prompts.js";
3
+ /** Attach the System lifecycle without mixing it with Program commands. */
4
+ export default function systemCommands(program, provided) {
5
+ let lifecycle = provided;
6
+ const current = () => lifecycle ?? (lifecycle = new SystemLifecycle());
7
+ const system = program.command("system").description("install and manage the PhreshOS System");
8
+ system.command("install")
9
+ .description("install or update the System and start its service")
10
+ .action(async function () {
11
+ const interaction = prompts();
12
+ interaction.begin("Install System", "official stable release");
13
+ const status = await interaction.progress("Installing PhreshOS", "PhreshOS installed", () => current().install());
14
+ report(interaction, status);
15
+ interaction.finish("System installed");
16
+ });
17
+ system.command("uninstall")
18
+ .description("remove the System installation and service")
19
+ .action(async function () {
20
+ const interaction = prompts();
21
+ interaction.begin("Uninstall System");
22
+ await interaction.progress("Removing PhreshOS", "PhreshOS removed", () => current().uninstall());
23
+ interaction.message("Persistent System data was kept.");
24
+ interaction.finish("System uninstalled");
25
+ });
26
+ system.command("status")
27
+ .description("show installation, service, and readiness state")
28
+ .action(async function () {
29
+ const interaction = prompts();
30
+ interaction.begin("System Status");
31
+ report(interaction, await current().status());
32
+ });
33
+ action(system, "start", "start the background service", () => current().start());
34
+ action(system, "stop", "stop the background service", () => current().stop());
35
+ action(system, "enable", "enable automatic startup", () => current().enable());
36
+ action(system, "disable", "disable automatic startup", () => current().disable());
37
+ return system;
38
+ }
39
+ function action(system, name, description, work) {
40
+ system.command(name)
41
+ .description(description)
42
+ .action(async function () {
43
+ const interaction = prompts();
44
+ interaction.begin(`System ${title(name)}`);
45
+ const status = await interaction.progress(`${title(name)}ing PhreshOS`, `PhreshOS ${past(name)}`, work);
46
+ report(interaction, status);
47
+ interaction.finish(`System ${past(name)}`);
48
+ });
49
+ }
50
+ function report(interaction, status) {
51
+ interaction.detail("installation", status.installed ? `installed · ${status.installed.version}` : "not installed");
52
+ interaction.detail("service", status.registered ? "registered" : "not registered");
53
+ interaction.detail("startup", status.enabled ? "enabled" : "disabled");
54
+ interaction.detail("process", status.running ? `running${status.pid ? ` · ${status.pid}` : ""}` : "stopped");
55
+ interaction.detail("intake", status.ready ? `ready · ${status.intake}` : "not ready");
56
+ interaction.detail("files", status.root);
57
+ interaction.detail("log", status.log);
58
+ }
59
+ function title(value) {
60
+ return `${value[0]?.toUpperCase()}${value.slice(1)}`;
61
+ }
62
+ function past(value) {
63
+ if (value === "stop")
64
+ return "stopped";
65
+ if (value === "enable")
66
+ return "enabled";
67
+ if (value === "disable")
68
+ return "disabled";
69
+ return "started";
70
+ }
@@ -0,0 +1,241 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
3
+ import { mkdir, mkdtemp, open, readFile, readdir, readlink, rename, rm, symlink, writeFile } from "node:fs/promises";
4
+ import { dirname, isAbsolute, join, relative, resolve } from "node:path";
5
+ import { requireSuccess } from "./process.js";
6
+ import AdmZip from "adm-zip";
7
+ /** Owns immutable release directories and the one atomic active record. */
8
+ export default class SystemInstallation {
9
+ paths;
10
+ dependencies;
11
+ constructor(paths, dependencies = installProductionDependencies) {
12
+ this.paths = paths;
13
+ this.dependencies = dependencies;
14
+ }
15
+ async current() {
16
+ let directory;
17
+ let value;
18
+ try {
19
+ directory = resolve(dirname(this.paths.current), await readlink(this.paths.current));
20
+ value = JSON.parse(await readFile(join(directory, ".release.json"), "utf8"));
21
+ }
22
+ catch (error) {
23
+ if (record(error) && error.code === "ENOENT")
24
+ return undefined;
25
+ throw new Error(`The System current release is invalid: ${this.paths.current}`);
26
+ }
27
+ const within = relative(this.paths.releases, directory);
28
+ if (!releaseRecord(value) || isAbsolute(within) || within.startsWith("..") || within === "") {
29
+ throw new Error(`The System current release is invalid: ${this.paths.current}`);
30
+ }
31
+ if (!existsSync(join(directory, "server", "main.js")))
32
+ throw new Error(`The installed System ${value.version} is incomplete`);
33
+ return { ...value, directory };
34
+ }
35
+ /** Serialize operations that can change files or native-service state. */
36
+ async exclusive(work) {
37
+ await mkdir(this.paths.root, { recursive: true });
38
+ const lock = join(this.paths.root, ".operation");
39
+ const owner = String(process.pid);
40
+ while (true) {
41
+ try {
42
+ const handle = await open(lock, "wx", 0o600);
43
+ try {
44
+ await handle.writeFile(`${owner}\n`);
45
+ return await work();
46
+ }
47
+ finally {
48
+ await handle.close();
49
+ await rm(lock, { force: true });
50
+ }
51
+ }
52
+ catch (error) {
53
+ if (!record(error) || error.code !== "EEXIST")
54
+ throw error;
55
+ const pid = await lockOwner(lock);
56
+ if (pid !== undefined && processAlive(pid))
57
+ throw new Error(`Another PhreshOS System operation is running (${pid})`);
58
+ const stale = `${lock}.stale-${randomUUID()}`;
59
+ try {
60
+ await rename(lock, stale);
61
+ await rm(stale, { force: true });
62
+ }
63
+ catch (replacement) {
64
+ if (!record(replacement) || replacement.code !== "ENOENT")
65
+ throw replacement;
66
+ }
67
+ }
68
+ }
69
+ }
70
+ async prepare(release) {
71
+ await mkdir(this.paths.releases, { recursive: true });
72
+ const directory = await mkdtemp(join(this.paths.releases, ".staging-"));
73
+ try {
74
+ extract(release.bytes, directory);
75
+ await validateDistribution(directory);
76
+ await this.dependencies(directory);
77
+ return { release, directory };
78
+ }
79
+ catch (error) {
80
+ await rm(directory, { recursive: true, force: true });
81
+ throw error;
82
+ }
83
+ }
84
+ async abandon(prepared) {
85
+ await rm(prepared.directory, { recursive: true, force: true });
86
+ }
87
+ async activate(prepared, previous) {
88
+ const directory = join(this.paths.releases, prepared.release.version);
89
+ const backup = `${directory}.previous-${randomUUID()}`;
90
+ const hadDirectory = existsSync(directory);
91
+ const installedAt = new Date().toISOString();
92
+ if (hadDirectory)
93
+ await rename(directory, backup);
94
+ try {
95
+ await writeFile(join(prepared.directory, ".release.json"), `${JSON.stringify({
96
+ version: prepared.release.version,
97
+ digest: prepared.release.digest,
98
+ installedAt
99
+ }, null, 2)}\n`, { mode: 0o600 });
100
+ await rename(prepared.directory, directory);
101
+ await this.pointTo(directory);
102
+ }
103
+ catch (error) {
104
+ await rm(directory, { recursive: true, force: true });
105
+ if (hadDirectory)
106
+ await rename(backup, directory);
107
+ if (previous)
108
+ await this.pointTo(previous.directory);
109
+ throw error;
110
+ }
111
+ const installed = {
112
+ version: prepared.release.version,
113
+ digest: prepared.release.digest,
114
+ directory,
115
+ installedAt
116
+ };
117
+ let settled = false;
118
+ return {
119
+ installed,
120
+ commit: async () => {
121
+ if (settled)
122
+ return;
123
+ settled = true;
124
+ if (hadDirectory)
125
+ await rm(backup, { recursive: true, force: true });
126
+ await this.removeOtherReleases(directory).catch(() => undefined);
127
+ },
128
+ rollback: async () => {
129
+ if (settled)
130
+ return;
131
+ settled = true;
132
+ if (previous)
133
+ await this.pointTo(previous.directory);
134
+ else
135
+ await rm(this.paths.current, { force: true });
136
+ await rm(directory, { recursive: true, force: true });
137
+ if (hadDirectory)
138
+ await rename(backup, directory);
139
+ }
140
+ };
141
+ }
142
+ async remove() {
143
+ await rm(this.paths.root, { recursive: true, force: true });
144
+ }
145
+ async removeOtherReleases(current) {
146
+ for (const entry of await readdir(this.paths.releases, { withFileTypes: true })) {
147
+ if (!entry.isDirectory())
148
+ continue;
149
+ const directory = join(this.paths.releases, entry.name);
150
+ if (directory !== current)
151
+ await rm(directory, { recursive: true, force: true });
152
+ }
153
+ }
154
+ async pointTo(directory) {
155
+ await mkdir(dirname(this.paths.current), { recursive: true });
156
+ const temporary = `${this.paths.current}.${randomUUID()}.tmp`;
157
+ try {
158
+ await symlink(relative(dirname(this.paths.current), directory), temporary, "dir");
159
+ await rename(temporary, this.paths.current);
160
+ }
161
+ finally {
162
+ await rm(temporary, { force: true });
163
+ }
164
+ }
165
+ }
166
+ async function validateDistribution(directory) {
167
+ let manifest;
168
+ try {
169
+ manifest = JSON.parse(await readFile(join(directory, "package.json"), "utf8"));
170
+ }
171
+ catch {
172
+ throw new Error("The System release has no valid production package manifest");
173
+ }
174
+ if (!record(manifest) || manifest.type !== "module" || !record(manifest.scripts) || manifest.scripts.start !== "node server/main.js" || !record(manifest.dependencies)) {
175
+ throw new Error("The System release package manifest is invalid");
176
+ }
177
+ for (const path of ["server/main.js", "client/index.html"]) {
178
+ if (!existsSync(join(directory, path)))
179
+ throw new Error(`The System release is missing ${path}`);
180
+ }
181
+ }
182
+ function extract(bytes, directory) {
183
+ const archive = new AdmZip(bytes);
184
+ for (const entry of archive.getEntries()) {
185
+ const name = entry.entryName.replaceAll("\\", "/");
186
+ const destination = resolve(directory, name);
187
+ const within = relative(directory, destination);
188
+ if (!name || name.startsWith("/") || name.split("/").includes("..") || isAbsolute(within) || within.startsWith("..")) {
189
+ throw new Error(`The System release contains an unsafe path: ${entry.entryName}`);
190
+ }
191
+ if (entry.isDirectory) {
192
+ requireDirectory(destination);
193
+ continue;
194
+ }
195
+ requireDirectory(dirname(destination));
196
+ writeFileSync(destination, entry.getData(), { mode: 0o600 });
197
+ }
198
+ }
199
+ function requireDirectory(path) {
200
+ mkdirSync(path, { recursive: true, mode: 0o700 });
201
+ }
202
+ async function installProductionDependencies(directory) {
203
+ const npm = process.platform === "win32" ? "npm.cmd" : "npm";
204
+ await requireSuccess(npm, ["install", "--omit=dev", "--no-audit", "--no-fund", "--package-lock=false"], { cwd: directory });
205
+ }
206
+ function releaseRecord(value) {
207
+ return record(value)
208
+ && typeof value.version === "string"
209
+ && /^[0-9]+\.[0-9]+\.[0-9]+$/.test(value.version)
210
+ && typeof value.digest === "string"
211
+ && /^[a-f0-9]{64}$/.test(value.digest)
212
+ && typeof value.installedAt === "string";
213
+ }
214
+ function record(value) {
215
+ return typeof value === "object" && value !== null && !Array.isArray(value);
216
+ }
217
+ async function lockOwner(path) {
218
+ for (let attempt = 0; attempt < 2; attempt += 1) {
219
+ try {
220
+ const value = (await readFile(path, "utf8")).trim();
221
+ if (/^[1-9][0-9]*$/.test(value))
222
+ return Number(value);
223
+ }
224
+ catch (error) {
225
+ if (!record(error) || error.code !== "ENOENT")
226
+ throw error;
227
+ return undefined;
228
+ }
229
+ await new Promise(settle => setTimeout(settle, 50));
230
+ }
231
+ return undefined;
232
+ }
233
+ function processAlive(pid) {
234
+ try {
235
+ process.kill(pid, 0);
236
+ return true;
237
+ }
238
+ catch (error) {
239
+ return record(error) && error.code === "EPERM";
240
+ }
241
+ }
@@ -0,0 +1,174 @@
1
+ import SystemInstallation from "./installation.js";
2
+ import { downloadSystemRelease, resolveSystemRelease } from "./release.js";
3
+ import { intakeReady, waitForIntake } from "./readiness.js";
4
+ import systemPaths from "./paths.js";
5
+ import systemService from "./service/index.js";
6
+ import nodeExecutable from "./node.js";
7
+ import { join } from "node:path";
8
+ /** Coordinates acquisition, immutable files, and the native service as one transaction. */
9
+ export default class SystemLifecycle {
10
+ dependencies;
11
+ constructor(dependencies) {
12
+ const paths = systemPaths();
13
+ const service = dependencies?.service ?? systemService();
14
+ this.dependencies = {
15
+ installation: dependencies?.installation ?? new SystemInstallation(paths),
16
+ service,
17
+ resolveRelease: dependencies?.resolveRelease ?? resolveSystemRelease,
18
+ downloadRelease: dependencies?.downloadRelease ?? downloadSystemRelease,
19
+ ready: dependencies?.ready ?? intakeReady,
20
+ wait: dependencies?.wait ?? waitForIntake
21
+ };
22
+ }
23
+ async install() {
24
+ return await this.dependencies.installation.exclusive(() => this.installExclusive());
25
+ }
26
+ async installExclusive() {
27
+ const { installation, service } = this.dependencies;
28
+ const previous = await installation.current();
29
+ const previousService = await service.inspect();
30
+ const executable = await nodeExecutable();
31
+ const release = await this.dependencies.resolveRelease();
32
+ const downloaded = await this.dependencies.downloadRelease(release);
33
+ const prepared = await installation.prepare(downloaded);
34
+ const activation = await this.activate(prepared, previous, previousService);
35
+ try {
36
+ await service.register(definition(installation, executable));
37
+ await service.enable();
38
+ await service.start();
39
+ await this.waitUntilReady();
40
+ await activation.commit();
41
+ return await this.status();
42
+ }
43
+ catch (error) {
44
+ await service.stop().catch(() => undefined);
45
+ await activation.rollback();
46
+ return await this.restoredFailure(error, previous, previousService);
47
+ }
48
+ }
49
+ async uninstall() {
50
+ return await this.dependencies.installation.exclusive(() => this.uninstallExclusive());
51
+ }
52
+ async uninstallExclusive() {
53
+ const { installation, service } = this.dependencies;
54
+ const state = await service.inspect();
55
+ if (state.running)
56
+ await service.stop();
57
+ if (state.enabled)
58
+ await service.disable();
59
+ if (state.registered)
60
+ await service.unregister();
61
+ await installation.remove();
62
+ }
63
+ async start() {
64
+ return await this.dependencies.installation.exclusive(() => this.startExclusive());
65
+ }
66
+ async startExclusive() {
67
+ await this.requireInstalledService();
68
+ await this.dependencies.service.start();
69
+ await this.waitUntilReady();
70
+ return await this.status();
71
+ }
72
+ async stop() {
73
+ return await this.dependencies.installation.exclusive(() => this.stopExclusive());
74
+ }
75
+ async stopExclusive() {
76
+ await this.requireInstalledService();
77
+ await this.dependencies.service.stop();
78
+ return await this.status();
79
+ }
80
+ async enable() {
81
+ return await this.dependencies.installation.exclusive(() => this.enableExclusive());
82
+ }
83
+ async enableExclusive() {
84
+ await this.requireInstalledService();
85
+ await this.dependencies.service.enable();
86
+ return await this.status();
87
+ }
88
+ async disable() {
89
+ return await this.dependencies.installation.exclusive(() => this.disableExclusive());
90
+ }
91
+ async disableExclusive() {
92
+ await this.requireInstalledService();
93
+ await this.dependencies.service.disable();
94
+ return await this.status();
95
+ }
96
+ async status() {
97
+ const { installation, service } = this.dependencies;
98
+ const [installed, state] = await Promise.all([installation.current(), service.inspect()]);
99
+ const ready = state.running && await this.dependencies.ready(installation.paths.intake);
100
+ return {
101
+ ...(installed ? { installed } : {}),
102
+ ...state,
103
+ ready,
104
+ root: installation.paths.root,
105
+ intake: installation.paths.intake,
106
+ log: installation.paths.log
107
+ };
108
+ }
109
+ async requireInstalledService() {
110
+ const [installed, state] = await Promise.all([
111
+ this.dependencies.installation.current(),
112
+ this.dependencies.service.inspect()
113
+ ]);
114
+ if (!installed)
115
+ throw new Error("PhreshOS System is not installed — run phresh system install");
116
+ if (!state.registered)
117
+ throw new Error("The PhreshOS System service is not registered — run phresh system install");
118
+ }
119
+ async waitUntilReady() {
120
+ const { installation, service } = this.dependencies;
121
+ try {
122
+ await this.dependencies.wait(installation.paths.intake, async () => (await service.inspect()).running);
123
+ }
124
+ catch (error) {
125
+ const message = error instanceof Error ? error.message : String(error);
126
+ throw new Error(`${message}. Service log: ${installation.paths.log}`, { cause: error });
127
+ }
128
+ }
129
+ async activate(prepared, previous, state) {
130
+ const { installation, service } = this.dependencies;
131
+ try {
132
+ if (state.running)
133
+ await service.stop();
134
+ return await installation.activate(prepared, previous);
135
+ }
136
+ catch (error) {
137
+ await installation.abandon(prepared);
138
+ return await this.restoredFailure(error, previous, state);
139
+ }
140
+ }
141
+ async restore(previous, state) {
142
+ const { installation, service } = this.dependencies;
143
+ if (!previous) {
144
+ await service.unregister().catch(() => undefined);
145
+ return;
146
+ }
147
+ await service.register(definition(installation, await nodeExecutable()));
148
+ if (state.enabled)
149
+ await service.enable();
150
+ else
151
+ await service.disable();
152
+ if (state.running) {
153
+ await service.start();
154
+ await this.waitUntilReady();
155
+ }
156
+ }
157
+ async restoredFailure(error, previous, state) {
158
+ try {
159
+ await this.restore(previous, state);
160
+ }
161
+ catch (restoration) {
162
+ throw new AggregateError([error, restoration], "The System update failed and its previous service could not be restored");
163
+ }
164
+ throw error;
165
+ }
166
+ }
167
+ function definition(installation, executable) {
168
+ return {
169
+ executable,
170
+ entry: join(installation.paths.current, "server", "main.js"),
171
+ directory: installation.paths.current,
172
+ output: installation.paths.log
173
+ };
174
+ }
@@ -0,0 +1,13 @@
1
+ import { isAbsolute } from "node:path";
2
+ import { requireSuccess } from "./process.js";
3
+ /** Resolve the real Node executable even when another runtime invoked the CLI. */
4
+ export default async function nodeExecutable() {
5
+ if (!process.versions.bun && process.release.name === "node" && isAbsolute(process.execPath))
6
+ return process.execPath;
7
+ const command = process.platform === "win32" ? "node.exe" : "node";
8
+ const result = await requireSuccess(command, ["-p", "process.execPath"]);
9
+ const executable = result.stdout.trim();
10
+ if (!isAbsolute(executable))
11
+ throw new Error("A real Node.js executable is required to run the PhreshOS System service");
12
+ return executable;
13
+ }
@@ -0,0 +1,28 @@
1
+ import { homedir } from "node:os";
2
+ import { isAbsolute, join } from "node:path";
3
+ /** The installation is separate from the persistent state it operates on. */
4
+ export default function systemPaths(platform = process.platform, userHome = homedir(), variables = process.env) {
5
+ const storage = join(userHome, ".phreshos");
6
+ const root = platform === "darwin"
7
+ ? join(userHome, "Library", "Application Support", "PhreshOS", "System")
8
+ : platform === "linux"
9
+ ? join(absoluteOr(variables.XDG_DATA_HOME, join(userHome, ".local", "share"), "XDG_DATA_HOME"), "phreshos", "system")
10
+ : platform === "win32"
11
+ ? join(absoluteOr(variables.LOCALAPPDATA, join(userHome, "AppData", "Local"), "LOCALAPPDATA"), "PhreshOS", "System")
12
+ : join(userHome, ".local", "share", "phreshos", "system");
13
+ return {
14
+ root,
15
+ releases: join(root, "releases"),
16
+ current: join(root, "current"),
17
+ storage,
18
+ intake: join(storage, "intake.sock"),
19
+ log: join(storage, "service.log")
20
+ };
21
+ }
22
+ function absoluteOr(value, fallback, name) {
23
+ if (value === undefined)
24
+ return fallback;
25
+ if (!isAbsolute(value))
26
+ throw new Error(`${name} must be an absolute filesystem path`);
27
+ return value;
28
+ }
@@ -0,0 +1,23 @@
1
+ import { spawn } from "node:child_process";
2
+ /** Execute one exact program without involving a command shell. */
3
+ export function execute(command, args, options = {}) {
4
+ return new Promise(function (settle, refuse) {
5
+ const child = spawn(command, args, {
6
+ cwd: options.cwd,
7
+ env: options.env,
8
+ stdio: ["ignore", "pipe", "pipe"]
9
+ });
10
+ let stdout = "";
11
+ let stderr = "";
12
+ child.stdout.setEncoding("utf8").on("data", chunk => stdout += chunk);
13
+ child.stderr.setEncoding("utf8").on("data", chunk => stderr += chunk);
14
+ child.once("error", refuse);
15
+ child.once("close", code => settle({ code: code ?? 1, stdout, stderr }));
16
+ });
17
+ }
18
+ export async function requireSuccess(command, args, options) {
19
+ const result = await execute(command, args, options);
20
+ if (result.code !== 0)
21
+ throw new Error(result.stderr.trim() || result.stdout.trim() || `${command} exited with code ${result.code}`);
22
+ return result;
23
+ }
@@ -0,0 +1,29 @@
1
+ import { connect } from "node:net";
2
+ export async function intakeReady(path) {
3
+ return await new Promise(function (settle) {
4
+ const socket = connect(path);
5
+ const timeout = setTimeout(() => finish(false), 500);
6
+ let finished = false;
7
+ socket.once("connect", () => finish(true));
8
+ socket.once("error", () => finish(false));
9
+ function finish(ready) {
10
+ if (finished)
11
+ return;
12
+ finished = true;
13
+ clearTimeout(timeout);
14
+ socket.destroy();
15
+ settle(ready);
16
+ }
17
+ });
18
+ }
19
+ export async function waitForIntake(path, running, timeout = 15_000) {
20
+ const until = Date.now() + timeout;
21
+ while (Date.now() < until) {
22
+ if (await intakeReady(path))
23
+ return;
24
+ if (!await running())
25
+ throw new Error("The PhreshOS System stopped before its intake became ready");
26
+ await new Promise(settle => setTimeout(settle, 100));
27
+ }
28
+ throw new Error(`The PhreshOS System did not become ready within ${Math.ceil(timeout / 1000)} seconds`);
29
+ }
@@ -0,0 +1,86 @@
1
+ import { createHash } from "node:crypto";
2
+ const releases = "https://api.github.com/repos/PhreshOS/system/releases?per_page=100";
3
+ const compatible = { major: 0, minor: 1 };
4
+ /** Resolve the newest stable release in the System line supported by this CLI. */
5
+ export async function resolveSystemRelease(fetcher = fetch) {
6
+ const response = await fetcher(releases, {
7
+ headers: {
8
+ Accept: "application/vnd.github+json",
9
+ "User-Agent": "@phreshos/cli"
10
+ },
11
+ signal: AbortSignal.timeout(30_000)
12
+ });
13
+ if (!response.ok)
14
+ throw new Error(`The System release list could not be read (${response.status} ${response.statusText})`);
15
+ return selectSystemRelease(await response.json());
16
+ }
17
+ export function selectSystemRelease(value) {
18
+ if (!Array.isArray(value))
19
+ throw new Error("The System release list is invalid");
20
+ const candidates = value.flatMap(function (item) {
21
+ if (!record(item) || item.draft === true || item.prerelease === true || typeof item.tag_name !== "string" || !Array.isArray(item.assets))
22
+ return [];
23
+ const version = parseVersion(item.tag_name);
24
+ if (!version || version.major !== compatible.major || version.minor !== compatible.minor)
25
+ return [];
26
+ const archiveName = `phreshos@${version.value}.zip`;
27
+ const checksumName = `${archiveName}.sha256`;
28
+ const archive = asset(item.assets, archiveName);
29
+ const checksum = asset(item.assets, checksumName);
30
+ return archive && checksum ? [{ version: version.value, archive, checksum }] : [];
31
+ });
32
+ candidates.sort((left, right) => compare(right.version, left.version));
33
+ const selected = candidates[0];
34
+ if (!selected)
35
+ throw new Error(`No compatible PhreshOS System ${compatible.major}.${compatible.minor}.x release is available`);
36
+ return selected;
37
+ }
38
+ /** Download both release assets and refuse any byte not named by the checksum. */
39
+ export async function downloadSystemRelease(release, fetcher = fetch) {
40
+ const [archive, checksum] = await Promise.all([
41
+ fetchAsset(release.archive, fetcher),
42
+ fetchAsset(release.checksum, fetcher)
43
+ ]);
44
+ const bytes = Buffer.from(await archive.arrayBuffer());
45
+ const said = (await checksum.text()).trim();
46
+ const name = `phreshos@${release.version}.zip`;
47
+ const match = /^([a-f0-9]{64})\s+(.+)$/i.exec(said);
48
+ if (!match || match[2] !== name)
49
+ throw new Error(`The checksum for ${name} is invalid`);
50
+ const digest = createHash("sha256").update(bytes).digest("hex");
51
+ if (digest !== match[1]?.toLowerCase())
52
+ throw new Error(`The downloaded ${name} does not match its SHA-256 checksum`);
53
+ return { ...release, bytes, digest };
54
+ }
55
+ async function fetchAsset(url, fetcher) {
56
+ const response = await fetcher(url, {
57
+ headers: { "User-Agent": "@phreshos/cli" },
58
+ signal: AbortSignal.timeout(120_000)
59
+ });
60
+ if (!response.ok)
61
+ throw new Error(`A System release asset could not be downloaded (${response.status} ${response.statusText})`);
62
+ return response;
63
+ }
64
+ function asset(assets, name) {
65
+ const found = assets.find(item => record(item) && item.name === name && typeof item.browser_download_url === "string");
66
+ return record(found) && typeof found.browser_download_url === "string" ? found.browser_download_url : undefined;
67
+ }
68
+ function parseVersion(tag) {
69
+ const match = /^v(\d+)\.(\d+)\.(\d+)$/.exec(tag);
70
+ if (!match)
71
+ return undefined;
72
+ return {
73
+ value: tag.slice(1),
74
+ major: Number(match[1]),
75
+ minor: Number(match[2]),
76
+ patch: Number(match[3])
77
+ };
78
+ }
79
+ function compare(left, right) {
80
+ const a = left.split(".").map(Number);
81
+ const b = right.split(".").map(Number);
82
+ return (a[0] ?? 0) - (b[0] ?? 0) || (a[1] ?? 0) - (b[1] ?? 0) || (a[2] ?? 0) - (b[2] ?? 0);
83
+ }
84
+ function record(value) {
85
+ return typeof value === "object" && value !== null;
86
+ }
@@ -0,0 +1,11 @@
1
+ import { homedir } from "node:os";
2
+ import LinuxSystemService from "./linux.js";
3
+ import MacOSSystemService from "./macos.js";
4
+ /** Select the native per-user service manager without changing its semantics. */
5
+ export default function systemService(platform = process.platform, userHome = homedir()) {
6
+ if (platform === "darwin")
7
+ return new MacOSSystemService(userHome);
8
+ if (platform === "linux")
9
+ return new LinuxSystemService(userHome);
10
+ throw new Error(`PhreshOS System services are not supported on ${platform}`);
11
+ }
@@ -0,0 +1,94 @@
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=${quote(definition.directory)}
83
+ Restart=on-failure
84
+ RestartSec=2
85
+ StandardOutput=append:${quote(definition.output)}
86
+ StandardError=append:${quote(definition.output)}
87
+
88
+ [Install]
89
+ WantedBy=default.target
90
+ `;
91
+ }
92
+ function quote(value) {
93
+ return JSON.stringify(value);
94
+ }
@@ -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("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;");
120
+ }
121
+ function escapePattern(value) {
122
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
123
+ }
File without changes
@@ -16,7 +16,7 @@
16
16
  "react-dom": "^19.2.8"
17
17
  },
18
18
  "devDependencies": {
19
- "@phreshos/cli": "^0.1.5",
19
+ "@phreshos/cli": "^0.1.6",
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,12 +1,19 @@
1
1
  {
2
2
  "name": "@phreshos/cli",
3
3
  "type": "module",
4
- "version": "0.1.5",
4
+ "version": "0.1.6",
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:macos-service": "node --run compile && node scripts/verify-macos-service.mjs",
10
17
  "prepack": "node --run build"
11
18
  },
12
19
  "bin": {
@@ -14,8 +21,29 @@
14
21
  },
15
22
  "files": [
16
23
  "dist",
24
+ "LICENSE",
17
25
  "README.md"
18
26
  ],
27
+ "author": "Zohayr SLILEH",
28
+ "license": "MIT",
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "git+https://github.com/PhreshOS/cli.git"
32
+ },
33
+ "bugs": {
34
+ "url": "https://github.com/PhreshOS/cli/issues"
35
+ },
36
+ "homepage": "https://github.com/PhreshOS/cli#readme",
37
+ "keywords": [
38
+ "phreshos",
39
+ "cli",
40
+ "program"
41
+ ],
42
+ "publishConfig": {
43
+ "access": "public",
44
+ "provenance": true
45
+ },
46
+ "packageManager": "bun@1.3.14",
19
47
  "dependencies": {
20
48
  "@clack/prompts": "^1.7.0",
21
49
  "@phreshos/core": "^0.1.1",