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.
- package/LICENSE +7 -0
- package/README.md +148 -0
- package/dist/core/checker.d.ts +5 -0
- package/dist/core/checker.js +148 -0
- package/dist/core/config.d.ts +35 -0
- package/dist/core/config.js +15 -0
- package/dist/core/decorators.d.ts +26 -0
- package/dist/core/decorators.js +86 -0
- package/dist/core/output.d.ts +8 -0
- package/dist/core/output.js +19 -0
- package/dist/core/resource.d.ts +20 -0
- package/dist/core/resource.js +77 -0
- package/dist/core/stack.d.ts +20 -0
- package/dist/core/stack.js +120 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +12 -0
- package/dist/providers/aws/acm.d.ts +22 -0
- package/dist/providers/aws/acm.js +109 -0
- package/dist/providers/aws/api.d.ts +28 -0
- package/dist/providers/aws/api.js +36 -0
- package/dist/providers/aws/apigateway.d.ts +24 -0
- package/dist/providers/aws/apigateway.js +157 -0
- package/dist/providers/aws/cloudfront.d.ts +31 -0
- package/dist/providers/aws/cloudfront.js +205 -0
- package/dist/providers/aws/fargate.d.ts +43 -0
- package/dist/providers/aws/fargate.js +277 -0
- package/dist/providers/aws/index.d.ts +23 -0
- package/dist/providers/aws/index.js +29 -0
- package/dist/providers/aws/lambda.d.ts +30 -0
- package/dist/providers/aws/lambda.js +159 -0
- package/dist/providers/aws/list.d.ts +2 -0
- package/dist/providers/aws/list.js +44 -0
- package/dist/providers/aws/rds.d.ts +46 -0
- package/dist/providers/aws/rds.js +227 -0
- package/dist/providers/aws/route53.d.ts +38 -0
- package/dist/providers/aws/route53.js +218 -0
- package/dist/providers/aws/s3.d.ts +20 -0
- package/dist/providers/aws/s3.js +165 -0
- package/dist/providers/aws/secrets.d.ts +25 -0
- package/dist/providers/aws/secrets.js +151 -0
- package/dist/providers/aws/sqs.d.ts +33 -0
- package/dist/providers/aws/sqs.js +178 -0
- package/dist/providers/do/api.d.ts +11 -0
- package/dist/providers/do/api.js +52 -0
- package/dist/providers/do/certificate.d.ts +7 -0
- package/dist/providers/do/certificate.js +36 -0
- package/dist/providers/do/domain.d.ts +21 -0
- package/dist/providers/do/domain.js +81 -0
- package/dist/providers/do/droplet.d.ts +35 -0
- package/dist/providers/do/droplet.js +180 -0
- package/dist/providers/do/firewall.d.ts +23 -0
- package/dist/providers/do/firewall.js +94 -0
- package/dist/providers/do/index.d.ts +15 -0
- package/dist/providers/do/index.js +21 -0
- package/dist/providers/do/list.d.ts +2 -0
- package/dist/providers/do/list.js +59 -0
- package/dist/providers/do/load_balancer.d.ts +12 -0
- package/dist/providers/do/load_balancer.js +62 -0
- package/dist/providers/firebase/api.d.ts +4 -0
- package/dist/providers/firebase/api.js +62 -0
- package/dist/providers/firebase/auth.d.ts +35 -0
- package/dist/providers/firebase/auth.js +147 -0
- package/dist/providers/firebase/firestore.d.ts +28 -0
- package/dist/providers/firebase/firestore.js +120 -0
- package/dist/providers/firebase/functions.d.ts +50 -0
- package/dist/providers/firebase/functions.js +163 -0
- package/dist/providers/firebase/hosting.d.ts +14 -0
- package/dist/providers/firebase/hosting.js +144 -0
- package/dist/providers/firebase/index.d.ts +15 -0
- package/dist/providers/firebase/index.js +15 -0
- package/dist/providers/firebase/remoteconfig.d.ts +22 -0
- package/dist/providers/firebase/remoteconfig.js +135 -0
- package/dist/providers/firebase/storage.d.ts +34 -0
- package/dist/providers/firebase/storage.js +117 -0
- package/dist/providers/proxmox/api.d.ts +12 -0
- package/dist/providers/proxmox/api.js +50 -0
- package/dist/providers/proxmox/index.d.ts +15 -0
- package/dist/providers/proxmox/index.js +10 -0
- package/dist/providers/proxmox/list.d.ts +2 -0
- package/dist/providers/proxmox/list.js +15 -0
- package/dist/providers/proxmox/vm.d.ts +61 -0
- package/dist/providers/proxmox/vm.js +482 -0
- package/dist/types/aws.d.ts +55 -0
- package/dist/types/aws.js +48 -0
- package/dist/types/do.d.ts +19 -0
- package/dist/types/do.js +19 -0
- package/dist/types/gcp.d.ts +9 -0
- package/dist/types/gcp.js +9 -0
- package/dist/types/inventory.d.ts +87 -0
- package/dist/types/inventory.js +2 -0
- package/dist/types/proxmox.d.ts +11 -0
- package/dist/types/proxmox.js +28 -0
- 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,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,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
|
+
}
|