@phreshos/cli 0.1.3 → 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
@@ -14,6 +14,7 @@ Its Program commands depend on `@phreshos/core`, and its runtime operations
14
14
  require a compatible system installation.
15
15
 
16
16
  ```bash
17
+ phresh create app # create a complete new Program
17
18
  phresh init # describe this program, once
18
19
  phresh pack # run the optional build, then package its result
19
20
  phresh install # lay this program out on this machine
@@ -33,6 +34,33 @@ and has no word for a process, a window, a store or a setting. The system's
33
34
  local intake accepts exactly the Program this project declares, and so does
34
35
  this tool.
35
36
 
37
+ ## create
38
+
39
+ `phresh create <directory>` creates a complete Server and Client Program from
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
+
46
+ The directory name becomes the stable kebab-case Program identity. In a
47
+ terminal, `create` asks for the directory when it is omitted, the readable
48
+ Program name, and the package manager. With no terminal, the directory is the
49
+ first argument and every optional choice is named:
50
+
51
+ ```bash
52
+ phresh create status-board \
53
+ --name "Status Board" \
54
+ --package-manager npm
55
+ ```
56
+
57
+ Dependencies are installed by default. `--no-install` creates the same valid
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
+
36
64
  ## Saying something to a program you start
37
65
 
38
66
  ```bash
