@phreshos/cli 0.1.5 → 0.1.7

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Zohayr SLILEH
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # @phreshos/cli
2
2
 
3
- The `phresh` command for creating, running, packaging, installing, and
4
- uninstalling Programs.
3
+ The `phresh` command for creating and operating Programs, and for installing
4
+ and managing the PhreshOS System on the current machine.
5
5
 
6
6
  ## Package status
7
7
 
@@ -21,6 +21,7 @@ phresh install # lay this program out on this machine
21
21
  phresh uninstall # remove its installed form
22
22
  phresh start # run what your build left, and stay with it
23
23
  phresh dev # run from source, and stay with it
24
+ phresh system status # inspect the local System and its background service
24
25
  ```
25
26
 
26
27
  `phresh --help` lists them, `phresh <command> --help` explains one, and
@@ -28,11 +29,45 @@ phresh dev # run from source, and stay with it
28
29
  unknown command, an unknown flag and a malformed option are each refused
29
30
  and named.
30
31
 
31
- **This tool acts on the current project, and the list ends there.** It is
32
- not the machine's control panel it accepts no arbitrary program identity
33
- and has no word for a process, a window, a store or a setting. The system's
34
- local intake accepts exactly the Program this project declares, and so does
35
- this tool.
32
+ Every top-level Program command acts on the current project, and that list ends
33
+ there. It accepts no arbitrary Program identity and has no word for a Process,
34
+ Window, store, or setting. Machine lifecycle is isolated under `phresh system`;
35
+ it manages the System installation and native service, not the state inside the
36
+ System.
37
+
38
+ ## System lifecycle
39
+
40
+ ```bash
41
+ phresh system install
42
+ phresh system uninstall
43
+ phresh system status
44
+ phresh system version
45
+ phresh system start
46
+ phresh system stop
47
+ phresh system enable
48
+ phresh system disable
49
+ ```
50
+
51
+ `install` resolves the newest compatible stable release from the official
52
+ [`PhreshOS/system`](https://github.com/PhreshOS/system) GitHub Releases. It
53
+ downloads the production archive and adjacent checksum, verifies every byte,
54
+ installs production dependencies into a staged version directory, atomically
55
+ points the stable `current` path at it, then registers, enables, and starts the
56
+ native per-user service. The selected release and the service entry therefore
57
+ cannot become two competing sources of truth if installation is interrupted.
58
+ It never reads a source checkout and never requires Bun or TypeScript.
59
+
60
+ The System runs under `launchd` on macOS and `systemd --user` on Linux. The
61
+ native manager owns it after the CLI exits and restarts a failed active
62
+ service. `start` and `stop` change current execution only; `enable` and
63
+ `disable` change automatic startup only. `status` reports the installed version,
64
+ service readiness, and automatic startup without changing them; `version`
65
+ reports only the installed System release.
66
+
67
+ Installation files and persistent System state have separate homes. Removing
68
+ the System unregisters its service and removes its release files while keeping
69
+ `~/.phreshos`, including Programs and owner data. Windows remains unsupported
70
+ until the System has an equally strong local-intake authorization model there.
36
71
 
37
72
  ## create
38
73
 
@@ -55,11 +90,9 @@ phresh create status-board \
55
90
  ```
56
91
 
57
92
  Dependencies are installed by default. `--no-install` creates the same valid
58
- project and leaves installation as the first reported next step. A project
59
- created inside this repository's workspace uses its sibling dev-kits through
60
- the workspace protocol. A standalone project, including one created by a
61
- distributed CLI, uses the published versions embedded in the template. Both
62
- choices pass through the same dependency resolver used by `init`.
93
+ project and leaves installation as the first reported next step. Generated and
94
+ initialized Programs always use the published package ranges embedded in the
95
+ CLI; repository layout never changes dependency meaning.
63
96
 
64
97
  ## Saying something to a program you start
65
98
 
package/dist/cli.js CHANGED
@@ -1,22 +1,27 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command, Option } from "commander";
3
3
  import metadata from "../package.json" with { type: "json" };
4
- import { PromptCancelled } from "./prompts.js";
4
+ import { PromptCancelled, ReportedFailure } from "./prompts.js";
5
5
  import create from "./create.js";
