@phreshos/cli 0.1.4 → 0.1.6

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.
Files changed (40) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +63 -26
  3. package/dist/build-template.js +73 -42
  4. package/dist/cli.js +135 -241
  5. package/dist/create.js +73 -80
  6. package/dist/derive.js +4 -4
  7. package/dist/init.js +113 -122
  8. package/dist/pack.js +9 -8
  9. package/dist/program-intake.js +10 -2
  10. package/dist/project-dependency.js +13 -44
  11. package/dist/project.js +2 -3
  12. package/dist/prompts.js +105 -0
  13. package/dist/style.js +9 -14
  14. package/dist/system/command.js +70 -0
  15. package/dist/system/installation.js +241 -0
  16. package/dist/system/lifecycle.js +174 -0
  17. package/dist/system/node.js +13 -0
  18. package/dist/system/paths.js +28 -0
  19. package/dist/system/process.js +23 -0
  20. package/dist/system/readiness.js +29 -0
  21. package/dist/system/release.js +86 -0
  22. package/dist/system/service/index.js +11 -0
  23. package/dist/system/service/linux.js +94 -0
  24. package/dist/system/service/macos.js +123 -0
  25. package/dist/system/types.js +0 -0
  26. package/dist/template/README.md +1 -1
  27. package/dist/template/icon.png +0 -0
  28. package/dist/template/package.json +8 -7
  29. package/dist/template/phresh.config.ts +65 -25
  30. package/dist/template/source/build.ts +3 -3
  31. package/dist/template/source/client/app.tsx +3 -8
  32. package/dist/template/source/server/main.ts +1 -1
  33. package/dist/template/vite.client.ts +22 -0
  34. package/dist/template/vite.server.ts +27 -0
  35. package/dist/template.json +5 -0
  36. package/package.json +36 -4
  37. package/dist/questions.js +0 -25
  38. package/dist/relative-value.js +0 -118
  39. package/dist/template/icons/128x128.png +0 -0
  40. package/dist/template/vite.config.ts +0 -38
package/dist/cli.js CHANGED
@@ -1,261 +1,155 @@
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";
7
8
  import init from "./init.js";
8
9
  import pack from "./pack.js";
9
10
  import uninstall from "./uninstall.js";
11
+ import systemCommands from "./system/command.js";
10
12
  const { version } = metadata;
11
13
  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
14
  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
