@phreshos/cli 0.1.28 → 0.1.29

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
@@ -146,7 +146,7 @@ rule everything else here follows from.
146
146
 
147
147
  It follows that every field which lands in a `program.json` is spelled
148
148
  the way the contract spells it and crosses untouched: `size`, not a
149
- width and a height; `startCommand`, not a command.
149
+ width and a height; `startCommand` or `entryFile`, not a generic command.
150
150
 
151
151
  An optional top-level `buildCommand` is authoring metadata. `phresh start`,
152
152
  `phresh install`, and `phresh pack` run it from this project before consuming
@@ -244,11 +244,19 @@ Say how the server runs or where the client is served:
244
244
  ```
245
245
 
246
246
  Each half may carry a `development` block, but the two shapes are deliberately
247
- different. A server block requires `startCommand`; `phresh dev` runs it from the
248
- directory containing `phresh.config.ts`, and that directory becomes the derived
249
- server location. A client block requires an HTTP(S) `url`; development clients
247
+ different. A Server and its development block each select exactly one of
248
+ `startCommand` or `entryFile`. A command starts an isolated operating-system
249
+ process tree; an entry module runs as a Worker owned by the System. In
250
+ development, the directory containing `phresh.config.ts` becomes the derived
251
+ Server location. A client block requires an HTTP(S) `url`; development clients
250
252
  are never resolved from filesystem paths.
251
253
 
254
+ An `entryFile` is a path inside its Server location. Packaging copies it with
255
+ the rest of that directory, and the System rejects a path that escapes those
256
+ files. A Worker has no independent process working directory and is not a
257
+ security boundary; resolve module-owned resources with `import.meta.url` and
258
+ use the Server SDK for Program storage.
259
+
252
260
  The client development shape may also declare `startCommand`. `phresh dev`
253
261
  runs it from the project directory as a foreground development tool; the
254
262
  command is never derived into the Program sent to the system. The tool and
@@ -267,9 +275,10 @@ identity, version, and description from `package.json`, ensures the project has
267
275
  the matching `@phreshos/core` development dependency, and writes the typed
268
276
  `phresh.config.ts` authoring description.
269
277
 
270
- In a terminal, `init` asks for the production locations and commands needed by
271
- `start` and `install`, including whether a package build should prepare those
272
- locations. It then offers the development command and URL needed by `dev`.
278
+ In a terminal, `init` asks for the production locations and Server execution
279
+ modes needed by `start` and `install`, including whether a package build should
280
+ prepare those locations. It then offers the development Server mode, Client
281
+ command, and URL needed by `dev`.
273
282
  Existing `build` and `dev` package scripts become editable suggestions, never
274
283
  silent assumptions. A single Endpoint defaults to `dist`; when both Endpoints
275
284
  exist, their defaults are `dist/server` and `dist/client`. API documentation is
package/dist/cli.js CHANGED
@@ -54,7 +54,9 @@ describe(program.command("init")
54
54
  .option("--server", "include a Server endpoint")
55
55
  .option("--server-location <path>", "production Server directory")
56
56
  .option("--server-start-command <command>", "production Server command")
57
+ .option("--server-entry-file <path>", "production Server worker entry")
57
58
  .option("--server-development-start-command <command>", "development Server command")
59
+ .option("--server-development-entry-file <path>", "development Server worker entry")
58
60
  .option("--client", "include a Client endpoint")
59
61
  .option("--client-location <path>", "production Client directory")
60
62
  .option("--client-development-url <url>", "development Client URL")
@@ -64,10 +66,12 @@ describe(program.command("init")
64
66
  await init({
65
67
  name: options.name,
66
68
  buildCommand: options.buildCommand,
67
- server: options.server === true || options.serverLocation !== undefined || options.serverStartCommand !== undefined,
69
+ server: options.server === true || options.serverLocation !== undefined || options.serverStartCommand !== undefined || options.serverEntryFile !== undefined,
68
70
  serverLocation: options.serverLocation,
69
71
  serverStartCommand: options.serverStartCommand,
72
+ serverEntryFile: options.serverEntryFile,
70
73
  serverDevelopmentStartCommand: options.serverDevelopmentStartCommand,
74
+ serverDevelopmentEntryFile: options.serverDevelopmentEntryFile,
71
75
  client: options.client === true || options.clientLocation !== undefined,
72
76
  clientLocation: options.clientLocation,
73
77
  clientDevelopmentUrl: options.clientDevelopmentUrl,
package/dist/derive.js CHANGED
@@ -14,6 +14,7 @@ export default function derive(config, directory, which) {
14
14
  " server: {",
15
15
  " …",
16
16
  " development: { startCommand: \"tsx source/server/main.ts\" }",
17
+ " // or: development: { entryFile: \"source/server/main.js\" }",
17
18
  " }",
18
19
  "",
19
20
  " client: {",
@@ -45,7 +46,7 @@ export default function derive(config, directory, which) {
45
46
  start: server.start,
46
47
  installCommand: config.server?.installCommand,
47
48
  uninstallCommand: config.server?.uninstallCommand,
48
- startCommand: server.startCommand
49
+ ...serverExecution(server)
49
50
  } },
50
51
  // A URL stands as written; a directory is made absolute. The
51
52
  // contract reads both from the one field, and which it is is
@@ -67,15 +68,23 @@ export default function derive(config, directory, which) {
67
68
  function serverHalf(half, which) {
68
69
  if (!half)
69
70
  return null;
70
- const { development, ...declared } = half;
71
+ const { development, startCommand, entryFile, ...description } = half;
71
72
  if (which === "production" || !development)
72
- return declared;
73
+ return {
74
+ ...description,
75
+ ...serverExecution({ startCommand, entryFile })
76
+ };
73
77
  return {
74
- ...declared,
78
+ ...description,
75
79
  location: ".",
76
- startCommand: development.startCommand
80
+ ...serverExecution(development)
77
81
  };
78
82
  }
83
+ function serverExecution(server) {
84
+ return server.startCommand !== undefined
85
+ ? { startCommand: server.startCommand }
86
+ : { entryFile: server.entryFile };
87
+ }
79
88
  function clientHalf(half, which) {
80
89
  if (!half)
81
90
  return null;
package/dist/init.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import {} from "@phreshos/core";
2
- import { configFile, readManifest } from "./project.js";
2
+ import { configFile, containedServerEntry, readManifest } from "./project.js";
3
3
  import { dim } from "./style.js";
4
4
  import ensureProjectDependency, { projectScript } from "./project-dependency.js";
5
5
  import prompts from "./prompts.js";
@@ -59,21 +59,19 @@ export default async function init(options = {}, directory = process.cwd(), core
59
59
  let server;
60
60
  if (serverSelected) {
61
61
  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") : "");
62
- const startCommand = options.serverStartCommand ?? (interactive ? await ask("This command runs inside the production Server directory.", "What command starts the production Server?", "node main.js") : "");
63
62
  if (!location)
64
63
  throw new Error("--server-location is required without a terminal");
65
- if (!startCommand)
66
- throw new Error("--server-start-command is required without a terminal");
67
- let development = options.serverDevelopmentStartCommand;
64
+ const production = options.serverStartCommand === undefined && options.serverEntryFile === undefined && interactive
65
+ ? await askExecution(interaction, "production", "node main.js")
66
+ : execution(options.serverStartCommand, options.serverEntryFile, "production Server");
67
+ let development = explicitExecution(options.serverDevelopmentStartCommand, options.serverDevelopmentEntryFile, "development Server");
68
68
  if (interactive && development === undefined) {
69
69
  const suggested = manifest.scripts?.dev && projectScript(directory, manifest.packageManager, "dev");
70
70
  if (await yes("Development mode can run the Server directly from the project source.", "Run the Server from source during development?", Boolean(suggested && !clientSelected))) {
71
- development = await ask("This command remains attached to the phresh dev session.", "What command starts the development Server?", suggested || undefined);
71
+ development = await askExecution(interaction, "development", suggested || undefined);
72
72
  }
73
73
  }
74
- if (development !== undefined && development.trim().length === 0)
75
- throw new Error("A server development command must not be empty");
76
- server = { location, startCommand, ...development && { development: { startCommand: development } } };
74
+ server = { location, ...production, ...development && { development } };
77
75
  }
78
76
  let client;
79
77
  if (clientSelected) {
@@ -117,7 +115,7 @@ export default async function init(options = {}, directory = process.cwd(), core
117
115
  await ensureProjectDependency("@phreshos/core", coreRange, directory);
118
116
  writeFileSync(path, compose(config));
119
117
  if (config.server)
120
- interaction.detail("server", config.server.startCommand, `./${config.server.location}`);
118
+ interaction.detail(config.server.startCommand ? "server" : "server worker", config.server.startCommand ?? config.server.entryFile, `./${config.server.location}`);
121
119
  if (config.client)
122
120
  interaction.detail("client", `./${config.client.location}`);
123
121
  const next = [config.server?.development || config.client?.development ? "phresh dev" : null, "phresh start", "phresh install"].filter(Boolean);
@@ -140,7 +138,9 @@ function compose(config) {
140
138
  field("buildCommand", config.buildCommand),
141
139
  half("server", config.server && {
142
140
  location: config.server.location,
143
- startCommand: config.server.startCommand,
141
+ ...(config.server.startCommand !== undefined
142
+ ? { startCommand: config.server.startCommand }
143
+ : { entryFile: config.server.entryFile }),
144
144
  ...config.server.development && { development: config.server.development }
145
145
  }),
146
146
  half("client", config.client && {
@@ -180,3 +180,35 @@ function httpUrl(value) {
180
180
  return false;
181
181
  }
182
182
  }
183
+ function execution(startCommand, entryFile, owner) {
184
+ const selected = explicitExecution(startCommand, entryFile, owner);
185
+ if (!selected)
186
+ throw new Error(`Choose exactly one --server-start-command or --server-entry-file for the ${owner}`);
187
+ return selected;
188
+ }
189
+ function explicitExecution(startCommand, entryFile, owner) {
190
+ if (startCommand !== undefined && entryFile !== undefined)
191
+ throw new Error(`The ${owner} cannot declare both a start command and an entry file`);
192
+ if (startCommand !== undefined) {
193
+ if (startCommand.trim().length === 0)
194
+ throw new Error(`The ${owner} command must not be empty`);
195
+ return { startCommand };
196
+ }
197
+ if (entryFile !== undefined) {
198
+ if (entryFile.trim().length === 0)
199
+ throw new Error(`The ${owner} entry file must not be empty`);
200
+ if (!containedServerEntry(entryFile))
201
+ throw new Error(`The ${owner} entry file must remain inside its Server directory`);
202
+ return { entryFile };
203
+ }
204
+ }
205
+ async function askExecution(interaction, mode, suggestedCommand) {
206
+ const worker = await interaction.yes("A Worker uses fewer resources but shares the System's Node.js process.", `Run the ${mode} Server as a System-owned Worker?`, false);
207
+ if (worker)
208
+ return {
209
+ entryFile: await interaction.ask("This JavaScript module remains inside the Server files.", `What is the ${mode} Server entry file?`, mode === "production" ? "main.js" : "source/server/main.js")
210
+ };
211
+ return {
212
+ startCommand: await interaction.ask(`This command runs from the ${mode === "production" ? "production Server directory" : "project directory"}.`, `What command starts the ${mode} Server?`, suggestedCommand)
213
+ };
214
+ }
package/dist/launch.js CHANGED
@@ -50,7 +50,7 @@ export default async function launch(which, directory = process.cwd(), options =
50
50
  const clientStartCommand = clientDevelopmentConfig?.startCommand;
51
51
  heading(`${program.name ?? program.identity}${program.version ? ` ${program.version}` : ""}`, which);
52
52
  if (program.server)
53
- line("server", String(program.server.startCommand), place(directory, program.server.location));
53
+ line(program.server.startCommand ? "server" : "server worker", String(program.server.startCommand ?? program.server.entryFile), place(directory, program.server.location));
54
54
  if (program.client)
55
55
  line("client", clientStartCommand ?? place(directory, program.client.location), clientStartCommand ? place(directory, program.client.location) : undefined);
56
56
  line("storage", place(directory, String(program.storage)));
package/dist/pack.js CHANGED
@@ -76,10 +76,21 @@ function program(config, version) {
76
76
  categories: config.categories,
77
77
  keywords: config.keywords,
78
78
  website: config.website,
79
- ...config.server && { server: { location: "server", start: config.server.start, installCommand: config.server.installCommand, uninstallCommand: config.server.uninstallCommand, startCommand: config.server.startCommand } },
79
+ ...config.server && { server: {
80
+ location: "server",
81
+ start: config.server.start,
82
+ installCommand: config.server.installCommand,
83
+ uninstallCommand: config.server.uninstallCommand,
84
+ ...serverExecution(config.server)
85
+ } },
80
86
  ...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 } }
81
87
  };
82
88
  }
89
+ function serverExecution(server) {
90
+ return server.startCommand !== undefined
91
+ ? { startCommand: server.startCommand }
92
+ : { entryFile: server.entryFile };
93
+ }
83
94
  // What is at a half's location, where the package keeps it. A half that
84
95
  // was declared and is not there is the answer to whether it was built.
85
96
  function place(zip, directory, location, half) {
package/dist/project.js CHANGED
@@ -8,7 +8,7 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
8
8
  };
9
9
  import { isRelativeValue, layers } from "@phreshos/core";
10
10
  import { existsSync } from "node:fs";
11
- import { resolve } from "node:path";
11
+ import { isAbsolute, normalize, resolve, sep } from "node:path";
12
12
  import { pathToFileURL } from "node:url";
13
13
  export const configFile = "phresh.config.ts";
14
14
  export async function readConfig(directory = process.cwd()) {
@@ -76,8 +76,8 @@ function coherent(config) {
76
76
  }
77
77
  if (!(config.server && (config.server.start ?? true)) && !(config.client && (config.client.start ?? true)))
78
78
  throw new Error("A Program's default Process must start a server endpoint, a client endpoint, or both");
79
- if (config.server && (typeof config.server.startCommand !== "string" || config.server.startCommand.length === 0))
80
- throw new Error("A server half must say what starts it");
79
+ if (config.server)
80
+ execution(config.server, "A server half");
81
81
  if (config.server?.installCommand !== undefined && typeof config.server.installCommand !== "string")
82
82
  throw new Error("A server half's install command must be text");
83
83
  if (config.server?.uninstallCommand !== undefined && typeof config.server.uninstallCommand !== "string")
@@ -101,8 +101,8 @@ function coherent(config) {
101
101
  const serverDevelopment = config.server?.development;
102
102
  if (serverDevelopment !== undefined && (typeof serverDevelopment !== "object" || serverDevelopment === null || Array.isArray(serverDevelopment)))
103
103
  throw new Error("server.development must be a declaration");
104
- if (serverDevelopment !== undefined && (typeof serverDevelopment.startCommand !== "string" || serverDevelopment.startCommand.trim().length === 0))
105
- throw new Error("server.development.startCommand must be non-empty text");
104
+ if (serverDevelopment !== undefined)
105
+ execution(serverDevelopment, "server.development");
106
106
  const clientDevelopment = config.client?.development;
107
107
  if (clientDevelopment !== undefined && (typeof clientDevelopment !== "object" || clientDevelopment === null || Array.isArray(clientDevelopment)))
108
108
  throw new Error("client.development must be a declaration");
@@ -120,6 +120,24 @@ function coherent(config) {
120
120
  throw new Error(`A window's ${what} is a finite pixel number or a relative expression such as "50% + 10"`);
