@phreshos/cli 0.1.9 → 0.1.11
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/build-template.js +2 -2
- package/dist/cli.js +9 -5
- package/dist/install.js +36 -30
- package/dist/pack.js +6 -2
- package/dist/program-installation.js +54 -0
- package/dist/program-intake.js +5 -4
- package/dist/program-release.js +165 -0
- package/dist/system/installation.js +8 -1
- package/dist/system/lifecycle.js +18 -2
- package/dist/system/node.js +31 -2
- package/dist/template/gitignore +1 -0
- package/dist/template/package.json +2 -2
- package/dist/template/phresh.config.ts +1 -1
- package/dist/template.json +1 -1
- package/package.json +1 -1
package/dist/build-template.js
CHANGED
|
@@ -6,8 +6,8 @@ import AdmZip from "adm-zip";
|
|
|
6
6
|
import { readConfig } from "./project.js";
|
|
7
7
|
const repository = "PhreshOS/phresh-program";
|
|
8
8
|
const release = {
|
|
9
|
-
version: "0.1.
|
|
10
|
-
sha256: "
|
|
9
|
+
version: "0.1.2",
|
|
10
|
+
sha256: "dba5f1fd868c46501d5bd3463f4f8f0e098c7a231cb75794c01118689dc69339"
|
|
11
11
|
};
|
|
12
12
|
const archiveUrl = `https://github.com/${repository}/archive/refs/tags/v${release.version}.zip`;
|
|
13
13
|
const output = resolve(import.meta.dirname, "template");
|
package/dist/cli.js
CHANGED
|
@@ -86,12 +86,16 @@ describe(program.command("pack")
|
|
|
86
86
|
"declared for each endpoint."
|
|
87
87
|
]);
|
|
88
88
|
describe(program.command("install")
|
|
89
|
-
.description("install
|
|
90
|
-
.
|
|
91
|
-
|
|
89
|
+
.description("install a local or official Program")
|
|
90
|
+
.argument("[name]", "name of an official Program")
|
|
91
|
+
.option("--run", "run the installed Program now")
|
|
92
|
+
.option("--startup", "run the Program when the System starts")
|
|
93
|
+
.action(async function (name, options) {
|
|
94
|
+
await install({ name, run: options.run === true, startup: options.startup === true });
|
|
92
95
|
}), [
|
|
93
|
-
"
|
|
94
|
-
"
|
|
96
|
+
"Without a name, builds and installs the Program declared by this project.",
|
|
97
|
+
"A name installs its verified official production release. --run launches",
|
|
98
|
+
"it now; --startup persists the same default launch for future starts."
|
|
95
99
|
]);
|
|
96
100
|
describe(program.command("uninstall")
|
|
97
101
|
.description("uninstall this Program")
|
package/dist/install.js
CHANGED
|
@@ -1,45 +1,51 @@
|
|
|
1
1
|
import derive from "./derive.js";
|
|
2
2
|
import { readConfig } from "./project.js";
|
|
3
|
-
import { dim, heading } from "./style.js";
|
|
4
|
-
import
|
|
3
|
+
import { dim, heading, line } from "./style.js";
|
|
4
|
+
import installProgram, {} from "./program-installation.js";
|
|
5
|
+
import { prepareOfficialProgram } from "./program-release.js";
|
|
5
6
|
import build from "./build-command.js";
|
|
6
7
|
/**
|
|
7
8
|
* Lay this program out on this machine's system.
|
|
8
9
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* **Installing takes no package, and neither does the system.** A
|
|
16
|
-
* package is a way of *carrying* a program to another machine, which is
|
|
17
|
-
* a different act from laying one out — and installing programs at all
|
|
18
|
-
* is work for a program rather than for the core, so what a person
|
|
19
|
-
* installs *from* is not this command's business either. `phresh pack`
|
|
20
|
-
* makes a package when you have somewhere to send it, and stops there.
|
|
10
|
+
* A local project is built and derived from its authoring declaration. An
|
|
11
|
+
* official name resolves a verified production package and turns its
|
|
12
|
+
* canonical paths into the same concrete description. From that point on,
|
|
13
|
+
* both sources cross the exact same intake and the System performs the exact
|
|
14
|
+
* same authoritative installation.
|
|
21
15
|
*
|
|
22
16
|
* When the author config declares `buildCommand`, it runs here before the
|
|
23
17
|
* production Program is derived and sent. The command remains authoring
|
|
24
18
|
* metadata and never becomes part of the installed Program.
|
|
25
19
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
20
|
+
* Installation remains distinct from execution unless `run` or `startup`
|
|
21
|
+
* is explicitly requested. A run created here belongs to the installed
|
|
22
|
+
* Program and therefore outlives this command; `phresh start` and `phresh
|
|
23
|
+
* dev` remain attached authoring runs whose lifetime is the terminal's.
|
|
30
24
|
*/
|
|
31
|
-
export default async function install(
|
|
25
|
+
export default async function install(options = {}) {
|
|
26
|
+
const directory = options.directory ?? process.cwd();
|
|
27
|
+
const prepared = options.name ? await prepareOfficialProgram(options.name) : null;
|
|
28
|
+
try {
|
|
29
|
+
const program = prepared?.program ?? await localProgram(directory);
|
|
30
|
+
const result = await installProgram(program, options);
|
|
31
|
+
if (options.announce !== false) {
|
|
32
|
+
const { name, identity, version } = result.program;
|
|
33
|
+
heading(`${name || identity}${version ? ` ${version}` : ""}`, result.replaced ? "reinstalled" : "installed");
|
|
34
|
+
if (result.replaced)
|
|
35
|
+
console.log(` ${dim("its storage was kept, and its previous processes were ended")}\n`);
|
|
36
|
+
if (result.startupEnabled)
|
|
37
|
+
line("startup", "enabled");
|
|
38
|
+
if (result.process)
|
|
39
|
+
line("process", result.process);
|
|
40
|
+
}
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
await prepared?.dispose();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
async function localProgram(directory) {
|
|
32
48
|
const config = await readConfig(directory);
|
|
33
49
|
await build(config, directory);
|
|
34
|
-
|
|
35
|
-
await speak({ word: "install", program }, function (event) {
|
|
36
|
-
if (event.event !== "installed")
|
|
37
|
-
return;
|
|
38
|
-
const said = event.program;
|
|
39
|
-
heading(`${said.name ?? String(said.identity)}${said.version ? ` ${said.version}` : ""}`, event.replaced ? "reinstalled" : "installed");
|
|
40
|
-
// What it kept. Every process ended before the installed paths
|
|
41
|
-
// changed, while storage stayed in its canonical place.
|
|
42
|
-
if (event.replaced)
|
|
43
|
-
console.log(` ${dim("its storage was kept, and its previous processes were ended")}\n`);
|
|
44
|
-
});
|
|
50
|
+
return derive(config, directory, "production");
|
|
45
51
|
}
|
package/dist/pack.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readConfig, readManifest } from "./project.js";
|
|
2
|
-
import {
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
3
4
|
import { resolve } from "node:path";
|
|
4
5
|
import AdmZip from "adm-zip";
|
|
5
6
|
import build from "./build-command.js";
|
|
@@ -47,7 +48,10 @@ export default async function pack(directory = process.cwd()) {
|
|
|
47
48
|
document(zip, directory, config.apiDocs);
|
|
48
49
|
zip.addFile("program.json", Buffer.from(JSON.stringify(program(config, version), null, 4) + "\n"));
|
|
49
50
|
const archive = `${config.identity}@${version ?? "0.0.0"}.zip`;
|
|
50
|
-
zip.
|
|
51
|
+
const bytes = zip.toBuffer();
|
|
52
|
+
writeFileSync(resolve(directory, archive), bytes);
|
|
53
|
+
const digest = createHash("sha256").update(bytes).digest("hex");
|
|
54
|
+
writeFileSync(resolve(directory, `${archive}.sha256`), `${digest} ${archive}\n`);
|
|
51
55
|
console.log(`\nPacked ${archive}`);
|
|
52
56
|
return archive;
|
|
53
57
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import speak from "./program-intake.js";
|
|
2
|
+
/** Install one prepared Program and await every explicitly requested outcome. */
|
|
3
|
+
export default async function installProgram(program, options = {}) {
|
|
4
|
+
let installedValue;
|
|
5
|
+
let replaced = false;
|
|
6
|
+
let startupEnabled = false;
|
|
7
|
+
let processValue;
|
|
8
|
+
await speak({
|
|
9
|
+
word: "install",
|
|
10
|
+
program,
|
|
11
|
+
run: options.run === true,
|
|
12
|
+
startup: options.startup === true
|
|
13
|
+
}, function (event) {
|
|
14
|
+
if (event.event === "installed") {
|
|
15
|
+
installedValue = event.program;
|
|
16
|
+
replaced = event.replaced === true;
|
|
17
|
+
}
|
|
18
|
+
else if (event.event === "startupEnabled")
|
|
19
|
+
startupEnabled = true;
|
|
20
|
+
else if (event.event === "running")
|
|
21
|
+
processValue = event.process;
|
|
22
|
+
});
|
|
23
|
+
if (installedValue === undefined)
|
|
24
|
+
throw new Error("The System ended Program installation without confirming it");
|
|
25
|
+
const installed = installedProgram(installedValue);
|
|
26
|
+
if (options.startup && !startupEnabled)
|
|
27
|
+
throw new Error("The System installed the Program without confirming startup");
|
|
28
|
+
const process = processValue === undefined ? null : processIdentity(processValue);
|
|
29
|
+
if (options.run && !process)
|
|
30
|
+
throw new Error("The System installed the Program without confirming that it is running");
|
|
31
|
+
return {
|
|
32
|
+
program: installed,
|
|
33
|
+
replaced,
|
|
34
|
+
startupEnabled,
|
|
35
|
+
process
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function processIdentity(value) {
|
|
39
|
+
if (typeof value !== "string" || !value)
|
|
40
|
+
throw new Error("The System returned an invalid Process identity");
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
function installedProgram(value) {
|
|
44
|
+
if (!record(value)
|
|
45
|
+
|| typeof value.identity !== "string"
|
|
46
|
+
|| typeof value.name !== "string"
|
|
47
|
+
|| value.version !== null && typeof value.version !== "string") {
|
|
48
|
+
throw new Error("The System returned an invalid installed Program");
|
|
49
|
+
}
|
|
50
|
+
return { identity: value.identity, name: value.name, version: value.version };
|
|
51
|
+
}
|
|
52
|
+
function record(value) {
|
|
53
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
54
|
+
}
|
package/dist/program-intake.js
CHANGED
|
@@ -14,10 +14,11 @@ import { isAbsolute, join } from "node:path";
|
|
|
14
14
|
* question would make lifetime ambiguous. A line delimiter keeps the
|
|
15
15
|
* connection available for the stream of events that follows a launch.
|
|
16
16
|
*
|
|
17
|
-
* One question, then events until the system closes.
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
17
|
+
* One question, then events until the system closes. Installation confirms
|
|
18
|
+
* each requested outcome — laid out, startup enabled, running — and then
|
|
19
|
+
* ends. Uninstalling says one thing and ends; an attached run says how it
|
|
20
|
+
* began, whatever the Program says, and how it ended. None pretends to be a
|
|
21
|
+
* remote method returning through an unrelated transport.
|
|
21
22
|
*/
|
|
22
23
|
export function programIntakePath(environment = process.env, userHome = homedir()) {
|
|
23
24
|
const instanceHome = environment.PHRESHOS_HOME;
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
6
|
+
import AdmZip from "adm-zip";
|
|
7
|
+
const officialPrograms = {
|
|
8
|
+
phresh: {
|
|
9
|
+
identity: "phresh-program",
|
|
10
|
+
repository: "PhreshOS/phresh-program"
|
|
11
|
+
},
|
|
12
|
+
setup: {
|
|
13
|
+
identity: "setup",
|
|
14
|
+
repository: "PhreshOS/setup-program"
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
/** Resolve, verify, and unpack one official production Program release. */
|
|
18
|
+
export async function prepareOfficialProgram(name, fetcher = fetch) {
|
|
19
|
+
const release = await resolveOfficialProgramRelease(name, fetcher);
|
|
20
|
+
const bytes = await downloadProgramRelease(release, fetcher);
|
|
21
|
+
const directory = await mkdtemp(join(tmpdir(), `phresh-${release.identity}-`));
|
|
22
|
+
try {
|
|
23
|
+
extract(bytes, directory);
|
|
24
|
+
const program = await readProgram(directory, release);
|
|
25
|
+
return {
|
|
26
|
+
program,
|
|
27
|
+
release,
|
|
28
|
+
dispose: async () => { await rm(directory, { recursive: true, force: true }); }
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
await rm(directory, { recursive: true, force: true });
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export async function resolveOfficialProgramRelease(name, fetcher = fetch) {
|
|
37
|
+
const official = officialPrograms[name];
|
|
38
|
+
if (!official)
|
|
39
|
+
throw new Error(`No official Program is named "${name}"`);
|
|
40
|
+
const response = await fetcher(`https://api.github.com/repos/${official.repository}/releases?per_page=100`, {
|
|
41
|
+
headers: {
|
|
42
|
+
Accept: "application/vnd.github+json",
|
|
43
|
+
"User-Agent": "@phreshos/cli"
|
|
44
|
+
},
|
|
45
|
+
signal: AbortSignal.timeout(30_000)
|
|
46
|
+
});
|
|
47
|
+
if (!response.ok)
|
|
48
|
+
throw new Error(`The ${name} release list could not be read (${response.status} ${response.statusText})`);
|
|
49
|
+
return selectProgramRelease(official.identity, await response.json());
|
|
50
|
+
}
|
|
51
|
+
export function selectProgramRelease(identity, value) {
|
|
52
|
+
if (!Array.isArray(value))
|
|
53
|
+
throw new Error(`The ${identity} release list is invalid`);
|
|
54
|
+
const releases = value.flatMap(function (item) {
|
|
55
|
+
if (!record(item) || item.draft === true || item.prerelease === true || typeof item.tag_name !== "string" || !Array.isArray(item.assets))
|
|
56
|
+
return [];
|
|
57
|
+
const version = parseVersion(item.tag_name);
|
|
58
|
+
if (!version)
|
|
59
|
+
return [];
|
|
60
|
+
const archiveName = `${identity}@${version}.zip`;
|
|
61
|
+
const archive = asset(item.assets, archiveName);
|
|
62
|
+
const checksum = asset(item.assets, `${archiveName}.sha256`);
|
|
63
|
+
return archive && checksum ? [{ identity, version, archive, checksum }] : [];
|
|
64
|
+
});
|
|
65
|
+
releases.sort((left, right) => compare(right.version, left.version));
|
|
66
|
+
const selected = releases[0];
|
|
67
|
+
if (!selected)
|
|
68
|
+
throw new Error(`No stable ${identity} Program release is available`);
|
|
69
|
+
return selected;
|
|
70
|
+
}
|
|
71
|
+
export async function downloadProgramRelease(release, fetcher = fetch) {
|
|
72
|
+
const [archive, checksum] = await Promise.all([
|
|
73
|
+
fetchAsset(release.archive, fetcher),
|
|
74
|
+
fetchAsset(release.checksum, fetcher)
|
|
75
|
+
]);
|
|
76
|
+
const bytes = Buffer.from(await archive.arrayBuffer());
|
|
77
|
+
const said = (await checksum.text()).trim();
|
|
78
|
+
const name = `${release.identity}@${release.version}.zip`;
|
|
79
|
+
const match = /^([a-f0-9]{64})\s+(.+)$/i.exec(said);
|
|
80
|
+
if (!match || match[2] !== name)
|
|
81
|
+
throw new Error(`The checksum for ${name} is invalid`);
|
|
82
|
+
const digest = createHash("sha256").update(bytes).digest("hex");
|
|
83
|
+
if (digest !== match[1]?.toLowerCase())
|
|
84
|
+
throw new Error(`The downloaded ${name} does not match its SHA-256 checksum`);
|
|
85
|
+
return bytes;
|
|
86
|
+
}
|
|
87
|
+
async function readProgram(directory, release) {
|
|
88
|
+
let value;
|
|
89
|
+
try {
|
|
90
|
+
value = JSON.parse(await readFile(join(directory, "program.json"), "utf8"));
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
throw new Error(`The ${release.identity} Program package has no valid program.json`);
|
|
94
|
+
}
|
|
95
|
+
if (!record(value) || value.identity !== release.identity || value.version !== release.version) {
|
|
96
|
+
throw new Error(`The ${release.identity} Program package identity or version does not match its release`);
|
|
97
|
+
}
|
|
98
|
+
if (value.storage !== undefined)
|
|
99
|
+
throw new Error("A published Program package cannot choose its installed storage");
|
|
100
|
+
return {
|
|
101
|
+
...value,
|
|
102
|
+
...pathField(value, directory, "apiDocs", "api-docs.md"),
|
|
103
|
+
...pathField(value, directory, "icon", "icon.png"),
|
|
104
|
+
...half(value, directory, "server"),
|
|
105
|
+
...half(value, directory, "client")
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
function half(value, directory, name) {
|
|
109
|
+
const declared = value[name];
|
|
110
|
+
if (declared === undefined)
|
|
111
|
+
return {};
|
|
112
|
+
if (!record(declared) || declared.location !== name)
|
|
113
|
+
throw new Error(`A published Program's ${name} must be packaged at ./${name}`);
|
|
114
|
+
return { [name]: { ...declared, location: join(directory, name) } };
|
|
115
|
+
}
|
|
116
|
+
function pathField(value, directory, field, canonical) {
|
|
117
|
+
const declared = value[field];
|
|
118
|
+
if (declared === undefined)
|
|
119
|
+
return {};
|
|
120
|
+
if (declared !== canonical)
|
|
121
|
+
throw new Error(`A published Program's ${field} must be packaged as ${canonical}`);
|
|
122
|
+
return { [field]: join(directory, canonical) };
|
|
123
|
+
}
|
|
124
|
+
function extract(bytes, directory) {
|
|
125
|
+
const archive = new AdmZip(bytes);
|
|
126
|
+
for (const entry of archive.getEntries()) {
|
|
127
|
+
const name = entry.entryName.replaceAll("\\", "/");
|
|
128
|
+
const destination = resolve(directory, name);
|
|
129
|
+
const within = relative(directory, destination);
|
|
130
|
+
if (!name || name.startsWith("/") || name.split("/").includes("..") || isAbsolute(within) || within.startsWith("..")) {
|
|
131
|
+
throw new Error(`The Program package contains an unsafe path: ${entry.entryName}`);
|
|
132
|
+
}
|
|
133
|
+
if (entry.isDirectory) {
|
|
134
|
+
mkdirSync(destination, { recursive: true, mode: 0o700 });
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
mkdirSync(dirname(destination), { recursive: true, mode: 0o700 });
|
|
138
|
+
writeFileSync(destination, entry.getData(), { mode: 0o600 });
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
async function fetchAsset(url, fetcher) {
|
|
142
|
+
const response = await fetcher(url, {
|
|
143
|
+
headers: { "User-Agent": "@phreshos/cli" },
|
|
144
|
+
signal: AbortSignal.timeout(120_000)
|
|
145
|
+
});
|
|
146
|
+
if (!response.ok)
|
|
147
|
+
throw new Error(`A Program release asset could not be downloaded (${response.status} ${response.statusText})`);
|
|
148
|
+
return response;
|
|
149
|
+
}
|
|
150
|
+
function asset(assets, name) {
|
|
151
|
+
const found = assets.find(item => record(item) && item.name === name && typeof item.browser_download_url === "string");
|
|
152
|
+
return record(found) && typeof found.browser_download_url === "string" ? found.browser_download_url : undefined;
|
|
153
|
+
}
|
|
154
|
+
function parseVersion(tag) {
|
|
155
|
+
const match = /^v(\d+)\.(\d+)\.(\d+)$/.exec(tag);
|
|
156
|
+
return match ? tag.slice(1) : undefined;
|
|
157
|
+
}
|
|
158
|
+
function compare(left, right) {
|
|
159
|
+
const a = left.split(".").map(Number);
|
|
160
|
+
const b = right.split(".").map(Number);
|
|
161
|
+
return (a[0] ?? 0) - (b[0] ?? 0) || (a[1] ?? 0) - (b[1] ?? 0) || (a[2] ?? 0) - (b[2] ?? 0);
|
|
162
|
+
}
|
|
163
|
+
function record(value) {
|
|
164
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
165
|
+
}
|
|
@@ -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 { minimumSystemNodeVersion } from "./node.js";
|
|
6
7
|
import AdmZip from "adm-zip";
|
|
7
8
|
/** Owns immutable release directories and the one atomic active record. */
|
|
8
9
|
export default class SystemInstallation {
|
|
@@ -171,7 +172,13 @@ async function validateDistribution(directory) {
|
|
|
171
172
|
catch {
|
|
172
173
|
throw new Error("The System release has no valid production package manifest");
|
|
173
174
|
}
|
|
174
|
-
if (!record(manifest)
|
|
175
|
+
if (!record(manifest)
|
|
176
|
+
|| manifest.type !== "module"
|
|
177
|
+
|| !record(manifest.scripts)
|
|
178
|
+
|| manifest.scripts.start !== "node server/main.js"
|
|
179
|
+
|| !record(manifest.engines)
|
|
180
|
+
|| manifest.engines.node !== `>=${minimumSystemNodeVersion}`
|
|
181
|
+
|| !record(manifest.dependencies)) {
|
|
175
182
|
throw new Error("The System release package manifest is invalid");
|
|
176
183
|
}
|
|
177
184
|
for (const path of ["server/main.js", "client/index.html"]) {
|
package/dist/system/lifecycle.js
CHANGED
|
@@ -4,6 +4,7 @@ import { intakeReady, waitForIntake } from "./readiness.js";
|
|
|
4
4
|
import systemPaths from "./paths.js";
|
|
5
5
|
import systemService from "./service/index.js";
|
|
6
6
|
import nodeExecutable from "./node.js";
|
|
7
|
+
import installProgram from "../install.js";
|
|
7
8
|
import { existsSync } from "node:fs";
|
|
8
9
|
import { join } from "node:path";
|
|
9
10
|
/** Coordinates acquisition, immutable files, and the native service as one transaction. */
|
|
@@ -18,7 +19,8 @@ export default class SystemLifecycle {
|
|
|
18
19
|
resolveRelease: dependencies?.resolveRelease ?? resolveSystemRelease,
|
|
19
20
|
downloadRelease: dependencies?.downloadRelease ?? downloadSystemRelease,
|
|
20
21
|
ready: dependencies?.ready ?? intakeReady,
|
|
21
|
-
wait: dependencies?.wait ?? waitForIntake
|
|
22
|
+
wait: dependencies?.wait ?? waitForIntake,
|
|
23
|
+
provisionSetup: dependencies?.provisionSetup ?? provisionSetup
|
|
22
24
|
};
|
|
23
25
|
}
|
|
24
26
|
async install() {
|
|
@@ -40,13 +42,24 @@ export default class SystemLifecycle {
|
|
|
40
42
|
await service.start();
|
|
41
43
|
await this.waitUntilReady();
|
|
42
44
|
await activation.commit();
|
|
43
|
-
return await this.status();
|
|
44
45
|
}
|
|
45
46
|
catch (error) {
|
|
46
47
|
await service.stop().catch(() => undefined);
|
|
47
48
|
await activation.rollback();
|
|
48
49
|
return await this.restoredFailure(error, previous, previousService);
|
|
49
50
|
}
|
|
51
|
+
// The System transaction is complete before a Program crosses its
|
|
52
|
+
// live intake. A provisioning failure therefore leaves a healthy
|
|
53
|
+
// System available for a retry instead of rolling it back around a
|
|
54
|
+
// separate Program installation that may already have succeeded.
|
|
55
|
+
try {
|
|
56
|
+
await this.dependencies.provisionSetup();
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
60
|
+
throw new Error(`The PhreshOS System is running, but Setup could not be provisioned: ${reason}`, { cause: error });
|
|
61
|
+
}
|
|
62
|
+
return await this.status();
|
|
50
63
|
}
|
|
51
64
|
async uninstall() {
|
|
52
65
|
return await this.dependencies.installation.exclusive(() => this.uninstallExclusive());
|
|
@@ -168,6 +181,9 @@ export default class SystemLifecycle {
|
|
|
168
181
|
throw error;
|
|
169
182
|
}
|
|
170
183
|
}
|
|
184
|
+
async function provisionSetup() {
|
|
185
|
+
await installProgram({ name: "setup", run: true, startup: true, announce: false });
|
|
186
|
+
}
|
|
171
187
|
function definition(installation, executable) {
|
|
172
188
|
return {
|
|
173
189
|
executable,
|
package/dist/system/node.js
CHANGED
|
@@ -1,9 +1,28 @@
|
|
|
1
1
|
import { isAbsolute } from "node:path";
|
|
2
2
|
import { requireSuccess } from "./process.js";
|
|
3
|
+
export const minimumSystemNodeVersion = "24.15.0";
|
|
3
4
|
/** Resolve the real Node executable even when another runtime invoked the CLI. */
|
|
4
5
|
export default async function nodeExecutable() {
|
|
5
|
-
|
|
6
|
-
|
|
6
|
+
const executable = !process.versions.bun && process.release.name === "node" && isAbsolute(process.execPath)
|
|
7
|
+
? process.execPath
|
|
8
|
+
: await discoveredNodeExecutable();
|
|
9
|
+
const result = await requireSuccess(executable, ["-p", "process.versions.node"]);
|
|
10
|
+
const version = result.stdout.trim();
|
|
11
|
+
if (!supportsSystemNode(version)) {
|
|
12
|
+
throw new Error(`Node.js ${minimumSystemNodeVersion} or newer is required to run the PhreshOS System${version ? ` (found ${version})` : ""}`);
|
|
13
|
+
}
|
|
14
|
+
return executable;
|
|
15
|
+
}
|
|
16
|
+
export function supportsSystemNode(version) {
|
|
17
|
+
const actual = parseVersion(version);
|
|
18
|
+
const minimum = parseVersion(minimumSystemNodeVersion);
|
|
19
|
+
if (!actual || !minimum)
|
|
20
|
+
return false;
|
|
21
|
+
return actual.major > minimum.major
|
|
22
|
+
|| actual.major === minimum.major && actual.minor > minimum.minor
|
|
23
|
+
|| actual.major === minimum.major && actual.minor === minimum.minor && actual.patch >= minimum.patch;
|
|
24
|
+
}
|
|
25
|
+
async function discoveredNodeExecutable() {
|
|
7
26
|
const command = process.platform === "win32" ? "node.exe" : "node";
|
|
8
27
|
const result = await requireSuccess(command, ["-p", "process.execPath"]);
|
|
9
28
|
const executable = result.stdout.trim();
|
|
@@ -11,3 +30,13 @@ export default async function nodeExecutable() {
|
|
|
11
30
|
throw new Error("A real Node.js executable is required to run the PhreshOS System service");
|
|
12
31
|
return executable;
|
|
13
32
|
}
|
|
33
|
+
function parseVersion(version) {
|
|
34
|
+
const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(version);
|
|
35
|
+
if (!match)
|
|
36
|
+
return undefined;
|
|
37
|
+
return {
|
|
38
|
+
major: Number(match[1]),
|
|
39
|
+
minor: Number(match[2]),
|
|
40
|
+
patch: Number(match[3])
|
|
41
|
+
};
|
|
42
|
+
}
|
package/dist/template/gitignore
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "phresh-program",
|
|
3
3
|
"private": true,
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.2",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"dev": "phresh dev",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"react-dom": "^19.2.8"
|
|
17
17
|
},
|
|
18
18
|
"devDependencies": {
|
|
19
|
-
"@phreshos/cli": "^0.1.
|
|
19
|
+
"@phreshos/cli": "^0.1.11",
|
|
20
20
|
"@types/node": "^26.2.0",
|
|
21
21
|
"@types/react": "^19.2.18",
|
|
22
22
|
"@types/react-dom": "^19.2.4",
|
|
@@ -22,7 +22,7 @@ export default defineConfig({
|
|
|
22
22
|
// these values determines the Program's identity.
|
|
23
23
|
name: "Phresh Program",
|
|
24
24
|
description: "A simple counter whose state lives on the Server.",
|
|
25
|
-
version: "0.1.
|
|
25
|
+
version: "0.1.2",
|
|
26
26
|
|
|
27
27
|
// Markdown entry point for the API owned by this Program. It documents the
|
|
28
28
|
// counter service contract; PhreshOS endpoint mechanics belong in PhreshOS
|
package/dist/template.json
CHANGED