@phreshos/cli 0.1.0
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 +341 -0
- package/dist/attach.js +29 -0
- package/dist/build-command.js +22 -0
- package/dist/cli.js +220 -0
- package/dist/client-development.js +149 -0
- package/dist/command-environment.js +10 -0
- package/dist/derive.js +89 -0
- package/dist/init.js +201 -0
- package/dist/install.js +45 -0
- package/dist/launch.js +111 -0
- package/dist/pack.js +84 -0
- package/dist/program-intake.js +65 -0
- package/dist/project-dependency.js +83 -0
- package/dist/project.js +125 -0
- package/dist/relative-value.js +118 -0
- package/dist/style.js +27 -0
- package/dist/uninstall.js +15 -0
- package/package.json +26 -0
package/README.md
ADDED
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
# @phreshos/cli
|
|
2
|
+
|
|
3
|
+
The `phresh` command for creating, running, packaging, installing, and
|
|
4
|
+
uninstalling Programs.
|
|
5
|
+
|
|
6
|
+
## Package status
|
|
7
|
+
|
|
8
|
+
This package is one component of a larger architecture that is still under
|
|
9
|
+
active testing. The architecture's components will be released in stages as
|
|
10
|
+
their contracts and integrations are verified.
|
|
11
|
+
|
|
12
|
+
`@phreshos/cli` is not intended to be used independently of that architecture.
|
|
13
|
+
Its Program commands depend on `@phreshos/core`, and its runtime operations
|
|
14
|
+
require a compatible system installation.
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
phresh init # describe this program, once
|
|
18
|
+
phresh pack # run the optional build, then package its result
|
|
19
|
+
phresh install # lay this program out on this machine
|
|
20
|
+
phresh uninstall # remove its installed form
|
|
21
|
+
phresh start # run what your build left, and stay with it
|
|
22
|
+
phresh dev # run from source, and stay with it
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`phresh --help` lists them, `phresh <command> --help` explains one, and
|
|
26
|
+
`phresh --version` says which CLI you have. Nothing is guessed: an
|
|
27
|
+
unknown command, an unknown flag and a malformed option are each refused
|
|
28
|
+
and named.
|
|
29
|
+
|
|
30
|
+
**This tool acts on the current project, and the list ends there.** It is
|
|
31
|
+
not the machine's control panel — it accepts no arbitrary program identity
|
|
32
|
+
and has no word for a process, a window, a store or a setting. The system's
|
|
33
|
+
local intake accepts exactly the Program this project declares, and so does
|
|
34
|
+
this tool.
|
|
35
|
+
|
|
36
|
+
## Saying something to a program you start
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
phresh dev --run-option-path=/notes.md --run-option-line=42
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Read back by name, on either half:
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
const path = await current.option("path")
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
**Options are text, all of them.** An option must mean the same thing
|
|
49
|
+
however the process was started, and a command line can only hand over
|
|
50
|
+
text — a number made here would be a guess about your program's meaning
|
|
51
|
+
by the one party with no way to know it. Is `--run-option-id=007` seven,
|
|
52
|
+
or a string with two noughts in front? Only your program knows, so your
|
|
53
|
+
program decides: `Number(...)`, once, where the meaning is.
|
|
54
|
+
|
|
55
|
+
Which is what argv and the environment have always been, for the same
|
|
56
|
+
reason. The prefix is long because these share a line with the tool's
|
|
57
|
+
own flags, and a program wanting an option called `client` should not
|
|
58
|
+
have to fight the CLI for the word.
|
|
59
|
+
|
|
60
|
+
## phresh.config is not a program's configuration
|
|
61
|
+
|
|
62
|
+
A program's configuration is **derived** from it — three times, and the
|
|
63
|
+
derivations differ only in where each half is said to be. That is the
|
|
64
|
+
rule everything else here follows from.
|
|
65
|
+
|
|
66
|
+
It follows that every field which lands in a `program.json` is spelled
|
|
67
|
+
the way the contract spells it and crosses untouched: `size`, not a
|
|
68
|
+
width and a height; `startCommand`, not a command.
|
|
69
|
+
|
|
70
|
+
An optional top-level `buildCommand` is authoring metadata. `phresh start`,
|
|
71
|
+
`phresh install`, and `phresh pack` run it from this project before consuming
|
|
72
|
+
the production files. It never crosses into `program.json` or the system.
|
|
73
|
+
`phresh dev` uses the development declarations and does not build.
|
|
74
|
+
|
|
75
|
+
Everything else is yours: where each half is left.
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
import { defineConfig } from "@phreshos/core"
|
|
79
|
+
|
|
80
|
+
export default defineConfig({
|
|
81
|
+
|
|
82
|
+
identity: "file-manager", // kebab-case: the program's stable address
|
|
83
|
+
|
|
84
|
+
name: "File Manager", // what a person reads
|
|
85
|
+
|
|
86
|
+
version: "0.1.0",
|
|
87
|
+
|
|
88
|
+
description: "A file manager",
|
|
89
|
+
|
|
90
|
+
apiDocs: "api.md",
|
|
91
|
+
|
|
92
|
+
icons: "icons",
|
|
93
|
+
|
|
94
|
+
buildCommand: "bun run build",
|
|
95
|
+
|
|
96
|
+
server: {
|
|
97
|
+
|
|
98
|
+
location: "build/server",
|
|
99
|
+
|
|
100
|
+
installCommand: "npm ci",
|
|
101
|
+
|
|
102
|
+
startCommand: "node main.js",
|
|
103
|
+
|
|
104
|
+
development: {
|
|
105
|
+
|
|
106
|
+
startCommand: "tsx server/main.ts"
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
client: {
|
|
111
|
+
|
|
112
|
+
location: "dist",
|
|
113
|
+
|
|
114
|
+
size: { width: "1/2", height: 440 },
|
|
115
|
+
|
|
116
|
+
position: { x: 60, y: 40 },
|
|
117
|
+
|
|
118
|
+
development: {
|
|
119
|
+
|
|
120
|
+
url: "http://localhost:5173",
|
|
121
|
+
|
|
122
|
+
startCommand: "bun run dev"
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
})
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
**`identity` identifies; `name` is read.** The identity is kebab-case
|
|
129
|
+
because it is also the directory the system lays your program out in, so
|
|
130
|
+
it is a path component before anything else. The name is free-form,
|
|
131
|
+
identifies nothing, and absent means the identity serves for both.
|
|
132
|
+
|
|
133
|
+
`identity`, `version` and `description` begin from your `package.json`
|
|
134
|
+
during `init`; the readable `name` is asked for. They are written into
|
|
135
|
+
the config rather than read from the manifest later. `pack` says so if
|
|
136
|
+
the two versions have drifted apart.
|
|
137
|
+
|
|
138
|
+
A window's `size` and `position` are finite pixel numbers or linear
|
|
139
|
+
expressions. Fractions and percentages are equivalent relative terms, so
|
|
140
|
+
`"1/2"` and `"50%"` mean the same thing; pixel offsets may be combined with
|
|
141
|
+
them, as in `"50% + 10"`. Every value survives derivation unchanged.
|
|
142
|
+
|
|
143
|
+
## development — what `phresh dev` needs
|
|
144
|
+
|
|
145
|
+
`phresh init` offers to configure development for each declared half. It uses
|
|
146
|
+
the project's `dev` script as a suggested command when one exists, but records
|
|
147
|
+
nothing unless the author chooses it. If development is left unconfigured,
|
|
148
|
+
`phresh dev` refuses and names the declarations it needs:
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
Nothing here says how this program is developed.
|
|
152
|
+
|
|
153
|
+
Say how the server runs or where the client is served:
|
|
154
|
+
|
|
155
|
+
server: { …, development: { startCommand: "tsx source/server/main.ts" } }
|
|
156
|
+
client: { …, development: { url: "http://localhost:5173", startCommand: "bun run dev" } }
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Each half may carry a `development` block, but the two shapes are deliberately
|
|
160
|
+
different. A server block requires `startCommand`; `phresh dev` runs it from the
|
|
161
|
+
directory containing `phresh.config.ts`, and that directory becomes the derived
|
|
162
|
+
server location. A client block requires an HTTP(S) `url`; development clients
|
|
163
|
+
are never resolved from filesystem paths.
|
|
164
|
+
|
|
165
|
+
The client development shape may also declare `startCommand`. `phresh dev`
|
|
166
|
+
runs it from the project directory as a foreground development tool; the
|
|
167
|
+
command is never derived into the Program sent to the system. The tool and
|
|
168
|
+
the attached Program share one lifetime, so ending either ends the other.
|
|
169
|
+
|
|
170
|
+
Before launching the Program, `phresh dev` waits up to 15 seconds for the client
|
|
171
|
+
development URL to respond. While it remains unavailable, the URL is printed
|
|
172
|
+
every two seconds. A command that exits first is reported immediately. This
|
|
173
|
+
means the window is never deliberately opened onto a client that the authoring
|
|
174
|
+
tool already knows is unavailable.
|
|
175
|
+
|
|
176
|
+
## init
|
|
177
|
+
|
|
178
|
+
`phresh init` turns an existing package into a Program project. It reads the
|
|
179
|
+
identity, version, and description from `package.json`, ensures the project has
|
|
180
|
+
the matching `@phreshos/core` development dependency, and writes the typed
|
|
181
|
+
`phresh.config.ts` authoring description.
|
|
182
|
+
|
|
183
|
+
In a terminal, `init` asks for the production locations and commands needed by
|
|
184
|
+
`start` and `install`, including whether a package build should prepare those
|
|
185
|
+
locations. It then offers the development command and URL needed by `dev`.
|
|
186
|
+
Existing `build` and `dev` package scripts become editable suggestions, never
|
|
187
|
+
silent assumptions.
|
|
188
|
+
|
|
189
|
+
Outside a terminal it never waits for input; the same values are supplied as
|
|
190
|
+
named options:
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
phresh init --client \
|
|
194
|
+
--client-location dist \
|
|
195
|
+
--build-command "bun run build" \
|
|
196
|
+
--client-development-url http://localhost:5173 \
|
|
197
|
+
--client-development-start-command "bun run dev"
|
|
198
|
+
|
|
199
|
+
phresh init --server \
|
|
200
|
+
--server-location build/server \
|
|
201
|
+
--server-start-command "node main.js"
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Use `phresh init --help` for the complete option list. An existing config is
|
|
205
|
+
never replaced silently: a terminal asks, while automation must say `--force`.
|
|
206
|
+
|
|
207
|
+
A program must have a server half, a client half, or both. Neither is
|
|
208
|
+
refused during the interview rather than at the border, which is the
|
|
209
|
+
earliest place it can be refused.
|
|
210
|
+
|
|
211
|
+
The final `Next` line is derived from the resulting config. It always shows
|
|
212
|
+
`phresh start` and `phresh install`; it shows `phresh dev` only when at least
|
|
213
|
+
one half received a development declaration.
|
|
214
|
+
|
|
215
|
+
Visual and advanced runtime defaults remain for the author to add deliberately:
|
|
216
|
+
`icons`, `size`, `position`, `installCommand`, `start`, layers, and minimization
|
|
217
|
+
are not guessed. An omitted `start` is `true`; only a default-off half needs to
|
|
218
|
+
say `start: false`.
|
|
219
|
+
|
|
220
|
+
## start and dev
|
|
221
|
+
|
|
222
|
+
Both **run your program without installing it, and stay attached.** They
|
|
223
|
+
print the `program.json` it will be declared as, hand that to the system
|
|
224
|
+
through the socket it listens on — `~/.phreshos/intake.sock`, which only
|
|
225
|
+
your account can open, so nothing is sent to prove anything — and then
|
|
226
|
+
hold.
|
|
227
|
+
|
|
228
|
+
**The connection is the tether, in both directions.** Ctrl-C and your
|
|
229
|
+
program stops. Close the window it opened and the command returns, with
|
|
230
|
+
your program's own exit status as its own. Its `stdout` and `stderr`
|
|
231
|
+
arrive in your terminal as well as its system log. The system always
|
|
232
|
+
drains a server process; attachment adds the terminal as an audience
|
|
233
|
+
rather than changing how the process starts.
|
|
234
|
+
|
|
235
|
+
Nothing has to promise to clean up, which is the point — a promise would
|
|
236
|
+
not survive `kill -9`, a closed terminal, or a dropped ssh session. All
|
|
237
|
+
three end the command without running a line of it, and all three still
|
|
238
|
+
close the socket, which is what the system is watching.
|
|
239
|
+
|
|
240
|
+
**Attached means not installed; installed means persistent.** A program
|
|
241
|
+
meant to outlive your terminal is installed rather than run.
|
|
242
|
+
|
|
243
|
+
The run is registered as an ordinary uninstalled Program under the identity
|
|
244
|
+
declared by this project. Before registration, the system ends and forgets any
|
|
245
|
+
runtime Program already using that identity, whether it was installed or
|
|
246
|
+
uninstalled. Forgetting never uninstalls: installed files and storage remain
|
|
247
|
+
untouched while the attached Program becomes the sole runtime occupant. Its
|
|
248
|
+
root process tethers the whole Program to this command; when it exits, remaining
|
|
249
|
+
processes end and the runtime record disappears. A later `phresh install` can
|
|
250
|
+
replace the preserved installed files and immediately register the identity as
|
|
251
|
+
installed again. If no system is listening, the intake says so plainly rather
|
|
252
|
+
than exposing `ENOENT`.
|
|
253
|
+
|
|
254
|
+
An attached Program still owns persistent project storage. The authoring tool
|
|
255
|
+
declares `<project>/storage` explicitly, so `start` and `dev` keep the same
|
|
256
|
+
database, store, data, cache, and logs as any other runtime form without
|
|
257
|
+
inventing a path from the system's working directory. Installation changes
|
|
258
|
+
where Program files are laid out; it does not change the logging contract.
|
|
259
|
+
|
|
260
|
+
They are one derivation over one config, differing only in where each
|
|
261
|
+
half is said to be:
|
|
262
|
+
|
|
263
|
+
| | locations from | derived form |
|
|
264
|
+
|---|---|---|
|
|
265
|
+
| `pack` | `location` | none — the system lays an installed program out |
|
|
266
|
+
| `start` | `location` | absolute |
|
|
267
|
+
| `dev` | project root for a declared server development block; `development.url` for a declared client block | server absolute, client URL |
|
|
268
|
+
|
|
269
|
+
Every derived filesystem path is absolute because relative paths resolve
|
|
270
|
+
against the `program.json` they were read from, and a derived one does not live
|
|
271
|
+
beside your source. A client development URL remains the URL the author wrote.
|
|
272
|
+
|
|
273
|
+
## install
|
|
274
|
+
|
|
275
|
+
**A program has two ways of being used: run it, or install it.**
|
|
276
|
+
|
|
277
|
+
```bash
|
|
278
|
+
phresh install # this project, laid out on this machine
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
**It takes no package, and neither does the system.** What is sent is
|
|
282
|
+
the description this directory derives, and the system copies what it
|
|
283
|
+
names into place — your program's parts are already on this disk at the
|
|
284
|
+
locations it names, so there is nothing an archive would carry that the
|
|
285
|
+
description does not already point at. `phresh pack` is for when you have
|
|
286
|
+
somewhere to send a program; installing here is a different act.
|
|
287
|
+
|
|
288
|
+
If `buildCommand` is declared, it completes successfully before anything is
|
|
289
|
+
sent to the system. Without it, install uses the production files exactly as
|
|
290
|
+
they stand.
|
|
291
|
+
|
|
292
|
+
Installing is the persistent one — laid out under `~/.phreshos/programs/<identity>`,
|
|
293
|
+
marked installed, and reconstructed after a restart. Running is the other one:
|
|
294
|
+
`phresh start` / `phresh dev` register it under its declared identity and
|
|
295
|
+
attach its whole lifetime to your terminal.
|
|
296
|
+
|
|
297
|
+
The command installs through the machine's local intake. A running
|
|
298
|
+
Program may also install itself through the server SDK; installation is not a
|
|
299
|
+
client capability.
|
|
300
|
+
|
|
301
|
+
That is also why it names **paths** rather than sending bytes: install
|
|
302
|
+
used to want an upload because the installer was a browser, which has
|
|
303
|
+
bytes and no path. You have the paths.
|
|
304
|
+
|
|
305
|
+
## uninstall
|
|
306
|
+
|
|
307
|
+
```bash
|
|
308
|
+
phresh uninstall
|
|
309
|
+
phresh uninstall --everything
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
Ordinary uninstall removes the installed Program files while preserving its
|
|
313
|
+
running Processes, stored data, and runtime Program. `--everything` explicitly
|
|
314
|
+
ends those Processes, removes everything the system owns for the Program, and
|
|
315
|
+
forgets its runtime record.
|
|
316
|
+
|
|
317
|
+
The identity comes from this project's `phresh.config.ts`; the command does not
|
|
318
|
+
accept an arbitrary Program identity.
|
|
319
|
+
|
|
320
|
+
## What pack produces
|
|
321
|
+
|
|
322
|
+
`pack` takes what is at each half's `location` and writes
|
|
323
|
+
`<identity>@<version>.zip`. Your program may leave its halves anywhere; the
|
|
324
|
+
package always keeps them in the same places, so the artifact's shape
|
|
325
|
+
belongs to the contract rather than to your project. That is why the
|
|
326
|
+
`program.json` it writes names `server` and `client` explicitly — those
|
|
327
|
+
are the canonical locations the package just created. An explicit
|
|
328
|
+
`start: false` crosses with its half; an omitted value remains omitted
|
|
329
|
+
and means `true`. At least one declared half must resolve to true.
|
|
330
|
+
|
|
331
|
+
When `apiDocs` is declared, its Markdown file is copied to `api-docs.md` and
|
|
332
|
+
the packaged description names that canonical entry point. A missing declared
|
|
333
|
+
file is an error, not an undocumented Program.
|
|
334
|
+
|
|
335
|
+
There is **no wrapping directory**: `program.json`, `server/`, `client/`
|
|
336
|
+
`icons/`, and optional `api-docs.md` sit at the package's root, and the system names the
|
|
337
|
+
directory it installs into from your program's `identity`.
|
|
338
|
+
|
|
339
|
+
Nothing about the system moves because this exists. `program.json` is
|
|
340
|
+
still the only thing the system reads, and a package assembled by hand
|
|
341
|
+
is still a package. This just means you do not have to.
|
package/dist/attach.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import speak, { socketPath } from "./program-intake.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 = socketPath, signal) {
|
|
15
|
+
let ended = null;
|
|
16
|
+
await speak({ 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
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
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
|
+
}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { bold, dim, heading } from "./style.js";
|
|
3
|
+
import metadata from "../package.json" with { type: "json" };
|
|
4
|
+
import install from "./install.js";
|
|
5
|
+
import launch from "./launch.js";
|
|
6
|
+
import init from "./init.js";
|
|
7
|
+
import pack from "./pack.js";
|
|
8
|
+
import uninstall from "./uninstall.js";
|
|
9
|
+
const { version } = metadata;
|
|
10
|
+
/**
|
|
11
|
+
* One command system.
|
|
12
|
+
*
|
|
13
|
+
* Every command declares its options beside its help and all of them pass
|
|
14
|
+
* through the same parser, error presentation, and exit rules. Interactive
|
|
15
|
+
* behavior belongs to the command that needs it; parsing never guesses.
|
|
16
|
+
*/
|
|
17
|
+
const commands = [
|
|
18
|
+
{
|
|
19
|
+
name: "init",
|
|
20
|
+
summary: "initialize an existing Program project",
|
|
21
|
+
detail: [
|
|
22
|
+
"Reads identity, version, and description from package.json, then",
|
|
23
|
+
"writes phresh.config.ts. In a terminal it asks only for the Program",
|
|
24
|
+
"shape and values package.json cannot provide.",
|
|
25
|
+
"",
|
|
26
|
+
"Without a terminal, declare at least one half with named options.",
|
|
27
|
+
"No prompt is opened and no input is awaited."
|
|
28
|
+
],
|
|
29
|
+
options: [
|
|
30
|
+
{ name: "name", value: "name", summary: "human-readable Program name" },
|
|
31
|
+
{ name: "api-docs", value: "path", summary: "official Program API documentation" },
|
|
32
|
+
{ name: "build-command", value: "command", summary: "prepare production files before use" },
|
|
33
|
+
{ name: "server", summary: "include a server half" },
|
|
34
|
+
{ name: "server-location", value: "path", summary: "production server directory" },
|
|
35
|
+
{ name: "server-start-command", value: "command", summary: "production server command" },
|
|
36
|
+
{ name: "server-development-start-command", value: "command", summary: "development server command" },
|
|
37
|
+
{ name: "client", summary: "include a client half" },
|
|
38
|
+
{ name: "client-location", value: "path", summary: "production client directory" },
|
|
39
|
+
{ name: "client-development-url", value: "url", summary: "development client URL" },
|
|
40
|
+
{ name: "client-development-start-command", value: "command", summary: "client development command" },
|
|
41
|
+
{ name: "force", summary: "replace an existing phresh.config.ts" }
|
|
42
|
+
],
|
|
43
|
+
run: options => init({
|
|
44
|
+
name: text(options, "name"),
|
|
45
|
+
apiDocs: text(options, "api-docs"),
|
|
46
|
+
buildCommand: text(options, "build-command"),
|
|
47
|
+
server: options.server === true || text(options, "server-location") !== undefined || text(options, "server-start-command") !== undefined,
|
|
48
|
+
serverLocation: text(options, "server-location"),
|
|
49
|
+
serverStartCommand: text(options, "server-start-command"),
|
|
50
|
+
serverDevelopmentStartCommand: text(options, "server-development-start-command"),
|
|
51
|
+
client: options.client === true || text(options, "client-location") !== undefined,
|
|
52
|
+
clientLocation: text(options, "client-location"),
|
|
53
|
+
clientDevelopmentUrl: text(options, "client-development-url"),
|
|
54
|
+
clientDevelopmentStartCommand: text(options, "client-development-start-command"),
|
|
55
|
+
force: options.force === true
|
|
56
|
+
}, process.cwd(), version)
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: "pack",
|
|
60
|
+
summary: "build and package this Program",
|
|
61
|
+
detail: [
|
|
62
|
+
"Runs an optional buildCommand, then assembles each half from where",
|
|
63
|
+
"the config says the production files are.",
|
|
64
|
+
"",
|
|
65
|
+
"A package carries a Program to another machine. Installing this",
|
|
66
|
+
"project takes no package because its description already names the",
|
|
67
|
+
"files on this machine."
|
|
68
|
+
],
|
|
69
|
+
run: () => pack()
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: "install",
|
|
73
|
+
summary: "install this Program",
|
|
74
|
+
detail: [
|
|
75
|
+
"Builds and lays out the Program declared by this project.",
|
|
76
|
+
"",
|
|
77
|
+
"Running processes end before installed paths change. Program data is",
|
|
78
|
+
"preserved."
|
|
79
|
+
],
|
|
80
|
+
run: () => install()
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: "uninstall",
|
|
84
|
+
summary: "uninstall this Program",
|
|
85
|
+
detail: [
|
|
86
|
+
"Removes the installed Program files. Its running processes, stored",
|
|
87
|
+
"data, and runtime Program remain available.",
|
|
88
|
+
"",
|
|
89
|
+
"--everything ends its processes, removes everything the system owns",
|
|
90
|
+
"for it, and forgets the runtime Program."
|
|
91
|
+
],
|
|
92
|
+
options: [
|
|
93
|
+
{ name: "everything", summary: "also remove processes, data, and runtime state" }
|
|
94
|
+
],
|
|
95
|
+
run: options => uninstall(options.everything === true)
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
name: "start",
|
|
99
|
+
summary: "run the production Program without installing",
|
|
100
|
+
detail: [
|
|
101
|
+
"Runs this Program attached to the terminal. Its output arrives here,",
|
|
102
|
+
"and when this command ends the system stops the Program.",
|
|
103
|
+
"",
|
|
104
|
+
"An optional buildCommand runs before the production Program starts.",
|
|
105
|
+
"The Program's exit status becomes this command's exit status."
|
|
106
|
+
],
|
|
107
|
+
runOptions: true,
|
|
108
|
+
run: options => launch("production", process.cwd(), options.run)
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
name: "dev",
|
|
112
|
+
summary: "run the development Program without installing",
|
|
113
|
+
detail: [
|
|
114
|
+
"Runs the same attached lifecycle as start, using each half's",
|
|
115
|
+
"development declaration.",
|
|
116
|
+
"",
|
|
117
|
+
"A declared client development command belongs to this session. Its",
|
|
118
|
+
"URL must respond within 15 seconds before the Program launches."
|
|
119
|
+
],
|
|
120
|
+
runOptions: true,
|
|
121
|
+
run: options => launch("development", process.cwd(), options.run)
|
|
122
|
+
}
|
|
123
|
+
];
|
|
124
|
+
const runOptionPrefix = "--run-option-";
|
|
125
|
+
const [asked = "", ...rest] = process.argv.slice(2);
|
|
126
|
+
if (asked === "--version" || asked === "-v") {
|
|
127
|
+
console.log(version);
|
|
128
|
+
process.exit(0);
|
|
129
|
+
}
|
|
130
|
+
const wanted = commands.find(command => command.name === asked);
|
|
131
|
+
if (!asked || asked === "--help" || asked === "-h") {
|
|
132
|
+
usage();
|
|
133
|
+
process.exit(0);
|
|
134
|
+
}
|
|
135
|
+
if (!wanted) {
|
|
136
|
+
console.error(`\n phresh: no such command "${asked}"`);
|
|
137
|
+
usage();
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
if (rest.includes("--help") || rest.includes("-h")) {
|
|
141
|
+
about(wanted);
|
|
142
|
+
process.exit(0);
|
|
143
|
+
}
|
|
144
|
+
try {
|
|
145
|
+
await wanted.run(parse(wanted, rest));
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
console.error(`\n phresh: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
149
|
+
process.exit(1);
|
|
150
|
+
}
|
|
151
|
+
function parse(command, args) {
|
|
152
|
+
const options = { run: {} };
|
|
153
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
154
|
+
const argument = args[index];
|
|
155
|
+
if (command.runOptions && argument.startsWith(runOptionPrefix)) {
|
|
156
|
+
const said = argument.slice(runOptionPrefix.length);
|
|
157
|
+
const at = said.indexOf("=");
|
|
158
|
+
if (at < 1)
|
|
159
|
+
throw new Error(`"${argument}" says no value — write ${runOptionPrefix}<name>=<value>, and end with = for an empty value`);
|
|
160
|
+
const name = said.slice(0, at);
|
|
161
|
+
if (Object.hasOwn(options.run, name))
|
|
162
|
+
throw new Error(`The run option "${name}" was given more than once`);
|
|
163
|
+
options.run[name] = said.slice(at + 1);
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (!argument.startsWith("--"))
|
|
167
|
+
throw new Error(`${command.name} takes no positional arguments, and I was given "${argument}"`);
|
|
168
|
+
const at = argument.indexOf("=");
|
|
169
|
+
const name = argument.slice(2, at < 0 ? undefined : at);
|
|
170
|
+
const declared = command.options?.find(option => option.name === name);
|
|
171
|
+
if (!declared)
|
|
172
|
+
throw new Error(`${command.name} does not know the option "--${name}"`);
|
|
173
|
+
if (Object.hasOwn(options, name))
|
|
174
|
+
throw new Error(`The option "--${name}" was given more than once`);
|
|
175
|
+
if (!declared.value) {
|
|
176
|
+
if (at >= 0)
|
|
177
|
+
throw new Error(`--${name} does not take a value`);
|
|
178
|
+
options[name] = true;
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
const value = at >= 0 ? argument.slice(at + 1) : args[++index];
|
|
182
|
+
if (value === undefined || at < 0 && value.startsWith("--"))
|
|
183
|
+
throw new Error(`--${name} needs <${declared.value}>`);
|
|
184
|
+
options[name] = value;
|
|
185
|
+
}
|
|
186
|
+
return options;
|
|
187
|
+
}
|
|
188
|
+
function text(options, name) {
|
|
189
|
+
const value = options[name];
|
|
190
|
+
return typeof value === "string" ? value : undefined;
|
|
191
|
+
}
|
|
192
|
+
function usage() {
|
|
193
|
+
heading(`phresh ${version}`, "create and manage Programs");
|
|
194
|
+
for (const command of commands)
|
|
195
|
+
console.log(` ${bold(command.name.padEnd(12))}${command.summary}`);
|
|
196
|
+
console.log("");
|
|
197
|
+
console.log(` ${dim("phresh <command> --help".padEnd(32))}${dim("show one command in detail")}`);
|
|
198
|
+
console.log("");
|
|
199
|
+
}
|
|
200
|
+
function about(command) {
|
|
201
|
+
heading(`phresh ${command.name}${command.options?.length || command.runOptions ? " [options]" : ""}`, command.summary);
|
|
202
|
+
for (const said of command.detail)
|
|
203
|
+
console.log(said ? ` ${said}` : "");
|
|
204
|
+
if (command.options?.length || command.runOptions) {
|
|
205
|
+
console.log("");
|
|
206
|
+
console.log(` ${bold("Options")}`);
|
|
207
|
+
console.log("");
|
|
208
|
+
const signatures = (command.options ?? []).map(option => `--${option.name}${option.value ? ` <${option.value}>` : ""}`);
|
|
209
|
+
if (command.runOptions)
|
|
210
|
+
signatures.push(runOptionPrefix + "<name>=<value>");
|
|
211
|
+
const width = Math.max(34, ...signatures.map(signature => signature.length + 2));
|
|
212
|
+
for (const option of command.options ?? []) {
|
|
213
|
+
const signature = `--${option.name}${option.value ? ` <${option.value}>` : ""}`;
|
|
214
|
+
console.log(` ${signature.padEnd(width)}${dim(option.summary)}`);
|
|
215
|
+
}
|
|
216
|
+
if (command.runOptions)
|
|
217
|
+
console.log(` ${(runOptionPrefix + "<name>=<value>").padEnd(width)}${dim("pass text to the launched Program")}`);
|
|
218
|
+
}
|
|
219
|
+
console.log("");
|
|
220
|
+
}
|