@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 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
+ }