@phreshos/cli 0.1.4 → 0.1.6

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.
Files changed (40) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +63 -26
  3. package/dist/build-template.js +73 -42
  4. package/dist/cli.js +135 -241
  5. package/dist/create.js +73 -80
  6. package/dist/derive.js +4 -4
  7. package/dist/init.js +113 -122
  8. package/dist/pack.js +9 -8
  9. package/dist/program-intake.js +10 -2
  10. package/dist/project-dependency.js +13 -44
  11. package/dist/project.js +2 -3
  12. package/dist/prompts.js +105 -0
  13. package/dist/style.js +9 -14
  14. package/dist/system/command.js +70 -0
  15. package/dist/system/installation.js +241 -0
  16. package/dist/system/lifecycle.js +174 -0
  17. package/dist/system/node.js +13 -0
  18. package/dist/system/paths.js +28 -0
  19. package/dist/system/process.js +23 -0
  20. package/dist/system/readiness.js +29 -0
  21. package/dist/system/release.js +86 -0
  22. package/dist/system/service/index.js +11 -0
  23. package/dist/system/service/linux.js +94 -0
  24. package/dist/system/service/macos.js +123 -0
  25. package/dist/system/types.js +0 -0
  26. package/dist/template/README.md +1 -1
  27. package/dist/template/icon.png +0 -0
  28. package/dist/template/package.json +8 -7
  29. package/dist/template/phresh.config.ts +65 -25
  30. package/dist/template/source/build.ts +3 -3
  31. package/dist/template/source/client/app.tsx +3 -8
  32. package/dist/template/source/server/main.ts +1 -1
  33. package/dist/template/vite.client.ts +22 -0
  34. package/dist/template/vite.server.ts +27 -0
  35. package/dist/template.json +5 -0
  36. package/package.json +36 -4
  37. package/dist/questions.js +0 -25
  38. package/dist/relative-value.js +0 -118
  39. package/dist/template/icons/128x128.png +0 -0
  40. package/dist/template/vite.config.ts +0 -38
