@phreshos/cli 0.1.21 → 0.1.23

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
@@ -22,21 +22,35 @@ 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
24
  phresh system status # inspect the local System and its background service
25
+ phresh program list # inspect authoritative state in the running System
26
+ phresh describe endpoint ask # read one capability's exact contract
25
27
  ```
26
28
 
27
29
  `phresh --help` lists them, `phresh <command> --help` explains one, and
28
- `phresh --version` says which CLI you have. Nothing is guessed: an
30
+ `phresh --version` says which CLI you have. `phresh describe [path...]` exposes
31
+ the same complete command tree as machine-readable contracts. Nothing is guessed: an
29
32
  unknown command, an unknown flag and a malformed option are each refused
30
33
  and named.
31
34
 
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.
35
+ Program authoring commands act on the current project when no Program name is
36
+ provided. `install` and `uninstall` accept a direct Program name. Running-System
37
+ capabilities live in the top-level `program`, `process`, `endpoint`, and `window`
38
+ namespaces. System execution and native lifecycle remain isolated under
39
+ `phresh system`.
37
40
 
38
41
  ## System lifecycle
39
42
 
43
+ Run a System project or prepared release directly while selecting an independent
44
+ absolute state home:
45
+
46
+ ```bash
47
+ PHRESHOS_HOME=/absolute/state phresh system .
48
+ ```
49
+
50
+ When `PHRESHOS_HOME` is omitted here, the CLI passes `~/.phreshos`. Running the
51
+ System project itself without the CLI instead defaults to that project's
52
+ `storage/` directory.
53
+
40
54
  ```bash
41
55
  phresh system install
42
56
  phresh system uninstall
@@ -70,7 +84,7 @@ changing them; `version` reports only the installed System release.
70
84
 
71
85
  Installation files and persistent System state have separate homes. Removing
72
86
  the System unregisters its service and removes its release files while keeping
73
- `~/.phreshos`, including Programs and owner data. Local Program intake uses an
87
+ `~/.phreshos`, including Programs and owner data. The owner-local gateway uses an
74
88
  owner-only socket file on POSIX and an owner-created duplex named pipe on
75
89
  Windows; neither becomes a network endpoint or introduces a bearer secret.
76
90
 
@@ -297,9 +311,9 @@ say `start: false`.
297
311
  Both **run your program without installing it, and stay attached.** They
298
312
  print the `program.json` it will be declared as, hand that to the system
299
313
  through the socket below the selected system home. With no override, that is
300
- `~/.phreshos/intake.sock`. Set `PHRESHOS_HOME` to an absolute system home to
314
+ `~/.phreshos/gateway.sock`. Set `PHRESHOS_HOME` to an absolute system home to
301
315
  address another system instance; the CLI derives
302
- `<PHRESHOS_HOME>/intake.sock` from it. The socket is not selected
316
+ `<PHRESHOS_HOME>/gateway.sock` from it. The socket is not selected
303
317
  separately from its instance. Only your account can open it, so nothing is
304
318
  sent to prove anything, and then the command holds.
305
319
 
@@ -326,7 +340,7 @@ untouched while the attached Program becomes the sole runtime occupant. Its
326
340
  root process tethers the whole Program to this command; when it exits, remaining
327
341
  processes end and the runtime record disappears. A later `phresh install` can
328
342
  replace the preserved installed files and immediately register the identity as
329
- installed again. If no system is listening, the intake says so plainly rather
343
+ installed again. If no system is listening, the gateway says so plainly rather
330
344
  than exposing `ENOENT`.
331
345
 
332
346
  An attached Program still owns persistent project storage. The authoring tool
@@ -354,15 +368,19 @@ beside your source. A client development URL remains the URL the author wrote.
354
368
 
355
369
  ```bash
356
370
  phresh install # this project, laid out on this machine
371
+ phresh install flambo # the official Flambo Program
357
372
  ```
358
373
 
359
- **It takes no package, and neither does the system.** What is sent is
374
+ Without a name, what is sent is
360
375
  the description this directory derives, and the system copies what it
361
376
  names into place — your program's parts are already on this disk at the
362
377
  locations it names, so there is nothing an archive would carry that the
363
378
  description does not already point at. `phresh pack` is for when you have
364
379
  somewhere to send a program; installing here is a different act.
365
380
 
381
+ With a name, the CLI resolves and verifies that official Program's production
382
+ release directly; the current directory is irrelevant.
383
+
366
384
  If `buildCommand` is declared, it completes successfully before anything is
367
385
  sent to the system. Without it, install uses the production files exactly as
368
386
  they stand.
@@ -372,7 +390,7 @@ marked installed, and reconstructed after a restart. Running is the other one:
372
390
  `phresh start` / `phresh dev` register it under its declared identity and
373
391
  attach its whole lifetime to your terminal.
374
392
 
375
- The command installs through the machine's local intake. A running
393
+ The command installs through the machine's local gateway. A running
376
394
  Program may also install itself through the server SDK; installation is not a
377
395
  client capability.
378
396
 
@@ -388,6 +406,7 @@ bytes and no path. You have the paths.
388
406
 
389
407
  ```bash
390
408
  phresh uninstall
409
+ phresh uninstall flambo
391
410
  phresh uninstall --everything
392
411
  ```
393
412
 
@@ -401,8 +420,9 @@ that Server directory before removing files. `phresh uninstall` writes its
401
420
  ordered `stdout` and `stderr` chunks as they arrive. A failed cleanup command
402
421
  aborts removal and reports the failure.
403
422
 