6
6
  import install from "./install.js";
7
7
  import launch from "./launch.js";
8
8
  import init from "./init.js";
9
9
  import pack from "./pack.js";
10
10
  import uninstall from "./uninstall.js";
11
+ import systemCommands from "./system/command.js";
11
12
  const { version } = metadata;
12
13
  const coreRange = metadata.dependencies["@phreshos/core"];
13
14
  const runOptionPrefix = "--run-option-";
14
15
  const program = new Command()
15
16
  .name("phresh")
16
- .description("Create and manage Programs")
17
+ .description("Create Programs and manage PhreshOS")
17
18
  .version(version, "-v, --version")
18
19
  .showHelpAfterError()
19
- .showSuggestionAfterError();
20
+ .showSuggestionAfterError()
21
+ .configureOutput({
22
+ writeOut: value => process.stdout.write(spaced(value)),
23
+ writeErr: value => process.stderr.write(spaced(value))
24
+ });
20
25
  program.addHelpText("after", "\nRun phresh <command> --help for detailed command guidance.\n");
21
26
  describe(program.command("create")
22
27
  .description("create a new Program project")
@@ -106,6 +111,7 @@ attached("dev", "run the development Program without installing", [
106
111
  "Runs the same attached lifecycle as start using the development",
107
112
  "declarations. A Client URL must respond within 15 seconds before launch."
108
113
  ], "development");
114
+ systemCommands(program);
109
115
  // Every command begins with the same breathing room. Keep this at the entry
110
116
  // point so individual commands never need to manufacture their own opening.
111
117
  console.log("");
@@ -117,11 +123,18 @@ try {
117
123
  catch (error) {
118
124
  if (error instanceof PromptCancelled)
119
125
  process.exitCode = 0;
126
+ else if (error instanceof ReportedFailure)
127
+ process.exitCode = 1;
120
128
  else {
121
129
  console.error(`\n phresh: ${error instanceof Error ? error.message : String(error)}\n`);
122
130
  process.exitCode = 1;
123
131
  }
124
132
  }
133
+ function spaced(value) {
134
+ if (value.endsWith("\n\n"))
135
+ return value;
136
+ return value.endsWith("\n") ? `${value}\n` : `${value}\n\n`;
137
+ }
125
138
  function attached(name, summary, detail, mode) {
126
139
  describe(program.command(name)
127
140
  .description(summary)
package/dist/create.js CHANGED
@@ -1,4 +1,4 @@
1
- import { projectDependency, installProjectDependencies, projectPackageManager, projectScript } from "./project-dependency.js";
1
+ import { installProjectDependencies, projectPackageManager, projectScript } from "./project-dependency.js";
2
2
  import prompts from "./prompts.js";
3
3
  import { accent, bold } from "./style.js";
4
4
  import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
@@ -36,7 +36,7 @@ export default async function create(options = {}, directory = process.cwd()) {
36
36
  try {
37
37
  cpSync(bundled.directory, staging, { recursive: true });
38
38
  renameSync(resolve(staging, "gitignore"), resolve(staging, ".gitignore"));
39
- customize(staging, target, identity, name, manager);
39
+ customize(staging, identity, name, manager);
40
40
  renameSync(staging, target);
41
41
  placed = true;
42
42
  // A project inside this repository becomes a real workspace member
@@ -60,6 +60,7 @@ export default async function create(options = {}, directory = process.cwd()) {
60
60
  console.log(bold(`\n${bundled.development ? "Run Development Program" : "Run Program"}`));
61
61
  console.log(accent(script));
62
62
  console.log(bold("\nYou can now open the project and start building your Program"));
63
+ console.log("");
63
64
  }
64
65
  function template() {
65
66
  const candidates = [
@@ -79,7 +80,7 @@ function template() {
79
80
  }
80
81
  throw new Error("The CLI template has not been built — run its build command and try again");
81
82
  }
82
- function customize(directory, finalDirectory, identity, name, manager) {
83
+ function customize(directory, identity, name, manager) {
83
84
  for (const path of textFiles(directory)) {
84
85
  let content = readFileSync(path, "utf-8");
85
86
  content = content.replaceAll("phresh-program", identity).replaceAll("Phresh Program", name);
@@ -93,15 +94,6 @@ function customize(directory, finalDirectory, identity, name, manager) {
93
94
  const manifestPath = resolve(directory, "package.json");
94
95
  const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
95
96
  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
97
  writeFileSync(manifestPath, JSON.stringify(manifest, null, 4) + "\n");
106
98
  }
107
99
  function textFiles(directory) {
@@ -120,13 +112,6 @@ function packageManager(value) {
120
112
  return value;
121
113
  throw new Error(`The package manager must be bun, npm, pnpm, or yarn; received "${value}"`);
122
114
  }
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
115
  function title(identity) {
131
116
  return identity.split("-").map(word => word[0].toUpperCase() + word.slice(1)).join(" ");
132
117
  }
@@ -1,37 +1,11 @@
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 { isAbsolute, relative, resolve } from "node:path";
4
+ import { 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
7
  return `${projectPackageManager(directory, declared).name} run ${script}`;
8
8
  }
9
- /**
10
- * Resolves an SDK through the one source policy shared by `init` and `create`.
11
- *
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
- */
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
9
  /** Detects the package manager declared by, present in, or invoking a project. */
36
10
  export function projectPackageManager(directory, declared, preferred) {
37
11
  const named = preferred ?? packageManagerName(declared) ?? packageManagerFromLocks(directory) ?? packageManagerFromInvocation();
@@ -57,9 +31,8 @@ export default async function ensureProjectDependency(name, range, directory = p
57
31
  if (manifest.dependencies?.[name] || manifest.devDependencies?.[name])
58
32
  return;
59
33
  const manager = projectPackageManager(directory, manifest.packageManager);
60
- const source = projectDependency(name, range, directory);
61
- line("dependency", name, `${manager.name}, ${source.local ? "local dev-kit" : range}`);
62
- await run(manager.name, [...manager.addArgs(section), source.installSpecifier], directory);
34
+ line("dependency", name, `${manager.name}, ${range}`);
35
+ await run(manager.name, [...manager.addArgs(section), `${name}@${range}`], directory);
63
36
  console.log("");
64
37
  }
65
38
  function run(command, args, directory, output = "inherit") {
@@ -85,21 +58,6 @@ function run(command, args, directory, output = "inherit") {
85
58
  });
86
59
  });
87
60
  }
88
- function accepts(range, version) {
89
- return range === "workspace:*" || range === version || range === `^${version}` || range === `~${version}`;
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
- }
103
61
  function manifestAt(directory) {
104
62
  const path = resolve(directory, "package.json");
105
63
  if (!existsSync(path))
@@ -132,13 +90,6 @@ function packageManagerFromInvocation() {
132
90
  function isPackageManager(value) {
133
91
  return value === "bun" || value === "npm" || value === "pnpm" || value === "yarn";
134
92
  }
135
- const localDirectories = {
136
- "@phreshos/core": "core-sdk",
137
- "@phreshos/client": "client-sdk",
138
- "@phreshos/server": "server-sdk",
139
- "@phreshos/react": "react-sdk",
140
- "@phreshos/cli": "cli"
141
- };
142
93
  const managers = {
143
94
  bun: {
144
95
  name: "bun",
package/dist/prompts.js CHANGED
@@ -1,9 +1,12 @@
1
1
  import { cancel, confirm, intro, isCancel, log, outro, select, spinner, text } from "@clack/prompts";
2
2
  import colors from "picocolors";
3
- import { column, heading, line } from "./style.js";
3
+ import { caution, column, ending, line, section } from "./style.js";
4
4
  /** Signals an ordinary interactive cancellation rather than an operation failure. */
5
5
  export class PromptCancelled extends Error {
6
6
  }
7
+ /** An unsuccessful command whose complete explanation has already been shown. */
8
+ export class ReportedFailure extends Error {
9
+ }
7
10
  /**
8
11
  * One interaction language shared by commands that can ask questions.
9
12
  *
@@ -16,26 +19,32 @@ export default function prompts() {
16
19
  if (interactive)
17
20
  intro(`${colors.bold(title)}${context ? ` ${colors.dim(`· ${context}`)}` : ""}`);
18
21
  else
19
- heading(title, context);
22
+ section(title, context);
20
23
  }
21
24
  function finish(message) {
22
25
  if (interactive)
23
26
  outro(colors.bold(message));
24
27
  else
25
- heading(message);
28
+ ending(message);
26
29
  }
27
30
  function detail(label, value, source) {
28
31
  if (interactive)
29
- log.message(`${colors.dim(column(label))}${value}${source ? ` ${colors.dim(source)}` : ""}`);
32
+ log.message(`${colors.dim(column(label))}${value}${source ? ` ${colors.dim(source)}` : ""}`, { spacing: 0 });
30
33
  else
31
34
  line(label, value, source);
32
35
  }
33
36
  function message(value = "") {
34
37
  if (interactive)
35
- log.message(value);
38
+ log.message(value, { spacing: 0 });
36
39
  else
37
40
  console.log(value ? ` ${value}` : "");
38
41
  }
42
+ function warning(value) {
43
+ if (interactive)
44
+ log.warning(value, { spacing: 0 });
45
+ else
46
+ line("warning", caution(value));
47
+ }
39
48
  async function progress(message, completed, work) {
40
49
  if (!interactive)
41
50
  return await work();
@@ -55,7 +64,7 @@ export default function prompts() {
55
64
  async function ask(explanation, question, fallback) {
56
65
  if (!interactive)
57
66
  throw new Error(`${question} Supply the corresponding option when no terminal is attached`);
58
- log.message(colors.dim(explanation));
67
+ log.message(colors.dim(explanation), { spacing: 0 });
59
68
  const value = await text({
60
69
  message: question,
61
70
  placeholder: fallback,
@@ -68,7 +77,7 @@ export default function prompts() {
68
77
  async function yes(explanation, question, fallback) {
69
78
  if (!interactive)
70
79
  throw new Error(`${question} Supply the corresponding option when no terminal is attached`);
71
- log.message(colors.dim(explanation));
80
+ log.message(colors.dim(explanation), { spacing: 0 });
72
81
  const value = await confirm({ message: question, initialValue: fallback });
73
82
  if (isCancel(value))
74
83
  stop();
@@ -77,7 +86,7 @@ export default function prompts() {
77
86
  async function choose(explanation, question, values, fallback) {
78
87
  if (!interactive)
79
88
  throw new Error(`${question} Supply the corresponding option when no terminal is attached`);
80
- log.message(colors.dim(explanation));
89
+ log.message(colors.dim(explanation), { spacing: 0 });
81
90
  const value = await select({
82
91
  message: question,
83
92
  options: values.map(value => ({ value, label: value })),
@@ -97,6 +106,7 @@ export default function prompts() {
97
106
  finish,
98
107
  detail,
99
108
  message,
109
+ warning,
100
110
  progress,
101
111
  ask,
102
112
  yes,
package/dist/style.js CHANGED
@@ -3,6 +3,9 @@ import colors from "picocolors";
3
3
  export const dim = colors.dim;
4
4
  export const bold = colors.bold;
5
5
  export const accent = colors.cyan;
6
+ export const positive = colors.green;
7
+ export const caution = colors.yellow;
8
+ export const negative = colors.red;
6
9
  // A label, what it says, and where that came from. The label is quiet
7
10
  // and the value is not, because the value is the thing being reported.
8
11
  //
@@ -17,6 +20,13 @@ export function column(label) {
17
20
  }
18
21
  export function heading(title, note) {
19
22
  console.log("");
23
+ section(title, note);
24
+ console.log("");
25
+ }
26
+ export function section(title, note) {
20
27
  console.log(` ${bold(title)}${note ? ` ${dim("·")} ${dim(note)}` : ""}`);
28
+ }
29
+ export function ending(message) {
30
+ console.log(` ${bold(message)}`);
21
31
  console.log("");
22
32
  }
@@ -0,0 +1,96 @@
1
+ import SystemLifecycle from "./lifecycle.js";
2
+ import prompts, { ReportedFailure } from "../prompts.js";
3
+ import { accent, caution, dim, negative, positive } from "../style.js";
4
+ /** Attach the System lifecycle without mixing it with Program commands. */
5
+ export default function systemCommands(program, provided) {
6
+ let lifecycle = provided;
7
+ const current = () => lifecycle ?? (lifecycle = new SystemLifecycle());
8
+ const system = program.command("system").description("install and manage the PhreshOS System");
9
+ system.command("install")
10
+ .description("install or update the System and start its service")
11
+ .action(async function () {
12
+ const interaction = prompts();
13
+ interaction.begin("Install System", "official stable release");
14
+ const status = await interaction.progress("Installing PhreshOS", "PhreshOS installed", () => current().install());
15
+ interaction.finish(`PhreshOS ${accent(status.installed?.version ?? "")} installed`);
16
+ });
17
+ system.command("uninstall")
18
+ .description("remove the System installation and service")
19
+ .action(async function () {
20
+ const interaction = prompts();
21
+ interaction.begin("Uninstall System");
22
+ await interaction.progress("Removing PhreshOS", "PhreshOS removed", () => current().uninstall());
23
+ interaction.message("Persistent System data was kept.");
24
+ interaction.finish("System uninstalled");
25
+ });
26
+ system.command("status")
27
+ .description("show the System version and operating state")
28
+ .action(async function () {
29
+ const status = await installed(current());
30
+ const interaction = prompts();
31
+ interaction.begin("System Status");
32
+ report(interaction, status);
33
+ interaction.finish(status.ready ? positive("System ready") : caution("System not ready"));
34
+ });
35
+ system.command("version")
36
+ .description("show the installed System version")
37
+ .action(async function () {
38
+ const status = await installed(current());
39
+ const interaction = prompts();
40
+ interaction.begin("System Version");
41
+ interaction.finish(`PhreshOS ${accent(status.installed.version)}`);
42
+ });
43
+ action(system, "start", "start the background service", current, lifecycle => lifecycle.start());
44
+ action(system, "stop", "stop the background service", current, lifecycle => lifecycle.stop());
45
+ action(system, "enable", "enable automatic startup", current, lifecycle => lifecycle.enable());
46
+ action(system, "disable", "disable automatic startup", current, lifecycle => lifecycle.disable());
47
+ return system;
48
+ }
49
+ function action(system, name, description, current, work) {
50
+ system.command(name)
51
+ .description(description)
52
+ .action(async function () {
53
+ const lifecycle = current();
54
+ await installed(lifecycle);
55
+ const interaction = prompts();
56
+ interaction.begin(`System ${title(name)}`);
57
+ await interaction.progress(`${title(name)}ing PhreshOS`, `PhreshOS ${past(name)}`, () => work(lifecycle));
58
+ interaction.finish(`System ${past(name)}`);
59
+ });
60
+ }
61
+ function report(interaction, status) {
62
+ interaction.detail("version", accent(status.installed?.version ?? "unknown"));
63
+ interaction.detail("service", service(status));
64
+ interaction.detail("startup", status.enabled ? positive("enabled") : dim("disabled"));
65
+ }
66
+ async function installed(lifecycle) {
67
+ const status = await lifecycle.status();
68
+ if (status.installed)
69
+ return { ...status, installed: status.installed };
70
+ const interaction = prompts();
71
+ interaction.begin("PhreshOS System");
72
+ interaction.warning("PhreshOS System is not installed");
73
+ interaction.finish(`${dim("Install it with")} ${accent("phresh system install")}`);
74
+ throw new ReportedFailure();
75
+ }
76
+ function service(status) {
77
+ if (status.ready)
78
+ return positive("ready");
79
+ if (status.running)
80
+ return caution("starting");
81
+ if (status.registered)
82
+ return caution("stopped");
83
+ return negative("not registered");
84
+ }
85
+ function title(value) {
86
+ return `${value[0]?.toUpperCase()}${value.slice(1)}`;
87
+ }
88
+ function past(value) {
89
+ if (value === "stop")
90
+ return "stopped";
91
+ if (value === "enable")
92
+ return "enabled";
93
+ if (value === "disable")
94
+ return "disabled";
95
+ return "started";
96
+ }