@@ -0,0 +1,94 @@
1
+ import { execute } from "../process.js";
2
+ import { existsSync } from "node:fs";
3
+ import { mkdir, rename, rm, writeFile } from "node:fs/promises";
4
+ import { dirname, join } from "node:path";
5
+ import { randomUUID } from "node:crypto";
6
+ const command = "systemctl";
7
+ const unit = "phreshos.service";
8
+ export default class LinuxSystemService {
9
+ run;
10
+ file;
11
+ constructor(userHome, run = execute) {
12
+ this.run = run;
13
+ this.file = join(userHome, ".config", "systemd", "user", unit);
14
+ }
15
+ async inspect() {
16
+ const registered = existsSync(this.file);
17
+ const active = await this.run(command, ["--user", "is-active", "--quiet", unit]);
18
+ const enabled = await this.run(command, ["--user", "is-enabled", "--quiet", unit]);
19
+ const pid = active.code === 0 ? await this.run(command, ["--user", "show", unit, "--property", "MainPID", "--value"]) : undefined;
20
+ const value = pid && /^[0-9]+$/.test(pid.stdout.trim()) ? Number(pid.stdout.trim()) : undefined;
21
+ return {
22
+ registered,
23
+ enabled: registered && enabled.code === 0,
24
+ running: registered && active.code === 0,
25
+ ...(value ? { pid: value } : {})
26
+ };
27
+ }
28
+ async register(definition) {
29
+ await this.stop();
30
+ await mkdir(dirname(this.file), { recursive: true });
31
+ await mkdir(dirname(definition.output), { recursive: true });
32
+ const temporary = `${this.file}.${randomUUID()}.tmp`;
33
+ try {
34
+ await writeFile(temporary, service(definition), { mode: 0o600 });
35
+ await rename(temporary, this.file);
36
+ }
37
+ finally {
38
+ await rm(temporary, { force: true });
39
+ }
40
+ await this.require(["--user", "daemon-reload"]);
41
+ }
42
+ async unregister() {
43
+ await this.stop();
44
+ await this.run(command, ["--user", "disable", unit]);
45
+ await rm(this.file, { force: true });
46
+ await this.require(["--user", "daemon-reload"]);
47
+ await this.run(command, ["--user", "reset-failed", unit]);
48
+ }
49
+ async start() {
50
+ if (!existsSync(this.file))
51
+ throw new Error("The PhreshOS System service is not registered");
52
+ await this.require(["--user", "start", unit]);
53
+ }
54
+ async stop() {
55
+ const state = await this.run(command, ["--user", "is-active", "--quiet", unit]);
56
+ if (state.code === 0)
57
+ await this.require(["--user", "stop", unit]);
58
+ }
59
+ async enable() {
60
+ if (!existsSync(this.file))
61
+ throw new Error("The PhreshOS System service is not registered");
62
+ await this.require(["--user", "enable", unit]);
63
+ }
64
+ async disable() {
65
+ if (!existsSync(this.file))
66
+ throw new Error("The PhreshOS System service is not registered");
67
+ await this.require(["--user", "disable", unit]);
68
+ }
69
+ async require(args) {
70
+ const result = await this.run(command, args);
71
+ if (result.code !== 0)
72
+ throw new Error(result.stderr.trim() || result.stdout.trim() || `${command} exited with code ${result.code}`);
73
+ }
74
+ }
75
+ function service(definition) {
76
+ return `[Unit]
77
+ Description=PhreshOS System
78
+
79
+ [Service]
80
+ Type=simple
81
+ ExecStart=${quote(definition.executable)} ${quote(definition.entry)}
82
+ WorkingDirectory=${quote(definition.directory)}
83
+ Restart=on-failure
84
+ RestartSec=2
85
+ StandardOutput=append:${quote(definition.output)}
86
+ StandardError=append:${quote(definition.output)}
87
+
88
+ [Install]
89
+ WantedBy=default.target
90
+ `;
91
+ }
92
+ function quote(value) {
93
+ return JSON.stringify(value);
94
+ }
@@ -0,0 +1,123 @@
1
+ import { execute } from "../process.js";
2
+ import { existsSync } from "node:fs";
3
+ import { mkdir, rename, rm, writeFile } from "node:fs/promises";
4
+ import { dirname, join } from "node:path";
5
+ import { randomUUID } from "node:crypto";
6
+ const command = "/bin/launchctl";
7
+ const defaultLabel = "com.phreshos.system";
8
+ export default class MacOSSystemService {
9
+ run;
10
+ label;
11
+ plist;
12
+ domain;
13
+ target;
14
+ constructor(userHome, run = execute, label = defaultLabel) {
15
+ this.run = run;
16
+ this.label = label;
17
+ const uid = process.getuid?.();
18
+ if (uid === undefined)
19
+ throw new Error("The current macOS user could not be identified");
20
+ this.plist = join(userHome, "Library", "LaunchAgents", `${this.label}.plist`);
21
+ this.domain = `gui/${uid}`;
22
+ this.target = `${this.domain}/${this.label}`;
23
+ }
24
+ async inspect() {
25
+ const registered = existsSync(this.plist);
26
+ const service = await this.run(command, ["print", this.target]);
27
+ const disabled = await this.run(command, ["print-disabled", this.domain]);
28
+ const explicitlyDisabled = new RegExp(`"${escapePattern(this.label)}"\\s*=>\\s*(?:true|disabled)`).test(disabled.stdout);
29
+ const pid = /\bpid\s*=\s*(\d+)/.exec(service.stdout)?.[1];
30
+ return {
31
+ registered,
32
+ enabled: registered && !explicitlyDisabled,
33
+ running: service.code === 0 && /\bstate\s*=\s*running\b/.test(service.stdout),
34
+ ...(pid ? { pid: Number(pid) } : {})
35
+ };
36
+ }
37
+ async register(definition) {
38
+ await this.stop();
39
+ await mkdir(dirname(this.plist), { recursive: true });
40
+ await mkdir(dirname(definition.output), { recursive: true });
41
+ const temporary = `${this.plist}.${randomUUID()}.tmp`;
42
+ try {
43
+ await writeFile(temporary, plist(this.label, definition), { mode: 0o600 });
44
+ await rename(temporary, this.plist);
45
+ }
46
+ finally {
47
+ await rm(temporary, { force: true });
48
+ }
49
+ }
50
+ async unregister() {
51
+ await this.stop();
52
+ await rm(this.plist, { force: true });
53
+ }
54
+ async start() {
55
+ if (!existsSync(this.plist))
56
+ throw new Error("The PhreshOS System service is not registered");
57
+ const state = await this.inspect();
58
+ if (state.running)
59
+ return;
60
+ const loaded = await this.run(command, ["print", this.target]);
61
+ if (loaded.code === 0)
62
+ await this.require(["kickstart", "-k", this.target]);
63
+ else
64
+ await this.require(["bootstrap", this.domain, this.plist]);
65
+ }
66
+ async stop() {
67
+ const loaded = await this.run(command, ["print", this.target]);
68
+ if (loaded.code === 0)
69
+ await this.require(["bootout", this.target]);
70
+ }
71
+ async enable() {
72
+ if (!existsSync(this.plist))
73
+ throw new Error("The PhreshOS System service is not registered");
74
+ await this.require(["enable", this.target]);
75
+ }
76
+ async disable() {
77
+ if (!existsSync(this.plist))
78
+ throw new Error("The PhreshOS System service is not registered");
79
+ await this.require(["disable", this.target]);
80
+ }
81
+ async require(args) {
82
+ const result = await this.run(command, args);
83
+ if (result.code !== 0)
84
+ throw new Error(result.stderr.trim() || result.stdout.trim() || `${command} exited with code ${result.code}`);
85
+ }
86
+ }
87
+ function plist(label, definition) {
88
+ return `<?xml version="1.0" encoding="UTF-8"?>
89
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
90
+ <plist version="1.0">
91
+ <dict>
92
+ <key>Label</key>
93
+ <string>${xml(label)}</string>
94
+ <key>ProgramArguments</key>
95
+ <array>
96
+ <string>${xml(definition.executable)}</string>
97
+ <string>${xml(definition.entry)}</string>
98
+ </array>
99
+ <key>WorkingDirectory</key>
100
+ <string>${xml(definition.directory)}</string>
101
+ <key>RunAtLoad</key>
102
+ <true/>
103
+ <key>KeepAlive</key>
104
+ <dict>
105
+ <key>SuccessfulExit</key>
106
+ <false/>
107
+ </dict>
108
+ <key>ThrottleInterval</key>
109
+ <integer>2</integer>
110
+ <key>StandardOutPath</key>
111
+ <string>${xml(definition.output)}</string>
112
+ <key>StandardErrorPath</key>
113
+ <string>${xml(definition.output)}</string>
114
+ </dict>
115
+ </plist>
116
+ `;
117
+ }
118
+ function xml(value) {
119
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;");
120
+ }
121
+ function escapePattern(value) {
122
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
123
+ }
File without changes
@@ -1,4 +1,4 @@
1
- # Get Started
1
+ # Phresh Program
2
2
 
