@elpapi42/pi-fleet 0.1.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -0
- package/LICENSE +21 -0
- package/README.md +201 -0
- package/bin/pifleet-runtime.mjs +15 -0
- package/bin/pifleet.mjs +15 -0
- package/dist/cli-meta.json +843 -0
- package/dist/cli.mjs +4234 -0
- package/dist/cli.mjs.map +7 -0
- package/dist/installer-meta.json +274 -0
- package/dist/installer.mjs +391 -0
- package/dist/installer.mjs.map +7 -0
- package/dist/runtime-manifest.json +105 -0
- package/dist/runtime-meta.json +6963 -0
- package/dist/runtime.mjs +8150 -0
- package/dist/runtime.mjs.map +7 -0
- package/dist/sqlite-worker-meta.json +84 -0
- package/dist/sqlite-worker.mjs +304 -0
- package/dist/sqlite-worker.mjs.map +7 -0
- package/package.json +77 -0
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
{
|
|
2
|
+
"inputs": {
|
|
3
|
+
"src/platform/install/service-definition.ts": {
|
|
4
|
+
"bytes": 2243,
|
|
5
|
+
"imports": [
|
|
6
|
+
{
|
|
7
|
+
"path": "node:path",
|
|
8
|
+
"kind": "import-statement",
|
|
9
|
+
"external": true
|
|
10
|
+
}
|
|
11
|
+
],
|
|
12
|
+
"format": "esm"
|
|
13
|
+
},
|
|
14
|
+
"src/platform/install/service-installer.ts": {
|
|
15
|
+
"bytes": 5091,
|
|
16
|
+
"imports": [
|
|
17
|
+
{
|
|
18
|
+
"path": "node:fs",
|
|
19
|
+
"kind": "import-statement",
|
|
20
|
+
"external": true
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"path": "node:fs/promises",
|
|
24
|
+
"kind": "import-statement",
|
|
25
|
+
"external": true
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"path": "node:os",
|
|
29
|
+
"kind": "import-statement",
|
|
30
|
+
"external": true
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"path": "node:path",
|
|
34
|
+
"kind": "import-statement",
|
|
35
|
+
"external": true
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"path": "src/platform/install/service-definition.ts",
|
|
39
|
+
"kind": "import-statement",
|
|
40
|
+
"original": "./service-definition.js"
|
|
41
|
+
}
|
|
42
|
+
],
|
|
43
|
+
"format": "esm"
|
|
44
|
+
},
|
|
45
|
+
"src/platform/install/tree-integrity.ts": {
|
|
46
|
+
"bytes": 2225,
|
|
47
|
+
"imports": [
|
|
48
|
+
{
|
|
49
|
+
"path": "node:crypto",
|
|
50
|
+
"kind": "import-statement",
|
|
51
|
+
"external": true
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"path": "node:fs/promises",
|
|
55
|
+
"kind": "import-statement",
|
|
56
|
+
"external": true
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"path": "node:path",
|
|
60
|
+
"kind": "import-statement",
|
|
61
|
+
"external": true
|
|
62
|
+
}
|
|
63
|
+
],
|
|
64
|
+
"format": "esm"
|
|
65
|
+
},
|
|
66
|
+
"src/platform/install/runtime-release.ts": {
|
|
67
|
+
"bytes": 4620,
|
|
68
|
+
"imports": [
|
|
69
|
+
{
|
|
70
|
+
"path": "node:crypto",
|
|
71
|
+
"kind": "import-statement",
|
|
72
|
+
"external": true
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"path": "node:fs/promises",
|
|
76
|
+
"kind": "import-statement",
|
|
77
|
+
"external": true
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"path": "node:path",
|
|
81
|
+
"kind": "import-statement",
|
|
82
|
+
"external": true
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"path": "src/platform/install/tree-integrity.ts",
|
|
86
|
+
"kind": "import-statement",
|
|
87
|
+
"original": "./tree-integrity.js"
|
|
88
|
+
}
|
|
89
|
+
],
|
|
90
|
+
"format": "esm"
|
|
91
|
+
},
|
|
92
|
+
"src/platform/shared/paths.ts": {
|
|
93
|
+
"bytes": 1440,
|
|
94
|
+
"imports": [
|
|
95
|
+
{
|
|
96
|
+
"path": "node:os",
|
|
97
|
+
"kind": "import-statement",
|
|
98
|
+
"external": true
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"path": "node:path",
|
|
102
|
+
"kind": "import-statement",
|
|
103
|
+
"external": true
|
|
104
|
+
}
|
|
105
|
+
],
|
|
106
|
+
"format": "esm"
|
|
107
|
+
},
|
|
108
|
+
"src/entry/installer.ts": {
|
|
109
|
+
"bytes": 2140,
|
|
110
|
+
"imports": [
|
|
111
|
+
{
|
|
112
|
+
"path": "node:child_process",
|
|
113
|
+
"kind": "import-statement",
|
|
114
|
+
"external": true
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
"path": "node:path",
|
|
118
|
+
"kind": "import-statement",
|
|
119
|
+
"external": true
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
"path": "node:url",
|
|
123
|
+
"kind": "import-statement",
|
|
124
|
+
"external": true
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
"path": "node:util",
|
|
128
|
+
"kind": "import-statement",
|
|
129
|
+
"external": true
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
"path": "src/platform/install/service-installer.ts",
|
|
133
|
+
"kind": "import-statement",
|
|
134
|
+
"original": "../platform/install/service-installer.js"
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
"path": "src/platform/install/runtime-release.ts",
|
|
138
|
+
"kind": "import-statement",
|
|
139
|
+
"original": "../platform/install/runtime-release.js"
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
"path": "src/platform/shared/paths.ts",
|
|
143
|
+
"kind": "import-statement",
|
|
144
|
+
"original": "../platform/shared/paths.js"
|
|
145
|
+
}
|
|
146
|
+
],
|
|
147
|
+
"format": "esm"
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
"outputs": {
|
|
151
|
+
"dist/installer.mjs.map": {
|
|
152
|
+
"imports": [],
|
|
153
|
+
"exports": [],
|
|
154
|
+
"inputs": {},
|
|
155
|
+
"bytes": 29042
|
|
156
|
+
},
|
|
157
|
+
"dist/installer.mjs": {
|
|
158
|
+
"imports": [
|
|
159
|
+
{
|
|
160
|
+
"path": "node:child_process",
|
|
161
|
+
"kind": "import-statement",
|
|
162
|
+
"external": true
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
"path": "node:path",
|
|
166
|
+
"kind": "import-statement",
|
|
167
|
+
"external": true
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
"path": "node:url",
|
|
171
|
+
"kind": "import-statement",
|
|
172
|
+
"external": true
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
"path": "node:util",
|
|
176
|
+
"kind": "import-statement",
|
|
177
|
+
"external": true
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
"path": "node:fs",
|
|
181
|
+
"kind": "import-statement",
|
|
182
|
+
"external": true
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
"path": "node:fs/promises",
|
|
186
|
+
"kind": "import-statement",
|
|
187
|
+
"external": true
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
"path": "node:os",
|
|
191
|
+
"kind": "import-statement",
|
|
192
|
+
"external": true
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
"path": "node:path",
|
|
196
|
+
"kind": "import-statement",
|
|
197
|
+
"external": true
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
"path": "node:path",
|
|
201
|
+
"kind": "import-statement",
|
|
202
|
+
"external": true
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
"path": "node:crypto",
|
|
206
|
+
"kind": "import-statement",
|
|
207
|
+
"external": true
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
"path": "node:fs/promises",
|
|
211
|
+
"kind": "import-statement",
|
|
212
|
+
"external": true
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
"path": "node:path",
|
|
216
|
+
"kind": "import-statement",
|
|
217
|
+
"external": true
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
"path": "node:crypto",
|
|
221
|
+
"kind": "import-statement",
|
|
222
|
+
"external": true
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
"path": "node:fs/promises",
|
|
226
|
+
"kind": "import-statement",
|
|
227
|
+
"external": true
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
"path": "node:path",
|
|
231
|
+
"kind": "import-statement",
|
|
232
|
+
"external": true
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
"path": "node:os",
|
|
236
|
+
"kind": "import-statement",
|
|
237
|
+
"external": true
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
"path": "node:path",
|
|
241
|
+
"kind": "import-statement",
|
|
242
|
+
"external": true
|
|
243
|
+
}
|
|
244
|
+
],
|
|
245
|
+
"exports": [
|
|
246
|
+
"installRuntimeService",
|
|
247
|
+
"repairRuntimeService",
|
|
248
|
+
"uninstallRuntimeService"
|
|
249
|
+
],
|
|
250
|
+
"entryPoint": "src/entry/installer.ts",
|
|
251
|
+
"inputs": {
|
|
252
|
+
"src/entry/installer.ts": {
|
|
253
|
+
"bytesInOutput": 1705
|
|
254
|
+
},
|
|
255
|
+
"src/platform/install/service-installer.ts": {
|
|
256
|
+
"bytesInOutput": 4151
|
|
257
|
+
},
|
|
258
|
+
"src/platform/install/service-definition.ts": {
|
|
259
|
+
"bytesInOutput": 1889
|
|
260
|
+
},
|
|
261
|
+
"src/platform/install/runtime-release.ts": {
|
|
262
|
+
"bytesInOutput": 4023
|
|
263
|
+
},
|
|
264
|
+
"src/platform/install/tree-integrity.ts": {
|
|
265
|
+
"bytesInOutput": 1890
|
|
266
|
+
},
|
|
267
|
+
"src/platform/shared/paths.ts": {
|
|
268
|
+
"bytesInOutput": 1207
|
|
269
|
+
}
|
|
270
|
+
},
|
|
271
|
+
"bytes": 15347
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
// src/entry/installer.ts
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
3
|
+
import { dirname as dirname2, join as join5 } from "node:path";
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
|
|
7
|
+
// src/platform/install/service-installer.ts
|
|
8
|
+
import { constants } from "node:fs";
|
|
9
|
+
import { access, chmod, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
10
|
+
import { homedir } from "node:os";
|
|
11
|
+
import { dirname, join } from "node:path";
|
|
12
|
+
|
|
13
|
+
// src/platform/install/service-definition.ts
|
|
14
|
+
import { isAbsolute } from "node:path";
|
|
15
|
+
function systemdUserUnit(options) {
|
|
16
|
+
validate(options);
|
|
17
|
+
const environment = options.stateRoot === void 0 ? "" : `Environment=PIFLEET_STATE_ROOT=${systemdEscape(options.stateRoot)}
|
|
18
|
+
`;
|
|
19
|
+
return `[Unit]
|
|
20
|
+
Description=Pi Fleet user runtime
|
|
21
|
+
After=default.target
|
|
22
|
+
|
|
23
|
+
[Service]
|
|
24
|
+
Type=simple
|
|
25
|
+
ExecStart=${systemdEscape(options.nodePath)} ${systemdEscape(options.runtimePath)}
|
|
26
|
+
${environment}Restart=on-failure
|
|
27
|
+
RestartSec=1
|
|
28
|
+
TimeoutStopSec=10
|
|
29
|
+
UMask=0077
|
|
30
|
+
KillMode=control-group
|
|
31
|
+
|
|
32
|
+
[Install]
|
|
33
|
+
WantedBy=default.target
|
|
34
|
+
`;
|
|
35
|
+
}
|
|
36
|
+
function launchdAgentPlist(options) {
|
|
37
|
+
validate(options);
|
|
38
|
+
const environment = options.stateRoot === void 0 ? "" : ` <key>EnvironmentVariables</key>
|
|
39
|
+
<dict><key>PIFLEET_STATE_ROOT</key><string>${xmlEscape(options.stateRoot)}</string></dict>
|
|
40
|
+
`;
|
|
41
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
42
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
43
|
+
<plist version="1.0">
|
|
44
|
+
<dict>
|
|
45
|
+
<key>Label</key><string>works.elpapi.pifleet</string>
|
|
46
|
+
<key>ProgramArguments</key>
|
|
47
|
+
<array>
|
|
48
|
+
<string>${xmlEscape(options.nodePath)}</string>
|
|
49
|
+
<string>${xmlEscape(options.runtimePath)}</string>
|
|
50
|
+
</array>
|
|
51
|
+
${environment} <key>RunAtLoad</key><true/>
|
|
52
|
+
<key>KeepAlive</key><dict><key>SuccessfulExit</key><false/></dict>
|
|
53
|
+
<key>ProcessType</key><string>Background</string>
|
|
54
|
+
</dict>
|
|
55
|
+
</plist>
|
|
56
|
+
`;
|
|
57
|
+
}
|
|
58
|
+
function validate(options) {
|
|
59
|
+
if (!isAbsolute(options.nodePath) || !isAbsolute(options.runtimePath)) {
|
|
60
|
+
throw new Error("Service executables must use absolute paths");
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function systemdEscape(value) {
|
|
64
|
+
if (!/^[A-Za-z0-9_./:@-]+$/.test(value)) throw new Error(`Unsafe systemd path ${value}`);
|
|
65
|
+
return value;
|
|
66
|
+
}
|
|
67
|
+
function xmlEscape(value) {
|
|
68
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// src/platform/install/service-installer.ts
|
|
72
|
+
async function installUserService(options) {
|
|
73
|
+
const home = options.home ?? homedir();
|
|
74
|
+
if (options.platform === "linux") {
|
|
75
|
+
const path = join(home, ".config", "systemd", "user", "pi-fleet.service");
|
|
76
|
+
const effectiveDefinition = await preserveInstalledStateRoot(path, options.definition, "linux");
|
|
77
|
+
const definition = systemdUserUnit(effectiveDefinition);
|
|
78
|
+
const needsRepair = await serviceDefinitionNeedsRepair(path, definition, effectiveDefinition);
|
|
79
|
+
if (needsRepair) {
|
|
80
|
+
await atomicWrite(path, definition);
|
|
81
|
+
await options.executor.run("systemctl", ["--user", "daemon-reload"]);
|
|
82
|
+
}
|
|
83
|
+
await options.executor.run("systemctl", ["--user", "enable", "--now", "pi-fleet.service"]);
|
|
84
|
+
if (needsRepair) {
|
|
85
|
+
await options.executor.run("systemctl", ["--user", "restart", "pi-fleet.service"]);
|
|
86
|
+
}
|
|
87
|
+
return path;
|
|
88
|
+
}
|
|
89
|
+
if (options.platform === "darwin") {
|
|
90
|
+
const path = join(home, "Library", "LaunchAgents", "works.elpapi.pifleet.plist");
|
|
91
|
+
const effectiveDefinition = await preserveInstalledStateRoot(
|
|
92
|
+
path,
|
|
93
|
+
options.definition,
|
|
94
|
+
"darwin"
|
|
95
|
+
);
|
|
96
|
+
await atomicWrite(path, launchdAgentPlist(effectiveDefinition));
|
|
97
|
+
const domain = `gui/${options.uid ?? process.getuid?.() ?? 0}`;
|
|
98
|
+
await options.executor.run("launchctl", ["bootout", domain, path]).catch(() => void 0);
|
|
99
|
+
await options.executor.run("launchctl", ["bootstrap", domain, path]);
|
|
100
|
+
await options.executor.run("launchctl", ["kickstart", `${domain}/works.elpapi.pifleet`]);
|
|
101
|
+
return path;
|
|
102
|
+
}
|
|
103
|
+
throw new Error(`Native Pi Fleet supervision is unsupported on ${options.platform}`);
|
|
104
|
+
}
|
|
105
|
+
async function uninstallUserService(options) {
|
|
106
|
+
const home = options.home ?? homedir();
|
|
107
|
+
if (options.platform === "linux") {
|
|
108
|
+
const path = join(home, ".config", "systemd", "user", "pi-fleet.service");
|
|
109
|
+
await options.executor.run("systemctl", ["--user", "disable", "--now", "pi-fleet.service"]);
|
|
110
|
+
await rm(path, { force: true });
|
|
111
|
+
await options.executor.run("systemctl", ["--user", "daemon-reload"]);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (options.platform === "darwin") {
|
|
115
|
+
const path = join(home, "Library", "LaunchAgents", "works.elpapi.pifleet.plist");
|
|
116
|
+
const domain = `gui/${options.uid ?? process.getuid?.() ?? 0}`;
|
|
117
|
+
await options.executor.run("launchctl", ["bootout", domain, path]).catch(() => void 0);
|
|
118
|
+
await rm(path, { force: true });
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
throw new Error(`Native Pi Fleet supervision is unsupported on ${options.platform}`);
|
|
122
|
+
}
|
|
123
|
+
async function serviceDefinitionNeedsRepair(path, expectedContents, definition) {
|
|
124
|
+
const current = await readFile(path, "utf8").catch((error) => {
|
|
125
|
+
if (error.code === "ENOENT") return null;
|
|
126
|
+
throw error;
|
|
127
|
+
});
|
|
128
|
+
if (current !== expectedContents) return true;
|
|
129
|
+
try {
|
|
130
|
+
await access(definition.nodePath, constants.X_OK);
|
|
131
|
+
await access(definition.runtimePath, constants.R_OK);
|
|
132
|
+
return false;
|
|
133
|
+
} catch {
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
async function preserveInstalledStateRoot(path, definition, platform) {
|
|
138
|
+
if (definition.stateRoot !== void 0) return definition;
|
|
139
|
+
const current = await readFile(path, "utf8").catch((error) => {
|
|
140
|
+
if (error.code === "ENOENT") return null;
|
|
141
|
+
throw error;
|
|
142
|
+
});
|
|
143
|
+
if (current === null) return definition;
|
|
144
|
+
const encoded = platform === "linux" ? /^Environment=PIFLEET_STATE_ROOT=(.+)$/m.exec(current)?.[1] : /<key>PIFLEET_STATE_ROOT<\/key><string>([^<]+)<\/string>/.exec(current)?.[1];
|
|
145
|
+
if (encoded === void 0) return definition;
|
|
146
|
+
const stateRoot = platform === "linux" ? encoded : encoded.replaceAll("'", "'").replaceAll(""", '"').replaceAll(">", ">").replaceAll("<", "<").replaceAll("&", "&");
|
|
147
|
+
return { ...definition, stateRoot };
|
|
148
|
+
}
|
|
149
|
+
async function atomicWrite(path, contents) {
|
|
150
|
+
await mkdir(dirname(path), { recursive: true, mode: 448 });
|
|
151
|
+
const temporary = `${path}.tmp-${process.pid}`;
|
|
152
|
+
await writeFile(temporary, contents, { mode: 384 });
|
|
153
|
+
await chmod(temporary, 384);
|
|
154
|
+
await rename(temporary, path);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// src/platform/install/runtime-release.ts
|
|
158
|
+
import { createHash as createHash2, randomUUID } from "node:crypto";
|
|
159
|
+
import { chmod as chmod2, cp, lstat as lstat2, mkdir as mkdir2, readFile as readFile3, rename as rename2, rm as rm2, stat as stat2 } from "node:fs/promises";
|
|
160
|
+
import { join as join3, resolve as resolve2, sep as sep2 } from "node:path";
|
|
161
|
+
|
|
162
|
+
// src/platform/install/tree-integrity.ts
|
|
163
|
+
import { createHash } from "node:crypto";
|
|
164
|
+
import { lstat, readFile as readFile2, readdir, stat } from "node:fs/promises";
|
|
165
|
+
import { join as join2, relative, resolve, sep } from "node:path";
|
|
166
|
+
async function hashDirectoryTree(root, manifestPath) {
|
|
167
|
+
const resolvedRoot = resolve(root);
|
|
168
|
+
const entries = await collectFiles(resolvedRoot, resolvedRoot);
|
|
169
|
+
const hash = createHash("sha256");
|
|
170
|
+
let bytes = 0;
|
|
171
|
+
for (const entry of entries) {
|
|
172
|
+
const contents = await readFile2(entry.absolutePath);
|
|
173
|
+
bytes += contents.length;
|
|
174
|
+
hash.update(entry.relativePath);
|
|
175
|
+
hash.update("\0");
|
|
176
|
+
hash.update(String(contents.length));
|
|
177
|
+
hash.update("\0");
|
|
178
|
+
hash.update(contents);
|
|
179
|
+
hash.update("\0");
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
path: manifestPath,
|
|
183
|
+
files: entries.length,
|
|
184
|
+
bytes,
|
|
185
|
+
sha256: hash.digest("hex")
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
async function collectFiles(root, directory) {
|
|
189
|
+
const output = [];
|
|
190
|
+
for (const name of (await readdir(directory)).sort()) {
|
|
191
|
+
const absolutePath = join2(directory, name);
|
|
192
|
+
const linkInfo = await lstat(absolutePath);
|
|
193
|
+
const info = linkInfo.isSymbolicLink() ? await stat(absolutePath) : linkInfo;
|
|
194
|
+
if (info.isDirectory()) {
|
|
195
|
+
if (linkInfo.isSymbolicLink()) {
|
|
196
|
+
throw new Error(`Runtime dependency tree contains a directory symlink: ${absolutePath}`);
|
|
197
|
+
}
|
|
198
|
+
output.push(...await collectFiles(root, absolutePath));
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
if (!info.isFile()) {
|
|
202
|
+
throw new Error(`Runtime dependency tree contains an unsupported entry: ${absolutePath}`);
|
|
203
|
+
}
|
|
204
|
+
const relativePath = relative(root, absolutePath).split(sep).join("/");
|
|
205
|
+
if (relativePath.length === 0 || relativePath.startsWith("../")) {
|
|
206
|
+
throw new Error(`Runtime dependency path escapes its root: ${absolutePath}`);
|
|
207
|
+
}
|
|
208
|
+
output.push({ absolutePath, relativePath });
|
|
209
|
+
}
|
|
210
|
+
return output.sort((left, right) => left.relativePath.localeCompare(right.relativePath));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// src/platform/install/runtime-release.ts
|
|
214
|
+
async function materializeRuntime(options) {
|
|
215
|
+
const sourceRoot = resolve2(options.sourceRoot);
|
|
216
|
+
const applicationRoot = resolve2(options.applicationRoot);
|
|
217
|
+
await ensurePrivateDirectory(applicationRoot);
|
|
218
|
+
const manifestBytes = await readFile3(join3(sourceRoot, "dist", "runtime-manifest.json"));
|
|
219
|
+
const manifest = JSON.parse(manifestBytes.toString("utf8"));
|
|
220
|
+
const manifestHash = createHash2("sha256").update(manifestBytes).digest("hex").slice(0, 16);
|
|
221
|
+
const releasesRoot = join3(applicationRoot, "releases");
|
|
222
|
+
await ensurePrivateDirectory(releasesRoot);
|
|
223
|
+
const destination = join3(releasesRoot, `${manifest.package.version}-${manifestHash}`);
|
|
224
|
+
if (await pathExists(destination)) {
|
|
225
|
+
await verifyRuntime(destination, manifest);
|
|
226
|
+
return destination;
|
|
227
|
+
}
|
|
228
|
+
const staging = join3(releasesRoot, `.staging-${randomUUID()}`);
|
|
229
|
+
await mkdir2(staging, { mode: 448 });
|
|
230
|
+
try {
|
|
231
|
+
await cp(join3(sourceRoot, "dist"), join3(staging, "dist"), {
|
|
232
|
+
recursive: true,
|
|
233
|
+
dereference: true
|
|
234
|
+
});
|
|
235
|
+
await cp(join3(sourceRoot, "bin"), join3(staging, "bin"), { recursive: true, dereference: true });
|
|
236
|
+
await cp(join3(sourceRoot, "package.json"), join3(staging, "package.json"));
|
|
237
|
+
for (const tree of manifest.trees) {
|
|
238
|
+
const source = resolveInside(sourceRoot, tree.path);
|
|
239
|
+
const destination2 = resolveInside(staging, tree.path);
|
|
240
|
+
await mkdir2(resolve2(destination2, ".."), { recursive: true, mode: 448 });
|
|
241
|
+
await cp(source, destination2, { recursive: true, dereference: true });
|
|
242
|
+
}
|
|
243
|
+
await verifyRuntime(staging, manifest);
|
|
244
|
+
await chmod2(staging, 448);
|
|
245
|
+
await rename2(staging, destination);
|
|
246
|
+
return destination;
|
|
247
|
+
} catch (error) {
|
|
248
|
+
await rm2(staging, { recursive: true, force: true });
|
|
249
|
+
throw error;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
async function verifyRuntime(root, manifest) {
|
|
253
|
+
const expected = manifest ?? JSON.parse(
|
|
254
|
+
await readFile3(join3(root, "dist", "runtime-manifest.json"), "utf8")
|
|
255
|
+
);
|
|
256
|
+
const resolvedRoot = resolve2(root);
|
|
257
|
+
for (const file of expected.files) {
|
|
258
|
+
const path = resolveInside(resolvedRoot, file.path);
|
|
259
|
+
const info = await stat2(path);
|
|
260
|
+
if (!info.isFile() || info.size !== file.bytes) {
|
|
261
|
+
throw new Error(`Runtime artifact ${file.path} has changed`);
|
|
262
|
+
}
|
|
263
|
+
const hash = createHash2("sha256").update(await readFile3(path)).digest("hex");
|
|
264
|
+
if (hash !== file.sha256) {
|
|
265
|
+
throw new Error(`Runtime artifact ${file.path} failed verification`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
for (const expectedTree of expected.trees) {
|
|
269
|
+
const treeRoot = resolveInside(resolvedRoot, expectedTree.path);
|
|
270
|
+
const actualTree = await hashDirectoryTree(treeRoot, expectedTree.path);
|
|
271
|
+
if (actualTree.files !== expectedTree.files || actualTree.bytes !== expectedTree.bytes || actualTree.sha256 !== expectedTree.sha256) {
|
|
272
|
+
throw new Error(`Runtime dependency tree ${expectedTree.path} failed verification`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
function resolveInside(root, path) {
|
|
277
|
+
const resolved = resolve2(root, path);
|
|
278
|
+
if (!resolved.startsWith(`${root}${sep2}`)) {
|
|
279
|
+
throw new Error(`Runtime manifest contains an unsafe path ${path}`);
|
|
280
|
+
}
|
|
281
|
+
return resolved;
|
|
282
|
+
}
|
|
283
|
+
async function ensurePrivateDirectory(path) {
|
|
284
|
+
await mkdir2(path, { recursive: true, mode: 448 });
|
|
285
|
+
const info = await lstat2(path);
|
|
286
|
+
if (!info.isDirectory() || info.isSymbolicLink()) {
|
|
287
|
+
throw new Error(`Refusing unsafe runtime release path ${path}`);
|
|
288
|
+
}
|
|
289
|
+
if (typeof process.getuid === "function" && info.uid !== process.getuid()) {
|
|
290
|
+
throw new Error(`Runtime release path ${path} is not owned by the current user`);
|
|
291
|
+
}
|
|
292
|
+
await chmod2(path, 448);
|
|
293
|
+
}
|
|
294
|
+
async function pathExists(path) {
|
|
295
|
+
try {
|
|
296
|
+
await lstat2(path);
|
|
297
|
+
return true;
|
|
298
|
+
} catch (error) {
|
|
299
|
+
if (error.code === "ENOENT") return false;
|
|
300
|
+
throw error;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// src/platform/shared/paths.ts
|
|
305
|
+
import { homedir as homedir2, tmpdir } from "node:os";
|
|
306
|
+
import { join as join4, resolve as resolve3 } from "node:path";
|
|
307
|
+
function resolveApplicationRoot(env = process.env) {
|
|
308
|
+
return env.PIFLEET_APPLICATION_ROOT ?? (process.platform === "darwin" ? join4(homedir2(), "Library", "Application Support", "Pi Fleet", "runtime") : join4(env.XDG_DATA_HOME ?? join4(homedir2(), ".local", "share"), "pi-fleet"));
|
|
309
|
+
}
|
|
310
|
+
function resolveFleetPaths(env = process.env) {
|
|
311
|
+
const explicitRoot = env.PIFLEET_STATE_ROOT;
|
|
312
|
+
if (explicitRoot !== void 0) {
|
|
313
|
+
const root = resolve3(explicitRoot);
|
|
314
|
+
return {
|
|
315
|
+
runtimeRoot: root,
|
|
316
|
+
stateRoot: root,
|
|
317
|
+
socketPath: join4(root, "control.sock"),
|
|
318
|
+
databasePath: join4(root, "fleet.sqlite")
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
const runtimeRoot = join4(
|
|
322
|
+
env.XDG_RUNTIME_DIR ?? tmpdir(),
|
|
323
|
+
`pifleet-${process.getuid?.() ?? "user"}`
|
|
324
|
+
);
|
|
325
|
+
const stateRoot = process.platform === "darwin" ? join4(homedir2(), "Library", "Application Support", "Pi Fleet") : join4(env.XDG_STATE_HOME ?? join4(homedir2(), ".local", "state"), "pi-fleet");
|
|
326
|
+
return {
|
|
327
|
+
runtimeRoot,
|
|
328
|
+
stateRoot,
|
|
329
|
+
socketPath: join4(runtimeRoot, "control.sock"),
|
|
330
|
+
databasePath: join4(stateRoot, "fleet.sqlite")
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// src/entry/installer.ts
|
|
335
|
+
var executor = {
|
|
336
|
+
async run(command, args) {
|
|
337
|
+
await promisify(execFile)(command, [...args]);
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
async function installRuntimeService() {
|
|
341
|
+
return installMaterializedService(resolveFleetPaths().stateRoot);
|
|
342
|
+
}
|
|
343
|
+
async function repairRuntimeService() {
|
|
344
|
+
return installMaterializedService(process.env.PIFLEET_STATE_ROOT);
|
|
345
|
+
}
|
|
346
|
+
async function installMaterializedService(stateRoot) {
|
|
347
|
+
const sourceRoot = dirname2(dirname2(fileURLToPath(import.meta.url)));
|
|
348
|
+
const release = await materializeRuntime({
|
|
349
|
+
sourceRoot,
|
|
350
|
+
applicationRoot: resolveApplicationRoot()
|
|
351
|
+
});
|
|
352
|
+
return installUserService({
|
|
353
|
+
platform: process.platform,
|
|
354
|
+
definition: {
|
|
355
|
+
nodePath: process.execPath,
|
|
356
|
+
runtimePath: join5(release, "bin", "pifleet-runtime.mjs"),
|
|
357
|
+
...stateRoot === void 0 ? {} : { stateRoot }
|
|
358
|
+
},
|
|
359
|
+
executor
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
async function uninstallRuntimeService() {
|
|
363
|
+
await uninstallUserService({ platform: process.platform, executor });
|
|
364
|
+
}
|
|
365
|
+
async function main() {
|
|
366
|
+
const command = process.argv[2];
|
|
367
|
+
if (command === "install") {
|
|
368
|
+
process.stdout.write(`${await installRuntimeService()}
|
|
369
|
+
`);
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
if (command === "repair") {
|
|
373
|
+
process.stdout.write(`${await repairRuntimeService()}
|
|
374
|
+
`);
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
if (command === "uninstall") {
|
|
378
|
+
await uninstallRuntimeService();
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
throw new Error("Usage: node dist/installer.mjs <install|repair|uninstall>");
|
|
382
|
+
}
|
|
383
|
+
if (process.argv[1] !== void 0 && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
384
|
+
await main();
|
|
385
|
+
}
|
|
386
|
+
export {
|
|
387
|
+
installRuntimeService,
|
|
388
|
+
repairRuntimeService,
|
|
389
|
+
uninstallRuntimeService
|
|
390
|
+
};
|
|
391
|
+
//# sourceMappingURL=installer.mjs.map
|