@phreshos/cli 0.1.22 → 0.1.24
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 +34 -14
- package/dist/attach.js +3 -3
- package/dist/cli.js +13 -4
- package/dist/command-contract.js +12 -0
- package/dist/control-client.js +7 -0
- package/dist/control-command.js +50 -0
- package/dist/describe-command.js +60 -0
- package/dist/gateway.js +96 -0
- package/dist/home.js +16 -0
- package/dist/install.js +1 -1
- package/dist/program-installation.js +2 -2
- package/dist/system/command.js +16 -1
- package/dist/system/{readiness.js → gateway-readiness.js} +7 -7
- package/dist/system/lifecycle.js +36 -11
- package/dist/system/paths.js +6 -3
- package/dist/system/run.js +37 -0
- package/dist/system/service/background.js +4 -2
- package/dist/system/service/macos.js +1 -0
- package/dist/system/service/systemd.js +1 -1
- package/dist/system/service/windows-runner.js +4 -2
- package/dist/template/package.json +6 -6
- package/dist/template/phresh.config.ts +1 -1
- package/dist/template.json +2 -2
- package/dist/uninstall.js +9 -7
- package/package.json +2 -2
- package/dist/intake-address.js +0 -14
- package/dist/program-intake.js +0 -75
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.
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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.
|
|
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/
|
|
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>/
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
405
|
-
|
|
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
|
|
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 =
|
|
14
|
+
export default async function attach(program, options = {}, watching = {}, path = gatewayPath(), signal) {
|
|
15
15
|
let ended = null;
|
|
16
|
-
await
|
|
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
|
|
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
|
-
"
|
|
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,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
|
+
}
|
package/dist/gateway.js
ADDED
|
@@ -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
|
|
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
|
|
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
|
|
9
|
+
await streamProgram({
|
|
10
10
|
word: "install",
|
|
11
11
|
program,
|
|
12
12
|
run: options.run === true,
|
package/dist/system/command.js
CHANGED
|
@@ -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")
|
|
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
|
|
3
|
-
return await new Promise(function (
|
|
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
|
-
|
|
15
|
+
resolve(ready);
|
|
16
16
|
}
|
|
17
17
|
});
|
|
18
18
|
}
|
|
19
|
-
export async function
|
|
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
|
|
22
|
+
if (await gatewayReady(path))
|
|
23
23
|
return;
|
|
24
24
|
if (!await running())
|
|
25
|
-
throw new Error("The PhreshOS System stopped before its
|
|
26
|
-
await new Promise(
|
|
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
|
}
|
package/dist/system/lifecycle.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import SystemInstallation from "./installation.js";
|
|
2
2
|
import { downloadSystemRelease, resolveSystemRelease } from "./release.js";
|
|
3
|
-
import {
|
|
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 {
|
|
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 ??
|
|
22
|
-
wait: dependencies?.wait ??
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
};
|
package/dist/system/paths.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import
|
|
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 =
|
|
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
|
-
|
|
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=${
|
|
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
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "phresh",
|
|
3
3
|
"private": true,
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.16",
|
|
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.
|
|
18
|
-
"@phreshos/core": "^0.1.
|
|
19
|
-
"@phreshos/react": "^0.1.
|
|
20
|
-
"@phreshos/server": "^0.1.
|
|
17
|
+
"@phreshos/client": "^0.1.21",
|
|
18
|
+
"@phreshos/core": "^0.1.19",
|
|
19
|
+
"@phreshos/react": "^0.1.9",
|
|
20
|
+
"@phreshos/server": "^0.1.19",
|
|
21
21
|
"react": "^19.2.8",
|
|
22
22
|
"react-dom": "^19.2.8"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
|
-
"@phreshos/cli": "^0.1.
|
|
25
|
+
"@phreshos/cli": "^0.1.24",
|
|
26
26
|
"@types/node": "^26.2.0",
|
|
27
27
|
"@types/react": "^19.2.18",
|
|
28
28
|
"@types/react-dom": "^19.2.4",
|
|
@@ -4,7 +4,7 @@ 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.
|
|
7
|
+
version: "0.1.16",
|
|
8
8
|
icon: "icon.png",
|
|
9
9
|
categories: ["Development"],
|
|
10
10
|
keywords: ["example", "counter", "client", "server"],
|
package/dist/template.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"repository": "PhreshOS/phresh-program",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"sha256": "
|
|
3
|
+
"version": "0.1.16",
|
|
4
|
+
"sha256": "b50f38818b69ca4b818ed8a8845cab3a9d35baf653bc122af0a4b39ed05e6ef7",
|
|
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
|
|
3
|
+
import { streamProgram } from "./gateway.js";
|
|
4
4
|
import writeProgramCommandOutput from "./program-command-output.js";
|
|
5
|
-
/** Uninstall
|
|
6
|
-
export default async function uninstall(
|
|
7
|
-
const
|
|
8
|
-
|
|
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(
|
|
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.
|
|
4
|
+
"version": "0.1.24",
|
|
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.
|
|
52
|
+
"@phreshos/core": "^0.1.19",
|
|
53
53
|
"adm-zip": "^0.6.0",
|
|
54
54
|
"commander": "^15.0.0",
|
|
55
55
|
"picocolors": "^1.1.1"
|
package/dist/intake-address.js
DELETED
|
@@ -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
|
-
}
|
package/dist/program-intake.js
DELETED
|
@@ -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
|
-
}
|