3
3
  A small Counter built as a complete Program. Its Node.js Server owns the
4
4
  number, while its React Client reads and increments that same authoritative
Binary file
@@ -1,21 +1,22 @@
1
1
  {
2
- "name": "get-started",
2
+ "name": "phresh-program",
3
3
  "private": true,
4
4
  "version": "0.1.0",
5
5
  "type": "module",
6
6
  "scripts": {
7
- "phresh": "phresh"
7
+ "dev": "phresh dev",
8
+ "start": "phresh start"
8
9
  },
9
10
  "dependencies": {
10
- "@phreshos/client": "^0.1.0",
11
- "@phreshos/react": "^0.1.0",
12
- "@phreshos/server": "^0.1.0",
13
- "@phreshos/core": "^0.1.0",
11
+ "@phreshos/client": "^0.1.1",
12
+ "@phreshos/react": "^0.1.1",
13
+ "@phreshos/server": "^0.1.1",
14
+ "@phreshos/core": "^0.1.1",
14
15
  "react": "^19.2.8",
15
16
  "react-dom": "^19.2.8"
16
17
  },
17
18
  "devDependencies": {
18
- "@phreshos/cli": "^0.1.4",
19
+ "@phreshos/cli": "^0.1.6",
19
20
  "@types/node": "^26.2.0",
20
21
  "@types/react": "^19.2.18",
21
22
  "@types/react-dom": "^19.2.4",
@@ -1,69 +1,109 @@
1
1
  import { defineConfig } from "@phreshos/core"
2
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
+ */
3
14
  export default defineConfig({
4
15
 
5
- // The stable, kebab-case address used by programs and the system.
6
- identity: "get-started",
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",
7
20
 
8
- // Human-facing details shown by the desktop and development tools.
9
- name: "Get Started",
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",
10
24
  description: "A simple counter whose state lives on the Server.",
11
25
  version: "0.1.0",
12
26
 
13
- // The official entry point for this Program's own API.
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.
14
30
  apiDocs: "api-docs.md",
15
31
 
16
- // A directory containing files named by size, such as 128x128.png.
17
- icons: "icons",
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",
18
35
 
19
- // Runs before production start, install, and pack; dev uses the
20
- // development declarations below instead.
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.
21
39
  buildCommand: "node --import tsx source/build.ts",
22
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.
23
44
  server: {
24
45
 
25
- // The built server directory and the command run from within it.
46
+ // Production directory containing the Server artifact. The start
47
+ // command runs with this directory as its working directory.
26
48
  location: "dist/server",
27
49
  startCommand: "node main.js",
28
50
 
29
- // Optional preparation performed once before the installed server
30
- // first starts. This bundled example does not need it.
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.
31
54
  // installCommand: "npm install",
32
55
 
33
- // A declared half starts by default. Set this to false when it
34
- // should exist but only be included when explicitly requested.
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.
35
59
  // start: false,
36
60
 
37
61
  development: {
38
62
 
39
- // Development server commands run from the project root.
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.
40
66
  startCommand: "node --watch --import tsx source/server/main.ts"
41
67
  }
42
68
  },
43
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.
44
73
  client: {
45
74
 
46
- // The directory containing the built browser application.
75
+ // Production directory containing the browser application's
76
+ // `index.html` and all files reachable from it.
47
77
  location: "dist/client",
48
78
 
49
- title: "Get Started",
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",
50
83
  size: { width: 600, height: 500 },
51
84
 
52
- // Optional defaults for the window created with this client half.
53
- // title: "Get Started",
54
- // Fractions and percentages are relative to the Window's layer. Pixel
55
- // offsets may be combined with them in one linear expression.
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.
56
88
  // size: { width: "50% + 20", height: 440 },
57
89
  // position: { x: "1/2 + 10", y: 40 },
58
- // layer: "window", // "under" and "over" are frameless desktop layers.
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.
59
96
  // minimize: true,
60
97
 
61
98
  development: {
62
99
 
63
- // The CLI starts this command, then waits for the URL before it
64
- // launches the development Program.
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.
65
105
  url: "http://localhost:5200/",
66
- startCommand: "vite dev"
106
+ startCommand: "vite dev --config vite.client.ts"
67
107
  }
68
108
  }
69
109
  })
@@ -1,4 +1,4 @@
1
- import { externalDependencies } from "@/vite.config"
1
+ import { externalDependencies } from "@/vite.server"
2
2
  import { writeFile } from "node:fs/promises"
3
3
  import packageConfig from "@/package.json"
4
4
  import { build } from "vite"
@@ -10,8 +10,8 @@ for (const externalDependency of externalDependencies) {
10
10
  dependencies[externalDependency] = packageConfig.dependencies[externalDependency]
11
11
  }
12
12
 
13
- await build({ build: { ssr: true } })
13
+ await build({ configFile: "vite.server.ts" })
14
14
 
15
- await build()
15
+ await build({ configFile: "vite.client.ts" })
16
16
 
17
17
  await writeFile("dist/server/package.json", JSON.stringify({ type: "module", dependencies }))
@@ -1,4 +1,4 @@
1
- import { CurrentProvider, useSubscribe } from "@phreshos/react"
1
+ import { useSubscribe } from "@phreshos/react"
2
2
  import { useEffect, useState } from "react"
3
3
  import { current } from "@phreshos/client"
4
4
 
@@ -16,7 +16,7 @@ function Counter() {
16
16
 
17
17
  return <main className="counter-card">
18
18
 
19
- <span className="label">Get Started</span>
19
+ <span className="label">Phresh Program</span>
20
20
 
21
21
  <h1>Server counter</h1>
22
22
 
@@ -32,10 +32,5 @@ function Counter() {
32
32
  }
33
33
 
34
34
  export default function App() {
35
-
36
- return <CurrentProvider waitServer fallback={<main className="loading">Connecting to the Server…</main>}>
37
-
38
- <Counter />
39
-
40
- </CurrentProvider>
35
+ return <Counter />
41
36
  }
@@ -6,7 +6,7 @@ let count = 0
6
6
 
7
7
  current.answer("read", () => count)
8
8
 
9
- current.subscribe("increment", function () {
9
+ current.subscribe("increment", async function () {
10
10
 
11
11
  count += 1
12
12
 
@@ -0,0 +1,22 @@
1
+ import react from "@vitejs/plugin-react"
2
+ import { defineConfig } from "vite"
3
+ import { resolve } from "node:path"
4
+
5
+ export default defineConfig({
6
+ root: "source/client",
7
+ plugins: [react()],
8
+ base: "./",
9
+ resolve: {
10
+ tsconfigPaths: true,
11
+ dedupe: ["react"]
12
+ },
13
+ server: {
14
+ cors: true,
15
+ port: 5200,
16
+ strictPort: true
17
+ },
18
+ build: {
19
+ emptyOutDir: true,
20
+ outDir: resolve(import.meta.dirname, "dist/client")
21
+ }
22
+ })
@@ -0,0 +1,27 @@
1
+ import packageConfig from "./package.json" with { type: "json" }
2
+ import { defineConfig } from "vite"
3
+ import { resolve } from "node:path"
4
+
5
+ export const externalDependencies: (keyof typeof packageConfig.dependencies)[] = [
6
+
7
+ // Add packages here that should NOT be bundled during server, Example: "sqlite3"
8
+ ]
9
+
10
+ export default defineConfig({
11
+ root: "source/server",
12
+ resolve: {
13
+ tsconfigPaths: true
14
+ },
15
+ ssr: {
16
+ noExternal: true,
17
+ external: externalDependencies
18
+ },
19
+ build: {
20
+ ssr: true,
21
+ emptyOutDir: true,
22
+ outDir: resolve(import.meta.dirname, "dist/server"),
23
+ rolldownOptions: {
24
+ input: "main.ts"
25
+ }
26
+ }
27
+ })
@@ -0,0 +1,5 @@
1
+ {
2
+ "repository": "PhreshOS/phresh-program",
3
+ "version": "0.1.0",
4
+ "development": true
5
+ }
package/package.json CHANGED
@@ -1,11 +1,19 @@
1
1
  {
2
2
  "name": "@phreshos/cli",
3
3
  "type": "module",
4
- "version": "0.1.4",
4
+ "version": "0.1.6",
5
5
  "description": "The Phresh command-line interface for Program projects and system management.",
6
+ "engines": {
7
+ "node": ">=20.10"
8
+ },
6
9
  "scripts": {
10
+ "clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
7
11
  "compile": "tsc --noEmit false --outDir dist --rootDir source --rewriteRelativeImportExtensions true --allowImportingTsExtensions true",
8
- "build": "node --run compile && node dist/build-template.js",
12
+ "build": "node --run clean && node --run compile && node dist/build-template.js",
13
+ "test": "node --run compile && node --test tests/*.test.mjs",
14
+ "verify": "node --run build && node --run test && node scripts/verify-package.mjs",
15
+ "verify:system-release": "node --run compile && node scripts/verify-system-release.mjs",
16
+ "verify:macos-service": "node --run compile && node scripts/verify-macos-service.mjs",
9
17
  "prepack": "node --run build"
10
18
  },
11
19
  "bin": {
@@ -13,11 +21,35 @@
13
21
  },
14
22
  "files": [
15
23
  "dist",
24
+ "LICENSE",
16
25
  "README.md"
17
26
  ],
27
+ "author": "Zohayr SLILEH",
28
+ "license": "MIT",
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "git+https://github.com/PhreshOS/cli.git"
32
+ },
33
+ "bugs": {
34
+ "url": "https://github.com/PhreshOS/cli/issues"
35
+ },
36
+ "homepage": "https://github.com/PhreshOS/cli#readme",
37
+ "keywords": [
38
+ "phreshos",
39
+ "cli",
40
+ "program"
41
+ ],
42
+ "publishConfig": {
43
+ "access": "public",
44
+ "provenance": true
45
+ },
46
+ "packageManager": "bun@1.3.14",
18
47
  "dependencies": {
19
- "@phreshos/core": "^0.1.0",
20
- "adm-zip": "^0.6.0"
48
+ "@clack/prompts": "^1.7.0",
49
+ "@phreshos/core": "^0.1.1",
50
+ "adm-zip": "^0.6.0",
51
+ "commander": "^15.0.0",
52
+ "picocolors": "^1.1.1"
21
53
  },
22
54
  "devDependencies": {
23
55
  "@types/adm-zip": "^0.5.8",
package/dist/questions.js DELETED
@@ -1,25 +0,0 @@
1
- import { bold, dim } from "./style.js";
2
- import { createInterface } from "node:readline/promises";
3
- /** One prompt language shared by every interactive command. */
4
- export default function questions() {
5
- const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
6
- const readline = interactive ? createInterface({ input: process.stdin, output: process.stdout }) : null;
7
- async function ask(explanation, question, fallback) {
8
- if (!readline)
9
- throw new Error(`${question} Supply the corresponding option when no terminal is attached`);
10
- const suffix = fallback === undefined ? "" : ` ${dim(`(${fallback})`)}`;
11
- console.log(` ${dim(explanation)}`);
12
- const said = (await readline.question(` ${bold(question)}${suffix} `)).trim();
13
- console.log("");
14
- return said || fallback || "";
15
- }
16
- async function yes(explanation, question, fallback) {
17
- return (await ask(explanation, question, fallback ? "Y/n" : "y/N")).toLowerCase().startsWith("y");
18
- }
19
- return {
20
- interactive,
21
- ask,
22
- yes,
23
- close: () => readline?.close()
24
- };
25
- }