@ebowwa/seedinstallation 0.3.0 → 0.4.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/dist/bootstrap.js CHANGED
@@ -1,225 +1,304 @@
1
- /**
2
- * Bootstrap/provisioning status tracking for edge servers.
3
- * Manages phase markers, status files, and polling for completion.
4
- * Works with both local and SSH contexts via ExecContext from sudo.ts.
5
- */
6
- // Re-export helpers
7
- async function exec(args, opts) {
8
- const { sudo } = await import("./sudo.js");
9
- return sudo(args, opts);
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
+ });
10
40
  }
11
41
  async function writeFile(path, content, opts) {
12
- const { writeFile: wf } = await import("./sudo.js");
13
- return wf(path, content, opts);
14
- }
15
- // ---------------------------------------------------------------------------
16
- // Status file parsing
17
- // ---------------------------------------------------------------------------
18
- /**
19
- * Read and parse a bootstrap status file.
20
- *
21
- * File format is key=value lines:
22
- * ```
23
- * status=started
24
- * started_at=2024-01-01T00:00:00+00:00
25
- * source=cloud-init
26
- * phase.bun.status=complete
27
- * phase.bun.completed_at=2024-01-01T00:01:00+00:00
28
- * phase.seed.status=running
29
- * phase.seed.started_at=2024-01-01T00:01:30+00:00
30
- * ```
31
- */
32
- export async function getBootstrapStatus(statusFile, opts) {
33
- const result = await exec(["cat", statusFile], { ...opts, quiet: true });
34
- if (!result.ok) {
35
- // File doesn't exist or isn't readable
36
- return {
37
- status: "started",
38
- phases: {},
39
- raw: "",
40
- };
41
- }
42
- return parseBootstrapStatus(result.stdout);
43
- }
44
- /**
45
- * Parse bootstrap status from file content.
46
- */
47
- export function parseBootstrapStatus(content) {
48
- const phases = {};
49
- const data = {};
50
- // Parse key=value lines
51
- for (const line of content.trim().split("\n")) {
52
- if (!line || !line.includes("="))
53
- continue;
54
- const [key, ...valueParts] = line.split("=");
55
- const value = valueParts.join("=").trim();
56
- data[key] = value;
57
- }
58
- // Extract overall status
59
- const status = (data.status || "started");
60
- const startedAt = data.started_at;
61
- const completedAt = data.completed_at;
62
- const source = data.source;
63
- // Extract phases
64
- for (const [key, value] of Object.entries(data)) {
65
- if (!key.startsWith("phase."))
66
- continue;
67
- // phase.{name}.{field}
68
- const parts = key.split(".");
69
- if (parts.length < 3)
70
- continue;
71
- const [, phaseName, field] = parts;
72
- if (!phases[phaseName]) {
73
- phases[phaseName] = { name: phaseName, status: "pending" };
74
- }
75
- switch (field) {
76
- case "status":
77
- phases[phaseName].status = value;
78
- break;
79
- case "started_at":
80
- phases[phaseName].startedAt = value;
81
- break;
82
- case "completed_at":
83
- phases[phaseName].completedAt = value;
84
- break;
85
- case "error":
86
- phases[phaseName].error = value;
87
- break;
88
- }
89
- }
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 finalArgs = opts.context.type === "ssh" ? [...buildSshPrefix(opts.context), args.map(shellEscape).join(" ")] : args;
72
+ const proc = Bun.spawn(finalArgs, {
73
+ stdout: "pipe",
74
+ stderr: "pipe",
75
+ timeout: opts.timeout ?? 30000
76
+ });
77
+ const exitCode = await proc.exited;
78
+ const stdout = opts.quiet ? "" : await new Response(proc.stdout).text();
79
+ const stderr = await new Response(proc.stderr).text();
80
+ return { stdout, stderr, exitCode, ok: exitCode === 0 };
81
+ }
82
+ async function execPipe(input, args, opts) {
83
+ if (opts.context.type === "ssh") {
84
+ const sshPrefix = buildSshPrefix(opts.context);
85
+ const remoteCmd = args.join(" ");
86
+ const fullArgs = [...sshPrefix, remoteCmd];
87
+ const proc2 = Bun.spawn(fullArgs, {
88
+ stdin: new TextEncoder().encode(input),
89
+ stdout: "pipe",
90
+ stderr: "pipe",
91
+ timeout: opts.timeout ?? 30000
92
+ });
93
+ const exitCode2 = await proc2.exited;
94
+ const stdout2 = opts.quiet ? "" : await new Response(proc2.stdout).text();
95
+ const stderr2 = await new Response(proc2.stderr).text();
96
+ return { stdout: stdout2, stderr: stderr2, exitCode: exitCode2, ok: exitCode2 === 0 };
97
+ }
98
+ const proc = Bun.spawn(["sh", "-c", args.join(" ")], {
99
+ stdin: new TextEncoder().encode(input),
100
+ stdout: "pipe",
101
+ stderr: "pipe",
102
+ timeout: opts.timeout ?? 30000
103
+ });
104
+ const exitCode = await proc.exited;
105
+ const stdout = opts.quiet ? "" : await new Response(proc.stdout).text();
106
+ const stderr = await new Response(proc.stderr).text();
107
+ return { stdout, stderr, exitCode, ok: exitCode === 0 };
108
+ }
109
+ function shellEscape(s) {
110
+ if (/^[a-zA-Z0-9._\-\/=:@]+$/.test(s))
111
+ return s;
112
+ return `'${s.replace(/'/g, "'\\''")}'`;
113
+ }
114
+ var installCmd, updateCmd;
115
+ var init_sudo = __esm(() => {
116
+ installCmd = {
117
+ apt: ["apt-get", "install", "-y"],
118
+ dnf: ["dnf", "install", "-y"],
119
+ apk: ["apk", "add", "--no-cache"]
120
+ };
121
+ updateCmd = {
122
+ apt: ["apt-get", "update", "-qq"],
123
+ dnf: ["dnf", "check-update"],
124
+ apk: ["apk", "update"]
125
+ };
126
+ });
127
+
128
+ // bootstrap.ts
129
+ async function exec2(args, opts) {
130
+ const { sudo: sudo2 } = await Promise.resolve().then(() => (init_sudo(), exports_sudo));
131
+ return sudo2(args, opts);
132
+ }
133
+ async function writeFile2(path, content, opts) {
134
+ const { writeFile: wf } = await Promise.resolve().then(() => (init_sudo(), exports_sudo));
135
+ return wf(path, content, opts);
136
+ }
137
+ async function getBootstrapStatus(statusFile, opts) {
138
+ const result = await exec2(["cat", statusFile], { ...opts, quiet: true });
139
+ if (!result.ok) {
90
140
  return {
91
- status,
92
- startedAt,
93
- completedAt,
94
- source,
95
- phases,
96
- raw: content,
141
+ status: "pending",
142
+ phases: {},
143
+ raw: ""
97
144
  };
145
+ }
146
+ return parseBootstrapStatus(result.stdout);
98
147
  }
