@phreshos/cli 0.1.0 → 0.1.1
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 +10 -3
- package/dist/client-development.js +17 -5
- package/dist/init.js +38 -24
- package/dist/launch.js +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -204,17 +204,17 @@ phresh init --server \
|
|
|
204
204
|
Use `phresh init --help` for the complete option list. An existing config is
|
|
205
205
|
never replaced silently: a terminal asks, while automation must say `--force`.
|
|
206
206
|
|
|
207
|
-
A
|
|
207
|
+
A Program must declare a Server endpoint, a Client endpoint, or both. Neither is
|
|
208
208
|
refused during the interview rather than at the border, which is the
|
|
209
209
|
earliest place it can be refused.
|
|
210
210
|
|
|
211
211
|
The final `Next` line is derived from the resulting config. It always shows
|
|
212
212
|
`phresh start` and `phresh install`; it shows `phresh dev` only when at least
|
|
213
|
-
one
|
|
213
|
+
one Endpoint received a development declaration.
|
|
214
214
|
|
|
215
215
|
Visual and advanced runtime defaults remain for the author to add deliberately:
|
|
216
216
|
`icons`, `size`, `position`, `installCommand`, `start`, layers, and minimization
|
|
217
|
-
are not guessed. An omitted `start` is `true`; only a default-off
|
|
217
|
+
are not guessed. An omitted `start` is `true`; only a default-off Endpoint needs to
|
|
218
218
|
say `start: false`.
|
|
219
219
|
|
|
220
220
|
## start and dev
|
|
@@ -332,6 +332,13 @@ When `apiDocs` is declared, its Markdown file is copied to `api-docs.md` and
|
|
|
332
332
|
the packaged description names that canonical entry point. A missing declared
|
|
333
333
|
file is an error, not an undocumented Program.
|
|
334
334
|
|
|
335
|
+
The document covers only what the Program owns: its capabilities, operation
|
|
336
|
+
and event names, payloads, behavior, state, and Program-defined failures. It
|
|
337
|
+
does not explain how to find a Program, obtain an Endpoint, publish or ask, or
|
|
338
|
+
register a subscription. Those are system contracts documented once by the
|
|
339
|
+
SDKs; `api-docs.md` supplies only the Program-specific meaning carried through
|
|
340
|
+
them.
|
|
341
|
+
|
|
335
342
|
There is **no wrapping directory**: `program.json`, `server/`, `client/`
|
|
336
343
|
`icons/`, and optional `api-docs.md` sit at the package's root, and the system names the
|
|
337
344
|
directory it installs into from your program's `identity`.
|
|
@@ -5,6 +5,7 @@ import commandEnvironment from "./command-environment.js";
|
|
|
5
5
|
const timeout = 15_000;
|
|
6
6
|
const reportEvery = 2_000;
|
|
7
7
|
const pollEvery = 200;
|
|
8
|
+
const clientOrigin = "null";
|
|
8
9
|
/** Refuse to start an owned command over a URL already served by something else. */
|
|
9
10
|
export async function assertClientDevelopmentUrlFree(url) {
|
|
10
11
|
if (!await occupied(url))
|
|
@@ -82,8 +83,15 @@ export async function waitForClientDevelopment(url, command, waiting = timeout,
|
|
|
82
83
|
let nextReport = began + reporting;
|
|
83
84
|
const commandEnded = command?.exited.then(exit => { throw commandFailure(exit); });
|
|
84
85
|
while (Date.now() - began < waiting) {
|
|
85
|
-
|
|
86
|
+
const availability = await inspect(url, waiting - (Date.now() - began));
|
|
87
|
+
if (availability === "ready")
|
|
86
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
|
+
}
|
|
87
95
|
const now = Date.now();
|
|
88
96
|
if (now >= nextReport) {
|
|
89
97
|
line("waiting for", url);
|
|
@@ -108,14 +116,18 @@ export function commandFailure(exit) {
|
|
|
108
116
|
return new Error(`Client development command ended on ${exit.signal}`);
|
|
109
117
|
return new Error(`Client development command exited with ${exit.code ?? 0}`);
|
|
110
118
|
}
|
|
111
|
-
async function
|
|
119
|
+
async function inspect(url, remaining) {
|
|
112
120
|
try {
|
|
113
|
-
const response = await fetch(url, {
|
|
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();
|
|
114
126
|
await response.body?.cancel();
|
|
115
|
-
return
|
|
127
|
+
return allowedOrigin === "*" || allowedOrigin === clientOrigin ? "ready" : "cors-blocked";
|
|
116
128
|
}
|
|
117
129
|
catch {
|
|
118
|
-
return
|
|
130
|
+
return "unavailable";
|
|
119
131
|
}
|
|
120
132
|
}
|
|
121
133
|
function terminate(child, signal) {
|
package/dist/init.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {} from "@phreshos/core";
|
|
2
2
|
import { configFile, readManifest } from "./project.js";
|
|
3
|
-
import { dim, heading, line } from "./style.js";
|
|
3
|
+
import { bold, dim, heading, line } from "./style.js";
|
|
4
4
|
import ensureProjectDependency, { projectScript } from "./project-dependency.js";
|
|
5
5
|
import { createInterface } from "node:readline/promises";
|
|
6
6
|
import { existsSync, writeFileSync } from "node:fs";
|
|
@@ -21,21 +21,23 @@ export default async function init(options = {}, directory = process.cwd(), core
|
|
|
21
21
|
if (existsSync(path) && options.force !== true && !interactive)
|
|
22
22
|
throw new Error(`${configFile} already exists — use --force to replace it`);
|
|
23
23
|
const readline = interactive ? createInterface({ input: process.stdin, output: process.stdout }) : null;
|
|
24
|
-
async function ask(question, fallback) {
|
|
24
|
+
async function ask(explanation, question, fallback) {
|
|
25
25
|
if (!readline)
|
|
26
26
|
throw new Error(`${question} Supply the corresponding option when no terminal is attached`);
|
|
27
27
|
const suffix = fallback === undefined ? "" : ` ${dim(`(${fallback})`)}`;
|
|
28
|
-
|
|
28
|
+
console.log(` ${dim(explanation)}`);
|
|
29
|
+
const said = (await readline.question(` ${bold(question)}${suffix} `)).trim();
|
|
30
|
+
console.log("");
|
|
29
31
|
return said || fallback || "";
|
|
30
32
|
}
|
|
31
|
-
async function yes(question, fallback) {
|
|
32
|
-
return (await ask(question, fallback ? "Y/n" : "y/N")).toLowerCase().startsWith("y");
|
|
33
|
+
async function yes(explanation, question, fallback) {
|
|
34
|
+
return (await ask(explanation, question, fallback ? "Y/n" : "y/N")).toLowerCase().startsWith("y");
|
|
33
35
|
}
|
|
34
36
|
try {
|
|
35
37
|
if (existsSync(path) && options.force !== true) {
|
|
36
38
|
heading(configFile, "already initialized");
|
|
37
|
-
if (!await yes("Replace it?", false)) {
|
|
38
|
-
console.log(
|
|
39
|
+
if (!await yes("The existing configuration must be replaced before this project can be initialized again.", "Replace it?", false)) {
|
|
40
|
+
console.log(` ${dim("No changes made.")}\n`);
|
|
39
41
|
return;
|
|
40
42
|
}
|
|
41
43
|
}
|
|
@@ -51,35 +53,36 @@ export default async function init(options = {}, directory = process.cwd(), core
|
|
|
51
53
|
if (!explicitShape) {
|
|
52
54
|
if (!interactive)
|
|
53
55
|
throw new Error("Choose at least one half with --server, --client, or both");
|
|
54
|
-
serverSelected = await yes("
|
|
55
|
-
clientSelected = !serverSelected || await yes("
|
|
56
|
+
serverSelected = await yes("A Server runs on the host machine and can keep working without an open desktop.", "Does this Program have a Server?", true);
|
|
57
|
+
clientSelected = !serverSelected || await yes("A Client runs through a desktop and provides the Program's visual interface.", "Does this Program have a Client?", true);
|
|
56
58
|
}
|
|
57
59
|
if (!serverSelected && !clientSelected)
|
|
58
60
|
throw new Error("A Program must have a server half, a client half, or both");
|
|
59
|
-
const name = options.name ?? (interactive ? await ask("What name should people see?", manifest.name) : undefined);
|
|
61
|
+
const name = options.name ?? (interactive ? await ask("This name is shown to people; the package name remains the Program identity.", "What name should people see?", manifest.name) : undefined);
|
|
60
62
|
if (name !== undefined && name.trim().length === 0)
|
|
61
63
|
throw new Error("--name must not be empty");
|
|
62
64
|
let apiDocs = options.apiDocs;
|
|
63
65
|
if (interactive && apiDocs === undefined) {
|
|
64
66
|
const suggested = ["api-docs.md", "README.md"].find(file => existsSync(resolve(directory, file)));
|
|
65
|
-
if (await yes("
|
|
66
|
-
apiDocs = await ask("Where is
|
|
67
|
+
if (await yes("API documentation explains only the services this Program itself provides.", "Does this Program provide API documentation?", Boolean(suggested))) {
|
|
68
|
+
apiDocs = await ask("The path is resolved from the project root and becomes the official API entry point.", "Where is the API documentation file?", suggested);
|
|
69
|
+
}
|
|
67
70
|
}
|
|
68
71
|
if (apiDocs !== undefined && apiDocs.trim().length === 0)
|
|
69
72
|
throw new Error("An API documentation path must not be empty");
|
|
70
73
|
let buildCommand = options.buildCommand;
|
|
71
74
|
if (interactive && buildCommand === undefined) {
|
|
72
75
|
const suggested = manifest.scripts?.build && projectScript(directory, manifest.packageManager, "build");
|
|
73
|
-
const builds = await yes("
|
|
76
|
+
const builds = await yes("Production operations need the Program's built files.", "Build before start, install, and pack?", Boolean(suggested));
|
|
74
77
|
if (builds)
|
|
75
|
-
buildCommand = await ask("
|
|
78
|
+
buildCommand = await ask("This command produces the production Server and Client files.", "What command builds the Program?", suggested || undefined);
|
|
76
79
|
}
|
|
77
80
|
if (buildCommand !== undefined && buildCommand.trim().length === 0)
|
|
78
81
|
throw new Error("A build command must not be empty");
|
|
79
82
|
let server;
|
|
80
83
|
if (serverSelected) {
|
|
81
|
-
const location = options.serverLocation ?? (interactive ? await ask("Where are the production
|
|
82
|
-
const startCommand = options.serverStartCommand ?? (interactive ? await ask("What starts the production
|
|
84
|
+
const location = options.serverLocation ?? (interactive ? await ask("The system runs the production Server from this project-relative directory.", "Where are the production Server files?", "build/server") : "");
|
|
85
|
+
const startCommand = options.serverStartCommand ?? (interactive ? await ask("This command runs inside the production Server directory.", "What command starts the production Server?", "node main.js") : "");
|
|
83
86
|
if (!location)
|
|
84
87
|
throw new Error("--server-location is required without a terminal");
|
|
85
88
|
if (!startCommand)
|
|
@@ -87,8 +90,9 @@ export default async function init(options = {}, directory = process.cwd(), core
|
|
|
87
90
|
let development = options.serverDevelopmentStartCommand;
|
|
88
91
|
if (interactive && development === undefined) {
|
|
89
92
|
const suggested = manifest.scripts?.dev && projectScript(directory, manifest.packageManager, "dev");
|
|
90
|
-
if (await yes("
|
|
91
|
-
development = await ask("What starts the development
|
|
93
|
+
if (await yes("Development mode can run the Server directly from the project source.", "Run the Server from source during development?", Boolean(suggested && !clientSelected))) {
|
|
94
|
+
development = await ask("This command remains attached to the phresh dev session.", "What command starts the development Server?", suggested || undefined);
|
|
95
|
+
}
|
|
92
96
|
}
|
|
93
97
|
if (development !== undefined && development.trim().length === 0)
|
|
94
98
|
throw new Error("A server development command must not be empty");
|
|
@@ -96,17 +100,18 @@ export default async function init(options = {}, directory = process.cwd(), core
|
|
|
96
100
|
}
|
|
97
101
|
let client;
|
|
98
102
|
if (clientSelected) {
|
|
99
|
-
const location = options.clientLocation ?? (interactive ? await ask("Where are the production
|
|
103
|
+
const location = options.clientLocation ?? (interactive ? await ask("The system serves the production Client from this project-relative directory.", "Where are the production Client files?", "dist") : "");
|
|
100
104
|
if (!location)
|
|
101
105
|
throw new Error("--client-location is required without a terminal");
|
|
102
106
|
let developmentUrl = options.clientDevelopmentUrl;
|
|
103
107
|
let developmentStartCommand = options.clientDevelopmentStartCommand;
|
|
104
108
|
if (interactive && developmentUrl === undefined && developmentStartCommand === undefined) {
|
|
105
109
|
const suggested = manifest.scripts?.dev && projectScript(directory, manifest.packageManager, "dev");
|
|
106
|
-
if (await yes("Use a
|
|
107
|
-
developmentUrl = await ask("
|
|
108
|
-
if (await yes("
|
|
109
|
-
developmentStartCommand = await ask("What starts
|
|
110
|
+
if (await yes("A development server can provide live updates instead of built Client files.", "Use a Client development server?", Boolean(suggested))) {
|
|
111
|
+
developmentUrl = await ask("The desktop opens this exact HTTP or HTTPS address during phresh dev.", "What URL serves the development Client?", "http://localhost:5173/");
|
|
112
|
+
if (await yes("The CLI can own the development server and stop it when the session ends.", "Should phresh dev start the Client server?", Boolean(suggested))) {
|
|
113
|
+
developmentStartCommand = await ask("This command remains attached to the phresh dev session.", "What command starts the Client development server?", suggested || undefined);
|
|
114
|
+
}
|
|
110
115
|
}
|
|
111
116
|
}
|
|
112
117
|
if (developmentStartCommand !== undefined && developmentUrl === undefined)
|
|
@@ -142,7 +147,16 @@ export default async function init(options = {}, directory = process.cwd(), core
|
|
|
142
147
|
line("client", `./${config.client.location}`);
|
|
143
148
|
console.log("");
|
|
144
149
|
const next = [config.server?.development || config.client?.development ? "phresh dev" : null, "phresh start", "phresh install"].filter(Boolean);
|
|
145
|
-
console.log(` ${dim("Next:")} ${next.join(` ${dim("or")} `)}
|
|
150
|
+
console.log(` ${dim("Next:")} ${next.join(` ${dim("or")} `)}`);
|
|
151
|
+
if (config.client) {
|
|
152
|
+
heading("Client requirements");
|
|
153
|
+
line("base URL", "./", "required for production assets");
|
|
154
|
+
if (config.client.development)
|
|
155
|
+
line("CORS", "enabled", "required on the development server");
|
|
156
|
+
console.log("");
|
|
157
|
+
}
|
|
158
|
+
else
|
|
159
|
+
console.log("");
|
|
146
160
|
}
|
|
147
161
|
finally {
|
|
148
162
|
readline?.close();
|
package/dist/launch.js
CHANGED
|
@@ -81,7 +81,7 @@ export default async function launch(which, directory = process.cwd(), options =
|
|
|
81
81
|
if (Object.keys(options).length)
|
|
82
82
|
line("options", Object.entries(options).map(([name, value]) => `${name}=${value}`).join(" "));
|
|
83
83
|
const attachment = attach(program, options, {
|
|
84
|
-
started: identity => console.log(
|
|
84
|
+
started: identity => console.log(`${clientDevelopment ? "\n" : ""} ${dim("running as")} ${identity}\n`),
|
|
85
85
|
output: (stream, text) => (stream === "err" ? process.stderr : process.stdout).write(text)
|
|
86
86
|
}, undefined, controller.signal);
|
|
87
87
|
const ended = clientDevelopment ? await Promise.race([
|
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.1",
|
|
5
5
|
"description": "The Phresh command-line interface for Program projects and system management.",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"build": "tsc --noEmit false --outDir dist --rootDir source --rewriteRelativeImportExtensions true --allowImportingTsExtensions true",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"README.md"
|
|
16
16
|
],
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@phreshos/core": "
|
|
18
|
+
"@phreshos/core": "workspace:*",
|
|
19
19
|
"adm-zip": "^0.6.0"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|