@@ -89,7 +117,7 @@ export default defineConfig({
89
117
 
90
118
  apiDocs: "api.md",
91
119
 
92
- icons: "icons",
120
+ icon: "icon.png",
93
121
 
94
122
  buildCommand: "bun run build",
95
123
 
@@ -184,7 +212,9 @@ In a terminal, `init` asks for the production locations and commands needed by
184
212
  `start` and `install`, including whether a package build should prepare those
185
213
  locations. It then offers the development command and URL needed by `dev`.
186
214
  Existing `build` and `dev` package scripts become editable suggestions, never
187
- 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.
188
218
 
189
219
  Outside a terminal it never waits for input; the same values are supplied as
190
220
  named options:
@@ -197,7 +227,7 @@ phresh init --client \
197
227
  --client-development-start-command "bun run dev"
198
228
 
199
229
  phresh init --server \
200
- --server-location build/server \
230
+ --server-location dist \
201
231
  --server-start-command "node main.js"
202
232
  ```
203
233
 
@@ -213,7 +243,7 @@ The final `Next` line is derived from the resulting config. It always shows
213
243
  one Endpoint received a development declaration.
214
244
 
215
245
  Visual and advanced runtime defaults remain for the author to add deliberately:
216
- `icons`, `size`, `position`, `installCommand`, `start`, layers, and minimization
246
+ `icon`, `size`, `position`, `installCommand`, `start`, layers, and minimization
217
247
  are not guessed. An omitted `start` is `true`; only a default-off Endpoint needs to
218
248
  say `start: false`.
219
249
 
@@ -221,9 +251,12 @@ say `start: false`.
221
251
 
222
252
  Both **run your program without installing it, and stay attached.** They
223
253
  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.
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.
227
260
 
228
261
  **The connection is the tether, in both directions.** Ctrl-C and your
229
262
  program stops. Close the window it opened and the command returns, with
@@ -339,8 +372,8 @@ register a subscription. Those are system contracts documented once by the
339
372
  SDKs; `api-docs.md` supplies only the Program-specific meaning carried through
340
373
  them.
341
374
 
342
- There is **no wrapping directory**: `program.json`, `server/`, `client/`
343
- `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
344
377
  directory it installs into from your program's `identity`.
345
378
 
346
379
  Nothing about the system moves because this exists. `program.json` is
@@ -0,0 +1,90 @@
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`;
13
+ const output = resolve(import.meta.dirname, "template");
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"]);
17
+ const excludedFiles = new Set([
18
+ ".DS_Store",
19
+ "bun.lock",
20
+ "bun.lockb",
21
+ "package-lock.json",
22
+ "pnpm-lock.yaml",
23
+ "yarn.lock"
24
+ ]);
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];
38
+ rmSync(output, { recursive: true, force: true });
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))
45
+ continue;
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());
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;
69
+ writeFileSync(manifestPath, JSON.stringify(manifest, null, 4) + "\n");
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("/");
80
+ if (parts.some(part => excludedDirectories.has(part)))
81
+ return false;
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");
87
+ }
88
+ function select(source, names) {
89
+ return Object.fromEntries(names.flatMap(name => source?.[name] === undefined ? [] : [[name, source[name]]]));
90
+ }
package/dist/cli.js CHANGED
@@ -1,6 +1,8 @@
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";
5
+ import create from "./create.js";
4
6
  import install from "./install.js";
5
7
  import launch from "./launch.js";
6
8
  import init from "./init.js";
@@ -8,214 +10,144 @@ import pack from "./pack.js";
8
10
  import uninstall from "./uninstall.js";
9
11
  const { version } = metadata;
10
12
  const coreRange = metadata.dependencies["@phreshos/core"];
11
- /**
12
- * One command system.
13
- *
14
- * Every command declares its options beside its help and all of them pass
15
- * through the same parser, error presentation, and exit rules. Interactive
16
- * behavior belongs to the command that needs it; parsing never guesses.
17
- */
18
- const commands = [
19
- {
20
- name: "init",
21
- summary: "initialize an existing Program project",
22
- detail: [
23
- "Reads identity, version, and description from package.json, then",
24
- "writes phresh.config.ts. In a terminal it asks only for the Program",
25
- "shape and values package.json cannot provide.",
26
- "",
27
- "Without a terminal, declare at least one half with named options.",
28
- "No prompt is opened and no input is awaited."
29
- ],
30
- options: [
31
- { name: "name", value: "name", summary: "human-readable Program name" },
32
- { name: "api-docs", value: "path", summary: "official Program API documentation" },
33
- { name: "build-command", value: "command", summary: "prepare production files before use" },
34
- { name: "server", summary: "include a server half" },
35
- { name: "server-location", value: "path", summary: "production server directory" },
36
- { name: "server-start-command", value: "command", summary: "production server command" },
37
- { name: "server-development-start-command", value: "command", summary: "development server command" },
38
- { name: "client", summary: "include a client half" },
39
- { name: "client-location", value: "path", summary: "production client directory" },
40
- { name: "client-development-url", value: "url", summary: "development client URL" },
41
- { name: "client-development-start-command", value: "command", summary: "client development command" },
42
- { name: "force", summary: "replace an existing phresh.config.ts" }
43
- ],
44
- run: options => init({
45
- name: text(options, "name"),
46
- apiDocs: text(options, "api-docs"),
47
- buildCommand: text(options, "build-command"),
48
- server: options.server === true || text(options, "server-location") !== undefined || text(options, "server-start-command") !== undefined,
49
- serverLocation: text(options, "server-location"),
50
- serverStartCommand: text(options, "server-start-command"),
51
- serverDevelopmentStartCommand: text(options, "server-development-start-command"),
52
- client: options.client === true || text(options, "client-location") !== undefined,
53
- clientLocation: text(options, "client-location"),
54
- clientDevelopmentUrl: text(options, "client-development-url"),
55
- clientDevelopmentStartCommand: text(options, "client-development-start-command"),
56
- force: options.force === true
57
- }, process.cwd(), coreRange)
58
- },
59
- {
60
- name: "pack",
61
- summary: "build and package this Program",
62
- detail: [
63
- "Runs an optional buildCommand, then assembles each half from where",
64
- "the config says the production files are.",
65
- "",
66
- "A package carries a Program to another machine. Installing this",
67
- "project takes no package because its description already names the",
68
- "files on this machine."
69
- ],
70
- run: () => pack()
71
- },
72
- {
73
- name: "install",
74
- summary: "install this Program",
75
- detail: [
76
- "Builds and lays out the Program declared by this project.",
77
- "",
78
- "Running processes end before installed paths change. Program data is",
79
- "preserved."
80
- ],
81
- run: () => install()
82
- },
83
- {
84
- name: "uninstall",
85
- summary: "uninstall this Program",
86
- detail: [
87
- "Removes the installed Program files. Its running processes, stored",
88
- "data, and runtime Program remain available.",
89
- "",
90
- "--everything ends its processes, removes everything the system owns",
91
- "for it, and forgets the runtime Program."
92
- ],
93
- options: [
94
- { name: "everything", summary: "also remove processes, data, and runtime state" }
95
- ],
96
- run: options => uninstall(options.everything === true)
97
- },
98
- {
99
- name: "start",
100
- summary: "run the production Program without installing",
101
- detail: [
102
- "Runs this Program attached to the terminal. Its output arrives here,",
103
- "and when this command ends the system stops the Program.",
104
- "",
105
- "An optional buildCommand runs before the production Program starts.",
106
- "The Program's exit status becomes this command's exit status."
107
- ],
108
- runOptions: true,
109
- run: options => launch("production", process.cwd(), options.run)
110
- },
111
- {
112
- name: "dev",
113
- summary: "run the development Program without installing",
114
- detail: [
115
- "Runs the same attached lifecycle as start, using each half's",
116
- "development declaration.",
117
- "",
118
- "A declared client development command belongs to this session. Its",
119
- "URL must respond within 15 seconds before the Program launches."
120
- ],
121
- runOptions: true,
122
- run: options => launch("development", process.cwd(), options.run)
123
- }
124
- ];
125
13
  const runOptionPrefix = "--run-option-";
126
- const [asked = "", ...rest] = process.argv.slice(2);
127
- if (asked === "--version" || asked === "-v") {
128
- console.log(version);
129
- process.exit(0);
130
- }
131
- const wanted = commands.find(command => command.name === asked);
132
- if (!asked || asked === "--help" || asked === "-h") {
133
- usage();
134
- process.exit(0);
135
- }
136
- if (!wanted) {
137
- console.error(`\n phresh: no such command "${asked}"`);
138
- usage();
139
- process.exit(1);
140
- }
141
- if (rest.includes("--help") || rest.includes("-h")) {
142
- about(wanted);
143
- process.exit(0);
144
- }
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();
145
114
  try {
146
- await wanted.run(parse(wanted, rest));
115
+ await program.parseAsync();
147
116
  }
148
117
  catch (error) {
149
- console.error(`\n phresh: ${error instanceof Error ? error.message : String(error)}\n`);
150
- process.exit(1);
151
- }
152
- function parse(command, args) {
153
- const options = { run: {} };
154
- for (let index = 0; index < args.length; index += 1) {
155
- const argument = args[index];
156
- if (command.runOptions && argument.startsWith(runOptionPrefix)) {
157
- const said = argument.slice(runOptionPrefix.length);
158
- const at = said.indexOf("=");
159
- if (at < 1)
160
- throw new Error(`"${argument}" says no value — write ${runOptionPrefix}<name>=<value>, and end with = for an empty value`);
161
- const name = said.slice(0, at);
162
- if (Object.hasOwn(options.run, name))
163
- throw new Error(`The run option "${name}" was given more than once`);
164
- options.run[name] = said.slice(at + 1);
165
- continue;
166
- }
167
- if (!argument.startsWith("--"))
168
- throw new Error(`${command.name} takes no positional arguments, and I was given "${argument}"`);
169
- const at = argument.indexOf("=");
170
- const name = argument.slice(2, at < 0 ? undefined : at);
171
- const declared = command.options?.find(option => option.name === name);
172
- if (!declared)
173
- throw new Error(`${command.name} does not know the option "--${name}"`);
174
- if (Object.hasOwn(options, name))
175
- throw new Error(`The option "--${name}" was given more than once`);
176
- if (!declared.value) {
177
- if (at >= 0)
178
- throw new Error(`--${name} does not take a value`);
179
- options[name] = true;
180
- continue;
181
- }
182
- const value = at >= 0 ? argument.slice(at + 1) : args[++index];
183
- if (value === undefined || at < 0 && value.startsWith("--"))
184
- throw new Error(`--${name} needs <${declared.value}>`);
185
- 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;
186
123
  }
187
- return options;
188
124
  }
189
- function text(options, name) {
190
- const value = options[name];
191
- return typeof value === "string" ? value : undefined;
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);
192
133
  }
193
- function usage() {
194
- heading(`phresh ${version}`, "create and manage Programs");
195
- for (const command of commands)
196
- console.log(` ${bold(command.name.padEnd(12))}${command.summary}`);
197
- console.log("");
198
- console.log(` ${dim("phresh <command> --help".padEnd(32))}${dim("show one command in detail")}`);
199
- console.log("");
134
+ function describe(command, paragraphs) {
135
+ command.addHelpText("after", `\n${paragraphs.map(line => line ? ` ${line}` : "").join("\n")}\n`);
136
+ return command;
200
137
  }
201
- function about(command) {
202
- heading(`phresh ${command.name}${command.options?.length || command.runOptions ? " [options]" : ""}`, command.summary);
203
- for (const said of command.detail)
204
- console.log(said ? ` ${said}` : "");
205
- if (command.options?.length || command.runOptions) {
206
- console.log("");
207
- console.log(` ${bold("Options")}`);
208
- console.log("");
209
- const signatures = (command.options ?? []).map(option => `--${option.name}${option.value ? ` <${option.value}>` : ""}`);
210
- if (command.runOptions)
211
- signatures.push(runOptionPrefix + "<name>=<value>");
212
- const width = Math.max(34, ...signatures.map(signature => signature.length + 2));
213
- for (const option of command.options ?? []) {
214
- const signature = `--${option.name}${option.value ? ` <${option.value}>` : ""}`;
215
- console.log(` ${signature.padEnd(width)}${dim(option.summary)}`);
216
- }
217
- if (command.runOptions)
218
- 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);
219
151
  }
220
- console.log("");
152
+ return options;
221
153
  }