@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/dist/create.js ADDED
@@ -0,0 +1,139 @@
1
+ import { projectDependency, installProjectDependencies, projectPackageManager, projectScript } from "./project-dependency.js";
2
+ import prompts from "./prompts.js";
3
+ import { accent, bold } from "./style.js";
4
+ import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
5
+ import { basename, dirname, extname, relative, resolve } from "node:path";
6
+ /** Creates a complete Program from the bundled Phresh Program snapshot. */
7
+ export default async function create(options = {}, directory = process.cwd()) {
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;
36
+ try {
37
+ cpSync(bundled.directory, staging, { recursive: true });
38
+ renameSync(resolve(staging, "gitignore"), resolve(staging, ".gitignore"));
39
+ customize(staging, target, 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.
45
+ if (install)
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;
51
+ }
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)));
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"));
63
+ }
64
+ function template() {
65
+ const candidates = [
66
+ resolve(import.meta.dirname, "template"),
67
+ resolve(import.meta.dirname, "..", "dist", "template")
68
+ ];
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");
81
+ }
82
+ function customize(directory, finalDirectory, identity, name, manager) {
83
+ for (const path of textFiles(directory)) {
84
+ let content = readFileSync(path, "utf-8");
85
+ content = content.replaceAll("phresh-program", identity).replaceAll("Phresh Program", name);
86
+ if (basename(path) === "README.md") {
87
+ content = content
88
+ .replaceAll("bun install", `${manager} install`)
89
+ .replaceAll("bun phresh ", "phresh ");
90
+ }
91
+ writeFileSync(path, content);
92
+ }
93
+ const manifestPath = resolve(directory, "package.json");
94
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
95
+ manifest.name = identity;
96
+ for (const dependencies of [manifest.dependencies, manifest.devDependencies]) {
97
+ if (!dependencies)
98
+ continue;
99
+ for (const [dependency, range] of Object.entries(dependencies)) {
100
+ if (!isProjectPackage(dependency))
101
+ continue;
102
+ dependencies[dependency] = projectDependency(dependency, range, finalDirectory).manifestSpecifier;
103
+ }
104
+ }
105
+ writeFileSync(manifestPath, JSON.stringify(manifest, null, 4) + "\n");
106
+ }
107
+ function textFiles(directory) {
108
+ const files = [];
109
+ for (const entry of readdirSync(directory, { withFileTypes: true })) {
110
+ const path = resolve(directory, entry.name);
111
+ if (entry.isDirectory())
112
+ files.push(...textFiles(path));
113
+ else if (entry.isFile() && (textExtensions.has(extname(entry.name)) || textNames.has(entry.name)))
114
+ files.push(path);
115
+ }
116
+ return files;
117
+ }
118
+ function packageManager(value) {
119
+ if (value === "bun" || value === "npm" || value === "pnpm" || value === "yarn")
120
+ return value;
121
+ throw new Error(`The package manager must be bun, npm, pnpm, or yarn; received "${value}"`);
122
+ }
123
+ function isProjectPackage(value) {
124
+ return value === "@phreshos/core"
125
+ || value === "@phreshos/client"
126
+ || value === "@phreshos/server"
127
+ || value === "@phreshos/react"
128
+ || value === "@phreshos/cli";
129
+ }
130
+ function title(identity) {
131
+ return identity.split("-").map(word => word[0].toUpperCase() + word.slice(1)).join(" ");
132
+ }
133
+ function installCommand(manager) {
134
+ return `${manager} install`;
135
+ }
136
+ const programIdentity = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
137
+ const textExtensions = new Set([".css", ".html", ".json", ".md", ".ts", ".tsx"]);
138
+ const textNames = new Set([".gitignore", "gitignore"]);
139
+ 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
package/dist/init.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import {} from "@phreshos/core";
2
2
  import { configFile, readManifest } from "./project.js";
3
- import { bold, dim, heading, line } from "./style.js";
3
+ import { dim } from "./style.js";
4
4
  import ensureProjectDependency, { projectScript } from "./project-dependency.js";
5
- import { createInterface } from "node:readline/promises";
5
+ import prompts from "./prompts.js";
6
6
  import { existsSync, writeFileSync } from "node:fs";
7
7
  import { resolve } from "node:path";
