@ebowwa/seedinstallation 0.2.4
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 +21 -0
- package/README.md +172 -0
- package/dist/bootstrap.d.ts +114 -0
- package/dist/bootstrap.js +225 -0
- package/dist/clone.d.ts +36 -0
- package/dist/clone.js +74 -0
- package/dist/device-auth.d.ts +85 -0
- package/dist/device-auth.js +176 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +10 -0
- package/dist/runtime.d.ts +60 -0
- package/dist/runtime.js +122 -0
- package/dist/sudo.d.ts +57 -0
- package/dist/sudo.js +235 -0
- package/dist/systemd.d.ts +114 -0
- package/dist/systemd.js +186 -0
- package/package.json +75 -0
package/dist/sudo.js
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Composable sudo command runner for local and remote (SSH) contexts.
|
|
3
|
+
* Supports arbitrary commands, package installs, file writes, and service management.
|
|
4
|
+
*/
|
|
5
|
+
import { spawn } from "child_process";
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Core: run a command with sudo
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
/** Run an arbitrary command with sudo. */
|
|
10
|
+
export async function sudo(cmd, opts) {
|
|
11
|
+
const parts = Array.isArray(cmd) ? cmd : cmd.split(/\s+/);
|
|
12
|
+
const envPrefix = opts.env
|
|
13
|
+
? Object.entries(opts.env).map(([k, v]) => `${k}=${shellEscape(v)}`)
|
|
14
|
+
: [];
|
|
15
|
+
const sudoCmd = ["sudo", ...envPrefix, ...parts];
|
|
16
|
+
return exec(sudoCmd, opts);
|
|
17
|
+
}
|
|
18
|
+
const installCmd = {
|
|
19
|
+
apt: ["apt-get", "install", "-y"],
|
|
20
|
+
dnf: ["dnf", "install", "-y"],
|
|
21
|
+
apk: ["apk", "add", "--no-cache"],
|
|
22
|
+
};
|
|
23
|
+
const updateCmd = {
|
|
24
|
+
apt: ["apt-get", "update", "-qq"],
|
|
25
|
+
dnf: ["dnf", "check-update"],
|
|
26
|
+
apk: ["apk", "update"],
|
|
27
|
+
};
|
|
28
|
+
/** Install one or more system packages. */
|
|
29
|
+
export 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
|
+
// Update package index first
|
|
36
|
+
await sudo(updateCmd[pm], { ...opts, env: envOverride, quiet: true });
|
|
37
|
+
return sudo([...installCmd[pm], ...packages], {
|
|
38
|
+
...opts,
|
|
39
|
+
env: envOverride,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
/** Write content to a privileged file path using tee. */
|
|
43
|
+
export async function writeFile(path, content, opts) {
|
|
44
|
+
const op = opts.append ? "-a" : "";
|
|
45
|
+
const teeCmd = `tee ${op} ${shellEscape(path)}`.trim();
|
|
46
|
+
// Pipe content through sudo tee
|
|
47
|
+
const result = await execPipe(content, ["sudo", teeCmd], opts);
|
|
48
|
+
if (result.ok && opts.mode) {
|
|
49
|
+
await sudo(["chmod", opts.mode, path], opts);
|
|
50
|
+
}
|
|
51
|
+
if (result.ok && opts.owner) {
|
|
52
|
+
await sudo(["chown", opts.owner, path], opts);
|
|
53
|
+
}
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
/** Manage a systemd service. */
|
|
57
|
+
export async function service(name, action, opts) {
|
|
58
|
+
return sudo(["systemctl", action, name], opts);
|
|
59
|
+
}
|
|
60
|
+
/** Enable and start a service in one call. */
|
|
61
|
+
export async function serviceEnable(name, opts) {
|
|
62
|
+
return sudo(["systemctl", "enable", "--now", name], opts);
|
|
63
|
+
}
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
// Internals
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
function buildSshPrefix(ctx) {
|
|
68
|
+
const parts = ["ssh", "-F", "/dev/null", "-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null"];
|
|
69
|
+
if (ctx.keyPath)
|
|
70
|
+
parts.push("-i", ctx.keyPath);
|
|
71
|
+
else if (ctx.key)
|
|
72
|
+
parts.push("-i", ctx.key);
|
|
73
|
+
if (ctx.port)
|
|
74
|
+
parts.push("-p", String(ctx.port));
|
|
75
|
+
parts.push(`${ctx.user ?? "root"}@${ctx.host}`);
|
|
76
|
+
return parts;
|
|
77
|
+
}
|
|
78
|
+
export async function exec(args, opts) {
|
|
79
|
+
const finalArgs = opts.context.type === "ssh"
|
|
80
|
+
? [...buildSshPrefix(opts.context), args.map(shellEscape).join(" ")]
|
|
81
|
+
: args;
|
|
82
|
+
return new Promise((resolve, reject) => {
|
|
83
|
+
const timeout = opts.timeout ?? 30_000;
|
|
84
|
+
let timer;
|
|
85
|
+
const spawnArgs = finalArgs[0].includes(" ")
|
|
86
|
+
? ["sh", "-c", finalArgs.join(" ")]
|
|
87
|
+
: finalArgs;
|
|
88
|
+
const proc = spawn(spawnArgs[0], spawnArgs.slice(1), {
|
|
89
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
90
|
+
});
|
|
91
|
+
let stdout = "";
|
|
92
|
+
let stderr = "";
|
|
93
|
+
proc.stdout?.on("data", (data) => {
|
|
94
|
+
stdout += data.toString();
|
|
95
|
+
});
|
|
96
|
+
proc.stderr?.on("data", (data) => {
|
|
97
|
+
stderr += data.toString();
|
|
98
|
+
});
|
|
99
|
+
if (timeout) {
|
|
100
|
+
timer = setTimeout(() => {
|
|
101
|
+
proc.kill();
|
|
102
|
+
resolve({
|
|
103
|
+
stdout: opts.quiet ? "" : stdout,
|
|
104
|
+
stderr: stderr + "\nCommand timed out",
|
|
105
|
+
exitCode: 124,
|
|
106
|
+
ok: false,
|
|
107
|
+
});
|
|
108
|
+
}, timeout);
|
|
109
|
+
}
|
|
110
|
+
proc.on("close", (code) => {
|
|
111
|
+
if (timer)
|
|
112
|
+
clearTimeout(timer);
|
|
113
|
+
resolve({
|
|
114
|
+
stdout: opts.quiet ? "" : stdout,
|
|
115
|
+
stderr,
|
|
116
|
+
exitCode: code ?? 0,
|
|
117
|
+
ok: code === 0,
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
proc.on("error", (err) => {
|
|
121
|
+
if (timer)
|
|
122
|
+
clearTimeout(timer);
|
|
123
|
+
reject(new Error(`Command failed: ${err.message}`));
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
async function execPipe(input, args, opts) {
|
|
128
|
+
if (opts.context.type === "ssh") {
|
|
129
|
+
// Over SSH: echo content | ssh user@host 'sudo tee /path'
|
|
130
|
+
const sshPrefix = buildSshPrefix(opts.context);
|
|
131
|
+
const remoteCmd = args.join(" ");
|
|
132
|
+
const fullArgs = [...sshPrefix, remoteCmd];
|
|
133
|
+
return new Promise((resolve, reject) => {
|
|
134
|
+
const timeout = opts.timeout ?? 30_000;
|
|
135
|
+
let timer;
|
|
136
|
+
const spawnArgs = fullArgs[0].includes(" ")
|
|
137
|
+
? ["sh", "-c", fullArgs.join(" ")]
|
|
138
|
+
: fullArgs;
|
|
139
|
+
const proc = spawn(spawnArgs[0], spawnArgs.slice(1), {
|
|
140
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
141
|
+
});
|
|
142
|
+
// Write input to stdin
|
|
143
|
+
if (proc.stdin) {
|
|
144
|
+
proc.stdin.write(input);
|
|
145
|
+
proc.stdin.end();
|
|
146
|
+
}
|
|
147
|
+
let stdout = "";
|
|
148
|
+
let stderr = "";
|
|
149
|
+
proc.stdout?.on("data", (data) => {
|
|
150
|
+
stdout += data.toString();
|
|
151
|
+
});
|
|
152
|
+
proc.stderr?.on("data", (data) => {
|
|
153
|
+
stderr += data.toString();
|
|
154
|
+
});
|
|
155
|
+
if (timeout) {
|
|
156
|
+
timer = setTimeout(() => {
|
|
157
|
+
proc.kill();
|
|
158
|
+
resolve({
|
|
159
|
+
stdout: opts.quiet ? "" : stdout,
|
|
160
|
+
stderr: stderr + "\nCommand timed out",
|
|
161
|
+
exitCode: 124,
|
|
162
|
+
ok: false,
|
|
163
|
+
});
|
|
164
|
+
}, timeout);
|
|
165
|
+
}
|
|
166
|
+
proc.on("close", (code) => {
|
|
167
|
+
if (timer)
|
|
168
|
+
clearTimeout(timer);
|
|
169
|
+
resolve({
|
|
170
|
+
stdout: opts.quiet ? "" : stdout,
|
|
171
|
+
stderr,
|
|
172
|
+
exitCode: code ?? 0,
|
|
173
|
+
ok: code === 0,
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
proc.on("error", (err) => {
|
|
177
|
+
if (timer)
|
|
178
|
+
clearTimeout(timer);
|
|
179
|
+
reject(new Error(`Command failed: ${err.message}`));
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
// Local: pipe through shell
|
|
184
|
+
return new Promise((resolve, reject) => {
|
|
185
|
+
const timeout = opts.timeout ?? 30_000;
|
|
186
|
+
let timer;
|
|
187
|
+
const proc = spawn("sh", ["-c", args.join(" ")], {
|
|
188
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
189
|
+
});
|
|
190
|
+
// Write input to stdin
|
|
191
|
+
if (proc.stdin) {
|
|
192
|
+
proc.stdin.write(input);
|
|
193
|
+
proc.stdin.end();
|
|
194
|
+
}
|
|
195
|
+
let stdout = "";
|
|
196
|
+
let stderr = "";
|
|
197
|
+
proc.stdout?.on("data", (data) => {
|
|
198
|
+
stdout += data.toString();
|
|
199
|
+
});
|
|
200
|
+
proc.stderr?.on("data", (data) => {
|
|
201
|
+
stderr += data.toString();
|
|
202
|
+
});
|
|
203
|
+
if (timeout) {
|
|
204
|
+
timer = setTimeout(() => {
|
|
205
|
+
proc.kill();
|
|
206
|
+
resolve({
|
|
207
|
+
stdout: opts.quiet ? "" : stdout,
|
|
208
|
+
stderr: stderr + "\nCommand timed out",
|
|
209
|
+
exitCode: 124,
|
|
210
|
+
ok: false,
|
|
211
|
+
});
|
|
212
|
+
}, timeout);
|
|
213
|
+
}
|
|
214
|
+
proc.on("close", (code) => {
|
|
215
|
+
if (timer)
|
|
216
|
+
clearTimeout(timer);
|
|
217
|
+
resolve({
|
|
218
|
+
stdout: opts.quiet ? "" : stdout,
|
|
219
|
+
stderr,
|
|
220
|
+
exitCode: code ?? 0,
|
|
221
|
+
ok: code === 0,
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
proc.on("error", (err) => {
|
|
225
|
+
if (timer)
|
|
226
|
+
clearTimeout(timer);
|
|
227
|
+
reject(new Error(`Command failed: ${err.message}`));
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
function shellEscape(s) {
|
|
232
|
+
if (/^[a-zA-Z0-9._\-\/=:@]+$/.test(s))
|
|
233
|
+
return s;
|
|
234
|
+
return `'${s.replace(/'/g, "'\\''")}'`;
|
|
235
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Systemd service unit creation and management.
|
|
3
|
+
* Works with both local and SSH contexts via ExecContext from sudo.ts.
|
|
4
|
+
*/
|
|
5
|
+
import type { SudoOptions, ExecResult } from "./sudo.js";
|
|
6
|
+
export interface ServiceUnitOptions {
|
|
7
|
+
/** Service description */
|
|
8
|
+
description: string;
|
|
9
|
+
/** Services to start before this one */
|
|
10
|
+
after?: string[];
|
|
11
|
+
/** Services that this one wants */
|
|
12
|
+
wants?: string[];
|
|
13
|
+
/** Service type (default: "simple") */
|
|
14
|
+
type?: "simple" | "oneshot" | "exec" | "forking" | "notify" | "idle";
|
|
15
|
+
/** User to run as (default: "root") */
|
|
16
|
+
user?: string;
|
|
17
|
+
/** Group to run as */
|
|
18
|
+
group?: string;
|
|
19
|
+
/** Working directory */
|
|
20
|
+
workingDirectory: string;
|
|
21
|
+
/** Command to execute */
|
|
22
|
+
execStart: string;
|
|
23
|
+
/** Command to execute before start (oneshot type) */
|
|
24
|
+
execStartPre?: string[];
|
|
25
|
+
/** Command to execute after stop */
|
|
26
|
+
execStopPost?: string[];
|
|
27
|
+
/** Restart policy (default: "always") */
|
|
28
|
+
restart?: "no" | "always" | "on-success" | "on-failure" | "on-abnormal" | "on-abort" | "on-watchdog";
|
|
29
|
+
/** Seconds to wait before restart */
|
|
30
|
+
restartSec?: number;
|
|
31
|
+
/** Environment variables */
|
|
32
|
+
environment?: Record<string, string>;
|
|
33
|
+
/** Environment file to load */
|
|
34
|
+
environmentFile?: string;
|
|
35
|
+
/** Standard output mode (default: "journal") */
|
|
36
|
+
standardOutput?: "journal" | "syslog" | "null" | "append:path";
|
|
37
|
+
/** Standard error mode */
|
|
38
|
+
standardError?: "journal" | "syslog" | "null" | "inherit";
|
|
39
|
+
/** Nice level (-20 to 19) */
|
|
40
|
+
nice?: number;
|
|
41
|
+
/** Additional unit file sections (e.g. [Install], [Unit] extras) */
|
|
42
|
+
extras?: string;
|
|
43
|
+
}
|
|
44
|
+
export interface ServiceResult extends ExecResult {
|
|
45
|
+
/** Path to the unit file */
|
|
46
|
+
unitPath?: string;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Generate a systemd unit file content from options.
|
|
50
|
+
*/
|
|
51
|
+
export declare function generateServiceUnit(name: string, opts: ServiceUnitOptions): string;
|
|
52
|
+
/**
|
|
53
|
+
* Create and install a systemd service unit file.
|
|
54
|
+
*/
|
|
55
|
+
export declare function createServiceUnit(name: string, opts: ServiceUnitOptions & SudoOptions): Promise<ServiceResult>;
|
|
56
|
+
/**
|
|
57
|
+
* Enable a service to start on boot.
|
|
58
|
+
*/
|
|
59
|
+
export declare function enableService(name: string, opts: SudoOptions): Promise<ExecResult>;
|
|
60
|
+
/**
|
|
61
|
+
* Disable a service from starting on boot.
|
|
62
|
+
*/
|
|
63
|
+
export declare function disableService(name: string, opts: SudoOptions): Promise<ExecResult>;
|
|
64
|
+
/**
|
|
65
|
+
* Start a service.
|
|
66
|
+
*/
|
|
67
|
+
export declare function startService(name: string, opts: SudoOptions): Promise<ExecResult>;
|
|
68
|
+
/**
|
|
69
|
+
* Stop a service.
|
|
70
|
+
*/
|
|
71
|
+
export declare function stopService(name: string, opts: SudoOptions): Promise<ExecResult>;
|
|
72
|
+
/**
|
|
73
|
+
* Restart a service.
|
|
74
|
+
*/
|
|
75
|
+
export declare function restartService(name: string, opts: SudoOptions): Promise<ExecResult>;
|
|
76
|
+
/**
|
|
77
|
+
* Reload a service (sends SIGHUP).
|
|
78
|
+
*/
|
|
79
|
+
export declare function reloadService(name: string, opts: SudoOptions): Promise<ExecResult>;
|
|
80
|
+
/**
|
|
81
|
+
* Get service status.
|
|
82
|
+
*/
|
|
83
|
+
export declare function getServiceStatus(name: string, opts: SudoOptions): Promise<ServiceStatus>;
|
|
84
|
+
export interface ServiceStatus {
|
|
85
|
+
/** Whether the unit file is loaded */
|
|
86
|
+
loaded: boolean;
|
|
87
|
+
/** Whether the service is active (running) */
|
|
88
|
+
active: boolean;
|
|
89
|
+
/** Sub-state (running, exited, failed, etc.) */
|
|
90
|
+
subState: string;
|
|
91
|
+
/** Main process PID */
|
|
92
|
+
mainPid: number;
|
|
93
|
+
/** Service description */
|
|
94
|
+
description: string;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Enable and start a service in one call.
|
|
98
|
+
*/
|
|
99
|
+
export declare function enableAndStartService(name: string, opts: SudoOptions): Promise<ExecResult>;
|
|
100
|
+
/**
|
|
101
|
+
* Check if a service exists.
|
|
102
|
+
*/
|
|
103
|
+
export declare function serviceExists(name: string, opts: SudoOptions): Promise<boolean>;
|
|
104
|
+
/**
|
|
105
|
+
* Get service logs via journalctl.
|
|
106
|
+
*/
|
|
107
|
+
export declare function getServiceLogs(name: string, opts: SudoOptions & {
|
|
108
|
+
/** Number of lines to show (default: 100) */
|
|
109
|
+
lines?: number;
|
|
110
|
+
/** Follow logs */
|
|
111
|
+
follow?: boolean;
|
|
112
|
+
/** Show logs since timestamp */
|
|
113
|
+
since?: string;
|
|
114
|
+
}): Promise<string>;
|
package/dist/systemd.js
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Systemd service unit creation and management.
|
|
3
|
+
* Works with both local and SSH contexts via ExecContext from sudo.ts.
|
|
4
|
+
*/
|
|
5
|
+
// Re-export helpers
|
|
6
|
+
async function exec(args, opts) {
|
|
7
|
+
const { sudo } = await import("./sudo.js");
|
|
8
|
+
return sudo(args, opts);
|
|
9
|
+
}
|
|
10
|
+
async function writeFile(path, content, opts) {
|
|
11
|
+
const { writeFile: wf } = await import("./sudo.js");
|
|
12
|
+
return wf(path, content, opts);
|
|
13
|
+
}
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Service creation
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
/**
|
|
18
|
+
* Generate a systemd unit file content from options.
|
|
19
|
+
*/
|
|
20
|
+
export function generateServiceUnit(name, opts) {
|
|
21
|
+
const lines = [];
|
|
22
|
+
// [Unit] section
|
|
23
|
+
lines.push("[Unit]");
|
|
24
|
+
lines.push(`Description=${opts.description}`);
|
|
25
|
+
if (opts.after?.length) {
|
|
26
|
+
lines.push(`After=${opts.after.join(" ")}`);
|
|
27
|
+
}
|
|
28
|
+
if (opts.wants?.length) {
|
|
29
|
+
lines.push(`Wants=${opts.wants.join(" ")}`);
|
|
30
|
+
}
|
|
31
|
+
lines.push("");
|
|
32
|
+
// [Service] section
|
|
33
|
+
lines.push("[Service]");
|
|
34
|
+
if (opts.type)
|
|
35
|
+
lines.push(`Type=${opts.type}`);
|
|
36
|
+
lines.push(`User=${opts.user ?? "root"}`);
|
|
37
|
+
if (opts.group)
|
|
38
|
+
lines.push(`Group=${opts.group}`);
|
|
39
|
+
lines.push(`WorkingDirectory=${opts.workingDirectory}`);
|
|
40
|
+
lines.push(`ExecStart=${opts.execStart}`);
|
|
41
|
+
if (opts.execStartPre?.length) {
|
|
42
|
+
opts.execStartPre.forEach((cmd, i) => {
|
|
43
|
+
lines.push(`ExecStartPre${i > 0 ? `=${i}` : ""}=${cmd}`);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
if (opts.execStopPost?.length) {
|
|
47
|
+
opts.execStopPost.forEach((cmd, i) => {
|
|
48
|
+
lines.push(`ExecStopPost${i > 0 ? `=${i}` : ""}=${cmd}`);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
if (opts.restart)
|
|
52
|
+
lines.push(`Restart=${opts.restart}`);
|
|
53
|
+
if (opts.restartSec)
|
|
54
|
+
lines.push(`RestartSec=${opts.restartSec}`);
|
|
55
|
+
if (opts.environment) {
|
|
56
|
+
Object.entries(opts.environment).forEach(([k, v]) => {
|
|
57
|
+
lines.push(`Environment="${k}=${v}"`);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
if (opts.environmentFile) {
|
|
61
|
+
lines.push(`EnvironmentFile=${opts.environmentFile}`);
|
|
62
|
+
}
|
|
63
|
+
if (opts.standardOutput)
|
|
64
|
+
lines.push(`StandardOutput=${opts.standardOutput}`);
|
|
65
|
+
if (opts.standardError)
|
|
66
|
+
lines.push(`StandardError=${opts.standardError}`);
|
|
67
|
+
if (opts.nice !== undefined)
|
|
68
|
+
lines.push(`Nice=${opts.nice}`);
|
|
69
|
+
// Extras
|
|
70
|
+
if (opts.extras) {
|
|
71
|
+
lines.push("");
|
|
72
|
+
lines.push(opts.extras);
|
|
73
|
+
}
|
|
74
|
+
return lines.join("\n");
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Create and install a systemd service unit file.
|
|
78
|
+
*/
|
|
79
|
+
export async function createServiceUnit(name, opts) {
|
|
80
|
+
const unitPath = `/etc/systemd/system/${name}.service`;
|
|
81
|
+
const content = generateServiceUnit(name, opts);
|
|
82
|
+
// Write unit file
|
|
83
|
+
const writeResult = await writeFile(unitPath, content, {
|
|
84
|
+
context: opts.context,
|
|
85
|
+
mode: "644",
|
|
86
|
+
});
|
|
87
|
+
if (!writeResult.ok) {
|
|
88
|
+
return writeResult;
|
|
89
|
+
}
|
|
90
|
+
// Reload systemd
|
|
91
|
+
await exec(["systemctl", "daemon-reload"], opts);
|
|
92
|
+
return {
|
|
93
|
+
...writeResult,
|
|
94
|
+
unitPath,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
// Service control
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
/**
|
|
101
|
+
* Enable a service to start on boot.
|
|
102
|
+
*/
|
|
103
|
+
export async function enableService(name, opts) {
|
|
104
|
+
return exec(["systemctl", "enable", name], opts);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Disable a service from starting on boot.
|
|
108
|
+
*/
|
|
109
|
+
export async function disableService(name, opts) {
|
|
110
|
+
return exec(["systemctl", "disable", name], opts);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Start a service.
|
|
114
|
+
*/
|
|
115
|
+
export async function startService(name, opts) {
|
|
116
|
+
return exec(["systemctl", "start", name], opts);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Stop a service.
|
|
120
|
+
*/
|
|
121
|
+
export async function stopService(name, opts) {
|
|
122
|
+
return exec(["systemctl", "stop", name], opts);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Restart a service.
|
|
126
|
+
*/
|
|
127
|
+
export async function restartService(name, opts) {
|
|
128
|
+
return exec(["systemctl", "restart", name], opts);
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Reload a service (sends SIGHUP).
|
|
132
|
+
*/
|
|
133
|
+
export async function reloadService(name, opts) {
|
|
134
|
+
return exec(["systemctl", "reload", name], opts);
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Get service status.
|
|
138
|
+
*/
|
|
139
|
+
export async function getServiceStatus(name, opts) {
|
|
140
|
+
const result = await exec(["systemctl", "show", name, "--property=LoadState,ActiveState,SubState,MainPID,Description"], {
|
|
141
|
+
...opts,
|
|
142
|
+
quiet: true,
|
|
143
|
+
});
|
|
144
|
+
if (!result.ok) {
|
|
145
|
+
return { loaded: false, active: false, subState: "unknown", mainPid: 0, description: "" };
|
|
146
|
+
}
|
|
147
|
+
const parse = (key) => {
|
|
148
|
+
const match = result.stdout.match(new RegExp(`^${key}=(.+)$`, "m"));
|
|
149
|
+
return match?.[1]?.trim() ?? "";
|
|
150
|
+
};
|
|
151
|
+
return {
|
|
152
|
+
loaded: parse("LoadState") === "loaded",
|
|
153
|
+
active: parse("ActiveState") === "active",
|
|
154
|
+
subState: parse("SubState"),
|
|
155
|
+
mainPid: parseInt(parse("MainPID") || "0", 10),
|
|
156
|
+
description: parse("Description"),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Enable and start a service in one call.
|
|
161
|
+
*/
|
|
162
|
+
export async function enableAndStartService(name, opts) {
|
|
163
|
+
return exec(["systemctl", "enable", "--now", name], opts);
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Check if a service exists.
|
|
167
|
+
*/
|
|
168
|
+
export async function serviceExists(name, opts) {
|
|
169
|
+
const result = await exec(["systemctl", "list-unit-files", name + ".service"], {
|
|
170
|
+
...opts,
|
|
171
|
+
quiet: true,
|
|
172
|
+
});
|
|
173
|
+
return result.ok && result.stdout.includes(name);
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Get service logs via journalctl.
|
|
177
|
+
*/
|
|
178
|
+
export async function getServiceLogs(name, opts) {
|
|
179
|
+
const args = ["journalctl", "-u", name, "-n", String(opts.lines ?? 100)];
|
|
180
|
+
if (opts.since)
|
|
181
|
+
args.push("--since", opts.since);
|
|
182
|
+
if (!opts.follow)
|
|
183
|
+
args.push("-n", String(opts.lines ?? 100));
|
|
184
|
+
const result = await exec(args, opts);
|
|
185
|
+
return result.stdout;
|
|
186
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ebowwa/seedinstallation",
|
|
3
|
+
"version": "0.2.4",
|
|
4
|
+
"description": "Composable server installation utilities for edge deployment automation",
|
|
5
|
+
"author": "Ebowwa Labs <labs@ebowwa.com>",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"homepage": "https://github.com/cheapspaces/seedInstallation#readme",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/cheapspaces/seedInstallation.git"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/cheapspaces/seedInstallation/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"server",
|
|
18
|
+
"installation",
|
|
19
|
+
"provisioning",
|
|
20
|
+
"systemd",
|
|
21
|
+
"ssh",
|
|
22
|
+
"automation",
|
|
23
|
+
"devops"
|
|
24
|
+
],
|
|
25
|
+
"files": [
|
|
26
|
+
"dist",
|
|
27
|
+
"README.md",
|
|
28
|
+
"LICENSE"
|
|
29
|
+
],
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "tsc",
|
|
32
|
+
"dev": "tsx src/index.ts",
|
|
33
|
+
"prepublishOnly": "npm run build",
|
|
34
|
+
"typecheck": "tsc --noEmit"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/node": "^22.10.2",
|
|
39
|
+
"typescript": "^5.7.2",
|
|
40
|
+
"tsx": "^4.19.2"
|
|
41
|
+
},
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=18.0.0"
|
|
44
|
+
},
|
|
45
|
+
"exports": {
|
|
46
|
+
".": {
|
|
47
|
+
"types": "./dist/index.d.ts",
|
|
48
|
+
"import": "./dist/index.js"
|
|
49
|
+
},
|
|
50
|
+
"./clone": {
|
|
51
|
+
"types": "./dist/clone.d.ts",
|
|
52
|
+
"import": "./dist/clone.js"
|
|
53
|
+
},
|
|
54
|
+
"./sudo": {
|
|
55
|
+
"types": "./dist/sudo.d.ts",
|
|
56
|
+
"import": "./dist/sudo.js"
|
|
57
|
+
},
|
|
58
|
+
"./runtime": {
|
|
59
|
+
"types": "./dist/runtime.d.ts",
|
|
60
|
+
"import": "./dist/runtime.js"
|
|
61
|
+
},
|
|
62
|
+
"./systemd": {
|
|
63
|
+
"types": "./dist/systemd.d.ts",
|
|
64
|
+
"import": "./dist/systemd.js"
|
|
65
|
+
},
|
|
66
|
+
"./device-auth": {
|
|
67
|
+
"types": "./dist/device-auth.d.ts",
|
|
68
|
+
"import": "./dist/device-auth.js"
|
|
69
|
+
},
|
|
70
|
+
"./bootstrap": {
|
|
71
|
+
"types": "./dist/bootstrap.d.ts",
|
|
72
|
+
"import": "./dist/bootstrap.js"
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|