99
- // ---------------------------------------------------------------------------
100
- // Status file writing
101
- // ---------------------------------------------------------------------------
102
- /**
103
- * Write initial bootstrap status file.
104
- */
105
- export async function initBootstrap(statusFile, source, opts) {
106
- const now = new Date().toISOString();
107
- const content = `status=started\nstarted_at=${now}\nsource=${source}\n`;
108
- return writeFile(statusFile, content, opts);
109
- }
110
- /**
111
- * Mark a phase as started.
112
- */
113
- export async function startPhase(statusFile, phase, opts) {
114
- const now = new Date().toISOString();
115
- const line = `phase.${phase}.status=running\nphase.${phase}.started_at=${now}\n`;
116
- return writeFile(statusFile, line, { ...opts, append: true });
117
- }
118
- /**
119
- * Mark a phase as complete.
120
- */
121
- export async function completePhase(statusFile, phase, opts) {
122
- const now = new Date().toISOString();
123
- const line = `phase.${phase}.status=complete\nphase.${phase}.completed_at=${now}\n`;
124
- return writeFile(statusFile, line, { ...opts, append: true });
125
- }
126
- /**
127
- * Mark a phase as failed.
128
- */
129
- export async function failPhase(statusFile, phase, error, opts) {
130
- const now = new Date().toISOString();
131
- const safeError = error.replace(/\n/g, " "); // Don't break the format
132
- const line = `phase.${phase}.status=failed\nphase.${phase}.completed_at=${now}\nphase.${phase}.error=${safeError}\n`;
133
- return writeFile(statusFile, line, { ...opts, append: true });
134
- }
135
- /**
136
- * Mark entire bootstrap as complete.
137
- */
138
- export async function completeBootstrap(statusFile, opts) {
139
- const now = new Date().toISOString();
140
- const line = `status=complete\ncompleted_at=${now}\n`;
141
- return writeFile(statusFile, line, { ...opts, append: true });
142
- }
143
- /**
144
- * Mark bootstrap as failed.
145
- */
146
- export async function failBootstrap(statusFile, error, opts) {
147
- const now = new Date().toISOString();
148
- const safeError = error.replace(/\n/g, " ");
149
- const line = `status=failed\ncompleted_at=${now}\nerror=${safeError}\n`;
150
- return writeFile(statusFile, line, { ...opts, append: true });
151
- }
152
- // ---------------------------------------------------------------------------
153
- // Marker files
154
- // ---------------------------------------------------------------------------
155
- /**
156
- * Check if a marker file exists.
157
- * Use for simple completion flags (e.g. .seed-setup-complete).
158
- */
159
- export async function checkMarker(markerPath, opts) {
160
- const result = await exec(["test", "-f", markerPath, "&&", "echo", "exists"], {
161
- ...opts,
162
- quiet: true,
163
- });
164
- return result.ok && result.stdout.trim() === "exists";
165
- }
166
- /**
167
- * Create a marker file.
168
- */
169
- export async function setMarker(markerPath, opts) {
170
- return exec(["touch", markerPath], opts);
171
- }
172
- /**
173
- * Remove a marker file.
174
- */
175
- export async function removeMarker(markerPath, opts) {
176
- return exec(["rm", "-f", markerPath], opts);
177
- }
178
- // ---------------------------------------------------------------------------
179
- // Polling
180
- // ---------------------------------------------------------------------------
181
- /**
182
- * Poll until bootstrap completes or times out.
183
- *
184
- * Returns the final status (complete or failed/timeout).
185
- */
186
- export async function waitForBootstrap(statusFile, opts) {
187
- const maxAttempts = opts.maxAttempts ?? 30;
188
- const intervalMs = opts.intervalMs ?? 2000;
189
- for (let attempt = 1; attempt <= maxAttempts; attempt++) {
190
- const status = await getBootstrapStatus(statusFile, opts);
191
- if (status.status === "complete") {
192
- return { completed: true, status };
193
- }
194
- if (status.status === "failed") {
195
- return { completed: false, status };
196
- }
197
- opts.onProgress?.(attempt, status);
198
- await sleep(intervalMs);
148
+ function parseBootstrapStatus(content) {
149
+ const phases = {};
150
+ const data = {};
151
+ for (const line of content.trim().split(`
152
+ `)) {
153
+ if (!line || !line.includes("="))
154
+ continue;
155
+ const [key, ...valueParts] = line.split("=");
156
+ const value = valueParts.join("=").trim();
157
+ data[key] = value;
158
+ }
159
+ const status = data.status || "started";
160
+ const startedAt = data.started_at;
161
+ const completedAt = data.completed_at;
162
+ const source = data.source;
163
+ for (const [key, value] of Object.entries(data)) {
164
+ if (!key.startsWith("phase."))
165
+ continue;
166
+ const parts = key.split(".");
167
+ if (parts.length < 3)
168
+ continue;
169
+ const [, phaseName, field] = parts;
170
+ if (!phases[phaseName]) {
171
+ phases[phaseName] = { name: phaseName, status: "pending" };
172
+ }
173
+ switch (field) {
174
+ case "status":
175
+ phases[phaseName].status = value;
176
+ break;
177
+ case "started_at":
178
+ phases[phaseName].startedAt = value;
179
+ break;
180
+ case "completed_at":
181
+ phases[phaseName].completedAt = value;
182
+ break;
183
+ case "error":
184
+ phases[phaseName].error = value;
185
+ break;
186
+ }
187
+ }
188
+ return {
189
+ status,
190
+ startedAt,
191
+ completedAt,
192
+ source,
193
+ phases,
194
+ raw: content
195
+ };
196
+ }
197
+ async function initBootstrap(statusFile, source, opts) {
198
+ const now = new Date().toISOString();
199
+ const content = `status=started
200
+ started_at=${now}
201
+ source=${source}
202
+ `;
203
+ return writeFile2(statusFile, content, opts);
204
+ }
205
+ async function startPhase(statusFile, phase, opts) {
206
+ const now = new Date().toISOString();
207
+ const line = `phase.${phase}.status=running
208
+ phase.${phase}.started_at=${now}
209
+ `;
210
+ return writeFile2(statusFile, line, { ...opts, append: true });
211
+ }
212
+ async function completePhase(statusFile, phase, opts) {
213
+ const now = new Date().toISOString();
214
+ const line = `phase.${phase}.status=complete
215
+ phase.${phase}.completed_at=${now}
216
+ `;
217
+ return writeFile2(statusFile, line, { ...opts, append: true });
218
+ }
219
+ async function failPhase(statusFile, phase, error, opts) {
220
+ const now = new Date().toISOString();
221
+ const safeError = error.replace(/\n/g, " ");
222
+ const line = `phase.${phase}.status=failed
223
+ phase.${phase}.completed_at=${now}
224
+ phase.${phase}.error=${safeError}
225
+ `;
226
+ return writeFile2(statusFile, line, { ...opts, append: true });
227
+ }
228
+ async function completeBootstrap(statusFile, opts) {
229
+ const now = new Date().toISOString();
230
+ const line = `status=complete
231
+ completed_at=${now}
232
+ `;
233
+ return writeFile2(statusFile, line, { ...opts, append: true });
234
+ }
235
+ async function failBootstrap(statusFile, error, opts) {
236
+ const now = new Date().toISOString();
237
+ const safeError = error.replace(/\n/g, " ");
238
+ const line = `status=failed
239
+ completed_at=${now}
240
+ error=${safeError}
241
+ `;
242
+ return writeFile2(statusFile, line, { ...opts, append: true });
243
+ }
244
+ async function checkMarker(markerPath, opts) {
245
+ const result = await exec2(["test", "-f", markerPath, "&&", "echo", "exists"], {
246
+ ...opts,
247
+ quiet: true
248
+ });
249
+ return result.ok && result.stdout.trim() === "exists";
250
+ }
251
+ async function setMarker(markerPath, opts) {
252
+ return exec2(["touch", markerPath], opts);
253
+ }
254
+ async function removeMarker(markerPath, opts) {
255
+ return exec2(["rm", "-f", markerPath], opts);
256
+ }
257
+ async function waitForBootstrap(statusFile, opts = {}) {
258
+ const maxAttempts = opts.maxAttempts ?? 30;
259
+ const intervalMs = opts.intervalMs ?? 2000;
260
+ for (let attempt = 1;attempt <= maxAttempts; attempt++) {
261
+ const status = await getBootstrapStatus(statusFile, opts);
262
+ if (status.status === "complete") {
263
+ return { completed: true, status };
199
264
  }
200
- // Timeout - get final status for error info
201
- const finalStatus = await getBootstrapStatus(statusFile, opts);
202
- return { completed: false, status: finalStatus };
203
- }
204
- /**
205
- * Poll until a marker file exists.
206
- */
207
- export async function waitForMarker(markerPath, opts) {
208
- const maxAttempts = opts.maxAttempts ?? 30;
209
- const intervalMs = opts.intervalMs ?? 2000;
210
- for (let attempt = 1; attempt <= maxAttempts; attempt++) {
211
- const exists = await checkMarker(markerPath, opts);
212
- if (exists) {
213
- return { exists: true, timedOut: false };
214
- }
215
- opts.onProgress?.(attempt, { status: "running" });
216
- await sleep(intervalMs);
265
+ if (status.status === "failed") {
266
+ return { completed: false, status };
267
+ }
268
+ opts.onProgress?.(attempt, status);
269
+ await sleep(intervalMs);
270
+ }
271
+ const finalStatus = await getBootstrapStatus(statusFile, opts);
272
+ return { completed: false, status: finalStatus };
273
+ }
274
+ async function waitForMarker(markerPath, opts = {}) {
275
+ const maxAttempts = opts.maxAttempts ?? 30;
276
+ const intervalMs = opts.intervalMs ?? 2000;
277
+ for (let attempt = 1;attempt <= maxAttempts; attempt++) {
278
+ const exists = await checkMarker(markerPath, opts);
279
+ if (exists) {
280
+ return { exists: true, timedOut: false };
217
281
  }
218
- return { exists: false, timedOut: true };
282
+ opts.onProgress?.(attempt, { status: "running" });
283
+ await sleep(intervalMs);
284
+ }
285
+ return { exists: false, timedOut: true };
219
286
  }
220
- // ---------------------------------------------------------------------------
221
- // Utilities
222
- // ---------------------------------------------------------------------------
223
287
  function sleep(ms) {
224
- return new Promise((resolve) => setTimeout(resolve, ms));
288
+ return new Promise((resolve) => setTimeout(resolve, ms));
225
289
  }
290
+ export {
291
+ waitForMarker,
292
+ waitForBootstrap,
293
+ startPhase,
294
+ setMarker,
295
+ removeMarker,
296
+ parseBootstrapStatus,
297
+ initBootstrap,
298
+ getBootstrapStatus,
299
+ failPhase,
300
+ failBootstrap,
301
+ completePhase,
302
+ completeBootstrap,
303
+ checkMarker
304
+ };
package/dist/clone.d.ts CHANGED
@@ -34,3 +34,4 @@ export interface CloneResult {
34
34
  * await clone({ repo: "https://github.com/org/repo.git", sparse: ["src/lib", "src/types"], depth: 1 });
35
35
  */
36
36
  export declare function clone(opts: CloneOptions): Promise<CloneResult>;
37
+ //# sourceMappingURL=clone.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"clone.d.ts","sourceRoot":"","sources":["../clone.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,YAAY;IAC3B,oCAAoC;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,+CAA+C;IAC/C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,yCAAyC;IACzC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,0DAA0D;IAC1D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,8CAA8C;IAC9C,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,wDAAwD;IACxD,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,WAAW;IAC1B,uCAAuC;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,mCAAmC;IACnC,GAAG,EAAE,MAAM,CAAC;IACZ,wBAAwB;IACxB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,KAAK,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC,CA+BpE"}