puls-dev 0.1.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.
Files changed (93) hide show
  1. package/LICENSE +7 -0
  2. package/README.md +148 -0
  3. package/dist/core/checker.d.ts +5 -0
  4. package/dist/core/checker.js +148 -0
  5. package/dist/core/config.d.ts +35 -0
  6. package/dist/core/config.js +15 -0
  7. package/dist/core/decorators.d.ts +26 -0
  8. package/dist/core/decorators.js +86 -0
  9. package/dist/core/output.d.ts +8 -0
  10. package/dist/core/output.js +19 -0
  11. package/dist/core/resource.d.ts +20 -0
  12. package/dist/core/resource.js +77 -0
  13. package/dist/core/stack.d.ts +20 -0
  14. package/dist/core/stack.js +120 -0
  15. package/dist/index.d.ts +12 -0
  16. package/dist/index.js +12 -0
  17. package/dist/providers/aws/acm.d.ts +22 -0
  18. package/dist/providers/aws/acm.js +109 -0
  19. package/dist/providers/aws/api.d.ts +28 -0
  20. package/dist/providers/aws/api.js +36 -0
  21. package/dist/providers/aws/apigateway.d.ts +24 -0
  22. package/dist/providers/aws/apigateway.js +157 -0
  23. package/dist/providers/aws/cloudfront.d.ts +31 -0
  24. package/dist/providers/aws/cloudfront.js +205 -0
  25. package/dist/providers/aws/fargate.d.ts +43 -0
  26. package/dist/providers/aws/fargate.js +277 -0
  27. package/dist/providers/aws/index.d.ts +23 -0
  28. package/dist/providers/aws/index.js +29 -0
  29. package/dist/providers/aws/lambda.d.ts +30 -0
  30. package/dist/providers/aws/lambda.js +159 -0
  31. package/dist/providers/aws/list.d.ts +2 -0
  32. package/dist/providers/aws/list.js +44 -0
  33. package/dist/providers/aws/rds.d.ts +46 -0
  34. package/dist/providers/aws/rds.js +227 -0
  35. package/dist/providers/aws/route53.d.ts +38 -0
  36. package/dist/providers/aws/route53.js +218 -0
  37. package/dist/providers/aws/s3.d.ts +20 -0
  38. package/dist/providers/aws/s3.js +165 -0
  39. package/dist/providers/aws/secrets.d.ts +25 -0
  40. package/dist/providers/aws/secrets.js +151 -0
  41. package/dist/providers/aws/sqs.d.ts +33 -0
  42. package/dist/providers/aws/sqs.js +178 -0
  43. package/dist/providers/do/api.d.ts +11 -0
  44. package/dist/providers/do/api.js +52 -0
  45. package/dist/providers/do/certificate.d.ts +7 -0
  46. package/dist/providers/do/certificate.js +36 -0
  47. package/dist/providers/do/domain.d.ts +21 -0
  48. package/dist/providers/do/domain.js +81 -0
  49. package/dist/providers/do/droplet.d.ts +35 -0
  50. package/dist/providers/do/droplet.js +180 -0
  51. package/dist/providers/do/firewall.d.ts +23 -0
  52. package/dist/providers/do/firewall.js +94 -0
  53. package/dist/providers/do/index.d.ts +15 -0
  54. package/dist/providers/do/index.js +21 -0
  55. package/dist/providers/do/list.d.ts +2 -0
  56. package/dist/providers/do/list.js +59 -0
  57. package/dist/providers/do/load_balancer.d.ts +12 -0
  58. package/dist/providers/do/load_balancer.js +62 -0
  59. package/dist/providers/firebase/api.d.ts +4 -0
  60. package/dist/providers/firebase/api.js +62 -0
  61. package/dist/providers/firebase/auth.d.ts +35 -0
  62. package/dist/providers/firebase/auth.js +147 -0
  63. package/dist/providers/firebase/firestore.d.ts +28 -0
  64. package/dist/providers/firebase/firestore.js +120 -0
  65. package/dist/providers/firebase/functions.d.ts +50 -0
  66. package/dist/providers/firebase/functions.js +163 -0
  67. package/dist/providers/firebase/hosting.d.ts +14 -0
  68. package/dist/providers/firebase/hosting.js +144 -0
  69. package/dist/providers/firebase/index.d.ts +15 -0
  70. package/dist/providers/firebase/index.js +15 -0
  71. package/dist/providers/firebase/remoteconfig.d.ts +22 -0
  72. package/dist/providers/firebase/remoteconfig.js +135 -0
  73. package/dist/providers/firebase/storage.d.ts +34 -0
  74. package/dist/providers/firebase/storage.js +117 -0
  75. package/dist/providers/proxmox/api.d.ts +12 -0
  76. package/dist/providers/proxmox/api.js +50 -0
  77. package/dist/providers/proxmox/index.d.ts +15 -0
  78. package/dist/providers/proxmox/index.js +10 -0
  79. package/dist/providers/proxmox/list.d.ts +2 -0
  80. package/dist/providers/proxmox/list.js +15 -0
  81. package/dist/providers/proxmox/vm.d.ts +61 -0
  82. package/dist/providers/proxmox/vm.js +482 -0
  83. package/dist/types/aws.d.ts +55 -0
  84. package/dist/types/aws.js +48 -0
  85. package/dist/types/do.d.ts +19 -0
  86. package/dist/types/do.js +19 -0
  87. package/dist/types/gcp.d.ts +9 -0
  88. package/dist/types/gcp.js +9 -0
  89. package/dist/types/inventory.d.ts +87 -0
  90. package/dist/types/inventory.js +2 -0
  91. package/dist/types/proxmox.d.ts +11 -0
  92. package/dist/types/proxmox.js +28 -0
  93. package/package.json +56 -0