- }
15
+ const program = new Command()
16
+ .name("phresh")
17
+ .description("Create Programs and manage PhreshOS")
18
+ .version(version, "-v, --version")
19
+ .showHelpAfterError()
20
+ .showSuggestionAfterError();
21
+ program.addHelpText("after", "\nRun phresh <command> --help for detailed command guidance.\n");
22
+ describe(program.command("create")
23
+ .description("create a new Program project")
24
+ .argument("[directory]", "directory for the new Program")
25
+ .option("--name <name>", "human-readable Program name")
26
+ .addOption(new Option("--package-manager <manager>", "package manager used to install dependencies").choices(["bun", "npm", "pnpm", "yarn"]))
27
+ .option("--no-install", "create files without installing dependencies")
28
+ .action(async function (directory, options) {
29
+ await create({
30
+ directory,
31
+ name: options.name,
32
+ packageManager: options.packageManager,
33
+ install: options.install
34
+ });
35
+ }), [
36
+ "Creates a complete Server and Client project from the maintained",
37
+ "Phresh Program template bundled with this CLI.",
38
+ "",
39
+ "In a terminal, omitted values are collected interactively. Automation",
40
+ "supplies the directory and optional choices through named options."
41
+ ]);
42
+ describe(program.command("init")
43
+ .description("initialize an existing Program project")
44
+ .option("--name <name>", "human-readable Program name")
45
+ .option("--api-docs <path>", "official Program API documentation")
46
+ .option("--build-command <command>", "prepare production files before use")
47
+ .option("--server", "include a Server endpoint")
48
+ .option("--server-location <path>", "production Server directory")
49
+ .option("--server-start-command <command>", "production Server command")
50
+ .option("--server-development-start-command <command>", "development Server command")
51
+ .option("--client", "include a Client endpoint")
52
+ .option("--client-location <path>", "production Client directory")
53
+ .option("--client-development-url <url>", "development Client URL")
54
+ .option("--client-development-start-command <command>", "development Client command")
55
+ .option("--force", "replace an existing phresh.config.ts")
56
+ .action(async function (options) {
57
+ await init({
58
+ name: options.name,
59
+ apiDocs: options.apiDocs,
60
+ buildCommand: options.buildCommand,
61
+ server: options.server === true || options.serverLocation !== undefined || options.serverStartCommand !== undefined,
62
+ serverLocation: options.serverLocation,
63
+ serverStartCommand: options.serverStartCommand,
64
+ serverDevelopmentStartCommand: options.serverDevelopmentStartCommand,
65
+ client: options.client === true || options.clientLocation !== undefined,
66
+ clientLocation: options.clientLocation,
67
+ clientDevelopmentUrl: options.clientDevelopmentUrl,
68
+ clientDevelopmentStartCommand: options.clientDevelopmentStartCommand,
69
+ force: options.force === true
70
+ }, process.cwd(), coreRange);
71
+ }), [
72
+ "Reads identity, version, and description from package.json, then writes",
73
+ "phresh.config.ts. A terminal asks only for values the project cannot",
74
+ "provide. Without a terminal, declare at least one endpoint with options."
75
+ ]);
76
+ describe(program.command("pack")
77
+ .description("build and package this Program")
78
+ .action(async function () {
79
+ await pack();
80
+ }), [
81
+ "Runs an optional buildCommand, then packages the production files",
82
+ "declared for each endpoint."
83
+ ]);
84
+ describe(program.command("install")
85
+ .description("install this Program")
86
+ .action(async function () {
87
+ await install();
88
+ }), [
89
+ "Builds and installs the Program declared by this project. Running",
90
+ "Processes end before installed paths change; Program data is preserved."
91
+ ]);
92
+ describe(program.command("uninstall")
93
+ .description("uninstall this Program")
94
+ .option("--everything", "also remove Processes, data, and runtime state")
95
+ .action(async function (options) {
96
+ await uninstall(options.everything === true);
97
+ }), [
98
+ "Removes the installed Program files while preserving its Processes, data,",
99
+ "and runtime Program. --everything removes all system-owned state."
100
+ ]);
101
+ attached("start", "run the production Program without installing", [
102
+ "Runs this Program attached to the terminal. Output arrives here, and",
103
+ "ending this command stops the Program. An optional buildCommand runs",
104
+ "before launch, and the Program's exit status becomes this command's status."
105
+ ], "production");
106
+ attached("dev", "run the development Program without installing", [
107
+ "Runs the same attached lifecycle as start using the development",
108
+ "declarations. A Client URL must respond within 15 seconds before launch."
109
+ ], "development");
110
+ systemCommands(program);
111
+ // Every command begins with the same breathing room. Keep this at the entry
112
+ // point so individual commands never need to manufacture their own opening.
113
+ console.log("");
114
+ if (process.argv.length === 2)
115
+ program.help();
170
116
  try {
171
- await wanted.run(parse(wanted, rest));
117
+ await program.parseAsync();
172
118
  }
173
119
  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;
