puls-dev 0.3.3 → 0.3.5

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 (64) hide show
  1. package/dist/bin/install-shell.d.ts +2 -0
  2. package/dist/bin/install-shell.js +136 -0
  3. package/dist/bin/puls.d.ts +1 -0
  4. package/dist/bin/puls.js +145 -0
  5. package/dist/core/checker.js +74 -0
  6. package/dist/core/config.d.ts +3 -0
  7. package/dist/core/context.d.ts +1 -0
  8. package/dist/core/decorators.d.ts +1 -0
  9. package/dist/core/decorators.js +39 -5
  10. package/dist/core/output.js +8 -1
  11. package/dist/core/production.test.js +1 -0
  12. package/dist/core/resource.d.ts +35 -0
  13. package/dist/core/resource.js +57 -1
  14. package/dist/core/secret.d.ts +1 -0
  15. package/dist/core/secret.js +5 -0
  16. package/dist/core/stack.d.ts +11 -0
  17. package/dist/core/stack.js +141 -90
  18. package/dist/index.d.ts +2 -1
  19. package/dist/index.js +1 -1
  20. package/dist/providers/aws/api.js +3 -0
  21. package/dist/providers/aws/ec2.d.ts +5 -0
  22. package/dist/providers/aws/ec2.js +7 -0
  23. package/dist/providers/aws/lambda.d.ts +5 -0
  24. package/dist/providers/aws/lambda.js +24 -0
  25. package/dist/providers/aws/list.js +15 -3
  26. package/dist/providers/aws/rds.d.ts +9 -0
  27. package/dist/providers/aws/rds.js +19 -0
  28. package/dist/providers/do/database.d.ts +9 -0
  29. package/dist/providers/do/database.js +19 -0
  30. package/dist/providers/do/domain.js +1 -1
  31. package/dist/providers/do/droplet.d.ts +10 -0
  32. package/dist/providers/do/droplet.js +28 -3
  33. package/dist/providers/do/droplet.test.js +1 -1
  34. package/dist/providers/do/list.js +25 -2
  35. package/dist/providers/do/load_balancer.d.ts +5 -0
  36. package/dist/providers/do/load_balancer.js +7 -0
  37. package/dist/providers/do/vpc.d.ts +5 -0
  38. package/dist/providers/do/vpc.js +8 -0
  39. package/dist/providers/firebase/functions.d.ts +9 -0
  40. package/dist/providers/firebase/functions.js +28 -0
  41. package/dist/providers/firebase/list.js +34 -2
  42. package/dist/providers/gcp/api.js +6 -0
  43. package/dist/providers/gcp/cloudrun.d.ts +13 -0
  44. package/dist/providers/gcp/cloudrun.js +30 -0
  45. package/dist/providers/gcp/cloudsql.d.ts +9 -0
  46. package/dist/providers/gcp/cloudsql.js +20 -0
  47. package/dist/providers/gcp/list.js +12 -2
  48. package/dist/providers/gcp/template.d.ts +3 -0
  49. package/dist/providers/gcp/template.js +13 -1
  50. package/dist/providers/gcp/vm.d.ts +8 -0
  51. package/dist/providers/gcp/vm.js +22 -2
  52. package/dist/providers/proxmox/api.d.ts +1 -0
  53. package/dist/providers/proxmox/api.js +18 -3
  54. package/dist/providers/proxmox/base.d.ts +16 -0
  55. package/dist/providers/proxmox/base.js +121 -0
  56. package/dist/providers/proxmox/list.js +8 -1
  57. package/dist/providers/proxmox/template.d.ts +3 -10
  58. package/dist/providers/proxmox/template.js +51 -139
  59. package/dist/providers/proxmox/vm.d.ts +18 -10
  60. package/dist/providers/proxmox/vm.js +73 -152
  61. package/dist/types/diff.d.ts +17 -0
  62. package/dist/types/diff.js +1 -0
  63. package/dist/types/inventory.d.ts +65 -0
  64. package/package.json +7 -22
