@phreshos/cli 0.1.36 → 0.1.37

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/cli.js CHANGED
@@ -9,7 +9,7 @@ import init from "./init.js";
9
9
  import pack from "./pack.js";
10
10
  import uninstall from "./uninstall.js";
11
11
  import systemCommands from "./system/command.js";
12
- import controlCommands from "./control-command.js";
12
+ import controlCommands from "./control/command.js";
13
13
  import describeCommands from "./describe-command.js";
14
14
  import { commandContract } from "./command-contract.js";
15
15
  const metadata = createRequire(import.meta.url)("../package.json");
@@ -0,0 +1,12 @@
1
+ import endpointCommands from "./endpoint.js";
2
+ import processCommands from "./process.js";
3
+ import programCommands from "./program.js";
4
+ import { connectSystem } from "./shared.js";
5
+ import windowCommands from "./window.js";
6
+ /** Expose the shared System handles as explicit CLI commands. */
7
+ export default function controlCommands(program, connect = connectSystem) {
8
+ programCommands(program, connect);
9
+ processCommands(program, connect);
10
+ endpointCommands(program, connect);
11
+ windowCommands(program, connect);
12
+ }
@@ -0,0 +1,82 @@
1
+ import {} from "commander";
2
+ import { commandContract } from "../command-contract.js";
3
+ import { clientOverrideOptions, endpointOptions, outputOptions } from "./options.js";
4
+ import { bounded, clientLaunch, connected, endpoint, endpointView, integer, output, payload, requireProcess, wait } from "./shared.js";
5
+ export default function endpointCommands(root, connect) {
6
+ const endpoints = commandContract(root.command("endpoint")
7
+ .description("inspect, control, and communicate with Process Endpoints"));
8
+ outputOptions(endpointOptions(endpoints.command("inspect")
9
+ .description("read whether one Endpoint is declared and running")))
10
+ .action(async (options) => withEndpoint(connect, options, async (process, name) => {
11
+ output(await endpointView(process, name), options.compact);
12
+ }));
13
+ outputOptions(clientOverrideOptions(endpointOptions(endpoints.command("start")
14
+ .description("start a fresh Endpoint incarnation"))))
15
+ .action(async (options) => withEndpoint(connect, options, async (process, name) => {
16
+ const overrides = clientLaunch(options);
17
+ if (name === "server" && overrides !== undefined)
18
+ throw new Error("Client overrides require --endpoint client");
19
+ if (name === "client")
20
+ await process.client.start(typeof overrides === "object" ? overrides : undefined);
21
+ else
22
+ await process.server.start();
23
+ output(await endpointView(process, name), options.compact);
24
+ }));
25
+ outputOptions(endpointOptions(endpoints.command("stop")
26
+ .description("stop one Endpoint")))
27
+ .action(async (options) => withEndpoint(connect, options, async (process, name) => {
28
+ await endpoint(process, name).stop();
29
+ output(await endpointView(process, name), options.compact);
30
+ }));
31
+ outputOptions(endpointOptions(endpoints.command("waitReady")
32
+ .alias("wait-ready")
33
+ .description("wait until the Server Endpoint reports readiness")
34
+ .option("--timeout <milliseconds>", "maximum wait in milliseconds", integer)))
35
+ .action(async (options) => withEndpoint(connect, options, async (process, name) => {
36
+ if (name !== "server")
37
+ throw new Error("waitReady requires --endpoint server");
38
+ await process.server.waitReady(timeout(options.timeout));
39
+ output(await endpointView(process, "server"), options.compact);
40
+ }));
41
+ outputOptions(endpointOptions(endpoints.command("ask")
42
+ .description("ask a Server event and return its answer")
43
+ .requiredOption("--event <event>", "event name")
44
+ .option("--payload <json>", "arbitrary event payload as JSON")
45
+ .option("--timeout <milliseconds>", "maximum wait in milliseconds", integer)))
46
+ .action(async (options) => withEndpoint(connect, options, async (process, name) => {
47
+ if (name !== "server")
48
+ throw new Error("ask requires --endpoint server");
49
+ const value = payload(options.payload);
50
+ const answer = options.timeout === undefined
51
+ ? await process.server.ask(options.event, value)
52
+ : await process.server.timeout(timeout(options.timeout)).ask(options.event, value);
53
+ output(answer, options.compact);
54
+ }));
55
+ outputOptions(endpointOptions(endpoints.command("publish")
56
+ .description("publish one event without waiting for an answer")
57
+ .requiredOption("--event <event>", "event name")
58
+ .option("--payload <json>", "arbitrary event payload as JSON")))
59
+ .action(async (options) => withEndpoint(connect, options, async (process, name) => {
60
+ endpoint(process, name).publish(options.event, payload(options.payload));
61
+ output(await endpointView(process, name), options.compact);
62
+ }));
63
+ outputOptions(endpointOptions(endpoints.command("wait")
64
+ .description("wait for the next event emitted by one live Endpoint")
65
+ .requiredOption("--event <event>", "event name")
66
+ .option("--timeout <milliseconds>", "maximum wait in milliseconds", integer)))
67
+ .action(async (options) => withEndpoint(connect, options, async (process, name) => {
68
+ output({
69
+ scope: `endpoint:${process.identity}:${name}`,
70
+ event: options.event,
71
+ payload: await wait(endpoint(process, name), options.event, timeout(options.timeout))
72
+ }, options.compact);
73
+ }));
74
+ }
75
+ async function withEndpoint(connect, options, action) {
76
+ await connected(connect, async (system) => {
77
+ await action(await requireProcess(system, options.process, options.program), options.endpoint);
78
+ });
79
+ }
80
+ function timeout(value) {
81
+ return value === undefined ? undefined : bounded(value, "--timeout", 1);
82
+ }
@@ -0,0 +1,37 @@
1
+ import { Option } from "commander";
2
+ import { collect } from "./shared.js";
3
+ export function outputOptions(command) {
4
+ return command.option("--compact", "write machine-readable JSON on one line");
5
+ }
6
+ export function processOptions(command) {
7
+ return command
8
+ .requiredOption("--process <identity>", "Process identity or Program-local name")
9
+ .option("--program <identity>", "owning Program identity when resolving a Process name");
10
+ }
11
+ export function endpointOptions(command) {
12
+ return processOptions(command)
13
+ .addOption(new Option("--endpoint <endpoint>", "Endpoint kind").choices(["server", "client"]).makeOptionMandatory());
14
+ }
15
+ export function clientOptions(command) {
16
+ return clientOverrideOptions(command)
17
+ .option("--client", "start the Client Endpoint")
18
+ .option("--no-client", "do not start the Client Endpoint");
19
+ }
20
+ export function clientOverrideOptions(command) {
21
+ return command
22
+ .option("--client-title <title>", "initial Window title")
23
+ .option("--client-width <value>", "initial Window width")
24
+ .option("--client-height <value>", "initial Window height")
25
+ .option("--client-x <value>", "initial Window horizontal position")
26
+ .option("--client-y <value>", "initial Window vertical position")
27
+ .addOption(new Option("--client-layer <layer>", "initial Window layer").choices(["window", "under", "over"]))
28
+ .option("--client-location <location>", "initial page beneath the Client location")
29
+ .option("--client-minimized", "open the Window minimized");
30
+ }
31
+ export function launchOptions(command) {
32
+ return clientOptions(command)
33
+ .option("--name <name>", "stable Program-local Process name")
34
+ .option("--server", "start the Server Endpoint")
35
+ .option("--no-server", "do not start the Server Endpoint")
36
+ .option("--option <name=value>", "immutable Process option; repeat for more values", collect, []);
37
+ }
@@ -0,0 +1,94 @@
1
+ import { Option } from "commander";
2
+ import { commandContract } from "../command-contract.js";
3
+ import { launchOptions, outputOptions, processOptions } from "./options.js";
4
+ import { bounded, connected, integer, launch, output, page, processView, requireProcess, requireProgram, wait } from "./shared.js";
5
+ export default function processCommands(root, connect) {
6
+ const process = commandContract(root.command("process")
7
+ .description("discover and control live Program executions"));
8
+ outputOptions(process.command("list")
9
+ .description("list live Processes with bounded filtering")
10
+ .option("--program <identity>", "restrict results to one Program")
11
+ .option("--search <text>", "case-insensitive identity or name search")
12
+ .option("--limit <count>", "maximum returned Processes", integer, 30)
13
+ .option("--offset <count>", "number of matching Processes to skip", integer, 0))
14
+ .action(async (options) => connected(connect, async (system) => {
15
+ const processes = options.program
16
+ ? await (await requireProgram(system, options.program)).process.list()
17
+ : await system.process.list();
18
+ const selected = page(processes, options.search, bounded(options.offset, "--offset", 0), bounded(options.limit, "--limit", 1, 100), value => `${value.identity}\n${value.name ?? ""}`);
19
+ output({ ...selected, data: await Promise.all(selected.data.map(processView)) }, options.compact);
20
+ }));
21
+ outputOptions(processOptions(process.command("inspect")
22
+ .description("read one live Process and its Endpoint state")))
23
+ .action(async (options) => connected(connect, async (system) => {
24
+ output(await processView(await requireProcess(system, options.process, options.program)), options.compact);
25
+ }));
26
+ outputOptions(launchOptions(process.command("create")
27
+ .description("create a Process")
28
+ .requiredOption("--program <identity>", "owning Program identity")))
29
+ .action(async (options) => connected(connect, async (system) => {
30
+ const program = await requireProgram(system, options.program);
31
+ output(await processView(await program.process.create(launch(options))), options.compact);
32
+ }));
33
+ outputOptions(launchOptions(process.command("findOrCreate")
34
+ .alias("find-or-create")
35
+ .description("find the named Process or create it atomically")
36
+ .requiredOption("--program <identity>", "owning Program identity")))
37
+ .action(async (options) => connected(connect, async (system) => {
38
+ const program = await requireProgram(system, options.program);
39
+ output(await processView(await program.process.findOrCreate(launch(options, true))), options.compact);
40
+ }));
41
+ outputOptions(processOptions(process.command("exit")
42
+ .description("exit one Process and all of its live Endpoints")))
43
+ .action(async (options) => connected(connect, async (system) => {
44
+ const process = await requireProcess(system, options.process, options.program);
45
+ const snapshot = await processView(process);
46
+ await process.exit();
47
+ output(snapshot, options.compact);
48
+ }));
49
+ outputOptions(process.command("wait")
50
+ .description("wait for one Process lifecycle event")
51
+ .addOption(new Option("--event <event>", "Process lifecycle event")
52
+ .choices(["endpointStart", "endpointStop", "create", "exit"])
53
+ .makeOptionMandatory())
54
+ .option("--process <identity>", "scope the event to one Process")
55
+ .option("--program <identity>", "scope the event to one Program")
56
+ .option("--timeout <milliseconds>", "maximum wait in milliseconds", integer))
57
+ .action(async (options) => connected(connect, async (system) => {
58
+ if (options.process && options.event === "create")
59
+ throw new Error("An individual Process does not emit create");
60
+ const target = options.process
61
+ ? await requireProcess(system, options.process, options.program)
62
+ : options.program
63
+ ? (await requireProgram(system, options.program)).process
64
+ : system.process;
65
+ const message = await wait(target, options.event, options.timeout === undefined
66
+ ? undefined
67
+ : bounded(options.timeout, "--timeout", 1));
68
+ output({
69
+ scope: options.process ? `process:${options.process}` : options.program ? `program:${options.program}` : "process",
70
+ event: options.event,
71
+ payload: await eventView(options.event, message, options.process ? target : undefined)
72
+ }, options.compact);
73
+ }));
74
+ }
75
+ async function eventView(event, message, scoped) {
76
+ if (event === "create")
77
+ return processView(message);
78
+ if (event === "endpointStart" || event === "endpointStop") {
79
+ const endpoint = message;
80
+ const process = await endpoint.process();
81
+ return {
82
+ process: process.identity,
83
+ endpoint: endpoint === process.server ? "server" : "client"
84
+ };
85
+ }
86
+ const exit = message;
87
+ const process = exit.process ?? scoped;
88
+ return {
89
+ ...(process ? { process: await processView(process) } : {}),
90
+ status: exit.status,
91
+ code: exit.code,
92
+ signal: exit.signal
93
+ };
94
+ }
@@ -0,0 +1,64 @@
1
+ import { Option } from "commander";
2
+ import { commandContract } from "../command-contract.js";
3
+ import { outputOptions } from "./options.js";
4
+ import { bounded, connected, integer, output, page, programView, requireProgram, wait } from "./shared.js";
5
+ export default function programCommands(root, connect) {
6
+ const program = commandContract(root.command("program")
7
+ .description("discover PhreshOS Programs and their agent documentation"));
8
+ outputOptions(program.command("list")
9
+ .description("list Programs with bounded filtering")
10
+ .option("--installed-only", "return only installed Programs")
11
+ .option("--search <text>", "case-insensitive identity, name, or description search")
12
+ .option("--limit <count>", "maximum returned Programs", integer, 30)
13
+ .option("--offset <count>", "number of matching Programs to skip", integer, 0))
14
+ .action(async (options) => connected(connect, async (system) => {
15
+ const programs = await system.program.list(options.installedOnly === true);
16
+ const selected = page(programs, options.search, bounded(options.offset, "--offset", 0), bounded(options.limit, "--limit", 1, 100), value => `${value.identity}\n${value.name}\n${value.description ?? ""}`);
17
+ output({ ...selected, data: await Promise.all(selected.data.map(programView)) }, options.compact);
18
+ }));
19
+ outputOptions(program.command("inspect")
20
+ .description("read one Program declaration and installed state")
21
+ .requiredOption("--program <identity>", "Program identity"))
22
+ .action(async (options) => connected(connect, async (system) => {
23
+ output(await programView(await requireProgram(system, options.program)), options.compact);
24
+ }));
25
+ outputOptions(program.command("agent")
26
+ .description("read a Program's own agent operating policy")
27
+ .requiredOption("--program <identity>", "Program identity"))
28
+ .action(async (options) => connected(connect, async (system) => {
29
+ const program = await requireProgram(system, options.program);
30
+ const content = await program.agent();
31
+ if (content === null)
32
+ throw new Error(`Program "${program.identity}" has no agent documentation`);
33
+ output({ program: program.identity, content }, options.compact);
34
+ }));
35
+ outputOptions(program.command("wait")
36
+ .description("wait for one Program registry event")
37
+ .addOption(new Option("--event <event>", "Program lifecycle event")
38
+ .choices(["create", "forget", "install", "uninstall"])
39
+ .makeOptionMandatory())
40
+ .option("--program <identity>", "scope forget or uninstall to one Program")
41
+ .option("--timeout <milliseconds>", "maximum wait in milliseconds", integer))
42
+ .action(async (options) => connected(connect, async (system) => {
43
+ if (options.program && options.event !== "forget" && options.event !== "uninstall") {
44
+ throw new Error("An individual Program emits only forget and uninstall");
45
+ }
46
+ const target = options.program ? await requireProgram(system, options.program) : system.program;
47
+ const message = await wait(target, options.event, timeout(options.timeout));
48
+ output({
49
+ scope: options.program ? `program:${options.program}` : "program",
50
+ event: options.event,
51
+ payload: await eventView(options.event, message)
52
+ }, options.compact);
53
+ }));
54
+ }
55
+ async function eventView(event, message) {
56
+ if (event === "uninstall") {
57
+ const value = message;
58
+ return { program: await programView(value.program), everythingRemoved: value.everythingRemoved };
59
+ }
60
+ return programView(message);
61
+ }
62
+ function timeout(value) {
63
+ return value === undefined ? undefined : bounded(value, "--timeout", 1);
64
+ }
@@ -0,0 +1,181 @@
1
+ import { System } from "@phreshos/node";
2
+ export const connectSystem = () => System.connect();
3
+ export async function connected(connect, action) {
4
+ const system = await connect();
5
+ try {
6
+ return await action(system);
7
+ }
8
+ finally {
9
+ await system.disconnect();
10
+ }
11
+ }
12
+ export async function requireProgram(system, identity) {
13
+ const program = await system.program.find(identity);
14
+ if (!program)
15
+ throw new Error(`Unknown Program "${identity}"`);
16
+ return program;
17
+ }
18
+ export async function requireProcess(system, identity, programIdentity) {
19
+ const process = programIdentity
20
+ ? await (await requireProgram(system, programIdentity)).process.find(identity)
21
+ : await system.process.find(identity);
22
+ if (!process)
23
+ throw new Error(`Unknown Process "${identity}"`);
24
+ return process;
25
+ }
26
+ export function endpoint(process, name) {
27
+ return name === "server" ? process.server : process.client;
28
+ }
29
+ export async function programView(program) {
30
+ return {
31
+ identity: program.identity,
32
+ name: program.name,
33
+ version: program.version,
34
+ description: program.description,
35
+ installed: await program.installed(),
36
+ hasAgent: program.hasAgent,
37
+ server: program.server,
38
+ client: program.client
39
+ };
40
+ }
41
+ export async function processView(process) {
42
+ const [server, client] = await Promise.all([process.server.exists(), process.client.exists()]);
43
+ return {
44
+ identity: process.identity,
45
+ name: process.name,
46
+ program: process.program().identity,
47
+ startedAt: process.startedAt.toISOString(),
48
+ server: { declared: process.program().server !== null, running: server },
49
+ client: { declared: process.program().client !== null, running: client }
50
+ };
51
+ }
52
+ export async function endpointView(process, name) {
53
+ const program = process.program();
54
+ return {
55
+ process: process.identity,
56
+ program: program.identity,
57
+ endpoint: name,
58
+ declared: name === "server" ? program.server !== null : program.client !== null,
59
+ running: await endpoint(process, name).exists()
60
+ };
61
+ }
62
+ export async function windowView(process) {
63
+ const window = process.client.window;
64
+ const [title, position, size, minimized, front, layer, location] = await Promise.all([
65
+ window.title(),
66
+ window.position(),
67
+ window.size(),
68
+ window.minimized(),
69
+ window.front(),
70
+ window.layer(),
71
+ window.location()
72
+ ]);
73
+ return { process: process.identity, title, position, size, minimized, front, layer, location };
74
+ }
75
+ export function output(value, compact) {
76
+ console.log(JSON.stringify(value ?? null, null, compact ? undefined : 2));
77
+ }
78
+ export function payload(value) {
79
+ if (value === undefined)
80
+ return undefined;
81
+ try {
82
+ return JSON.parse(value);
83
+ }
84
+ catch {
85
+ throw new Error("--payload must be valid JSON");
86
+ }
87
+ }
88
+ export function metric(value) {
89
+ const trimmed = value.trim();
90
+ return /^-?(?:\d+\.?\d*|\.\d+)$/.test(trimmed) ? Number(trimmed) : trimmed;
91
+ }
92
+ export function integer(value) {
93
+ const parsed = Number(value);
94
+ if (!Number.isSafeInteger(parsed))
95
+ throw new Error(`Expected an integer, received "${value}"`);
96
+ return parsed;
97
+ }
98
+ export function bounded(value, name, minimum, maximum) {
99
+ if (value < minimum || (maximum !== undefined && value > maximum)) {
100
+ throw new Error(`${name} must be between ${minimum} and ${maximum ?? "Infinity"}`);
101
+ }
102
+ return value;
103
+ }
104
+ export function position(x, y) {
105
+ return { x: metric(x), y: metric(y) };
106
+ }
107
+ export function size(width, height) {
108
+ return { width: metric(width), height: metric(height) };
109
+ }
110
+ export function page(values, search, offset = 0, limit = 30, text) {
111
+ const query = search?.trim().toLocaleLowerCase();
112
+ const matches = query ? values.filter(value => text(value).toLocaleLowerCase().includes(query)) : [...values];
113
+ return {
114
+ data: matches.slice(offset, offset + limit),
115
+ total: matches.length,
116
+ truncated: offset + limit < matches.length
117
+ };
118
+ }
119
+ export function launch(options, named = false) {
120
+ const client = clientLaunch(options);
121
+ const values = entries(options.option);
122
+ const result = {};
123
+ if (options.name !== undefined)
124
+ result.name = options.name;
125
+ if (options.server !== undefined)
126
+ result.server = options.server;
127
+ if (client !== undefined)
128
+ result.client = client;
129
+ if (Object.keys(values).length)
130
+ result.options = values;
131
+ if (named && !result.name)
132
+ throw new Error("--name is required");
133
+ return result;
134
+ }
135
+ export function clientLaunch(options) {
136
+ const configured = options.clientTitle !== undefined
137
+ || options.clientWidth !== undefined
138
+ || options.clientHeight !== undefined
139
+ || options.clientX !== undefined
140
+ || options.clientY !== undefined
141
+ || options.clientLayer !== undefined
142
+ || options.clientLocation !== undefined
143
+ || options.clientMinimized === true;
144
+ if (!configured)
145
+ return options.client;
146
+ if (options.client === false)
147
+ throw new Error("Client overrides cannot be combined with --no-client");
148
+ if ((options.clientWidth === undefined) !== (options.clientHeight === undefined)) {
149
+ throw new Error("--client-width and --client-height must be supplied together");
150
+ }
151
+ if ((options.clientX === undefined) !== (options.clientY === undefined)) {
152
+ throw new Error("--client-x and --client-y must be supplied together");
153
+ }
154
+ return {
155
+ ...(options.clientTitle === undefined ? {} : { title: options.clientTitle }),
156
+ ...(options.clientWidth === undefined ? {} : { size: size(options.clientWidth, options.clientHeight) }),
157
+ ...(options.clientX === undefined ? {} : { position: position(options.clientX, options.clientY) }),
158
+ ...(options.clientLayer === undefined ? {} : { layer: options.clientLayer }),
159
+ ...(options.clientLocation === undefined ? {} : { location: options.clientLocation }),
160
+ ...(options.clientMinimized === true ? { minimize: true } : {})
161
+ };
162
+ }
163
+ export function collect(value, previous = []) {
164
+ return [...previous, value];
165
+ }
166
+ function entries(values = []) {
167
+ const result = {};
168
+ for (const value of values) {
169
+ const boundary = value.indexOf("=");
170
+ if (boundary < 1)
171
+ throw new Error("--option must use name=value");
172
+ result[value.slice(0, boundary)] = value.slice(boundary + 1);
173
+ }
174
+ return result;
175
+ }
176
+ export async function wait(target, event, timeout) {
177
+ return target.waitFor(event, timeout);
178
+ }
179
+ export function windowOf(process) {
180
+ return process.client.window;
181
+ }
@@ -0,0 +1,84 @@
1
+ import { Option } from "commander";
2
+ import { commandContract } from "../command-contract.js";
3
+ import { outputOptions, processOptions } from "./options.js";
4
+ import { bounded, connected, integer, output, position, requireProcess, size, wait, windowOf, windowView } from "./shared.js";
5
+ export default function windowCommands(root, connect) {
6
+ const windows = commandContract(root.command("window")
7
+ .description("inspect and control authoritative Client Windows"));
8
+ outputOptions(processOptions(windows.command("inspect")
9
+ .description("read the complete current Window state")))
10
+ .action(async (options) => withWindow(connect, options, async (process) => {
11
+ output(await windowView(process), options.compact);
12
+ }));
13
+ outputOptions(processOptions(windows.command("move")
14
+ .description("change Window position")
15
+ .requiredOption("--x <value>", "horizontal pixels or workspace-relative expression")
16
+ .requiredOption("--y <value>", "vertical pixels or workspace-relative expression")))
17
+ .action(async (options) => withWindow(connect, options, async (process) => {
18
+ await windowOf(process).move(position(options.x, options.y));
19
+ output(await windowView(process), options.compact);
20
+ }));
21
+ outputOptions(processOptions(windows.command("resize")
22
+ .description("change Window size")
23
+ .requiredOption("--width <value>", "width in pixels or a workspace-relative expression")
24
+ .requiredOption("--height <value>", "height in pixels or a workspace-relative expression")))
25
+ .action(async (options) => withWindow(connect, options, async (process) => {
26
+ await windowOf(process).resize(size(options.width, options.height));
27
+ output(await windowView(process), options.compact);
28
+ }));
29
+ outputOptions(processOptions(windows.command("setGeometry")
30
+ .alias("set-geometry")
31
+ .description("change Window position and size atomically")
32
+ .requiredOption("--x <value>", "horizontal pixels or workspace-relative expression")
33
+ .requiredOption("--y <value>", "vertical pixels or workspace-relative expression")
34
+ .requiredOption("--width <value>", "width in pixels or a workspace-relative expression")
35
+ .requiredOption("--height <value>", "height in pixels or a workspace-relative expression")))
36
+ .action(async (options) => withWindow(connect, options, async (process) => {
37
+ await windowOf(process).setGeometry({
38
+ position: position(options.x, options.y),
39
+ size: size(options.width, options.height)
40
+ });
41
+ output(await windowView(process), options.compact);
42
+ }));
43
+ outputOptions(processOptions(windows.command("minimize")
44
+ .description("set Window visibility without changing its order")
45
+ .option("--restore", "restore rather than minimize the Window")))
46
+ .action(async (options) => withWindow(connect, options, async (process) => {
47
+ await windowOf(process).minimize(options.restore !== true);
48
+ output(await windowView(process), options.compact);
49
+ }));
50
+ outputOptions(processOptions(windows.command("changeTitle")
51
+ .alias("change-title")
52
+ .description("change the human-readable Window title")
53
+ .requiredOption("--title <title>", "new Window title")))
54
+ .action(async (options) => withWindow(connect, options, async (process) => {
55
+ await windowOf(process).changeTitle(options.title);
56
+ output(await windowView(process), options.compact);
57
+ }));
58
+ outputOptions(processOptions(windows.command("raise")
59
+ .description("raise the Window within its own layer")))
60
+ .action(async (options) => withWindow(connect, options, async (process) => {
61
+ await windowOf(process).raise();
62
+ output(await windowView(process), options.compact);
63
+ }));
64
+ outputOptions(processOptions(windows.command("wait")
65
+ .description("wait for one authoritative Window change")
66
+ .addOption(new Option("--event <event>", "Window event")
67
+ .choices(["move", "resize", "geometry", "minimize", "changeTitle", "front"])
68
+ .makeOptionMandatory())
69
+ .option("--timeout <milliseconds>", "maximum wait in milliseconds", integer)))
70
+ .action(async (options) => withWindow(connect, options, async (process) => {
71
+ output({
72
+ scope: `window:${process.identity}`,
73
+ event: options.event,
74
+ payload: await wait(windowOf(process), options.event, options.timeout === undefined
75
+ ? undefined
76
+ : bounded(options.timeout, "--timeout", 1))
77
+ }, options.compact);
78
+ }));
79
+ }
80
+ async function withWindow(connect, options, action) {
81
+ await connected(connect, async (system) => {
82
+ await action(await requireProcess(system, options.process, options.program));
83
+ });
84
+ }
@@ -1,4 +1,3 @@
1
- import { systemControl, systemControlOperation } from "@phreshos/core";
2
1
  import { commandContract, readCommandContract } from "./command-contract.js";
