@phreshos/cli 0.1.11 → 0.1.13

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
@@ -57,20 +57,22 @@ native per-user service. The selected release and the service entry therefore
57
57
  cannot become two competing sources of truth if installation is interrupted.
58
58
  It never reads a source checkout and never requires Bun or TypeScript.
59
59
 
60
- The System runs under `launchd` on macOS and a real `systemd --user` manager on
61
- Linux. In Linux containers with no init manager, it runs as a detached
62
- user-owned background process that survives the terminal but ends with the
63
- container. Automatic startup is unavailable there rather than being reported
64
- as enabled. `start` and `stop` change current execution only; where a native
65
- manager exists, `enable` and `disable` change automatic startup only. Successful
66
- installation and startup show the desktop address. `status` reports that same
67
- address with the installed version, service readiness, and automatic startup
68
- without changing them; `version` reports only the installed System release.
60
+ The System runs under `launchd` on macOS, a real `systemd --user` manager on
61
+ Linux, and a least-privilege per-user scheduled task on Windows. In Linux
62
+ containers with no init manager, it runs as a detached user-owned background
63
+ process that survives the terminal but ends with the container. Automatic
64
+ startup is unavailable there rather than being reported as enabled. `start`
65
+ and `stop` change current execution only; where a native manager exists,
66
+ `enable` and `disable` change automatic startup only. Successful installation
67
+ and startup show the desktop address. `status` reports that same address with
68
+ the installed version, service readiness, and automatic startup without
69
+ changing them; `version` reports only the installed System release.
69
70
 
70
71
  Installation files and persistent System state have separate homes. Removing
71
72
  the System unregisters its service and removes its release files while keeping
72
- `~/.phreshos`, including Programs and owner data. Windows remains unsupported
73
- until the System has an equally strong local-intake authorization model there.
73
+ `~/.phreshos`, including Programs and owner data. Local Program intake uses an
74
+ owner-only socket file on POSIX and an owner-created duplex named pipe on
75
+ Windows; neither becomes a network endpoint or introduces a bearer secret.
74
76
 
75
77
  ## create
76
78
 