@@ -0,0 +1,482 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { spawn } from "node:child_process";
4
+ import { dirname } from "node:path";
5
+ import { createConnection } from "node:net";
6
+ import { BaseBuilder } from "../../core/resource.js";
7
+ import { Config } from "../../core/config.js";
8
+ import { Output } from "../../core/output.js";
9
+ import { getPMClient } from "./api.js";
10
+ export class VMBuilder extends BaseBuilder {
11
+ out = {
12
+ ip: new Output(),
13
+ vmid: new Output(),
14
+ };
15
+ resolvedVmid = null;
16
+ resolvedNode = null;
17
+ resolvedIp = null;
18
+ _image;
19
+ _cores = 2;
20
+ _memory = 2048;
21
+ _provision;
22
+ _replace;
23
+ _node;
24
+ _storage;
25
+ _vlan;
26
+ _ip;
27
+ _sshKeys;
28
+ constructor(name) {
29
+ super(name);
30
+ this.discoveryPromise = this.discoverVm(name);
31
+ }
32
+ async discoverVm(name) {
33
+ try {
34
+ const pm = getPMClient();
35
+ const resources = await pm.get("/cluster/resources?type=vm");
36
+ return ((resources ?? []).find((r) => r.name === name && !r.template) ?? null);
37
+ }
38
+ catch (e) {
39
+ if (e.message?.includes("not configured"))
40
+ return null;
41
+ throw e;
42
+ }
43
+ }
44
+ image(os) {
45
+ this._image = os;
46
+ return this;
47
+ }
48
+ cores(n) {
49
+ this._cores = n;
50
+ return this;
51
+ }
52
+ memory(mb) {
53
+ this._memory = mb;
54
+ return this;
55
+ }
56
+ provision(playbookPath) {
57
+ this._provision = playbookPath;
58
+ return this;
59
+ }
60
+ replace(oldVmName) {
61
+ this._replace = oldVmName;
62
+ return this;
63
+ }
64
+ node(n) {
65
+ this._node = n;
66
+ return this;
67
+ }
68
+ storage(pool) {
69
+ this._storage = pool;
70
+ return this;
71
+ }
72
+ vlan(tag) {
73
+ this._vlan = tag;
74
+ return this;
75
+ }
76
+ ip(address) {
77
+ this._ip = address;
78
+ return this;
79
+ }
80
+ sshKey(keys) {
81
+ this._sshKeys = Array.isArray(keys) ? [...keys] : keys;
82
+ return this;
83
+ }
84
+ async deploy() {
85
+ const dryRun = this.isDryRunActive();
86
+ const existing = await this.discoveryPromise;
87
+ const pm = getPMClient();
88
+ console.log(`\n🖥️ Finalizing Proxmox VM "${this.name}"...`);
89
+ if (existing) {
90
+ this.resolvedVmid = existing.vmid;
91
+ this.resolvedNode = existing.node;
92
+ this.out.vmid.resolve(existing.vmid);
93
+ if (this._ip)
94
+ this.out.ip.resolve(this._ip.split("/")[0]);
95
+ console.log(` ✅ VM "${this.name}" already exists (vmid=${existing.vmid}, node=${existing.node}, status=${existing.status})`);
96
+ return {
97
+ name: this.name,
98
+ vmid: this.resolvedVmid,
99
+ node: this.resolvedNode,
100
+ };
101
+ }
102
+ if (dryRun) {
103
+ console.log(` 📝 [PLAN] Create VM "${this.name}"`);
104
+ if (this._image)
105
+ console.log(` └─ Image: ${this._image}`);
106
+ console.log(` └─ Cores: ${this._cores} Memory: ${this._memory} MB`);
107
+ if (this._vlan)
108
+ console.log(` └─ VLAN: ${this._vlan}`);
109
+ if (this._provision)
110
+ console.log(` └─ Provision: ${this._provision}`);
111
+ if (this._replace)
112
+ console.log(` └─ Replace: "${this._replace}" after creation`);
113
+ this.out.vmid.resolve(-1);
114
+ this.out.ip.resolve(this._ip?.split("/")[0] ?? "0.0.0.0");
115
+ return { name: this.name, vmid: "PENDING" };
116
+ }
117
+ // Find the template — match by VMID (numeric string) or name substring
118
+ const resources = await pm.get("/cluster/resources?type=vm");
119
+ const isVmid = this._image && /^\d+$/.test(this._image);
120
+ const template = this._image
121
+ ? (resources ?? []).find((r) => r.template === 1 &&
122
+ (isVmid
123
+ ? String(r.vmid) === this._image
124
+ : r.name?.includes(this._image)))
125
+ : null;
126
+ if (this._image && !template) {
127
+ throw new Error(`No Proxmox template found matching "${this._image}". ` +
128
+ (isVmid
129
+ ? `Check that VMID ${this._image} exists and is marked as a template.`
130
+ : `Create a template whose name contains "${this._image}".`));
131
+ }
132
+ // Resolve target node: explicit → configured nodes list → template's node → API discovery
133
+ let node = this._node;
134
+ if (!node) {
135
+ const configuredNodes = Config.get().providers.proxmox?.nodes;
136
+ node = configuredNodes?.[0] ?? template?.node;
137
+ }
138
+ if (!node) {
139
+ const nodes = await pm.get("/nodes");
140
+ node = (nodes ?? [])[0]?.node;
141
+ }
142
+ if (!node)
143
+ throw new Error("No Proxmox nodes available");
144
+ const newVmid = await pm.get("/cluster/nextid");
145
+ const storage = this._storage ?? "rbd_pool";
146
+ if (template) {
147
+ console.log(` 📋 Cloning template "${template.name}" (vmid=${template.vmid}) → "${this.name}" (vmid=${newVmid})`);
148
+ const taskId = await pm.post(`/nodes/${node}/qemu/${template.vmid}/clone`, {
149
+ newid: newVmid,
150
+ name: this.name,
151
+ full: 1,
152
+ storage,
153
+ format: "raw",
154
+ });
155
+ // Clone is async — wait for the Proxmox task to finish before configuring
156
+ await this.waitForTask(node, taskId, pm);
157
+ }
158
+ else {
159
+ console.log(` 🆕 Creating blank VM "${this.name}" (vmid=${newVmid})`);
160
+ await pm.post(`/nodes/${node}/qemu`, {
161
+ vmid: newVmid,
162
+ name: this.name,
163
+ cores: this._cores,
164
+ memory: this._memory,
165
+ net0: `virtio,bridge=vmbr1${this._vlan ? `,tag=${this._vlan}` : ""}`,
166
+ ostype: "l26",
167
+ });
168
+ }
169
+ this.resolvedVmid = newVmid;
170
+ this.resolvedNode = node;
171
+ this.out.vmid.resolve(newVmid);
172
+ // Auto-resolve static IP from internal DNS if no explicit IP set
173
+ if (!this._ip) {
174
+ const domain = Config.get().providers.proxmox?.dnsDomain;
175
+ if (domain) {
176
+ try {
177
+ const { resolve4 } = await import("node:dns/promises");
178
+ const [addr] = await resolve4(`${this.name}.${domain}`);
179
+ this._ip = addr;
180
+ console.log(` 🔍 DNS: ${this.name}.${domain} → ${addr}`);
181
+ }
182
+ catch {
183
+ // Not in DNS — will fall through to DHCP
184
+ }
185
+ }
186
+ }
187
+ // Build net0 string — VirtIO on vmbr1, optional VLAN tag
188
+ const net0 = `virtio,bridge=vmbr1${this._vlan ? `,tag=${this._vlan}` : ""}`;
189
+ const configPatch = {
190
+ onboot: 1,
191
+ machine: "q35",
192
+ cores: this._cores,
193
+ memory: this._memory,
194
+ net0,
195
+ ipconfig0: this._ip
196
+ ? (() => {
197
+ const [addr, prefix = "24"] = this._ip.split("/");
198
+ const gw = addr.split(".").slice(0, 3).join(".") + ".1";
199
+ return `gw=${gw},ip=${addr}/${prefix}`;
200
+ })()
201
+ : "ip=dhcp",
202
+ nameserver: (Config.get().providers.proxmox?.dnsServers ?? ["1.1.1.1", "8.8.8.8"]).join(" "),
203
+ searchdomain: Config.get().providers.proxmox?.dnsDomain ?? "",
204
+ ciuser: "root",
205
+ };
206
+ const pubKeys = this.resolvePublicKeys();
207
+ if (pubKeys.length) {
208
+ configPatch.sshkeys = encodeURIComponent(pubKeys.join("\n"));
209
+ }
210
+ await pm.post(`/nodes/${node}/qemu/${newVmid}/config`, configPatch);
211
+ await pm.post(`/nodes/${node}/qemu/${newVmid}/status/start`);
212
+ console.log(`🚀 Started VM "${this.name}" (vmid=${newVmid}, node=${node})`);
213
+ if (this._ip) {
214
+ const [addr] = this._ip.split("/");
215
+ this.resolvedIp = addr;
216
+ this.out.ip.resolve(addr);
217
+ console.log(` 🌐 IP: ${this.resolvedIp} (static)`);
218
+ }
219
+ else {
220
+ // Wait for qemu-agent to report an IP
221
+ await this.waitFor(`VM "${this.name}" to boot and get an IP`, async () => {
222
+ try {
223
+ const ifaces = await pm.get(`/nodes/${node}/qemu/${newVmid}/agent/network-get-interfaces`);
224
+ const eth = (ifaces ?? []).find((i) => i.name !== "lo");
225
+ const addr = eth?.["ip-addresses"]?.find((a) => a["ip-address-type"] === "ipv4");
226
+ if (addr?.["ip-address"]) {
227
+ this.resolvedIp = addr["ip-address"];
228
+ return true;
229
+ }
230
+ return false;
231
+ }
232
+ catch {
233
+ return false;
234
+ }
235
+ }, { intervalMs: 10_000, timeoutMs: 300_000 });
236
+ if (this.resolvedIp)
237
+ this.out.ip.resolve(this.resolvedIp);
238
+ console.log(` 🌐 IP: ${this.resolvedIp}`);
239
+ }
240
+ if (this._provision) {
241
+ await this.waitFor(`SSH on ${this.resolvedIp} to be ready`, () => this.checkPort(this.resolvedIp, 22), { intervalMs: 10_000, timeoutMs: 300_000 });
242
+ await this.waitFor(`cloud-init to finish on ${this.resolvedIp}`, () => this.checkCloudInit(this.resolvedIp), { intervalMs: 15_000, timeoutMs: 300_000 });
243
+ await this.runProvisioner(this.resolvedIp, this._provision);
244
+ }
245
+ if (this._replace) {
246
+ await this.destroyVmByName(this._replace, pm);
247
+ }
248
+ return { name: this.name, vmid: this.resolvedVmid, ip: this.resolvedIp };
249
+ }
250
+ async destroy() {
251
+ const dryRun = this.isDryRunActive();
252
+ const existing = await this.discoveryPromise;
253
+ console.log(`\n🗑️ Destroying Proxmox VM "${this.name}"...`);
254
+ if (!existing) {
255
+ console.log(` ─ VM "${this.name}" not found`);
256
+ return { destroyed: false };
257
+ }
258
+ if (dryRun) {
259
+ console.log(` 📝 [PLAN] Stop + delete VM "${this.name}" (vmid=${existing.vmid})`);
260
+ return { destroyed: this.name };
261
+ }
262
+ const pm = getPMClient();
263
+ await this.destroyVmByName(this.name, pm);
264
+ return { destroyed: this.name };
265
+ }
266
+ // Poll a Proxmox task UPID until it exits, then throw if it failed
267
+ async waitForTask(node, upid, pm) {
268
+ const encoded = encodeURIComponent(upid);
269
+ await this.waitFor(`clone task to complete`, async () => {
270
+ const status = await pm.get(`/nodes/${node}/tasks/${encoded}/status`);
271
+ if (status?.status !== "stopped")
272
+ return false;
273
+ if (status.exitstatus && status.exitstatus !== "OK") {
274
+ throw new Error(`Clone task failed: ${status.exitstatus}`);
275
+ }
276
+ return true;
277
+ }, { intervalMs: 5_000, timeoutMs: 300_000 });
278
+ }
279
+ async destroyVmByName(name, pm) {
280
+ const resources = await pm.get("/cluster/resources?type=vm");
281
+ const vm = (resources ?? []).find((r) => r.name === name && !r.template);
282
+ if (!vm) {
283
+ console.log(` ℹ️ VM "${name}" not found — already gone`);
284
+ return;
285
+ }
286
+ if (vm.status === "running") {
287
+ await pm.post(`/nodes/${vm.node}/qemu/${vm.vmid}/status/stop`);
288
+ await this.waitFor(`VM "${name}" to stop`, async () => {
289
+ const s = await pm.get(`/nodes/${vm.node}/qemu/${vm.vmid}/status/current`);
290
+ return s?.status === "stopped";
291
+ }, { intervalMs: 5_000, timeoutMs: 120_000 });
292
+ }
293
+ await pm.delete(`/nodes/${vm.node}/qemu/${vm.vmid}?purge=1&destroy-unreferenced-disks=1`);
294
+ console.log(` 🗑️ Removed VM "${name}" (vmid=${vm.vmid})`);
295
+ }
296
+ resolvePublicKeys() {
297
+ const input = this._sshKeys;
298
+ if (!input) {
299
+ // Default: read ~/.ssh/id_rsa.pub if it exists
300
+ try {
301
+ return [
302
+ readFileSync(`${homedir()}/.ssh/id_ed25519.pub`, "utf-8").trim(),
303
+ ];
304
+ }
305
+ catch {
306
+ return [];
307
+ }
308
+ }
309
+ if (Array.isArray(input))
310
+ return input.map((k) => k.trim()).filter(Boolean);
311
+ // Single string: key literal (starts with ssh-) or file path
312
+ if (input.startsWith("ssh-") ||
313
+ input.startsWith("ecdsa-") ||
314
+ input.startsWith("sk-")) {
315
+ return [input.trim()];
316
+ }
317
+ try {
318
+ return [
319
+ readFileSync(input.replace(/^~/, homedir()), "utf-8").trim(),
320
+ ];
321
+ }
322
+ catch {
323
+ return [];
324
+ }
325
+ }
326
+ checkCloudInit(ip) {
327
+ const keyPath = this.sshKeyPath();
328
+ return new Promise(resolve => {
329
+ const proc = spawn('ssh', [
330
+ '-i', keyPath,
331
+ '-o', 'StrictHostKeyChecking=no',
332
+ '-o', 'ConnectTimeout=10',
333
+ '-o', 'BatchMode=yes',
334
+ `root@${ip}`,
335
+ 'cloud-init status',
336
+ ], { stdio: ['ignore', 'pipe', 'ignore'] });
337
+ let out = '';
338
+ proc.stdout.on('data', (d) => out += d.toString());
339
+ proc.on('close', () => resolve(out.includes('done') || out.includes('error')));
340
+ proc.on('error', () => resolve(false));
341
+ });
342
+ }
343
+ checkPort(ip, port) {
344
+ return new Promise(resolve => {
345
+ const socket = createConnection({ host: ip, port, timeout: 3_000 });
346
+ socket.on('connect', () => { socket.destroy(); resolve(true); });
347
+ socket.on('timeout', () => { socket.destroy(); resolve(false); });
348
+ socket.on('error', () => resolve(false));
349
+ });
350
+ }
351
+ sshKeyPath() {
352
+ const keyInput = Array.isArray(this._sshKeys)
353
+ ? null
354
+ : this._sshKeys;
355
+ return (keyInput &&
356
+ !keyInput.startsWith("ssh-") &&
357
+ !keyInput.startsWith("ecdsa-") &&
358
+ !keyInput.startsWith("sk-")
359
+ ? keyInput.replace(/\.pub$/, "")
360
+ : `${homedir()}/.ssh/id_ed25519`).replace(/^~/, homedir());
361
+ }
362
+ runProvisioner(ip, script) {
363
+ const ext = script.split(".").pop()?.toLowerCase();
364
+ if (ext === "sh")
365
+ return this.runShellScript(ip, script);
366
+ if (ext === "pp")
367
+ return this.runPuppet(ip, script);
368
+ return this.runAnsible(ip, script); // .yml / .yaml
369
+ }
370
+ runShellScript(ip, script) {
371
+ console.log(` 🔧 Running shell script: ${script} → ${ip}`);
372
+ const keyPath = this.sshKeyPath();
373
+ const scriptDir = dirname(script); // e.g. 'config'
374
+ const sshArgs = [
375
+ "-i",
376
+ keyPath,
377
+ "-o",
378
+ "StrictHostKeyChecking=no",
379
+ "-o",
380
+ "ConnectTimeout=30",
381
+ ];
382
+ return new Promise((resolve, reject) => {
383
+ // Copy the script's directory to the same path on the remote (e.g. config/ → /config/)
384
+ console.log(` 📂 Copying ${scriptDir}/ → ${ip}:/${scriptDir}/`);
385
+ const scp = spawn("scp", [
386
+ "-i",
387
+ keyPath,
388
+ "-o",
389
+ "StrictHostKeyChecking=no",
390
+ "-r",
391
+ scriptDir,
392
+ `root@${ip}:/`,
393
+ ], { stdio: "inherit" });
394
+ scp.on("error", (err) => reject(new Error(`scp failed: ${err.message}`)));
395
+ scp.on("close", (scpCode) => {
396
+ if (scpCode !== 0) {
397
+ reject(new Error(`scp exited with code ${scpCode}`));
398
+ return;
399
+ }
400
+ const proc = spawn("ssh", [...sshArgs, `root@${ip}`, "bash -s"], {
401
+ stdio: ["pipe", "inherit", "inherit"],
402
+ });
403
+ proc.stdin.write(readFileSync(script));
404
+ proc.stdin.end();
405
+ proc.on("close", (code) => {
406
+ if (code === 0) {
407
+ console.log(` ✅ Provisioning complete`);
408
+ resolve();
409
+ }
410
+ else
411
+ reject(new Error(`Shell script exited with code ${code}`));
412
+ });
413
+ proc.on("error", (err) => reject(new Error(`ssh failed: ${err.message}`)));
414
+ });
415
+ });
416
+ }
417
+ runPuppet(ip, manifest) {
418
+ console.log(` 🔧 Applying Puppet manifest: ${manifest} → ${ip}`);
419
+ const keyPath = this.sshKeyPath();
420
+ // Copy manifest then apply it
421
+ return new Promise((resolve, reject) => {
422
+ const scp = spawn("scp", [
423
+ "-i",
424
+ keyPath,
425
+ "-o",
426
+ "StrictHostKeyChecking=no",
427
+ manifest,
428
+ `root@${ip}:/tmp/manifest.pp`,
429
+ ], { stdio: "inherit" });
430
+ scp.on("close", (code) => {
431
+ if (code !== 0) {
432
+ reject(new Error(`scp exited with code ${code}`));
433
+ return;
434
+ }
435
+ const puppet = spawn("ssh", [
436
+ "-i",
437
+ keyPath,
438
+ "-o",
439
+ "StrictHostKeyChecking=no",
440
+ `root@${ip}`,
441
+ "puppet apply /tmp/manifest.pp",
442
+ ], { stdio: "inherit" });
443
+ puppet.on("close", (c) => {
444
+ if (c === 0) {
445
+ console.log(` ✅ Provisioning complete`);
446
+ resolve();
447
+ }
448
+ else
449
+ reject(new Error(`puppet apply exited with code ${c}`));
450
+ });
451
+ puppet.on("error", (err) => reject(new Error(`Failed to run puppet: ${err.message}`)));
452
+ });
453
+ scp.on("error", (err) => reject(new Error(`Failed to run scp: ${err.message}`)));
454
+ });
455
+ }
456
+ runAnsible(ip, playbook) {
457
+ console.log(` 🔧 Running Ansible: ${playbook} → ${ip}`);
458
+ const keyPath = this.sshKeyPath();
459
+ return new Promise((resolve, reject) => {
460
+ const proc = spawn("ansible-playbook", [
461
+ playbook,
462
+ "-i",
463
+ `${ip},`,
464
+ "-u",
465
+ "root",
466
+ "--private-key",
467
+ keyPath,
468
+ "--ssh-extra-args",
469
+ "-o StrictHostKeyChecking=no -o ConnectTimeout=30",
470
+ ], { stdio: "inherit" });
471
+ proc.on("close", (code) => {
472
+ if (code === 0) {
473
+ console.log(` ✅ Provisioning complete`);
474
+ resolve();
475
+ }
476
+ else
477
+ reject(new Error(`ansible-playbook exited with code ${code}`));
478
+ });
479
+ proc.on("error", (err) => reject(new Error(`Failed to run ansible-playbook: ${err.message}`)));
480
+ });
481
+ }
482
+ }
@@ -0,0 +1,55 @@
1
+ export declare const DB: {
2
+ readonly POSTGRES_16: {
3
+ readonly engine: "postgres";
4
+ readonly version: "16";
5
+ };
6
+ readonly POSTGRES_15: {
7
+ readonly engine: "postgres";
8
+ readonly version: "15";
9
+ };
10
+ readonly MYSQL_8: {
11
+ readonly engine: "mysql";
12
+ readonly version: "8.0";
13
+ };
14
+ readonly MARIADB_11: {
15
+ readonly engine: "mariadb";
16
+ readonly version: "11.4";
17
+ };
18
+ };
19
+ export declare const DB_SIZE: {
20
+ readonly MICRO: "db.t3.micro";
21
+ readonly SMALL: "db.t3.small";
22
+ readonly MEDIUM: "db.t3.medium";
23
+ readonly LARGE: "db.r6g.large";
24
+ };
25
+ export declare const RUNTIME: {
26
+ readonly NODEJS_20: "nodejs20.x";
27
+ readonly NODEJS_18: "nodejs18.x";
28
+ readonly PYTHON_3_12: "python3.12";
29
+ readonly PYTHON_3_11: "python3.11";
30
+ readonly JAVA_21: "java21";
31
+ readonly DOTNET_8: "dotnet8";
32
+ };
33
+ export declare const SECRETS: {
34
+ readonly API_KEY: "myapp/api-key";
35
+ readonly SUPERSECRET: "test-secret-super-secret";
36
+ };
37
+ export declare const REGION: {
38
+ readonly US_EAST_1: "us-east-1";
39
+ readonly US_WEST_2: "us-west-2";
40
+ readonly EU_CENTRAL_1: "eu-central-1";
41
+ readonly EU_WEST_1: "eu-west-1";
42
+ };
43
+ export interface RegistrantContact {
44
+ FIRSTNAME: string;
45
+ LASTNAME: string;
46
+ EMAIL: string;
47
+ MOBILE: string;
48
+ CONTACT_TYPE: string;
49
+ ORGANIZATION: string;
50
+ ADDRESSLINE: string;
51
+ CITY: string;
52
+ ZIPCODE: string;
53
+ COUNTRY: string;
54
+ }
55
+ export declare const DOMAIN_REGISTER: RegistrantContact;
@@ -0,0 +1,48 @@
1
+ // Define your secret IDs here — reference them everywhere instead of raw strings.
2
+ // Example:
3
+ // export const SECRETS = {
4
+ // API_KEY: "myapp/api-key",
5
+ // DB_PASSWORD: "myapp/db-password",
6
+ // } as const;
7
+ export const DB = {
8
+ POSTGRES_16: { engine: "postgres", version: "16" },
9
+ POSTGRES_15: { engine: "postgres", version: "15" },
10
+ MYSQL_8: { engine: "mysql", version: "8.0" },
11
+ MARIADB_11: { engine: "mariadb", version: "11.4" },
12
+ };
13
+ export const DB_SIZE = {
14
+ MICRO: "db.t3.micro", // free tier eligible
15
+ SMALL: "db.t3.small",
16
+ MEDIUM: "db.t3.medium",
17
+ LARGE: "db.r6g.large",
18
+ };
19
+ export const RUNTIME = {
20
+ NODEJS_20: "nodejs20.x",
21
+ NODEJS_18: "nodejs18.x",
22
+ PYTHON_3_12: "python3.12",
23
+ PYTHON_3_11: "python3.11",
24
+ JAVA_21: "java21",
25
+ DOTNET_8: "dotnet8",
26
+ };
27
+ export const SECRETS = {
28
+ API_KEY: "myapp/api-key",
29
+ SUPERSECRET: "test-secret-super-secret",
30
+ };
31
+ export const REGION = {
32
+ US_EAST_1: "us-east-1",
33
+ US_WEST_2: "us-west-2",
34
+ EU_CENTRAL_1: "eu-central-1",
35
+ EU_WEST_1: "eu-west-1",
36
+ };
37
+ export const DOMAIN_REGISTER = {
38
+ FIRSTNAME: "first-name",
39
+ LASTNAME: "last-name",
40
+ EMAIL: "email@domain.com",
41
+ MOBILE: "+46.xxxxxx",
42
+ CONTACT_TYPE: "COMPANY",
43
+ ORGANIZATION: "company.com",
44
+ ADDRESSLINE: "address-line",
45
+ CITY: "city",
46
+ ZIPCODE: "zip-code",
47
+ COUNTRY: "country",
48
+ };
@@ -0,0 +1,19 @@
1
+ export declare const OS: {
2
+ readonly DEBIAN_11: "debian-11-x64";
3
+ readonly UBUNTU_22_04: "ubuntu-22-04-x64";
4
+ };
5
+ export declare const REGION: {
6
+ readonly FRA: "fra1";
7
+ readonly NYC: "nyc3";
8
+ };
9
+ export declare const SIZE: {
10
+ readonly SMALL: "s-1vcpu-1gb";
11
+ readonly MEDIUM: "s-2vcpu-4gb";
12
+ readonly LARGE: "s-4vcpu-8gb";
13
+ };
14
+ export declare const NETWORK: {
15
+ readonly ANY: "0.0.0.0/0";
16
+ readonly ANY_V6: "::/0";
17
+ readonly OFFICE: "80.1.2.3/32";
18
+ readonly VPN: "10.0.0.0/24";
19
+ };
@@ -0,0 +1,19 @@
1
+ export const OS = {
2
+ DEBIAN_11: "debian-11-x64",
3
+ UBUNTU_22_04: "ubuntu-22-04-x64",
4
+ };
5
+ export const REGION = {
6
+ FRA: "fra1",
7
+ NYC: "nyc3",
8
+ };
9
+ export const SIZE = {
10
+ SMALL: "s-1vcpu-1gb",
11
+ MEDIUM: "s-2vcpu-4gb",
12
+ LARGE: "s-4vcpu-8gb",
13
+ };
14
+ export const NETWORK = {
15
+ ANY: "0.0.0.0/0",
16
+ ANY_V6: "::/0",
17
+ OFFICE: "80.1.2.3/32",
18
+ VPN: "10.0.0.0/24",
19
+ };
@@ -0,0 +1,9 @@
1
+ export declare const GCP_REGION: {
2
+ readonly EU_WEST1: "europe-west1";
3
+ readonly EU_WEST3: "europe-west3";
4
+ readonly EU_NORTH1: "europe-north1";
5
+ readonly US_CENTRAL1: "us-central1";
6
+ readonly US_EAST1: "us-east1";
7
+ readonly US_WEST1: "us-west1";
8
+ readonly ASIA_EAST1: "asia-east1";
9
+ };
@@ -0,0 +1,9 @@
1
+ export const GCP_REGION = {
2
+ EU_WEST1: "europe-west1",
3
+ EU_WEST3: "europe-west3",
4
+ EU_NORTH1: "europe-north1",
5
+ US_CENTRAL1: "us-central1",
6
+ US_EAST1: "us-east1",
7
+ US_WEST1: "us-west1",
8
+ ASIA_EAST1: "asia-east1",
9
+ };