@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.
@@ -1,55 +1,105 @@
1
1
  import { line } from "./style.js";
2
2
  import { spawn } from "node:child_process";
3
3
  import { existsSync, readFileSync } from "node:fs";
4
- import { resolve } from "node:path";
4
+ import { isAbsolute, relative, resolve } from "node:path";
5
5
  /** Returns the package-manager command that runs one project script. */
6
6
  export function projectScript(directory, declared, script) {
7
- return `${packageManager(directory, declared).name} run ${script}`;
7
+ return `${projectPackageManager(directory, declared).name} run ${script}`;
8
8
  }
9
9
  /**
10
- * Ensures one development dependency using the source appropriate to this CLI.
10
+ * Resolves an SDK through the one source policy shared by `init` and `create`.
11
11
  *
12
- * A CLI running from this repository uses its sibling dev-kit, so Program
13
- * authoring can be exercised before anything is published. An installed CLI
14
- * has no such sibling and names the declared registry range instead. This
15
- * decision belongs here once for both `init` and the future `create` command.
12
+ * A Program inside this repository's workspace uses its sibling dev-kit. A
13
+ * standalone project or distributed CLI keeps the published range embedded
14
+ * in the generated template. Local packages therefore never leak their own
15
+ * workspace-only development graph into an unrelated project.
16
16
  */