404
- The identity comes from this project's `phresh.config.ts`; the command does not
405
- accept an arbitrary Program identity.
423
+ Without a name, the identity comes from this project's `phresh.config.ts`.
424
+ With a name, the installed Program is addressed directly and the current
425
+ directory is irrelevant.
406
426
 
407
427
  ## What pack produces
408
428
 
package/dist/attach.js CHANGED
@@ -1,4 +1,4 @@
1
- import speak, { socketPath } from "./program-intake.js";
1
+ import { gatewayPath, streamProgram } from "./gateway.js";
2
2
  /**
3
3
  * Run a program on this machine's system, and stay with it.
4
4
  *
@@ -11,9 +11,9 @@ import speak, { socketPath } from "./program-intake.js";
11
11
  * end — when the program ends, the system says so and closes, and this
12
12
  * resolves with the program's own status.
13
13
  */
14
- export default async function attach(program, options = {}, watching = {}, path = socketPath, signal) {
14
+ export default async function attach(program, options = {}, watching = {}, path = gatewayPath(), signal) {
15
15
  let ended = null;
16
- await speak({ word: "run", program, options }, function (event) {
16
+ await streamProgram({ word: "run", program, options }, function (event) {
17
17
  if (event.event === "started")
18
18
  watching.started?.(String(event.process));
19
19
  if (event.event === "output")
package/dist/cli.js CHANGED
@@ -9,6 +9,9 @@ import init from "./init.js";
9
9
  import pack from "./pack.js";
10
10
  import uninstall from "./uninstall.js";
11
11
  import systemCommands from "./system/command.js";
12
+ import controlCommands from "./control-command.js";
13
+ import describeCommands from "./describe-command.js";
14
+ import { commandContract } from "./command-contract.js";
12
15
  const { version } = metadata;
13
16
  const coreRange = metadata.dependencies["@phreshos/core"];
14
17
  const runOptionPrefix = "--run-option-";
@@ -22,6 +25,7 @@ const program = new Command()
22
25
  writeOut: value => process.stdout.write(spaced(value)),
23
26
  writeErr: value => process.stderr.write(spaced(value))
24
27
  });
28
+ commandContract(program);
25
29
  program.addHelpText("after", "\nRun phresh <command> --help for detailed command guidance.\n");
26
30
  describe(program.command("create")
27
31
  .description("create a new Program project")
@@ -96,12 +100,14 @@ describe(program.command("install")
96
100
  "it now; --startup persists the same default launch for future starts."
97
101
  ]);
98
102
  describe(program.command("uninstall")
99
- .description("uninstall this Program")
103
+ .description("uninstall a local or installed Program")
104
+ .argument("[name]", "name of an installed Program")
100
105
  .option("--everything", "also remove Processes, data, and runtime state")
101
- .action(async function (options) {
102
- await uninstall(options.everything === true);
106
+ .action(async function (name, options) {
107
+ await uninstall({ name, everything: options.everything === true });
103
108
  }), [
104
- "Removes the installed Program files while preserving its Processes, data,",
109
+ "Without a name, uses the Program declared by this project. Removes its",
110
+ "installed files while preserving its Processes, data,",
105
111
  "and runtime Program. --everything removes all system-owned state."
106
112
  ]);
107
113
  attached("start", "run the production Program without installing", [
@@ -114,6 +120,8 @@ attached("dev", "run the development Program without installing", [
114
120
  "declarations. A Client URL must respond within 15 seconds before launch."
115
121
  ], "development");
116
122
  systemCommands(program);
123
+ controlCommands(program);
124
+ describeCommands(program);
117
125
  // Every command begins with the same breathing room. Keep this at the entry
118
126
  // point so individual commands never need to manufacture their own opening.
119
127
  console.log("");
@@ -148,6 +156,7 @@ function attached(name, summary, detail, mode) {
148
156
  }
149
157
  function describe(command, paragraphs) {
150
158
  command.addHelpText("after", `\n${paragraphs.map(line => line ? ` ${line}` : "").join("\n")}\n`);
159
+ commandContract(command, { guidance: paragraphs.filter(Boolean) });
151
160
  return command;
152
161
  }
153
162
  function runOptions(command, args) {
@@ -0,0 +1,12 @@
1
+ const contracts = new WeakMap();
2
+ /** Attach machine-readable policy to the same Commander node that owns syntax. */
3
+ export function commandContract(command, contract = {}) {
4
+ contracts.set(command, Object.freeze({
5
+ ...contract,
6
+ ...(contract.guidance ? { guidance: Object.freeze([...contract.guidance]) } : {})
7
+ }));
8
+ return command;
9
+ }
10
+ export function readCommandContract(command) {
11
+ return contracts.get(command);
12
+ }
@@ -0,0 +1,7 @@
1
+ import { requestSystem } from "./gateway.js";
2
+ /** Owner-local client for the authoritative running-System contract. */
3
+ export default class LocalSystemControl {
4
+ execute(request, signal) {
5
+ return requestSystem(request, undefined, signal);
6
+ }
7
+ }
@@ -0,0 +1,50 @@
1
+ import { systemControl, systemControlOperation } from "@phreshos/core";
2
+ import LocalSystemControl from "./control-client.js";
3
+ import { commandContract } from "./command-contract.js";
4
+ /** Build the top-level running-System commands from Core's authoritative catalog. */
5
+ export default function controlCommands(program, provided) {
6
+ let client = provided;
7
+ const current = () => client ?? (client = new LocalSystemControl());
8
+ for (const [capabilityName, capability] of Object.entries(systemControl)) {
9
+ const group = commandContract(program.command(capabilityName)
10
+ .description(capability.description)
11
+ .addHelpText("after", `\nOperating guidance:\n${capability.guidance.map(item => ` - ${item}`).join("\n")}\n`), {
12
+ guidance: capability.guidance,
13
+ capability
14
+ });
15
+ for (const [operationName, operation] of Object.entries(capability.operations)) {
16
+ commandContract(group.command(operationName)
17
+ .description(operation.description)
18
+ .option("--input <json>", "operation input as one JSON object", "{}")
19
+ .option("--compact", "write machine-readable JSON on one line")
20
+ .addHelpText("after", operationHelp(capabilityName, operationName))
21
+ .action(async function (options) {
22
+ const input = parseInput(options.input);
23
+ const result = await current().execute({
24
+ capability: capabilityName,
25
+ operation: operationName,
26
+ input
27
+ });
28
+ console.log(JSON.stringify(result ?? null, null, options.compact ? undefined : 2));
29
+ }), { capability: operation });
30
+ }
31
+ }
32
+ }
33
+ function operationHelp(capability, operation) {
34
+ const definition = systemControlOperation(capability, operation);
35
+ if (!definition)
36
+ return "";
37
+ return `\nInput schema:\n${JSON.stringify(definition.input, null, 2)}\n\nExamples:\n${definition.examples.map(example => ` phresh ${capability} ${operation} --input '${JSON.stringify(example)}'`).join("\n")}\n`;
38
+ }
39
+ function parseInput(value) {
40
+ let parsed;
41
+ try {
42
+ parsed = JSON.parse(value);
43
+ }
44
+ catch {
45
+ throw new Error("--input must be valid JSON");
46
+ }
47
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
48
+ throw new Error("--input must be one JSON object");
49
+ return parsed;
50
+ }
@@ -0,0 +1,60 @@
1
+ import { systemControl, systemControlOperation } from "@phreshos/core";
2
+ import { commandContract, readCommandContract } from "./command-contract.js";
3
+ /** Add one machine-readable description entry point for the complete CLI tree. */
4
+ export default function describeCommands(program) {
5
+ commandContract(program.command("describe")
6
+ .description("describe any Phresh capability as machine-readable data")
7
+ .argument("[path...]", "command path to describe")
8
+ .option("--compact", "write JSON on one line")
9
+ .action(function (path, options) {
10
+ const command = resolveCommand(program, path);
11
+ const description = describe(command, path);
12
+ console.log(JSON.stringify(description, null, options.compact ? undefined : 2));
13
+ }), { guidance: ["Omit the path to discover all top-level commands, then describe progressively deeper paths."] });
14
+ }
15
+ function resolveCommand(root, path) {
16
+ let command = root;
17
+ for (const name of path) {
18
+ const child = command.commands.find(candidate => candidate.name() === name);
19
+ if (!child)
20
+ throw new Error(`Unknown command path "${path.join(" ")}"`);
21
+ command = child;
22
+ }
23
+ return command;
24
+ }
25
+ function describe(command, path) {
26
+ const registered = readCommandContract(command);
27
+ const capability = registered?.capability ?? (path.length === 1 && Object.hasOwn(systemControl, path[0] ?? "")
28
+ ? systemControl[path[0]]
29
+ : null);
30
+ const operation = path.length === 2 ? systemControlOperation(path[0] ?? "", path[1] ?? "") : null;
31
+ return {
32
+ path,
33
+ name: command.name(),
34
+ description: command.description(),
35
+ arguments: command.registeredArguments.map(argumentDescription),
36
+ options: command.options.map(optionDescription),
37
+ commands: command.commands.map(child => ({ name: child.name(), description: child.description() })),
38
+ ...(registered?.guidance ? { guidance: registered.guidance } : {}),
39
+ ...(operation ? { capability: operation } : capability ? { capability } : {})
40
+ };
41
+ }
42
+ function argumentDescription(argument) {
43
+ return {
44
+ name: argument.name(),
45
+ description: argument.description,
46
+ required: argument.required,
47
+ variadic: argument.variadic,
48
+ ...(argument.defaultValue === undefined ? {} : { default: argument.defaultValue })
49
+ };
50
+ }
51
+ function optionDescription(option) {
52
+ return {
53
+ flags: option.flags,
54
+ description: option.description,
55
+ value: option.required ? "required" : option.optional ? "optional" : "none",
56
+ variadic: option.variadic,
57
+ ...(option.defaultValue === undefined ? {} : { default: option.defaultValue }),
58
+ ...(option.argChoices === undefined ? {} : { choices: option.argChoices })
59
+ };
60
+ }
@@ -0,0 +1,96 @@
1
+ import { createHash } from "node:crypto";
2
+ import { connect } from "node:net";
3
+ import { join } from "node:path";
4
+ import phreshosHome from "./home.js";
5
+ /** Resolve the one owner-local gateway address for the selected System home. */
6
+ export function gatewayPath(home = phreshosHome(), platform = process.platform) {
7
+ if (platform !== "win32")
8
+ return join(home, "gateway.sock");
9
+ const owner = home.replaceAll("\\", "/").replace(/\/+$/, "").toLowerCase();
10
+ const identity = createHash("sha256").update(owner).digest("hex").slice(0, 32);
11
+ return `\\\\.\\pipe\\phreshos-${identity}-gateway`;
12
+ }
13
+ /** Stream one Program lifecycle request over the shared gateway. */
14
+ export function streamProgram(request, heard, path = gatewayPath(), signal) {
15
+ return new Promise(function (resolve, reject) {
16
+ const socket = connect(path);
17
+ let buffer = "";
18
+ let failure = null;
19
+ let cancelled = false;
20
+ const cancel = () => {
21
+ cancelled = true;
22
+ socket.destroy();
23
+ };
24
+ if (signal?.aborted)
25
+ cancel();
26
+ else
27
+ signal?.addEventListener("abort", cancel, { once: true });
28
+ socket.on("connect", () => socket.write(`${JSON.stringify({ target: "program", request })}\n`));
29
+ socket.on("data", function (chunk) {
30
+ buffer += String(chunk);
31
+ const lines = buffer.split("\n");
32
+ buffer = lines.pop() ?? "";
33
+ for (const line of lines)
34
+ if (line.trim()) {
35
+ const event = JSON.parse(line);
36
+ if (event.event === "error")
37
+ failure = new Error(String(event.message));
38
+ else
39
+ heard(event);
40
+ }
41
+ });
42
+ socket.on("error", () => {
43
+ if (!cancelled)
44
+ reject(new Error(`No System gateway is listening at ${path} — start PhreshOS first`));
45
+ });
46
+ socket.on("close", () => {
47
+ signal?.removeEventListener("abort", cancel);
48
+ if (cancelled)
49
+ reject(new Error("The attached launch was stopped"));
50
+ else if (failure)
51
+ reject(failure);
52
+ else
53
+ resolve();
54
+ });
55
+ });
56
+ }
57
+ /** Execute one short request over the shared gateway. */
58
+ export function requestSystem(request, path = gatewayPath(), signal) {
59
+ return new Promise((resolve, reject) => {
60
+ const socket = connect(path);
61
+ let buffer = "";
62
+ let settled = false;
63
+ const finish = (work) => {
64
+ if (settled)
65
+ return;
66
+ settled = true;
67
+ signal?.removeEventListener("abort", cancel);
68
+ socket.destroy();
69
+ work();
70
+ };
71
+ const cancel = () => finish(() => reject(signal?.reason instanceof Error ? signal.reason : new Error("The request was cancelled")));
72
+ signal?.addEventListener("abort", cancel, { once: true });
73
+ socket.on("connect", () => socket.write(`${JSON.stringify({ target: "system", request })}\n`));
74
+ socket.on("data", function (chunk) {
75
+ buffer += String(chunk);
76
+ const boundary = buffer.indexOf("\n");
77
+ if (boundary < 0)
78
+ return;
79
+ let outcome;
80
+ try {
81
+ outcome = JSON.parse(buffer.slice(0, boundary));
82
+ }
83
+ catch {
84
+ return finish(() => reject(new Error("The System returned an invalid gateway response")));
85
+ }
86
+ if (outcome.success)
87
+ finish(() => resolve(outcome.result));
88
+ else
89
+ finish(() => reject(new Error(outcome.error)));
90
+ });
91
+ socket.on("error", () => finish(() => reject(new Error(`No System gateway is listening at ${path} — start PhreshOS first`))));
92
+ socket.on("close", () => finish(() => reject(new Error("The System closed the gateway request without an answer"))));
93
+ if (signal?.aborted)
94
+ cancel();
95
+ });
96
+ }
package/dist/home.js ADDED
@@ -0,0 +1,16 @@
1
+ import { homedir } from "node:os";
2
+ import { existsSync, realpathSync } from "node:fs";
3
+ import { isAbsolute, join, normalize } from "node:path";
4
+ /** Resolve the absolute PhreshOS home selected for this CLI invocation. */
5
+ export default function phreshosHome(environment = process.env, userHome = homedir()) {
6
+ const selected = environment.PHRESHOS_HOME;
7
+ if (selected === undefined)
8
+ return canonical(join(userHome, ".phreshos"));
9
+ if (!isAbsolute(selected))
10
+ throw new Error("PHRESHOS_HOME must be an absolute filesystem path");
11
+ return canonical(selected);
12
+ }
13
+ function canonical(path) {
14
+ const normalized = normalize(path);
15
+ return existsSync(normalized) ? realpathSync(normalized) : normalized;
16
+ }
package/dist/install.js CHANGED
@@ -10,7 +10,7 @@ import build from "./build-command.js";
10
10
  * A local project is built and derived from its authoring declaration. An
11
11
  * official name resolves a verified production package and turns its
12
12
  * canonical paths into the same concrete description. From that point on,
13
- * both sources cross the exact same intake and the System performs the exact
13
+ * both sources cross the exact same gateway and the System performs the exact
14
14
  * same authoritative installation.
15
15
  *
16
16
  * When the author config declares `buildCommand`, it runs here before the
@@ -1,4 +1,4 @@
1
- import speak from "./program-intake.js";
1
+ import { streamProgram } from "./gateway.js";
2
2
  import writeProgramCommandOutput from "./program-command-output.js";
3
3
  /** Install one prepared Program and await every explicitly requested outcome. */
4
4
  export default async function installProgram(program, options = {}) {
@@ -6,7 +6,7 @@ export default async function installProgram(program, options = {}) {
6
6
  let replaced = false;
7
7
  let startupEnabled = false;
8
8
  let processValue;
9
- await speak({
9
+ await streamProgram({
10
10
  word: "install",
11
11
  program,
12
12
  run: options.run === true,
@@ -1,11 +1,26 @@
1
1
  import SystemLifecycle from "./lifecycle.js";
2
2
  import prompts, { ReportedFailure } from "../prompts.js";
3
3
  import { accent, caution, dim, negative, positive } from "../style.js";
4
+ import runSystem from "./run.js";
5
+ import { commandContract } from "../command-contract.js";
4
6
  /** Attach the System lifecycle without mixing it with Program commands. */
5
7
  export default function systemCommands(program, provided) {
6
8
  let lifecycle = provided;
7
9
  const current = () => lifecycle ?? (lifecycle = new SystemLifecycle());
8
- const system = program.command("system").description("install and manage the PhreshOS System");
10
+ const system = commandContract(program.command("system")
11
+ .description("run, install, and manage the PhreshOS System")
12
+ .argument("[source]", "System project or prepared release to run directly")
13
+ .action(async function (source) {
14
+ if (!source)
15
+ return system.help();
16
+ await runSystem(source);
17
+ }), {
18
+ guidance: [
19
+ "A source argument runs that System directly; it is resolved independently from PHRESHOS_HOME.",
20
+ "Without a source argument, choose one native lifecycle operation.",
21
+ "Running-System Programs, Processes, Endpoints, and Windows are top-level capabilities, not System subcommands."
22
+ ]
23
+ });
9
24
  system.command("install")
10
25
  .description("install or update the System and start its service")
11
26
  .action(async function () {
@@ -1,6 +1,6 @@
1
1
  import { connect } from "node:net";
2
- export async function intakeReady(path) {
3
- return await new Promise(function (settle) {
2
+ export async function gatewayReady(path) {
3
+ return await new Promise(function (resolve) {
4
4
  const socket = connect(path);
5
5
  const timeout = setTimeout(() => finish(false), 500);
6
6
  let finished = false;
@@ -12,18 +12,18 @@ export async function intakeReady(path) {
12
12
  finished = true;
13
13
  clearTimeout(timeout);
14
14
  socket.destroy();
15
- settle(ready);
15
+ resolve(ready);
16
16
  }
17
17
  });
18
18
  }
19
- export async function waitForIntake(path, running, timeout = 15_000) {
19
+ export async function waitForGateway(path, running, timeout = 15_000) {
20
20
  const until = Date.now() + timeout;
21
21
  while (Date.now() < until) {
22
- if (await intakeReady(path))
22
+ if (await gatewayReady(path))
23
23
  return;
24
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));
25
+ throw new Error("The PhreshOS System stopped before its gateway became ready");
26
+ await new Promise(resolve => setTimeout(resolve, 100));
27
27
  }
28
28
  throw new Error(`The PhreshOS System did not become ready within ${Math.ceil(timeout / 1000)} seconds`);
29
29
  }
@@ -1,12 +1,15 @@
1
1
  import SystemInstallation from "./installation.js";
2
2
  import { downloadSystemRelease, resolveSystemRelease } from "./release.js";
3
- import { intakeReady, waitForIntake } from "./readiness.js";
3
+ import { gatewayReady, waitForGateway } from "./gateway-readiness.js";
4
4
  import systemPaths from "./paths.js";
5
5
  import systemService from "./service/index.js";
6
6
  import nodeExecutable from "./node.js";
7
7
  import installProgram from "../install.js";
8
+ import phreshosHome from "../home.js";
8
9
  import { existsSync } from "node:fs";
9
- import { join } from "node:path";
10
+ import { mkdir, rm, writeFile } from "node:fs/promises";
11
+ import { homedir } from "node:os";
12
+ import { dirname, join } from "node:path";
10
13
  /** Coordinates acquisition, immutable files, and the native service as one transaction. */
11
14
  export default class SystemLifecycle {
12
15
  dependencies;
@@ -18,8 +21,8 @@ export default class SystemLifecycle {
18
21
  service,
19
22
  resolveRelease: dependencies?.resolveRelease ?? resolveSystemRelease,
20
23
  downloadRelease: dependencies?.downloadRelease ?? downloadSystemRelease,
21
- ready: dependencies?.ready ?? intakeReady,
22
- wait: dependencies?.wait ?? waitForIntake,
24
+ ready: dependencies?.ready ?? gatewayReady,
25
+ wait: dependencies?.wait ?? waitForGateway,
23
26
  provisionSetup: dependencies?.provisionSetup ?? provisionSetup
24
27
  };
25
28
  }
@@ -39,7 +42,7 @@ export default class SystemLifecycle {
39
42
  await service.register(definition(installation, executable));
40
43
  if ((await service.inspect()).automaticStartup)
41
44
  await service.enable();
42
- await service.start();
45
+ await this.launchService();
43
46
  await this.waitUntilReady();
44
47
  await activation.commit();
45
48
  }
@@ -49,7 +52,7 @@ export default class SystemLifecycle {
49
52
  return await this.restoredFailure(error, previous, previousService);
50
53
  }
51
54
  // The System transaction is complete before a Program crosses its
52
- // live intake. A provisioning failure therefore leaves a healthy
55
+ // live gateway. A provisioning failure therefore leaves a healthy
53
56
  // System available for a retry instead of rolling it back around a
54
57
  // separate Program installation that may already have succeeded.
55
58
  try {
@@ -80,7 +83,7 @@ export default class SystemLifecycle {
80
83
  }
81
84
  async startExclusive() {
82
85
  await this.requireInstalledService();
83
- await this.dependencies.service.start();
86
+ await this.launchService();
84
87
  await this.waitUntilReady();
85
88
  return await this.status();
86
89
  }
@@ -111,14 +114,14 @@ export default class SystemLifecycle {
111
114
  async status() {
112
115
  const { installation, service } = this.dependencies;
113
116
  const [installed, state] = await Promise.all([installation.current(), service.inspect()]);
114
- const ready = state.running && await this.dependencies.ready(installation.paths.intake);
117
+ const ready = state.running && await this.dependencies.ready(installation.paths.gateway);
115
118
  return {
116
119
  ...(installed ? { installed } : {}),
117
120
  desktop: "http://localhost:4300",
118
121
  ...state,
119
122
  ready,
120
123
  root: installation.paths.root,
121
- intake: installation.paths.intake,
124
+ gateway: installation.paths.gateway,
122
125
  log: installation.paths.log
123
126
  };
124
127
  }
@@ -135,7 +138,7 @@ export default class SystemLifecycle {
135
138
  async waitUntilReady() {
136
139
  const { installation, service } = this.dependencies;
137
140
  try {
138
- await this.dependencies.wait(installation.paths.intake, async () => (await service.inspect()).running);
141
+ await this.dependencies.wait(installation.paths.gateway, async () => (await service.inspect()).running);
139
142
  }
140
143
  catch (error) {
141
144
  const message = error instanceof Error ? error.message : String(error);
@@ -167,7 +170,7 @@ export default class SystemLifecycle {
167
170
  else
168
171
  await service.disable();
169
172
  if (state.running) {
170
- await service.start();
173
+ await this.launchService();
171
174
  await this.waitUntilReady();
172
175
  }
173
176
  }
@@ -180,6 +183,22 @@ export default class SystemLifecycle {
180
183
  }
181
184
  throw error;
182
185
  }
186
+ async launchService() {
187
+ const { installation, service } = this.dependencies;
188
+ const { homeRequest, transientHome } = installation.paths;
189
+ await rm(homeRequest, { force: true });
190
+ if (transientHome) {
191
+ await mkdir(dirname(homeRequest), { recursive: true });
192
+ await writeFile(homeRequest, transientHome, { mode: 0o600 });
193
+ }
194
+ try {
195
+ await service.start();
196
+ }
197
+ catch (error) {
198
+ await rm(homeRequest, { force: true });
199
+ throw error;
200
+ }
201
+ }
183
202
  }
184
203
  async function provisionSetup() {
185
204
  await installProgram({ name: "setup", run: true, startup: true, announce: false });
@@ -188,6 +207,12 @@ function definition(installation, executable) {
188
207
  return {
189
208
  executable,
190
209
  entry: join(installation.paths.current, "server", "main.js"),
210
+ arguments: [
211
+ "--default-home",
212
+ phreshosHome({}, homedir()),
213
+ "--home-request",
214
+ installation.paths.homeRequest
215
+ ],
191
216
  directory: installation.paths.current,
192
217
  output: installation.paths.log
193
218
  };
@@ -1,9 +1,10 @@
1
- import intakeAddress from "../intake-address.js";
1
+ import { gatewayPath } from "../gateway.js";
2
+ import phreshosHome from "../home.js";
2
3
  import { homedir } from "node:os";
3
4
  import { isAbsolute, join } from "node:path";
4
5
  /** The installation is separate from the persistent state it operates on. */
5
6
  export default function systemPaths(platform = process.platform, userHome = homedir(), variables = process.env) {
6
- const storage = join(userHome, ".phreshos");
7
+ const storage = phreshosHome(variables, userHome);
7
8
  const root = platform === "darwin"
8
9
  ? join(userHome, "Library", "Application Support", "PhreshOS", "System")
9
10
  : platform === "linux"
@@ -16,7 +17,9 @@ export default function systemPaths(platform = process.platform, userHome = home
16
17
  releases: join(root, "releases"),
17
18
  current: join(root, "current"),
18
19
  storage,
19
- intake: intakeAddress(storage, platform),
20
+ gateway: gatewayPath(storage, platform),
21
+ homeRequest: join(root, "next-home"),
22
+ ...(variables.PHRESHOS_HOME === undefined ? {} : { transientHome: storage }),
20
23
  log: join(storage, "service.log")
21
24
  };
22
25
  }
@@ -0,0 +1,37 @@
1
+ import phreshosHome from "../home.js";
2
+ import { existsSync, statSync } from "node:fs";
3
+ import { readFile } from "node:fs/promises";
4
+ import { join, resolve } from "node:path";
5
+ import { spawn } from "node:child_process";
6
+ /** Run one System source directly with an explicit absolute state home. */
7
+ export default async function runSystem(source, environment = process.env) {
8
+ const directory = resolve(source);
9
+ if (!existsSync(directory) || !statSync(directory).isDirectory())
10
+ throw new Error(`The System source does not exist: ${directory}`);
11
+ const entry = join(directory, "server", "main.js");
12
+ const command = existsSync(entry)
13
+ ? { file: process.execPath, args: [entry] }
14
+ : await projectCommand(directory);
15
+ const child = spawn(command.file, command.args, {
16
+ cwd: directory,
17
+ env: { ...environment, PHRESHOS_HOME: phreshosHome(environment) },
18
+ stdio: "inherit"
19
+ });
20
+ const result = await new Promise((resolve, reject) => {
21
+ child.once("error", reject);
22
+ child.once("exit", (code, signal) => resolve({ code, signal }));
23
+ });
24
+ if (result.signal)
25
+ throw new Error(`The System stopped on ${result.signal}`);
26
+ if (result.code !== 0)
27
+ throw new Error(`The System exited with code ${result.code ?? 1}`);
28
+ }
29
+ async function projectCommand(directory) {
30
+ const path = join(directory, "package.json");
31
+ if (!existsSync(path))
32
+ throw new Error(`The System source has neither server/main.js nor package.json: ${directory}`);
33
+ const manifest = JSON.parse(await readFile(path, "utf8"));
34
+ if (typeof manifest.scripts?.start !== "string")
35
+ throw new Error(`The System source has no start command: ${directory}`);
36
+ return { file: process.execPath, args: ["--run", "start"] };
37
+ }
@@ -43,7 +43,7 @@ export default class BackgroundSystemService {
43
43
  await mkdir(dirname(definition.output), { recursive: true });
44
44
  const output = await open(definition.output, "a", 0o600);
45
45
  try {
46
- const child = spawn(definition.executable, [definition.entry], {
46
+ const child = spawn(definition.executable, [definition.entry, ...definition.arguments], {
47
47
  cwd: definition.directory,
48
48
  detached: true,
49
49
  stdio: ["ignore", output.fd, output.fd]
@@ -135,5 +135,7 @@ function definition(value) {
135
135
  if (!value || typeof value !== "object")
136
136
  return false;
137
137
  const candidate = value;
138
- return ["executable", "entry", "directory", "output"].every(name => typeof candidate[name] === "string" && candidate[name] !== "");
138
+ return ["executable", "entry", "directory", "output"].every(name => typeof candidate[name] === "string" && candidate[name] !== "")
139
+ && Array.isArray(candidate.arguments)
140
+ && candidate.arguments.every(value => typeof value === "string");
139
141
  }
@@ -95,6 +95,7 @@ function plist(label, definition) {
95
95
  <array>
96
96
  <string>${xml(definition.executable)}</string>
97
97
  <string>${xml(definition.entry)}</string>
98
+ ${definition.arguments.map(argument => ` <string>${xml(argument)}</string>`).join("\n")}
98
99
  </array>
99
100
  <key>WorkingDirectory</key>
100
101
  <string>${xml(definition.directory)}</string>
@@ -80,7 +80,7 @@ Description=PhreshOS System
80
80
 
81
81
  [Service]
82
82
  Type=simple
83
- ExecStart=${quote(definition.executable)} ${quote(definition.entry)}
83
+ ExecStart=${[definition.executable, definition.entry, ...definition.arguments].map(quote).join(" ")}
84
84
  WorkingDirectory=${setting(definition.directory)}
85
85
  Restart=on-failure
86
86
  RestartSec=2
@@ -3,7 +3,7 @@ import { open, writeFile } from "node:fs/promises";
3
3
  const payload = decode(process.argv[2]);
4
4
  const output = await open(payload.definition.output, "a", 0o600);
5
5
  try {
6
- const child = spawn(payload.definition.executable, [payload.definition.entry], {
6
+ const child = spawn(payload.definition.executable, [payload.definition.entry, ...payload.definition.arguments], {
7
7
  cwd: payload.definition.directory,
8
8
  stdio: ["ignore", output.fd, output.fd]
9
9
  });
@@ -41,5 +41,7 @@ function definition(value) {
41
41
  if (!value || typeof value !== "object")
42
42
  return false;
43
43
  const candidate = value;
44
- return ["executable", "entry", "directory", "output"].every(name => typeof candidate[name] === "string" && candidate[name] !== "");
44
+ return ["executable", "entry", "directory", "output"].every(name => typeof candidate[name] === "string" && candidate[name] !== "")
45
+ && Array.isArray(candidate.arguments)
46
+ && candidate.arguments.every(value => typeof value === "string");
45
47
  }
@@ -9,6 +9,7 @@ dist
9
9
  # Made by `phresh pack` in the project root.
10
10
  /*.zip
11
11
  /*.zip.sha256
12
+ /program.json
12
13
 
13
14
  # Editor and machine residue.
14
15
  .vscode
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "phresh",
3
3
  "private": true,
4
- "version": "0.1.13",
4
+ "version": "0.1.15",
5
5
  "description": "The official PhreshOS starter Program.",
6
6
  "type": "module",
7
7
  "scripts": {
@@ -14,15 +14,15 @@
14
14
  "starter"
15
15
  ],
16
16
  "dependencies": {
17
- "@phreshos/client": "^0.1.13",
18
- "@phreshos/core": "^0.1.9",
17
+ "@phreshos/client": "^0.1.18",
18
+ "@phreshos/core": "^0.1.14",
19
19
  "@phreshos/react": "^0.1.7",
20
- "@phreshos/server": "^0.1.11",
20
+ "@phreshos/server": "^0.1.17",
21
21
  "react": "^19.2.8",
22
22
  "react-dom": "^19.2.8"
23
23
  },
24
24
  "devDependencies": {
25
- "@phreshos/cli": "^0.1.21",
25
+ "@phreshos/cli": "^0.1.23",
26
26
  "@types/node": "^26.2.0",
27
27
  "@types/react": "^19.2.18",
28
28
  "@types/react-dom": "^19.2.4",
@@ -4,8 +4,11 @@ export default defineConfig({
4
4
  identity: "phresh",
5
5
  name: "Phresh Program",
6
6
  description: "A minimal counter with direct Client and Server endpoints.",
7
- version: "0.1.13",
7
+ version: "0.1.15",
8
8
  icon: "icon.png",
9
+ categories: ["Development"],
10
+ keywords: ["example", "counter", "client", "server"],
11
+ website: "https://github.com/PhreshOS/phresh-program",
9
12
  buildCommand: "vite-node scripts/build.ts",
10
13
  server: {
11
14
  location: "dist/server",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "repository": "PhreshOS/phresh-program",
3
- "version": "0.1.13",
4
- "sha256": "c55ff8129295c9c90b7a78fef8689fef6e0d180c7a284fdb180127700b74fcb0",
3
+ "version": "0.1.15",
4
+ "sha256": "33e7b8119bc75d3bd8d7c3413749dfbd001bff3f6a0a6392d1498c9f38cf0683",
5
5
  "development": true
6
6
  }
package/dist/uninstall.js CHANGED
@@ -1,17 +1,19 @@
1
1
  import { readConfig } from "./project.js";
2
2
  import { dim, heading } from "./style.js";
3
- import speak from "./program-intake.js";
3
+ import { streamProgram } from "./gateway.js";
4
4
  import writeProgramCommandOutput from "./program-command-output.js";
5
- /** Uninstall the Program declared by the current project. */
6
- export default async function uninstall(everything = false, directory = process.cwd()) {
7
- const config = await readConfig(directory);
8
- await speak({ word: "uninstall", identity: config.identity, everything }, function (event) {
5
+ /** Uninstall an installed Program by name or by the current project's identity. */
6
+ export default async function uninstall(options = {}) {
7
+ const identity = options.name ?? (await readConfig(options.directory ?? process.cwd())).identity;
8
+ if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(identity))
9
+ throw new Error(`The Program name "${identity}" is invalid`);
10
+ await streamProgram({ word: "uninstall", identity, everything: options.everything === true }, function (event) {
9
11
  if (event.event === "output")
10
12
  return writeProgramCommandOutput(event);
11
13
  if (event.event !== "uninstalled")
12
14
  return;
13
- heading(config.name ?? config.identity, "uninstalled");
14
- console.log(everything
15
+ heading(identity, "uninstalled");
16
+ console.log(options.everything
15
17
  ? ` ${dim("Its processes, installed files, stored data, and runtime record were removed.")}\n`
16
18
  : ` ${dim("Its installed files were removed. Processes, stored data, and runtime state were kept.")}\n`);
17
19
  });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@phreshos/cli",
3
3
  "type": "module",
4
- "version": "0.1.21",
4
+ "version": "0.1.23",
5
5
  "description": "The Phresh command-line interface for Program projects and system management.",
6
6
  "engines": {
7
7
  "node": ">=20.10"
@@ -49,7 +49,7 @@
49
49
  "packageManager": "bun@1.3.14",
50
50
  "dependencies": {
51
51
  "@clack/prompts": "^1.7.0",
52
- "@phreshos/core": "^0.1.13",
52
+ "@phreshos/core": "^0.1.18",
53
53
  "adm-zip": "^0.6.0",
54
54
  "commander": "^15.0.0",
55
55
  "picocolors": "^1.1.1"
@@ -1,14 +0,0 @@
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,75 +0,0 @@
1
- import { connect } from "node:net";
2
- import { homedir } from "node:os";
3
- import { isAbsolute, join } from "node:path";
4
- import intakeAddress from "./intake-address.js";
5
- /**
6
- * The local Program intake, from the CLI's side.
7
- *
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.
13
- *
14
- * A message is a line. Closing our own side to mark the end of a
15
- * question would make lifetime ambiguous. A line delimiter keeps the
16
- * connection available for the stream of events that follows a launch.
17
- *
18
- * One question, then events until the system closes. Installation confirms
19
- * each requested outcome — laid out, startup enabled, running — and then
20
- * ends. Uninstalling says one thing and ends; an attached run says how it
21
- * began, whatever the Program says, and how it ended. None pretends to be a
22
- * remote method returning through an unrelated transport.
23
- */
24
- export function programIntakePath(environment = process.env, userHome = homedir()) {
25
- const instanceHome = environment.PHRESHOS_HOME;
26
- if (instanceHome === undefined)
27
- return intakeAddress(join(userHome, ".phreshos"));
28
- if (!isAbsolute(instanceHome))
29
- throw new Error("PHRESHOS_HOME must be an absolute filesystem path");
30
- return intakeAddress(instanceHome);
31
- }
32
- export const socketPath = programIntakePath();
33
- export default function speak(question, heard, path = socketPath, signal) {
34
- return new Promise(function (settle, refuse) {
35
- const socket = connect(path);
36
- let said = "";
37
- let failed = null;
38
- let aborted = false;
39
- const abort = () => {
40
- aborted = true;
41
- socket.destroy();
42
- };
43
- if (signal?.aborted)
44
- abort();
45
- else
46
- signal?.addEventListener("abort", abort, { once: true });
47
- socket.on("connect", () => socket.write(JSON.stringify(question) + "\n"));
48
- socket.on("data", function (chunk) {
49
- said += String(chunk);
50
- const lines = said.split("\n");
51
- said = lines.pop() ?? "";
52
- for (const line of lines)
53
- if (line.trim()) {
54
- const event = JSON.parse(line);
55
- if (event.event === "error")
56
- failed = new Error(String(event.message));
57
- else
58
- heard(event);
59
- }
60
- });
61
- // The system is not running, or is running as somebody else. Said
62
- // plainly, because "ENOENT" is not what went wrong from here.
63
- socket.on("error", () => { if (!aborted)
64
- refuse(new Error(`No system is listening at ${path} — start one, or check that it is yours`)); });
65
- socket.on("close", () => {
66
- signal?.removeEventListener("abort", abort);
67
- if (aborted)
68
- refuse(new Error("The attached launch was stopped"));
69
- else if (failed)
70
- refuse(failed);
71
- else
72
- settle();
73
- });
74
- });
75
- }
@@ -1,11 +0,0 @@
1
- {
2
- "schema": 1,
3
- "identity": "phresh",
4
- "version": "0.1.13",
5
- "name": "Phresh Program",
6
- "description": "A minimal Client and Server Program for exploring PhreshOS development.",
7
- "icon": "icon.png",
8
- "categories": ["Development"],
9
- "keywords": ["example", "counter", "client", "server"],
10
- "website": "https://github.com/PhreshOS/phresh-program"
11
- }