@@ -151,8 +153,6 @@ export default defineConfig({
151
153
 
152
154
  description: "A file manager",
153
155
 
154
- apiDocs: "api.md",
155
-
156
156
  icon: "icon.png",
157
157
 
158
158
  buildCommand: "bun run build",
@@ -397,20 +397,11 @@ are the canonical locations the package just created. An explicit
397
397
  `start: false` crosses with its half; an omitted value remains omitted
398
398
  and means `true`. At least one declared half must resolve to true.
399
399
 
400
- When `apiDocs` is declared, its Markdown file is copied to `api-docs.md` and
401
- the packaged description names that canonical entry point. A missing declared
402
- file is an error, not an undocumented Program.
403
-
404
- The document covers only what the Program owns: its capabilities, operation
405
- and event names, payloads, behavior, state, and Program-defined failures. It
406
- does not explain how to find a Program, obtain an Endpoint, publish or ask, or
407
- register a subscription. Those are system contracts documented once by the
408
- SDKs; `api-docs.md` supplies only the Program-specific meaning carried through
409
- them.
410
-
411
400
  There is **no wrapping directory**: `program.json`, `server/`, `client/`,
412
- optional `icon.png`, and optional `api-docs.md` sit at the package's root, and the system names the
413
- directory it installs into from your program's `identity`.
401
+ and optional `icon.png` sit at the package's root, and the system names the
402
+ directory it installs into from your program's `identity`. Service API
403
+ documentation belongs to the live service that exposes it and is therefore
404
+ not part of the Program package.
414
405
 
415
406
  Nothing about the system moves because this exists. `program.json` is
416
407
  still the only thing the system reads, and a package assembled by hand
package/dist/cli.js CHANGED
@@ -46,7 +46,6 @@ describe(program.command("create")
46
46
  describe(program.command("init")
47
47
  .description("initialize an existing Program project")
48
48
  .option("--name <name>", "human-readable Program name")
49
- .option("--api-docs <path>", "official Program API documentation")
50
49
  .option("--build-command <command>", "prepare production files before use")
51
50
  .option("--server", "include a Server endpoint")
52
51
  .option("--server-location <path>", "production Server directory")
@@ -60,7 +59,6 @@ describe(program.command("init")
60
59
  .action(async function (options) {
61
60
  await init({
62
61
  name: options.name,
63
- apiDocs: options.apiDocs,
64
62
  buildCommand: options.buildCommand,
65
63
  server: options.server === true || options.serverLocation !== undefined || options.serverStartCommand !== undefined,
66
64
  serverLocation: options.serverLocation,
package/dist/derive.js CHANGED
@@ -27,11 +27,6 @@ export default function derive(config, directory, which) {
27
27
  name: config.name,
28
28
  version: config.version,
29
29
  description: config.description,
30
- // Like the icon, the document stays where the author put it for an
31
- // attached run. Installation and packaging give it its canonical
32
- // name; the runtime receives an absolute source path here because a
33
- // derived description has no file beside which to resolve it.
34
- apiDocs: config.apiDocs && resolve(directory, config.apiDocs),
35
30
  // Where it already is. Unlike pack, which gives it its canonical
36
31
  // name, this points into the authoring tree and leaves it alone.
37
32
  icon: config.icon && resolve(directory, config.icon),
package/dist/init.js CHANGED
@@ -47,15 +47,6 @@ export default async function init(options = {}, directory = process.cwd(), core
47
47
  const name = options.name ?? (interactive ? await ask("This name is shown to people; the package name remains the Program identity.", "What name should people see?", manifest.name) : undefined);
48
48
  if (name !== undefined && name.trim().length === 0)
49
49
  throw new Error("--name must not be empty");
50
- let apiDocs = options.apiDocs;
51
- if (interactive && apiDocs === undefined) {
52
- const suggested = ["api-docs.md", "README.md"].find(file => existsSync(resolve(directory, file)));
53
- if (await yes("API documentation explains only the services this Program itself provides.", "Does this Program provide API documentation?", false)) {
54
- apiDocs = await ask("The path is resolved from the project root and becomes the official API entry point.", "Where is the API documentation file?", suggested);
55
- }
56
- }
57
- if (apiDocs !== undefined && apiDocs.trim().length === 0)
58
- throw new Error("An API documentation path must not be empty");
59
50
  let buildCommand = options.buildCommand;
60
51
  if (interactive && buildCommand === undefined) {
61
52
  const suggested = manifest.scripts?.build && projectScript(directory, manifest.packageManager, "build");
@@ -118,7 +109,6 @@ export default async function init(options = {}, directory = process.cwd(), core
118
109
  name,
119
110
  version: manifest.version,
120
111
  description: manifest.description,
121
- apiDocs,
122
112
  buildCommand
123
113
  };
124
114
  const config = server
@@ -147,7 +137,6 @@ function compose(config) {
147
137
  field("name", config.name),
148
138
  field("version", config.version),
149
139
  field("description", config.description),
150
- field("apiDocs", config.apiDocs),
151
140
  field("buildCommand", config.buildCommand),
152
141
  half("server", config.server && {
153
142
  location: config.server.location,
@@ -0,0 +1,14 @@
1
+ import { createHash } from "node:crypto";
2
+ import { join } from "node:path";
3
+ /** Address the same owner-local intake without turning it into a network port. */
4
+ export default function intakeAddress(storage, platform = process.platform) {
5
+ if (platform !== "win32")
6
+ return join(storage, "intake.sock");
7
+ // Windows pipe names share one machine-wide namespace. The storage root
8
+ // separates users and isolated instances while case folding follows the
9
+ // filesystem they came from. Access remains the pipe creator's default
10
+ // duplex ACL; the name is identity, not a secret or an authorization.
11
+ const owner = storage.replaceAll("\\", "/").replace(/\/+$/, "").toLowerCase();
12
+ const identity = createHash("sha256").update(owner).digest("hex").slice(0, 32);
13
+ return `\\\\.\\pipe\\phreshos-${identity}-intake`;
14
+ }
package/dist/pack.js CHANGED
@@ -44,8 +44,6 @@ export default async function pack(directory = process.cwd()) {
44
44
  }
45
45
  if (config.icon)
46
46
  file(zip, directory, config.icon, "icon.png", "Program icon");
47
- if (config.apiDocs)
48
- document(zip, directory, config.apiDocs);
49
47
  zip.addFile("program.json", Buffer.from(JSON.stringify(program(config, version), null, 4) + "\n"));
50
48
  const archive = `${config.identity}@${version ?? "0.0.0"}.zip`;
51
49
  const bytes = zip.toBuffer();
@@ -64,7 +62,6 @@ function program(config, version) {
64
62
  name: config.name,
65
63
  version,
66
64
  description: config.description,
67
- apiDocs: config.apiDocs ? "api-docs.md" : undefined,
68
65
  icon: config.icon ? "icon.png" : undefined,
69
66
  ...config.server && { server: { location: "server", start: config.server.start, installCommand: config.server.installCommand, startCommand: config.server.startCommand } },
70
67
  ...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 } }
@@ -78,9 +75,6 @@ function place(zip, directory, location, half) {
78
75
  throw new Error(`The ${half} files are not at ${location} — nothing was built there`);
79
76
  zip.addLocalFolder(from, half);
80
77
  }
81
- function document(zip, directory, location) {
82
- file(zip, directory, location, "api-docs.md", "API documentation file");
83
- }
84
78
  function file(zip, directory, location, target, label) {
85
79
  const from = resolve(directory, location);
86
80
  if (!existsSync(from) || !statSync(from).isFile())
@@ -1,14 +1,15 @@
1
1
  import { connect } from "node:net";
2
2
  import { homedir } from "node:os";
3
3
  import { isAbsolute, join } from "node:path";
4
+ import intakeAddress from "./intake-address.js";
4
5
  /**
5
6
  * The local Program intake, from the CLI's side.
6
7
  *
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.
8
+ * An owner-local IPC address rather than a port: a mode-0600 socket file
9
+ * on POSIX and an owner-created duplex named pipe on Windows. Only the
10
+ * account that owns this machine can complete the channel, and that account
11
+ * is exactly who may run and install programs on it. Nothing is sent to
12
+ * prove anything, because being able to connect is the proof.
12
13
  *
13
14
  * A message is a line. Closing our own side to mark the end of a
14
15
  * question would make lifetime ambiguous. A line delimiter keeps the
@@ -23,10 +24,10 @@ import { isAbsolute, join } from "node:path";
23
24
  export function programIntakePath(environment = process.env, userHome = homedir()) {
24
25
  const instanceHome = environment.PHRESHOS_HOME;
25
26
  if (instanceHome === undefined)
26
- return join(userHome, ".phreshos", "intake.sock");
27
+ return intakeAddress(join(userHome, ".phreshos"));
27
28
  if (!isAbsolute(instanceHome))
28
29
  throw new Error("PHRESHOS_HOME must be an absolute filesystem path");
29
- return join(instanceHome, "intake.sock");
30
+ return intakeAddress(instanceHome);
30
31
  }
31
32
  export const socketPath = programIntakePath();
32
33
  export default function speak(question, heard, path = socketPath, signal) {
@@ -99,7 +99,6 @@ async function readProgram(directory, release) {
99
99
  throw new Error("A published Program package cannot choose its installed storage");
100
100
  return {
101
101
  ...value,
102
- ...pathField(value, directory, "apiDocs", "api-docs.md"),
103
102
  ...pathField(value, directory, "icon", "icon.png"),
104
103
  ...half(value, directory, "server"),
105
104
  ...half(value, directory, "client")
package/dist/project.js CHANGED
@@ -43,8 +43,6 @@ function coherent(config) {
43
43
  if (config[field] !== undefined && typeof config[field] !== "string")
44
44
  throw new Error(`A program's ${field} must be text`);
45
45
  }
46
- if (config.apiDocs !== undefined && (typeof config.apiDocs !== "string" || config.apiDocs.trim().length === 0))
47
- throw new Error("A program's apiDocs must be a non-empty path");
48
46
  if (config.buildCommand !== undefined && (typeof config.buildCommand !== "string" || config.buildCommand.trim().length === 0))
49
47
  throw new Error("A program's buildCommand must be non-empty text");
50
48
  for (const half of ["server", "client"]) {
@@ -3,6 +3,7 @@ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
3
3
  import { mkdir, mkdtemp, open, readFile, readdir, readlink, rename, rm, symlink, writeFile } from "node:fs/promises";
4
4
  import { dirname, isAbsolute, join, relative, resolve } from "node:path";
5
5
  import { requireSuccess } from "./process.js";
6
+ import npmInvocation from "./npm.js";
6
7
  import { minimumSystemNodeVersion } from "./node.js";
7
8
  import AdmZip from "adm-zip";
8
9
  /** Owns immutable release directories and the one atomic active record. */
@@ -207,8 +208,8 @@ function requireDirectory(path) {
207
208
  mkdirSync(path, { recursive: true, mode: 0o700 });
208
209
  }
209
210
  async function installProductionDependencies(directory) {
210
- const npm = process.platform === "win32" ? "npm.cmd" : "npm";
211
- await requireSuccess(npm, ["install", "--omit=dev", "--no-audit", "--no-fund", "--package-lock=false"], { cwd: directory });
211
+ const npm = npmInvocation(["install", "--omit=dev", "--no-audit", "--no-fund", "--package-lock=false"]);
212
+ await requireSuccess(npm.command, npm.args, { cwd: directory });
212
213
  }
213
214
  function releaseRecord(value) {
214
215
  return record(value)
@@ -0,0 +1,8 @@
1
+ import { win32 } from "node:path";
2
+ /** Invoke npm without asking a platform command shell to interpret its shim. */
3
+ export default function npmInvocation(args, platform = process.platform, executable = process.execPath) {
4
+ if (platform !== "win32")
5
+ return { command: "npm", args };
6
+ const npm = win32.join(win32.dirname(executable), "node_modules", "npm", "bin", "npm-cli.js");
7
+ return { command: executable, args: [npm, ...args] };
8
+ }
@@ -1,3 +1,4 @@
1
+ import intakeAddress from "../intake-address.js";
1
2
  import { homedir } from "node:os";
2
3
  import { isAbsolute, join } from "node:path";
3
4
  /** The installation is separate from the persistent state it operates on. */
@@ -15,7 +16,7 @@ export default function systemPaths(platform = process.platform, userHome = home
15
16
  releases: join(root, "releases"),
16
17
  current: join(root, "current"),
17
18
  storage,
18
- intake: join(storage, "intake.sock"),
19
+ intake: intakeAddress(storage, platform),
19
20
  log: join(storage, "service.log")
20
21
  };
21
22
  }
@@ -1,11 +1,14 @@
1
1
  import { homedir } from "node:os";
2
2
  import LinuxSystemService from "./linux.js";
3
3
  import MacOSSystemService from "./macos.js";
4
+ import WindowsSystemService from "./windows.js";
4
5
  /** Select the per-user service implementation available in this environment. */
5
6
  export default function systemService(platform = process.platform, userHome = homedir()) {
6
7
  if (platform === "darwin")
7
8
  return new MacOSSystemService(userHome);
8
9
  if (platform === "linux")
9
10
  return new LinuxSystemService(userHome);
11
+ if (platform === "win32")
12
+ return new WindowsSystemService(userHome);
10
13
  throw new Error(`PhreshOS System services are not supported on ${platform}`);
11
14
  }
@@ -11,10 +11,9 @@ export default class MacOSSystemService {
11
11
  plist;
12
12
  domain;
13
13
  target;
14
- constructor(userHome, run = execute, label = defaultLabel) {
14
+ constructor(userHome, run = execute, label = defaultLabel, uid = process.getuid?.()) {
15
15
  this.run = run;
16
16
  this.label = label;
17
- const uid = process.getuid?.();
18
17
  if (uid === undefined)
19
18
  throw new Error("The current macOS user could not be identified");
20
19
  this.plist = join(userHome, "Library", "LaunchAgents", `${this.label}.plist`);
@@ -0,0 +1,45 @@
1
+ import { spawn } from "node:child_process";
2
+ import { open, writeFile } from "node:fs/promises";
3
+ const payload = decode(process.argv[2]);
4
+ const output = await open(payload.definition.output, "a", 0o600);
5
+ try {
6
+ const child = spawn(payload.definition.executable, [payload.definition.entry], {
7
+ cwd: payload.definition.directory,
8
+ stdio: ["ignore", output.fd, output.fd]
9
+ });
10
+ await new Promise(function (settle, refuse) {
11
+ child.once("spawn", settle);
12
+ child.once("error", refuse);
13
+ });
14
+ if (child.pid === undefined)
15
+ throw new Error("The PhreshOS System process has no pid");
16
+ await writeFile(payload.state, JSON.stringify({ runner: process.pid, child: child.pid }), { mode: 0o600 });
17
+ const result = await new Promise(settle => {
18
+ child.once("close", (code, signal) => settle({ code, signal }));
19
+ });
20
+ process.exitCode = result.signal ? 1 : result.code ?? 1;
21
+ }
22
+ finally {
23
+ await output.close();
24
+ }
25
+ function decode(encoded) {
26
+ let value;
27
+ try {
28
+ value = JSON.parse(Buffer.from(encoded ?? "", "base64url").toString("utf8"));
29
+ }
30
+ catch {
31
+ throw new Error("The PhreshOS System task payload is invalid");
32
+ }
33
+ if (!value || typeof value !== "object")
34
+ throw new Error("The PhreshOS System task payload is invalid");
35
+ const candidate = value;
36
+ if (typeof candidate.state !== "string" || !definition(candidate.definition))
37
+ throw new Error("The PhreshOS System task payload is invalid");
38
+ return { state: candidate.state, definition: candidate.definition };
39
+ }
40
+ function definition(value) {
41
+ if (!value || typeof value !== "object")
42
+ return false;
43
+ const candidate = value;
44
+ return ["executable", "entry", "directory", "output"].every(name => typeof candidate[name] === "string" && candidate[name] !== "");
45
+ }
@@ -0,0 +1,238 @@
1
+ import { execute } from "../process.js";
2
+ import { randomUUID } from "node:crypto";
3
+ import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
4
+ import { dirname, join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ const scheduler = "schtasks.exe";
7
+ const powershell = "powershell.exe";
8
+ const defaultTask = "PhreshOS System";
9
+ const runner = fileURLToPath(new URL("./windows-runner.js", import.meta.url));
10
+ /** A per-user Windows Task Scheduler service requiring no administrator rights. */
11
+ export default class WindowsSystemService {
12
+ userHome;
13
+ run;
14
+ task;
15
+ constructor(userHome, run = execute, task = defaultTask) {
16
+ this.userHome = userHome;
17
+ this.run = run;
18
+ this.task = task;
19
+ this.state = join(userHome, ".phreshos", "system-service.json");
20
+ }
21
+ state;
22
+ async inspect() {
23
+ const result = await this.run(powershell, powershellArguments(inspectScript(this.task)));
24
+ if (result.code === 3)
25
+ return absent();
26
+ if (result.code !== 0)
27
+ throw failure(powershell, result);
28
+ const match = /^([0-4]),([01])$/.exec(result.stdout.trim());
29
+ if (!match)
30
+ throw new Error("The PhreshOS System scheduled task returned an invalid state");
31
+ const pid = match[1] === "4" ? (await this.readState())?.child : undefined;
32
+ return {
33
+ registered: true,
34
+ automaticStartup: true,
35
+ enabled: match[2] === "1",
36
+ running: match[1] === "4",
37
+ ...(pid && alive(pid) ? { pid } : {})
38
+ };
39
+ }
40
+ async register(definition) {
41
+ await this.stop();
42
+ await mkdir(dirname(definition.output), { recursive: true });
43
+ await mkdir(dirname(this.state), { recursive: true });
44
+ await rm(this.state, { force: true });
45
+ const sid = await this.userSid();
46
+ const temporary = join(this.userHome, `.phreshos-system-${randomUUID()}.xml`);
47
+ try {
48
+ await writeFile(temporary, utf16(task(this.task, sid, definition, this.state)));
49
+ await this.require(scheduler, ["/Create", "/TN", this.task, "/XML", temporary, "/F"]);
50
+ }
51
+ finally {
52
+ await rm(temporary, { force: true });
53
+ }
54
+ }
55
+ async unregister() {
56
+ const state = await this.inspect();
57
+ if (!state.registered) {
58
+ await rm(this.state, { force: true });
59
+ return;
60
+ }
61
+ if (state.running)
62
+ await this.stop();
63
+ await this.require(scheduler, ["/Delete", "/TN", this.task, "/F"]);
64
+ await rm(this.state, { force: true });
65
+ }
66
+ async start() {
67
+ const state = await this.inspect();
68
+ if (!state.registered)
69
+ throw new Error("The PhreshOS System service is not registered");
70
+ if (state.running)
71
+ return;
72
+ // Task Scheduler refuses an explicit run while a task is disabled.
73
+ // Enablement names future logons, not current execution, so borrow it
74
+ // only for the launch and restore the persisted choice immediately.
75
+ if (!state.enabled)
76
+ await this.change("/Enable");
77
+ try {
78
+ await this.require(scheduler, ["/Run", "/TN", this.task]);
79
+ }
80
+ finally {
81
+ if (!state.enabled)
82
+ await this.change("/Disable");
83
+ }
84
+ }
85
+ async stop() {
86
+ const state = await this.inspect();
87
+ if (!state.registered || !state.running)
88
+ return;
89
+ await this.require(scheduler, ["/End", "/TN", this.task]);
90
+ const until = Date.now() + 5_000;
91
+ while (Date.now() < until) {
92
+ const processes = await this.readState();
93
+ if (!(await this.inspect()).running && !running(processes))
94
+ break;
95
+ await new Promise(settle => setTimeout(settle, 50));
96
+ }
97
+ const processes = await this.readState();
98
+ if (running(processes)) {
99
+ if (processes?.child && alive(processes.child))
100
+ process.kill(processes.child);
101
+ await settle(processes?.child);
102
+ if (processes?.runner && alive(processes.runner))
103
+ process.kill(processes.runner);
104
+ await settle(processes?.runner);
105
+ }
106
+ if ((await this.inspect()).running || running(await this.readState()))
107
+ throw new Error("The PhreshOS System scheduled task did not stop");
108
+ await rm(this.state, { force: true });
109
+ }
110
+ async enable() {
111
+ await this.requireRegistered();
112
+ await this.change("/Enable");
113
+ }
114
+ async disable() {
115
+ await this.requireRegistered();
116
+ await this.change("/Disable");
117
+ }
118
+ async userSid() {
119
+ const result = await this.run(powershell, powershellArguments("[Console]::Out.Write([Security.Principal.WindowsIdentity]::GetCurrent().User.Value)"));
120
+ if (result.code !== 0)
121
+ throw failure(powershell, result);
122
+ const sid = result.stdout.trim();
123
+ if (!/^S-[0-9]+(?:-[0-9]+)+$/.test(sid))
124
+ throw new Error("The current Windows user could not be identified");
125
+ return sid;
126
+ }
127
+ async readState() {
128
+ try {
129
+ const value = JSON.parse(await readFile(this.state, "utf8"));
130
+ if (integer(value.runner) && integer(value.child))
131
+ return { runner: value.runner, child: value.child };
132
+ }
133
+ catch { }
134
+ return undefined;
135
+ }
136
+ async requireRegistered() {
137
+ if (!(await this.inspect()).registered)
138
+ throw new Error("The PhreshOS System service is not registered");
139
+ }
140
+ async change(action) {
141
+ await this.require(scheduler, ["/Change", "/TN", this.task, action]);
142
+ }
143
+ async require(command, args) {
144
+ const result = await this.run(command, args);
145
+ if (result.code !== 0)
146
+ throw failure(command, result);
147
+ }
148
+ }
149
+ function absent() {
150
+ return { registered: false, automaticStartup: true, enabled: false, running: false };
151
+ }
152
+ function inspectScript(name) {
153
+ const selected = quotePowerShell(name);
154
+ return `$task = Get-ScheduledTask -TaskName ${selected} -TaskPath '\\' -ErrorAction SilentlyContinue; if ($null -eq $task) { exit 3 }; [Console]::Out.Write(("{0},{1}" -f [int]$task.State, [int]$task.Settings.Enabled))`;
155
+ }
156
+ function task(name, sid, definition, state) {
157
+ const payload = Buffer.from(JSON.stringify({ definition, state })).toString("base64url");
158
+ const argumentsValue = `"${runner}" ${payload}`;
159
+ return `<?xml version="1.0" encoding="UTF-16"?>
160
+ <Task version="1.4" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
161
+ <RegistrationInfo>
162
+ <Description>PhreshOS System</Description>
163
+ <URI>\\${xml(name)}</URI>
164
+ </RegistrationInfo>
165
+ <Triggers>
166
+ <LogonTrigger>
167
+ <Enabled>true</Enabled>
168
+ <UserId>${xml(sid)}</UserId>
169
+ </LogonTrigger>
170
+ </Triggers>
171
+ <Principals>
172
+ <Principal id="User">
173
+ <UserId>${xml(sid)}</UserId>
174
+ <LogonType>InteractiveToken</LogonType>
175
+ <RunLevel>LeastPrivilege</RunLevel>
176
+ </Principal>
177
+ </Principals>
178
+ <Settings>
179
+ <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
180
+ <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
181
+ <StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
182
+ <AllowHardTerminate>true</AllowHardTerminate>
183
+ <StartWhenAvailable>true</StartWhenAvailable>
184
+ <AllowStartOnDemand>true</AllowStartOnDemand>
185
+ <Enabled>true</Enabled>
186
+ <ExecutionTimeLimit>PT0S</ExecutionTimeLimit>
187
+ <RestartOnFailure>
188
+ <Interval>PT1M</Interval>
189
+ <Count>999</Count>
190
+ </RestartOnFailure>
191
+ </Settings>
192
+ <Actions Context="User">
193
+ <Exec>
194
+ <Command>${xml(definition.executable)}</Command>
195
+ <Arguments>${xml(argumentsValue)}</Arguments>
196
+ <WorkingDirectory>${xml(definition.directory)}</WorkingDirectory>
197
+ </Exec>
198
+ </Actions>
199
+ </Task>
200
+ `;
201
+ }
202
+ function powershellArguments(script) {
203
+ return ["-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", script];
204
+ }
205
+ function quotePowerShell(value) {
206
+ return `'${value.replaceAll("'", "''")}'`;
207
+ }
208
+ function xml(value) {
209
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;");
210
+ }
211
+ function utf16(value) {
212
+ return Buffer.concat([Buffer.from([0xff, 0xfe]), Buffer.from(value, "utf16le")]);
213
+ }
214
+ function failure(command, result) {
215
+ return new Error(result.stderr.trim() || result.stdout.trim() || `${command} exited with code ${result.code}`);
216
+ }
217
+ function integer(value) {
218
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
219
+ }
220
+ function alive(pid) {
221
+ try {
222
+ process.kill(pid, 0);
223
+ return true;
224
+ }
225
+ catch {
226
+ return false;
227
+ }
228
+ }
229
+ function running(value) {
230
+ return Boolean(value && (alive(value.runner) || alive(value.child)));
231
+ }
232
+ async function settle(pid) {
233
+ if (!pid)
234
+ return;
235
+ const until = Date.now() + 1_000;
236
+ while (Date.now() < until && alive(pid))
237
+ await new Promise(resolve => setTimeout(resolve, 25));
238
+ }
@@ -16,7 +16,7 @@
16
16
  "react-dom": "^19.2.8"
17
17
  },
18
18
  "devDependencies": {
19
- "@phreshos/cli": "^0.1.11",
19
+ "@phreshos/cli": "^0.1.13",
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.11",
4
+ "version": "0.1.13",
5
5
  "description": "The Phresh command-line interface for Program projects and system management.",
6
6
  "engines": {
7
7
  "node": ">=20.10"
@@ -15,6 +15,7 @@
15
15
  "verify:system-release": "node --run compile && node scripts/verify-system-release.mjs",
16
16
  "verify:linux-service": "node --run compile && node scripts/verify-linux-service.mjs",
17
17
  "verify:macos-service": "node --run compile && node scripts/verify-macos-service.mjs",
18
+ "verify:windows-service": "node --run compile && node scripts/verify-windows-service.mjs",
18
19
  "prepack": "node --run build"
19
20
  },
20
21
  "bin": {
@@ -47,7 +48,7 @@
47
48
  "packageManager": "bun@1.3.14",
48
49
  "dependencies": {
49
50
  "@clack/prompts": "^1.7.0",
50
- "@phreshos/core": "^0.1.1",
51
+ "@phreshos/core": "^0.1.4",
51
52
  "adm-zip": "^0.6.0",
52
53
  "commander": "^15.0.0",
53
54
  "picocolors": "^1.1.1"