@@ -1,6 +1,10 @@
1
1
  import { Output } from "./output.js";
2
2
  import { Config } from "./config.js";
3
+ import { resourceContextStorage } from "./context.js";
3
4
  export const resolvedSecrets = new Set();
5
+ export function clearResolvedSecrets() {
6
+ resolvedSecrets.clear();
7
+ }
4
8
  /**
5
9
  * Secret represents a lazy, secure credential that is fetched asynchronously
6
10
  * at deployment time instead of during the eager construction phase.
@@ -25,6 +29,7 @@ export class Secret extends Output {
25
29
  this.resolve(val);
26
30
  if (val && val.length >= 3) {
27
31
  resolvedSecrets.add(val);
32
+ resourceContextStorage.getStore()?.secrets.add(val);
28
33
  }
29
34
  }
30
35
  catch (err) {
@@ -1,4 +1,5 @@
1
1
  import "reflect-metadata";
2
+ import type { StackDiff } from "../types/diff.js";
2
3
  export declare abstract class Stack {
3
4
  /** @internal - called by @Deploy to register the instance for cross-stack references. */
4
5
  static _register(cls: Function, instance: Stack, region?: string): void;
@@ -16,6 +17,16 @@ export declare abstract class Stack {
16
17
  * }
17
18
  */
18
19
  static from<T extends Stack>(cls: new (...args: any[]) => T, region?: string): T;
20
+ /**
21
+ * Compares every declared resource against its live cloud state without
22
+ * making any API writes. Returns a structured `StackDiff` and prints a
23
+ * formatted report to the console.
24
+ *
25
+ * Field-level drift is surfaced for providers that implement `getDiff()`.
26
+ * Resources with no `getDiff()` override show only existence status
27
+ * (missing / in-sync / adopted).
28
+ */
29
+ diff(): Promise<StackDiff>;
19
30
  deploy(): Promise<Record<string, any>>;
20
31
  destroy(): Promise<Record<string, any>>;
21
32
  }
@@ -3,6 +3,42 @@ import { BaseBuilder } from "./resource.js";
3
3
  import { Config } from "./config.js";
4
4
  import { resourceContextStorage } from "./context.js";
5
5
  import { resolvedSecrets } from "./secret.js";
6
+ async function withRedactedConsole(secrets, fn) {
7
+ const originalLog = console.log;
8
+ const redact = (message) => {
9
+ if (typeof message !== "string")
10
+ return message;
11
+ let result = message;
12
+ for (const secret of secrets) {
13
+ if (secret && secret.length >= 3) {
14
+ const escaped = secret.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&");
15
+ result = result.replace(new RegExp(escaped, "g"), "********");
16
+ }
17
+ }
18
+ return result;
19
+ };
20
+ console.log = (...args) => {
21
+ const redactedArgs = args.map((arg) => {
22
+ if (typeof arg === "string")
23
+ return redact(arg);
24
+ try {
25
+ const str = String(arg);
26
+ const hasSecret = [...secrets].some((s) => s && s.length >= 3 && str.includes(s));
27
+ if (hasSecret)
28
+ return redact(str);
29
+ }
30
+ catch { }
31
+ return arg;
32
+ });
33
+ originalLog(...redactedArgs);
34
+ };
35
+ try {
36
+ return await fn();
37
+ }
38
+ finally {
39
+ console.log = originalLog;
40
+ }
41
+ }
6
42
  const _registry = new Map();
7
43
  function formatEntry(val, parentKey) {
8
44
  const isSensitiveKey = (k) => /password|secret|token|key/i.test(k);
@@ -71,6 +107,45 @@ function printOutputs(stackName, outputs) {
71
107
  }
72
108
  console.log(` └${line}┘`);
73
109
  }
