@phreshos/node 0.1.12 → 0.1.13
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 +5 -0
- package/dist/development-client.d.ts +20 -0
- package/dist/development-client.js +259 -0
- package/dist/main.d.ts +2 -2
- package/dist/main.js +1 -1
- package/dist/process-tree.d.ts +10 -0
- package/dist/process-tree.js +67 -0
- package/dist/program-resources.d.ts +3 -3
- package/dist/program-resources.js +10 -9
- package/dist/project.d.ts +5 -3
- package/dist/project.js +53 -13
- package/dist/shell.d.ts +3 -0
- package/dist/shell.js +125 -0
- package/dist/storage.d.ts +6 -2
- package/dist/storage.js +27 -8
- package/dist/system.d.ts +25 -20
- package/dist/system.js +54 -39
- package/dist/traffic.d.ts +2 -2
- package/dist/traffic.js +1 -1
- package/dist/transport.d.ts +2 -2
- package/dist/transport.js +18 -4
- package/dist/uploads.d.ts +4 -1
- package/dist/uploads.js +20 -5
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -49,6 +49,11 @@ await system.disconnect()
|
|
|
49
49
|
definitions, installation, execution shortcuts, and packaging. The System
|
|
50
50
|
receives only the resulting Program definition.
|
|
51
51
|
|
|
52
|
+
`Project.dev()` starts and supervises a declared Client development command. It
|
|
53
|
+
assigns an available port when no URL is declared and provides the port and
|
|
54
|
+
public Program asset base through `PHRESHOS_CLIENT_PORT` and
|
|
55
|
+
`PHRESHOS_CLIENT_BASE`.
|
|
56
|
+
|
|
52
57
|
`Project.open()` starts from the current working directory by default.
|
|
53
58
|
`System.connect()` resolves an explicit System home, then `PHRESHOS_HOME`,
|
|
54
59
|
then the current owner's default System home.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ClientDevelopment, ProgramProcessRunEvent } from "@phreshos/core";
|
|
2
|
+
/** One prepared Client development source owned by a Project development run. */
|
|
3
|
+
export default class DevelopmentClient {
|
|
4
|
+
readonly url: string;
|
|
5
|
+
private readonly startCommand;
|
|
6
|
+
private readonly directory;
|
|
7
|
+
private command;
|
|
8
|
+
private controller;
|
|
9
|
+
private releaseSignal;
|
|
10
|
+
private constructor();
|
|
11
|
+
/** Select the development address without starting the authored command. */
|
|
12
|
+
static prepare(development: ClientDevelopment, directory: string): Promise<DevelopmentClient>;
|
|
13
|
+
/** Start and verify the Client beneath its System-created asset address. */
|
|
14
|
+
start(assetId: string, signal?: AbortSignal): Promise<void>;
|
|
15
|
+
processSignal(fallback?: AbortSignal): AbortSignal | undefined;
|
|
16
|
+
supervise(lifecycle: AsyncGenerator<ProgramProcessRunEvent, void, void>): AsyncGenerator<ProgramProcessRunEvent, void, void>;
|
|
17
|
+
dispose(reason: unknown): Promise<void>;
|
|
18
|
+
private waitUntilReady;
|
|
19
|
+
private supervisedLifecycle;
|
|
20
|
+
}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { connect, createServer } from "node:net";
|
|
3
|
+
import { delimiter, join } from "node:path";
|
|
4
|
+
const readinessTimeout = 15_000;
|
|
5
|
+
const pollingInterval = 200;
|
|
6
|
+
/** One prepared Client development source owned by a Project development run. */
|
|
7
|
+
export default class DevelopmentClient {
|
|
8
|
+
url;
|
|
9
|
+
startCommand;
|
|
10
|
+
directory;
|
|
11
|
+
command = null;
|
|
12
|
+
controller = null;
|
|
13
|
+
releaseSignal = () => undefined;
|
|
14
|
+
constructor(url, startCommand, directory) {
|
|
15
|
+
this.url = url;
|
|
16
|
+
this.startCommand = startCommand;
|
|
17
|
+
this.directory = directory;
|
|
18
|
+
}
|
|
19
|
+
/** Select the development address without starting the authored command. */
|
|
20
|
+
static async prepare(development, directory) {
|
|
21
|
+
const url = development.url ?? `http://localhost:${await availablePort()}/`;
|
|
22
|
+
if (development.startCommand)
|
|
23
|
+
await assertAvailable(url);
|
|
24
|
+
return new DevelopmentClient(url, development.startCommand ?? null, directory);
|
|
25
|
+
}
|
|
26
|
+
/** Start and verify the Client beneath its System-created asset address. */
|
|
27
|
+
async start(assetId, signal) {
|
|
28
|
+
if (this.controller)
|
|
29
|
+
throw new Error("The Client development source has already started");
|
|
30
|
+
const base = `/program/${assetId}/assets/`;
|
|
31
|
+
const controller = new AbortController();
|
|
32
|
+
this.controller = controller;
|
|
33
|
+
this.releaseSignal = forwardAbort(signal, controller);
|
|
34
|
+
try {
|
|
35
|
+
if (this.startCommand) {
|
|
36
|
+
this.command = new OwnedCommand(this.startCommand, this.directory, {
|
|
37
|
+
PHRESHOS_CLIENT_BASE: base,
|
|
38
|
+
PHRESHOS_CLIENT_PORT: String(portOf(this.url))
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
await this.waitUntilReady(new URL(base, this.url).href);
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
await this.dispose(error);
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
processSignal(fallback) {
|
|
49
|
+
return this.command ? this.controller?.signal : fallback;
|
|
50
|
+
}
|
|
51
|
+
supervise(lifecycle) {
|
|
52
|
+
if (!this.command) {
|
|
53
|
+
this.releaseSignal();
|
|
54
|
+
return lifecycle;
|
|
55
|
+
}
|
|
56
|
+
return this.supervisedLifecycle(lifecycle);
|
|
57
|
+
}
|
|
58
|
+
async dispose(reason) {
|
|
59
|
+
this.releaseSignal();
|
|
60
|
+
this.controller?.abort(reason);
|
|
61
|
+
await this.command?.stop();
|
|
62
|
+
}
|
|
63
|
+
async waitUntilReady(url) {
|
|
64
|
+
const deadline = Date.now() + readinessTimeout;
|
|
65
|
+
while (Date.now() < deadline) {
|
|
66
|
+
this.controller.signal.throwIfAborted();
|
|
67
|
+
const exit = this.command?.exitResult();
|
|
68
|
+
if (exit)
|
|
69
|
+
throw commandFailure(exit);
|
|
70
|
+
try {
|
|
71
|
+
const response = await fetch(url, {
|
|
72
|
+
signal: AbortSignal.timeout(Math.max(1, Math.min(500, deadline - Date.now())))
|
|
73
|
+
});
|
|
74
|
+
await response.body?.cancel();
|
|
75
|
+
if (response.ok)
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
catch { /* The development server is still starting. */ }
|
|
79
|
+
await pause(Math.min(pollingInterval, Math.max(0, deadline - Date.now())), this.controller.signal);
|
|
80
|
+
}
|
|
81
|
+
throw new Error(`Client development URL did not respond within 15 seconds: ${url}`);
|
|
82
|
+
}
|
|
83
|
+
async *supervisedLifecycle(lifecycle) {
|
|
84
|
+
const iterator = lifecycle[Symbol.asyncIterator]();
|
|
85
|
+
const command = this.command;
|
|
86
|
+
try {
|
|
87
|
+
while (true) {
|
|
88
|
+
const next = iterator.next();
|
|
89
|
+
const outcome = await Promise.race([
|
|
90
|
+
next.then(result => ({ source: "system", result }), error => ({ source: "system-error", error })),
|
|
91
|
+
command.exited().then(result => ({ source: "client", result }))
|
|
92
|
+
]);
|
|
93
|
+
if (outcome.source === "client") {
|
|
94
|
+
const error = commandFailure(outcome.result);
|
|
95
|
+
this.controller?.abort(error);
|
|
96
|
+
await next.catch(() => undefined);
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
if (outcome.source === "system-error")
|
|
100
|
+
throw outcome.error;
|
|
101
|
+
if (outcome.result.done)
|
|
102
|
+
return;
|
|
103
|
+
yield outcome.result.value;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
await this.dispose(new Error("The development lifecycle ended"));
|
|
108
|
+
await iterator.return?.();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/** One operating-system command supervised as a complete process tree. */
|
|
113
|
+
class OwnedCommand {
|
|
114
|
+
child;
|
|
115
|
+
completion;
|
|
116
|
+
result = null;
|
|
117
|
+
stopped = false;
|
|
118
|
+
constructor(command, directory, environment) {
|
|
119
|
+
this.child = spawn(command, {
|
|
120
|
+
cwd: directory,
|
|
121
|
+
env: commandEnvironment(directory, environment),
|
|
122
|
+
shell: true,
|
|
123
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
124
|
+
detached: true
|
|
125
|
+
});
|
|
126
|
+
this.child.stdout?.pipe(process.stdout, { end: false });
|
|
127
|
+
this.child.stderr?.pipe(process.stderr, { end: false });
|
|
128
|
+
this.completion = new Promise(resolve => {
|
|
129
|
+
const finish = (exit) => {
|
|
130
|
+
if (this.result)
|
|
131
|
+
return;
|
|
132
|
+
this.result = exit;
|
|
133
|
+
resolve(exit);
|
|
134
|
+
};
|
|
135
|
+
this.child.once("error", error => finish({ code: null, signal: null, error }));
|
|
136
|
+
this.child.once("exit", (code, signal) => finish({ code, signal, error: null }));
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
exited() { return this.completion; }
|
|
140
|
+
exitResult() { return this.result; }
|
|
141
|
+
async stop() {
|
|
142
|
+
if (this.stopped)
|
|
143
|
+
return;
|
|
144
|
+
this.stopped = true;
|
|
145
|
+
if (!running(this.child))
|
|
146
|
+
return;
|
|
147
|
+
terminate(this.child, "SIGTERM");
|
|
148
|
+
await waitUntilStopped(this.child, 1_000);
|
|
149
|
+
if (running(this.child))
|
|
150
|
+
terminate(this.child, "SIGKILL");
|
|
151
|
+
await waitUntilStopped(this.child, 1_000);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
async function availablePort() {
|
|
155
|
+
const server = createServer();
|
|
156
|
+
return await new Promise((done, fail) => {
|
|
157
|
+
server.once("error", fail);
|
|
158
|
+
server.listen(0, "localhost", () => {
|
|
159
|
+
const address = server.address();
|
|
160
|
+
if (!address || typeof address === "string") {
|
|
161
|
+
server.close();
|
|
162
|
+
fail(new Error("An available Client development port could not be selected"));
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
server.close(error => error ? fail(error) : done(address.port));
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
async function assertAvailable(url) {
|
|
170
|
+
const location = new URL(url);
|
|
171
|
+
const occupied = await new Promise(done => {
|
|
172
|
+
const socket = connect({ host: location.hostname, port: portOf(url) });
|
|
173
|
+
let finished = false;
|
|
174
|
+
const finish = (value) => {
|
|
175
|
+
if (finished)
|
|
176
|
+
return;
|
|
177
|
+
finished = true;
|
|
178
|
+
socket.destroy();
|
|
179
|
+
done(value);
|
|
180
|
+
};
|
|
181
|
+
socket.setTimeout(500);
|
|
182
|
+
socket.once("connect", () => finish(true));
|
|
183
|
+
socket.once("error", () => finish(false));
|
|
184
|
+
socket.once("timeout", () => finish(false));
|
|
185
|
+
});
|
|
186
|
+
if (occupied)
|
|
187
|
+
throw new Error(`Client development URL is already in use: ${url}`);
|
|
188
|
+
}
|
|
189
|
+
function portOf(url) {
|
|
190
|
+
const location = new URL(url);
|
|
191
|
+
return Number(location.port || (location.protocol === "https:" ? 443 : 80));
|
|
192
|
+
}
|
|
193
|
+
function commandEnvironment(directory, additions) {
|
|
194
|
+
const key = Object.keys(process.env).find(name => name.toLowerCase() === "path") ?? "PATH";
|
|
195
|
+
const inherited = process.env[key];
|
|
196
|
+
return { ...process.env, ...additions, [key]: [join(directory, "node_modules", ".bin"), inherited].filter(Boolean).join(delimiter) };
|
|
197
|
+
}
|
|
198
|
+
function forwardAbort(source, target) {
|
|
199
|
+
if (!source)
|
|
200
|
+
return () => undefined;
|
|
201
|
+
const abort = () => target.abort(source.reason);
|
|
202
|
+
if (source.aborted)
|
|
203
|
+
abort();
|
|
204
|
+
else
|
|
205
|
+
source.addEventListener("abort", abort, { once: true });
|
|
206
|
+
return () => source.removeEventListener("abort", abort);
|
|
207
|
+
}
|
|
208
|
+
function commandFailure(exit) {
|
|
209
|
+
if (exit.error)
|
|
210
|
+
return new Error(`Client development command failed: ${exit.error.message}`);
|
|
211
|
+
if (exit.signal)
|
|
212
|
+
return new Error(`Client development command ended on ${exit.signal}`);
|
|
213
|
+
return new Error(`Client development command exited with ${exit.code ?? 0}`);
|
|
214
|
+
}
|
|
215
|
+
function terminate(child, signal) {
|
|
216
|
+
if (!child.pid)
|
|
217
|
+
return;
|
|
218
|
+
try {
|
|
219
|
+
process.kill(-child.pid, signal);
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
child.kill(signal);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
function running(child) {
|
|
226
|
+
if (!child.pid)
|
|
227
|
+
return false;
|
|
228
|
+
try {
|
|
229
|
+
process.kill(-child.pid, 0);
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
return child.exitCode === null && child.signalCode === null;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
async function waitUntilStopped(child, milliseconds) {
|
|
237
|
+
const deadline = Date.now() + milliseconds;
|
|
238
|
+
while (running(child) && Date.now() < deadline)
|
|
239
|
+
await new Promise(resolve => setTimeout(resolve, 20));
|
|
240
|
+
}
|
|
241
|
+
function pause(milliseconds, signal) {
|
|
242
|
+
return new Promise((done, fail) => {
|
|
243
|
+
if (signal.aborted) {
|
|
244
|
+
fail(signal.reason);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
const timeout = setTimeout(finish, milliseconds);
|
|
248
|
+
const abort = () => finish(signal.reason);
|
|
249
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
250
|
+
function finish(error) {
|
|
251
|
+
clearTimeout(timeout);
|
|
252
|
+
signal.removeEventListener("abort", abort);
|
|
253
|
+
if (error !== undefined)
|
|
254
|
+
fail(error);
|
|
255
|
+
else
|
|
256
|
+
done();
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
}
|
package/dist/main.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { gatewayAddress } from "./address.js";
|
|
2
|
-
export {
|
|
3
|
-
export { Service, type ClientLaunch, type Launch, type
|
|
2
|
+
export { ClientEndpoint, ClientService, Endpoint, Process, Program, ServerEndpoint, ServerService, System, type ProgramProcessRunEvent, type ProgramProcessRunOptions } from "./system.js";
|
|
3
|
+
export { Service, type ClientLaunch, type Launch, type Permission, type PermissionChange, type PermissionInput, type Permissions, type ProgramPermissions, type ProgramDefinition, type ServerLaunch, type ServiceKey, type ShellEvent, type ShellOptions, type ProgramStartup, type Storage, } from "@phreshos/core";
|
|
4
4
|
export { resolveHome } from "./home.js";
|
|
5
5
|
export { Project, type Manifest, type PackedProject, type ProjectMode, type ProjectOptions, type ProjectRunOptions } from "./project.js";
|
package/dist/main.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { gatewayAddress } from "./address.js";
|
|
2
|
-
export {
|
|
2
|
+
export { ClientEndpoint, ClientService, Endpoint, Process, Program, ServerEndpoint, ServerService, System } from "./system.js";
|
|
3
3
|
export { Service, } from "@phreshos/core";
|
|
4
4
|
export { resolveHome } from "./home.js";
|
|
5
5
|
export { Project } from "./project.js";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ChildProcess } from "node:child_process";
|
|
2
|
+
/** The complete operating-system process tree beneath one shell command. */
|
|
3
|
+
export default class ProcessTree {
|
|
4
|
+
private readonly child;
|
|
5
|
+
private readonly ended;
|
|
6
|
+
private forcing;
|
|
7
|
+
constructor(child: ChildProcess, ended: (code: number | null, signal: NodeJS.Signals | null) => void | Promise<void>);
|
|
8
|
+
stop(): void;
|
|
9
|
+
private finish;
|
|
10
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
const terminationGrace = 1_000;
|
|
2
|
+
/** The complete operating-system process tree beneath one shell command. */
|
|
3
|
+
export default class ProcessTree {
|
|
4
|
+
child;
|
|
5
|
+
ended;
|
|
6
|
+
forcing = null;
|
|
7
|
+
constructor(child, ended) {
|
|
8
|
+
this.child = child;
|
|
9
|
+
this.ended = ended;
|
|
10
|
+
child.on("exit", (code, signal) => { this.finish(code, signal).catch(() => undefined); });
|
|
11
|
+
}
|
|
12
|
+
stop() {
|
|
13
|
+
signalTree(this.child, "SIGTERM");
|
|
14
|
+
if (this.forcing)
|
|
15
|
+
return;
|
|
16
|
+
this.forcing = setTimeout(() => signalTree(this.child, "SIGKILL"), terminationGrace);
|
|
17
|
+
this.forcing.unref();
|
|
18
|
+
}
|
|
19
|
+
async finish(code, signal) {
|
|
20
|
+
if (this.forcing)
|
|
21
|
+
clearTimeout(this.forcing);
|
|
22
|
+
this.forcing = null;
|
|
23
|
+
await finishTree(this.child);
|
|
24
|
+
await this.ended(code, signal);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function signalTree(child, signal) {
|
|
28
|
+
if (!child.pid)
|
|
29
|
+
return;
|
|
30
|
+
try {
|
|
31
|
+
process.kill(-child.pid, signal);
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
if (error.code === "ESRCH")
|
|
35
|
+
return;
|
|
36
|
+
if (child.exitCode === null && child.signalCode === null)
|
|
37
|
+
child.kill(signal);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
async function finishTree(child) {
|
|
41
|
+
const pid = child.pid;
|
|
42
|
+
if (!pid || !treeExists(pid))
|
|
43
|
+
return;
|
|
44
|
+
signalTree(child, "SIGTERM");
|
|
45
|
+
if (await waitForTree(pid))
|
|
46
|
+
return;
|
|
47
|
+
signalTree(child, "SIGKILL");
|
|
48
|
+
await waitForTree(pid);
|
|
49
|
+
}
|
|
50
|
+
async function waitForTree(pid) {
|
|
51
|
+
const began = Date.now();
|
|
52
|
+
while (treeExists(pid)) {
|
|
53
|
+
if (Date.now() - began >= terminationGrace)
|
|
54
|
+
return false;
|
|
55
|
+
await new Promise(resolve => setTimeout(resolve, 20));
|
|
56
|
+
}
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
function treeExists(pid) {
|
|
60
|
+
try {
|
|
61
|
+
process.kill(-pid, 0);
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
return error.code !== "ESRCH";
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type ProgramPermissions, type ProgramSql, type ProgramStore } from "@phreshos/core";
|
|
2
2
|
type Request = (value: object) => Promise<unknown>;
|
|
3
3
|
type ProgramAddress = Readonly<{
|
|
4
4
|
identity: string;
|
|
@@ -8,6 +8,6 @@ type ProgramAddress = Readonly<{
|
|
|
8
8
|
export declare function programStore(request: Request, handle: ProgramAddress): ProgramStore;
|
|
9
9
|
/** Program-owned SQL capability carried through the owner-local Gateway. */
|
|
10
10
|
export declare function programSql(request: Request, handle: ProgramAddress, database: "database" | "logs"): ProgramSql;
|
|
11
|
-
/**
|
|
12
|
-
export declare function
|
|
11
|
+
/** Program permission management carried through the owner-local Gateway. */
|
|
12
|
+
export declare function programPermissions(request: Request, handle: ProgramAddress): ProgramPermissions;
|
|
13
13
|
export {};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { parsePermission, parsePermissionChange, parsePermissions } from "@phreshos/core";
|
|
1
2
|
/** Program-owned key-value storage carried through the owner-local Gateway. */
|
|
2
3
|
export function programStore(request, handle) {
|
|
3
4
|
const operate = (storeOperation, key, value, ttl) => request({
|
|
@@ -26,21 +27,21 @@ export function programSql(request, handle, database) {
|
|
|
26
27
|
}
|
|
27
28
|
};
|
|
28
29
|
}
|
|
29
|
-
/**
|
|
30
|
-
export function
|
|
31
|
-
const operate = (permissionOperation, name,
|
|
30
|
+
/** Program permission management carried through the owner-local Gateway. */
|
|
31
|
+
export function programPermissions(request, handle) {
|
|
32
|
+
const operate = (permissionOperation, name, permission) => request({
|
|
32
33
|
capability: "program",
|
|
33
|
-
operation: "
|
|
34
|
+
operation: "permissions",
|
|
34
35
|
handle,
|
|
35
36
|
permissionOperation,
|
|
36
37
|
name,
|
|
37
|
-
|
|
38
|
+
permission
|
|
38
39
|
});
|
|
39
40
|
return {
|
|
40
|
-
get
|
|
41
|
-
|
|
42
|
-
set
|
|
43
|
-
delete
|
|
41
|
+
async get(name) { return parsePermission(await operate("get", name)); },
|
|
42
|
+
async all() { return parsePermissions(await operate("all")); },
|
|
43
|
+
async set(name, permission) { return parsePermissionChange(await operate("set", name, permission)); },
|
|
44
|
+
async delete(name) { return parsePermissionChange(await operate("delete", name)); }
|
|
44
45
|
};
|
|
45
46
|
}
|
|
46
47
|
function written(statement, rest) {
|
package/dist/project.d.ts
CHANGED
|
@@ -18,9 +18,11 @@ export declare class Project {
|
|
|
18
18
|
/** Run the optional author-owned production build command. */
|
|
19
19
|
build(): Promise<void>;
|
|
20
20
|
/** Build this Project and return its production Process lifecycle generator. */
|
|
21
|
-
start(system: SystemContract, options?: ProjectRunOptions): Promise<AsyncGenerator<import("@phreshos/core").
|
|
22
|
-
/**
|
|
23
|
-
dev(system: SystemContract, options?: ProjectRunOptions): Promise<AsyncGenerator<import("@phreshos/core").
|
|
21
|
+
start(system: SystemContract, options?: ProjectRunOptions): Promise<AsyncGenerator<import("@phreshos/core").ProgramProcessRunEvent, void, void>>;
|
|
22
|
+
/** Prepare this Project's Client development source and return its Process lifecycle. */
|
|
23
|
+
dev(system: SystemContract, options?: ProjectRunOptions): Promise<AsyncGenerator<import("@phreshos/core").ProgramProcessRunEvent, void, void>>;
|
|
24
|
+
private developmentRun;
|
|
25
|
+
private prepareDevelopment;
|
|
24
26
|
/** Build this Project and return its Program installation generator. */
|
|
25
27
|
install(system: SystemContract): Promise<AsyncGenerator<Readonly<{
|
|
26
28
|
stream: "stdout" | "stderr";
|
package/dist/project.js
CHANGED
|
@@ -6,7 +6,9 @@ import { existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
|
6
6
|
import { readFile } from "node:fs/promises";
|
|
7
7
|
import { delimiter, isAbsolute, join, normalize, resolve, sep } from "node:path";
|
|
8
8
|
import { createJiti } from "jiti";
|
|
9
|
+
import DevelopmentClient from "./development-client.js";
|
|
9
10
|
const configFile = "phresh.config.ts";
|
|
11
|
+
const defaultClientPort = 5200;
|
|
10
12
|
/** One loaded Program authoring project rooted at an absolute directory. */
|
|
11
13
|
export class Project {
|
|
12
14
|
directory;
|
|
@@ -52,10 +54,10 @@ export class Project {
|
|
|
52
54
|
developmentDefinition() {
|
|
53
55
|
return this.definition("development");
|
|
54
56
|
}
|
|
55
|
-
definition(mode) {
|
|
57
|
+
definition(mode, developmentClientUrl) {
|
|
56
58
|
const config = this.config;
|
|
57
59
|
const server = serverHalf(config.server, mode);
|
|
58
|
-
const client = clientHalf(config.client, mode);
|
|
60
|
+
const client = clientHalf(config.client, mode, developmentClientUrl);
|
|
59
61
|
if (mode === "development" && !config.server?.development && !config.client?.development) {
|
|
60
62
|
throw new Error("Nothing here says how this Program is developed");
|
|
61
63
|
}
|
|
@@ -83,7 +85,8 @@ export class Project {
|
|
|
83
85
|
size: config.client?.size,
|
|
84
86
|
position: config.client?.position,
|
|
85
87
|
layer: config.client?.layer,
|
|
86
|
-
minimize: config.client?.minimize
|
|
88
|
+
minimize: config.client?.minimize,
|
|
89
|
+
permissions: config.client?.permissions
|
|
87
90
|
} }
|
|
88
91
|
};
|
|
89
92
|
}
|
|
@@ -115,9 +118,34 @@ export class Project {
|
|
|
115
118
|
await this.build();
|
|
116
119
|
return await this.run(system, this.productionDefinition(), options);
|
|
117
120
|
}
|
|
118
|
-
/**
|
|
121
|
+
/** Prepare this Project's Client development source and return its Process lifecycle. */
|
|
119
122
|
async dev(system, options = {}) {
|
|
120
|
-
|
|
123
|
+
const development = this.config.client?.development;
|
|
124
|
+
const startsClient = Boolean(development && (this.config.client?.start ?? true));
|
|
125
|
+
if (!startsClient || !development)
|
|
126
|
+
return await this.run(system, this.developmentDefinition(), options);
|
|
127
|
+
if (development.startCommand)
|
|
128
|
+
return this.developmentRun(system, development, options);
|
|
129
|
+
const prepared = await this.prepareDevelopment(system, development, options);
|
|
130
|
+
return prepared.client.supervise(prepared.lifecycle);
|
|
131
|
+
}
|
|
132
|
+
async *developmentRun(system, development, options) {
|
|
133
|
+
const prepared = await this.prepareDevelopment(system, development, options);
|
|
134
|
+
yield* prepared.client.supervise(prepared.lifecycle);
|
|
135
|
+
}
|
|
136
|
+
async prepareDevelopment(system, development, options) {
|
|
137
|
+
const client = await DevelopmentClient.prepare(development, this.directory);
|
|
138
|
+
const program = await system.forceCreateProgram(this.definition("development", client.url));
|
|
139
|
+
try {
|
|
140
|
+
await client.start(program.assetId, options.signal);
|
|
141
|
+
const lifecycle = program.process.run({ options: options.options ?? {} }, { signal: client.processSignal(options.signal) });
|
|
142
|
+
return { client, lifecycle };
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
await client.dispose(error);
|
|
146
|
+
await program.forget().catch(() => undefined);
|
|
147
|
+
throw error;
|
|
148
|
+
}
|
|
121
149
|
}
|
|
122
150
|
/** Build this Project and return its Program installation generator. */
|
|
123
151
|
async install(system) {
|
|
@@ -171,18 +199,20 @@ function serverHalf(half, mode) {
|
|
|
171
199
|
function serverExecution(server) {
|
|
172
200
|
return server.startCommand !== undefined ? { startCommand: server.startCommand } : { entryFile: server.entryFile };
|
|
173
201
|
}
|
|
174
|
-
function clientHalf(half, mode) {
|
|
202
|
+
function clientHalf(half, mode, developmentUrl) {
|
|
175
203
|
if (!half)
|
|
176
204
|
return null;
|
|
177
205
|
const { development, ...declared } = half;
|
|
178
|
-
return mode === "development" && development
|
|
206
|
+
return mode === "development" && development
|
|
207
|
+
? { ...declared, location: developmentUrl ?? development.url ?? `http://localhost:${defaultClientPort}/` }
|
|
208
|
+
: declared;
|
|
179
209
|
}
|
|
180
210
|
function validateConfig(config) {
|
|
181
211
|
if (typeof config.identity !== "string" || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(config.identity)) {
|
|
182
212
|
throw new Error("A Program's identity must be kebab-case");
|
|
183
213
|
}
|
|
184
214
|
if (!config.server && !config.client)
|
|
185
|
-
throw new Error("A Program must have a Server
|
|
215
|
+
throw new Error("A Program must have a Server Endpoint, a Client Endpoint, or both");
|
|
186
216
|
for (const field of ["name", "version", "description", "icon", "agent", "website"]) {
|
|
187
217
|
if (config[field] !== undefined && typeof config[field] !== "string")
|
|
188
218
|
throw new Error(`A Program's ${field} must be text`);
|
|
@@ -212,14 +242,23 @@ function validateConfig(config) {
|
|
|
212
242
|
throw new Error("A Program's default Process must start a Server Endpoint, a Client Endpoint, or both");
|
|
213
243
|
}
|
|
214
244
|
if (config.server)
|
|
215
|
-
execution(config.server, "A Server
|
|
245
|
+
execution(config.server, "A Server Endpoint");
|
|
216
246
|
if (config.server?.development)
|
|
217
247
|
execution(config.server.development, "server.development");
|
|
218
248
|
if (config.client?.layer !== undefined && !layers.includes(config.client.layer)) {
|
|
219
|
-
throw new Error(`A Client
|
|
249
|
+
throw new Error(`A Client Endpoint's layer must be one of ${layers.join(", ")}`);
|
|
220
250
|
}
|
|
221
|
-
if (config.client?.development
|
|
222
|
-
|
|
251
|
+
if (config.client?.development) {
|
|
252
|
+
const development = config.client.development;
|
|
253
|
+
if (development.url !== undefined && !httpUrl(development.url)) {
|
|
254
|
+
throw new Error("client.development.url must be a valid HTTP or HTTPS URL");
|
|
255
|
+
}
|
|
256
|
+
if (development.startCommand !== undefined && (typeof development.startCommand !== "string" || !development.startCommand.trim())) {
|
|
257
|
+
throw new Error("client.development.startCommand must be non-empty text");
|
|
258
|
+
}
|
|
259
|
+
if (development.url === undefined && development.startCommand === undefined) {
|
|
260
|
+
throw new Error("client.development must declare a URL or start command");
|
|
261
|
+
}
|
|
223
262
|
}
|
|
224
263
|
for (const [name, value] of [["size", config.client?.size], ["position", config.client?.position]]) {
|
|
225
264
|
if (value === undefined)
|
|
@@ -286,7 +325,8 @@ function packageDefinition(config, version) {
|
|
|
286
325
|
size: config.client.size,
|
|
287
326
|
position: config.client.position,
|
|
288
327
|
layer: config.client.layer,
|
|
289
|
-
minimize: config.client.minimize
|
|
328
|
+
minimize: config.client.minimize,
|
|
329
|
+
permissions: config.client.permissions
|
|
290
330
|
} }
|
|
291
331
|
};
|
|
292
332
|
}
|
package/dist/shell.d.ts
ADDED