121
121
  }
122
122
  }
123
+ function execution(value, owner) {
124
+ const command = typeof value.startCommand === "string" && value.startCommand.trim().length > 0;
125
+ const entry = typeof value.entryFile === "string" && value.entryFile.trim().length > 0;
126
+ if (value.startCommand !== undefined && !command)
127
+ throw new Error(`${owner}'s startCommand must be non-empty text`);
128
+ if (value.entryFile !== undefined && !entry)
129
+ throw new Error(`${owner}'s entryFile must be a non-empty path`);
130
+ if (command === entry)
131
+ throw new Error(`${owner} must declare exactly one non-empty startCommand or entryFile`);
132
+ if (entry && !containedServerEntry(value.entryFile))
133
+ throw new Error(`${owner}'s entryFile must remain inside its Server directory`);
134
+ }
135
+ export function containedServerEntry(entry) {
136
+ if (isAbsolute(entry))
137
+ return false;
138
+ const path = normalize(entry);
139
+ return path !== ".." && !path.startsWith(`..${sep}`);
140
+ }
123
141
  function httpUrl(value) {
124
142
  if (typeof value !== "string" || value.trim().length === 0)
125
143
  return false;
@@ -22,7 +22,7 @@
22
22
  "react-dom": "^19.2.8"
23
23
  },
24
24
  "devDependencies": {
25
- "@phreshos/cli": "^0.1.28",
25
+ "@phreshos/cli": "^0.1.29",
26
26
  "@types/node": "^26.2.0",
27
27
  "@types/react": "^19.2.18",
28
28
  "@types/react-dom": "^19.2.4",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@phreshos/cli",
3
3
  "type": "module",
4
- "version": "0.1.28",
4
+ "version": "0.1.29",
5
5
  "description": "The Phresh command-line interface for Program projects and system management.",
6
6
  "engines": {
7
7
  "node": ">=20.10"
@@ -49,7 +49,7 @@
49
49
  "packageManager": "bun@1.3.14",
50
50
  "dependencies": {
51
51
  "@clack/prompts": "^1.7.0",
52
- "@phreshos/core": "^0.1.19",
52
+ "@phreshos/core": "^0.1.20",
53
53
  "adm-zip": "^0.6.0",
54
54
  "commander": "^15.0.0",
55
55
  "picocolors": "^1.1.1"