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
package/LICENSE ADDED
@@ -0,0 +1,7 @@
1
+ ISC License (ISC)
2
+
3
+ Copyright (c) 2026, Pulsdev.io
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,148 @@
1
+ # Puls-dev
2
+
3
+ Intent-driven infrastructure-as-code. Describe what you want — Puls figures out create, update, or skip.
4
+
5
+ ```typescript
6
+ @Deploy({ proxmox: CONFIG.STAGING })
7
+ class GameInfra extends Stack {
8
+ server = Proxmox.VM("example-vm")
9
+ .image(OS.UBUNTU_24_04)
10
+ .cores(4).memory(8192)
11
+ .ip("1.1.1.1").vlan(2010)
12
+ .sshKey(KEYS)
13
+ .provision("config/default.yaml");
14
+ }
15
+ ```
16
+
17
+ No state files. No plan step. Runs against real APIs — idempotent by default.
18
+
19
+ ---
20
+
21
+ ## How it works
22
+
23
+ Puls uses **eager discovery**: the moment you declare a resource, it checks the real API in the background. By the time `deploy()` runs, it already knows the current state.
24
+
25
+ ```
26
+ Declare resource → Discovery fires immediately (async)
27
+ → You chain config (.cores(), .ip(), ...)
28
+ → deploy() awaits discovery, diffs, acts
29
+ ```
30
+
31
+ Running the same stack twice is always safe — existing resources are detected and skipped.
32
+
33
+ ---
34
+
35
+ ## Providers
36
+
37
+ | Provider | Resources |
38
+ |----------|-----------|
39
+ | [DigitalOcean](docs/providers/digitalocean.md) | Droplet, Domain, Firewall, Certificate, LoadBalancer |
40
+ | [AWS](docs/providers/aws.md) | Route53, ACM (wildcard SSL), CloudFront, S3 |
41
+ | [Proxmox](docs/providers/proxmox.md) | VM (clone, cloud-init, provision, replace) |
42
+
43
+ ---
44
+
45
+ ## Quick examples
46
+
47
+ ### DigitalOcean
48
+
49
+ ```typescript
50
+ import { DO, DO_TYPES, Stack, Deploy } from "puls";
51
+ const { SIZE, REGION } = DO_TYPES;
52
+
53
+ @Deploy({ token: process.env.DO_TOKEN! })
54
+ class Production extends Stack {
55
+ web = DO.Droplet("prod-web").size(SIZE.MEDIUM).region(REGION.FRA).allowPublicWeb();
56
+ dns = DO.Domain("example.com").pointer("@", this.web).withSSL();
57
+ }
58
+ ```
59
+
60
+ ### AWS
61
+
62
+ ```typescript
63
+ import { AWS, AWS_TYPES, Stack, Deploy } from "puls";
64
+ const { DISTRO, BUCKET, DOMAIN_REGISTER, REGION } = AWS_TYPES;
65
+
66
+ @Deploy({ region: REGION.US_EAST_1 })
67
+ class CDNStack extends Stack {
68
+ domain = AWS.Route53().randomDomain().register(DOMAIN_REGISTER).withWildcardSSL();
69
+
70
+ cdn = AWS.CloudFront(`CDN-${this.domain.zoneName.slice(0, 8)}`)
71
+ .copyFrom(DISTRO.TURKEY_CDN)
72
+ .forDomain(this.domain, ["ec", "nc"]);
73
+
74
+ bucket = AWS.S3(BUCKET.NLC_GAMES_UREG)
75
+ .allowFrom(this.cdn)
76
+ .region(REGION.EU_WEST_1);
77
+ }
78
+ ```
79
+
80
+ ### Proxmox
81
+
82
+ ```typescript
83
+ import { Proxmox, PROXMOX_TYPES, Stack, Deploy, Protected } from "puls";
84
+ const { CONFIG, OS, KEYS } = PROXMOX_TYPES;
85
+
86
+ @Deploy({ proxmox: CONFIG.STAGING })
87
+ class StagingInfra extends Stack {
88
+ @Protected
89
+ db = Proxmox.VM("ix-sto1-db01")
90
+ .image(OS.UBUNTU_24_04)
91
+ .cores(2).memory(4096)
92
+ .ip("1.1.1.1").vlan(2010)
93
+ .sshKey(KEYS);
94
+
95
+ app = Proxmox.VM("ix-sto1-app01")
96
+ .image(OS.UBUNTU_24_04)
97
+ .cores(4).memory(8192)
98
+ .ip("1.1.1.1").vlan(2010)
99
+ .sshKey(KEYS)
100
+ .provision("config/default.yaml");
101
+ }
102
+ ```
103
+
104
+ ---
105
+
106
+ ## Decorators
107
+
108
+ | Decorator | Effect |
109
+ |-----------|--------|
110
+ | `@Deploy({ ... })` | Deploy all resources in the stack |
111
+ | `@Deploy({ dryRun: true })` | Print plan without making changes |
112
+ | `@Destroy` | Tear down all resources in the stack |
113
+ | `@Destroy({ proxmox: CONFIG.STAGING })` | Tear down with provider credentials |
114
+ | `@DryRun` | Shorthand for `@Deploy({ dryRun: true })` |
115
+ | `@Protected` (property) | Block changes/destruction of that resource |
116
+
117
+ See [docs/decorators.md](docs/decorators.md) for full reference.
118
+
119
+ ---
120
+
121
+ ## Running
122
+
123
+ ```bash
124
+ npm install
125
+ npx tsx your-stack.ts
126
+ ```
127
+
128
+ Requires Node 20+.
129
+
130
+ **.env**
131
+ ```
132
+ # DigitalOcean
133
+ DO_TOKEN=
134
+
135
+ # AWS (standard SDK env vars)
136
+ AWS_ACCESS_KEY_ID=
137
+ AWS_SECRET_ACCESS_KEY=
138
+ AWS_REGION=us-east-1
139
+
140
+ # Proxmox
141
+ PROXMOX_URL=https://pve.example.com:8006
142
+ PROXMOX_USER=root@pam
143
+ PROXMOX_TOKEN_NAME=puls
144
+ PROXMOX_TOKEN_SECRET=some-super-secret
145
+ PROXMOX_NODES=pve1,pve2
146
+ PROXMOX_DNS_DOMAIN=nolimit.int
147
+ PROXMOX_DNS_SERVERS=1.1.1.1,2.2.2.2
148
+ ```
@@ -0,0 +1,5 @@
1
+ import "reflect-metadata";
2
+ import type { InventoryResult } from '../types/inventory.ts';
3
+ export declare abstract class Checker {
4
+ check(): Promise<InventoryResult>;
5
+ }
@@ -0,0 +1,148 @@
1
+ import "reflect-metadata";
2
+ import { Config } from './config.js';
3
+ import { listProxmoxVMs } from '../providers/proxmox/list.js';
4
+ import { listDoResources } from '../providers/do/list.js';
5
+ import { listAwsResources } from '../providers/aws/list.js';
6
+ function clip(text, width) {
7
+ return text.length > width ? text.slice(0, width - 1) + '…' : text.padEnd(width);
8
+ }
9
+ function printSection(title, rows, cols) {
10
+ const colsWidth = cols.reduce((s, c) => s + c.width, 0) + (cols.length - 1) * 2;
11
+ const innerWidth = Math.max(colsWidth + 4, title.length + 4);
12
+ const bar = (l, r) => ` ${l}${'─'.repeat(innerWidth)}${r}`;
13
+ const row = (content) => ` │ ${content.padEnd(innerWidth - 4)} │`;
14
+ console.log('');
15
+ console.log(bar('ā”Œ', '┐'));
16
+ console.log(row(title));
17
+ console.log(bar('ā”œ', '┤'));
18
+ if (rows.length === 0) {
19
+ console.log(row('(none)'));
20
+ }
21
+ else {
22
+ const headerLine = cols.map((c) => clip(c.header.toUpperCase(), c.width)).join(' ');
23
+ console.log(row(headerLine));
24
+ console.log(bar('ā”œ', '┤'));
25
+ for (const r of rows) {
26
+ console.log(row(cols.map((c) => clip(c.render(r), c.width)).join(' ')));
27
+ }
28
+ }
29
+ console.log(bar('ā””', 'ā”˜'));
30
+ }
31
+ // ─── Per-provider renderers ───────────────────────────────────────────────────
32
+ function renderProxmox(inv) {
33
+ const running = inv.vms.filter((v) => v.status === 'running').length;
34
+ printSection(`Proxmox Ā· ${inv.vms.length} VM${inv.vms.length !== 1 ? 's' : ''} (${running} running)`, inv.vms, [
35
+ { header: 'Name', width: 26, render: (v) => v.name },
36
+ { header: 'VMID', width: 6, render: (v) => String(v.vmid) },
37
+ { header: 'Node', width: 12, render: (v) => v.node },
38
+ { header: 'Status', width: 8, render: (v) => v.status },
39
+ { header: 'Mem', width: 6, render: (v) => `${Math.round(v.maxmem / 1024 ** 3)}GB` },
40
+ { header: 'Disk', width: 6, render: (v) => `${Math.round(v.maxdisk / 1024 ** 3)}GB` },
41
+ ]);
42
+ }
43
+ function renderDo(inv) {
44
+ const costStr = inv.totalMonthlyCost > 0 ? ` Ā· $${inv.totalMonthlyCost}/mo` : '';
45
+ printSection(`DigitalOcean Droplets Ā· ${inv.droplets.length}${costStr}`, inv.droplets, [
46
+ { header: 'Name', width: 24, render: (d) => d.name },
47
+ { header: 'Region', width: 6, render: (d) => d.region },
48
+ { header: 'Size', width: 18, render: (d) => d.size },
49
+ { header: 'Status', width: 8, render: (d) => d.status },
50
+ { header: 'IP', width: 15, render: (d) => d.ip ?? '—' },
51
+ { header: '$/mo', width: 5, render: (d) => d.monthlyCost > 0 ? `$${d.monthlyCost}` : '?' },
52
+ ]);
53
+ if (inv.firewalls.length > 0) {
54
+ printSection(`DigitalOcean Firewalls Ā· ${inv.firewalls.length}`, inv.firewalls, [
55
+ { header: 'Name', width: 32, render: (f) => f.name },
56
+ { header: 'Droplets', width: 8, render: (f) => String(f.dropletCount) },
57
+ ]);
58
+ }
59
+ if (inv.loadBalancers.length > 0) {
60
+ printSection(`DigitalOcean Load Balancers Ā· ${inv.loadBalancers.length}`, inv.loadBalancers, [
61
+ { header: 'Name', width: 24, render: (lb) => lb.name },
62
+ { header: 'Region', width: 6, render: (lb) => lb.region },
63
+ { header: 'IP', width: 15, render: (lb) => lb.ip },
64
+ { header: 'Status', width: 8, render: (lb) => lb.status },
65
+ ]);
66
+ }
67
+ if (inv.domains.length > 0) {
68
+ printSection(`DigitalOcean Domains Ā· ${inv.domains.length}`, inv.domains, [
69
+ { header: 'Domain', width: 42, render: (d) => d.name },
70
+ { header: 'TTL', width: 6, render: (d) => String(d.ttl) },
71
+ ]);
72
+ }
73
+ }
74
+ function renderAws(inv) {
75
+ if (inv.distributions.length > 0) {
76
+ printSection(`AWS CloudFront Ā· ${inv.distributions.length} Ā· ${inv.region}`, inv.distributions, [
77
+ { header: 'ID', width: 14, render: (d) => d.id },
78
+ { header: 'Domain', width: 34, render: (d) => d.aliases[0] ?? d.domain },
79
+ { header: 'Status', width: 10, render: (d) => d.status },
80
+ ]);
81
+ }
82
+ if (inv.buckets.length > 0) {
83
+ printSection(`AWS S3 Ā· ${inv.buckets.length} buckets`, inv.buckets, [
84
+ { header: 'Bucket', width: 52, render: (b) => b.name },
85
+ ]);
86
+ }
87
+ if (inv.lambdas.length > 0) {
88
+ printSection(`AWS Lambda Ā· ${inv.lambdas.length} functions`, inv.lambdas, [
89
+ { header: 'Function', width: 32, render: (f) => f.name },
90
+ { header: 'Runtime', width: 12, render: (f) => f.runtime },
91
+ { header: 'Memory', width: 8, render: (f) => `${f.memorySizeMb}MB` },
92
+ ]);
93
+ }
94
+ if (inv.rdsInstances.length > 0) {
95
+ printSection(`AWS RDS Ā· ${inv.rdsInstances.length} instances`, inv.rdsInstances, [
96
+ { header: 'Identifier', width: 26, render: (i) => i.identifier },
97
+ { header: 'Engine', width: 18, render: (i) => i.engine },
98
+ { header: 'Class', width: 14, render: (i) => i.instanceClass },
99
+ { header: 'Status', width: 10, render: (i) => i.status },
100
+ ]);
101
+ }
102
+ if (inv.hostedZones.length > 0) {
103
+ printSection(`AWS Route53 Ā· ${inv.hostedZones.length} zones`, inv.hostedZones, [
104
+ { header: 'Zone', width: 38, render: (z) => z.name },
105
+ { header: 'Records', width: 7, render: (z) => String(z.recordCount) },
106
+ ]);
107
+ }
108
+ }
109
+ // ─── Checker ──────────────────────────────────────────────────────────────────
110
+ export class Checker {
111
+ async check() {
112
+ const cfg = Config.get();
113
+ const errors = [];
114
+ const result = { errors };
115
+ const tasks = [];
116
+ console.log(`\nšŸ” Checking infrastructure...`);
117
+ if (cfg.providers.proxmox?.url && cfg.providers.proxmox?.tokenSecret) {
118
+ tasks.push(listProxmoxVMs()
119
+ .then((inv) => { result.proxmox = inv; })
120
+ .catch((err) => { errors.push({ provider: 'proxmox', message: err.message }); }));
121
+ }
122
+ if (cfg.providers.do?.token) {
123
+ tasks.push(listDoResources()
124
+ .then((inv) => { result.do = inv; })
125
+ .catch((err) => { errors.push({ provider: 'do', message: err.message }); }));
126
+ }
127
+ if (cfg.providers.aws?.region) {
128
+ tasks.push(listAwsResources()
129
+ .then((inv) => { result.aws = inv; })
130
+ .catch((err) => { errors.push({ provider: 'aws', message: err.message }); }));
131
+ }
132
+ await Promise.all(tasks);
133
+ if (result.proxmox)
134
+ renderProxmox(result.proxmox);
135
+ if (result.do)
136
+ renderDo(result.do);
137
+ if (result.aws)
138
+ renderAws(result.aws);
139
+ for (const e of errors) {
140
+ console.warn(`\n [!] ${e.provider}: ${e.message}`);
141
+ }
142
+ const totalCost = result.do?.totalMonthlyCost ?? 0;
143
+ if (totalCost > 0) {
144
+ console.log(`\n Total estimated monthly cost (DigitalOcean): $${totalCost}`);
145
+ }
146
+ return result;
147
+ }
148
+ }
@@ -0,0 +1,35 @@
1
+ export interface GlobalConfig {
2
+ dryRun?: boolean;
3
+ providers: {
4
+ do?: {
5
+ token: string;
6
+ defaultRegion?: string;
7
+ };
8
+ aws?: {
9
+ region: string;
10
+ };
11
+ proxmox?: {
12
+ url: string;
13
+ user: string;
14
+ tokenName: string;
15
+ tokenSecret: string;
16
+ nodes?: string[];
17
+ storage?: string;
18
+ dnsDomain?: string;
19
+ dnsServers?: string[];
20
+ verifySsl?: boolean;
21
+ };
22
+ firebase?: {
23
+ projectId: string;
24
+ serviceAccountPath: string;
25
+ };
26
+ };
27
+ }
28
+ declare class ConfigManager {
29
+ private config;
30
+ set(newConfig: Partial<GlobalConfig>): void;
31
+ get(): GlobalConfig;
32
+ isGlobalDryRun(): boolean;
33
+ }
34
+ export declare const Config: ConfigManager;
35
+ export {};
@@ -0,0 +1,15 @@
1
+ class ConfigManager {
2
+ config = {
3
+ providers: {},
4
+ };
5
+ set(newConfig) {
6
+ this.config = { ...this.config, ...newConfig };
7
+ }
8
+ get() {
9
+ return this.config;
10
+ }
11
+ isGlobalDryRun() {
12
+ return this.config.dryRun ?? false;
13
+ }
14
+ }
15
+ export const Config = new ConfigManager();
@@ -0,0 +1,26 @@
1
+ import "reflect-metadata";
2
+ type ProviderOpts = {
3
+ token?: string;
4
+ region?: string;
5
+ dryRun?: boolean;
6
+ firebase?: string;
7
+ proxmox?: {
8
+ url: string;
9
+ user: string;
10
+ tokenName: string;
11
+ tokenSecret: string;
12
+ nodes?: string[];
13
+ storage?: string;
14
+ dnsDomain?: string;
15
+ dnsServers?: string[];
16
+ verifySsl?: boolean;
17
+ };
18
+ };
19
+ export declare function Protected(target: any, propertyKey: string): void;
20
+ export declare function Destroy(target: any, propertyKey: string): void;
21
+ export declare function Destroy(target: Function): void;
22
+ export declare function Destroy(opts: ProviderOpts): (constructor: any) => void;
23
+ export declare function Deploy(opts?: ProviderOpts): (constructor: any) => void;
24
+ export declare function Check(opts?: ProviderOpts): (constructor: any) => void;
25
+ export declare function DryRun(opts?: ProviderOpts | any): ((constructor: any) => void) | undefined;
26
+ export {};
@@ -0,0 +1,86 @@
1
+ import "reflect-metadata";
2
+ import { readFileSync } from "node:fs";
3
+ import { Config } from "./config.js";
4
+ import { Stack } from "./stack.js";
5
+ function applyConfig(opts) {
6
+ if (opts.dryRun !== undefined)
7
+ Config.set({ dryRun: opts.dryRun });
8
+ if (opts.token)
9
+ Config.set({
10
+ providers: { ...Config.get().providers, do: { token: opts.token } },
11
+ });
12
+ if (opts.region)
13
+ Config.set({
14
+ providers: { ...Config.get().providers, aws: { region: opts.region } },
15
+ });
16
+ if (opts.proxmox)
17
+ Config.set({
18
+ providers: { ...Config.get().providers, proxmox: opts.proxmox },
19
+ });
20
+ if (opts.firebase) {
21
+ const sa = JSON.parse(readFileSync(opts.firebase, "utf8"));
22
+ Config.set({
23
+ providers: { ...Config.get().providers, firebase: { projectId: sa.project_id, serviceAccountPath: opts.firebase } },
24
+ });
25
+ }
26
+ }
27
+ export function Protected(target, propertyKey) {
28
+ Reflect.defineMetadata("protected", true, target, propertyKey);
29
+ }
30
+ export function Destroy(optsOrTarget, propertyKey) {
31
+ if (propertyKey !== undefined) {
32
+ Reflect.defineMetadata("destroy", true, optsOrTarget, propertyKey);
33
+ return;
34
+ }
35
+ if (typeof optsOrTarget === "function") {
36
+ const instance = new optsOrTarget();
37
+ Stack._register(optsOrTarget, instance);
38
+ Promise.resolve().then(async () => {
39
+ if (typeof instance.destroy === "function")
40
+ await instance.destroy();
41
+ });
42
+ return;
43
+ }
44
+ return function (constructor) {
45
+ applyConfig(optsOrTarget);
46
+ const instance = new constructor();
47
+ Stack._register(constructor, instance);
48
+ Promise.resolve().then(async () => {
49
+ if (typeof instance.destroy === "function")
50
+ await instance.destroy();
51
+ });
52
+ };
53
+ }
54
+ // THE "MAGIC": Auto-executing Stack Decorator
55
+ export function Deploy(opts = {}) {
56
+ return function (constructor) {
57
+ applyConfig(opts);
58
+ // Instantiate synchronously so resource discovery kicks off immediately and
59
+ // other stacks can call Stack.from(ThisClass) to reference its Output fields.
60
+ const instance = new constructor();
61
+ Stack._register(constructor, instance);
62
+ Promise.resolve().then(async () => {
63
+ if (typeof instance.deploy === "function")
64
+ await instance.deploy();
65
+ });
66
+ };
67
+ }
68
+ export function Check(opts = {}) {
69
+ return function (constructor) {
70
+ applyConfig(opts);
71
+ const instance = new constructor();
72
+ Promise.resolve().then(async () => {
73
+ if (typeof instance.check === "function")
74
+ await instance.check();
75
+ });
76
+ };
77
+ }
78
+ // Shortcut for Dry Run — accepts the same options as @Deploy
79
+ export function DryRun(opts = {}) {
80
+ if (typeof opts === "function") {
81
+ Deploy({ dryRun: true })(opts);
82
+ }
83
+ else {
84
+ return Deploy({ ...opts, dryRun: true });
85
+ }
86
+ }
@@ -0,0 +1,8 @@
1
+ export declare class Output<T> {
2
+ private _promise;
3
+ private _resolve;
4
+ constructor();
5
+ resolve(value: T): void;
6
+ get(): Promise<T>;
7
+ apply<U>(fn: (val: T) => U): Output<U>;
8
+ }
@@ -0,0 +1,19 @@
1
+ export class Output {
2
+ _promise;
3
+ _resolve;
4
+ constructor() {
5
+ this._promise = new Promise(resolve => (this._resolve = resolve));
6
+ }
7
+ resolve(value) {
8
+ this._resolve(value);
9
+ }
10
+ get() {
11
+ return this._promise;
12
+ }
13
+ // Transform this output into a new Output<U> without awaiting it yourself.
14
+ apply(fn) {
15
+ const out = new Output();
16
+ this._promise.then(v => out.resolve(fn(v)));
17
+ return out;
18
+ }
19
+ }
@@ -0,0 +1,20 @@
1
+ export declare abstract class BaseBuilder {
2
+ name: string;
3
+ protected isProtected: boolean;
4
+ protected localDryRun: boolean | null;
5
+ protected discoveryPromise: Promise<any>;
6
+ protected sidecars: BaseBuilder[];
7
+ constructor(name: string);
8
+ protect(): this;
9
+ dryRun(enabled?: boolean): this;
10
+ protected isDryRunActive(): boolean;
11
+ protected checkProtection(hasChanges: boolean): Promise<boolean>;
12
+ protected waitFor(label: string, condition: () => Promise<boolean>, opts?: {
13
+ intervalMs?: number;
14
+ timeoutMs?: number;
15
+ }): Promise<void>;
16
+ protected deploySidecars(): Promise<void>;
17
+ protected destroySidecars(): Promise<void>;
18
+ destroy(): Promise<any>;
19
+ abstract deploy(): Promise<any>;
20
+ }
@@ -0,0 +1,77 @@
1
+ import { Config } from "./config.js";
2
+ export class BaseBuilder {
3
+ name;
4
+ isProtected = false;
5
+ localDryRun = null;
6
+ discoveryPromise;
7
+ sidecars = [];
8
+ constructor(name) {
9
+ this.name = name;
10
+ }
11
+ protect() {
12
+ this.isProtected = true;
13
+ return this;
14
+ }
15
+ dryRun(enabled = true) {
16
+ this.localDryRun = enabled;
17
+ return this;
18
+ }
19
+ isDryRunActive() {
20
+ return this.localDryRun !== null
21
+ ? this.localDryRun
22
+ : Config.isGlobalDryRun();
23
+ }
24
+ async checkProtection(hasChanges) {
25
+ if (this.isProtected && hasChanges) {
26
+ console.error(`\nšŸ›‘ [CRITICAL] Resource "${this.name}" is PROTECTED.`);
27
+ console.error(` Refusing to apply changes to a protected resource.`);
28
+ console.error(` Please remove .protect() if you intentionally want to modify this.`);
29
+ return true;
30
+ }
31
+ return false;
32
+ }
33
+ // Waits for a long-running cloud operation to complete.
34
+ // In dry-run mode: skips entirely — no waiting.
35
+ // In real mode: polls via the provided condition fn until it returns true.
36
+ // The mock fallback simulates a realistic delay with progress output.
37
+ async waitFor(label, condition, opts = {}) {
38
+ if (this.isDryRunActive()) {
39
+ console.log(` ā­ļø [PLAN] Would wait for: ${label}`);
40
+ return;
41
+ }
42
+ const intervalMs = opts.intervalMs ?? 5000;
43
+ const timeoutMs = opts.timeoutMs ?? 600_000; // 10 min default
44
+ const started = Date.now();
45
+ process.stdout.write(` ā³ Waiting for ${label}`);
46
+ while (true) {
47
+ const done = await condition();
48
+ if (done) {
49
+ console.log(` āœ…`);
50
+ return;
51
+ }
52
+ if (Date.now() - started > timeoutMs) {
53
+ console.log(` āŒ`);
54
+ throw new Error(`Timed out waiting for: ${label}`);
55
+ }
56
+ process.stdout.write(`.`);
57
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
58
+ }
59
+ }
60
+ async deploySidecars() {
61
+ for (const sidecar of this.sidecars) {
62
+ await sidecar.deploy();
63
+ }
64
+ }
65
+ async destroySidecars() {
66
+ for (const sidecar of [...this.sidecars].reverse()) {
67
+ await sidecar.destroy();
68
+ }
69
+ }
70
+ async destroy() {
71
+ const dryRun = this.isDryRunActive();
72
+ console.log(`\nšŸ—‘ļø Destroying "${this.name}"...`);
73
+ console.log(` āœ… [${dryRun ? 'PLAN' : 'OK'}] Resource "${this.name}" marked for destruction.`);
74
+ await this.destroySidecars();
75
+ return { destroyed: this.name };
76
+ }
77
+ }
@@ -0,0 +1,20 @@
1
+ import "reflect-metadata";
2
+ export declare abstract class Stack {
3
+ /** @internal — called by @Deploy to register the instance for cross-stack references. */
4
+ static _register(cls: Function, instance: Stack): void;
5
+ /**
6
+ * Returns the already-constructed instance of another Stack so you can reference
7
+ * its resource Output fields before deployment completes.
8
+ *
9
+ * The target stack must be decorated with @Deploy and imported before this call.
10
+ *
11
+ * @example
12
+ * class DNSStack extends Stack {
13
+ * private infra = Stack.from(InfraStack);
14
+ * dns = DO.Domain("example.com").pointer("app", this.infra.app.ip);
15
+ * }
16
+ */
17
+ static from<T extends Stack>(cls: new (...args: any[]) => T): T;
18
+ deploy(): Promise<Record<string, any>>;
19
+ destroy(): Promise<Record<string, any>>;
20
+ }