@phreshos/cli 0.1.6 → 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/README.md CHANGED
@@ -41,6 +41,7 @@ System.
41
41
  phresh system install
42
42
  phresh system uninstall
43
43
  phresh system status
44
+ phresh system version
44
45
  phresh system start
45
46
  phresh system stop
46
47
  phresh system enable
@@ -59,9 +60,9 @@ 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
60
61
  native manager owns it after the CLI exits and restarts a failed active
61
62
  service. `start` and `stop` change current execution only; `enable` and
62
- `disable` change automatic startup only. `status` reads installation,
63
- registration, startup, process, and local-intake readiness without changing
64
- any of them.
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.
65
66
 
66
67
  Installation files and persistent System state have separate homes. Removing
67
68
  the System unregisters its service and removes its release files while keeping
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
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";
@@ -17,7 +17,11 @@ const program = new Command()
17
17
  .description("Create Programs and manage PhreshOS")
18
18
  .version(version, "-v, --version")
19
19
  .showHelpAfterError()
20
- .showSuggestionAfterError();
20
+ .showSuggestionAfterError()
21
+ .configureOutput({
22
+ writeOut: value => process.stdout.write(spaced(value)),
23
+ writeErr: value => process.stderr.write(spaced(value))
24
+ });
21
25
  program.addHelpText("after", "\nRun phresh <command> --help for detailed command guidance.\n");