120
+ if (error instanceof PromptCancelled)
121
+ process.exitCode = 0;
122
+ else {
123
+ console.error(`\n phresh: ${error instanceof Error ? error.message : String(error)}\n`);
124
+ process.exitCode = 1;
216
125
  }
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
126
  }
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}"`);
127
+ function attached(name, summary, detail, mode) {
128
+ describe(program.command(name)
129
+ .description(summary)
130
+ .argument("[run-options...]", "--run-option-<name>=<value> values passed to the Program")
131
+ .allowUnknownOption()
132
+ .action(async function (args) {
133
+ await launch(mode, process.cwd(), runOptions(name, args));
134
+ }), detail);
230
135
  }
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("");
136
+ function describe(command, paragraphs) {
137
+ command.addHelpText("after", `\n${paragraphs.map(line => line ? ` ${line}` : "").join("\n")}\n`);
138
+ return command;
238
139
  }
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")}`);
140
+ function runOptions(command, args) {
141
+ const options = {};
142
+ for (const argument of args) {
143
+ if (!argument.startsWith(runOptionPrefix))
144
+ throw new Error(`${command} accepts Program options only as ${runOptionPrefix}<name>=<value>; received "${argument}"`);
145
+ const said = argument.slice(runOptionPrefix.length);
146
+ const at = said.indexOf("=");
147
+ if (at < 1)
148
+ throw new Error(`"${argument}" says no value — write ${runOptionPrefix}<name>=<value>, and end with = for an empty value`);
149
+ const name = said.slice(0, at);
150
+ if (Object.hasOwn(options, name))
151
+ throw new Error(`The run option "${name}" was given more than once`);
152
+ options[name] = said.slice(at + 1);
259
153
  }
260
- console.log("");
154
+ return options;
261
155
  }
package/dist/create.js CHANGED
@@ -1,83 +1,88 @@
1
- import { projectDependency, installProjectDependencies, projectPackageManager } from "./project-dependency.js";
2
- import questions from "./questions.js";
3
- import { dim, heading, line } from "./style.js";
1
+ import { installProjectDependencies, projectPackageManager, projectScript } from "./project-dependency.js";
2
+ import prompts from "./prompts.js";
3
+ import { accent, bold } from "./style.js";
4
4
  import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
5
5
  import { basename, dirname, extname, relative, resolve } from "node:path";