17
- export default async function ensureProjectDependency(name, range, directory = process.cwd()) {
17
+ export function projectDependency(name, range, directory) {
18
+ const workspaceDirectory = resolve(import.meta.dirname, "..", "..", "..");
19
+ const workspace = manifestAt(workspaceDirectory);
20
+ const sourceDirectory = resolve(import.meta.dirname, "..", "..", localDirectories[name]);
21
+ const manifest = manifestAt(sourceDirectory);
22
+ if (workspace?.name === "@phreshos/workspace" && workspaceIncludes(workspaceDirectory, directory, workspace.workspaces) && manifest?.name === name && manifest.version && accepts(range, manifest.version)) {
23
+ return {
24
+ installSpecifier: `${name}@workspace:*`,
25
+ manifestSpecifier: "workspace:*",
26
+ local: true
27
+ };
28
+ }
29
+ return {
30
+ installSpecifier: `${name}@${range}`,
31
+ manifestSpecifier: range,
32
+ local: false
33
+ };
34
+ }
35
+ /** Detects the package manager declared by, present in, or invoking a project. */
36
+ export function projectPackageManager(directory, declared, preferred) {
37
+ const named = preferred ?? packageManagerName(declared) ?? packageManagerFromLocks(directory) ?? packageManagerFromInvocation();
38
+ return managers[named ?? "npm"];
39
+ }
40
+ /** Installs the dependencies already declared by a generated project. */
41
+ export async function installProjectDependencies(directory, preferred, output = "inherit") {
42
+ const manifest = manifestAt(directory);
43
+ const manager = projectPackageManager(directory, manifest?.packageManager, preferred);
44
+ await run(manager.name, manager.installArgs, directory, output);
45
+ return manager.name;
46
+ }
47
+ /**
48
+ * Ensures one project dependency using the source appropriate to this CLI.
49
+ *
50
+ * `init` uses development dependencies because Core supplies the authoring
51
+ * contract for an existing project. `create` preserves the canonical
52
+ * template's own dependency sections instead.
53
+ */
54
+ export default async function ensureProjectDependency(name, range, directory = process.cwd(), section = "devDependencies") {
18
55
  const manifestPath = resolve(directory, "package.json");
19
56
  const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
20
57
  if (manifest.dependencies?.[name] || manifest.devDependencies?.[name])
21
58
  return;
22
- const manager = packageManager(directory, manifest.packageManager);
23
- const source = packageSource(name, range);
59
+ const manager = projectPackageManager(directory, manifest.packageManager);
60
+ const source = projectDependency(name, range, directory);
24
61
  line("dependency", name, `${manager.name}, ${source.local ? "local dev-kit" : range}`);
25
- await new Promise(function (settle, refuse) {
26
- const child = spawn(manager.name, [...manager.args, source.specifier], {
62
+ await run(manager.name, [...manager.addArgs(section), source.installSpecifier], directory);
63
+ console.log("");
64
+ }
65
+ function run(command, args, directory, output = "inherit") {
66
+ return new Promise(function (settle, refuse) {
67
+ let diagnostic = "";
68
+ const child = spawn(command, args, {
27
69
  cwd: directory,
28
- stdio: "inherit",
70
+ stdio: output === "capture" ? ["ignore", "pipe", "pipe"] : "inherit",
29
71
  shell: process.platform === "win32"
30
72
  });
31
- child.once("error", error => refuse(new Error(`Could not run ${manager.name}: ${error.message}`)));
73
+ if (output === "capture") {
74
+ child.stdout?.on("data", chunk => { diagnostic += String(chunk); });
75
+ child.stderr?.on("data", chunk => { diagnostic += String(chunk); });
76
+ }
77
+ child.once("error", error => refuse(new Error(`Could not run ${command}: ${error.message}`)));
32
78
  child.once("exit", function (code, signal) {
33
79
  if (signal)
34
- refuse(new Error(`${manager.name} ended on ${signal}`));
80
+ refuse(new Error(`${command} ended on ${signal}`));
35
81
  else if (code !== 0)
36
- refuse(new Error(`${manager.name} exited with ${code ?? 0}`));
82
+ refuse(new Error(`${command} exited with ${code ?? 0}${diagnostic.trim() ? `\n\n${diagnostic.trim()}` : ""}`));
37
83
  else
38
84
  settle();
39
85
  });
40
86
  });
41
- console.log("");
42
- }
43
- function packageSource(name, range) {
44
- const directory = resolve(import.meta.dirname, "..", "..", localDirectories[name]);
45
- const manifest = manifestAt(directory);
46
- if (manifest?.name === name && manifest.version && accepts(range, manifest.version))
47
- return { specifier: directory, local: true };
48
- return { specifier: `${name}@${range}`, local: false };
49
87
  }
50
88
  function accepts(range, version) {
51
89
  return range === "workspace:*" || range === version || range === `^${version}` || range === `~${version}`;
52
90
  }
91
+ function workspaceIncludes(root, directory, patterns) {
92
+ const local = relative(root, resolve(directory)).replaceAll("\\", "/");
93
+ if (!local || local === ".." || local.startsWith("../") || isAbsolute(local))
94
+ return false;
95
+ return patterns?.some(function (pattern) {
96
+ if (!pattern.endsWith("/*"))
97
+ return local === pattern;
98
+ const prefix = pattern.slice(0, -1);
99
+ const child = local.startsWith(prefix) ? local.slice(prefix.length) : "";
100
+ return Boolean(child) && !child.includes("/");
101
+ }) ?? false;
102
+ }
53
103
  function manifestAt(directory) {
54
104
  const path = resolve(directory, "package.json");
55
105
  if (!existsSync(path))
@@ -61,26 +111,53 @@ function manifestAt(directory) {
61
111
  return null;
62
112
  }
63
113
  }
64
- function packageManager(directory, declared) {
114
+ function packageManagerName(declared) {
65
115
  const named = declared?.split("@")[0];
66
- if (named === "bun")
67
- return { name: "bun", args: ["add", "--dev"] };
68
- if (named === "pnpm")
69
- return { name: "pnpm", args: ["add", "--save-dev"] };
70
- if (named === "yarn")
71
- return { name: "yarn", args: ["add", "--dev"] };
72
- if (named === "npm")
73
- return { name: "npm", args: ["install", "--save-dev", "--no-fund", "--no-audit"] };
116
+ return isPackageManager(named) ? named : undefined;
117
+ }
118
+ function packageManagerFromLocks(directory) {
74
119
  if (existsSync(resolve(directory, "bun.lock")) || existsSync(resolve(directory, "bun.lockb")))
75
- return { name: "bun", args: ["add", "--dev"] };
120
+ return "bun";
76
121
  if (existsSync(resolve(directory, "pnpm-lock.yaml")))
77
- return { name: "pnpm", args: ["add", "--save-dev"] };
122
+ return "pnpm";
78
123
  if (existsSync(resolve(directory, "yarn.lock")))
79
- return { name: "yarn", args: ["add", "--dev"] };
80
- return { name: "npm", args: ["install", "--save-dev", "--no-fund", "--no-audit"] };
124
+ return "yarn";
125
+ if (existsSync(resolve(directory, "package-lock.json")))
126
+ return "npm";
127
+ }
128
+ function packageManagerFromInvocation() {
129
+ const named = process.env.npm_config_user_agent?.split("/")[0];
130
+ return isPackageManager(named) ? named : undefined;
131
+ }
132
+ function isPackageManager(value) {
133
+ return value === "bun" || value === "npm" || value === "pnpm" || value === "yarn";
81
134
  }
82
135
  const localDirectories = {
83
136
  "@phreshos/core": "core-sdk",
84
137
  "@phreshos/client": "client-sdk",
85
- "@phreshos/server": "server-sdk"
138
+ "@phreshos/server": "server-sdk",
139
+ "@phreshos/react": "react-sdk",
140
+ "@phreshos/cli": "cli"
141
+ };
142
+ const managers = {
143
+ bun: {
144
+ name: "bun",
145
+ installArgs: ["install"],
146
+ addArgs: section => ["add", ...section === "devDependencies" ? ["--dev"] : []]
147
+ },
148
+ npm: {
149
+ name: "npm",
150
+ installArgs: ["install", "--no-fund", "--no-audit"],
151
+ addArgs: section => ["install", ...section === "devDependencies" ? ["--save-dev"] : ["--save"], "--no-fund", "--no-audit"]
152
+ },
153
+ pnpm: {
154
+ name: "pnpm",
155
+ installArgs: ["install"],
156
+ addArgs: section => ["add", ...section === "devDependencies" ? ["--save-dev"] : ["--save-prod"]]
157
+ },
158
+ yarn: {
159
+ name: "yarn",
160
+ installArgs: ["install"],
161
+ addArgs: section => ["add", ...section === "devDependencies" ? ["--dev"] : []]
162
+ }
86
163
  };
package/dist/project.js CHANGED
@@ -6,11 +6,10 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
6
6
  }
7
7
  return path;
8
8
  };
9
- import { layers } from "@phreshos/core";
9
+ import { isRelativeValue, layers } from "@phreshos/core";
10
10
  import { existsSync } from "node:fs";
11
11
  import { resolve } from "node:path";
12
12
  import { pathToFileURL } from "node:url";
13
- import { isRelativeValue } from "./relative-value.js";
14
13
  export const configFile = "phresh.config.ts";
15
14
  export async function readConfig(directory = process.cwd()) {
16
15
  const path = resolve(directory, configFile);
@@ -40,7 +39,7 @@ function coherent(config) {
40
39
  throw new Error("A program's identity is kebab-case, because it is also the name of its directory");
41
40
  if (!config.server && !config.client)
42
41
  throw new Error("A program must have a server half, a client half, or both");
43
- for (const field of ["name", "version", "description", "icons"]) {
42
+ for (const field of ["name", "version", "description", "icon"]) {
44
43
  if (config[field] !== undefined && typeof config[field] !== "string")
45
44
  throw new Error(`A program's ${field} must be text`);
46
45
  }
@@ -0,0 +1,105 @@
1
+ import { cancel, confirm, intro, isCancel, log, outro, select, spinner, text } from "@clack/prompts";
2
+ import colors from "picocolors";
3
+ import { column, heading, line } from "./style.js";
4
+ /** Signals an ordinary interactive cancellation rather than an operation failure. */
5
+ export class PromptCancelled extends Error {
6
+ }
7
+ /**
8
+ * One interaction language shared by commands that can ask questions.
9
+ *
10
+ * Clack owns terminal behavior. This adapter owns only the product's wording
11
+ * and the rule that automation never opens or waits for a prompt.
12
+ */
13
+ export default function prompts() {
14
+ const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
15
+ function begin(title, context) {
16
+ if (interactive)
17
+ intro(`${colors.bold(title)}${context ? ` ${colors.dim(`· ${context}`)}` : ""}`);
18
+ else
19
+ heading(title, context);
20
+ }
21
+ function finish(message) {
22
+ if (interactive)
23
+ outro(colors.bold(message));
24
+ else
25
+ heading(message);
26
+ }
27
+ function detail(label, value, source) {
28
+ if (interactive)
29
+ log.message(`${colors.dim(column(label))}${value}${source ? ` ${colors.dim(source)}` : ""}`);
30
+ else
31
+ line(label, value, source);
32
+ }
33
+ function message(value = "") {
34
+ if (interactive)
35
+ log.message(value);
36
+ else
37
+ console.log(value ? ` ${value}` : "");
38
+ }
39
+ async function progress(message, completed, work) {
40
+ if (!interactive)
41
+ return await work();
42
+ const indicator = spinner();
43
+ indicator.start(message);
44
+ try {
45
+ const result = await work();
46
+ indicator.clear();
47
+ log.success(completed, { spacing: 0 });
48
+ return result;
49
+ }
50
+ catch (error) {
51
+ indicator.error(`${message} failed`);
52
+ throw error;
53
+ }
54
+ }
55
+ async function ask(explanation, question, fallback) {
56
+ if (!interactive)
57
+ throw new Error(`${question} Supply the corresponding option when no terminal is attached`);
58
+ log.message(colors.dim(explanation));
59
+ const value = await text({
60
+ message: question,
61
+ placeholder: fallback,
62
+ defaultValue: fallback
63
+ });
64
+ if (isCancel(value))
65
+ stop();
66
+ return value.trim();
67
+ }
68
+ async function yes(explanation, question, fallback) {
69
+ if (!interactive)
70
+ throw new Error(`${question} Supply the corresponding option when no terminal is attached`);
71
+ log.message(colors.dim(explanation));
72
+ const value = await confirm({ message: question, initialValue: fallback });
73
+ if (isCancel(value))
74
+ stop();
75
+ return value;
76
+ }
77
+ async function choose(explanation, question, values, fallback) {
78
+ if (!interactive)
79
+ throw new Error(`${question} Supply the corresponding option when no terminal is attached`);
80
+ log.message(colors.dim(explanation));
81
+ const value = await select({
82
+ message: question,
83
+ options: values.map(value => ({ value, label: value })),
84
+ initialValue: fallback
85
+ });
86
+ if (isCancel(value))
87
+ stop();
88
+ return value;
89
+ }
90
+ function stop() {
91
+ cancel("No changes made");
92
+ throw new PromptCancelled();
93
+ }
94
+ return {
95
+ interactive,
96
+ begin,
97
+ finish,
98
+ detail,
99
+ message,
100
+ progress,
101
+ ask,
102
+ yes,
103
+ choose
104
+ };
105
+ }
package/dist/style.js CHANGED
@@ -1,16 +1,8 @@
1
- /**
2
- * How every command looks.
3
- *
4
- * Six escapes rather than a dependency, and nothing at all where a
5
- * terminal would print them instead of obeying them. Shared because four
6
- * commands speaking three visual languages is worse than any one of
7
- * them: a person reading `phresh install` after `phresh init` should not
8
- * have to work out that it is the same tool.
9
- */
10
- const colour = process.stdout.isTTY && !process.env.NO_COLOR;
11
- const escape = String.fromCharCode(27);
12
- export const dim = (text) => colour ? `${escape}[2m${text}${escape}[22m` : text;
13
- export const bold = (text) => colour ? `${escape}[1m${text}${escape}[22m` : text;
1
+ import colors from "picocolors";
2
+ /** The quiet and emphatic text used by non-interactive command reports. */
3
+ export const dim = colors.dim;
4
+ export const bold = colors.bold;
5
+ export const accent = colors.cyan;
14
6
  // A label, what it says, and where that came from. The label is quiet
15
7
  // and the value is not, because the value is the thing being reported.
16
8
  //
@@ -18,7 +10,10 @@ export const bold = (text) => colour ? `${escape}[1m${text}${escape}[22m` : text
18
10
  // even when one overruns: a label that runs into its value is worse than
19
11
  // a column that does not line up.
20
12
  export function line(label, value, note) {
21
- console.log(` ${dim(label.padEnd(12))}${value}${note ? ` ${dim(note)}` : ""}`);
13
+ console.log(` ${dim(column(label))}${value}${note ? ` ${dim(note)}` : ""}`);
14
+ }
15
+ export function column(label) {
16
+ return label.length < 12 ? label.padEnd(12) : `${label} `;
22
17
  }
23
18
  export function heading(title, note) {
24
19
  console.log("");
@@ -0,0 +1,28 @@
1
+ # Phresh Program
2
+
3
+ A small Counter built as a complete Program. Its Node.js Server owns the
4
+ number, while its React Client reads and increments that same authoritative
5
+ state.
6
+
7
+ ```bash
8
+ bun install
9
+ bun phresh dev
10
+ ```
11
+
12
+ Development starts the Server source and Vite Client together. For the
13
+ production shape, build and attach the Program with:
14
+
15
+ ```bash
16
+ bun phresh start
17
+ ```
18
+
19
+ The structure follows the endpoint boundaries directly:
20
+
21
+ ```text
22
+ source/
23
+ ├── client/ React interface
24
+ └── server/ authoritative counter and API
25
+ ```
26
+
27
+ The Program declaration lives in `phresh.config.ts`. Its own service contract
28
+ is documented in `api-docs.md`.
@@ -0,0 +1,38 @@
1
+ # Counter API
2
+
3
+ The Server owns one number for the lifetime of its Process.
4
+
5
+ | Name | Destination | Interaction | Payload | Answer |
6
+ | --- | --- | --- | --- | --- |
7
+ | `read` | Server | `ask()` | `undefined` | `number` |
8
+ | `increment` | Server | `publish()` | `undefined` | None |
9
+ | `changed` | Server | `subscribe()` | `number` | None |
10
+
11
+ ## Ask the Server: `read`
12
+
13
+ `read` is a question addressed to this Process's Server. It answers with the
14
+ current counter value.
15
+
16
+ ```ts
17
+ const count = await process.server.ask<number>("read")
18
+ ```
19
+
20
+ ## Publish to the Server: `increment`
21
+
22
+ `increment` is a one-way event addressed to this Process's Server. It adds one
23
+ to the counter and does not produce an answer.
24
+
25
+ ```ts
26
+ process.server.publish("increment")
27
+ ```
28
+
29
+ ## Subscribe to the Server: `changed`
30
+
31
+ After an increment, the Server emits `changed` with the new counter value.
32
+ Anyone holding that Process's Server can subscribe to the event.
33
+
34
+ ```ts
35
+ process.server.subscribe<number>("changed", count => {
36
+ // use the latest count
37
+ })
38
+ ```
@@ -0,0 +1,30 @@
1
+ # Logs
2
+ logs
3
+ *.log
4
+ npm-debug.log*
5
+ yarn-debug.log*
6
+ yarn-error.log*
7
+ pnpm-debug.log*
8
+ lerna-debug.log*
9
+
10
+ node_modules
11
+ dist
12
+ dist-ssr
13
+ *.local
14
+
15
+ # Made by `phresh pack` in the project root.
16
+ /*.zip
17
+
18
+ # Editor directories and files
19
+ .vscode/*
20
+ !.vscode/extensions.json
21
+ .idea
22
+ .DS_Store
23
+ *.suo
24
+ *.ntvs*
25
+ *.njsproj
26
+ *.sln
27
+ *.sw?
28
+
29
+ # Made by the system the first time this program runs.
30
+ storage
Binary file
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "phresh-program",
3
+ "private": true,
4
+ "version": "0.1.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "phresh dev",
8
+ "start": "phresh start"
9
+ },
10
+ "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",
15
+ "react": "^19.2.8",
16
+ "react-dom": "^19.2.8"
17
+ },
18
+ "devDependencies": {
19
+ "@phreshos/cli": "^0.1.5",
20
+ "@types/node": "^26.2.0",
21
+ "@types/react": "^19.2.18",
22
+ "@types/react-dom": "^19.2.4",
23
+ "@vitejs/plugin-react": "^6.0.5",
24
+ "tsx": "^4.23.12",
25
+ "typescript": "^7.0.2",
26
+ "vite": "^8.2.1"
27
+ }
28
+ }
@@ -0,0 +1,109 @@
1
+ import { defineConfig } from "@phreshos/core"
2
+
3
+ /**
4
+ * This is the Program's authoring declaration, not a runtime configuration
5
+ * loaded by either endpoint. The Phresh CLI reads it and derives the concrete
6
+ * Program description needed for development, production, installation, or
7
+ * packaging.
8
+ *
9
+ * Production uses the built locations and commands declared below.
10
+ * Development replaces only each endpoint's location and start command with
11
+ * its `development` declaration. Packaging relocates the production files
12
+ * into the Program archive. Relative paths begin at this project directory.
13
+ */
14
+ export default defineConfig({
15
+
16
+ // Permanent public address of the Program. It is kebab-case because the
17
+ // system also uses it as the installed directory name. Unlike `name`, it
18
+ // is an identifier and must remain stable across releases.
19
+ identity: "phresh-program",
20
+
21
+ // Human-facing metadata shown by the desktop and authoring tools. None of
22
+ // these values determines the Program's identity.
23
+ name: "Phresh Program",
24
+ description: "A simple counter whose state lives on the Server.",
25
+ version: "0.1.0",
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",
31
+
32
+ // One authored PNG. Installation gives it a canonical name and the system
33
+ // derives the standard hosted icon sizes from it.
34
+ icon: "icon.png",
35
+
36
+ // Prepares both production endpoint directories. The CLI runs it from this
37
+ // project before `phresh start`, `phresh install`, and `phresh pack`.
38
+ // `phresh dev` does not build and uses the declarations below instead.
39
+ buildCommand: "node --import tsx source/build.ts",
40
+
41
+ // A Process may run its Server and Client independently. This declaration
42
+ // says how the Server is prepared and started; it does not merge Server
43
+ // code into the browser Client.
44
+ server: {
45
+
46
+ // Production directory containing the Server artifact. The start
47
+ // command runs with this directory as its working directory.
48
+ location: "dist/server",
49
+ startCommand: "node main.js",
50
+
51
+ // An install command is optional and runs inside `location` when the
52
+ // Program is installed. This build bundles its Server dependencies, so
53
+ // the example does not need one.
54
+ // installCommand: "npm install",
55
+
56
+ // Declared endpoints start in a default Process unless `start` is
57
+ // false. A false value keeps the capability available for an explicit
58
+ // Process launch without starting it automatically.
59
+ // start: false,
60
+
61
+ development: {
62
+
63
+ // `phresh dev` runs this command from the project directory. The
64
+ // 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"
67
+ }
68
+ },
69
+
70
+ // The Client declaration also defines the initial Window created for it.
71
+ // It contains presentation defaults only; live Window state belongs to
72
+ // each running Process.
73
+ client: {
74
+
75
+ // Production directory containing the browser application's
76
+ // `index.html` and all files reachable from it.
77
+ location: "dist/client",
78
+
79
+ // Initial Window values. The title defaults to the Program name when
80
+ // omitted. This example chooses a fixed initial size while leaving
81
+ // placement, layer, and minimized state to their system defaults.
82
+ title: "Phresh Program",
83
+ size: { width: 600, height: 500 },
84
+
85
+ // Geometry accepts finite pixel numbers or linear values. Fractions
86
+ // and percentages are relative to the selected desktop layer, and a
87
+ // pixel offset may be combined with either form.
88
+ // size: { width: "50% + 20", height: 440 },
89
+ // position: { x: "1/2 + 10", y: 40 },
90
+
91
+ // `window` is the ordinary framed layer. `under` and `over` are
92
+ // structurally isolated, frameless desktop layers.
93
+ // layer: "window",
94
+
95
+ // The initial Window may also be declared to open minimized.
96
+ // minimize: true,
97
+
98
+ development: {
99
+
100
+ // Development Clients are addressed by URL rather than a local
101
+ // artifact directory. The CLI starts this optional tool, waits for
102
+ // the URL to respond, and only then launches the Program. The dev
103
+ // server must allow the desktop origin through CORS; this project's
104
+ // Vite configuration does so.
105
+ url: "http://localhost:5200/",
106
+ startCommand: "vite dev --config vite.client.ts"
107
+ }
108
+ }
109
+ })
@@ -0,0 +1,17 @@
1
+ import { externalDependencies } from "@/vite.server"
2
+ import { writeFile } from "node:fs/promises"
3
+ import packageConfig from "@/package.json"
4
+ import { build } from "vite"
5
+
6
+ const dependencies: Partial<typeof packageConfig.dependencies> = {}
7
+
8
+ for (const externalDependency of externalDependencies) {
9
+
10
+ dependencies[externalDependency] = packageConfig.dependencies[externalDependency]
11
+ }
12
+
13
+ await build({ configFile: "vite.server.ts" })
14
+
15
+ await build({ configFile: "vite.client.ts" })
16
+
17
+ await writeFile("dist/server/package.json", JSON.stringify({ type: "module", dependencies }))
@@ -0,0 +1,36 @@
1
+ import { useSubscribe } from "@phreshos/react"
2
+ import { useEffect, useState } from "react"
3
+ import { current } from "@phreshos/client"
4
+
5
+ function Counter() {
6
+
7
+ const [count, setCount] = useState<number>()
8
+
9
+ useSubscribe(current.server, "changed", setCount)
10
+
11
+ useEffect(function () {
12
+
13
+ current.server.ask<number>("read").then(setCount)
14
+
15
+ }, [])
16
+
17
+ return <main className="counter-card">
18
+
19
+ <span className="label">Phresh Program</span>
20
+
21
+ <h1>Server counter</h1>
22
+
23
+ <p>The value belongs to the Server and is shared with every Client representation.</p>
24
+
25
+ <button type="button" onClick={() => current.server.publish("increment")}>
26
+
27
+ count is {count ?? "…"}
28
+
29
+ </button>
30
+
31
+ </main>
32
+ }
33
+
34
+ export default function App() {
35
+ return <Counter />
36
+ }
@@ -0,0 +1 @@
1
+ /// <reference types="vite/client" />