@phreshos/cli 0.1.4 → 0.1.5
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 +24 -19
- package/dist/build-template.js +73 -42
- package/dist/cli.js +133 -241
- package/dist/create.js +72 -63
- package/dist/derive.js +4 -4
- package/dist/init.js +113 -122
- package/dist/pack.js +9 -8
- package/dist/program-intake.js +10 -2
- package/dist/project-dependency.js +32 -14
- package/dist/project.js +2 -3
- package/dist/prompts.js +105 -0
- package/dist/style.js +9 -14
- package/dist/template/README.md +1 -1
- package/dist/template/icon.png +0 -0
- package/dist/template/package.json +8 -7
- package/dist/template/phresh.config.ts +65 -25
- package/dist/template/source/build.ts +3 -3
- package/dist/template/source/client/app.tsx +3 -8
- package/dist/template/source/server/main.ts +1 -1
- package/dist/template/vite.client.ts +22 -0
- package/dist/template/vite.server.ts +27 -0
- package/dist/template.json +5 -0
- package/package.json +8 -4
- package/dist/questions.js +0 -25
- package/dist/relative-value.js +0 -118
- package/dist/template/icons/128x128.png +0 -0
- package/dist/template/vite.config.ts +0 -38
package/README.md
CHANGED
|
@@ -37,11 +37,11 @@ this tool.
|
|
|
37
37
|
## create
|
|
38
38
|
|
|
39
39
|
`phresh create <directory>` creates a complete Server and Client Program from
|
|
40
|
-
the maintained
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
therefore creates projects offline without
|
|
44
|
-
a second template by hand.
|
|
40
|
+
the maintained `PhreshOS/phresh-program` repository. The CLI build downloads
|
|
41
|
+
one exact tagged source release, verifies its SHA-256 checksum, removes
|
|
42
|
+
repository-only material, and bundles the resulting authoring project. The
|
|
43
|
+
installed CLI therefore creates projects offline without reading a live branch
|
|
44
|
+
or maintaining a second template by hand.
|
|
45
45
|
|
|
46
46
|
The directory name becomes the stable kebab-case Program identity. In a
|
|
47
47
|
terminal, `create` asks for the directory when it is omitted, the readable
|
|
@@ -55,11 +55,11 @@ phresh create status-board \
|
|
|
55
55
|
```
|
|
56
56
|
|
|
57
57
|
Dependencies are installed by default. `--no-install` creates the same valid
|
|
58
|
-
project and leaves installation as the first reported next step.
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
`init`.
|
|
58
|
+
project and leaves installation as the first reported next step. A project
|
|
59
|
+
created inside this repository's workspace uses its sibling dev-kits through
|
|
60
|
+
the workspace protocol. A standalone project, including one created by a
|
|
61
|
+
distributed CLI, uses the published versions embedded in the template. Both
|
|
62
|
+
choices pass through the same dependency resolver used by `init`.
|
|
63
63
|
|
|
64
64
|
## Saying something to a program you start
|
|
65
65
|
|
|
@@ -117,7 +117,7 @@ export default defineConfig({
|
|
|
117
117
|
|
|
118
118
|
apiDocs: "api.md",
|
|
119
119
|
|
|
120
|
-
|
|
120
|
+
icon: "icon.png",
|
|
121
121
|
|
|
122
122
|
buildCommand: "bun run build",
|
|
123
123
|
|
|
@@ -212,7 +212,9 @@ In a terminal, `init` asks for the production locations and commands needed by
|
|
|
212
212
|
`start` and `install`, including whether a package build should prepare those
|
|
213
213
|
locations. It then offers the development command and URL needed by `dev`.
|
|
214
214
|
Existing `build` and `dev` package scripts become editable suggestions, never
|
|
215
|
-
silent assumptions.
|
|
215
|
+
silent assumptions. A single Endpoint defaults to `dist`; when both Endpoints
|
|
216
|
+
exist, their defaults are `dist/server` and `dist/client`. API documentation is
|
|
217
|
+
opt-in and defaults to disabled even when a likely document already exists.
|
|
216
218
|
|
|
217
219
|
Outside a terminal it never waits for input; the same values are supplied as
|
|
218
220
|
named options:
|
|
@@ -225,7 +227,7 @@ phresh init --client \
|
|
|
225
227
|
--client-development-start-command "bun run dev"
|
|
226
228
|
|
|
227
229
|
phresh init --server \
|
|
228
|
-
--server-location
|
|
230
|
+
--server-location dist \
|
|
229
231
|
--server-start-command "node main.js"
|
|
230
232
|
```
|
|
231
233
|
|
|
@@ -241,7 +243,7 @@ The final `Next` line is derived from the resulting config. It always shows
|
|
|
241
243
|
one Endpoint received a development declaration.
|
|
242
244
|
|
|
243
245
|
Visual and advanced runtime defaults remain for the author to add deliberately:
|
|
244
|
-
`
|
|
246
|
+
`icon`, `size`, `position`, `installCommand`, `start`, layers, and minimization
|
|
245
247
|
are not guessed. An omitted `start` is `true`; only a default-off Endpoint needs to
|
|
246
248
|
say `start: false`.
|
|
247
249
|
|
|
@@ -249,9 +251,12 @@ say `start: false`.
|
|
|
249
251
|
|
|
250
252
|
Both **run your program without installing it, and stay attached.** They
|
|
251
253
|
print the `program.json` it will be declared as, hand that to the system
|
|
252
|
-
through the socket
|
|
253
|
-
|
|
254
|
-
|
|
254
|
+
through the socket below the selected system home. With no override, that is
|
|
255
|
+
`~/.phreshos/intake.sock`. Set `PHRESHOS_HOME` to an absolute system home to
|
|
256
|
+
address another system instance; the CLI derives
|
|
257
|
+
`<PHRESHOS_HOME>/intake.sock` from it. The socket is not selected
|
|
258
|
+
separately from its instance. Only your account can open it, so nothing is
|
|
259
|
+
sent to prove anything, and then the command holds.
|
|
255
260
|
|
|
256
261
|
**The connection is the tether, in both directions.** Ctrl-C and your
|
|
257
262
|
program stops. Close the window it opened and the command returns, with
|
|
@@ -367,8 +372,8 @@ register a subscription. Those are system contracts documented once by the
|
|
|
367
372
|
SDKs; `api-docs.md` supplies only the Program-specific meaning carried through
|
|
368
373
|
them.
|
|
369
374
|
|
|
370
|
-
There is **no wrapping directory**: `program.json`, `server/`, `client
|
|
371
|
-
`
|
|
375
|
+
There is **no wrapping directory**: `program.json`, `server/`, `client/`,
|
|
376
|
+
optional `icon.png`, and optional `api-docs.md` sit at the package's root, and the system names the
|
|
372
377
|
directory it installs into from your program's `identity`.
|
|
373
378
|
|
|
374
379
|
Nothing about the system moves because this exists. `program.json` is
|
package/dist/build-template.js
CHANGED
|
@@ -1,8 +1,19 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
|
|
1
|
+
import metadata from "../package.json" with { type: "json" };
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { dirname, resolve, sep } from "node:path";
|
|
5
|
+
import AdmZip from "adm-zip";
|
|
6
|
+
import { readConfig } from "./project.js";
|
|
7
|
+
const repository = "PhreshOS/phresh-program";
|
|
8
|
+
const release = {
|
|
9
|
+
version: "0.1.0",
|
|
10
|
+
sha256: "6e1a87a037a6ac5f82ff4c7d420757f972098387d73a8b4165aab2ef331cec36"
|
|
11
|
+
};
|
|
12
|
+
const archiveUrl = `https://github.com/${repository}/archive/refs/tags/v${release.version}.zip`;
|
|
4
13
|
const output = resolve(import.meta.dirname, "template");
|
|
5
|
-
const
|
|
14
|
+
const descriptionOutput = resolve(import.meta.dirname, "template.json");
|
|
15
|
+
const excludedDirectories = new Set([".github", "dist", "node_modules", "scripts", "storage"]);
|
|
16
|
+
const excludedRootFiles = new Set(["CONTRIBUTING.md", "LICENSE", "SECURITY.md"]);
|
|
6
17
|
const excludedFiles = new Set([
|
|
7
18
|
".DS_Store",
|
|
8
19
|
"bun.lock",
|
|
@@ -11,49 +22,69 @@ const excludedFiles = new Set([
|
|
|
11
22
|
"pnpm-lock.yaml",
|
|
12
23
|
"yarn.lock"
|
|
13
24
|
]);
|
|
14
|
-
|
|
15
|
-
|
|
25
|
+
const response = await fetch(archiveUrl);
|
|
26
|
+
if (!response.ok)
|
|
27
|
+
throw new Error(`Could not download ${repository} v${release.version}: ${response.status} ${response.statusText}`);
|
|
28
|
+
const bytes = Buffer.from(await response.arrayBuffer());
|
|
29
|
+
const digest = createHash("sha256").update(bytes).digest("hex");
|
|
30
|
+
if (digest !== release.sha256)
|
|
31
|
+
throw new Error(`The ${repository} v${release.version} source archive failed integrity verification`);
|
|
32
|
+
const archive = new AdmZip(bytes);
|
|
33
|
+
const entries = archive.getEntries();
|
|
34
|
+
const roots = new Set(entries.map(entry => entry.entryName.split("/")[0]).filter(Boolean));
|
|
35
|
+
if (roots.size !== 1)
|
|
36
|
+
throw new Error(`The ${repository} v${release.version} source archive has an invalid root`);
|
|
37
|
+
const root = [...roots][0];
|
|
16
38
|
rmSync(output, { recursive: true, force: true });
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
if (!dependencies)
|
|
39
|
+
rmSync(descriptionOutput, { force: true });
|
|
40
|
+
for (const entry of entries) {
|
|
41
|
+
if (entry.isDirectory || !entry.entryName.startsWith(`${root}/`))
|
|
42
|
+
continue;
|
|
43
|
+
const local = entry.entryName.slice(root.length + 1);
|
|
44
|
+
if (!included(local))
|
|
24
45
|
continue;
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
dependencies[name] = `^${version}`;
|
|
32
|
-
}
|
|
46
|
+
const target = local === ".gitignore" ? "gitignore" : local;
|
|
47
|
+
const path = resolve(output, target);
|
|
48
|
+
if (!path.startsWith(`${output}${sep}`))
|
|
49
|
+
throw new Error(`The ${repository} source archive contains an unsafe path: ${local}`);
|
|
50
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
51
|
+
writeFileSync(path, entry.getData());
|
|
33
52
|
}
|
|
53
|
+
const manifestPath = resolve(output, "package.json");
|
|
54
|
+
const manifestSource = archive.readFile(`${root}/package.json`);
|
|
55
|
+
if (!manifestSource)
|
|
56
|
+
throw new Error(`The ${repository} v${release.version} source archive has no package.json`);
|
|
57
|
+
const manifest = JSON.parse(manifestSource.toString("utf8"));
|
|
58
|
+
manifest.scripts = select(manifest.scripts, ["dev", "start"]);
|
|
59
|
+
manifest.devDependencies = {
|
|
60
|
+
...manifest.devDependencies,
|
|
61
|
+
"@phreshos/cli": `^${metadata.version}`
|
|
62
|
+
};
|
|
63
|
+
delete manifest.author;
|
|
64
|
+
delete manifest.license;
|
|
65
|
+
delete manifest.repository;
|
|
66
|
+
delete manifest.bugs;
|
|
67
|
+
delete manifest.homepage;
|
|
68
|
+
delete manifest.packageManager;
|
|
34
69
|
writeFileSync(manifestPath, JSON.stringify(manifest, null, 4) + "\n");
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
70
|
+
const config = await readConfig(output);
|
|
71
|
+
if (config.identity !== "phresh-program" || config.name !== "Phresh Program")
|
|
72
|
+
throw new Error(`The ${repository} release is not Phresh Program`);
|
|
73
|
+
writeFileSync(descriptionOutput, JSON.stringify({
|
|
74
|
+
repository,
|
|
75
|
+
version: release.version,
|
|
76
|
+
development: Boolean(config.server?.development || config.client?.development)
|
|
77
|
+
}, null, 4) + "\n");
|
|
78
|
+
function included(local) {
|
|
79
|
+
const parts = local.split("/");
|
|
40
80
|
if (parts.some(part => excludedDirectories.has(part)))
|
|
41
81
|
return false;
|
|
42
|
-
|
|
43
|
-
|
|
82
|
+
if (parts.length === 1 && excludedRootFiles.has(local))
|
|
83
|
+
return false;
|
|
84
|
+
if (excludedFiles.has(parts.at(-1)))
|
|
85
|
+
return false;
|
|
86
|
+
return !local.endsWith(".zip");
|
|
44
87
|
}
|
|
45
|
-
function
|
|
46
|
-
|
|
47
|
-
const devKit = resolve(source, "..");
|
|
48
|
-
for (const entry of readdirSync(devKit, { withFileTypes: true })) {
|
|
49
|
-
if (!entry.isDirectory())
|
|
50
|
-
continue;
|
|
51
|
-
const path = resolve(devKit, entry.name, "package.json");
|
|
52
|
-
if (!existsSync(path))
|
|
53
|
-
continue;
|
|
54
|
-
const manifest = JSON.parse(readFileSync(path, "utf-8"));
|
|
55
|
-
if (manifest.name && manifest.version)
|
|
56
|
-
versions.set(manifest.name, manifest.version);
|
|
57
|
-
}
|
|
58
|
-
return versions;
|
|
88
|
+
function select(source, names) {
|
|
89
|
+
return Object.fromEntries(names.flatMap(name => source?.[name] === undefined ? [] : [[name, source[name]]]));
|
|
59
90
|
}
|
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { Command, Option } from "commander";
|
|
3
3
|
import metadata from "../package.json" with { type: "json" };
|
|
4
|
+
import { PromptCancelled } from "./prompts.js";
|
|
4
5
|
import create from "./create.js";
|
|
5
6
|
import install from "./install.js";
|
|
6
7
|
import launch from "./launch.js";
|
|
@@ -9,253 +10,144 @@ import pack from "./pack.js";
|
|
|
9
10
|
import uninstall from "./uninstall.js";
|
|
10
11
|
const { version } = metadata;
|
|
11
12
|
const coreRange = metadata.dependencies["@phreshos/core"];
|
|
12
|
-
/**
|
|
13
|
-
* One command system.
|
|
14
|
-
*
|
|
15
|
-
* Every command declares its options beside its help and all of them pass
|
|
16
|
-
* through the same parser, error presentation, and exit rules. Interactive
|
|
17
|
-
* behavior belongs to the command that needs it; parsing never guesses.
|
|
18
|
-
*/
|
|
19
|
-
const commands = [
|
|
20
|
-
{
|
|
21
|
-
name: "create",
|
|
22
|
-
summary: "create a new Program project",
|
|
23
|
-
detail: [
|
|
24
|
-
"Creates a complete Server and Client project from the maintained",
|
|
25
|
-
"Get Started template bundled with this CLI.",
|
|
26
|
-
"",
|
|
27
|
-
"In a terminal, an omitted directory and the readable Program name",
|
|
28
|
-
"are collected interactively. Automation supplies the directory as",
|
|
29
|
-
"the first argument and may use named options."
|
|
30
|
-
],
|
|
31
|
-
positionals: [{ name: "directory", required: false }],
|
|
32
|
-
options: [
|
|
33
|
-
{ name: "name", value: "name", summary: "human-readable Program name" },
|
|
34
|
-
{ name: "package-manager", value: "manager", summary: "bun, npm, pnpm, or yarn" },
|
|
35
|
-
{ name: "no-install", summary: "create files without installing dependencies" }
|
|
36
|
-
],
|
|
37
|
-
run: options => create({
|
|
38
|
-
directory: options.positionals[0],
|
|
39
|
-
name: text(options, "name"),
|
|
40
|
-
packageManager: packageManager(text(options, "package-manager")),
|
|
41
|
-
install: options["no-install"] !== true
|
|
42
|
-
})
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
name: "init",
|
|
46
|
-
summary: "initialize an existing Program project",
|
|
47
|
-
detail: [
|
|
48
|
-
"Reads identity, version, and description from package.json, then",
|
|
49
|
-
"writes phresh.config.ts. In a terminal it asks only for the Program",
|
|
50
|
-
"shape and values package.json cannot provide.",
|
|
51
|
-
"",
|
|
52
|
-
"Without a terminal, declare at least one half with named options.",
|
|
53
|
-
"No prompt is opened and no input is awaited."
|
|
54
|
-
],
|
|
55
|
-
options: [
|
|
56
|
-
{ name: "name", value: "name", summary: "human-readable Program name" },
|
|
57
|
-
{ name: "api-docs", value: "path", summary: "official Program API documentation" },
|
|
58
|
-
{ name: "build-command", value: "command", summary: "prepare production files before use" },
|
|
59
|
-
{ name: "server", summary: "include a server half" },
|
|
60
|
-
{ name: "server-location", value: "path", summary: "production server directory" },
|
|
61
|
-
{ name: "server-start-command", value: "command", summary: "production server command" },
|
|
62
|
-
{ name: "server-development-start-command", value: "command", summary: "development server command" },
|
|
63
|
-
{ name: "client", summary: "include a client half" },
|
|
64
|
-
{ name: "client-location", value: "path", summary: "production client directory" },
|
|
65
|
-
{ name: "client-development-url", value: "url", summary: "development client URL" },
|
|
66
|
-
{ name: "client-development-start-command", value: "command", summary: "client development command" },
|
|
67
|
-
{ name: "force", summary: "replace an existing phresh.config.ts" }
|
|
68
|
-
],
|
|
69
|
-
run: options => init({
|
|
70
|
-
name: text(options, "name"),
|
|
71
|
-
apiDocs: text(options, "api-docs"),
|
|
72
|
-
buildCommand: text(options, "build-command"),
|
|
73
|
-
server: options.server === true || text(options, "server-location") !== undefined || text(options, "server-start-command") !== undefined,
|
|
74
|
-
serverLocation: text(options, "server-location"),
|
|
75
|
-
serverStartCommand: text(options, "server-start-command"),
|
|
76
|
-
serverDevelopmentStartCommand: text(options, "server-development-start-command"),
|
|
77
|
-
client: options.client === true || text(options, "client-location") !== undefined,
|
|
78
|
-
clientLocation: text(options, "client-location"),
|
|
79
|
-
clientDevelopmentUrl: text(options, "client-development-url"),
|
|
80
|
-
clientDevelopmentStartCommand: text(options, "client-development-start-command"),
|
|
81
|
-
force: options.force === true
|
|
82
|
-
}, process.cwd(), coreRange)
|
|
83
|
-
},
|
|
84
|
-
{
|
|
85
|
-
name: "pack",
|
|
86
|
-
summary: "build and package this Program",
|
|
87
|
-
detail: [
|
|
88
|
-
"Runs an optional buildCommand, then assembles each half from where",
|
|
89
|
-
"the config says the production files are.",
|
|
90
|
-
"",
|
|
91
|
-
"A package carries a Program to another machine. Installing this",
|
|
92
|
-
"project takes no package because its description already names the",
|
|
93
|
-
"files on this machine."
|
|
94
|
-
],
|
|
95
|
-
run: () => pack()
|
|
96
|
-
},
|
|
97
|
-
{
|
|
98
|
-
name: "install",
|
|
99
|
-
summary: "install this Program",
|
|
100
|
-
detail: [
|
|
101
|
-
"Builds and lays out the Program declared by this project.",
|
|
102
|
-
"",
|
|
103
|
-
"Running processes end before installed paths change. Program data is",
|
|
104
|
-
"preserved."
|
|
105
|
-
],
|
|
106
|
-
run: () => install()
|
|
107
|
-
},
|
|
108
|
-
{
|
|
109
|
-
name: "uninstall",
|
|
110
|
-
summary: "uninstall this Program",
|
|
111
|
-
detail: [
|
|
112
|
-
"Removes the installed Program files. Its running processes, stored",
|
|
113
|
-
"data, and runtime Program remain available.",
|
|
114
|
-
"",
|
|
115
|
-
"--everything ends its processes, removes everything the system owns",
|
|
116
|
-
"for it, and forgets the runtime Program."
|
|
117
|
-
],
|
|
118
|
-
options: [
|
|
119
|
-
{ name: "everything", summary: "also remove processes, data, and runtime state" }
|
|
120
|
-
],
|
|
121
|
-
run: options => uninstall(options.everything === true)
|
|
122
|
-
},
|
|
123
|
-
{
|
|
124
|
-
name: "start",
|
|
125
|
-
summary: "run the production Program without installing",
|
|
126
|
-
detail: [
|
|
127
|
-
"Runs this Program attached to the terminal. Its output arrives here,",
|
|
128
|
-
"and when this command ends the system stops the Program.",
|
|
129
|
-
"",
|
|
130
|
-
"An optional buildCommand runs before the production Program starts.",
|
|
131
|
-
"The Program's exit status becomes this command's exit status."
|
|
132
|
-
],
|
|
133
|
-
runOptions: true,
|
|
134
|
-
run: options => launch("production", process.cwd(), options.run)
|
|
135
|
-
},
|
|
136
|
-
{
|
|
137
|
-
name: "dev",
|
|
138
|
-
summary: "run the development Program without installing",
|
|
139
|
-
detail: [
|
|
140
|
-
"Runs the same attached lifecycle as start, using each half's",
|
|
141
|
-
"development declaration.",
|
|
142
|
-
"",
|
|
143
|
-
"A declared client development command belongs to this session. Its",
|
|
144
|
-
"URL must respond within 15 seconds before the Program launches."
|
|
145
|
-
],
|
|
146
|
-
runOptions: true,
|
|
147
|
-
run: options => launch("development", process.cwd(), options.run)
|
|
148
|
-
}
|
|
149
|
-
];
|
|
150
13
|
const runOptionPrefix = "--run-option-";
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
14
|
+
const program = new Command()
|
|
15
|
+
.name("phresh")
|
|
16
|
+
.description("Create and manage Programs")
|
|
17
|
+
.version(version, "-v, --version")
|
|
18
|
+
.showHelpAfterError()
|
|
19
|
+
.showSuggestionAfterError();
|
|
20
|
+
program.addHelpText("after", "\nRun phresh <command> --help for detailed command guidance.\n");
|
|
21
|
+
describe(program.command("create")
|
|
22
|
+
.description("create a new Program project")
|
|
23
|
+
.argument("[directory]", "directory for the new Program")
|
|
24
|
+
.option("--name <name>", "human-readable Program name")
|
|
25
|
+
.addOption(new Option("--package-manager <manager>", "package manager used to install dependencies").choices(["bun", "npm", "pnpm", "yarn"]))
|
|
26
|
+
.option("--no-install", "create files without installing dependencies")
|
|
27
|
+
.action(async function (directory, options) {
|
|
28
|
+
await create({
|
|
29
|
+
directory,
|
|
30
|
+
name: options.name,
|
|
31
|
+
packageManager: options.packageManager,
|
|
32
|
+
install: options.install
|
|
33
|
+
});
|
|
34
|
+
}), [
|
|
35
|
+
"Creates a complete Server and Client project from the maintained",
|
|
36
|
+
"Phresh Program template bundled with this CLI.",
|
|
37
|
+
"",
|
|
38
|
+
"In a terminal, omitted values are collected interactively. Automation",
|
|
39
|
+
"supplies the directory and optional choices through named options."
|
|
40
|
+
]);
|
|
41
|
+
describe(program.command("init")
|
|
42
|
+
.description("initialize an existing Program project")
|
|
43
|
+
.option("--name <name>", "human-readable Program name")
|
|
44
|
+
.option("--api-docs <path>", "official Program API documentation")
|
|
45
|
+
.option("--build-command <command>", "prepare production files before use")
|
|
46
|
+
.option("--server", "include a Server endpoint")
|
|
47
|
+
.option("--server-location <path>", "production Server directory")
|
|
48
|
+
.option("--server-start-command <command>", "production Server command")
|
|
49
|
+
.option("--server-development-start-command <command>", "development Server command")
|
|
50
|
+
.option("--client", "include a Client endpoint")
|
|
51
|
+
.option("--client-location <path>", "production Client directory")
|
|
52
|
+
.option("--client-development-url <url>", "development Client URL")
|
|
53
|
+
.option("--client-development-start-command <command>", "development Client command")
|
|
54
|
+
.option("--force", "replace an existing phresh.config.ts")
|
|
55
|
+
.action(async function (options) {
|
|
56
|
+
await init({
|
|
57
|
+
name: options.name,
|
|
58
|
+
apiDocs: options.apiDocs,
|
|
59
|
+
buildCommand: options.buildCommand,
|
|
60
|
+
server: options.server === true || options.serverLocation !== undefined || options.serverStartCommand !== undefined,
|
|
61
|
+
serverLocation: options.serverLocation,
|
|
62
|
+
serverStartCommand: options.serverStartCommand,
|
|
63
|
+
serverDevelopmentStartCommand: options.serverDevelopmentStartCommand,
|
|
64
|
+
client: options.client === true || options.clientLocation !== undefined,
|
|
65
|
+
clientLocation: options.clientLocation,
|
|
66
|
+
clientDevelopmentUrl: options.clientDevelopmentUrl,
|
|
67
|
+
clientDevelopmentStartCommand: options.clientDevelopmentStartCommand,
|
|
68
|
+
force: options.force === true
|
|
69
|
+
}, process.cwd(), coreRange);
|
|
70
|
+
}), [
|
|
71
|
+
"Reads identity, version, and description from package.json, then writes",
|
|
72
|
+
"phresh.config.ts. A terminal asks only for values the project cannot",
|
|
73
|
+
"provide. Without a terminal, declare at least one endpoint with options."
|
|
74
|
+
]);
|
|
75
|
+
describe(program.command("pack")
|
|
76
|
+
.description("build and package this Program")
|
|
77
|
+
.action(async function () {
|
|
78
|
+
await pack();
|
|
79
|
+
}), [
|
|
80
|
+
"Runs an optional buildCommand, then packages the production files",
|
|
81
|
+
"declared for each endpoint."
|
|
82
|
+
]);
|
|
83
|
+
describe(program.command("install")
|
|
84
|
+
.description("install this Program")
|
|
85
|
+
.action(async function () {
|
|
86
|
+
await install();
|
|
87
|
+
}), [
|
|
88
|
+
"Builds and installs the Program declared by this project. Running",
|
|
89
|
+
"Processes end before installed paths change; Program data is preserved."
|
|
90
|
+
]);
|
|
91
|
+
describe(program.command("uninstall")
|
|
92
|
+
.description("uninstall this Program")
|
|
93
|
+
.option("--everything", "also remove Processes, data, and runtime state")
|
|
94
|
+
.action(async function (options) {
|
|
95
|
+
await uninstall(options.everything === true);
|
|
96
|
+
}), [
|
|
97
|
+
"Removes the installed Program files while preserving its Processes, data,",
|
|
98
|
+
"and runtime Program. --everything removes all system-owned state."
|
|
99
|
+
]);
|
|
100
|
+
attached("start", "run the production Program without installing", [
|
|
101
|
+
"Runs this Program attached to the terminal. Output arrives here, and",
|
|
102
|
+
"ending this command stops the Program. An optional buildCommand runs",
|
|
103
|
+
"before launch, and the Program's exit status becomes this command's status."
|
|
104
|
+
], "production");
|
|
105
|
+
attached("dev", "run the development Program without installing", [
|
|
106
|
+
"Runs the same attached lifecycle as start using the development",
|
|
107
|
+
"declarations. A Client URL must respond within 15 seconds before launch."
|
|
108
|
+
], "development");
|
|
109
|
+
// Every command begins with the same breathing room. Keep this at the entry
|
|
110
|
+
// point so individual commands never need to manufacture their own opening.
|
|
111
|
+
console.log("");
|
|
112
|
+
if (process.argv.length === 2)
|
|
113
|
+
program.help();
|
|
170
114
|
try {
|
|
171
|
-
await
|
|
115
|
+
await program.parseAsync();
|
|
172
116
|
}
|
|
173
117
|
catch (error) {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
180
|
-
const argument = args[index];
|
|
181
|
-
if (command.runOptions && argument.startsWith(runOptionPrefix)) {
|
|
182
|
-
const said = argument.slice(runOptionPrefix.length);
|
|
183
|
-
const at = said.indexOf("=");
|
|
184
|
-
if (at < 1)
|
|
185
|
-
throw new Error(`"${argument}" says no value — write ${runOptionPrefix}<name>=<value>, and end with = for an empty value`);
|
|
186
|
-
const name = said.slice(0, at);
|
|
187
|
-
if (Object.hasOwn(options.run, name))
|
|
188
|
-
throw new Error(`The run option "${name}" was given more than once`);
|
|
189
|
-
options.run[name] = said.slice(at + 1);
|
|
190
|
-
continue;
|
|
191
|
-
}
|
|
192
|
-
if (!argument.startsWith("--")) {
|
|
193
|
-
const positional = command.positionals?.[options.positionals.length];
|
|
194
|
-
if (!positional)
|
|
195
|
-
throw new Error(`${command.name} takes no more positional arguments, and I was given "${argument}"`);
|
|
196
|
-
options.positionals.push(argument);
|
|
197
|
-
continue;
|
|
198
|
-
}
|
|
199
|
-
const at = argument.indexOf("=");
|
|
200
|
-
const name = argument.slice(2, at < 0 ? undefined : at);
|
|
201
|
-
const declared = command.options?.find(option => option.name === name);
|
|
202
|
-
if (!declared)
|
|
203
|
-
throw new Error(`${command.name} does not know the option "--${name}"`);
|
|
204
|
-
if (Object.hasOwn(options, name))
|
|
205
|
-
throw new Error(`The option "--${name}" was given more than once`);
|
|
206
|
-
if (!declared.value) {
|
|
207
|
-
if (at >= 0)
|
|
208
|
-
throw new Error(`--${name} does not take a value`);
|
|
209
|
-
options[name] = true;
|
|
210
|
-
continue;
|
|
211
|
-
}
|
|
212
|
-
const value = at >= 0 ? argument.slice(at + 1) : args[++index];
|
|
213
|
-
if (value === undefined || at < 0 && value.startsWith("--"))
|
|
214
|
-
throw new Error(`--${name} needs <${declared.value}>`);
|
|
215
|
-
options[name] = value;
|
|
118
|
+
if (error instanceof PromptCancelled)
|
|
119
|
+
process.exitCode = 0;
|
|
120
|
+
else {
|
|
121
|
+
console.error(`\n phresh: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
122
|
+
process.exitCode = 1;
|
|
216
123
|
}
|
|
217
|
-
const missing = command.positionals?.slice(options.positionals.length).find(positional => positional.required);
|
|
218
|
-
if (missing)
|
|
219
|
-
throw new Error(`${command.name} needs <${missing.name}>`);
|
|
220
|
-
return options;
|
|
221
|
-
}
|
|
222
|
-
function text(options, name) {
|
|
223
|
-
const value = options[name];
|
|
224
|
-
return typeof value === "string" ? value : undefined;
|
|
225
124
|
}
|
|
226
|
-
function
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
125
|
+
function attached(name, summary, detail, mode) {
|
|
126
|
+
describe(program.command(name)
|
|
127
|
+
.description(summary)
|
|
128
|
+
.argument("[run-options...]", "--run-option-<name>=<value> values passed to the Program")
|
|
129
|
+
.allowUnknownOption()
|
|
130
|
+
.action(async function (args) {
|
|
131
|
+
await launch(mode, process.cwd(), runOptions(name, args));
|
|
132
|
+
}), detail);
|
|
230
133
|
}
|
|
231
|
-
function
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
console.log(` ${bold(command.name.padEnd(12))}${command.summary}`);
|
|
235
|
-
console.log("");
|
|
236
|
-
console.log(` ${dim("phresh <command> --help".padEnd(32))}${dim("show one command in detail")}`);
|
|
237
|
-
console.log("");
|
|
134
|
+
function describe(command, paragraphs) {
|
|
135
|
+
command.addHelpText("after", `\n${paragraphs.map(line => line ? ` ${line}` : "").join("\n")}\n`);
|
|
136
|
+
return command;
|
|
238
137
|
}
|
|
239
|
-
function
|
|
240
|
-
const
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
const width = Math.max(34, ...signatures.map(signature => signature.length + 2));
|
|
253
|
-
for (const option of command.options ?? []) {
|
|
254
|
-
const signature = `--${option.name}${option.value ? ` <${option.value}>` : ""}`;
|
|
255
|
-
console.log(` ${signature.padEnd(width)}${dim(option.summary)}`);
|
|
256
|
-
}
|
|
257
|
-
if (command.runOptions)
|
|
258
|
-
console.log(` ${(runOptionPrefix + "<name>=<value>").padEnd(width)}${dim("pass text to the launched Program")}`);
|
|
138
|
+
function runOptions(command, args) {
|
|
139
|
+
const options = {};
|
|
140
|
+
for (const argument of args) {
|
|
141
|
+
if (!argument.startsWith(runOptionPrefix))
|
|
142
|
+
throw new Error(`${command} accepts Program options only as ${runOptionPrefix}<name>=<value>; received "${argument}"`);
|
|
143
|
+
const said = argument.slice(runOptionPrefix.length);
|
|
144
|
+
const at = said.indexOf("=");
|
|
145
|
+
if (at < 1)
|
|
146
|
+
throw new Error(`"${argument}" says no value — write ${runOptionPrefix}<name>=<value>, and end with = for an empty value`);
|
|
147
|
+
const name = said.slice(0, at);
|
|
148
|
+
if (Object.hasOwn(options, name))
|
|
149
|
+
throw new Error(`The run option "${name}" was given more than once`);
|
|
150
|
+
options[name] = said.slice(at + 1);
|
|
259
151
|
}
|
|
260
|
-
|
|
152
|
+
return options;
|
|
261
153
|
}
|