8
8
  /**
@@ -16,151 +16,130 @@ import { resolve } from "node:path";
16
16
  export default async function init(options = {}, directory = process.cwd(), coreRange = "^0.1.0") {
17
17
  const manifest = await readManifest(directory);
18
18
  const path = resolve(directory, configFile);
19
- const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
19
+ const interaction = prompts();
20
+ const { interactive, ask, yes } = interaction;
20
21
  const explicitShape = options.server === true || options.client === true;
21
22
  if (existsSync(path) && options.force !== true && !interactive)
22
23
  throw new Error(`${configFile} already exists — use --force to replace it`);
23
- const readline = interactive ? createInterface({ input: process.stdin, output: process.stdout }) : null;
24
- async function ask(explanation, question, fallback) {
25
- if (!readline)
26
- throw new Error(`${question} Supply the corresponding option when no terminal is attached`);
27
- const suffix = fallback === undefined ? "" : ` ${dim(`(${fallback})`)}`;
28
- console.log(` ${dim(explanation)}`);
29
- const said = (await readline.question(` ${bold(question)}${suffix} `)).trim();
30
- console.log("");
31
- return said || fallback || "";
24
+ interaction.begin("Initialize Program", manifest.name);
25
+ if (existsSync(path) && options.force !== true) {
26
+ interaction.detail(configFile, "already initialized");
27
+ if (!await yes("The existing configuration must be replaced before this project can be initialized again.", "Replace it?", false)) {
28
+ interaction.finish("No changes made");
29
+ return;
30
+ }
32
31
  }
33
- async function yes(explanation, question, fallback) {
34
- return (await ask(explanation, question, fallback ? "Y/n" : "y/N")).toLowerCase().startsWith("y");
32
+ interaction.detail("identity", manifest.name, "package.json");
33
+ if (manifest.version)
34
+ interaction.detail("version", manifest.version, "package.json");
35
+ if (manifest.description)
36
+ interaction.detail("description", manifest.description, "package.json");
37
+ let serverSelected = options.server === true;
38
+ let clientSelected = options.client === true;
39
+ if (!explicitShape) {
40
+ if (!interactive)
41
+ throw new Error("Choose at least one half with --server, --client, or both");
42
+ serverSelected = await yes("A Server runs on the host machine and can keep working without an open desktop.", "Does this Program have a Server?", true);
43
+ clientSelected = !serverSelected || await yes("A Client runs through a desktop and provides the Program's visual interface.", "Does this Program have a Client?", true);
35
44
  }
36
- try {
37
- if (existsSync(path) && options.force !== true) {
38
- heading(configFile, "already initialized");
39
- if (!await yes("The existing configuration must be replaced before this project can be initialized again.", "Replace it?", false)) {
40
- console.log(` ${dim("No changes made.")}\n`);
41
- return;
42
- }
43
- }
44
- heading("Initialize Program", manifest.name);
45
- line("identity", manifest.name, "package.json");
46
- if (manifest.version)
47
- line("version", manifest.version, "package.json");
48
- if (manifest.description)
49
- line("description", manifest.description, "package.json");
50
- console.log("");
51
- let serverSelected = options.server === true;
52
- let clientSelected = options.client === true;
53
- if (!explicitShape) {
54
- if (!interactive)
55
- throw new Error("Choose at least one half with --server, --client, or both");
56
- serverSelected = await yes("A Server runs on the host machine and can keep working without an open desktop.", "Does this Program have a Server?", true);
57
- clientSelected = !serverSelected || await yes("A Client runs through a desktop and provides the Program's visual interface.", "Does this Program have a Client?", true);
58
- }
59
- if (!serverSelected && !clientSelected)
60
- throw new Error("A Program must have a server half, a client half, or both");
61
- const name = options.name ?? (interactive ? await ask("This name is shown to people; the package name remains the Program identity.", "What name should people see?", manifest.name) : undefined);
62
- if (name !== undefined && name.trim().length === 0)
63
- throw new Error("--name must not be empty");
64
- let apiDocs = options.apiDocs;
65
- if (interactive && apiDocs === undefined) {
66
- const suggested = ["api-docs.md", "README.md"].find(file => existsSync(resolve(directory, file)));
67
- if (await yes("API documentation explains only the services this Program itself provides.", "Does this Program provide API documentation?", Boolean(suggested))) {
68
- apiDocs = await ask("The path is resolved from the project root and becomes the official API entry point.", "Where is the API documentation file?", suggested);
69
- }
70
- }
71
- if (apiDocs !== undefined && apiDocs.trim().length === 0)
72
- throw new Error("An API documentation path must not be empty");
73
- let buildCommand = options.buildCommand;
74
- if (interactive && buildCommand === undefined) {
75
- const suggested = manifest.scripts?.build && projectScript(directory, manifest.packageManager, "build");
76
- const builds = await yes("Production operations need the Program's built files.", "Build before start, install, and pack?", Boolean(suggested));
77
- if (builds)
78
- buildCommand = await ask("This command produces the production Server and Client files.", "What command builds the Program?", suggested || undefined);
45
+ if (!serverSelected && !clientSelected)
46
+ throw new Error("A Program must have a server half, a client half, or both");
47
+ const name = options.name ?? (interactive ? await ask("This name is shown to people; the package name remains the Program identity.", "What name should people see?", manifest.name) : undefined);
48
+ if (name !== undefined && name.trim().length === 0)
49
+ throw new Error("--name must not be empty");
50
+ let apiDocs = options.apiDocs;
51
+ if (interactive && apiDocs === undefined) {
52
+ const suggested = ["api-docs.md", "README.md"].find(file => existsSync(resolve(directory, file)));
53
+ if (await yes("API documentation explains only the services this Program itself provides.", "Does this Program provide API documentation?", false)) {
54
+ apiDocs = await ask("The path is resolved from the project root and becomes the official API entry point.", "Where is the API documentation file?", suggested);
79
55
  }
80
- if (buildCommand !== undefined && buildCommand.trim().length === 0)
81
- throw new Error("A build command must not be empty");
82
- let server;
83
- if (serverSelected) {
84
- const location = options.serverLocation ?? (interactive ? await ask("The system runs the production Server from this project-relative directory.", "Where are the production Server files?", "build/server") : "");
85
- const startCommand = options.serverStartCommand ?? (interactive ? await ask("This command runs inside the production Server directory.", "What command starts the production Server?", "node main.js") : "");
86
- if (!location)
87
- throw new Error("--server-location is required without a terminal");
88
- if (!startCommand)
89
- throw new Error("--server-start-command is required without a terminal");
90
- let development = options.serverDevelopmentStartCommand;
91
- if (interactive && development === undefined) {
92
- const suggested = manifest.scripts?.dev && projectScript(directory, manifest.packageManager, "dev");
93
- if (await yes("Development mode can run the Server directly from the project source.", "Run the Server from source during development?", Boolean(suggested && !clientSelected))) {
94
- development = await ask("This command remains attached to the phresh dev session.", "What command starts the development Server?", suggested || undefined);
95
- }
56
+ }
57
+ if (apiDocs !== undefined && apiDocs.trim().length === 0)
58
+ throw new Error("An API documentation path must not be empty");
59
+ let buildCommand = options.buildCommand;
60
+ if (interactive && buildCommand === undefined) {
61
+ const suggested = manifest.scripts?.build && projectScript(directory, manifest.packageManager, "build");
62
+ const builds = await yes("Production operations need the Program's built files.", "Build before start, install, and pack?", Boolean(suggested));
63
+ if (builds)
64
+ buildCommand = await ask("This command produces the production Server and Client files.", "What command builds the Program?", suggested || undefined);
65
+ }
66
+ if (buildCommand !== undefined && buildCommand.trim().length === 0)
67
+ throw new Error("A build command must not be empty");
68
+ let server;
69
+ if (serverSelected) {
70
+ const location = options.serverLocation ?? (interactive ? await ask("The system runs the production Server from this project-relative directory.", "Where are the production Server files?", clientSelected ? "dist/server" : "dist") : "");
71
+ const startCommand = options.serverStartCommand ?? (interactive ? await ask("This command runs inside the production Server directory.", "What command starts the production Server?", "node main.js") : "");
72
+ if (!location)
73
+ throw new Error("--server-location is required without a terminal");
74
+ if (!startCommand)
75
+ throw new Error("--server-start-command is required without a terminal");
76
+ let development = options.serverDevelopmentStartCommand;
77
+ if (interactive && development === undefined) {
78
+ const suggested = manifest.scripts?.dev && projectScript(directory, manifest.packageManager, "dev");
79
+ if (await yes("Development mode can run the Server directly from the project source.", "Run the Server from source during development?", Boolean(suggested && !clientSelected))) {
80
+ development = await ask("This command remains attached to the phresh dev session.", "What command starts the development Server?", suggested || undefined);
96
81
  }
97
- if (development !== undefined && development.trim().length === 0)
98
- throw new Error("A server development command must not be empty");
99
- server = { location, startCommand, ...development && { development: { startCommand: development } } };
100
82
  }
101
- let client;
102
- if (clientSelected) {
103
- const location = options.clientLocation ?? (interactive ? await ask("The system serves the production Client from this project-relative directory.", "Where are the production Client files?", "dist") : "");
104
- if (!location)
105
- throw new Error("--client-location is required without a terminal");
106
- let developmentUrl = options.clientDevelopmentUrl;
107
- let developmentStartCommand = options.clientDevelopmentStartCommand;
108
- if (interactive && developmentUrl === undefined && developmentStartCommand === undefined) {
109
- const suggested = manifest.scripts?.dev && projectScript(directory, manifest.packageManager, "dev");
110
- if (await yes("A development server can provide live updates instead of built Client files.", "Use a Client development server?", Boolean(suggested))) {
111
- developmentUrl = await ask("The desktop opens this exact HTTP or HTTPS address during phresh dev.", "What URL serves the development Client?", "http://localhost:5173/");
112
- if (await yes("The CLI can own the development server and stop it when the session ends.", "Should phresh dev start the Client server?", Boolean(suggested))) {
113
- developmentStartCommand = await ask("This command remains attached to the phresh dev session.", "What command starts the Client development server?", suggested || undefined);
114
- }
83
+ if (development !== undefined && development.trim().length === 0)
84
+ throw new Error("A server development command must not be empty");
85
+ server = { location, startCommand, ...development && { development: { startCommand: development } } };
86
+ }
87
+ let client;
88
+ if (clientSelected) {
89
+ const location = options.clientLocation ?? (interactive ? await ask("The system serves the production Client from this project-relative directory.", "Where are the production Client files?", serverSelected ? "dist/client" : "dist") : "");
90
+ if (!location)
91
+ throw new Error("--client-location is required without a terminal");
92
+ let developmentUrl = options.clientDevelopmentUrl;
93
+ let developmentStartCommand = options.clientDevelopmentStartCommand;
94
+ if (interactive && developmentUrl === undefined && developmentStartCommand === undefined) {
95
+ const suggested = manifest.scripts?.dev && projectScript(directory, manifest.packageManager, "dev");
96
+ if (await yes("A development server can provide live updates instead of built Client files.", "Use a Client development server?", Boolean(suggested))) {
97
+ developmentUrl = await ask("The desktop opens this exact HTTP or HTTPS address during phresh dev.", "What URL serves the development Client?", "http://localhost:5173/");
98
+ if (await yes("The CLI can own the development server and stop it when the session ends.", "Should phresh dev start the Client server?", Boolean(suggested))) {
99
+ developmentStartCommand = await ask("This command remains attached to the phresh dev session.", "What command starts the Client development server?", suggested || undefined);
115
100
  }
116
101
  }
117
- if (developmentStartCommand !== undefined && developmentUrl === undefined)
118
- throw new Error("--client-development-url is required with --client-development-start-command");
119
- if (developmentUrl !== undefined && developmentUrl.trim().length === 0)
120
- throw new Error("A client development URL must not be empty");
121
- if (developmentUrl !== undefined && !httpUrl(developmentUrl))
122
- throw new Error("A client development URL must use HTTP or HTTPS");
123
- if (developmentStartCommand !== undefined && developmentStartCommand.trim().length === 0)
124
- throw new Error("A client development command must not be empty");
125
- client = {
126
- location,
127
- ...developmentUrl && { development: { url: developmentUrl, ...developmentStartCommand && { startCommand: developmentStartCommand } } }
128
- };
129
102
  }
130
- const described = {
131
- identity: manifest.name,
132
- name,
133
- version: manifest.version,
134
- description: manifest.description,
135
- apiDocs,
136
- buildCommand
103
+ if (developmentStartCommand !== undefined && developmentUrl === undefined)
104
+ throw new Error("--client-development-url is required with --client-development-start-command");
105
+ if (developmentUrl !== undefined && developmentUrl.trim().length === 0)
106
+ throw new Error("A client development URL must not be empty");
107
+ if (developmentUrl !== undefined && !httpUrl(developmentUrl))
108
+ throw new Error("A client development URL must use HTTP or HTTPS");
109
+ if (developmentStartCommand !== undefined && developmentStartCommand.trim().length === 0)
110
+ throw new Error("A client development command must not be empty");
111
+ client = {
112
+ location,
113
+ ...developmentUrl && { development: { url: developmentUrl, ...developmentStartCommand && { startCommand: developmentStartCommand } } }
137
114
  };
138
- const config = server
139
- ? { ...described, server, ...client && { client } }
140
- : { ...described, client: client };
141
- await ensureProjectDependency("@phreshos/core", coreRange, directory);
142
- writeFileSync(path, compose(config));
143
- heading(configFile, "created");
144
- if (config.server)
145
- line("server", config.server.startCommand, `./${config.server.location}`);
146
- if (config.client)
147
- line("client", `./${config.client.location}`);
148
- console.log("");
149
- const next = [config.server?.development || config.client?.development ? "phresh dev" : null, "phresh start", "phresh install"].filter(Boolean);
150
- console.log(` ${dim("Next:")} ${next.join(` ${dim("or")} `)}`);
151
- if (config.client) {
152
- heading("Client requirements");
153
- line("base URL", "./", "required for production assets");
154
- if (config.client.development)
155
- line("CORS", "enabled", "required on the development server");
156
- console.log("");
157
- }
158
- else
159
- console.log("");
160
115
  }
161
- finally {
162
- readline?.close();
116
+ const described = {
117
+ identity: manifest.name,
118
+ name,
119
+ version: manifest.version,
120
+ description: manifest.description,
121
+ apiDocs,
122
+ buildCommand
123
+ };
124
+ const config = server
125
+ ? { ...described, server, ...client && { client } }
126
+ : { ...described, client: client };
127
+ await ensureProjectDependency("@phreshos/core", coreRange, directory);
128
+ writeFileSync(path, compose(config));
129
+ if (config.server)
130
+ interaction.detail("server", config.server.startCommand, `./${config.server.location}`);
131
+ if (config.client)
132
+ interaction.detail("client", `./${config.client.location}`);
133
+ const next = [config.server?.development || config.client?.development ? "phresh dev" : null, "phresh start", "phresh install"].filter(Boolean);
134
+ interaction.message();
135
+ interaction.message(`${dim("Next:")} ${next.join(` ${dim("or")} `)}`);
136
+ if (config.client) {
137
+ interaction.message();
138
+ interaction.detail("base URL", "./", "required for production assets");
139
+ if (config.client.development)
140
+ interaction.detail("CORS", "enabled", "required on the development server");
163
141
  }
142
+ interaction.finish(`${configFile} created`);
164
143
  }
165
144
  function compose(config) {
166
145
  const blocks = [
@@ -184,8 +163,7 @@ function compose(config) {
184
163
  `import { defineConfig } from "@phreshos/core"`,
185
164
  "",
186
165
  "export default defineConfig({",
187
- "",
188
- blocks.filter(Boolean).join(",\n\n"),
166
+ blocks.filter(Boolean).join(",\n"),
189
167
  "})",
190
168
  ""
191
169
  ].join("\n");
@@ -200,9 +178,9 @@ function half(name, values) {
200
178
  if (typeof value === "string")
201
179
  return ` ${key}: ${JSON.stringify(value)}`;
202
180
  const nested = Object.entries(value).map(([nestedKey, nestedValue]) => ` ${nestedKey}: ${JSON.stringify(nestedValue)}`);
203
- return ` ${key}: {\n\n${nested.join(",\n\n")}\n }`;
181
+ return ` ${key}: {\n${nested.join(",\n")}\n }`;
204
182
  });
205
- return ` ${name}: {\n\n${inside.join(",\n\n")}\n }`;
183
+ return ` ${name}: {\n${inside.join(",\n")}\n }`;
206
184
  }
207
185
  function httpUrl(value) {
208
186
  try {
package/dist/pack.js CHANGED
@@ -17,7 +17,7 @@ import build from "./build-command.js";
17
17
  * a declared half always has a location, even when the system chose it.
18
18
  *
19
19
  * There is **no wrapping directory**. `program.json`, `server/`,
20
- * `client/` and `icons/` sit at the package's root, and the directory
20
+ * `client/` and optional `icon.png` sit at the package's root, and the directory
21
21
  * the system installs into is named from the program's own `identity`.
22
22
  * The archive itself therefore needs no second naming layer.
23
23
  *
@@ -41,11 +41,8 @@ export default async function pack(directory = process.cwd()) {
41
41
  if (!zip.getEntry("client/index.html"))
42
42
  throw new Error(`The client files have no index.html — ${config.client.location} is not where a client half is`);
43
43
  }
44
- // Icons are the program's own affair rather than either half's: a
45
- // headless program is shown too. Packaged like a half, and like a
46
- // half their place is not the program's to choose.
47
- if (config.icons)
48
- place(zip, directory, config.icons, "icons");
44
+ if (config.icon)
45
+ file(zip, directory, config.icon, "icon.png", "Program icon");
49
46
  if (config.apiDocs)
50
47
  document(zip, directory, config.apiDocs);
51
48
  zip.addFile("program.json", Buffer.from(JSON.stringify(program(config, version), null, 4) + "\n"));
@@ -64,6 +61,7 @@ function program(config, version) {
64
61
  version,
65
62
  description: config.description,
66
63
  apiDocs: config.apiDocs ? "api-docs.md" : undefined,
64
+ icon: config.icon ? "icon.png" : undefined,
67
65
  ...config.server && { server: { location: "server", start: config.server.start, installCommand: config.server.installCommand, startCommand: config.server.startCommand } },
68
66
  ...config.client && { client: { location: "client", start: config.client.start, title: config.client.title, size: config.client.size, position: config.client.position, layer: config.client.layer, minimize: config.client.minimize } }
69
67
  };
@@ -77,8 +75,11 @@ function place(zip, directory, location, half) {
77
75
  zip.addLocalFolder(from, half);
78
76
  }
79
77
  function document(zip, directory, location) {
78
+ file(zip, directory, location, "api-docs.md", "API documentation file");
79
+ }
80
+ function file(zip, directory, location, target, label) {
80
81
  const from = resolve(directory, location);
81
82
  if (!existsSync(from) || !statSync(from).isFile())
82
- throw new Error(`The API documentation file is not at ${location}`);
83
- zip.addFile("api-docs.md", readFileSync(from));
83
+ throw new Error(`The ${label} is not at ${location}`);
84
+ zip.addFile(target, readFileSync(from));
84
85
  }
@@ -1,6 +1,6 @@
1
1
  import { connect } from "node:net";
2
2
  import { homedir } from "node:os";
3
- import { join } from "node:path";
3
+ import { isAbsolute, join } from "node:path";
4
4
  /**
5
5
  * The local Program intake, from the CLI's side.
6
6
  *
@@ -19,7 +19,15 @@ import { join } from "node:path";
19
19
  * the Program says, then how it ended. None pretends to be a remote method
20
20
  * returning through an unrelated transport.
21
21
  */
22
- export const socketPath = join(homedir(), ".phreshos", "intake.sock");
22
+ export function programIntakePath(environment = process.env, userHome = homedir()) {
23
+ const instanceHome = environment.PHRESHOS_HOME;
24
+ if (instanceHome === undefined)
25
+ return join(userHome, ".phreshos", "intake.sock");
26
+ if (!isAbsolute(instanceHome))
27
+ throw new Error("PHRESHOS_HOME must be an absolute filesystem path");
28
+ return join(instanceHome, "intake.sock");
29
+ }
30
+ export const socketPath = programIntakePath();
23
31
  export default function speak(question, heard, path = socketPath, signal) {
24
32
  return new Promise(function (settle, refuse) {
25
33
  const socket = connect(path);