puls-dev 0.2.6 → 0.2.8

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 (83) hide show
  1. package/README.md +1 -1
  2. package/dist/core/config.d.ts +2 -0
  3. package/dist/core/decorators.d.ts +2 -0
  4. package/dist/core/decorators.js +48 -16
  5. package/dist/core/hooks.d.ts +21 -0
  6. package/dist/core/hooks.js +116 -0
  7. package/dist/core/hooks.test.d.ts +1 -0
  8. package/dist/core/hooks.test.js +194 -0
  9. package/dist/core/multiregion.test.d.ts +1 -0
  10. package/dist/core/multiregion.test.js +87 -0
  11. package/dist/core/output.d.ts +2 -0
  12. package/dist/core/output.js +9 -2
  13. package/dist/core/parser.d.ts +10 -0
  14. package/dist/core/parser.js +140 -0
  15. package/dist/core/parser.test.d.ts +1 -0
  16. package/dist/core/parser.test.js +117 -0
  17. package/dist/core/provisioner.d.ts +4 -0
  18. package/dist/core/provisioner.js +105 -0
  19. package/dist/core/resource.d.ts +16 -0
  20. package/dist/core/resource.js +44 -0
  21. package/dist/core/secret.d.ts +40 -0
  22. package/dist/core/secret.js +95 -0
  23. package/dist/core/secret.test.d.ts +1 -0
  24. package/dist/core/secret.test.js +166 -0
  25. package/dist/core/stack.d.ts +4 -3
  26. package/dist/core/stack.js +50 -9
  27. package/dist/index.d.ts +2 -0
  28. package/dist/index.js +2 -0
  29. package/dist/providers/aws/ec2.d.ts +48 -0
  30. package/dist/providers/aws/ec2.js +297 -0
  31. package/dist/providers/aws/ec2.test.d.ts +1 -0
  32. package/dist/providers/aws/ec2.test.js +279 -0
  33. package/dist/providers/aws/index.d.ts +2 -0
  34. package/dist/providers/aws/index.js +2 -0
  35. package/dist/providers/aws/route53.d.ts +1 -0
  36. package/dist/providers/aws/route53.js +15 -2
  37. package/dist/providers/aws/route53.test.js +47 -0
  38. package/dist/providers/do/api.d.ts +1 -1
  39. package/dist/providers/do/api.js +2 -1
  40. package/dist/providers/do/app.d.ts +26 -0
  41. package/dist/providers/do/app.js +124 -0
  42. package/dist/providers/do/app.test.d.ts +1 -0
  43. package/dist/providers/do/app.test.js +268 -0
  44. package/dist/providers/do/database.d.ts +44 -0
  45. package/dist/providers/do/database.js +208 -0
  46. package/dist/providers/do/database.test.d.ts +1 -0
  47. package/dist/providers/do/database.test.js +293 -0
  48. package/dist/providers/do/domain.d.ts +2 -0
  49. package/dist/providers/do/domain.js +30 -0
  50. package/dist/providers/do/domain.test.js +49 -0
  51. package/dist/providers/do/droplet.d.ts +9 -0
  52. package/dist/providers/do/droplet.js +132 -8
  53. package/dist/providers/do/droplet.test.js +228 -1
  54. package/dist/providers/do/firewall.d.ts +2 -1
  55. package/dist/providers/do/firewall.js +23 -9
  56. package/dist/providers/do/firewall.test.js +54 -0
  57. package/dist/providers/do/index.d.ts +11 -0
  58. package/dist/providers/do/index.js +8 -0
  59. package/dist/providers/do/spaces.d.ts +27 -0
  60. package/dist/providers/do/spaces.js +142 -0
  61. package/dist/providers/do/spaces.test.d.ts +1 -0
  62. package/dist/providers/do/spaces.test.js +180 -0
  63. package/dist/providers/do/spaces_api.d.ts +2 -0
  64. package/dist/providers/do/spaces_api.js +20 -0
  65. package/dist/providers/do/vpc.d.ts +30 -0
  66. package/dist/providers/do/vpc.js +128 -0
  67. package/dist/providers/do/vpc.test.d.ts +1 -0
  68. package/dist/providers/do/vpc.test.js +258 -0
  69. package/dist/providers/gcp/clouddns.d.ts +1 -0
  70. package/dist/providers/gcp/clouddns.js +15 -2
  71. package/dist/providers/gcp/clouddns.test.js +45 -0
  72. package/dist/providers/gcp/index.d.ts +3 -1
  73. package/dist/providers/gcp/index.js +3 -1
  74. package/dist/providers/gcp/vm.d.ts +45 -0
  75. package/dist/providers/gcp/vm.js +332 -0
  76. package/dist/providers/gcp/vm.test.d.ts +1 -0
  77. package/dist/providers/gcp/vm.test.js +321 -0
  78. package/dist/providers/proxmox/hash.d.ts +3 -0
  79. package/dist/providers/proxmox/hash.js +46 -0
  80. package/dist/providers/proxmox/vm.d.ts +8 -7
  81. package/dist/providers/proxmox/vm.js +126 -106
  82. package/dist/providers/proxmox/vm.test.js +224 -0
  83. package/package.json +3 -1
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  **Intent-driven infrastructure-as-code. Describe what you want - Puls figures out create, update, or skip.**
4
4
 