6
- /** Creates a complete Program from the bundled Get Started snapshot. */
6
+ /** Creates a complete Program from the bundled Phresh Program snapshot. */
7
7
  export default async function create(options = {}, directory = process.cwd()) {
8
- const prompts = questions();
8
+ const interaction = prompts();
9
+ interaction.begin("Create Program", "Phresh Program");
10
+ const requested = options.directory ?? (interaction.interactive
11
+ ? await interaction.ask("A new directory will contain the complete Server and Client project.", "Where should the Program be created?", "my-program")
12
+ : undefined);
13
+ if (!requested)
14
+ throw new Error("create needs a <directory> when no terminal is attached");
15
+ const target = resolve(directory, requested);
16
+ const identity = basename(target);
17
+ if (!programIdentity.test(identity))
18
+ throw new Error(`The directory name must be a kebab-case Program identity; received "${identity}"`);
19
+ if (existsSync(target))
20
+ throw new Error(`The destination already exists: ${target}`);
21
+ const name = options.name ?? (interaction.interactive
22
+ ? await interaction.ask("This name is shown to people; the directory name remains the Program identity.", "What name should people see?", title(identity))
23
+ : title(identity));
24
+ if (!name.trim())
25
+ throw new Error("--name must not be empty");
26
+ const install = options.install !== false;
27
+ const detected = projectPackageManager(directory).name;
28
+ const manager = options.packageManager ?? (install && interaction.interactive
29
+ ? packageManager(await interaction.choose("The generated project remains portable; this choice installs its dependencies now.", "Which package manager should be used?", packageManagers, detected))
30
+ : detected);
31
+ const parent = dirname(target);
32
+ mkdirSync(parent, { recursive: true });
33
+ const staging = mkdtempSync(resolve(parent, `.${identity}-`));
34
+ const bundled = template();
35
+ let placed = false;
9
36
  try {
10
- const requested = options.directory ?? (prompts.interactive
11
- ? await prompts.ask("A new directory will contain the complete Server and Client project.", "Where should the Program be created?", "my-program")
12
- : undefined);
13
- if (!requested)
14
- throw new Error("create needs a <directory> when no terminal is attached");
15
- const target = resolve(directory, requested);
16
- const identity = basename(target);
17
- if (!programIdentity.test(identity))
18
- throw new Error(`The directory name must be a kebab-case Program identity; received "${identity}"`);
19
- if (existsSync(target))
20
- throw new Error(`The destination already exists: ${target}`);
21
- const name = options.name ?? (prompts.interactive
22
- ? await prompts.ask("This name is shown to people; the directory name remains the Program identity.", "What name should people see?", title(identity))
23
- : title(identity));
24
- if (!name.trim())
25
- throw new Error("--name must not be empty");
26
- const install = options.install !== false;
27
- const detected = projectPackageManager(directory).name;
28
- const manager = options.packageManager ?? (install && prompts.interactive
29
- ? packageManager(await prompts.ask("The generated project remains portable; this choice installs its dependencies now.", "Which package manager should be used?", detected))
30
- : detected);
31
- heading("Create Program", name);
32
- line("identity", identity);
33
- line("directory", target);
34
- line("template", "Get Started");
37
+ cpSync(bundled.directory, staging, { recursive: true });
38
+ renameSync(resolve(staging, "gitignore"), resolve(staging, ".gitignore"));
39
+ customize(staging, identity, name, manager);
40
+ renameSync(staging, target);
41
+ placed = true;
42
+ // A project inside this repository becomes a real workspace member
43
+ // only at its final path. Install there so package selection sees the
44
+ // same project boundary that subsequent commands will see.
35
45
  if (install)
36
- line("packages", manager);
37
- console.log("");
38
- const parent = dirname(target);
39
- mkdirSync(parent, { recursive: true });
40
- const staging = mkdtempSync(resolve(parent, `.${identity}-`));
41
- try {
42
- cpSync(template(), staging, { recursive: true });
43
- renameSync(resolve(staging, "gitignore"), resolve(staging, ".gitignore"));
44
- customize(staging, target, identity, name, manager);
45
- if (install)
46
- await installProjectDependencies(staging, manager);
47
- renameSync(staging, target);
48
- }
49
- catch (error) {
50
- rmSync(staging, { recursive: true, force: true });
51
- throw error;
52
- }
53
- heading(name, "created");
54
- line("directory", target);
55
- line("packages", install ? "installed" : "not installed");
56
- console.log("");
57
- console.log(` ${dim("Next:")} cd ${relative(directory, target) || "."}`);
58
- if (!install)
59
- console.log(` ${manager} install`);
60
- console.log(" phresh dev");
61
- console.log("");
46
+ await interaction.progress("Installing dependencies", "Dependencies installed", () => installProjectDependencies(target, manager, interaction.interactive ? "capture" : "inherit"));
47
+ }
48
+ catch (error) {
49
+ rmSync(placed ? target : staging, { recursive: true, force: true });
50
+ throw error;
62
51
  }
63
- finally {
64
- prompts.close();
52
+ interaction.finish("Done");
53
+ console.log(bold("\nOpen the project"));
54
+ console.log(accent(`cd ${relative(directory, target) || "."}`));
55
+ if (!install) {
56
+ console.log(bold("\nInstall dependencies"));
57
+ console.log(accent(installCommand(manager)));
65
58
  }
59
+ const script = projectScript(target, manager, bundled.development ? "dev" : "start");
60
+ console.log(bold(`\n${bundled.development ? "Run Development Program" : "Run Program"}`));
61
+ console.log(accent(script));
62
+ console.log(bold("\nYou can now open the project and start building your Program"));
66
63
  }
67
64
  function template() {
68
65
  const candidates = [
69
66
  resolve(import.meta.dirname, "template"),
70
67
  resolve(import.meta.dirname, "..", "dist", "template")
71
68
  ];
72
- const found = candidates.find(existsSync);
73
- if (!found)
74
- throw new Error("The CLI template has not been built — run its build command and try again");
75
- return found;
69
+ for (const directory of candidates) {
70
+ if (!existsSync(directory))
71
+ continue;
72
+ const descriptionPath = resolve(dirname(directory), "template.json");
73
+ if (!existsSync(descriptionPath))
74
+ throw new Error("The CLI template description has not been built — run its build command and try again");
75
+ const description = JSON.parse(readFileSync(descriptionPath, "utf-8"));
76
+ if (typeof description.development !== "boolean")
77
+ throw new Error("The CLI template description is invalid — run its build command and try again");
78
+ return { directory, development: description.development };
79
+ }
80
+ throw new Error("The CLI template has not been built — run its build command and try again");
76
81
  }
77
- function customize(directory, finalDirectory, identity, name, manager) {
82
+ function customize(directory, identity, name, manager) {
78
83
  for (const path of textFiles(directory)) {
79
84
  let content = readFileSync(path, "utf-8");
80
- content = content.replaceAll("get-started", identity).replaceAll("Get Started", name);
85
+ content = content.replaceAll("phresh-program", identity).replaceAll("Phresh Program", name);
81
86
  if (basename(path) === "README.md") {
82
87
  content = content
83
88
  .replaceAll("bun install", `${manager} install`)
@@ -88,15 +93,6 @@ function customize(directory, finalDirectory, identity, name, manager) {
88
93
  const manifestPath = resolve(directory, "package.json");
89
94
  const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
90
95
  manifest.name = identity;
91
- for (const dependencies of [manifest.dependencies, manifest.devDependencies]) {
92
- if (!dependencies)
93
- continue;
94
- for (const [dependency, range] of Object.entries(dependencies)) {
95
- if (!isProjectPackage(dependency))
96
- continue;
97
- dependencies[dependency] = projectDependency(dependency, range, finalDirectory).manifestSpecifier;
98
- }
99
- }
100
96
  writeFileSync(manifestPath, JSON.stringify(manifest, null, 4) + "\n");
101
97
  }
102
98
  function textFiles(directory) {
@@ -115,16 +111,13 @@ function packageManager(value) {
115
111
  return value;
116
112
  throw new Error(`The package manager must be bun, npm, pnpm, or yarn; received "${value}"`);
117
113
  }
118
- function isProjectPackage(value) {
119
- return value === "@phreshos/core"
120
- || value === "@phreshos/client"
121
- || value === "@phreshos/server"
122
- || value === "@phreshos/react"
123
- || value === "@phreshos/cli";
124
- }
125
114
  function title(identity) {
126
115
  return identity.split("-").map(word => word[0].toUpperCase() + word.slice(1)).join(" ");
127
116
  }
117
+ function installCommand(manager) {
118
+ return `${manager} install`;
119
+ }
128
120
  const programIdentity = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
129
121
  const textExtensions = new Set([".css", ".html", ".json", ".md", ".ts", ".tsx"]);
130
122
  const textNames = new Set([".gitignore", "gitignore"]);
123
+ const packageManagers = ["bun", "npm", "pnpm", "yarn"];
package/dist/derive.js CHANGED
@@ -27,14 +27,14 @@ export default function derive(config, directory, which) {
27
27
  name: config.name,
28
28
  version: config.version,
29
29
  description: config.description,
30
- // Like icons, the document stays where the author put it for an
30
+ // Like the icon, the document stays where the author put it for an
31
31
  // attached run. Installation and packaging give it its canonical
32
32
  // name; the runtime receives an absolute source path here because a
33
33
  // derived description has no file beside which to resolve it.
34
34
  apiDocs: config.apiDocs && resolve(directory, config.apiDocs),
35
- // Where they already are. Unlike pack, which stages them, this
36
- // points into the authoring tree and leaves it alone.
37
- icons: config.icons && resolve(directory, config.icons),
35
+ // Where it already is. Unlike pack, which gives it its canonical
36
+ // name, this points into the authoring tree and leaves it alone.
37
+ icon: config.icon && resolve(directory, config.icon),
38
38
  // Beside the source, and said out loud. A program built from an
39
39
  // object resolves what it leaves unsaid against the *system's*
40
40
  // working directory — so silence here means a program keeps what