3
2
  /** Add one machine-readable description entry point for the complete CLI tree. */
4
3
  export default function describeCommands(program) {
@@ -24,10 +23,6 @@ function resolveCommand(root, path) {
24
23
  }
25
24
  function describe(command, path) {
26
25
  const registered = readCommandContract(command);
27
- const capability = registered?.capability ?? (path.length === 1 && Object.hasOwn(systemControl, path[0] ?? "")
28
- ? systemControl[path[0]]
29
- : null);
30
- const operation = path.length === 2 ? systemControlOperation(path[0] ?? "", path[1] ?? "") : null;
31
26
  return {
32
27
  path,
33
28
  name: command.name(),
@@ -35,8 +30,7 @@ function describe(command, path) {
35
30
  arguments: command.registeredArguments.map(argumentDescription),
36
31
  options: command.options.map(optionDescription),
37
32
  commands: command.commands.map(child => ({ name: child.name(), description: child.description() })),
38
- ...(registered?.guidance ? { guidance: registered.guidance } : {}),
39
- ...(operation ? { capability: operation } : capability ? { capability } : {})
33
+ ...(registered?.guidance ? { guidance: registered.guidance } : {})
40
34
  };
41
35
  }
42
36
  function argumentDescription(argument) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "phresh",
3
3
  "private": true,
4
- "version": "0.1.19",
4
+ "version": "0.1.20",
5
5
  "description": "The official PhreshOS starter Program.",
6
6
  "type": "module",
7
7
  "scripts": {
@@ -14,15 +14,15 @@
14
14
  "starter"
15
15
  ],
16
16
  "dependencies": {
17
- "@phreshos/client": "^0.1.23",
18
- "@phreshos/core": "^0.1.22",
19
- "@phreshos/react": "^0.1.11",
20
- "@phreshos/server": "^0.1.23",
17
+ "@phreshos/client": "^0.1.24",
18
+ "@phreshos/core": "^0.1.25",
19
+ "@phreshos/react": "^0.1.12",
20
+ "@phreshos/server": "^0.1.25",
21
21
  "react": "^19.2.8",
22
22
  "react-dom": "^19.2.8"
23
23
  },
24
24
  "devDependencies": {
25
- "@phreshos/cli": "^0.1.36",
25
+ "@phreshos/cli": "^0.1.37",
26
26
  "@types/node": "^26.2.0",
27
27
  "@types/react": "^19.2.18",
28
28
  "@types/react-dom": "^19.2.4",
@@ -4,7 +4,7 @@ export default defineConfig({
4
4
  identity: "phresh",
5
5
  name: "Phresh Program",
6
6
  description: "A minimal counter with direct Client and Server endpoints.",
7
- version: "0.1.19",
7
+ version: "0.1.20",
8
8
  icon: "icon.png",
9
9
  categories: ["Development"],
10
10
  keywords: ["example", "counter", "client", "server"],
@@ -4,6 +4,7 @@
4
4
  <head>
5
5
  <meta charset="UTF-8" />
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <meta name="color-scheme" content="light dark" />
7
8
  <title>Phresh Program</title>
8
9
  <script type="module" src="./main.tsx"></script>
9
10
  </head>
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "repository": "PhreshOS/phresh-program",
3
- "version": "0.1.19",
4
- "sha256": "e12f0c026b7b6f3e5f77a206442e5dcb6242021902d8f5a51c9886c7d75b0e24",
3
+ "version": "0.1.20",
4
+ "sha256": "645172d788eade71536ac9f9e31426eef7286d159846fb91382bcb49193b4f5e",
5
5
  "development": true
6
6
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@phreshos/cli",
3
3
  "type": "module",
4
- "version": "0.1.36",
4
+ "version": "0.1.37",
5
5
  "description": "The Phresh command-line interface for Program projects and system management.",
6
6
  "engines": {
7
7
  "node": ">=20.10"
@@ -49,8 +49,8 @@
49
49
  "packageManager": "bun@1.3.14",
50
50
  "dependencies": {
51
51
  "@clack/prompts": "^1.7.0",
52
- "@phreshos/core": "^0.1.23",
53
- "@phreshos/node": "^0.1.2",
52
+ "@phreshos/core": "^0.1.25",
53
+ "@phreshos/node": "^0.1.4",
54
54
  "adm-zip": "^0.6.0",
55
55
  "commander": "^15.0.0",
56
56
  "jiti": "^2.7.0",
@@ -1,44 +0,0 @@
1
- import { gatewayAddress, resolveHome } from "@phreshos/node";
2
- import { connect } from "node:net";
3
- /** Owner-local client for the authoritative running-System contract. */
4
- export default class LocalSystemControl {
5
- async execute(request, signal) {
6
- const address = gatewayAddress(resolveHome());
7
- return await new Promise((resolve, reject) => {
8
- const socket = connect(address);
9
- let buffer = "";
10
- let settled = false;
11
- const finish = (work) => {
12
- if (settled)
13
- return;
14
- settled = true;
15
- signal?.removeEventListener("abort", cancel);
16
- socket.destroy();
17
- work();
18
- };
19
- const cancel = () => finish(() => reject(signal?.reason instanceof Error ? signal.reason : new Error("The request was cancelled")));
20
- signal?.addEventListener("abort", cancel, { once: true });
21
- socket.on("connect", () => socket.write(`${JSON.stringify({ target: "system", request })}\n`));
22
- socket.on("data", chunk => {
23
- buffer += String(chunk);
24
- const boundary = buffer.indexOf("\n");
25
- if (boundary < 0)
26
- return;
27
- try {
28
- const outcome = JSON.parse(buffer.slice(0, boundary));
29
- if (outcome.success)
30
- finish(() => resolve(outcome.result));
31
- else
32
- finish(() => reject(new Error(outcome.error)));
33
- }
34
- catch {
35
- finish(() => reject(new Error("The System returned an invalid response")));
36
- }
37
- });
38
- socket.on("error", () => finish(() => reject(new Error(`No System is listening at ${address} — start PhreshOS first`))));
39
- socket.on("close", () => finish(() => reject(new Error("The System closed the request without an answer"))));
40
- if (signal?.aborted)
41
- cancel();
42
- });
43
- }
44
- }
@@ -1,50 +0,0 @@
1
- import { systemControl, systemControlOperation } from "@phreshos/core";
2
- import LocalSystemControl from "./control-client.js";
3
- import { commandContract } from "./command-contract.js";
4
- /** Build the top-level running-System commands from Core's authoritative catalog. */
5
- export default function controlCommands(program, provided) {
6
- let client = provided;
7
- const current = () => client ?? (client = new LocalSystemControl());
8
- for (const [capabilityName, capability] of Object.entries(systemControl)) {
9
- const group = commandContract(program.command(capabilityName)
10
- .description(capability.description)
11
- .addHelpText("after", `\nOperating guidance:\n${capability.guidance.map(item => ` - ${item}`).join("\n")}\n`), {
12
- guidance: capability.guidance,
13
- capability
14
- });
15
- for (const [operationName, operation] of Object.entries(capability.operations)) {
16
- commandContract(group.command(operationName)
17
- .description(operation.description)
18
- .option("--input <json>", "operation input as one JSON object", "{}")
19
- .option("--compact", "write machine-readable JSON on one line")
20
- .addHelpText("after", operationHelp(capabilityName, operationName))
21
- .action(async function (options) {
22
- const input = parseInput(options.input);
23
- const result = await current().execute({
24
- capability: capabilityName,
25
- operation: operationName,
26
- input
27
- });
28
- console.log(JSON.stringify(result ?? null, null, options.compact ? undefined : 2));
29
- }), { capability: operation });
30
- }
31
- }
32
- }
33
- function operationHelp(capability, operation) {
34
- const definition = systemControlOperation(capability, operation);
35
- if (!definition)
36
- return "";
37
- return `\nInput schema:\n${JSON.stringify(definition.input, null, 2)}\n\nExamples:\n${definition.examples.map(example => ` phresh ${capability} ${operation} --input '${JSON.stringify(example)}'`).join("\n")}\n`;
38
- }
39
- function parseInput(value) {
40
- let parsed;
41
- try {
42
- parsed = JSON.parse(value);
43
- }
44
- catch {
45
- throw new Error("--input must be valid JSON");
46
- }
47
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
48
- throw new Error("--input must be one JSON object");
49
- return parsed;
50
- }