@phreshos/cli 0.1.31 → 0.1.32
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 +3 -2
- package/dist/control-client.js +9 -3
- package/dist/gateway.js +4 -95
- package/dist/install.js +4 -9
- package/dist/launch.js +55 -96
- package/dist/pack.js +9 -105
- package/dist/program-installation.js +18 -17
- package/dist/project.js +7 -132
- package/dist/template/package.json +1 -1
- package/dist/uninstall.js +17 -11
- package/package.json +3 -2
- package/dist/attach.js +0 -29
- package/dist/build-command.js +0 -22
- package/dist/client-development.js +0 -161
- package/dist/command-environment.js +0 -10
- package/dist/derive.js +0 -98
package/README.md
CHANGED
|
@@ -10,8 +10,9 @@ active testing. The architecture's components will be released in stages as
|
|
|
10
10
|
their contracts and integrations are verified.
|
|
11
11
|
|
|
12
12
|
`@phreshos/cli` is not intended to be used independently of that architecture.
|
|
13
|
-
Its Program commands
|
|
14
|
-
|
|
13
|
+
Its Program commands use `@phreshos/node`, whose `Project` model owns local
|
|
14
|
+
project behavior and whose `system` value exposes the same Core contract used
|
|
15
|
+
inside Server Programs. Runtime operations require a compatible System.
|
|
15
16
|
|
|
16
17
|
```bash
|
|
17
18
|
phresh create app # create a complete new Program
|
package/dist/control-client.js
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Gateway } from "@phreshos/node";
|
|
2
2
|
/** Owner-local client for the authoritative running-System contract. */
|
|
3
3
|
export default class LocalSystemControl {
|
|
4
|
-
execute(request, signal) {
|
|
5
|
-
|
|
4
|
+
async execute(request, signal) {
|
|
5
|
+
const gateway = await Gateway.open();
|
|
6
|
+
try {
|
|
7
|
+
return await gateway.execute(request, signal);
|
|
8
|
+
}
|
|
9
|
+
finally {
|
|
10
|
+
await gateway.close();
|
|
11
|
+
}
|
|
6
12
|
}
|
|
7
13
|
}
|
package/dist/gateway.js
CHANGED
|
@@ -1,96 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
});
|
|
1
|
+
import { gatewayAddress, resolveHome } from "@phreshos/node";
|
|
2
|
+
/** Resolve the Gateway address while preserving the CLI's public helper. */
|
|
3
|
+
export function gatewayPath(home = resolveHome(), platform = process.platform) {
|
|
4
|
+
return gatewayAddress(home, platform);
|
|
96
5
|
}
|
package/dist/install.js
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { readConfig } from "./project.js";
|
|
1
|
+
import { Project } from "@phreshos/node";
|
|
3
2
|
import { dim, heading, line } from "./style.js";
|
|
4
3
|
import installProgram, {} from "./program-installation.js";
|
|
5
4
|
import { prepareOfficialProgram } from "./program-release.js";
|
|
6
|
-
import build from "./build-command.js";
|
|
7
5
|
/**
|
|
8
6
|
* Lay this program out on this machine's system.
|
|
9
7
|
*
|
|
@@ -26,7 +24,9 @@ export default async function install(options = {}) {
|
|
|
26
24
|
const directory = options.directory ?? process.cwd();
|
|
27
25
|
const prepared = options.name ? await prepareOfficialProgram(options.name) : null;
|
|
28
26
|
try {
|
|
29
|
-
const program = prepared
|
|
27
|
+
const program = prepared ? prepared.program : await Project.open(directory);
|
|
28
|
+
if (program instanceof Project && program.config.buildCommand)
|
|
29
|
+
line("build", program.config.buildCommand);
|
|
30
30
|
const result = await installProgram(program, options);
|
|
31
31
|
if (options.announce !== false) {
|
|
32
32
|
const { name, identity, version } = result.program;
|
|
@@ -44,8 +44,3 @@ export default async function install(options = {}) {
|
|
|
44
44
|
await prepared?.dispose();
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
|
-
async function localProgram(directory) {
|
|
48
|
-
const config = await readConfig(directory);
|
|
49
|
-
await build(config, directory);
|
|
50
|
-
return derive(config, directory, "production");
|
|
51
|
-
}
|
package/dist/launch.js
CHANGED
|
@@ -1,111 +1,70 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { readConfig } from "./project.js";
|
|
3
|
-
import { dim, heading, line } from "./style.js";
|
|
1
|
+
import { Gateway, Project } from "@phreshos/node";
|
|
4
2
|
import { relative } from "node:path";
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
* build left, or where the source is.
|
|
14
|
-
*
|
|
15
|
-
* What the derivation produced is shown before it is sent, because it is
|
|
16
|
-
* worth seeing rather than inferring from what happens next. Shown, not
|
|
17
|
-
* dumped: the whole program.json is a wall of braces an author already
|
|
18
|
-
* knows, and what they cannot know at a glance is which of their two
|
|
19
|
-
* answers each half took. So each half says where it came from, and a
|
|
20
|
-
* path is shortened back to the form they typed — the absolute one is
|
|
21
|
-
* the machine's business, and they are standing in the directory it is
|
|
22
|
-
* relative to.
|
|
23
|
-
*
|
|
24
|
-
* Nothing is installed. This local project is authoritative for attached
|
|
25
|
-
* use of its declared identity: the system ends and forgets any runtime
|
|
26
|
-
* Program already there, then registers this run as the sole uninstalled
|
|
27
|
-
* occupant. Installed files and storage are not removed. Its root process
|
|
28
|
-
* tethers the whole replacement to this command: when the process ends, the
|
|
29
|
-
* registry record and any remaining processes go too.
|
|
30
|
-
* **Attached means not installed; installed means persistent** — a
|
|
31
|
-
* program meant to outlive a terminal is installed rather than run.
|
|
32
|
-
*
|
|
33
|
-
* A client development command belongs to this local authoring session,
|
|
34
|
-
* never to the Program sent to the system. The declared client location
|
|
35
|
-
* has to become reachable before that Program is launched, so the first
|
|
36
|
-
* window does not open onto a destination already known to be absent.
|
|
37
|
-
*
|
|
38
|
-
* Its output arrives here because it has somewhere to arrive: the system
|
|
39
|
-
* pipes a launched program only when someone is listening, and this is
|
|
40
|
-
* the someone.
|
|
41
|
-
*/
|
|
42
|
-
export default async function launch(which, directory = process.cwd(), options = {}) {
|
|
43
|
-
const config = await readConfig(directory);
|
|
44
|
-
if (which === "production")
|
|
45
|
-
await build(config, directory);
|
|
46
|
-
const program = derive(config, directory, which);
|
|
47
|
-
const clientDevelopmentConfig = which === "development" && program.client && (program.client.start ?? true)
|
|
48
|
-
? config.client?.development
|
|
49
|
-
: undefined;
|
|
50
|
-
const clientStartCommand = clientDevelopmentConfig?.startCommand;
|
|
51
|
-
heading(`${program.name ?? program.identity}${program.version ? ` ${program.version}` : ""}`, which);
|
|
3
|
+
import { dim, heading, line } from "./style.js";
|
|
4
|
+
/** Run the current project attached to the System and present its event stream. */
|
|
5
|
+
export default async function launch(mode, directory = process.cwd(), options = {}) {
|
|
6
|
+
const project = await Project.open(directory);
|
|
7
|
+
const program = project.description(mode);
|
|
8
|
+
heading(`${program.name ?? program.identity}${program.version ? ` ${program.version}` : ""}`, mode);
|
|
9
|
+
if (mode === "production" && project.config.buildCommand)
|
|
10
|
+
line("build", project.config.buildCommand);
|
|
52
11
|
if (program.server)
|
|
53
|
-
line(program.server.startCommand ? "server" : "server worker", String(program.server.startCommand ?? program.server.entryFile), place(directory, program.server.location));
|
|
12
|
+
line(program.server.startCommand ? "server" : "server worker", String(program.server.startCommand ?? program.server.entryFile), place(project.directory, program.server.location));
|
|
54
13
|
if (program.client)
|
|
55
|
-
line("client",
|
|
56
|
-
line("storage", place(directory, String(program.storage)));
|
|
14
|
+
line("client", project.config.client?.development?.startCommand ?? place(project.directory, program.client.location));
|
|
15
|
+
line("storage", place(project.directory, String(program.storage)));
|
|
16
|
+
if (Object.keys(options).length)
|
|
17
|
+
line("options", Object.entries(options).map(([name, value]) => `${name}=${value}`).join(" "));
|
|
57
18
|
console.log("");
|
|
58
|
-
|
|
59
|
-
await assertClientDevelopmentUrlFree(program.client.location);
|
|
60
|
-
const clientDevelopment = clientDevelopmentConfig
|
|
61
|
-
? startClientDevelopment(clientStartCommand, directory)
|
|
62
|
-
: null;
|
|
19
|
+
const gateway = await Gateway.open();
|
|
63
20
|
const controller = new AbortController();
|
|
64
|
-
let
|
|
65
|
-
|
|
66
|
-
|
|
21
|
+
let interrupted = false;
|
|
22
|
+
let ended = null;
|
|
23
|
+
const stop = () => {
|
|
24
|
+
if (interrupted)
|
|
67
25
|
return;
|
|
68
|
-
|
|
69
|
-
controller.abort();
|
|
70
|
-
void (clientDevelopment?.stop() ?? Promise.resolve()).finally(() => process.exit(130));
|
|
26
|
+
interrupted = true;
|
|
27
|
+
controller.abort(new Error("The local Program run was interrupted"));
|
|
71
28
|
};
|
|
72
|
-
// A signal ends this command, and ending this command closes the
|
|
73
|
-
// socket, and closing the socket stops the program. A client development
|
|
74
|
-
// command is another child of the same session, so it is ended first.
|
|
75
29
|
for (const signal of ["SIGINT", "SIGTERM"])
|
|
76
|
-
process.on(signal,
|
|
77
|
-
let status = 0;
|
|
30
|
+
process.on(signal, stop);
|
|
78
31
|
try {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
attachment,
|
|
89
|
-
clientDevelopment.exited.then(exit => {
|
|
90
|
-
if (clientDevelopment.stopping)
|
|
91
|
-
return new Promise(() => undefined);
|
|
92
|
-
controller.abort();
|
|
93
|
-
throw commandFailure(exit);
|
|
94
|
-
})
|
|
95
|
-
]) : await attachment;
|
|
96
|
-
console.log(`\n ${dim(ended.signal ? `ended on ${ended.signal}` : `ended with ${ended.code ?? 0}`)}\n`);
|
|
97
|
-
status = ended.signal ? 128 : ended.code ?? 0;
|
|
32
|
+
const events = mode === "development"
|
|
33
|
+
? gateway.dev(project, { options, signal: controller.signal })
|
|
34
|
+
: gateway.start(project, { options, signal: controller.signal });
|
|
35
|
+
for await (const event of events)
|
|
36
|
+
ended = present(event) ?? ended;
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
if (!interrupted)
|
|
40
|
+
throw error;
|
|
98
41
|
}
|
|
99
42
|
finally {
|
|
100
43
|
for (const signal of ["SIGINT", "SIGTERM"])
|
|
101
|
-
process.off(signal,
|
|
102
|
-
await
|
|
44
|
+
process.off(signal, stop);
|
|
45
|
+
await gateway.close();
|
|
103
46
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
47
|
+
if (interrupted)
|
|
48
|
+
process.exit(130);
|
|
49
|
+
if (!ended)
|
|
50
|
+
throw new Error("The System closed before the Program ended");
|
|
51
|
+
console.log(`\n ${dim(ended.signal ? `ended on ${ended.signal}` : `ended with ${ended.code ?? 0}`)}\n`);
|
|
52
|
+
process.exit(ended.signal ? 128 : ended.code ?? 0);
|
|
53
|
+
}
|
|
54
|
+
function present(event) {
|
|
55
|
+
if (event.event === "started")
|
|
56
|
+
console.log(` ${dim("running as")} ${String(event.process)}\n`);
|
|
57
|
+
else if (event.event === "waiting")
|
|
58
|
+
line("waiting for", String(event.url));
|
|
59
|
+
else if (event.event === "output")
|
|
60
|
+
(event.stream === "err" ? process.stderr : process.stdout).write(String(event.text));
|
|
61
|
+
else if (event.event === "exited")
|
|
62
|
+
return {
|
|
63
|
+
code: typeof event.code === "number" ? event.code : null,
|
|
64
|
+
signal: typeof event.signal === "string" ? event.signal : null
|
|
65
|
+
};
|
|
66
|
+
return null;
|
|
107
67
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
return /^https?:\/\//i.test(where) ? where : `./${relative(directory, where)}`;
|
|
68
|
+
function place(directory, location) {
|
|
69
|
+
return /^https?:\/\//i.test(location) ? location : `./${relative(directory, location)}`;
|
|
111
70
|
}
|
package/dist/pack.js
CHANGED
|
@@ -1,107 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
import { resolve } from "node:path";
|
|
5
|
-
import AdmZip from "adm-zip";
|
|
6
|
-
import build from "./build-command.js";
|
|
7
|
-
/**
|
|
8
|
-
* Package what is at each half's location.
|
|
9
|
-
*
|
|
10
|
-
* When the author config declares `buildCommand`, it completes before
|
|
11
|
-
* any files are gathered. Pack remains an assembler rather than a
|
|
12
|
-
* build tool: it invokes the author's command, then packages its result.
|
|
13
|
-
*
|
|
14
|
-
* The authoring tree and the installable form differ. A program may
|
|
15
|
-
* leave its halves anywhere; the package always has them in the same
|
|
16
|
-
* places, so the artifact's shape is the contract's rather than the
|
|
17
|
-
* author's. The program.json names those canonical places explicitly:
|
|
18
|
-
* a declared half always has a location, even when the system chose it.
|
|
19
|
-
*
|
|
20
|
-
* There is **no wrapping directory**. `program.json`, `server/`,
|
|
21
|
-
* `client/`, optional `icon.png`, and optional `agent.md` sit at the package's
|
|
22
|
-
* root, and the directory
|
|
23
|
-
* the system installs into is named from the program's own `identity`.
|
|
24
|
-
* The archive itself therefore needs no second naming layer.
|
|
25
|
-
*
|
|
26
|
-
* The files go into the archive directly rather than through a staging
|
|
27
|
-
* directory: a copy of a build is a thing that can be stale. The generated
|
|
28
|
-
* standalone program.json is also placed beside the archive for release
|
|
29
|
-
* discovery; both declarations are made from the same bytes.
|
|
30
|
-
*/
|
|
1
|
+
import { Project } from "@phreshos/node";
|
|
2
|
+
import { line } from "./style.js";
|
|
3
|
+
/** Package the current Project and present only its command-facing progress. */
|
|
31
4
|
export default async function pack(directory = process.cwd()) {
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
39
|
-
const zip = new AdmZip();
|
|
40
|
-
if (config.server)
|
|
41
|
-
place(zip, directory, config.server.location, "server");
|
|
42
|
-
if (config.client) {
|
|
43
|
-
place(zip, directory, config.client.location, "client");
|
|
44
|
-
if (!zip.getEntry("client/index.html"))
|
|
45
|
-
throw new Error(`The client files have no index.html — ${config.client.location} is not where a client half is`);
|
|
46
|
-
}
|
|
47
|
-
if (config.icon)
|
|
48
|
-
file(zip, directory, config.icon, "icon.png", "Program icon");
|
|
49
|
-
if (config.agent)
|
|
50
|
-
file(zip, directory, config.agent, "agent.md", "Program agent documentation");
|
|
51
|
-
const declaration = Buffer.from(JSON.stringify(program(config, version), null, 4) + "\n");
|
|
52
|
-
zip.addFile("program.json", declaration);
|
|
53
|
-
// The standalone release declaration and the declaration inside the
|
|
54
|
-
// archive are the same bytes. There is no separately authored catalog
|
|
55
|
-
// description that can drift from the installable Program.
|
|
56
|
-
writeFileSync(resolve(directory, "program.json"), declaration);
|
|
57
|
-
const archive = `${config.identity}@${version ?? "0.0.0"}.zip`;
|
|
58
|
-
const bytes = zip.toBuffer();
|
|
59
|
-
writeFileSync(resolve(directory, archive), bytes);
|
|
60
|
-
const digest = createHash("sha256").update(bytes).digest("hex");
|
|
61
|
-
writeFileSync(resolve(directory, `${archive}.sha256`), `${digest} ${archive}\n`);
|
|
62
|
-
console.log(`\nPacked ${archive}`);
|
|
63
|
-
return archive;
|
|
64
|
-
}
|
|
65
|
-
// The derivation, and the whole of it. What the config says about the
|
|
66
|
-
// program passes through; what it says about the build does not. Half
|
|
67
|
-
// locations are rewritten to the canonical places inside the package.
|
|
68
|
-
function program(config, version) {
|
|
69
|
-
return {
|
|
70
|
-
identity: config.identity,
|
|
71
|
-
name: config.name,
|
|
72
|
-
version,
|
|
73
|
-
description: config.description,
|
|
74
|
-
icon: config.icon ? "icon.png" : undefined,
|
|
75
|
-
agent: config.agent ? "agent.md" : undefined,
|
|
76
|
-
categories: config.categories,
|
|
77
|
-
keywords: config.keywords,
|
|
78
|
-
website: config.website,
|
|
79
|
-
...config.server && { server: {
|
|
80
|
-
location: "server",
|
|
81
|
-
start: config.server.start,
|
|
82
|
-
installCommand: config.server.installCommand,
|
|
83
|
-
uninstallCommand: config.server.uninstallCommand,
|
|
84
|
-
...serverExecution(config.server)
|
|
85
|
-
} },
|
|
86
|
-
...config.client && { client: { location: "client", start: config.client.start, title: config.client.title, size: config.client.size, position: config.client.position, layer: config.client.layer, minimize: config.client.minimize } }
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
function serverExecution(server) {
|
|
90
|
-
return server.startCommand !== undefined
|
|
91
|
-
? { startCommand: server.startCommand }
|
|
92
|
-
: { entryFile: server.entryFile };
|
|
93
|
-
}
|
|
94
|
-
// What is at a half's location, where the package keeps it. A half that
|
|
95
|
-
// was declared and is not there is the answer to whether it was built.
|
|
96
|
-
function place(zip, directory, location, half) {
|
|
97
|
-
const from = resolve(directory, location);
|
|
98
|
-
if (!existsSync(from))
|
|
99
|
-
throw new Error(`The ${half} files are not at ${location} — nothing was built there`);
|
|
100
|
-
zip.addLocalFolder(from, half);
|
|
101
|
-
}
|
|
102
|
-
function file(zip, directory, location, target, label) {
|
|
103
|
-
const from = resolve(directory, location);
|
|
104
|
-
if (!existsSync(from) || !statSync(from).isFile())
|
|
105
|
-
throw new Error(`The ${label} is not at ${location}`);
|
|
106
|
-
zip.addFile(target, readFileSync(from));
|
|
5
|
+
const project = await Project.open(directory);
|
|
6
|
+
if (project.config.buildCommand)
|
|
7
|
+
line("build", project.config.buildCommand);
|
|
8
|
+
const packed = await project.pack();
|
|
9
|
+
console.log(`\nPacked ${packed.archive}`);
|
|
10
|
+
return packed.archive;
|
|
107
11
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Gateway } from "@phreshos/node";
|
|
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,23 +6,24 @@ export default async function installProgram(program, options = {}) {
|
|
|
6
6
|
let replaced = false;
|
|
7
7
|
let startupEnabled = false;
|
|
8
8
|
let processValue;
|
|
9
|
-
await
|
|
10
|
-
|
|
11
|
-
program,
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
9
|
+
const gateway = await Gateway.open();
|
|
10
|
+
try {
|
|
11
|
+
for await (const event of gateway.install(program, options)) {
|
|
12
|
+
if (event.event === "output")
|
|
13
|
+
writeProgramCommandOutput(event);
|
|
14
|
+
else if (event.event === "installed") {
|
|
15
|
+
installedValue = event.program;
|
|
16
|
+
replaced = event.replaced === true;
|
|
17
|
+
}
|
|
18
|
+
else if (event.event === "startupEnabled")
|
|
19
|
+
startupEnabled = true;
|
|
20
|
+
else if (event.event === "running")
|
|
21
|
+
processValue = event.process;
|
|
20
22
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
});
|
|
23
|
+
}
|
|
24
|
+
finally {
|
|
25
|
+
await gateway.close();
|
|
26
|
+
}
|
|
26
27
|
if (installedValue === undefined)
|
|
27
28
|
throw new Error("The System ended Program installation without confirming it");
|
|
28
29
|
const installed = installedProgram(installedValue);
|
package/dist/project.js
CHANGED
|
@@ -1,126 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Project } from "@phreshos/node";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
3
4
|
import { isAbsolute, normalize, resolve, sep } from "node:path";
|
|
4
|
-
import { createJiti } from "jiti";
|
|
5
5
|
export const configFile = "phresh.config.ts";
|
|
6
|
+
/** Read the current Project through the reusable Gateway project model. */
|
|
6
7
|
export async function readConfig(directory = process.cwd()) {
|
|
7
|
-
|
|
8
|
-
if (!existsSync(path))
|
|
9
|
-
throw new Error(`There is no ${configFile} here — run: phresh init`);
|
|
10
|
-
// Program declarations remain typed authoring files independently of the
|
|
11
|
-
// Node version that happens to execute the CLI.
|
|
12
|
-
const config = await createJiti(import.meta.url).import(path, { default: true }).catch(function (error) {
|
|
13
|
-
throw new Error(`${configFile} could not be read (${error.message})`);
|
|
14
|
-
});
|
|
15
|
-
if (!config)
|
|
16
|
-
throw new Error(`${configFile} must export its config as the default export`);
|
|
17
|
-
coherent(config);
|
|
18
|
-
return config;
|
|
19
|
-
}
|
|
20
|
-
// Whether the words agree with each other. Nothing here touches a disk
|
|
21
|
-
// or asks whether a build ran: it is the same question the system asks
|
|
22
|
-
// of a program.json when it is constructed, asked earlier, where the
|
|
23
|
-
// person who mistyped it is still standing.
|
|
24
|
-
function coherent(config) {
|
|
25
|
-
// A name is the name of a directory before it is anything else, and
|
|
26
|
-
// the system checks it again where it makes one. Checked here too so
|
|
27
|
-
// the mistake is found by the CLI rather than at the runtime border.
|
|
28
|
-
if (typeof config.identity !== "string" || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(config.identity))
|
|
29
|
-
throw new Error("A program's identity is kebab-case, because it is also the name of its directory");
|
|
30
|
-
if (!config.server && !config.client)
|
|
31
|
-
throw new Error("A program must have a server half, a client half, or both");
|
|
32
|
-
for (const field of ["name", "version", "description", "icon", "agent", "website"]) {
|
|
33
|
-
if (config[field] !== undefined && typeof config[field] !== "string")
|
|
34
|
-
throw new Error(`A program's ${field} must be text`);
|
|
35
|
-
}
|
|
36
|
-
if (config.agent !== undefined && config.agent.trim().length === 0)
|
|
37
|
-
throw new Error("A program's agent documentation must be a non-empty path");
|
|
38
|
-
if (config.website !== undefined) {
|
|
39
|
-
try {
|
|
40
|
-
new URL(config.website);
|
|
41
|
-
}
|
|
42
|
-
catch {
|
|
43
|
-
throw new Error("A program's website must be a valid URL");
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
for (const [field, maximum] of [["categories", 20], ["keywords", 50]]) {
|
|
47
|
-
const values = config[field];
|
|
48
|
-
if (values === undefined)
|
|
49
|
-
continue;
|
|
50
|
-
if (!Array.isArray(values) || values.length > maximum || values.some(value => typeof value !== "string" || value.trim().length === 0 || value.trim().length > 50)) {
|
|
51
|
-
throw new Error(`A program's ${field} must contain at most ${maximum} non-empty values of at most 50 characters`);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
if (config.buildCommand !== undefined && (typeof config.buildCommand !== "string" || config.buildCommand.trim().length === 0))
|
|
55
|
-
throw new Error("A program's buildCommand must be non-empty text");
|
|
56
|
-
for (const half of ["server", "client"]) {
|
|
57
|
-
const declared = config[half];
|
|
58
|
-
if (declared === undefined)
|
|
59
|
-
continue;
|
|
60
|
-
if (typeof declared !== "object" || declared === null || Array.isArray(declared))
|
|
61
|
-
throw new Error(`A program's ${half} half must be a declaration`);
|
|
62
|
-
if (typeof declared.location !== "string")
|
|
63
|
-
throw new Error(`A declared ${half} half must have a location`);
|
|
64
|
-
if (declared.start !== undefined && typeof declared.start !== "boolean")
|
|
65
|
-
throw new Error(`A declared ${half} endpoint's start default must be true or false`);
|
|
66
|
-
}
|
|
67
|
-
if (!(config.server && (config.server.start ?? true)) && !(config.client && (config.client.start ?? true)))
|
|
68
|
-
throw new Error("A Program's default Process must start a server endpoint, a client endpoint, or both");
|
|
69
|
-
if (config.server)
|
|
70
|
-
execution(config.server, "A server half");
|
|
71
|
-
if (config.server?.installCommand !== undefined && typeof config.server.installCommand !== "string")
|
|
72
|
-
throw new Error("A server half's install command must be text");
|
|
73
|
-
if (config.server?.uninstallCommand !== undefined && typeof config.server.uninstallCommand !== "string")
|
|
74
|
-
throw new Error("A server half's uninstall command must be text");
|
|
75
|
-
if (config.client?.title !== undefined && typeof config.client.title !== "string")
|
|
76
|
-
throw new Error("A client half's title must be text");
|
|
77
|
-
if (config.client?.minimize !== undefined && typeof config.client.minimize !== "boolean")
|
|
78
|
-
throw new Error("A client half's minimize default must be true or false");
|
|
79
|
-
if (config.client && /^https?:\/\//i.test(config.client.location)) {
|
|
80
|
-
try {
|
|
81
|
-
new URL(config.client.location);
|
|
82
|
-
}
|
|
83
|
-
catch {
|
|
84
|
-
throw new Error("A client half's URL must be a valid HTTP or HTTPS URL");
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
// Said wrong is caught here, where the author is standing, rather
|
|
88
|
-
// than becoming a window in no layer at all.
|
|
89
|
-
if (config.client?.layer !== undefined && !layers.includes(config.client.layer))
|
|
90
|
-
throw new Error(`A client half's layer is one of ${layers.join(", ")} — not "${String(config.client.layer)}"`);
|
|
91
|
-
const serverDevelopment = config.server?.development;
|
|
92
|
-
if (serverDevelopment !== undefined && (typeof serverDevelopment !== "object" || serverDevelopment === null || Array.isArray(serverDevelopment)))
|
|
93
|
-
throw new Error("server.development must be a declaration");
|
|
94
|
-
if (serverDevelopment !== undefined)
|
|
95
|
-
execution(serverDevelopment, "server.development");
|
|
96
|
-
const clientDevelopment = config.client?.development;
|
|
97
|
-
if (clientDevelopment !== undefined && (typeof clientDevelopment !== "object" || clientDevelopment === null || Array.isArray(clientDevelopment)))
|
|
98
|
-
throw new Error("client.development must be a declaration");
|
|
99
|
-
if (clientDevelopment !== undefined && !httpUrl(clientDevelopment.url))
|
|
100
|
-
throw new Error("client.development.url must be a valid HTTP or HTTPS URL");
|
|
101
|
-
if (clientDevelopment?.startCommand !== undefined && (typeof clientDevelopment.startCommand !== "string" || clientDevelopment.startCommand.trim().length === 0))
|
|
102
|
-
throw new Error("client.development.startCommand must be non-empty text");
|
|
103
|
-
for (const [what, value] of [["size", config.client?.size], ["position", config.client?.position]]) {
|
|
104
|
-
if (value === undefined)
|
|
105
|
-
continue;
|
|
106
|
-
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
107
|
-
throw new Error(`A window's ${what} must name both of its values`);
|
|
108
|
-
const pair = what === "size" ? [value.width, value.height] : [value.x, value.y];
|
|
109
|
-
if (!pair.every(isRelativeValue))
|
|
110
|
-
throw new Error(`A window's ${what} is a finite pixel number or a relative expression such as "50% + 10"`);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
function execution(value, owner) {
|
|
114
|
-
const command = typeof value.startCommand === "string" && value.startCommand.trim().length > 0;
|
|
115
|
-
const entry = typeof value.entryFile === "string" && value.entryFile.trim().length > 0;
|
|
116
|
-
if (value.startCommand !== undefined && !command)
|
|
117
|
-
throw new Error(`${owner}'s startCommand must be non-empty text`);
|
|
118
|
-
if (value.entryFile !== undefined && !entry)
|
|
119
|
-
throw new Error(`${owner}'s entryFile must be a non-empty path`);
|
|
120
|
-
if (command === entry)
|
|
121
|
-
throw new Error(`${owner} must declare exactly one non-empty startCommand or entryFile`);
|
|
122
|
-
if (entry && !containedServerEntry(value.entryFile))
|
|
123
|
-
throw new Error(`${owner}'s entryFile must remain inside its Server directory`);
|
|
8
|
+
return (await Project.open(directory)).config;
|
|
124
9
|
}
|
|
125
10
|
export function containedServerEntry(entry) {
|
|
126
11
|
if (isAbsolute(entry))
|
|
@@ -128,23 +13,13 @@ export function containedServerEntry(entry) {
|
|
|
128
13
|
const path = normalize(entry);
|
|
129
14
|
return path !== ".." && !path.startsWith(`..${sep}`);
|
|
130
15
|
}
|
|
131
|
-
function httpUrl(value) {
|
|
132
|
-
if (typeof value !== "string" || value.trim().length === 0)
|
|
133
|
-
return false;
|
|
134
|
-
try {
|
|
135
|
-
const url = new URL(value);
|
|
136
|
-
return url.protocol === "http:" || url.protocol === "https:";
|
|
137
|
-
}
|
|
138
|
-
catch {
|
|
139
|
-
return false;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
16
|
export async function readManifest(directory = process.cwd()) {
|
|
143
17
|
const path = resolve(directory, "package.json");
|
|
144
18
|
if (!existsSync(path))
|
|
145
19
|
throw new Error("There is no package.json here");
|
|
146
|
-
const manifest = JSON.parse(await
|
|
147
|
-
if (typeof manifest.name !== "string" || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(manifest.name))
|
|
20
|
+
const manifest = JSON.parse(await readFile(path, "utf8"));
|
|
21
|
+
if (typeof manifest.name !== "string" || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(manifest.name)) {
|
|
148
22
|
throw new Error("package.json must have a kebab-case name");
|
|
23
|
+
}
|
|
149
24
|
return manifest;
|
|
150
25
|
}
|
package/dist/uninstall.js
CHANGED
|
@@ -1,20 +1,26 @@
|
|
|
1
1
|
import { readConfig } from "./project.js";
|
|
2
2
|
import { dim, heading } from "./style.js";
|
|
3
|
-
import {
|
|
3
|
+
import { Gateway } from "@phreshos/node";
|
|
4
4
|
import writeProgramCommandOutput from "./program-command-output.js";
|
|
5
5
|
/** Uninstall an installed Program by name or by the current project's identity. */
|
|
6
6
|
export default async function uninstall(options = {}) {
|
|
7
7
|
const identity = options.name ?? (await readConfig(options.directory ?? process.cwd())).identity;
|
|
8
8
|
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(identity))
|
|
9
9
|
throw new Error(`The Program name "${identity}" is invalid`);
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
10
|
+
const gateway = await Gateway.open();
|
|
11
|
+
try {
|
|
12
|
+
for await (const event of gateway.uninstall(identity, { everything: options.everything === true })) {
|
|
13
|
+
if (event.event === "output")
|
|
14
|
+
return writeProgramCommandOutput(event);
|
|
15
|
+
if (event.event !== "uninstalled")
|
|
16
|
+
return;
|
|
17
|
+
heading(identity, "uninstalled");
|
|
18
|
+
console.log(options.everything
|
|
19
|
+
? ` ${dim("Its processes, installed files, stored data, and runtime record were removed.")}\n`
|
|
20
|
+
: ` ${dim("Its installed files were removed. Processes, stored data, and runtime state were kept.")}\n`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
finally {
|
|
24
|
+
await gateway.close();
|
|
25
|
+
}
|
|
20
26
|
}
|
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.32",
|
|
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,8 @@
|
|
|
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.21",
|
|
53
|
+
"@phreshos/node": "^0.1.0",
|
|
53
54
|
"adm-zip": "^0.6.0",
|
|
54
55
|
"commander": "^15.0.0",
|
|
55
56
|
"jiti": "^2.7.0",
|
package/dist/attach.js
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { gatewayPath, streamProgram } from "./gateway.js";
|
|
2
|
-
/**
|
|
3
|
-
* Run a program on this machine's system, and stay with it.
|
|
4
|
-
*
|
|
5
|
-
* **The connection is the tether, in both directions.** While it is open
|
|
6
|
-
* the program runs; when it closes the system stops the program. So this
|
|
7
|
-
* does not have to promise to clean up, and could not be trusted if it
|
|
8
|
-
* did: a `kill -9`, a closed terminal and a dropped ssh session all end
|
|
9
|
-
* this process without running a line of it, and every one of them still
|
|
10
|
-
* closes the socket. The other direction is the same fact from the other
|
|
11
|
-
* end — when the program ends, the system says so and closes, and this
|
|
12
|
-
* resolves with the program's own status.
|
|
13
|
-
*/
|
|
14
|
-
export default async function attach(program, options = {}, watching = {}, path = gatewayPath(), signal) {
|
|
15
|
-
let ended = null;
|
|
16
|
-
await streamProgram({ word: "run", program, options }, function (event) {
|
|
17
|
-
if (event.event === "started")
|
|
18
|
-
watching.started?.(String(event.process));
|
|
19
|
-
if (event.event === "output")
|
|
20
|
-
watching.output?.(event.stream === "err" ? "err" : "out", String(event.text));
|
|
21
|
-
if (event.event === "exited")
|
|
22
|
-
ended = { code: (event.code ?? null), signal: (event.signal ?? null) };
|
|
23
|
-
}, path, signal);
|
|
24
|
-
// Closed with no ending said: the system went away while the program
|
|
25
|
-
// was running, which is a different thing from the program ending.
|
|
26
|
-
if (!ended)
|
|
27
|
-
throw new Error("The system closed before the program ended");
|
|
28
|
-
return ended;
|
|
29
|
-
}
|
package/dist/build-command.js
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { line } from "./style.js";
|
|
2
|
-
import { spawn } from "node:child_process";
|
|
3
|
-
import commandEnvironment from "./command-environment.js";
|
|
4
|
-
/** Run the optional build owned by `start`, `install`, and `pack`, never by the system. */
|
|
5
|
-
export default async function build(config, directory) {
|
|
6
|
-
const command = config.buildCommand;
|
|
7
|
-
if (!command)
|
|
8
|
-
return;
|
|
9
|
-
line("build", command);
|
|
10
|
-
await new Promise(function (resolve, reject) {
|
|
11
|
-
const child = spawn(command, { cwd: directory, env: commandEnvironment(directory), shell: true, stdio: "inherit" });
|
|
12
|
-
child.once("error", error => reject(new Error(`Build command failed: ${error.message}`)));
|
|
13
|
-
child.once("exit", function (code, signal) {
|
|
14
|
-
if (signal)
|
|
15
|
-
reject(new Error(`Build command ended on ${signal}`));
|
|
16
|
-
else if (code !== 0)
|
|
17
|
-
reject(new Error(`Build command exited with ${code ?? 0}`));
|
|
18
|
-
else
|
|
19
|
-
resolve();
|
|
20
|
-
});
|
|
21
|
-
});
|
|
22
|
-
}
|
|
@@ -1,161 +0,0 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
|
-
import { connect } from "node:net";
|
|
3
|
-
import { line } from "./style.js";
|
|
4
|
-
import commandEnvironment from "./command-environment.js";
|
|
5
|
-
const timeout = 15_000;
|
|
6
|
-
const reportEvery = 2_000;
|
|
7
|
-
const pollEvery = 200;
|
|
8
|
-
const clientOrigin = "null";
|
|
9
|
-
/** Refuse to start an owned command over a URL already served by something else. */
|
|
10
|
-
export async function assertClientDevelopmentUrlFree(url) {
|
|
11
|
-
if (!await occupied(url))
|
|
12
|
-
return;
|
|
13
|
-
throw new Error(`Client development URL is already in use: ${url}`);
|
|
14
|
-
}
|
|
15
|
-
function occupied(url) {
|
|
16
|
-
const location = new URL(url);
|
|
17
|
-
const port = Number(location.port || (location.protocol === "https:" ? 443 : 80));
|
|
18
|
-
return new Promise(resolve => {
|
|
19
|
-
const socket = connect({ host: location.hostname, port });
|
|
20
|
-
let settled = false;
|
|
21
|
-
const settle = (value) => {
|
|
22
|
-
if (settled)
|
|
23
|
-
return;
|
|
24
|
-
settled = true;
|
|
25
|
-
socket.destroy();
|
|
26
|
-
resolve(value);
|
|
27
|
-
};
|
|
28
|
-
socket.setTimeout(500);
|
|
29
|
-
socket.once("connect", () => settle(true));
|
|
30
|
-
socket.once("error", () => settle(false));
|
|
31
|
-
socket.once("timeout", () => settle(false));
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
/** Start the author's client tool without making it part of ProgramConfig. */
|
|
35
|
-
export function startClientDevelopment(command, directory) {
|
|
36
|
-
if (!command)
|
|
37
|
-
return null;
|
|
38
|
-
const child = spawn(command, {
|
|
39
|
-
cwd: directory,
|
|
40
|
-
env: commandEnvironment(directory),
|
|
41
|
-
shell: true,
|
|
42
|
-
stdio: "inherit",
|
|
43
|
-
// The command and everything it starts are one owned tool. A Vite
|
|
44
|
-
// child must not survive after the shell that launched it is gone.
|
|
45
|
-
detached: true
|
|
46
|
-
});
|
|
47
|
-
let result = null;
|
|
48
|
-
let stopping = false;
|
|
49
|
-
let stoppingTask = null;
|
|
50
|
-
let settle = () => undefined;
|
|
51
|
-
const exited = new Promise(resolve => { settle = resolve; });
|
|
52
|
-
const finish = (exit) => {
|
|
53
|
-
if (result)
|
|
54
|
-
return;
|
|
55
|
-
result = exit;
|
|
56
|
-
settle(exit);
|
|
57
|
-
};
|
|
58
|
-
child.once("error", error => finish({ code: null, signal: null, error }));
|
|
59
|
-
child.once("exit", (code, signal) => finish({ code, signal, error: null }));
|
|
60
|
-
return {
|
|
61
|
-
exited,
|
|
62
|
-
get stopping() { return stopping; },
|
|
63
|
-
async stop() {
|
|
64
|
-
if (!stoppingTask) {
|
|
65
|
-
stopping = true;
|
|
66
|
-
stoppingTask = (async () => {
|
|
67
|
-
if (!running(child))
|
|
68
|
-
return;
|
|
69
|
-
terminate(child, "SIGTERM");
|
|
70
|
-
await waitUntilStopped(child, 1_000);
|
|
71
|
-
if (running(child))
|
|
72
|
-
terminate(child, "SIGKILL");
|
|
73
|
-
await waitUntilStopped(child, 1_000);
|
|
74
|
-
})();
|
|
75
|
-
}
|
|
76
|
-
await stoppingTask;
|
|
77
|
-
}
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
/** Wait for the exact client URL that the derived Program will open. */
|
|
81
|
-
export async function waitForClientDevelopment(url, command, waiting = timeout, reporting = reportEvery) {
|
|
82
|
-
const began = Date.now();
|
|
83
|
-
let nextReport = began + reporting;
|
|
84
|
-
const commandEnded = command?.exited.then(exit => { throw commandFailure(exit); });
|
|
85
|
-
while (Date.now() - began < waiting) {
|
|
86
|
-
const availability = await inspect(url, waiting - (Date.now() - began));
|
|
87
|
-
if (availability === "ready")
|
|
88
|
-
return;
|
|
89
|
-
if (availability === "cors-blocked") {
|
|
90
|
-
throw new Error([
|
|
91
|
-
`Client development URL responded, but does not allow the sandboxed Client origin: ${url}`,
|
|
92
|
-
`Enable CORS so the response includes Access-Control-Allow-Origin: * (for Vite, use server: { cors: true }).`
|
|
93
|
-
].join("\n"));
|
|
94
|
-
}
|
|
95
|
-
const now = Date.now();
|
|
96
|
-
if (now >= nextReport) {
|
|
97
|
-
line("waiting for", url);
|
|
98
|
-
while (nextReport <= now)
|
|
99
|
-
nextReport += reporting;
|
|
100
|
-
}
|
|
101
|
-
const remaining = waiting - (Date.now() - began);
|
|
102
|
-
if (remaining <= 0)
|
|
103
|
-
break;
|
|
104
|
-
await Promise.race([
|
|
105
|
-
pause(Math.min(pollEvery, remaining)),
|
|
106
|
-
...(commandEnded ? [commandEnded] : [])
|
|
107
|
-
]);
|
|
108
|
-
}
|
|
109
|
-
const seconds = waiting / 1_000;
|
|
110
|
-
throw new Error(`Client development URL did not respond within ${seconds} ${seconds === 1 ? "second" : "seconds"}: ${url}`);
|
|
111
|
-
}
|
|
112
|
-
export function commandFailure(exit) {
|
|
113
|
-
if (exit.error)
|
|
114
|
-
return new Error(`Client development command failed: ${exit.error.message}`);
|
|
115
|
-
if (exit.signal)
|
|
116
|
-
return new Error(`Client development command ended on ${exit.signal}`);
|
|
117
|
-
return new Error(`Client development command exited with ${exit.code ?? 0}`);
|
|
118
|
-
}
|
|
119
|
-
async function inspect(url, remaining) {
|
|
120
|
-
try {
|
|
121
|
-
const response = await fetch(url, {
|
|
122
|
-
headers: { origin: clientOrigin },
|
|
123
|
-
signal: AbortSignal.timeout(Math.max(1, Math.min(500, remaining)))
|
|
124
|
-
});
|
|
125
|
-
const allowedOrigin = response.headers.get("access-control-allow-origin")?.trim();
|
|
126
|
-
await response.body?.cancel();
|
|
127
|
-
return allowedOrigin === "*" || allowedOrigin === clientOrigin ? "ready" : "cors-blocked";
|
|
128
|
-
}
|
|
129
|
-
catch {
|
|
130
|
-
return "unavailable";
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
function terminate(child, signal) {
|
|
134
|
-
if (!child.pid)
|
|
135
|
-
return;
|
|
136
|
-
try {
|
|
137
|
-
process.kill(-child.pid, signal);
|
|
138
|
-
}
|
|
139
|
-
catch {
|
|
140
|
-
child.kill(signal);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
function running(child) {
|
|
144
|
-
if (!child.pid)
|
|
145
|
-
return false;
|
|
146
|
-
try {
|
|
147
|
-
process.kill(-child.pid, 0);
|
|
148
|
-
return true;
|
|
149
|
-
}
|
|
150
|
-
catch {
|
|
151
|
-
return child.exitCode === null && child.signalCode === null;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
async function waitUntilStopped(child, milliseconds) {
|
|
155
|
-
const deadline = Date.now() + milliseconds;
|
|
156
|
-
while (running(child) && Date.now() < deadline)
|
|
157
|
-
await pause(20);
|
|
158
|
-
}
|
|
159
|
-
function pause(milliseconds) {
|
|
160
|
-
return new Promise(resolve => setTimeout(resolve, milliseconds));
|
|
161
|
-
}
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { delimiter, join } from "node:path";
|
|
2
|
-
/** Give author commands the same project-local binary resolution as scripts. */
|
|
3
|
-
export default function commandEnvironment(directory) {
|
|
4
|
-
const key = Object.keys(process.env).find(name => name.toLowerCase() === "path") ?? "PATH";
|
|
5
|
-
const inherited = process.env[key];
|
|
6
|
-
return {
|
|
7
|
-
...process.env,
|
|
8
|
-
[key]: [join(directory, "node_modules", ".bin"), inherited].filter(Boolean).join(delimiter)
|
|
9
|
-
};
|
|
10
|
-
}
|
package/dist/derive.js
DELETED
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
import { resolve } from "node:path";
|
|
2
|
-
export default function derive(config, directory, which) {
|
|
3
|
-
const server = serverHalf(config.server, which);
|
|
4
|
-
const client = clientHalf(config.client, which);
|
|
5
|
-
if (which === "development" && !config.server?.development && !config.client?.development) {
|
|
6
|
-
// Where a `development` block is learned, now that nothing writes
|
|
7
|
-
// one for you. It shows each half's distinct shape because their
|
|
8
|
-
// development locations are deliberately not the same concept.
|
|
9
|
-
throw new Error([
|
|
10
|
-
"Nothing here says how this program is developed.",
|
|
11
|
-
"",
|
|
12
|
-
"Say how the server runs or where the client is served:",
|
|
13
|
-
"",
|
|
14
|
-
" server: {",
|
|
15
|
-
" …",
|
|
16
|
-
" development: { startCommand: \"tsx source/server/main.ts\" }",
|
|
17
|
-
" // or: development: { entryFile: \"source/server/main.js\" }",
|
|
18
|
-
" }",
|
|
19
|
-
"",
|
|
20
|
-
" client: {",
|
|
21
|
-
" …",
|
|
22
|
-
" development: { url: \"http://localhost:5173\", startCommand: \"bun run dev\" }",
|
|
23
|
-
" }"
|
|
24
|
-
].join("\n"));
|
|
25
|
-
}
|
|
26
|
-
return {
|
|
27
|
-
identity: config.identity,
|
|
28
|
-
name: config.name,
|
|
29
|
-
version: config.version,
|
|
30
|
-
description: config.description,
|
|
31
|
-
categories: config.categories,
|
|
32
|
-
keywords: config.keywords,
|
|
33
|
-
website: config.website,
|
|
34
|
-
// Where it already is. Unlike pack, which gives it its canonical
|
|
35
|
-
// name, this points into the authoring tree and leaves it alone.
|
|
36
|
-
icon: config.icon && resolve(directory, config.icon),
|
|
37
|
-
agent: config.agent && resolve(directory, config.agent),
|
|
38
|
-
// Beside the source, and said out loud. A program built from an
|
|
39
|
-
// object resolves what it leaves unsaid against the *system's*
|
|
40
|
-
// working directory — so silence here means a program keeps what
|
|
41
|
-
// it keeps wherever the system happened to be started, which is
|
|
42
|
-
// nobody's idea of its own place.
|
|
43
|
-
storage: resolve(directory, "storage"),
|
|
44
|
-
...server && { server: {
|
|
45
|
-
location: resolve(directory, server.location),
|
|
46
|
-
start: server.start,
|
|
47
|
-
installCommand: config.server?.installCommand,
|
|
48
|
-
uninstallCommand: config.server?.uninstallCommand,
|
|
49
|
-
...serverExecution(server)
|
|
50
|
-
} },
|
|
51
|
-
// A URL stands as written; a directory is made absolute. The
|
|
52
|
-
// contract reads both from the one field, and which it is is
|
|
53
|
-
// decided by the same test the system uses.
|
|
54
|
-
...client && { client: {
|
|
55
|
-
location: /^https?:\/\//i.test(client.location) ? client.location : resolve(directory, client.location),
|
|
56
|
-
start: client.start,
|
|
57
|
-
title: config.client?.title,
|
|
58
|
-
size: config.client?.size,
|
|
59
|
-
position: config.client?.position,
|
|
60
|
-
layer: config.client?.layer,
|
|
61
|
-
minimize: config.client?.minimize
|
|
62
|
-
} }
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
// Only the fields each development shape owns are selected. Unknown
|
|
66
|
-
// runtime values do not become program fields merely because they were
|
|
67
|
-
// present in a JavaScript object.
|
|
68
|
-
function serverHalf(half, which) {
|
|
69
|
-
if (!half)
|
|
70
|
-
return null;
|
|
71
|
-
const { development, startCommand, entryFile, ...description } = half;
|
|
72
|
-
if (which === "production" || !development)
|
|
73
|
-
return {
|
|
74
|
-
...description,
|
|
75
|
-
...serverExecution({ startCommand, entryFile })
|
|
76
|
-
};
|
|
77
|
-
return {
|
|
78
|
-
...description,
|
|
79
|
-
location: ".",
|
|
80
|
-
...serverExecution(development)
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
function serverExecution(server) {
|
|
84
|
-
return server.startCommand !== undefined
|
|
85
|
-
? { startCommand: server.startCommand }
|
|
86
|
-
: { entryFile: server.entryFile };
|
|
87
|
-
}
|
|
88
|
-
function clientHalf(half, which) {
|
|
89
|
-
if (!half)
|
|
90
|
-
return null;
|
|
91
|
-
const { development, ...declared } = half;
|
|
92
|
-
if (which === "production" || !development)
|
|
93
|
-
return declared;
|
|
94
|
-
return {
|
|
95
|
-
...declared,
|
|
96
|
-
location: development.url
|
|
97
|
-
};
|
|
98
|
-
}
|