@ebowwa/seedinstallation 0.3.0 → 0.4.1
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/LICENSE +1 -1
- package/README.md +36 -144
- package/dist/bootstrap.d.ts +5 -6
- package/dist/bootstrap.d.ts.map +1 -0
- package/dist/bootstrap.js +296 -215
- package/dist/clone.d.ts +1 -0
- package/dist/clone.d.ts.map +1 -0
- package/dist/clone.js +54 -68
- package/dist/device-auth.d.ts +4 -5
- package/dist/device-auth.d.ts.map +1 -0
- package/dist/device-auth.js +254 -164
- package/dist/index.d.ts +13 -12
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +943 -10
- package/dist/runtime.d.ts +9 -14
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +213 -114
- package/dist/sudo.d.ts +1 -0
- package/dist/sudo.d.ts.map +1 -0
- package/dist/sudo.js +125 -222
- package/dist/systemd.d.ts +207 -3
- package/dist/systemd.d.ts.map +1 -0
- package/dist/systemd.js +479 -177
- package/package.json +40 -40
package/dist/index.js
CHANGED
|
@@ -1,10 +1,943 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __export = (target, all) => {
|
|
3
|
+
for (var name in all)
|
|
4
|
+
__defProp(target, name, {
|
|
5
|
+
get: all[name],
|
|
6
|
+
enumerable: true,
|
|
7
|
+
configurable: true,
|
|
8
|
+
set: (newValue) => all[name] = () => newValue
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
12
|
+
|
|
13
|
+
// sudo.ts
|
|
14
|
+
var exports_sudo = {};
|
|
15
|
+
__export(exports_sudo, {
|
|
16
|
+
writeFile: () => writeFile,
|
|
17
|
+
sudo: () => sudo,
|
|
18
|
+
serviceEnable: () => serviceEnable,
|
|
19
|
+
service: () => service,
|
|
20
|
+
pkgInstall: () => pkgInstall,
|
|
21
|
+
exec: () => exec
|
|
22
|
+
});
|
|
23
|
+
async function sudo(cmd, opts) {
|
|
24
|
+
const parts = Array.isArray(cmd) ? cmd : cmd.split(/\s+/);
|
|
25
|
+
const envPrefix = opts.env ? Object.entries(opts.env).map(([k, v]) => `${k}=${shellEscape(v)}`) : [];
|
|
26
|
+
const sudoCmd = ["sudo", ...envPrefix, ...parts];
|
|
27
|
+
return exec(sudoCmd, opts);
|
|
28
|
+
}
|
|
29
|
+
async function pkgInstall(packages, opts) {
|
|
30
|
+
const pm = opts.pm ?? "apt";
|
|
31
|
+
const envOverride = {
|
|
32
|
+
...opts.env,
|
|
33
|
+
...opts.nonInteractive !== false ? { DEBIAN_FRONTEND: "noninteractive" } : {}
|
|
34
|
+
};
|
|
35
|
+
await sudo(updateCmd[pm], { ...opts, env: envOverride, quiet: true });
|
|
36
|
+
return sudo([...installCmd[pm], ...packages], {
|
|
37
|
+
...opts,
|
|
38
|
+
env: envOverride
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
async function writeFile(path, content, opts) {
|
|
42
|
+
const op = opts.append ? "-a" : "";
|
|
43
|
+
const teeCmd = `tee ${op} ${shellEscape(path)}`.trim();
|
|
44
|
+
const result = await execPipe(content, ["sudo", teeCmd], opts);
|
|
45
|
+
if (result.ok && opts.mode) {
|
|
46
|
+
await sudo(["chmod", opts.mode, path], opts);
|
|
47
|
+
}
|
|
48
|
+
if (result.ok && opts.owner) {
|
|
49
|
+
await sudo(["chown", opts.owner, path], opts);
|
|
50
|
+
}
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
async function service(name, action, opts) {
|
|
54
|
+
return sudo(["systemctl", action, name], opts);
|
|
55
|
+
}
|
|
56
|
+
async function serviceEnable(name, opts) {
|
|
57
|
+
return sudo(["systemctl", "enable", "--now", name], opts);
|
|
58
|
+
}
|
|
59
|
+
function buildSshPrefix(ctx) {
|
|
60
|
+
const parts = ["ssh", "-F", "/dev/null", "-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null"];
|
|
61
|
+
if (ctx.keyPath)
|
|
62
|
+
parts.push("-i", ctx.keyPath);
|
|
63
|
+
else if (ctx.key)
|
|
64
|
+
parts.push("-i", ctx.key);
|
|
65
|
+
if (ctx.port)
|
|
66
|
+
parts.push("-p", String(ctx.port));
|
|
67
|
+
parts.push(`${ctx.user ?? "root"}@${ctx.host}`);
|
|
68
|
+
return parts;
|
|
69
|
+
}
|
|
70
|
+
async function exec(args, opts) {
|
|
71
|
+
const ctx = opts.context ?? { type: "local" };
|
|
72
|
+
const finalArgs = ctx.type === "ssh" ? [...buildSshPrefix(ctx), args.map(shellEscape).join(" ")] : args;
|
|
73
|
+
const proc = Bun.spawn(finalArgs, {
|
|
74
|
+
stdout: "pipe",
|
|
75
|
+
stderr: "pipe",
|
|
76
|
+
timeout: opts.timeout ?? 30000
|
|
77
|
+
});
|
|
78
|
+
const exitCode = await proc.exited;
|
|
79
|
+
const stdout = opts.quiet ? "" : await new Response(proc.stdout).text();
|
|
80
|
+
const stderr = await new Response(proc.stderr).text();
|
|
81
|
+
return { stdout, stderr, exitCode, ok: exitCode === 0 };
|
|
82
|
+
}
|
|
83
|
+
async function execPipe(input, args, opts) {
|
|
84
|
+
const ctx = opts.context ?? { type: "local" };
|
|
85
|
+
if (ctx.type === "ssh") {
|
|
86
|
+
const sshPrefix = buildSshPrefix(ctx);
|
|
87
|
+
const remoteCmd = args.join(" ");
|
|
88
|
+
const fullArgs = [...sshPrefix, remoteCmd];
|
|
89
|
+
const proc2 = Bun.spawn(fullArgs, {
|
|
90
|
+
stdin: new TextEncoder().encode(input),
|
|
91
|
+
stdout: "pipe",
|
|
92
|
+
stderr: "pipe",
|
|
93
|
+
timeout: opts.timeout ?? 30000
|
|
94
|
+
});
|
|
95
|
+
const exitCode2 = await proc2.exited;
|
|
96
|
+
const stdout2 = opts.quiet ? "" : await new Response(proc2.stdout).text();
|
|
97
|
+
const stderr2 = await new Response(proc2.stderr).text();
|
|
98
|
+
return { stdout: stdout2, stderr: stderr2, exitCode: exitCode2, ok: exitCode2 === 0 };
|
|
99
|
+
}
|
|
100
|
+
const proc = Bun.spawn(["sh", "-c", args.join(" ")], {
|
|
101
|
+
stdin: new TextEncoder().encode(input),
|
|
102
|
+
stdout: "pipe",
|
|
103
|
+
stderr: "pipe",
|
|
104
|
+
timeout: opts.timeout ?? 30000
|
|
105
|
+
});
|
|
106
|
+
const exitCode = await proc.exited;
|
|
107
|
+
const stdout = opts.quiet ? "" : await new Response(proc.stdout).text();
|
|
108
|
+
const stderr = await new Response(proc.stderr).text();
|
|
109
|
+
return { stdout, stderr, exitCode, ok: exitCode === 0 };
|
|
110
|
+
}
|
|
111
|
+
function shellEscape(s) {
|
|
112
|
+
if (/^[a-zA-Z0-9._\-\/=:@]+$/.test(s))
|
|
113
|
+
return s;
|
|
114
|
+
return `'${s.replace(/'/g, "'\\''")}'`;
|
|
115
|
+
}
|
|
116
|
+
var installCmd, updateCmd;
|
|
117
|
+
var init_sudo = __esm(() => {
|
|
118
|
+
installCmd = {
|
|
119
|
+
apt: ["apt-get", "install", "-y"],
|
|
120
|
+
dnf: ["dnf", "install", "-y"],
|
|
121
|
+
apk: ["apk", "add", "--no-cache"]
|
|
122
|
+
};
|
|
123
|
+
updateCmd = {
|
|
124
|
+
apt: ["apt-get", "update", "-qq"],
|
|
125
|
+
dnf: ["dnf", "check-update"],
|
|
126
|
+
apk: ["apk", "update"]
|
|
127
|
+
};
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// clone.ts
|
|
131
|
+
async function clone(opts) {
|
|
132
|
+
const { repo, dest, ref, depth, sparse, cwd } = opts;
|
|
133
|
+
const repoName = dest ?? repoNameFrom(repo);
|
|
134
|
+
const targetDir = cwd ? `${cwd}/${repoName}` : repoName;
|
|
135
|
+
const args = ["git", "clone"];
|
|
136
|
+
if (depth)
|
|
137
|
+
args.push("--depth", String(depth));
|
|
138
|
+
if (ref && !sparse)
|
|
139
|
+
args.push("--branch", ref);
|
|
140
|
+
if (sparse)
|
|
141
|
+
args.push("--no-checkout", "--filter=blob:none");
|
|
142
|
+
args.push(repo, targetDir);
|
|
143
|
+
await run(args, cwd);
|
|
144
|
+
if (sparse) {
|
|
145
|
+
await run(["git", "sparse-checkout", "init", "--cone"], targetDir);
|
|
146
|
+
await run(["git", "sparse-checkout", "set", ...sparse], targetDir);
|
|
147
|
+
if (ref) {
|
|
148
|
+
await run(["git", "checkout", ref], targetDir);
|
|
149
|
+
} else {
|
|
150
|
+
await run(["git", "checkout"], targetDir);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const commit = (await run(["git", "rev-parse", "--short", "HEAD"], targetDir)).trim();
|
|
154
|
+
const checkedRef = (await run(["git", "rev-parse", "--abbrev-ref", "HEAD"], targetDir)).trim();
|
|
155
|
+
const absPath = (await run(["git", "rev-parse", "--show-toplevel"], targetDir)).trim();
|
|
156
|
+
return { path: absPath, ref: checkedRef, commit };
|
|
157
|
+
}
|
|
158
|
+
function repoNameFrom(url) {
|
|
159
|
+
return url.replace(/\.git$/, "").split("/").pop();
|
|
160
|
+
}
|
|
161
|
+
async function run(args, cwd) {
|
|
162
|
+
const proc = Bun.spawn(args, {
|
|
163
|
+
cwd,
|
|
164
|
+
stdout: "pipe",
|
|
165
|
+
stderr: "pipe"
|
|
166
|
+
});
|
|
167
|
+
const exitCode = await proc.exited;
|
|
168
|
+
const stdout = await new Response(proc.stdout).text();
|
|
169
|
+
if (exitCode !== 0) {
|
|
170
|
+
const stderr = await new Response(proc.stderr).text();
|
|
171
|
+
throw new Error(`${args.join(" ")} failed (exit ${exitCode}): ${stderr}`);
|
|
172
|
+
}
|
|
173
|
+
return stdout;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// runtime.ts
|
|
177
|
+
async function exec2(args, opts) {
|
|
178
|
+
const sudo2 = await Promise.resolve().then(() => (init_sudo(), exports_sudo)).then((m) => m.sudo);
|
|
179
|
+
return sudo2(args, opts);
|
|
180
|
+
}
|
|
181
|
+
async function installBun(opts = {}) {
|
|
182
|
+
const installCmd2 = opts.version ? `curl -fsSL https://bun.sh/install | bash -s "bun-${opts.version}"` : `curl -fsSL https://bun.sh/install | bash`;
|
|
183
|
+
await execShell(installCmd2, opts);
|
|
184
|
+
if (opts.systemWide) {
|
|
185
|
+
const bunPath = opts.installDir ?? "/root/.bun";
|
|
186
|
+
await addSystemPath(`${bunPath}/bin`, opts);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
async function addSystemPath(dir, opts = {}) {
|
|
190
|
+
const { writeFile: writeFile2 } = await Promise.resolve().then(() => (init_sudo(), exports_sudo));
|
|
191
|
+
const target = opts.target ?? "system";
|
|
192
|
+
const position = opts.position ?? "prepend";
|
|
193
|
+
const existingPath = target === "system" ? await getSystemPath(opts) : await getUserPath(opts);
|
|
194
|
+
const paths = existingPath.split(":");
|
|
195
|
+
if (paths.includes(dir)) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const newPath = position === "prepend" ? `${dir}:${existingPath}` : `${existingPath}:${dir}`;
|
|
199
|
+
if (target === "system") {
|
|
200
|
+
const content = `PATH="${newPath}"
|
|
201
|
+
`;
|
|
202
|
+
await writeFile2("/etc/environment", content, { ...opts, append: false });
|
|
203
|
+
} else {
|
|
204
|
+
const exportLine = `
|
|
205
|
+
export PATH="${newPath}"
|
|
206
|
+
`;
|
|
207
|
+
await writeFile2("/root/.bashrc", exportLine, { ...opts, append: true });
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
async function getSystemPath(opts) {
|
|
211
|
+
const result = await exec2(["grep", "^PATH=", "/etc/environment"], { ...opts, quiet: true });
|
|
212
|
+
if (result.ok && result.stdout) {
|
|
213
|
+
const match = result.stdout.match(/^PATH="([^"]+)"/);
|
|
214
|
+
if (match?.[1])
|
|
215
|
+
return match[1];
|
|
216
|
+
}
|
|
217
|
+
return "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
|
|
218
|
+
}
|
|
219
|
+
async function getUserPath(opts) {
|
|
220
|
+
const result = await exec2(["bash", "-lc", "echo $PATH"], { ...opts, quiet: true });
|
|
221
|
+
return result.stdout.trim();
|
|
222
|
+
}
|
|
223
|
+
async function symlink(source, target, opts = {}) {
|
|
224
|
+
const args = ["ln", "-s"];
|
|
225
|
+
if (opts.force)
|
|
226
|
+
args.push("-f");
|
|
227
|
+
args.push(source, target);
|
|
228
|
+
await exec2(args, opts);
|
|
229
|
+
}
|
|
230
|
+
async function linkBinaries(sourceDir, opts = {}) {
|
|
231
|
+
const result = await exec2(["ls", "-1", sourceDir], { ...opts, quiet: true });
|
|
232
|
+
if (!result.ok)
|
|
233
|
+
return;
|
|
234
|
+
const binaries = result.stdout.trim().split(`
|
|
235
|
+
`).filter(Boolean);
|
|
236
|
+
for (const bin of binaries) {
|
|
237
|
+
const source = `${sourceDir}/${bin}`;
|
|
238
|
+
const target = `/usr/local/bin/${bin}`;
|
|
239
|
+
await symlink(source, target, { ...opts, force: true });
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
async function getBunVersion(opts) {
|
|
243
|
+
const result = await exec2(["bash", "-lc", "bun --version"], { ...opts, quiet: true });
|
|
244
|
+
if (result.ok) {
|
|
245
|
+
return result.stdout.trim();
|
|
246
|
+
}
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
async function commandExists(cmd, opts) {
|
|
250
|
+
const result = await exec2(["which", cmd], { ...opts, quiet: true });
|
|
251
|
+
return result.ok;
|
|
252
|
+
}
|
|
253
|
+
async function execShell(cmd, opts) {
|
|
254
|
+
const result = await exec2(["bash", "-lc", cmd], opts);
|
|
255
|
+
if (!result.ok) {
|
|
256
|
+
throw new Error(`Shell command failed: ${result.stderr}`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// systemd.ts
|
|
261
|
+
async function exec3(args, opts) {
|
|
262
|
+
const { sudo: sudo2 } = await Promise.resolve().then(() => (init_sudo(), exports_sudo));
|
|
263
|
+
return sudo2(args, opts);
|
|
264
|
+
}
|
|
265
|
+
async function writeFile2(path, content, opts) {
|
|
266
|
+
const { writeFile: wf } = await Promise.resolve().then(() => (init_sudo(), exports_sudo));
|
|
267
|
+
return wf(path, content, opts);
|
|
268
|
+
}
|
|
269
|
+
var systemdCheckCache = null;
|
|
270
|
+
async function hasSystemd(opts) {
|
|
271
|
+
if (systemdCheckCache) {
|
|
272
|
+
return systemdCheckCache;
|
|
273
|
+
}
|
|
274
|
+
const sudoOpts = opts ?? { context: { type: "local" } };
|
|
275
|
+
const whichResult = await exec3(["which", "systemctl"], {
|
|
276
|
+
...sudoOpts,
|
|
277
|
+
quiet: true,
|
|
278
|
+
env: sudoOpts.env
|
|
279
|
+
});
|
|
280
|
+
if (!whichResult.ok || !whichResult.stdout.trim()) {
|
|
281
|
+
const result2 = {
|
|
282
|
+
available: false,
|
|
283
|
+
reason: "not_found",
|
|
284
|
+
message: "systemctl command not found - systemd is not installed or not in PATH"
|
|
285
|
+
};
|
|
286
|
+
systemdCheckCache = result2;
|
|
287
|
+
return result2;
|
|
288
|
+
}
|
|
289
|
+
const runningResult = await exec3(["systemctl", "is-system-running"], {
|
|
290
|
+
...sudoOpts,
|
|
291
|
+
quiet: true,
|
|
292
|
+
env: sudoOpts.env
|
|
293
|
+
});
|
|
294
|
+
if (runningResult.ok && runningResult.stdout.trim() !== "") {
|
|
295
|
+
const result2 = {
|
|
296
|
+
available: true,
|
|
297
|
+
message: "systemd is available and running"
|
|
298
|
+
};
|
|
299
|
+
systemdCheckCache = result2;
|
|
300
|
+
return result2;
|
|
301
|
+
}
|
|
302
|
+
const dirResult = await exec3(["test", "-d", "/run/systemd/system"], {
|
|
303
|
+
...sudoOpts,
|
|
304
|
+
quiet: true,
|
|
305
|
+
env: sudoOpts.env
|
|
306
|
+
});
|
|
307
|
+
if (dirResult.ok) {
|
|
308
|
+
const result2 = {
|
|
309
|
+
available: true,
|
|
310
|
+
message: "systemd detected via /run/systemd/system (container environment)"
|
|
311
|
+
};
|
|
312
|
+
systemdCheckCache = result2;
|
|
313
|
+
return result2;
|
|
314
|
+
}
|
|
315
|
+
const result = {
|
|
316
|
+
available: false,
|
|
317
|
+
reason: "not_running",
|
|
318
|
+
message: "systemd is not available on this system (container/initless environment)"
|
|
319
|
+
};
|
|
320
|
+
systemdCheckCache = result;
|
|
321
|
+
return result;
|
|
322
|
+
}
|
|
323
|
+
function clearSystemdCache() {
|
|
324
|
+
systemdCheckCache = null;
|
|
325
|
+
}
|
|
326
|
+
function generateServiceUnit(name, opts) {
|
|
327
|
+
const lines = [];
|
|
328
|
+
lines.push("[Unit]");
|
|
329
|
+
lines.push(`Description=${opts.description}`);
|
|
330
|
+
if (opts.after?.length) {
|
|
331
|
+
lines.push(`After=${opts.after.join(" ")}`);
|
|
332
|
+
}
|
|
333
|
+
if (opts.wants?.length) {
|
|
334
|
+
lines.push(`Wants=${opts.wants.join(" ")}`);
|
|
335
|
+
}
|
|
336
|
+
lines.push("");
|
|
337
|
+
lines.push("[Service]");
|
|
338
|
+
if (opts.type)
|
|
339
|
+
lines.push(`Type=${opts.type}`);
|
|
340
|
+
lines.push(`User=${opts.user ?? "root"}`);
|
|
341
|
+
if (opts.group)
|
|
342
|
+
lines.push(`Group=${opts.group}`);
|
|
343
|
+
lines.push(`WorkingDirectory=${opts.workingDirectory}`);
|
|
344
|
+
lines.push(`ExecStart=${opts.execStart}`);
|
|
345
|
+
if (opts.execStartPre?.length) {
|
|
346
|
+
opts.execStartPre.forEach((cmd, i) => {
|
|
347
|
+
lines.push(`ExecStartPre${i > 0 ? `=${i}` : ""}=${cmd}`);
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
if (opts.execStopPost?.length) {
|
|
351
|
+
opts.execStopPost.forEach((cmd, i) => {
|
|
352
|
+
lines.push(`ExecStopPost${i > 0 ? `=${i}` : ""}=${cmd}`);
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
if (opts.restart)
|
|
356
|
+
lines.push(`Restart=${opts.restart}`);
|
|
357
|
+
if (opts.restartSec)
|
|
358
|
+
lines.push(`RestartSec=${opts.restartSec}`);
|
|
359
|
+
if (opts.environment) {
|
|
360
|
+
Object.entries(opts.environment).forEach(([k, v]) => {
|
|
361
|
+
lines.push(`Environment="${k}=${v}"`);
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
if (opts.environmentFile) {
|
|
365
|
+
lines.push(`EnvironmentFile=${opts.environmentFile}`);
|
|
366
|
+
}
|
|
367
|
+
if (opts.standardOutput)
|
|
368
|
+
lines.push(`StandardOutput=${opts.standardOutput}`);
|
|
369
|
+
if (opts.standardError)
|
|
370
|
+
lines.push(`StandardError=${opts.standardError}`);
|
|
371
|
+
if (opts.nice !== undefined)
|
|
372
|
+
lines.push(`Nice=${opts.nice}`);
|
|
373
|
+
if (opts.noNewPrivileges)
|
|
374
|
+
lines.push("NoNewPrivileges=true");
|
|
375
|
+
if (opts.privateTmp)
|
|
376
|
+
lines.push("PrivateTmp=true");
|
|
377
|
+
if (opts.protectSystem)
|
|
378
|
+
lines.push(`ProtectSystem=${opts.protectSystem}`);
|
|
379
|
+
if (opts.protectHome === true)
|
|
380
|
+
lines.push("ProtectHome=true");
|
|
381
|
+
else if (opts.protectHome)
|
|
382
|
+
lines.push(`ProtectHome=${opts.protectHome}`);
|
|
383
|
+
if (opts.readOnlyPaths?.length)
|
|
384
|
+
lines.push(`ReadOnlyPaths=${opts.readOnlyPaths.join(" ")}`);
|
|
385
|
+
if (opts.readWritePaths?.length)
|
|
386
|
+
lines.push(`ReadWritePaths=${opts.readWritePaths.join(" ")}`);
|
|
387
|
+
if (opts.restrictRealtime)
|
|
388
|
+
lines.push("RestrictRealtime=true");
|
|
389
|
+
if (opts.memoryDenyWriteExecute)
|
|
390
|
+
lines.push("MemoryDenyWriteExecute=true");
|
|
391
|
+
if (opts.protectKernelTunables)
|
|
392
|
+
lines.push("ProtectKernelTunables=true");
|
|
393
|
+
if (opts.protectKernelModules)
|
|
394
|
+
lines.push("ProtectKernelModules=true");
|
|
395
|
+
if (opts.protectControlGroups)
|
|
396
|
+
lines.push("ProtectControlGroups=true");
|
|
397
|
+
if (opts.restrictAddressFamilies?.length)
|
|
398
|
+
lines.push(`RestrictAddressFamilies=${opts.restrictAddressFamilies.join(" ")}`);
|
|
399
|
+
if (opts.deviceAllow?.length)
|
|
400
|
+
lines.push(`DeviceAllow=${opts.deviceAllow.join(" ")}`);
|
|
401
|
+
if (opts.devicePolicy)
|
|
402
|
+
lines.push(`DevicePolicy=${opts.devicePolicy}`);
|
|
403
|
+
if (opts.systemCallFilter?.length)
|
|
404
|
+
lines.push(`SystemCallFilter=${opts.systemCallFilter.join(" ")}`);
|
|
405
|
+
if (opts.restrictNamespaces === true)
|
|
406
|
+
lines.push("RestrictNamespaces=true");
|
|
407
|
+
else if (Array.isArray(opts.restrictNamespaces) && opts.restrictNamespaces.length > 0)
|
|
408
|
+
lines.push(`RestrictNamespaces=${opts.restrictNamespaces.join(" ")}`);
|
|
409
|
+
if (opts.personality)
|
|
410
|
+
lines.push(`Personality=${opts.personality}`);
|
|
411
|
+
if (opts.lockPersonality)
|
|
412
|
+
lines.push("LockPersonality=true");
|
|
413
|
+
if (opts.removeCapability?.length)
|
|
414
|
+
opts.removeCapability.forEach((cap) => lines.push(`RemoveCapability=${cap}`));
|
|
415
|
+
if (opts.capabilityBoundingSet?.length)
|
|
416
|
+
lines.push(`CapabilityBoundingSet=${opts.capabilityBoundingSet.join(" ")}`);
|
|
417
|
+
if (opts.ambientCapabilities?.length)
|
|
418
|
+
lines.push(`AmbientCapabilities=${opts.ambientCapabilities.join(" ")}`);
|
|
419
|
+
if (opts.privateDevices)
|
|
420
|
+
lines.push("PrivateDevices=true");
|
|
421
|
+
if (opts.protectKernelLogs)
|
|
422
|
+
lines.push("ProtectKernelLogs=true");
|
|
423
|
+
if (opts.extras) {
|
|
424
|
+
lines.push("");
|
|
425
|
+
lines.push(opts.extras);
|
|
426
|
+
}
|
|
427
|
+
return lines.join(`
|
|
428
|
+
`);
|
|
429
|
+
}
|
|
430
|
+
async function createServiceUnit(name, opts) {
|
|
431
|
+
const unitPath = `/etc/systemd/system/${name}.service`;
|
|
432
|
+
const content = generateServiceUnit(name, opts);
|
|
433
|
+
const writeResult = await writeFile2(unitPath, content, {
|
|
434
|
+
context: opts.context,
|
|
435
|
+
mode: "644",
|
|
436
|
+
env: opts.env,
|
|
437
|
+
timeout: opts.timeout
|
|
438
|
+
});
|
|
439
|
+
if (!writeResult.ok) {
|
|
440
|
+
return writeResult;
|
|
441
|
+
}
|
|
442
|
+
await exec3(["systemctl", "daemon-reload"], opts);
|
|
443
|
+
return {
|
|
444
|
+
...writeResult,
|
|
445
|
+
unitPath
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
async function enableService(name, opts) {
|
|
449
|
+
return exec3(["systemctl", "enable", name], opts);
|
|
450
|
+
}
|
|
451
|
+
async function disableService(name, opts) {
|
|
452
|
+
return exec3(["systemctl", "disable", name], opts);
|
|
453
|
+
}
|
|
454
|
+
async function startService(name, opts) {
|
|
455
|
+
return exec3(["systemctl", "start", name], opts);
|
|
456
|
+
}
|
|
457
|
+
async function stopService(name, opts) {
|
|
458
|
+
return exec3(["systemctl", "stop", name], opts);
|
|
459
|
+
}
|
|
460
|
+
async function restartService(name, opts) {
|
|
461
|
+
return exec3(["systemctl", "restart", name], opts);
|
|
462
|
+
}
|
|
463
|
+
async function reloadService(name, opts) {
|
|
464
|
+
return exec3(["systemctl", "reload", name], opts);
|
|
465
|
+
}
|
|
466
|
+
async function getServiceStatus(name, opts) {
|
|
467
|
+
const result = await exec3(["systemctl", "show", name, "--property=LoadState,ActiveState,SubState,MainPID,Description"], {
|
|
468
|
+
...opts,
|
|
469
|
+
quiet: true
|
|
470
|
+
});
|
|
471
|
+
if (!result.ok) {
|
|
472
|
+
return { loaded: false, active: false, subState: "unknown", mainPid: 0, description: "" };
|
|
473
|
+
}
|
|
474
|
+
const parse = (key) => {
|
|
475
|
+
const match = result.stdout.match(new RegExp(`^${key}=(.+)$`, "m"));
|
|
476
|
+
return match?.[1]?.trim() ?? "";
|
|
477
|
+
};
|
|
478
|
+
return {
|
|
479
|
+
loaded: parse("LoadState") === "loaded",
|
|
480
|
+
active: parse("ActiveState") === "active",
|
|
481
|
+
subState: parse("SubState"),
|
|
482
|
+
mainPid: parseInt(parse("MainPID") || "0", 10),
|
|
483
|
+
description: parse("Description")
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
async function enableAndStartService(name, opts) {
|
|
487
|
+
return exec3(["systemctl", "enable", "--now", name], opts);
|
|
488
|
+
}
|
|
489
|
+
async function serviceExists(name, opts) {
|
|
490
|
+
const result = await exec3(["systemctl", "list-unit-files", name + ".service"], {
|
|
491
|
+
...opts,
|
|
492
|
+
quiet: true
|
|
493
|
+
});
|
|
494
|
+
return result.ok && result.stdout.includes(name);
|
|
495
|
+
}
|
|
496
|
+
async function getServiceLogs(name, opts = {}) {
|
|
497
|
+
const args = ["journalctl", "-u", name, "-n", String(opts.lines ?? 100)];
|
|
498
|
+
if (opts.since)
|
|
499
|
+
args.push("--since", opts.since);
|
|
500
|
+
if (!opts.follow)
|
|
501
|
+
args.push("-n", String(opts.lines ?? 100));
|
|
502
|
+
const result = await exec3(args, opts);
|
|
503
|
+
return result.stdout;
|
|
504
|
+
}
|
|
505
|
+
var SECURITY_PRESETS = {
|
|
506
|
+
strict: {
|
|
507
|
+
noNewPrivileges: true,
|
|
508
|
+
privateTmp: true,
|
|
509
|
+
protectSystem: "strict",
|
|
510
|
+
protectHome: true,
|
|
511
|
+
readOnlyPaths: ["/"],
|
|
512
|
+
readWritePaths: [],
|
|
513
|
+
restrictRealtime: true,
|
|
514
|
+
memoryDenyWriteExecute: true,
|
|
515
|
+
protectKernelTunables: true,
|
|
516
|
+
protectKernelModules: true,
|
|
517
|
+
protectControlGroups: true,
|
|
518
|
+
restrictAddressFamilies: ["AF_UNIX", "AF_INET", "AF_INET6"],
|
|
519
|
+
devicePolicy: "closed",
|
|
520
|
+
deviceAllow: [],
|
|
521
|
+
restrictNamespaces: true,
|
|
522
|
+
privateDevices: true,
|
|
523
|
+
protectKernelLogs: true,
|
|
524
|
+
lockPersonality: true,
|
|
525
|
+
capabilityBoundingSet: []
|
|
526
|
+
},
|
|
527
|
+
standard: {
|
|
528
|
+
noNewPrivileges: true,
|
|
529
|
+
privateTmp: true,
|
|
530
|
+
protectSystem: "full",
|
|
531
|
+
protectHome: true,
|
|
532
|
+
restrictRealtime: true,
|
|
533
|
+
memoryDenyWriteExecute: true,
|
|
534
|
+
protectKernelTunables: true,
|
|
535
|
+
protectKernelModules: true,
|
|
536
|
+
protectControlGroups: true,
|
|
537
|
+
restrictAddressFamilies: ["AF_UNIX", "AF_INET", "AF_INET6"],
|
|
538
|
+
devicePolicy: "auto",
|
|
539
|
+
restrictNamespaces: true,
|
|
540
|
+
privateDevices: true,
|
|
541
|
+
capabilityBoundingSet: ["CAP_NET_BIND_SERVICE"]
|
|
542
|
+
},
|
|
543
|
+
network: {
|
|
544
|
+
noNewPrivileges: true,
|
|
545
|
+
privateTmp: true,
|
|
546
|
+
protectSystem: "full",
|
|
547
|
+
protectHome: true,
|
|
548
|
+
restrictRealtime: true,
|
|
549
|
+
protectKernelTunables: true,
|
|
550
|
+
protectKernelModules: true,
|
|
551
|
+
protectControlGroups: true,
|
|
552
|
+
restrictAddressFamilies: ["AF_UNIX", "AF_INET", "AF_INET6"],
|
|
553
|
+
devicePolicy: "auto",
|
|
554
|
+
privateDevices: true,
|
|
555
|
+
capabilityBoundingSet: ["CAP_NET_BIND_SERVICE"],
|
|
556
|
+
ambientCapabilities: ["CAP_NET_BIND_SERVICE"]
|
|
557
|
+
},
|
|
558
|
+
minimal: {
|
|
559
|
+
noNewPrivileges: true,
|
|
560
|
+
privateTmp: true,
|
|
561
|
+
protectSystem: "yes",
|
|
562
|
+
restrictRealtime: true
|
|
563
|
+
},
|
|
564
|
+
deviceAccess: {
|
|
565
|
+
noNewPrivileges: true,
|
|
566
|
+
privateTmp: true,
|
|
567
|
+
protectSystem: "full",
|
|
568
|
+
protectHome: true,
|
|
569
|
+
restrictRealtime: true,
|
|
570
|
+
memoryDenyWriteExecute: true,
|
|
571
|
+
protectKernelTunables: true,
|
|
572
|
+
protectKernelModules: true,
|
|
573
|
+
protectControlGroups: true,
|
|
574
|
+
devicePolicy: "auto",
|
|
575
|
+
restrictNamespaces: true,
|
|
576
|
+
privateDevices: false
|
|
577
|
+
}
|
|
578
|
+
};
|
|
579
|
+
function withSecurityPreset(preset, customOptions) {
|
|
580
|
+
return {
|
|
581
|
+
...SECURITY_PRESETS[preset],
|
|
582
|
+
...customOptions
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
async function safeSystemd(operation, options = {}) {
|
|
586
|
+
const { requireSystemd = true, onNoSystemd, context, env, timeout, quiet } = options;
|
|
587
|
+
const sudoOpts = context ? { context, env, timeout, quiet } : { context: { type: "local" }, env, timeout, quiet };
|
|
588
|
+
const check = await hasSystemd(sudoOpts);
|
|
589
|
+
if (!check.available) {
|
|
590
|
+
if (onNoSystemd) {
|
|
591
|
+
await onNoSystemd(check);
|
|
592
|
+
}
|
|
593
|
+
if (requireSystemd) {
|
|
594
|
+
throw new Error(`Systemd required but not available: ${check.message}`);
|
|
595
|
+
}
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
return operation();
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// device-auth.ts
|
|
602
|
+
async function exec4(args, opts) {
|
|
603
|
+
const { sudo: sudo2 } = await Promise.resolve().then(() => (init_sudo(), exports_sudo));
|
|
604
|
+
return sudo2(args, opts);
|
|
605
|
+
}
|
|
606
|
+
function stripAnsi(text) {
|
|
607
|
+
return text.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, "");
|
|
608
|
+
}
|
|
609
|
+
async function deviceAuth(config, opts = {}) {
|
|
610
|
+
const {
|
|
611
|
+
cli,
|
|
612
|
+
loginCmd,
|
|
613
|
+
statusCmd,
|
|
614
|
+
patterns,
|
|
615
|
+
logFile,
|
|
616
|
+
pidFile,
|
|
617
|
+
statusFlags
|
|
618
|
+
} = config;
|
|
619
|
+
const maxAttempts = opts.maxAttempts ?? 60;
|
|
620
|
+
const intervalMs = opts.intervalMs ?? 2000;
|
|
621
|
+
const loginCmdStr = loginCmd ?? `${cli} login`;
|
|
622
|
+
const startResult = await exec4([`bash`, `-lc`, `nohup ${loginCmdStr} > ${logFile} 2>&1 & echo $!`], opts);
|
|
623
|
+
if (!startResult.ok) {
|
|
624
|
+
return { success: false, error: `Failed to start login: ${startResult.stderr}` };
|
|
625
|
+
}
|
|
626
|
+
const pid = startResult.stdout.trim();
|
|
627
|
+
if (pidFile) {
|
|
628
|
+
await exec4(["bash", `-lc`, `echo ${pid} > ${pidFile}`], opts);
|
|
629
|
+
}
|
|
630
|
+
await sleep(1000);
|
|
631
|
+
const extractResult = await exec4(["cat", logFile], { ...opts, quiet: false });
|
|
632
|
+
const logContent = stripAnsi(extractResult.stdout);
|
|
633
|
+
const urlMatch = patterns.url.exec(logContent);
|
|
634
|
+
const codeMatch = patterns.code.exec(logContent);
|
|
635
|
+
const url = urlMatch?.[1];
|
|
636
|
+
const code = codeMatch?.[1];
|
|
637
|
+
if (!url || !code) {
|
|
638
|
+
return {
|
|
639
|
+
success: false,
|
|
640
|
+
error: `Could not extract auth URL/code from log output. Log content:
|
|
641
|
+
${logContent}`
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
for (let attempt = 1;attempt <= maxAttempts; attempt++) {
|
|
645
|
+
const statusCmdStr = statusCmd ?? `${cli} status`;
|
|
646
|
+
const statusArgs = ["bash", "-lc", statusCmdStr];
|
|
647
|
+
if (statusFlags?.length)
|
|
648
|
+
statusArgs.push(...statusFlags);
|
|
649
|
+
const statusResult = await exec4(statusArgs, { ...opts, quiet: false });
|
|
650
|
+
const statusOutput = stripAnsi(statusResult.stdout);
|
|
651
|
+
if (patterns.success.test(statusOutput)) {
|
|
652
|
+
return {
|
|
653
|
+
success: true,
|
|
654
|
+
url,
|
|
655
|
+
code,
|
|
656
|
+
status: statusOutput
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
opts.onPoll?.(attempt, { success: false, url, code, status: statusOutput });
|
|
660
|
+
await sleep(intervalMs);
|
|
661
|
+
}
|
|
662
|
+
return {
|
|
663
|
+
success: false,
|
|
664
|
+
url,
|
|
665
|
+
code,
|
|
666
|
+
error: `Login did not complete after ${maxAttempts} attempts. Last status: ${await getStatusOutput(config, opts)}`
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
async function getStatusOutput(config, opts) {
|
|
670
|
+
const statusCmdStr = config.statusCmd ?? `${config.cli} status`;
|
|
671
|
+
const result = await exec4(["bash", "-lc", statusCmdStr], { ...opts, quiet: false });
|
|
672
|
+
return stripAnsi(result.stdout);
|
|
673
|
+
}
|
|
674
|
+
async function isAuthed(config, opts) {
|
|
675
|
+
const statusCmdStr = config.statusCmd ?? `${config.cli} status`;
|
|
676
|
+
const result = await exec4(["bash", "-lc", statusCmdStr], { ...opts, quiet: false });
|
|
677
|
+
const output = stripAnsi(result.stdout);
|
|
678
|
+
return config.patterns.success.test(output);
|
|
679
|
+
}
|
|
680
|
+
async function cleanupDeviceAuth(config, opts) {
|
|
681
|
+
if (config.pidFile) {
|
|
682
|
+
const pidResult = await exec4(["cat", config.pidFile], { ...opts, quiet: true });
|
|
683
|
+
if (pidResult.ok) {
|
|
684
|
+
const pid = pidResult.stdout.trim();
|
|
685
|
+
await exec4(["kill", pid], { ...opts, quiet: true });
|
|
686
|
+
}
|
|
687
|
+
await exec4(["rm", "-f", config.pidFile], { ...opts, quiet: true });
|
|
688
|
+
}
|
|
689
|
+
await exec4(["rm", "-f", config.logFile], { ...opts, quiet: true });
|
|
690
|
+
}
|
|
691
|
+
var dopplerConfig = {
|
|
692
|
+
cli: "doppler",
|
|
693
|
+
loginCmd: "doppler login -y",
|
|
694
|
+
statusCmd: "doppler configure get token --scope /",
|
|
695
|
+
patterns: {
|
|
696
|
+
url: /https:\/\/(?:cli|dashboard)\.doppler\.com\/[a-z\-\/]+/i,
|
|
697
|
+
code: /(?:Your authentication code is:|Your auth code is:)\s*([A-Z0-9_]+(?:-[A-Z0-9_]+)*)/i,
|
|
698
|
+
success: /.{10,}/
|
|
699
|
+
},
|
|
700
|
+
logFile: "/tmp/doppler-login.log",
|
|
701
|
+
pidFile: "/tmp/doppler-login.pid"
|
|
702
|
+
};
|
|
703
|
+
var githubConfig = {
|
|
704
|
+
cli: "gh",
|
|
705
|
+
loginCmd: "GH_BROWSER=echo sh -c 'gh auth login -p https -h github.com < /dev/null'",
|
|
706
|
+
statusCmd: "gh auth status",
|
|
707
|
+
patterns: {
|
|
708
|
+
url: /(https:\/\/github\.com\/login\/device)/i,
|
|
709
|
+
code: /one-time code:\s*([A-Z0-9]{4}-[A-Z0-9]{4})/i,
|
|
710
|
+
success: /Logged in (?:as|to)/i
|
|
711
|
+
},
|
|
712
|
+
logFile: "/tmp/gh-login.log",
|
|
713
|
+
pidFile: "/tmp/gh-login.pid"
|
|
714
|
+
};
|
|
715
|
+
var tailscaleConfig = {
|
|
716
|
+
cli: "tailscale",
|
|
717
|
+
loginCmd: "tailscale up --authkey",
|
|
718
|
+
patterns: {
|
|
719
|
+
url: /https:\/\/login\.tailscale\.com\/[a-z0-9\/]+/,
|
|
720
|
+
code: /https:\/\/login\.tailscale\.com\/[a-z0-9\/]+/,
|
|
721
|
+
success: /Tailscale is running/i
|
|
722
|
+
},
|
|
723
|
+
logFile: "/tmp/tailscale-login.log",
|
|
724
|
+
pidFile: "/tmp/tailscale-login.pid"
|
|
725
|
+
};
|
|
726
|
+
function sleep(ms) {
|
|
727
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// bootstrap.ts
|
|
731
|
+
async function exec5(args, opts) {
|
|
732
|
+
const { sudo: sudo2 } = await Promise.resolve().then(() => (init_sudo(), exports_sudo));
|
|
733
|
+
return sudo2(args, opts);
|
|
734
|
+
}
|
|
735
|
+
async function writeFile3(path, content, opts) {
|
|
736
|
+
const { writeFile: wf } = await Promise.resolve().then(() => (init_sudo(), exports_sudo));
|
|
737
|
+
return wf(path, content, opts);
|
|
738
|
+
}
|
|
739
|
+
async function getBootstrapStatus(statusFile, opts) {
|
|
740
|
+
const result = await exec5(["cat", statusFile], { ...opts, quiet: true });
|
|
741
|
+
if (!result.ok) {
|
|
742
|
+
return {
|
|
743
|
+
status: "pending",
|
|
744
|
+
phases: {},
|
|
745
|
+
raw: ""
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
return parseBootstrapStatus(result.stdout);
|
|
749
|
+
}
|
|
750
|
+
function parseBootstrapStatus(content) {
|
|
751
|
+
const phases = {};
|
|
752
|
+
const data = {};
|
|
753
|
+
for (const line of content.trim().split(`
|
|
754
|
+
`)) {
|
|
755
|
+
if (!line || !line.includes("="))
|
|
756
|
+
continue;
|
|
757
|
+
const [key, ...valueParts] = line.split("=");
|
|
758
|
+
const value = valueParts.join("=").trim();
|
|
759
|
+
data[key] = value;
|
|
760
|
+
}
|
|
761
|
+
const status = data.status || "started";
|
|
762
|
+
const startedAt = data.started_at;
|
|
763
|
+
const completedAt = data.completed_at;
|
|
764
|
+
const source = data.source;
|
|
765
|
+
for (const [key, value] of Object.entries(data)) {
|
|
766
|
+
if (!key.startsWith("phase."))
|
|
767
|
+
continue;
|
|
768
|
+
const parts = key.split(".");
|
|
769
|
+
if (parts.length < 3)
|
|
770
|
+
continue;
|
|
771
|
+
const [, phaseName, field] = parts;
|
|
772
|
+
if (!phases[phaseName]) {
|
|
773
|
+
phases[phaseName] = { name: phaseName, status: "pending" };
|
|
774
|
+
}
|
|
775
|
+
switch (field) {
|
|
776
|
+
case "status":
|
|
777
|
+
phases[phaseName].status = value;
|
|
778
|
+
break;
|
|
779
|
+
case "started_at":
|
|
780
|
+
phases[phaseName].startedAt = value;
|
|
781
|
+
break;
|
|
782
|
+
case "completed_at":
|
|
783
|
+
phases[phaseName].completedAt = value;
|
|
784
|
+
break;
|
|
785
|
+
case "error":
|
|
786
|
+
phases[phaseName].error = value;
|
|
787
|
+
break;
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
return {
|
|
791
|
+
status,
|
|
792
|
+
startedAt,
|
|
793
|
+
completedAt,
|
|
794
|
+
source,
|
|
795
|
+
phases,
|
|
796
|
+
raw: content
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
async function initBootstrap(statusFile, source, opts) {
|
|
800
|
+
const now = new Date().toISOString();
|
|
801
|
+
const content = `status=started
|
|
802
|
+
started_at=${now}
|
|
803
|
+
source=${source}
|
|
804
|
+
`;
|
|
805
|
+
return writeFile3(statusFile, content, opts);
|
|
806
|
+
}
|
|
807
|
+
async function startPhase(statusFile, phase, opts) {
|
|
808
|
+
const now = new Date().toISOString();
|
|
809
|
+
const line = `phase.${phase}.status=running
|
|
810
|
+
phase.${phase}.started_at=${now}
|
|
811
|
+
`;
|
|
812
|
+
return writeFile3(statusFile, line, { ...opts, append: true });
|
|
813
|
+
}
|
|
814
|
+
async function completePhase(statusFile, phase, opts) {
|
|
815
|
+
const now = new Date().toISOString();
|
|
816
|
+
const line = `phase.${phase}.status=complete
|
|
817
|
+
phase.${phase}.completed_at=${now}
|
|
818
|
+
`;
|
|
819
|
+
return writeFile3(statusFile, line, { ...opts, append: true });
|
|
820
|
+
}
|
|
821
|
+
async function failPhase(statusFile, phase, error, opts) {
|
|
822
|
+
const now = new Date().toISOString();
|
|
823
|
+
const safeError = error.replace(/\n/g, " ");
|
|
824
|
+
const line = `phase.${phase}.status=failed
|
|
825
|
+
phase.${phase}.completed_at=${now}
|
|
826
|
+
phase.${phase}.error=${safeError}
|
|
827
|
+
`;
|
|
828
|
+
return writeFile3(statusFile, line, { ...opts, append: true });
|
|
829
|
+
}
|
|
830
|
+
async function completeBootstrap(statusFile, opts) {
|
|
831
|
+
const now = new Date().toISOString();
|
|
832
|
+
const line = `status=complete
|
|
833
|
+
completed_at=${now}
|
|
834
|
+
`;
|
|
835
|
+
return writeFile3(statusFile, line, { ...opts, append: true });
|
|
836
|
+
}
|
|
837
|
+
async function failBootstrap(statusFile, error, opts) {
|
|
838
|
+
const now = new Date().toISOString();
|
|
839
|
+
const safeError = error.replace(/\n/g, " ");
|
|
840
|
+
const line = `status=failed
|
|
841
|
+
completed_at=${now}
|
|
842
|
+
error=${safeError}
|
|
843
|
+
`;
|
|
844
|
+
return writeFile3(statusFile, line, { ...opts, append: true });
|
|
845
|
+
}
|
|
846
|
+
async function checkMarker(markerPath, opts) {
|
|
847
|
+
const result = await exec5(["test", "-f", markerPath, "&&", "echo", "exists"], {
|
|
848
|
+
...opts,
|
|
849
|
+
quiet: true
|
|
850
|
+
});
|
|
851
|
+
return result.ok && result.stdout.trim() === "exists";
|
|
852
|
+
}
|
|
853
|
+
async function setMarker(markerPath, opts) {
|
|
854
|
+
return exec5(["touch", markerPath], opts);
|
|
855
|
+
}
|
|
856
|
+
async function removeMarker(markerPath, opts) {
|
|
857
|
+
return exec5(["rm", "-f", markerPath], opts);
|
|
858
|
+
}
|
|
859
|
+
async function waitForBootstrap(statusFile, opts = {}) {
|
|
860
|
+
const maxAttempts = opts.maxAttempts ?? 30;
|
|
861
|
+
const intervalMs = opts.intervalMs ?? 2000;
|
|
862
|
+
for (let attempt = 1;attempt <= maxAttempts; attempt++) {
|
|
863
|
+
const status = await getBootstrapStatus(statusFile, opts);
|
|
864
|
+
if (status.status === "complete") {
|
|
865
|
+
return { completed: true, status };
|
|
866
|
+
}
|
|
867
|
+
if (status.status === "failed") {
|
|
868
|
+
return { completed: false, status };
|
|
869
|
+
}
|
|
870
|
+
opts.onProgress?.(attempt, status);
|
|
871
|
+
await sleep2(intervalMs);
|
|
872
|
+
}
|
|
873
|
+
const finalStatus = await getBootstrapStatus(statusFile, opts);
|
|
874
|
+
return { completed: false, status: finalStatus };
|
|
875
|
+
}
|
|
876
|
+
async function waitForMarker(markerPath, opts = {}) {
|
|
877
|
+
const maxAttempts = opts.maxAttempts ?? 30;
|
|
878
|
+
const intervalMs = opts.intervalMs ?? 2000;
|
|
879
|
+
for (let attempt = 1;attempt <= maxAttempts; attempt++) {
|
|
880
|
+
const exists = await checkMarker(markerPath, opts);
|
|
881
|
+
if (exists) {
|
|
882
|
+
return { exists: true, timedOut: false };
|
|
883
|
+
}
|
|
884
|
+
opts.onProgress?.(attempt, { status: "running" });
|
|
885
|
+
await sleep2(intervalMs);
|
|
886
|
+
}
|
|
887
|
+
return { exists: false, timedOut: true };
|
|
888
|
+
}
|
|
889
|
+
function sleep2(ms) {
|
|
890
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
// index.ts
|
|
894
|
+
init_sudo();
|
|
895
|
+
export {
|
|
896
|
+
writeFile,
|
|
897
|
+
waitForMarker,
|
|
898
|
+
waitForBootstrap,
|
|
899
|
+
tailscaleConfig,
|
|
900
|
+
symlink,
|
|
901
|
+
sudo,
|
|
902
|
+
stripAnsi,
|
|
903
|
+
stopService,
|
|
904
|
+
startService,
|
|
905
|
+
startPhase,
|
|
906
|
+
setMarker,
|
|
907
|
+
serviceExists,
|
|
908
|
+
serviceEnable,
|
|
909
|
+
service,
|
|
910
|
+
safeSystemd,
|
|
911
|
+
restartService,
|
|
912
|
+
removeMarker,
|
|
913
|
+
reloadService,
|
|
914
|
+
pkgInstall,
|
|
915
|
+
parseBootstrapStatus,
|
|
916
|
+
linkBinaries,
|
|
917
|
+
isAuthed,
|
|
918
|
+
installBun,
|
|
919
|
+
initBootstrap,
|
|
920
|
+
hasSystemd,
|
|
921
|
+
githubConfig,
|
|
922
|
+
getServiceStatus,
|
|
923
|
+
getServiceLogs,
|
|
924
|
+
getBunVersion,
|
|
925
|
+
getBootstrapStatus,
|
|
926
|
+
generateServiceUnit,
|
|
927
|
+
failPhase,
|
|
928
|
+
failBootstrap,
|
|
929
|
+
enableService,
|
|
930
|
+
enableAndStartService,
|
|
931
|
+
dopplerConfig,
|
|
932
|
+
disableService,
|
|
933
|
+
deviceAuth,
|
|
934
|
+
createServiceUnit,
|
|
935
|
+
completePhase,
|
|
936
|
+
completeBootstrap,
|
|
937
|
+
commandExists,
|
|
938
|
+
clone,
|
|
939
|
+
clearSystemdCache,
|
|
940
|
+
cleanupDeviceAuth,
|
|
941
|
+
checkMarker,
|
|
942
|
+
addSystemPath
|
|
943
|
+
};
|