110
+ function printDiff(diff) {
111
+ console.log(`\n🔍 Diff: ${diff.stackName}`);
112
+ const propWidth = Math.max(...diff.resources.map((r) => r.prop.length), 4);
113
+ const nameWidth = Math.max(...diff.resources.map((r) => r.resource.length), 8);
114
+ for (const r of diff.resources) {
115
+ const prop = r.prop.padEnd(propWidth);
116
+ const name = r.resource.padEnd(nameWidth);
117
+ if (r.status === "in-sync") {
118
+ console.log(` ${prop} ${name} ✅ in-sync`);
119
+ }
120
+ else if (r.status === "adopted") {
121
+ console.log(` ${prop} ${name} 🔗 adopted`);
122
+ }
123
+ else if (r.status === "missing") {
124
+ console.log(` ${prop} ${name} ❌ missing (will create)`);
125
+ }
126
+ else {
127
+ console.log(` ${prop} ${name} ⚠️ drift`);
128
+ const fieldWidth = Math.max(...r.changes.map((c) => String(c.field).length), 8);
129
+ for (const c of r.changes) {
130
+ const field = String(c.field).padEnd(fieldWidth);
131
+ console.log(` └─ ${field} ${String(c.declared)} → ${c.live}`);
132
+ }
133
+ }
134
+ }
135
+ const driftCount = diff.resources.filter((r) => r.status === "drift").length;
136
+ const missingCount = diff.resources.filter((r) => r.status === "missing").length;
137
+ if (driftCount === 0 && missingCount === 0) {
138
+ console.log(`\n ✅ All ${diff.resources.length} resources are in sync.`);
139
+ }
140
+ else {
141
+ const parts = [];
142
+ if (driftCount > 0)
143
+ parts.push(`${driftCount} drifted`);
144
+ if (missingCount > 0)
145
+ parts.push(`${missingCount} missing`);
146
+ console.log(`\n ⚠️ ${parts.join(", ")} out of ${diff.resources.length} resources.`);
147
+ }
148
+ }
74
149
  export class Stack {
75
150
  /** @internal - called by @Deploy to register the instance for cross-stack references. */
76
151
  static _register(cls, instance, region) {
@@ -99,53 +174,67 @@ export class Stack {
99
174
  throw new Error(`Stack "${cls.name}" ${region ? `for region "${region}" ` : ""}is not registered. Make sure it is decorated with @Deploy and its module is imported before referencing it.`);
100
175
  return instance;
101
176
  }
177
+ /**
178
+ * Compares every declared resource against its live cloud state without
179
+ * making any API writes. Returns a structured `StackDiff` and prints a
180
+ * formatted report to the console.
181
+ *
182
+ * Field-level drift is surfaced for providers that implement `getDiff()`.
183
+ * Resources with no `getDiff()` override show only existence status
184
+ * (missing / in-sync / adopted).
185
+ */
186
+ async diff() {
187
+ const props = Object.getOwnPropertyNames(this);
188
+ const entries = [];
189
+ for (const prop of props) {
190
+ const val = this[prop];
191
+ if (val instanceof BaseBuilder) {
192
+ entries.push({ prop, resource: val });
193
+ }
194
+ else if (Array.isArray(val)) {
195
+ for (const item of val) {
196
+ if (item instanceof BaseBuilder) {
197
+ entries.push({ prop, resource: item });
198
+ }
199
+ }
200
+ }
201
+ }
202
+ const resources = [];
203
+ for (const { prop, resource } of entries) {
204
+ const existing = await resource._resolveDiscovery();
205
+ let status;
206
+ let changes = resource.getDiff(existing ?? {});
207
+ if (!existing) {
208
+ status = "missing";
209
+ changes = [];
210
+ }
211
+ else if (existing._adopted === true) {
212
+ status = "adopted";
213
+ changes = [];
214
+ }
215
+ else {
216
+ status = changes.length > 0 ? "drift" : "in-sync";
217
+ }
218
+ resources.push({ prop, resource: resource.name, status, changes });
219
+ }
220
+ const hasDrift = resources.some((r) => r.status === "drift" || r.status === "missing");
221
+ const result = { stackName: this.constructor.name, resources, hasDrift };
222
+ printDiff(result);
223
+ return result;
224
+ }
102
225
  async deploy() {
103
226
  const controller = new AbortController();
104
227
  const hosts = [];
228
+ // Snapshot current secrets; new secrets resolved during this run are added via context
229
+ const secrets = new Set(resolvedSecrets);
105
230
  const context = {
106
231
  abortSignal: controller.signal,
107
232
  hosts,
108
- stackName: this.constructor.name
233
+ stackName: this.constructor.name,
234
+ secrets,
109
235
  };
110
236
  return resourceContextStorage.run(context, async () => {
111
- const originalLog = console.log;
112
- console.log = (...args) => {
113
- const redact = (message) => {
114
- if (typeof message !== "string")
115
- return message;
116
- let result = message;
117
- for (const secret of resolvedSecrets) {
118
- if (secret && secret.length >= 3) {
119
- const escaped = secret.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
120
- const regex = new RegExp(escaped, 'g');
121
- result = result.replace(regex, '********');
122
- }
123
- }
124
- return result;
125
- };
126
- const redactedArgs = args.map(arg => {
127
- if (typeof arg === "string") {
128
- return redact(arg);
129
- }
130
- try {
131
- const str = String(arg);
132
- let hasSecret = false;
133
- for (const secret of resolvedSecrets) {
134
- if (secret && secret.length >= 3 && str.includes(secret)) {
135
- hasSecret = true;
136
- break;
137
- }
138
- }
139
- if (hasSecret) {
140
- return redact(str);
141
- }
142
- }
143
- catch { }
144
- return arg;
145
- });
146
- originalLog(...redactedArgs);
147
- };
148
- try {
237
+ return withRedactedConsole(secrets, async () => {
149
238
  console.log(`\n🏗️ Deploying Stack: ${this.constructor.name}`);
150
239
  // Stack-level beforeDeploy hook
151
240
  if (typeof this.beforeDeploy === "function") {
@@ -187,11 +276,13 @@ export class Stack {
187
276
  }
188
277
  // 2. Schedule execution
189
278
  if (isParallel) {
190
- const startPromise = Promise.resolve();
191
279
  const promises = resources.map(({ prop, resource }) => {
192
280
  resource._deployPromise = (async () => {
193
281
  try {
194
- await startPromise;
282
+ // Yield so the map() loop finishes assigning all _deployPromise values before
283
+ // any task checks its dependencies - a dependency that appears later in the list
284
+ // would otherwise have an undefined _deployPromise and be silently skipped.
285
+ await Promise.resolve();
195
286
  if (controller.signal.aborted) {
196
287
  throw new Error("Deployment aborted due to previous failure");
197
288
  }
@@ -287,59 +378,21 @@ export class Stack {
287
378
  await this.afterDeploy(outputs);
288
379
  }
289
380
  return outputs;
290
- }
291
- finally {
292
- console.log = originalLog;
293
- }
381
+ });
294
382
  });
295
383
  }
296
384
  async destroy() {
297
385
  const controller = new AbortController();
298
386
  const hosts = [];
387
+ const secrets = new Set(resolvedSecrets);
299
388
  const context = {
300
389
  abortSignal: controller.signal,
301
390
  hosts,
302
- stackName: this.constructor.name
391
+ stackName: this.constructor.name,
392
+ secrets,
303
393
  };
304
394
  return resourceContextStorage.run(context, async () => {
305
- const originalLog = console.log;
306
- console.log = (...args) => {
307
- const redact = (message) => {
308
- if (typeof message !== "string")
309
- return message;
310
- let result = message;
311
- for (const secret of resolvedSecrets) {
312
- if (secret && secret.length >= 3) {
313
- const escaped = secret.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
314
- const regex = new RegExp(escaped, 'g');
315
- result = result.replace(regex, '********');
316
- }
317
- }
318
- return result;
319
- };
320
- const redactedArgs = args.map(arg => {
321
- if (typeof arg === "string") {
322
- return redact(arg);
323
- }
324
- try {
325
- const str = String(arg);
326
- let hasSecret = false;
327
- for (const secret of resolvedSecrets) {
328
- if (secret && secret.length >= 3 && str.includes(secret)) {
329
- hasSecret = true;
330
- break;
331
- }
332
- }
333
- if (hasSecret) {
334
- return redact(str);
335
- }
336
- }
337
- catch { }
338
- return arg;
339
- });
340
- originalLog(...redactedArgs);
341
- };
342
- try {
395
+ return withRedactedConsole(secrets, async () => {
343
396
  console.log(`\n💥 Tearing down Stack: ${this.constructor.name}`);
344
397
  // Stack-level beforeDestroy hook
345
398
  if (typeof this.beforeDestroy === "function") {
@@ -374,12 +427,13 @@ export class Stack {
374
427
  }
375
428
  // 2. Schedule execution
376
429
  if (isParallel) {
377
- const startPromise = Promise.resolve();
378
430
  // In parallel destroy, await all dependents (reverse dependencies) first
379
431
  const promises = resources.map(({ prop, resource }) => {
380
432
  resource._destroyPromise = (async () => {
381
433
  try {
382
- await startPromise;
434
+ // Yield so the map() loop finishes assigning all _destroyPromise values before
435
+ // any task checks its dependents (same reason as parallel deploy).
436
+ await Promise.resolve();
383
437
  if (controller.signal.aborted) {
384
438
  throw new Error("Teardown aborted due to previous failure");
385
439
  }
@@ -458,10 +512,7 @@ export class Stack {
458
512
  await this.afterDestroy(outputs);
459
513
  }
460
514
  return outputs;
461
- }
462
- finally {
463
- console.log = originalLog;
464
- }
515
+ });
465
516
  });
466
517
  }
467
518
  }
package/dist/index.d.ts CHANGED
@@ -2,7 +2,8 @@ export * from "./core/stack.js";
2
2
  export * from "./core/decorators.js";
3
3
  export * from "./core/checker.js";
4
4
  export * from "./core/resource.js";
5
- export { Secret } from "./core/secret.js";
5
+ export { Secret, clearResolvedSecrets } from "./core/secret.js";
6
6
  export { Output } from "./core/output.js";
7
7
  export * as INVENTORY_TYPES from "./types/inventory.js";
8
+ export type { FieldDiff, ResourceDiff, StackDiff, ResourceStatus } from "./types/diff.js";
8
9
  export { SLACK, DISCORD } from "./core/hooks.js";
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ export * from "./core/stack.js";
2
2
  export * from "./core/decorators.js";
3
3
  export * from "./core/checker.js";
4
4
  export * from "./core/resource.js";
5
- export { Secret } from "./core/secret.js";
5
+ export { Secret, clearResolvedSecrets } from "./core/secret.js";
6
6
  export { Output } from "./core/output.js";
7
7
  export * as INVENTORY_TYPES from "./types/inventory.js";
8
8
  export { SLACK, DISCORD } from "./core/hooks.js";
@@ -60,6 +60,9 @@ function createAwsOfflineMock(command) {
60
60
  get(target, prop) {
61
61
  if (prop === "then")
62
62
  return undefined;
63
+ // Pagination tokens must be undefined so discovery loops exit cleanly
64
+ if (prop === "NextToken" || prop === "NextMarker" || prop === "Marker" || prop === "ContinuationToken")
65
+ return undefined;
63
66
  if (prop === "CertificateArn")
64
67
  return "arn:aws:acm:us-east-1:123456789012:certificate/mock-cert-uuid";
65
68
  if (prop === "HostedZoneId")
@@ -29,6 +29,11 @@ export declare class EC2VMBuilder extends BaseBuilder {
29
29
  sshPrivateKey(path: string): this;
30
30
  provision(...playbookPaths: (string | string[])[]): this;
31
31
  forceConfigCheck(): this;
32
+ getDiff(existing: any): {
33
+ field: string;
34
+ declared: string;
35
+ live: any;
36
+ }[];
32
37
  protected checkPort(ip: string, port: number): Promise<boolean>;
33
38
  protected runProvisioner(ip: string, script: string): Promise<void>;
34
39
  private discoverVM;
@@ -67,6 +67,13 @@ export class EC2VMBuilder extends BaseBuilder {
67
67
  this._forceConfigCheck = true;
68
68
  return this;
69
69
  }
70
+ getDiff(existing) {
71
+ const diffs = [];
72
+ if (existing.InstanceType !== this._instanceType) {
73
+ diffs.push({ field: "instanceType", declared: this._instanceType, live: existing.InstanceType });
74
+ }
75
+ return diffs;
76
+ }
70
77
  async checkPort(ip, port) {
71
78
  return checkPort(ip, port);
72
79
  }
@@ -20,6 +20,11 @@ export declare class LambdaBuilder extends BaseBuilder {
20
20
  timeout(seconds: number): this;
21
21
  role(arnOrBuilder: string | IAMRoleBuilder): this;
22
22
  env(vars: Record<string, string | SecretsBuilder>): this;
23
+ getDiff(existing: any): {
24
+ field: string;
25
+ declared: string;
26
+ live: any;
27
+ }[];
23
28
  private ensureRole;
24
29
  private buildZip;
25
30
  deploy(): Promise<{
@@ -78,6 +78,30 @@ export class LambdaBuilder extends BaseBuilder {
78
78
  this._env = { ...this._env, ...vars };
79
79
  return this;
80
80
  }
81
+ getDiff(existing) {
82
+ const diffs = [];
83
+ if (existing.Runtime !== this._runtime) {
84
+ diffs.push({ field: "runtime", declared: this._runtime, live: existing.Runtime });
85
+ }
86
+ if (existing.Handler !== this._handler) {
87
+ diffs.push({ field: "handler", declared: this._handler, live: existing.Handler });
88
+ }
89
+ if (existing.MemorySize !== this._memory) {
90
+ diffs.push({ field: "memory", declared: `${this._memory} MB`, live: `${existing.MemorySize} MB` });
91
+ }
92
+ if (existing.Timeout !== this._timeout) {
93
+ diffs.push({ field: "timeout", declared: `${this._timeout}s`, live: `${existing.Timeout}s` });
94
+ }
95
+ const liveEnv = existing.Environment?.Variables ?? {};
96
+ const declaredKeys = Object.keys(this._env);
97
+ const liveKeys = Object.keys(liveEnv);
98
+ const allKeys = new Set([...declaredKeys, ...liveKeys]);
99
+ const envDrift = [...allKeys].filter((k) => String(this._env[k]) !== String(liveEnv[k]));
100
+ if (envDrift.length > 0) {
101
+ diffs.push({ field: "env", declared: `${declaredKeys.length} vars`, live: `${liveKeys.length} vars (${envDrift.length} changed)` });
102
+ }
103
+ return diffs;
104
+ }
81
105
  async ensureRole() {
82
106
  if (this._roleBuilder) {
83
107
  return await this._roleBuilder.out.arn.get();
@@ -3,16 +3,18 @@ import { ListBucketsCommand } from '@aws-sdk/client-s3';
3
3
  import { ListFunctionsCommand } from '@aws-sdk/client-lambda';
4
4
  import { DescribeDBInstancesCommand } from '@aws-sdk/client-rds';
5
5
  import { ListHostedZonesCommand } from '@aws-sdk/client-route-53';
6
- import { getCFClient, getS3Client, getLambdaClient, getRDSClient, getR53Client } from './api.js';
6
+ import { DescribeInstancesCommand } from '@aws-sdk/client-ec2';
7
+ import { getCFClient, getS3Client, getLambdaClient, getRDSClient, getR53Client, getEC2Client } from './api.js';
7
8
  import { Config } from '../../core/config.js';
8
9
  export async function listAwsResources() {
9
10
  const region = Config.get().providers.aws.region;
10
- const [cfResult, s3Result, lambdaResult, rdsResult, r53Result] = await Promise.all([
11
+ const [cfResult, s3Result, lambdaResult, rdsResult, r53Result, ec2Result] = await Promise.all([
11
12
  getCFClient().send(new ListDistributionsCommand({})),
12
13
  getS3Client().send(new ListBucketsCommand({})),
13
14
  getLambdaClient().send(new ListFunctionsCommand({ MaxItems: 50 })),
14
15
  getRDSClient().send(new DescribeDBInstancesCommand({})),
15
16
  getR53Client().send(new ListHostedZonesCommand({})),
17
+ getEC2Client().send(new DescribeInstancesCommand({ MaxResults: 200 })),
16
18
  ]);
17
19
  const distributions = (cfResult.DistributionList?.Items ?? []).map((d) => ({
18
20
  id: d.Id,
@@ -40,5 +42,15 @@ export async function listAwsResources() {
40
42
  id: z.Id.replace('/hostedzone/', ''),
41
43
  recordCount: z.ResourceRecordSetCount ?? 0,
42
44
  }));
43
- return { region, distributions, buckets, lambdas, rdsInstances, hostedZones };
45
+ const ec2Instances = (ec2Result.Reservations ?? [])
46
+ .flatMap((r) => r.Instances ?? [])
47
+ .filter((i) => i.State?.Name !== 'terminated')
48
+ .map((i) => ({
49
+ id: i.InstanceId,
50
+ name: i.Tags?.find((t) => t.Key === 'Name')?.Value ?? i.InstanceId,
51
+ type: i.InstanceType ?? 'unknown',
52
+ state: i.State?.Name ?? 'unknown',
53
+ publicIp: i.PublicIpAddress,
54
+ }));
55
+ return { region, distributions, buckets, lambdas, rdsInstances, hostedZones, ec2Instances };
44
56
  }
@@ -24,6 +24,15 @@ export declare class RDSBuilder extends BaseBuilder {
24
24
  subnets(ids: string[]): this;
25
25
  securityGroups(ids: string[]): this;
26
26
  publicAccess(enabled?: boolean): this;
27
+ getDiff(existing: any): ({
28
+ field: string;
29
+ declared: string;
30
+ live: any;
31
+ } | {
32
+ field: string;
33
+ declared: boolean;
34
+ live: any;
35
+ })[];
27
36
  database(name: string): this;
28
37
  credentials(username: string, password: string): this;
29
38
  private discoverInstance;
@@ -54,6 +54,25 @@ export class RDSBuilder extends BaseBuilder {
54
54
  this._publicAccess = enabled;
55
55
  return this;
56
56
  }
57
+ getDiff(existing) {
58
+ const diffs = [];
59
+ if (existing.Engine !== this._engine) {
60
+ diffs.push({ field: "engine", declared: this._engine, live: existing.Engine });
61
+ }
62
+ if (existing.EngineVersion !== this._engineVersion) {
63
+ diffs.push({ field: "engineVersion", declared: this._engineVersion, live: existing.EngineVersion });
64
+ }
65
+ if (existing.DBInstanceClass !== this._instanceClass) {
66
+ diffs.push({ field: "instanceClass", declared: this._instanceClass, live: existing.DBInstanceClass });
67
+ }
68
+ if (existing.AllocatedStorage !== this._storage) {
69
+ diffs.push({ field: "storage", declared: `${this._storage} GB`, live: `${existing.AllocatedStorage} GB` });
70
+ }
71
+ if (existing.PubliclyAccessible !== this._publicAccess) {
72
+ diffs.push({ field: "publicAccess", declared: this._publicAccess, live: existing.PubliclyAccessible });
73
+ }
74
+ return diffs;
75
+ }
57
76
  database(name) {
58
77
  this._dbName = name;
59
78
  return this;
@@ -26,6 +26,15 @@ export declare class DatabaseBuilder extends BaseBuilder {
26
26
  allowIp(cidr: string): this;
27
27
  allowDroplet(dropletId: string): this;
28
28
  allowTag(tagName: string): this;
29
+ getDiff(existing: any): ({
30
+ field: string;
31
+ declared: string;
32
+ live: any;
33
+ } | {
34
+ field: string;
35
+ declared: number;
36
+ live: any;
37
+ })[];
29
38
  private discoverCluster;
30
39
  deploy(): Promise<{
31
40
  name: string;
@@ -58,6 +58,25 @@ export class DatabaseBuilder extends BaseBuilder {
58
58
  this._firewallRules.push({ type: "tag", value: tagName });
59
59
  return this;
60
60
  }
61
+ getDiff(existing) {
62
+ const diffs = [];
63
+ if (existing.engine !== this._engine) {
64
+ diffs.push({ field: "engine", declared: this._engine, live: existing.engine });
65
+ }
66
+ if (existing.version !== this._version) {
67
+ diffs.push({ field: "version", declared: this._version, live: existing.version });
68
+ }
69
+ if (existing.size !== this._size) {
70
+ diffs.push({ field: "size", declared: this._size, live: existing.size });
71
+ }
72
+ if (existing.region !== this._region) {
73
+ diffs.push({ field: "region", declared: this._region, live: existing.region });
74
+ }
75
+ if (existing.num_nodes !== this._nodes) {
76
+ diffs.push({ field: "nodes", declared: this._nodes, live: existing.num_nodes });
77
+ }
78
+ return diffs;
79
+ }
61
80
  async discoverCluster(name) {
62
81
  try {
63
82
  const api = getDoApi();
@@ -104,7 +104,7 @@ export class DomainBuilder extends BaseBuilder {
104
104
  if (existing) {
105
105
  try {
106
106
  const res = await api.get(`/domains/${this.domainName}/records?per_page=200`);
107
- existingRecords = res.domain_records;
107
+ existingRecords = res.domain_records ?? [];
108
108
  }
109
109
  catch {
110
110
  existingRecords = [];
@@ -12,6 +12,7 @@ export declare class DropletBuilder extends BaseBuilder {
12
12
  private dropletId?;
13
13
  private resolvedIp?;
14
14
  private sshKeyPath?;
15
+ private _sshUser?;
15
16
  private _provision;
16
17
  private _forceConfigCheck;
17
18
  constructor(name: string);
@@ -21,10 +22,19 @@ export declare class DropletBuilder extends BaseBuilder {
21
22
  image(image: (typeof OS)[keyof typeof OS] | string): this;
22
23
  region(region: (typeof REGION)[keyof typeof REGION] | string): this;
23
24
  size(size: (typeof SIZE)[keyof typeof SIZE] | string): this;
25
+ sshKey(keyPath: string): this;
26
+ /** @deprecated Use `.sshKey()` instead */
24
27
  sslKey(keyPath: string): this;
28
+ sshUser(user: string): this;
29
+ private resolveUser;
25
30
  vpc(uuid: string | Output<string>): this;
26
31
  provision(...playbookPaths: (string | string[])[]): this;
27
32
  forceConfigCheck(): this;
33
+ getDiff(existing: any): {
34
+ field: string;
35
+ declared: any;
36
+ live: any;
37
+ }[];
28
38
  protected checkPort(ip: string, port: number): Promise<boolean>;
29
39
  protected runProvisioner(ip: string, script: string): Promise<void>;
30
40
  private resolveOrRegisterSshKey;