@phreshos/cli 0.1.0

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/dist/pack.js ADDED
@@ -0,0 +1,84 @@
1
+ import { readConfig, readManifest } from "./project.js";
2
+ import { existsSync, readFileSync, statSync } from "node:fs";
3
+ import { resolve } from "node:path";
4
+ import AdmZip from "adm-zip";
5
+ import build from "./build-command.js";
6
+ /**
7
+ * Package what is at each half's location.
8
+ *
9
+ * When the author config declares `buildCommand`, it completes before
10
+ * any files are gathered. Pack remains an assembler rather than a
11
+ * build tool: it invokes the author's command, then packages its result.
12
+ *
13
+ * The authoring tree and the installable form differ. A program may
14
+ * leave its halves anywhere; the package always has them in the same
15
+ * places, so the artifact's shape is the contract's rather than the
16
+ * author's. The program.json names those canonical places explicitly:
17
+ * a declared half always has a location, even when the system chose it.
18
+ *
19
+ * There is **no wrapping directory**. `program.json`, `server/`,
20
+ * `client/` and `icons/` sit at the package's root, and the directory
21
+ * the system installs into is named from the program's own `identity`.
22
+ * The archive itself therefore needs no second naming layer.
23
+ *
24
+ * The files go into the archive directly rather than through a staging
25
+ * directory: a copy of a build is a thing that can be stale, and there
26
+ * is nothing a staging directory was doing that the archive does not.
27
+ */
28
+ export default async function pack(directory = process.cwd()) {
29
+ const config = await readConfig(directory);
30
+ await build(config, directory);
31
+ const manifest = await readManifest(directory);
32
+ const version = config.version ?? manifest.version;
33
+ if (config.version && manifest.version && config.version !== manifest.version) {
34
+ console.log(`phresh: packing ${config.version}, though package.json says ${manifest.version}\n`);
35
+ }
36
+ const zip = new AdmZip();
37
+ if (config.server)
38
+ place(zip, directory, config.server.location, "server");
39
+ if (config.client) {
40
+ place(zip, directory, config.client.location, "client");
41
+ if (!zip.getEntry("client/index.html"))
42
+ throw new Error(`The client files have no index.html — ${config.client.location} is not where a client half is`);
43
+ }
44
+ // Icons are the program's own affair rather than either half's: a
45
+ // headless program is shown too. Packaged like a half, and like a
46
+ // half their place is not the program's to choose.
47
+ if (config.icons)
48
+ place(zip, directory, config.icons, "icons");
49
+ if (config.apiDocs)
50
+ document(zip, directory, config.apiDocs);
51
+ zip.addFile("program.json", Buffer.from(JSON.stringify(program(config, version), null, 4) + "\n"));
52
+ const archive = `${config.identity}@${version ?? "0.0.0"}.zip`;
53
+ zip.writeZip(resolve(directory, archive));
54
+ console.log(`\nPacked ${archive}`);
55
+ return archive;
56
+ }
57
+ // The derivation, and the whole of it. What the config says about the
58
+ // program passes through; what it says about the build does not. Half
59
+ // locations are rewritten to the canonical places inside the package.
60
+ function program(config, version) {
61
+ return {
62
+ identity: config.identity,
63
+ name: config.name,
64
+ version,
65
+ description: config.description,
66
+ apiDocs: config.apiDocs ? "api-docs.md" : undefined,
67
+ ...config.server && { server: { location: "server", start: config.server.start, installCommand: config.server.installCommand, startCommand: config.server.startCommand } },
68
+ ...config.client && { client: { location: "client", start: config.client.start, title: config.client.title, size: config.client.size, position: config.client.position, layer: config.client.layer, minimize: config.client.minimize } }
69
+ };
70
+ }
71
+ // What is at a half's location, where the package keeps it. A half that
72
+ // was declared and is not there is the answer to whether it was built.
73
+ function place(zip, directory, location, half) {
74
+ const from = resolve(directory, location);
75
+ if (!existsSync(from))
76
+ throw new Error(`The ${half} files are not at ${location} — nothing was built there`);
77
+ zip.addLocalFolder(from, half);
78
+ }
79
+ function document(zip, directory, location) {
80
+ const from = resolve(directory, location);
81
+ if (!existsSync(from) || !statSync(from).isFile())
82
+ throw new Error(`The API documentation file is not at ${location}`);
83
+ zip.addFile("api-docs.md", readFileSync(from));
84
+ }
@@ -0,0 +1,65 @@
1
+ import { connect } from "node:net";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ /**
5
+ * The local Program intake, from the CLI's side.
6
+ *
7
+ * A socket file rather than a port, because the file's permissions are
8
+ * the authorization: only the account that owns this machine can open
9
+ * it, and that account is exactly who may run and install programs on
10
+ * it. Nothing is sent to prove anything, because being able to connect
11
+ * is the proof.
12
+ *
13
+ * A message is a line. Closing our own side to mark the end of a
14
+ * question would make lifetime ambiguous. A line delimiter keeps the
15
+ * connection available for the stream of events that follows a launch.
16
+ *
17
+ * One question, then events until the system closes. Installing and
18
+ * uninstalling say one thing and end; running says one thing, then whatever
19
+ * the Program says, then how it ended. None pretends to be a remote method
20
+ * returning through an unrelated transport.
21
+ */
22
+ export const socketPath = join(homedir(), ".phreshos", "intake.sock");
23
+ export default function speak(question, heard, path = socketPath, signal) {
24
+ return new Promise(function (settle, refuse) {
25
+ const socket = connect(path);
26
+ let said = "";
27
+ let failed = null;
28
+ let aborted = false;
29
+ const abort = () => {
30
+ aborted = true;
31
+ socket.destroy();
32
+ };
33
+ if (signal?.aborted)
34
+ abort();
35
+ else
36
+ signal?.addEventListener("abort", abort, { once: true });
37
+ socket.on("connect", () => socket.write(JSON.stringify(question) + "\n"));
38
+ socket.on("data", function (chunk) {
39
+ said += String(chunk);
40
+ const lines = said.split("\n");
41
+ said = lines.pop() ?? "";
42
+ for (const line of lines)
43
+ if (line.trim()) {
44
+ const event = JSON.parse(line);
45
+ if (event.event === "error")
46
+ failed = new Error(String(event.message));
47
+ else
48
+ heard(event);
49
+ }
50
+ });
51
+ // The system is not running, or is running as somebody else. Said
52
+ // plainly, because "ENOENT" is not what went wrong from here.
53
+ socket.on("error", () => { if (!aborted)
54
+ refuse(new Error(`No system is listening at ${path} — start one, or check that it is yours`)); });
55
+ socket.on("close", () => {
56
+ signal?.removeEventListener("abort", abort);
57
+ if (aborted)
58
+ refuse(new Error("The attached launch was stopped"));
59
+ else if (failed)
60
+ refuse(failed);
61
+ else
62
+ settle();
63
+ });
64
+ });
65
+ }
@@ -0,0 +1,83 @@
1
+ import { line } from "./style.js";
2
+ import { spawn } from "node:child_process";
3
+ import { existsSync, readFileSync } from "node:fs";
4
+ import { resolve } from "node:path";
5
+ /** Returns the package-manager command that runs one project script. */
6
+ export function projectScript(directory, declared, script) {
7
+ return `${packageManager(directory, declared).name} run ${script}`;
8
+ }
9
+ /**
10
+ * Ensures one development dependency using the source appropriate to this CLI.
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 matching registry version instead. This
15
+ * decision belongs here once for both `init` and the future `create` command.
16
+ */
17
+ export default async function ensureProjectDependency(name, version, directory = process.cwd()) {
18
+ const manifestPath = resolve(directory, "package.json");
19
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
20
+ if (manifest.dependencies?.[name] || manifest.devDependencies?.[name])
21
+ return;
22
+ const manager = packageManager(directory, manifest.packageManager);
23
+ const source = packageSource(name, version);
24
+ line("dependency", name, `${manager.name}, ${source.local ? "local dev-kit" : `^${version}`}`);
25
+ await new Promise(function (settle, refuse) {
26
+ const child = spawn(manager.name, [...manager.args, source.specifier], {
27
+ cwd: directory,
28
+ stdio: "inherit",
29
+ shell: process.platform === "win32"
30
+ });
31
+ child.once("error", error => refuse(new Error(`Could not run ${manager.name}: ${error.message}`)));
32
+ child.once("exit", function (code, signal) {
33
+ if (signal)
34
+ refuse(new Error(`${manager.name} ended on ${signal}`));
35
+ else if (code !== 0)
36
+ refuse(new Error(`${manager.name} exited with ${code ?? 0}`));
37
+ else
38
+ settle();
39
+ });
40
+ });
41
+ console.log("");
42
+ }
43
+ function packageSource(name, version) {
44
+ const directory = resolve(import.meta.dirname, "..", "..", localDirectories[name]);
45
+ const manifest = manifestAt(directory);
46
+ if (manifest?.name === name && manifest.version === version)
47
+ return { specifier: directory, local: true };
48
+ return { specifier: `${name}@^${version}`, local: false };
49
+ }
50
+ function manifestAt(directory) {
51
+ const path = resolve(directory, "package.json");
52
+ if (!existsSync(path))
53
+ return null;
54
+ try {
55
+ return JSON.parse(readFileSync(path, "utf-8"));
56
+ }
57
+ catch {
58
+ return null;
59
+ }
60
+ }
61
+ function packageManager(directory, declared) {
62
+ const named = declared?.split("@")[0];
63
+ if (named === "bun")
64
+ return { name: "bun", args: ["add", "--dev"] };
65
+ if (named === "pnpm")
66
+ return { name: "pnpm", args: ["add", "--save-dev"] };
67
+ if (named === "yarn")
68
+ return { name: "yarn", args: ["add", "--dev"] };
69
+ if (named === "npm")
70
+ return { name: "npm", args: ["install", "--save-dev", "--no-fund", "--no-audit"] };
71
+ if (existsSync(resolve(directory, "bun.lock")) || existsSync(resolve(directory, "bun.lockb")))
72
+ return { name: "bun", args: ["add", "--dev"] };
73
+ if (existsSync(resolve(directory, "pnpm-lock.yaml")))
74
+ return { name: "pnpm", args: ["add", "--save-dev"] };
75
+ if (existsSync(resolve(directory, "yarn.lock")))
76
+ return { name: "yarn", args: ["add", "--dev"] };
77
+ return { name: "npm", args: ["install", "--save-dev", "--no-fund", "--no-audit"] };
78
+ }
79
+ const localDirectories = {
80
+ "@phreshos/core": "core-sdk",
81
+ "@phreshos/client": "client-sdk",
82
+ "@phreshos/server": "server-sdk"
83
+ };
@@ -0,0 +1,125 @@
1
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
2
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
3
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
4
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
5
+ });
6
+ }
7
+ return path;
8
+ };
9
+ import { layers } from "@phreshos/core";
10
+ import { existsSync } from "node:fs";
11
+ import { resolve } from "node:path";
12
+ import { pathToFileURL } from "node:url";
13
+ import { isRelativeValue } from "./relative-value.js";
14
+ export const configFile = "phresh.config.ts";
15
+ export async function readConfig(directory = process.cwd()) {
16
+ const path = resolve(directory, configFile);
17
+ if (!existsSync(path))
18
+ throw new Error(`There is no ${configFile} here — run: phresh init`);
19
+ // The supported Node runtime reads erasable TypeScript directly. Keeping
20
+ // this file typed gives an author editor assistance without another config
21
+ // format or a build step.
22
+ const loaded = await import(__rewriteRelativeImportExtension(pathToFileURL(path).href)).catch(function (error) {
23
+ throw new Error(`${configFile} could not be read (${error.message})`);
24
+ });
25
+ const config = loaded.default;
26
+ if (!config)
27
+ throw new Error(`${configFile} must export its config as the default export`);
28
+ coherent(config);
29
+ return config;
30
+ }
31
+ // Whether the words agree with each other. Nothing here touches a disk
32
+ // or asks whether a build ran: it is the same question the system asks
33
+ // of a program.json when it is constructed, asked earlier, where the
34
+ // person who mistyped it is still standing.
35
+ function coherent(config) {
36
+ // A name is the name of a directory before it is anything else, and
37
+ // the system checks it again where it makes one. Checked here too so
38
+ // the mistake is found by the CLI rather than at the runtime border.
39
+ if (typeof config.identity !== "string" || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(config.identity))
40
+ throw new Error("A program's identity is kebab-case, because it is also the name of its directory");
41
+ if (!config.server && !config.client)
42
+ throw new Error("A program must have a server half, a client half, or both");
43
+ for (const field of ["name", "version", "description", "icons"]) {
44
+ if (config[field] !== undefined && typeof config[field] !== "string")
45
+ throw new Error(`A program's ${field} must be text`);
46
+ }
47
+ if (config.apiDocs !== undefined && (typeof config.apiDocs !== "string" || config.apiDocs.trim().length === 0))
48
+ throw new Error("A program's apiDocs must be a non-empty path");
49
+ if (config.buildCommand !== undefined && (typeof config.buildCommand !== "string" || config.buildCommand.trim().length === 0))
50
+ throw new Error("A program's buildCommand must be non-empty text");
51
+ for (const half of ["server", "client"]) {
52
+ const declared = config[half];
53
+ if (declared === undefined)
54
+ continue;
55
+ if (typeof declared !== "object" || declared === null || Array.isArray(declared))
56
+ throw new Error(`A program's ${half} half must be a declaration`);
57
+ if (typeof declared.location !== "string")
58
+ throw new Error(`A declared ${half} half must have a location`);
59
+ if (declared.start !== undefined && typeof declared.start !== "boolean")
60
+ throw new Error(`A declared ${half} endpoint's start default must be true or false`);
61
+ }
62
+ if (!(config.server && (config.server.start ?? true)) && !(config.client && (config.client.start ?? true)))
63
+ throw new Error("A Program's default Process must start a server endpoint, a client endpoint, or both");
64
+ if (config.server && (typeof config.server.startCommand !== "string" || config.server.startCommand.length === 0))
65
+ throw new Error("A server half must say what starts it");
66
+ if (config.server?.installCommand !== undefined && typeof config.server.installCommand !== "string")
67
+ throw new Error("A server half's install command must be text");
68
+ if (config.client?.title !== undefined && typeof config.client.title !== "string")
69
+ throw new Error("A client half's title must be text");
70
+ if (config.client?.minimize !== undefined && typeof config.client.minimize !== "boolean")
71
+ throw new Error("A client half's minimize default must be true or false");
72
+ if (config.client && /^https?:\/\//i.test(config.client.location)) {
73
+ try {
74
+ new URL(config.client.location);
75
+ }
76
+ catch {
77
+ throw new Error("A client half's URL must be a valid HTTP or HTTPS URL");
78
+ }
79
+ }
80
+ // Said wrong is caught here, where the author is standing, rather
81
+ // than becoming a window in no layer at all.
82
+ if (config.client?.layer !== undefined && !layers.includes(config.client.layer))
83
+ throw new Error(`A client half's layer is one of ${layers.join(", ")} — not "${String(config.client.layer)}"`);
84
+ const serverDevelopment = config.server?.development;
85
+ if (serverDevelopment !== undefined && (typeof serverDevelopment !== "object" || serverDevelopment === null || Array.isArray(serverDevelopment)))
86
+ throw new Error("server.development must be a declaration");
87
+ if (serverDevelopment !== undefined && (typeof serverDevelopment.startCommand !== "string" || serverDevelopment.startCommand.trim().length === 0))
88
+ throw new Error("server.development.startCommand must be non-empty text");
89
+ const clientDevelopment = config.client?.development;
90
+ if (clientDevelopment !== undefined && (typeof clientDevelopment !== "object" || clientDevelopment === null || Array.isArray(clientDevelopment)))
91
+ throw new Error("client.development must be a declaration");
92
+ if (clientDevelopment !== undefined && !httpUrl(clientDevelopment.url))
93
+ throw new Error("client.development.url must be a valid HTTP or HTTPS URL");
94
+ if (clientDevelopment?.startCommand !== undefined && (typeof clientDevelopment.startCommand !== "string" || clientDevelopment.startCommand.trim().length === 0))
95
+ throw new Error("client.development.startCommand must be non-empty text");
96
+ for (const [what, value] of [["size", config.client?.size], ["position", config.client?.position]]) {
97
+ if (value === undefined)
98
+ continue;
99
+ if (typeof value !== "object" || value === null || Array.isArray(value))
100
+ throw new Error(`A window's ${what} must name both of its values`);
101
+ const pair = what === "size" ? [value.width, value.height] : [value.x, value.y];
102
+ if (!pair.every(isRelativeValue))
103
+ throw new Error(`A window's ${what} is a finite pixel number or a relative expression such as "50% + 10"`);
104
+ }
105
+ }
106
+ function httpUrl(value) {
107
+ if (typeof value !== "string" || value.trim().length === 0)
108
+ return false;
109
+ try {
110
+ const url = new URL(value);
111
+ return url.protocol === "http:" || url.protocol === "https:";
112
+ }
113
+ catch {
114
+ return false;
115
+ }
116
+ }
117
+ export async function readManifest(directory = process.cwd()) {
118
+ const path = resolve(directory, "package.json");
119
+ if (!existsSync(path))
120
+ throw new Error("There is no package.json here");
121
+ const manifest = JSON.parse(await import("node:fs/promises").then(fs => fs.readFile(path, "utf-8")));
122
+ if (typeof manifest.name !== "string" || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(manifest.name))
123
+ throw new Error("package.json must have a kebab-case name");
124
+ return manifest;
125
+ }
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Reduces a strict linear expression into `span * relative + pixels`.
3
+ *
4
+ * Numbers are absolute values. Percentages and `n/d` fractions are relative
5
+ * values. Addition and subtraction combine terms; multiplication and division
6
+ * scale the preceding term by a number. No arbitrary code or CSS is accepted.
7
+ */
8
+ export function parseRelativeValue(input) {
9
+ if (typeof input === "number")
10
+ return Number.isFinite(input) ? { relative: 0, pixels: clean(input) } : null;
11
+ if (typeof input !== "string" || input.trim().length === 0)
12
+ return null;
13
+ return new Reader(input).parse();
14
+ }
15
+ /** Returns whether a value belongs to the linear relative-value grammar. */
16
+ export function isRelativeValue(value) {
17
+ return parseRelativeValue(value) !== null;
18
+ }
19
+ class Reader {
20
+ position = 0;
21
+ source;
22
+ constructor(source) {
23
+ this.source = source;
24
+ }
25
+ parse() {
26
+ const value = { relative: 0, pixels: 0 };
27
+ let direction = this.sign();
28
+ while (this.position < this.source.length) {
29
+ const term = this.term();
30
+ if (!term)
31
+ return null;
32
+ value.relative += term.relative * direction;
33
+ value.pixels += term.pixels * direction;
34
+ if (!finite(value))
35
+ return null;
36
+ this.space();
37
+ if (this.position === this.source.length)
38
+ return { relative: clean(value.relative), pixels: clean(value.pixels) };
39
+ const operator = this.source[this.position];
40
+ if (operator !== "+" && operator !== "-")
41
+ return null;
42
+ this.position += 1;
43
+ direction = (operator === "+" ? 1 : -1) * this.sign();
44
+ }
45
+ return null;
46
+ }
47
+ term() {
48
+ const value = this.measurement();
49
+ if (!value)
50
+ return null;
51
+ while (true) {
52
+ this.space();
53
+ const operator = this.source[this.position];
54
+ if (operator !== "*" && operator !== "/")
55
+ return value;
56
+ this.position += 1;
57
+ const scalar = this.scalar();
58
+ if (scalar === null || operator === "/" && scalar === 0)
59
+ return null;
60
+ const factor = operator === "*" ? scalar : 1 / scalar;
61
+ value.relative *= factor;
62
+ value.pixels *= factor;
63
+ if (!finite(value))
64
+ return null;
65
+ }
66
+ }
67
+ measurement() {
68
+ this.space();
69
+ const remainder = this.source.slice(this.position);
70
+ const fraction = remainder.match(/^(\d+(?:\.\d+)?|\.\d+)\s*\/\s*(\d+(?:\.\d+)?|\.\d+)(?![\d.])/);
71
+ if (fraction) {
72
+ const denominator = Number(fraction[2]);
73
+ if (!denominator)
74
+ return null;
75
+ this.position += fraction[0].length;
76
+ return { relative: Number(fraction[1]) / denominator, pixels: 0 };
77
+ }
78
+ const percentage = remainder.match(/^(\d+(?:\.\d+)?|\.\d+)\s*%/);
79
+ if (percentage) {
80
+ this.position += percentage[0].length;
81
+ return { relative: Number(percentage[1]) / 100, pixels: 0 };
82
+ }
83
+ const number = this.number();
84
+ return number === null ? null : { relative: 0, pixels: number };
85
+ }
86
+ scalar() {
87
+ const direction = this.sign();
88
+ const value = this.number();
89
+ return value === null ? null : value * direction;
90
+ }
91
+ number() {
92
+ this.space();
93
+ const number = this.source.slice(this.position).match(/^(\d+(?:\.\d+)?|\.\d+)/)?.[0];
94
+ if (!number)
95
+ return null;
96
+ this.position += number.length;
97
+ return Number(number);
98
+ }
99
+ sign() {
100
+ this.space();
101
+ const sign = this.source[this.position];
102
+ if (sign !== "+" && sign !== "-")
103
+ return 1;
104
+ this.position += 1;
105
+ this.space();
106
+ return sign === "-" ? -1 : 1;
107
+ }
108
+ space() {
109
+ while (/\s/.test(this.source[this.position] ?? ""))
110
+ this.position += 1;
111
+ }
112
+ }
113
+ function finite(value) {
114
+ return Number.isFinite(value.relative) && Number.isFinite(value.pixels);
115
+ }
116
+ function clean(value) {
117
+ return Object.is(value, -0) ? 0 : value;
118
+ }
package/dist/style.js ADDED
@@ -0,0 +1,27 @@
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;
14
+ // A label, what it says, and where that came from. The label is quiet
15
+ // and the value is not, because the value is the thing being reported.
16
+ //
17
+ // Wide enough for the longest label any command uses, and a space kept
18
+ // even when one overruns: a label that runs into its value is worse than
19
+ // a column that does not line up.
20
+ export function line(label, value, note) {
21
+ console.log(` ${dim(label.padEnd(12))}${value}${note ? ` ${dim(note)}` : ""}`);
22
+ }
23
+ export function heading(title, note) {
24
+ console.log("");
25
+ console.log(` ${bold(title)}${note ? ` ${dim("·")} ${dim(note)}` : ""}`);
26
+ console.log("");
27
+ }
@@ -0,0 +1,15 @@
1
+ import { readConfig } from "./project.js";
2
+ import { dim, heading } from "./style.js";
3
+ import speak from "./program-intake.js";
4
+ /** Uninstall the Program declared by the current project. */
5
+ export default async function uninstall(everything = false, directory = process.cwd()) {
6
+ const config = await readConfig(directory);
7
+ await speak({ word: "uninstall", identity: config.identity, everything }, function (event) {
8
+ if (event.event !== "uninstalled")
9
+ return;
10
+ heading(config.name ?? config.identity, "uninstalled");
11
+ console.log(everything
12
+ ? ` ${dim("Its processes, installed files, stored data, and runtime record were removed.")}\n`
13
+ : ` ${dim("Its installed files were removed. Processes, stored data, and runtime state were kept.")}\n`);
14
+ });
15
+ }
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@phreshos/cli",
3
+ "type": "module",
4
+ "version": "0.1.0",
5
+ "description": "The Phresh command-line interface for Program projects and system management.",
6
+ "scripts": {
7
+ "build": "tsc --noEmit false --outDir dist --rootDir source --rewriteRelativeImportExtensions true --allowImportingTsExtensions true",
8
+ "prepack": "node --run build"
9
+ },
10
+ "bin": {
11
+ "phresh": "dist/cli.js"
12
+ },
13
+ "files": [
14
+ "dist",
15
+ "README.md"
16
+ ],
17
+ "dependencies": {
18
+ "@phreshos/core": "0.1.0",
19
+ "adm-zip": "^0.6.0"
20
+ },
21
+ "devDependencies": {
22
+ "@types/adm-zip": "^0.5.8",
23
+ "@types/node": "^25.9.5",
24
+ "typescript": "^6.0.3"
25
+ }
26
+ }