@phreshos/cli 0.1.13 → 0.1.15

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
@@ -78,10 +78,11 @@ Windows; neither becomes a network endpoint or introduces a bearer secret.
78
78
 
79
79
  `phresh create <directory>` creates a complete Server and Client Program from
80
80
  the maintained `PhreshOS/phresh-program` repository. The CLI build downloads
81
- one exact tagged source release, verifies its SHA-256 checksum, removes
82
- repository-only material, and bundles the resulting authoring project. The
83
- installed CLI therefore creates projects offline without reading a live branch
84
- or maintaining a second template by hand.
81
+ the newest complete stable release, validates its source identity and version,
82
+ removes repository-only material, and bundles that exact authoring project.
83
+ The resolved source digest is recorded with the bundle. The installed CLI
84
+ therefore creates projects offline without reading a live branch or maintaining
85
+ a release pin or second template by hand.
85
86
 
86
87
  The directory name becomes the stable kebab-case Program identity. In a
87
88
  terminal, `create` asks for the directory when it is omitted, the readable
@@ -4,16 +4,6 @@ import { mkdtemp, readFile, rm } from "node:fs/promises";
4
4
  import { tmpdir } from "node:os";
5
5
  import { dirname, isAbsolute, join, relative, resolve } from "node:path";
6
6
  import AdmZip from "adm-zip";
7
- const officialPrograms = {
8
- phresh: {
9
- identity: "phresh-program",
10
- repository: "PhreshOS/phresh-program"
11
- },
12
- setup: {
13
- identity: "setup",
14
- repository: "PhreshOS/setup-program"
15
- }
16
- };
17
7
  /** Resolve, verify, and unpack one official production Program release. */
18
8
  export async function prepareOfficialProgram(name, fetcher = fetch) {
19
9
  const release = await resolveOfficialProgramRelease(name, fetcher);
@@ -34,10 +24,8 @@ export async function prepareOfficialProgram(name, fetcher = fetch) {
34
24
  }
35
25
  }