22
26
  describe(program.command("create")
23
27
  .description("create a new Program project")
@@ -119,11 +123,18 @@ try {
119
123
  catch (error) {
120
124
  if (error instanceof PromptCancelled)
121
125
  process.exitCode = 0;
126
+ else if (error instanceof ReportedFailure)
127
+ process.exitCode = 1;
122
128
  else {
123
129
  console.error(`\n phresh: ${error instanceof Error ? error.message : String(error)}\n`);
124
130
  process.exitCode = 1;
125
131
  }
126
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
+ }
127
138
  function attached(name, summary, detail, mode) {
128
139
  describe(program.command(name)
129
140
  .description(summary)
package/dist/create.js CHANGED
@@ -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 = [
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
  }
@@ -1,5 +1,6 @@
1
1
  import SystemLifecycle from "./lifecycle.js";
2
- import prompts from "../prompts.js";
2
+ import prompts, { ReportedFailure } from "../prompts.js";
3
+ import { accent, caution, dim, negative, positive } from "../style.js";
3
4
  /** Attach the System lifecycle without mixing it with Program commands. */
4
5
  export default function systemCommands(program, provided) {
5
6
  let lifecycle = provided;
@@ -11,8 +12,7 @@ export default function systemCommands(program, provided) {
11
12
  const interaction = prompts();
12
13
  interaction.begin("Install System", "official stable release");
13
14
  const status = await interaction.progress("Installing PhreshOS", "PhreshOS installed", () => current().install());
14
- report(interaction, status);
15
- interaction.finish("System installed");
15
+ interaction.finish(`PhreshOS ${accent(status.installed?.version ?? "")} installed`);
16
16
  });
17
17
  system.command("uninstall")
18
18
  .description("remove the System installation and service")
@@ -24,37 +24,63 @@ export default function systemCommands(program, provided) {
24
24
  interaction.finish("System uninstalled");
25
25
  });
26
26
  system.command("status")
27
- .description("show installation, service, and readiness state")
27
+ .description("show the System version and operating state")
28
28
  .action(async function () {
29
+ const status = await installed(current());
29
30
  const interaction = prompts();
30
31
  interaction.begin("System Status");
31
- report(interaction, await current().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)}`);
32
42
  });
33
- action(system, "start", "start the background service", () => current().start());
34
- action(system, "stop", "stop the background service", () => current().stop());
35
- action(system, "enable", "enable automatic startup", () => current().enable());
36
- action(system, "disable", "disable automatic startup", () => current().disable());
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());
37
47
  return system;
38
48
  }
39
- function action(system, name, description, work) {
49
+ function action(system, name, description, current, work) {
40
50
  system.command(name)
41
51
  .description(description)
42
52
  .action(async function () {
53
+ const lifecycle = current();
54
+ await installed(lifecycle);
43
55
  const interaction = prompts();
44
56
  interaction.begin(`System ${title(name)}`);
45
- const status = await interaction.progress(`${title(name)}ing PhreshOS`, `PhreshOS ${past(name)}`, work);
46
- report(interaction, status);
57
+ await interaction.progress(`${title(name)}ing PhreshOS`, `PhreshOS ${past(name)}`, () => work(lifecycle));
47
58
  interaction.finish(`System ${past(name)}`);
48
59
  });
49
60
  }
50
61
  function report(interaction, status) {
51
- interaction.detail("installation", status.installed ? `installed · ${status.installed.version}` : "not installed");
52
- interaction.detail("service", status.registered ? "registered" : "not registered");
53
- interaction.detail("startup", status.enabled ? "enabled" : "disabled");
54
- interaction.detail("process", status.running ? `running${status.pid ? ` · ${status.pid}` : ""}` : "stopped");
55
- interaction.detail("intake", status.ready ? `ready · ${status.intake}` : "not ready");
56
- interaction.detail("files", status.root);
57
- interaction.detail("log", status.log);
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");
58
84
  }
59
85
  function title(value) {
60
86
  return `${value[0]?.toUpperCase()}${value.slice(1)}`;
@@ -4,6 +4,7 @@ import { intakeReady, waitForIntake } from "./readiness.js";
4
4
  import systemPaths from "./paths.js";
5
5
  import systemService from "./service/index.js";
6
6
  import nodeExecutable from "./node.js";
7
+ import { existsSync } from "node:fs";
7
8
  import { join } from "node:path";
8
9
  /** Coordinates acquisition, immutable files, and the native service as one transaction. */
9
10
  export default class SystemLifecycle {
@@ -123,7 +124,8 @@ export default class SystemLifecycle {
123
124
  }
124
125
  catch (error) {
125
126
  const message = error instanceof Error ? error.message : String(error);
126
- throw new Error(`${message}. Service log: ${installation.paths.log}`, { cause: error });
127
+ const log = existsSync(installation.paths.log) ? `. Service log: ${installation.paths.log}` : "";
128
+ throw new Error(`${message}${log}`, { cause: error });
127
129
  }
128
130
  }
129
131
  async activate(prepared, previous, state) {
@@ -79,11 +79,11 @@ Description=PhreshOS System
79
79
  [Service]
80
80
  Type=simple
81
81
  ExecStart=${quote(definition.executable)} ${quote(definition.entry)}
82
- WorkingDirectory=${quote(definition.directory)}
82
+ WorkingDirectory=${setting(definition.directory)}
83
83
  Restart=on-failure
84
84
  RestartSec=2
85
- StandardOutput=append:${quote(definition.output)}
86
- StandardError=append:${quote(definition.output)}
85
+ StandardOutput=append:${setting(definition.output)}
86
+ StandardError=append:${setting(definition.output)}
87
87
 
88
88
  [Install]
89
89
  WantedBy=default.target
@@ -92,3 +92,8 @@ WantedBy=default.target
92
92
  function quote(value) {
93
93
  return JSON.stringify(value);
94
94
  }
95
+ function setting(value) {
96
+ if (value.includes("\n") || value.includes("\r"))
97
+ throw new Error("A systemd service path cannot contain a line break");
98
+ return value.replaceAll("%", "%%");
99
+ }
@@ -16,7 +16,7 @@
16
16
  "react-dom": "^19.2.8"
17
17
  },
18
18
  "devDependencies": {
19
- "@phreshos/cli": "^0.1.6",
19
+ "@phreshos/cli": "^0.1.7",
20
20
  "@types/node": "^26.2.0",
21
21
  "@types/react": "^19.2.18",
22
22
  "@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.6",
4
+ "version": "0.1.7",
5
5
  "description": "The Phresh command-line interface for Program projects and system management.",
6
6
  "engines": {
7
7
  "node": ">=20.10"
@@ -13,6 +13,7 @@
13
13
  "test": "node --run compile && node --test tests/*.test.mjs",
14
14
  "verify": "node --run build && node --run test && node scripts/verify-package.mjs",
15
15
  "verify:system-release": "node --run compile && node scripts/verify-system-release.mjs",
16
+ "verify:linux-service": "node --run compile && node scripts/verify-linux-service.mjs",
16
17
  "verify:macos-service": "node --run compile && node scripts/verify-macos-service.mjs",
17
18
  "prepack": "node --run build"
18
19
  },