@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 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 Get Started project. Get Started is the only authored template:
41
- the CLI build copies a filtered snapshot into the package, excluding build
42
- output, storage, installed modules, lockfiles, and archives. The installed CLI
43
- therefore creates projects offline without cloning a repository or maintaining
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. When the CLI
59
- runs from this repository, generated SDK dependencies point to the sibling
60
- dev-kits; a distributed CLI keeps the published versions embedded in its
61
- template. Both choices pass through the same dependency resolver used by
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
- icons: "icons",
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 build/server \
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
- `icons`, `size`, `position`, `installCommand`, `start`, layers, and minimization
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 it listens on `~/.phreshos/intake.sock`, which only
253
- your account can open, so nothing is sent to prove anything — and then
254
- hold.
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
- `icons/`, and optional `api-docs.md` sit at the package's root, and the system names the
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
@@ -1,8 +1,19 @@
1
- import { cpSync, existsSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
2
- import { basename, relative, resolve, sep } from "node:path";
3
- const source = resolve(import.meta.dirname, "..", "..", "get-started");
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 excludedDirectories = new Set(["dist", "node_modules", "storage"]);
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
- if (!existsSync(source))
15
- throw new Error(`The Get Started source is not there: ${source}`);
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
- cpSync(source, output, { recursive: true, filter: included });
18
- renameSync(resolve(output, ".gitignore"), resolve(output, "gitignore"));
19
- const manifestPath = resolve(output, "package.json");
20
- const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
21
- const versions = workspaceVersions();
22
- for (const dependencies of [manifest.dependencies, manifest.devDependencies]) {
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
- for (const [name, range] of Object.entries(dependencies)) {
26
- if (!range.startsWith("workspace:"))
27
- continue;
28
- const version = versions.get(name);
29
- if (!version)
30
- throw new Error(`The template depends on ${name}, but no matching workspace package exists`);
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
- function included(path) {
36
- const local = relative(source, path);
37
- if (!local)
38
- return true;
39
- const parts = local.split(sep);
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
- const name = basename(path);
43
- return !excludedFiles.has(name) && !name.endsWith(".zip");
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 workspaceVersions() {
46
- const versions = new Map();
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 { bold, dim, heading } from "./style.js";
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 [asked = "", ...rest] = process.argv.slice(2);
152
- if (asked === "--version" || asked === "-v") {
153
- console.log(version);
154
- process.exit(0);
155
- }
156
- const wanted = commands.find(command => command.name === asked);
157
- if (!asked || asked === "--help" || asked === "-h") {
158
- usage();
159
- process.exit(0);
160
- }
161
- if (!wanted) {
162
- console.error(`\n phresh: no such command "${asked}"`);
163
- usage();
164
- process.exit(1);
165
- }
166
- if (rest.includes("--help") || rest.includes("-h")) {
167
- about(wanted);
168
- process.exit(0);
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 wanted.run(parse(wanted, rest));
115
+ await program.parseAsync();
172
116
  }
173
117
  catch (error) {
174
- console.error(`\n phresh: ${error instanceof Error ? error.message : String(error)}\n`);
175
- process.exit(1);
176
- }
177
- function parse(command, args) {
178
- const options = { run: {}, positionals: [] };
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 packageManager(value) {
227
- if (value === undefined || value === "bun" || value === "npm" || value === "pnpm" || value === "yarn")
228
- return value;
229
- throw new Error(`--package-manager must be bun, npm, pnpm, or yarn; received "${value}"`);
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 usage() {
232
- heading(`phresh ${version}`, "create and manage Programs");
233
- for (const command of commands)
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 about(command) {
240
- const positionals = command.positionals?.map(positional => positional.required ? `<${positional.name}>` : `[${positional.name}]`).join(" ");
241
- const signature = [`phresh ${command.name}`, positionals, command.options?.length || command.runOptions ? "[options]" : ""].filter(Boolean).join(" ");
242
- heading(signature, command.summary);
243
- for (const said of command.detail)
244
- console.log(said ? ` ${said}` : "");
245
- if (command.options?.length || command.runOptions) {
246
- console.log("");
247
- console.log(` ${bold("Options")}`);
248
- console.log("");
249
- const signatures = (command.options ?? []).map(option => `--${option.name}${option.value ? ` <${option.value}>` : ""}`);
250
- if (command.runOptions)
251
- signatures.push(runOptionPrefix + "<name>=<value>");
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
- console.log("");
152
+ return options;
261
153
  }