5
- [Live Documentation](https://pulsdev.io/) | Discord: **pulsdev.io** ([Join](https://discord.gg/9PcwyjADZj))
5
+ [Live Documentation](https://pulsdev.io/) | Matrix|Gitter: **pulsdev.io** ([Join](https://matrix.to/#/#pulsdevio:gitter.im))
6
6
 
7
7
  > [!IMPORTANT]
8
8
  > **Active Pre-1.0 Development**
@@ -4,6 +4,8 @@ export interface GlobalConfig {
4
4
  do?: {
5
5
  token: string;
6
6
  defaultRegion?: string;
7
+ spacesAccessKey?: string;
8
+ spacesSecretKey?: string;
7
9
  };
8
10
  aws?: {
9
11
  region: string;
@@ -2,6 +2,7 @@ import "reflect-metadata";
2
2
  type ProviderOpts = {
3
3
  token?: string;
4
4
  region?: string;
5
+ regions?: string[];
5
6
  dryRun?: boolean;
6
7
  firebase?: string;
7
8
  proxmox?: {
@@ -17,6 +18,7 @@ type ProviderOpts = {
17
18
  };
18
19
  };
19
20
  export declare function Protected(target: any, propertyKey: string): void;
21
+ export declare function ForceConfigCheck(target: any, propertyKey: string): void;
20
22
  export declare function Destroy(target: any, propertyKey: string): void;
21
23
  export declare function Destroy(target: Function): void;
22
24
  export declare function Destroy(opts: ProviderOpts): (constructor: any) => void;
@@ -33,6 +33,9 @@ function applyConfig(opts) {
33
33
  export function Protected(target, propertyKey) {
34
34
  Reflect.defineMetadata("protected", true, target, propertyKey);
35
35
  }
36
+ export function ForceConfigCheck(target, propertyKey) {
37
+ Reflect.defineMetadata("forceConfigCheck", true, target, propertyKey);
38
+ }
36
39
  export function Destroy(optsOrTarget, propertyKey) {
37
40
  if (propertyKey !== undefined) {
38
41
  Reflect.defineMetadata("destroy", true, optsOrTarget, propertyKey);
@@ -48,27 +51,56 @@ export function Destroy(optsOrTarget, propertyKey) {
48
51
  return;
49
52
  }
50
53
  return function (constructor) {
51
- applyConfig(optsOrTarget);
52
- const instance = new constructor();
53
- Stack._register(constructor, instance);
54
- Promise.resolve().then(async () => {
55
- if (typeof instance.destroy === "function")
56
- await instance.destroy();
57
- });
54
+ const regions = optsOrTarget.regions ?? [];
55
+ if (regions.length > 0) {
56
+ Promise.resolve().then(async () => {
57
+ for (const r of regions) {
58
+ console.log(`\n🌍 [MULTI-REGION] Tearing down stack in region: ${r}`);
59
+ applyConfig({ ...optsOrTarget, region: r });
60
+ const instance = new constructor();
61
+ Stack._register(constructor, instance, r);
62
+ if (typeof instance.destroy === "function")
63
+ await instance.destroy();
64
+ }
65
+ });
66
+ }
67
+ else {
68
+ applyConfig(optsOrTarget);
69
+ const instance = new constructor();
70
+ Stack._register(constructor, instance);
71
+ Promise.resolve().then(async () => {
72
+ if (typeof instance.destroy === "function")
73
+ await instance.destroy();
74
+ });
75
+ }
58
76
  };
59
77
  }
60
78
  // THE "MAGIC": Auto-executing Stack Decorator
61
79
  export function Deploy(opts = {}) {
62
80
  return function (constructor) {
63
- applyConfig(opts);
64
- // Instantiate synchronously so resource discovery kicks off immediately and
65
- // other stacks can call Stack.from(ThisClass) to reference its Output fields.
66
- const instance = new constructor();
67
- Stack._register(constructor, instance);
68
- Promise.resolve().then(async () => {
69
- if (typeof instance.deploy === "function")
70
- await instance.deploy();
71
- });
81
+ const regions = opts.regions ?? [];
82
+ if (regions.length > 0) {
83
+ Promise.resolve().then(async () => {
84
+ for (const r of regions) {
85
+ console.log(`\n🌍 [MULTI-REGION] Deploying stack to region: ${r}`);
86
+ applyConfig({ ...opts, region: r });
87
+ const instance = new constructor();
88
+ Stack._register(constructor, instance, r);
89
+ if (typeof instance.deploy === "function") {
90
+ await instance.deploy();
91
+ }
92
+ }
93
+ });
94
+ }
95
+ else {
96
+ applyConfig(opts);
97
+ const instance = new constructor();
98
+ Stack._register(constructor, instance);
99
+ Promise.resolve().then(async () => {
100
+ if (typeof instance.deploy === "function")
101
+ await instance.deploy();
102
+ });
103
+ }
72
104
  };
73
105
  }
74
106
  export function Check(opts = {}) {
@@ -0,0 +1,21 @@
1
+ export declare const SLACK: {
2
+ /**
3
+ * Generates a callback that posts a structured lifecycle notification to a Slack webhook.
4
+ * In dry-run mode, it outputs a descriptive log to the terminal instead of executing HTTP writes.
5
+ *
6
+ * @param webhookUrl The Slack Incoming Webhook URL
7
+ */
8
+ notify: (webhookUrl: string) => (result: any) => Promise<void>;
9
+ };
10
+ export declare const DISCORD: {
11
+ /**
12
+ * Generates a callback that posts a rich embed lifecycle notification to a Discord webhook.
13
+ * In dry-run mode, it outputs a descriptive log to the terminal instead of executing HTTP writes.
14
+ *
15
+ * @param webhookUrl The Discord Incoming Webhook URL
16
+ * @example
17
+ * GCP.CloudRun("api")
18
+ * .afterDeploy(DISCORD.notify("https://discord.com/api/webhooks/..."))
19
+ */
20
+ notify: (webhookUrl: string) => (result: any) => Promise<void>;
21
+ };
@@ -0,0 +1,116 @@
1
+ import { Config } from "./config.js";
2
+ export const SLACK = {
3
+ /**
4
+ * Generates a callback that posts a structured lifecycle notification to a Slack webhook.
5
+ * In dry-run mode, it outputs a descriptive log to the terminal instead of executing HTTP writes.
6
+ *
7
+ * @param webhookUrl The Slack Incoming Webhook URL
8
+ */
9
+ notify: (webhookUrl) => {
10
+ return async (result) => {
11
+ const name = result?.name ?? "unknown-resource";
12
+ const isDryRun = Config.isGlobalDryRun();
13
+ // Check if it's a destroy result or deploy result
14
+ const isDestroy = result && ("destroyed" in result || result.destroyed === true);
15
+ const action = isDestroy ? "destroyed" : "deployed/updated";
16
+ const statusEmoji = isDestroy ? "🗑️" : "🚀";
17
+ // Filter and print simple key-value attributes
18
+ const details = Object.entries(result ?? {})
19
+ .filter(([k, v]) => k !== "name" &&
20
+ k !== "destroyed" &&
21
+ (typeof v === "string" || typeof v === "number" || typeof v === "boolean"))
22
+ .map(([k, v]) => `• *${k}*: \`${v}\``)
23
+ .join("\n");
24
+ const text = `${statusEmoji} *Puls Notification*: Resource *${name}* was successfully *${action}*!\n${details ? `*Details*:\n${details}` : ""}`;
25
+ if (isDryRun) {
26
+ console.log(`\n📢 [DRY RUN] Would post Slack notification to webhook: ${webhookUrl}`);
27
+ console.log(` Message: ${text.replace(/\n/g, "\n ")}`);
28
+ return;
29
+ }
30
+ try {
31
+ const res = await fetch(webhookUrl, {
32
+ method: "POST",
33
+ headers: { "Content-Type": "application/json" },
34
+ body: JSON.stringify({ text }),
35
+ });
36
+ if (!res.ok) {
37
+ console.warn(`[WARN] Slack webhook returned status ${res.status}`);
38
+ }
39
+ }
40
+ catch (err) {
41
+ console.error(`[ERROR] Failed to send Slack notification: ${err.message}`);
42
+ }
43
+ };
44
+ },
45
+ };
46
+ export const DISCORD = {
47
+ /**
48
+ * Generates a callback that posts a rich embed lifecycle notification to a Discord webhook.
49
+ * In dry-run mode, it outputs a descriptive log to the terminal instead of executing HTTP writes.
50
+ *
51
+ * @param webhookUrl The Discord Incoming Webhook URL
52
+ * @example
53
+ * GCP.CloudRun("api")
54
+ * .afterDeploy(DISCORD.notify("https://discord.com/api/webhooks/..."))
55
+ */
56
+ notify: (webhookUrl) => {
57
+ return async (result) => {
58
+ const name = result?.name ?? "unknown-resource";
59
+ const isDryRun = Config.isGlobalDryRun();
60
+ // Check if it's a destroy result or deploy result
61
+ const isDestroy = result && ("destroyed" in result || result.destroyed === true);
62
+ const action = isDestroy ? "destroyed" : "deployed/updated";
63
+ const statusEmoji = isDestroy ? "🗑️" : "🚀";
64
+ // Green (0x2ecc71) for deploy, Red (0xe74c3c) for destroy
65
+ const color = isDestroy ? 15158332 : 3066993;
66
+ const fields = Object.entries(result ?? {})
67
+ .filter(([k, v]) => k !== "name" &&
68
+ k !== "destroyed" &&
69
+ (typeof v === "string" || typeof v === "number" || typeof v === "boolean"))
70
+ .map(([k, v]) => ({
71
+ name: k,
72
+ value: `\`${v}\``,
73
+ inline: true,
74
+ }));
75
+ const payload = {
76
+ embeds: [
77
+ {
78
+ title: `${statusEmoji} Puls Notification`,
79
+ description: `Resource **${name}** was successfully **${action}**!`,
80
+ color,
81
+ fields,
82
+ footer: {
83
+ text: "Puls Dev Suite",
84
+ },
85
+ timestamp: new Date().toISOString(),
86
+ },
87
+ ],
88
+ };
89
+ if (isDryRun) {
90
+ console.log(`\n📢 [DRY RUN] Would post Discord notification to webhook: ${webhookUrl}`);
91
+ console.log(` Embed Title: ${statusEmoji} Puls Notification`);
92
+ console.log(` Embed Description: Resource **${name}** was successfully **${action}**!`);
93
+ if (fields.length > 0) {
94
+ console.log(" Fields:");
95
+ for (const f of fields) {
96
+ console.log(` • ${f.name}: ${f.value}`);
97
+ }
98
+ }
99
+ return;
100
+ }
101
+ try {
102
+ const res = await fetch(webhookUrl, {
103
+ method: "POST",
104
+ headers: { "Content-Type": "application/json" },
105
+ body: JSON.stringify(payload),
106
+ });
107
+ if (!res.ok) {
108
+ console.warn(`[WARN] Discord webhook returned status ${res.status}`);
109
+ }
110
+ }
111
+ catch (err) {
112
+ console.error(`[ERROR] Failed to send Discord notification: ${err.message}`);
113
+ }
114
+ };
115
+ },
116
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,194 @@
1
+ import { test, describe, beforeEach } from "node:test";
2
+ import assert from "node:assert";
3
+ import { Stack } from "./stack.js";
4
+ import { BaseBuilder } from "./resource.js";
5
+ import { Config } from "./config.js";
6
+ import { SLACK, DISCORD } from "./hooks.js";
7
+ // A minimal dummy resource builder for testing hooks
8
+ class TestResource extends BaseBuilder {
9
+ resultValue;
10
+ constructor(name, resultValue = { name: "test-res", ip: "1.2.3.4" }) {
11
+ super(name);
12
+ this.resultValue = resultValue;
13
+ }
14
+ async deploy() {
15
+ return this.resultValue;
16
+ }
17
+ async destroy() {
18
+ return { name: this.name, destroyed: true };
19
+ }
20
+ }
21
+ describe("Lifecycle Hooks & Notifier Tests", () => {
22
+ let executionLogs = [];
23
+ beforeEach(() => {
24
+ Config.set({
25
+ dryRun: false,
26
+ providers: {},
27
+ });
28
+ executionLogs = [];
29
+ });
30
+ test("runs Stack-level and Resource-level deploy hooks in the correct order", async () => {
31
+ class MyStack extends Stack {
32
+ async beforeDeploy() {
33
+ executionLogs.push("stack-before-deploy");
34
+ }
35
+ async afterDeploy(outputs) {
36
+ executionLogs.push(`stack-after-deploy:${outputs.res.ip}`);
37
+ }
38
+ res = new TestResource("my-res", { name: "my-res", ip: "9.9.9.9" })
39
+ .beforeDeploy(() => {
40
+ executionLogs.push("res-before-deploy");
41
+ })
42
+ .afterDeploy((result) => {
43
+ executionLogs.push(`res-after-deploy:${result.ip}`);
44
+ });
45
+ }
46
+ const stack = new MyStack();
47
+ const outputs = await stack.deploy();
48
+ // Verify ordering
49
+ assert.deepStrictEqual(executionLogs, [
50
+ "stack-before-deploy",
51
+ "res-before-deploy",
52
+ "res-after-deploy:9.9.9.9",
53
+ "stack-after-deploy:9.9.9.9",
54
+ ]);
55
+ assert.deepStrictEqual(outputs.res, { name: "my-res", ip: "9.9.9.9" });
56
+ });
57
+ test("runs Stack-level and Resource-level destroy hooks in the correct order", async () => {
58
+ class MyStack extends Stack {
59
+ async beforeDestroy() {
60
+ executionLogs.push("stack-before-destroy");
61
+ }
62
+ async afterDestroy(outputs) {
63
+ executionLogs.push(`stack-after-destroy:${outputs.res.destroyed}`);
64
+ }
65
+ res = new TestResource("my-res")
66
+ .beforeDestroy(() => {
67
+ executionLogs.push("res-before-destroy");
68
+ })
69
+ .afterDestroy((result) => {
70
+ executionLogs.push(`res-after-destroy:${result.destroyed}`);
71
+ });
72
+ }
73
+ const stack = new MyStack();
74
+ const outputs = await stack.destroy();
75
+ // Verify ordering (destroy properties are iterated in reverse order, which doesn't affect single resource)
76
+ assert.deepStrictEqual(executionLogs, [
77
+ "stack-before-destroy",
78
+ "res-before-destroy",
79
+ "res-after-destroy:true",
80
+ "stack-after-destroy:true",
81
+ ]);
82
+ assert.deepStrictEqual(outputs.res, { name: "my-res", destroyed: true });
83
+ });
84
+ test("SLACK.notify helper sends fetch call on real deploys", async () => {
85
+ const originalFetch = globalThis.fetch;
86
+ let fetchPayload = null;
87
+ let fetchUrl = "";
88
+ globalThis.fetch = (async (url, init) => {
89
+ fetchUrl = url;
90
+ fetchPayload = JSON.parse(init?.body);
91
+ return {
92
+ ok: true,
93
+ status: 200,
94
+ };
95
+ });
96
+ try {
97
+ const webhook = "https://hooks.slack.com/services/T00/B00/X00";
98
+ const notifyHook = SLACK.notify(webhook);
99
+ const deployResult = { name: "web-app", url: "https://web.run.app", ip: "1.2.3.4" };
100
+ await notifyHook(deployResult);
101
+ assert.strictEqual(fetchUrl, webhook);
102
+ assert.ok(fetchPayload);
103
+ assert.ok(fetchPayload.text.includes("🚀"));
104
+ assert.ok(fetchPayload.text.includes("web-app"));
105
+ assert.ok(fetchPayload.text.includes("https://web.run.app"));
106
+ assert.ok(fetchPayload.text.includes("1.2.3.4"));
107
+ }
108
+ finally {
109
+ globalThis.fetch = originalFetch;
110
+ }
111
+ });
112
+ test("SLACK.notify helper formats text and logs to console on dry-runs", async () => {
113
+ Config.set({
114
+ dryRun: true,
115
+ providers: {},
116
+ });
117
+ const originalLog = console.log;
118
+ let logOutput = "";
119
+ console.log = (...args) => {
120
+ logOutput += args.join(" ") + "\n";
121
+ };
122
+ try {
123
+ const notifyHook = SLACK.notify("https://hooks.slack.com/services/T00");
124
+ const destroyResult = { name: "old-db", destroyed: true };
125
+ await notifyHook(destroyResult);
126
+ assert.ok(logOutput.includes("📢 [DRY RUN]"));
127
+ assert.ok(logOutput.includes("Would post Slack notification"));
128
+ assert.ok(logOutput.includes("🗑️"));
129
+ assert.ok(logOutput.includes("old-db"));
130
+ assert.ok(logOutput.includes("destroyed"));
131
+ }
132
+ finally {
133
+ console.log = originalLog;
134
+ }
135
+ });
136
+ test("DISCORD.notify helper sends fetch call with rich embed on real deploys", async () => {
137
+ const originalFetch = globalThis.fetch;
138
+ let fetchPayload = null;
139
+ let fetchUrl = "";
140
+ globalThis.fetch = (async (url, init) => {
141
+ fetchUrl = url;
142
+ fetchPayload = JSON.parse(init?.body);
143
+ return {
144
+ ok: true,
145
+ status: 200,
146
+ };
147
+ });
148
+ try {
149
+ const webhook = "https://discord.com/api/webhooks/123/abc";
150
+ const notifyHook = DISCORD.notify(webhook);
151
+ const deployResult = { name: "api-srv", url: "https://api.run.app", active: true };
152
+ await notifyHook(deployResult);
153
+ assert.strictEqual(fetchUrl, webhook);
154
+ assert.ok(fetchPayload);
155
+ assert.ok(fetchPayload.embeds);
156
+ assert.strictEqual(fetchPayload.embeds.length, 1);
157
+ const embed = fetchPayload.embeds[0];
158
+ assert.strictEqual(embed.title, "🚀 Puls Notification");
159
+ assert.ok(embed.description.includes("api-srv"));
160
+ assert.ok(embed.description.includes("deployed/updated"));
161
+ assert.strictEqual(embed.color, 3066993); // Green
162
+ assert.strictEqual(embed.fields.length, 2);
163
+ assert.deepStrictEqual(embed.fields[0], { name: "url", value: "`https://api.run.app`", inline: true });
164
+ assert.deepStrictEqual(embed.fields[1], { name: "active", value: "`true`", inline: true });
165
+ }
166
+ finally {
167
+ globalThis.fetch = originalFetch;
168
+ }
169
+ });
170
+ test("DISCORD.notify helper formats embeds and logs to console on dry-runs", async () => {
171
+ Config.set({
172
+ dryRun: true,
173
+ providers: {},
174
+ });
175
+ const originalLog = console.log;
176
+ let logOutput = "";
177
+ console.log = (...args) => {
178
+ logOutput += args.join(" ") + "\n";
179
+ };
180
+ try {
181
+ const notifyHook = DISCORD.notify("https://discord.com/api/webhooks/123");
182
+ const destroyResult = { name: "old-cache", destroyed: true };
183
+ await notifyHook(destroyResult);
184
+ assert.ok(logOutput.includes("📢 [DRY RUN]"));
185
+ assert.ok(logOutput.includes("Would post Discord notification"));
186
+ assert.ok(logOutput.includes("🗑️"));
187
+ assert.ok(logOutput.includes("old-cache"));
188
+ assert.ok(logOutput.includes("destroyed"));
189
+ }
190
+ finally {
191
+ console.log = originalLog;
192
+ }
193
+ });
194
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,87 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { test, describe, beforeEach } from "node:test";
8
+ import assert from "node:assert";
9
+ import { Stack } from "./stack.js";
10
+ import { Deploy, Destroy } from "./decorators.js";
11
+ import { BaseBuilder } from "./resource.js";
12
+ import { Config } from "./config.js";
13
+ import { Output } from "./output.js";
14
+ class RegionResource extends BaseBuilder {
15
+ out = {
16
+ region: new Output(),
17
+ };
18
+ async deploy() {
19
+ const activeRegion = Config.get().providers.aws?.region ?? "unknown";
20
+ this.out.region.resolve(activeRegion);
21
+ return { name: this.name, region: activeRegion };
22
+ }
23
+ async destroy() {
24
+ const activeRegion = Config.get().providers.aws?.region ?? "unknown";
25
+ return { name: this.name, destroyed: true, region: activeRegion };
26
+ }
27
+ }
28
+ describe("Multi-Region Deployments Unit Tests", () => {
29
+ let executionLogs = [];
30
+ beforeEach(() => {
31
+ Config.set({
32
+ dryRun: false,
33
+ providers: {},
34
+ });
35
+ executionLogs = [];
36
+ });
37
+ test("runs sequential deployments across multiple regions and stores instances in registry", async () => {
38
+ let MultiStack = class MultiStack extends Stack {
39
+ res = new RegionResource("my-region-res").afterDeploy((result) => {
40
+ executionLogs.push(`deploy:${result.region}`);
41
+ });
42
+ };
43
+ MultiStack = __decorate([
44
+ Deploy({
45
+ regions: ["us-east-1", "eu-central-1", "ap-northeast-1"],
46
+ dryRun: false,
47
+ })
48
+ ], MultiStack);
49
+ // Wait for the asynchronous macro/microtask queue to resolve Deploy runs
50
+ await new Promise((resolve) => setTimeout(resolve, 100));
51
+ // Verify sequential region changes occurred in correct order
52
+ assert.deepStrictEqual(executionLogs, [
53
+ "deploy:us-east-1",
54
+ "deploy:eu-central-1",
55
+ "deploy:ap-northeast-1",
56
+ ]);
57
+ // Retrieve specific region stack outputs via Stack.from(cls, region)
58
+ const usStack = Stack.from(MultiStack, "us-east-1");
59
+ const euStack = Stack.from(MultiStack, "eu-central-1");
60
+ const apStack = Stack.from(MultiStack, "ap-northeast-1");
61
+ assert.ok(usStack);
62
+ assert.ok(euStack);
63
+ assert.ok(apStack);
64
+ assert.strictEqual(await usStack.res.out.region.get(), "us-east-1");
65
+ assert.strictEqual(await euStack.res.out.region.get(), "eu-central-1");
66
+ assert.strictEqual(await apStack.res.out.region.get(), "ap-northeast-1");
67
+ });
68
+ test("runs sequential teardowns across multiple regions on @Destroy", async () => {
69
+ let CleanStack = class CleanStack extends Stack {
70
+ res = new RegionResource("my-teardown-res").afterDestroy((result) => {
71
+ executionLogs.push(`destroy:${result.region}`);
72
+ });
73
+ };
74
+ CleanStack = __decorate([
75
+ Destroy({
76
+ regions: ["us-east-1", "eu-central-1"],
77
+ dryRun: false,
78
+ })
79
+ ], CleanStack);
80
+ // Wait for microtask queue to run Destroy
81
+ await new Promise((resolve) => setTimeout(resolve, 100));
82
+ assert.deepStrictEqual(executionLogs, [
83
+ "destroy:us-east-1",
84
+ "destroy:eu-central-1",
85
+ ]);
86
+ });
87
+ });
@@ -1,8 +1,10 @@
1
1
  export declare class Output<T> {
2
2
  private _promise;
3
3
  private _resolve;
4
+ private _reject;
4
5
  constructor();
5
6
  resolve(value: T): void;
7
+ reject(reason: any): void;
6
8
  get(): Promise<T>;
7
9
  apply<U>(fn: (val: T) => U): Output<U>;
8
10
  }
@@ -1,19 +1,26 @@
1
1
  export class Output {
2
2
  _promise;
3
3
  _resolve;
4
+ _reject;
4
5
  constructor() {
5
- this._promise = new Promise(resolve => (this._resolve = resolve));
6
+ this._promise = new Promise((resolve, reject) => {
7
+ this._resolve = resolve;
8
+ this._reject = reject;
9
+ });
6
10
  }
7
11
  resolve(value) {
8
12
  this._resolve(value);
9
13
  }
14
+ reject(reason) {
15
+ this._reject(reason);
16
+ }
10
17
  get() {
11
18
  return this._promise;
12
19
  }
13
20
  // Transform this output into a new Output<U> without awaiting it yourself.
14
21
  apply(fn) {
15
22
  const out = new Output();
16
- this._promise.then(v => out.resolve(fn(v)));
23
+ this._promise.then(v => out.resolve(fn(v)), err => out.reject(err));
17
24
  return out;
18
25
  }
19
26
  }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Robust, zero-dependency, indentation-aware YAML parser.
3
+ * Parses sequences of key-value maps and nested string arrays.
4
+ */
5
+ export declare function parseYaml(content: string): any[];
6
+ /**
7
+ * Resolves a file path relative to the current working directory,
8
+ * reads its content, and parses it according to its extension (.json vs .yaml/.yml).
9
+ */
10
+ export declare function loadRecordsFromFile(filePath: string): any[];