36
26
  export async function resolveOfficialProgramRelease(name, fetcher = fetch) {
37
- const official = officialPrograms[name];
38
- if (!official)
39
- throw new Error(`No official Program is named "${name}"`);
40
- const response = await fetcher(`https://api.github.com/repos/${official.repository}/releases?per_page=100`, {
27
+ const requested = programName(name);
28
+ const response = await fetcher(`https://api.github.com/repos/PhreshOS/${requested}-program/releases?per_page=100`, {
41
29
  headers: {
42
30
  Accept: "application/vnd.github+json",
43
31
  "User-Agent": "@phreshos/cli"
@@ -46,26 +34,24 @@ export async function resolveOfficialProgramRelease(name, fetcher = fetch) {
46
34
  });
47
35
  if (!response.ok)
48
36
  throw new Error(`The ${name} release list could not be read (${response.status} ${response.statusText})`);
49
- return selectProgramRelease(official.identity, await response.json());
37
+ return selectProgramRelease(await response.json());
50
38
  }
51
- export function selectProgramRelease(identity, value) {
39
+ export function selectProgramRelease(value) {
52
40
  if (!Array.isArray(value))
53
- throw new Error(`The ${identity} release list is invalid`);
41
+ throw new Error("The Program release list is invalid");
54
42
  const releases = value.flatMap(function (item) {
55
43
  if (!record(item) || item.draft === true || item.prerelease === true || typeof item.tag_name !== "string" || !Array.isArray(item.assets))
56
44
  return [];
57
45
  const version = parseVersion(item.tag_name);
58
46
  if (!version)
59
47
  return [];
60
- const archiveName = `${identity}@${version}.zip`;
61
- const archive = asset(item.assets, archiveName);
62
- const checksum = asset(item.assets, `${archiveName}.sha256`);
63
- return archive && checksum ? [{ identity, version, archive, checksum }] : [];
48
+ const files = programAssets(item.assets, version);
49
+ return files ? [{ version, ...files }] : [];
64
50
  });
65
51
  releases.sort((left, right) => compare(right.version, left.version));
66
52
  const selected = releases[0];
67
53
  if (!selected)
68
- throw new Error(`No stable ${identity} Program release is available`);
54
+ throw new Error("No stable Program release is available");
69
55
  return selected;
70
56
  }
71
57
  export async function downloadProgramRelease(release, fetcher = fetch) {
@@ -150,6 +136,27 @@ function asset(assets, name) {
150
136
  const found = assets.find(item => record(item) && item.name === name && typeof item.browser_download_url === "string");
151
137
  return record(found) && typeof found.browser_download_url === "string" ? found.browser_download_url : undefined;
152
138
  }
139
+ function programAssets(assets, version) {
140
+ const suffix = `@${version}.zip`;
141
+ const archives = assets.flatMap(function (item) {
142
+ if (!record(item) || typeof item.name !== "string" || typeof item.browser_download_url !== "string" || !item.name.endsWith(suffix))
143
+ return [];
144
+ const identity = item.name.slice(0, -suffix.length);
145
+ if (!validProgramName(identity))
146
+ return [];
147
+ const checksum = asset(assets, `${item.name}.sha256`);
148
+ return checksum ? [{ identity, archive: item.browser_download_url, checksum }] : [];
149
+ });
150
+ return archives.length === 1 ? archives[0] : undefined;
151
+ }
152
+ function programName(name) {
153
+ if (!validProgramName(name))
154
+ throw new Error(`The official Program name "${name}" is invalid`);
155
+ return name;
156
+ }
157
+ function validProgramName(name) {
158
+ return name.length <= 64 && /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name);
159
+ }
153
160
  function parseVersion(tag) {
154
161
  const match = /^v(\d+)\.(\d+)\.(\d+)$/.exec(tag);
155
162
  return match ? tag.slice(1) : undefined;
package/dist/style.js CHANGED
@@ -19,7 +19,6 @@ export function column(label) {
19
19
  return label.length < 12 ? label.padEnd(12) : `${label} `;
20
20
  }
21
21
  export function heading(title, note) {
22
- console.log("");
23
22
  section(title, note);
24
23
  console.log("");
25
24
  }
@@ -16,13 +16,18 @@ production shape, build and attach the Program with:
16
16
  bun phresh start
17
17
  ```
18
18
 
19
- The structure follows the endpoint boundaries directly:
19
+ The structure keeps composition, representation, and authoritative behavior distinct:
20
20
 
21
21
  ```text
22
22
  source/
23
- ├── client/ React interface
24
- └── server/ authoritative counter and API
23
+ ├── client/
24
+ │ ├── main.tsx Client composition
25
+ │ └── view/ React representation
26
+ └── server/
27
+ ├── main.ts Server composition
28
+ ├── core/ authoritative counter
29
+ └── view/ endpoint adapter and service exposure
25
30
  ```
26
31
 
27
32
  The Program declaration lives in `phresh.config.ts`. Its own service contract
28
- is documented in `api-docs.md`.
33
+ is exposed as the `counter` Server service and documented in `api-docs.md`.
@@ -1,6 +1,6 @@
1
1
  # Counter API
2
2
 
3
- The Server owns one number for the lifetime of its Process.
3
+ The `counter` Server service owns one number for the lifetime of its Process.
4
4
 
5
5
  | Name | Destination | Interaction | Payload | Answer |
6
6
  | --- | --- | --- | --- | --- |
@@ -14,7 +14,13 @@ The Server owns one number for the lifetime of its Process.
14
14
  current counter value.
15
15
 
16
16
  ```ts
17
- const count = await process.server.ask<number>("read")
17
+ const counter = host.service({
18
+ program: "phresh-program",
19
+ endpoint: "server",
20
+ name: "counter"
21
+ })
22
+
23
+ const count = await counter.channel.ask<number>("read")
18
24
  ```
19
25
 
20
26
  ## Publish to the Server: `increment`
@@ -23,7 +29,7 @@ const count = await process.server.ask<number>("read")
23
29
  to the counter and does not produce an answer.
24
30
 
25
31
  ```ts
26
- process.server.publish("increment")
32
+ counter.channel.publish("increment")
27
33
  ```
28
34
 
29
35
  ## Subscribe to the Server: `changed`
@@ -32,7 +38,7 @@ After an increment, the Server emits `changed` with the new counter value.
32
38
  Anyone holding that Process's Server can subscribe to the event.
33
39
 
34
40
  ```ts
35
- process.server.subscribe<number>("changed", count => {
41
+ counter.channel.subscribe<number>("changed", count => {
36
42
  // use the latest count
37
43
  })
38
44
  ```
@@ -1,31 +1,19 @@
1
1
  # Logs
2
2
  logs
3
3
  *.log
4
- npm-debug.log*
5
- yarn-debug.log*
6
- yarn-error.log*
7
- pnpm-debug.log*
8
- lerna-debug.log*
9
4
 
10
5
  node_modules
11
6
  dist
12
- dist-ssr
13
7
  *.local
14
8
 
15
9
  # Made by `phresh pack` in the project root.
16
10
  /*.zip
17
11
  /*.zip.sha256
18
12
 
19
- # Editor directories and files
20
- .vscode/*
21
- !.vscode/extensions.json
13
+ # Editor and machine residue.
14
+ .vscode
22
15
  .idea
23
16
  .DS_Store
24
- *.suo
25
- *.ntvs*
26
- *.njsproj
27
- *.sln
28
- *.sw?
29
17
 
30
- # Made by the system the first time this program runs.
18
+ # Made by the system the first time this Program runs.
31
19
  storage
@@ -1,28 +1,34 @@
1
1
  {
2
2
  "name": "phresh-program",
3
3
  "private": true,
4
- "version": "0.1.2",
4
+ "version": "0.1.6",
5
+ "description": "The official PhreshOS starter Program.",
5
6
  "type": "module",
6
7
  "scripts": {
7
8
  "dev": "phresh dev",
8
9
  "start": "phresh start"
9
10
  },
11
+ "keywords": [
12
+ "phreshos",
13
+ "program",
14
+ "starter"
15
+ ],
10
16
  "dependencies": {
11
- "@phreshos/client": "^0.1.1",
12
- "@phreshos/react": "^0.1.1",
13
- "@phreshos/server": "^0.1.1",
14
- "@phreshos/core": "^0.1.1",
17
+ "@phreshos/client": "^0.1.9",
18
+ "@phreshos/core": "^0.1.6",
19
+ "@phreshos/react": "^0.1.5",
20
+ "@phreshos/server": "^0.1.7",
15
21
  "react": "^19.2.8",
16
22
  "react-dom": "^19.2.8"
17
23
  },
18
24
  "devDependencies": {
19
- "@phreshos/cli": "^0.1.13",
25
+ "@phreshos/cli": "^0.1.15",
20
26
  "@types/node": "^26.2.0",
21
27
  "@types/react": "^19.2.18",
22
28
  "@types/react-dom": "^19.2.4",
23
29
  "@vitejs/plugin-react": "^6.0.5",
24
- "tsx": "^4.23.12",
25
30
  "typescript": "^7.0.2",
26
- "vite": "^8.2.1"
31
+ "vite": "^8.2.1",
32
+ "vite-node": "^6.0.0"
27
33
  }
28
34
  }
@@ -22,12 +22,7 @@ export default defineConfig({
22
22
  // these values determines the Program's identity.
23
23
  name: "Phresh Program",
24
24
  description: "A simple counter whose state lives on the Server.",
25
- version: "0.1.2",
26
-
27
- // Markdown entry point for the API owned by this Program. It documents the
28
- // counter service contract; PhreshOS endpoint mechanics belong in PhreshOS
29
- // documentation instead of being repeated here.
30
- apiDocs: "api-docs.md",
25
+ version: "0.1.6",
31
26
 
32
27
  // One authored PNG. Installation gives it a canonical name and the system
33
28
  // derives the standard hosted icon sizes from it.
@@ -36,7 +31,7 @@ export default defineConfig({
36
31
  // Prepares both production endpoint directories. The CLI runs it from this
37
32
  // project before `phresh start`, `phresh install`, and `phresh pack`.
38
33
  // `phresh dev` does not build and uses the declarations below instead.
39
- buildCommand: "node --import tsx source/build.ts",
34
+ buildCommand: "vite-node scripts/build.ts",
40
35
 
41
36
  // A Process may run its Server and Client independently. This declaration
42
37
  // says how the Server is prepared and started; it does not merge Server
@@ -62,8 +57,8 @@ export default defineConfig({
62
57
 
63
58
  // `phresh dev` runs this command from the project directory. The
64
59
  // project directory becomes the development Server location, so
65
- // source imports and watch mode work without a production build.
66
- startCommand: "node --watch --import tsx source/server/main.ts"
60
+ // source imports work without a production build.
61
+ startCommand: "vite-node source/server/main.ts"
67
62
  }
68
63
  },
69
64
 
@@ -1,7 +1,10 @@
1
- import { externalDependencies } from "@/vite.server"
1
+ import { externalDependencies } from "@/vite.config"
2
2
  import { writeFile } from "node:fs/promises"
3
3
  import packageConfig from "@/package.json"
4
- import { build } from "vite"
4
+
5
+ process.env.NODE_ENV = "production"
6
+
7
+ const { build } = await import("vite")
5
8
 
6
9
  const dependencies: Partial<typeof packageConfig.dependencies> = {}
7
10
 
@@ -10,8 +13,8 @@ for (const externalDependency of externalDependencies) {
10
13
  dependencies[externalDependency] = packageConfig.dependencies[externalDependency]
11
14
  }
12
15
 
13
- await build({ configFile: "vite.server.ts" })
16
+ await build({ configFile: "vite.config.ts", ssr: { noExternal: true } })
14
17
 
15
18
  await build({ configFile: "vite.client.ts" })
16
19
 
17
- await writeFile("dist/server/package.json", JSON.stringify({ type: "module", dependencies }))
20
+ await writeFile("dist/server/package.json", JSON.stringify({ type: "module", dependencies }))
@@ -1,8 +1,8 @@
1
1
  import client from "react-dom/client"
2
2
  import { StrictMode } from "react"
3
- import App from "./app"
3
+ import App from "./view/app"
4
4
  import "./style.css"
5
5
 
6
6
  const root = client.createRoot(document.body)
7
7
 
8
- root.render(<StrictMode><App /></StrictMode>)
8
+ root.render(<StrictMode><App /></StrictMode>)
@@ -0,0 +1,13 @@
1
+ /** Owns the Program's authoritative counter state without knowing how it is exposed. */
2
+ export default class Counter {
3
+ private value = 0
4
+
5
+ public read() {
6
+ return this.value
7
+ }
8
+
9
+ public increment() {
10
+ this.value += 1
11
+ return this.value
12
+ }
13
+ }
@@ -1,14 +1,3 @@
1
- import { current } from "@phreshos/server"
1
+ import view from "./view/view"
2
2
 
3
- // The Server owns the value. Every Client representation reads and changes
4
- // this same state instead of keeping an independent browser counter.
5
- let count = 0
6
-
7
- current.answer("read", () => count)
8
-
9
- current.subscribe("increment", async function () {
10
-
11
- count += 1
12
-
13
- current.publish("changed", count)
14
- })
3
+ await view()
@@ -0,0 +1,16 @@
1
+ import { current } from "@phreshos/server"
2
+ import docs from "@/api-docs.md?raw"
3
+ import Counter from "@server/core/counter"
4
+
5
+ const counter = new Counter()
6
+
7
+ /** Exposes the counter core through this Server endpoint. */
8
+ export default async function view() {
9
+ current.answer("read", () => counter.read())
10
+
11
+ current.subscribe("increment", () => {
12
+ current.publish("changed", counter.increment())
13
+ })
14
+
15
+ await current.enableService({ name: "counter", docs })
16
+ }
@@ -9,6 +9,10 @@
9
9
  "jsx": "react-jsx",
10
10
  "strict": true,
11
11
  "noEmit": true,
12
+ "types": [
13
+ "@types/node",
14
+ "vite/client"
15
+ ],
12
16
  "paths": {
13
17
  "@/*": [
14
18
  "./*"
@@ -13,7 +13,6 @@ export default defineConfig({
13
13
  tsconfigPaths: true
14
14
  },
15
15
  ssr: {
16
- noExternal: true,
17
16
  external: externalDependencies
18
17
  },
19
18
  build: {
@@ -24,4 +23,4 @@ export default defineConfig({
24
23
  input: "main.ts"
25
24
  }
26
25
  }
27
- })
26
+ })
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "repository": "PhreshOS/phresh-program",
3
- "version": "0.1.2",
3
+ "version": "0.1.6",
4
+ "sha256": "a56227927522a550ef5dab3eb5127274e23ecd9c0dabb912f8b46f37990a9602",
4
5
  "development": true
5
6
  }
package/package.json CHANGED
@@ -1,17 +1,18 @@
1
1
  {
2
2
  "name": "@phreshos/cli",
3
3
  "type": "module",
4
- "version": "0.1.13",
4
+ "version": "0.1.15",
5
5
  "description": "The Phresh command-line interface for Program projects and system management.",
6
6
  "engines": {
7
7
  "node": ">=20.10"
8
8
  },
9
9
  "scripts": {
10
10
  "clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
11
+ "check:scripts": "tsc --project tsconfig.scripts.json",
11
12
  "compile": "tsc --noEmit false --outDir dist --rootDir source --rewriteRelativeImportExtensions true --allowImportingTsExtensions true",
12
- "build": "node --run clean && node --run compile && node dist/build-template.js",
13
+ "build": "node --run clean && node --run compile && vite-node scripts/build-template.ts",
13
14
  "test": "node --run compile && node --test tests/*.test.mjs",
14
- "verify": "node --run build && node --run test && node scripts/verify-package.mjs",
15
+ "verify": "node --run check:scripts && node --run build && node --run test && node scripts/verify-package.mjs",
15
16
  "verify:system-release": "node --run compile && node scripts/verify-system-release.mjs",
16
17
  "verify:linux-service": "node --run compile && node scripts/verify-linux-service.mjs",
17
18
  "verify:macos-service": "node --run compile && node scripts/verify-macos-service.mjs",
@@ -56,6 +57,7 @@
56
57
  "devDependencies": {
57
58
  "@types/adm-zip": "^0.5.8",
58
59
  "@types/node": "^25.9.5",
59
- "typescript": "^6.0.3"
60
+ "typescript": "^6.0.3",
61
+ "vite-node": "^6.0.0"
60
62
  }
61
63
  }
@@ -1,90 +0,0 @@
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.2",
10
- sha256: "dba5f1fd868c46501d5bd3463f4f8f0e098c7a231cb75794c01118689dc69339"
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
- }
@@ -1 +0,0 @@
1
- /// <reference types="vite/client" />
@@ -1 +0,0 @@
1
- /// <reference types="@types/node" />