puls-dev 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/LICENSE +7 -0
  2. package/README.md +148 -0
  3. package/dist/core/checker.d.ts +5 -0
  4. package/dist/core/checker.js +148 -0
  5. package/dist/core/config.d.ts +35 -0
  6. package/dist/core/config.js +15 -0
  7. package/dist/core/decorators.d.ts +26 -0
  8. package/dist/core/decorators.js +86 -0
  9. package/dist/core/output.d.ts +8 -0
  10. package/dist/core/output.js +19 -0
  11. package/dist/core/resource.d.ts +20 -0
  12. package/dist/core/resource.js +77 -0
  13. package/dist/core/stack.d.ts +20 -0
  14. package/dist/core/stack.js +120 -0
  15. package/dist/index.d.ts +12 -0
  16. package/dist/index.js +12 -0
  17. package/dist/providers/aws/acm.d.ts +22 -0
  18. package/dist/providers/aws/acm.js +109 -0
  19. package/dist/providers/aws/api.d.ts +28 -0
  20. package/dist/providers/aws/api.js +36 -0
  21. package/dist/providers/aws/apigateway.d.ts +24 -0
  22. package/dist/providers/aws/apigateway.js +157 -0
  23. package/dist/providers/aws/cloudfront.d.ts +31 -0
  24. package/dist/providers/aws/cloudfront.js +205 -0
  25. package/dist/providers/aws/fargate.d.ts +43 -0
  26. package/dist/providers/aws/fargate.js +277 -0
  27. package/dist/providers/aws/index.d.ts +23 -0
  28. package/dist/providers/aws/index.js +29 -0
  29. package/dist/providers/aws/lambda.d.ts +30 -0
  30. package/dist/providers/aws/lambda.js +159 -0
  31. package/dist/providers/aws/list.d.ts +2 -0
  32. package/dist/providers/aws/list.js +44 -0
  33. package/dist/providers/aws/rds.d.ts +46 -0
  34. package/dist/providers/aws/rds.js +227 -0
  35. package/dist/providers/aws/route53.d.ts +38 -0
  36. package/dist/providers/aws/route53.js +218 -0
  37. package/dist/providers/aws/s3.d.ts +20 -0
  38. package/dist/providers/aws/s3.js +165 -0
  39. package/dist/providers/aws/secrets.d.ts +25 -0
  40. package/dist/providers/aws/secrets.js +151 -0
  41. package/dist/providers/aws/sqs.d.ts +33 -0
  42. package/dist/providers/aws/sqs.js +178 -0
  43. package/dist/providers/do/api.d.ts +11 -0
  44. package/dist/providers/do/api.js +52 -0
  45. package/dist/providers/do/certificate.d.ts +7 -0
  46. package/dist/providers/do/certificate.js +36 -0
  47. package/dist/providers/do/domain.d.ts +21 -0
  48. package/dist/providers/do/domain.js +81 -0
  49. package/dist/providers/do/droplet.d.ts +35 -0
  50. package/dist/providers/do/droplet.js +180 -0
  51. package/dist/providers/do/firewall.d.ts +23 -0
  52. package/dist/providers/do/firewall.js +94 -0
  53. package/dist/providers/do/index.d.ts +15 -0
  54. package/dist/providers/do/index.js +21 -0
  55. package/dist/providers/do/list.d.ts +2 -0
  56. package/dist/providers/do/list.js +59 -0
  57. package/dist/providers/do/load_balancer.d.ts +12 -0
  58. package/dist/providers/do/load_balancer.js +62 -0
  59. package/dist/providers/firebase/api.d.ts +4 -0
  60. package/dist/providers/firebase/api.js +62 -0
  61. package/dist/providers/firebase/auth.d.ts +35 -0
  62. package/dist/providers/firebase/auth.js +147 -0
  63. package/dist/providers/firebase/firestore.d.ts +28 -0
  64. package/dist/providers/firebase/firestore.js +120 -0
  65. package/dist/providers/firebase/functions.d.ts +50 -0
  66. package/dist/providers/firebase/functions.js +163 -0
  67. package/dist/providers/firebase/hosting.d.ts +14 -0
  68. package/dist/providers/firebase/hosting.js +144 -0
  69. package/dist/providers/firebase/index.d.ts +15 -0
  70. package/dist/providers/firebase/index.js +15 -0
  71. package/dist/providers/firebase/remoteconfig.d.ts +22 -0
  72. package/dist/providers/firebase/remoteconfig.js +135 -0
  73. package/dist/providers/firebase/storage.d.ts +34 -0
  74. package/dist/providers/firebase/storage.js +117 -0
  75. package/dist/providers/proxmox/api.d.ts +12 -0
  76. package/dist/providers/proxmox/api.js +50 -0
  77. package/dist/providers/proxmox/index.d.ts +15 -0
  78. package/dist/providers/proxmox/index.js +10 -0
  79. package/dist/providers/proxmox/list.d.ts +2 -0
  80. package/dist/providers/proxmox/list.js +15 -0
  81. package/dist/providers/proxmox/vm.d.ts +61 -0
  82. package/dist/providers/proxmox/vm.js +482 -0
  83. package/dist/types/aws.d.ts +55 -0
  84. package/dist/types/aws.js +48 -0
  85. package/dist/types/do.d.ts +19 -0
  86. package/dist/types/do.js +19 -0
  87. package/dist/types/gcp.d.ts +9 -0
  88. package/dist/types/gcp.js +9 -0
  89. package/dist/types/inventory.d.ts +87 -0
  90. package/dist/types/inventory.js +2 -0
  91. package/dist/types/proxmox.d.ts +11 -0
  92. package/dist/types/proxmox.js +28 -0
  93. package/package.json +56 -0
@@ -0,0 +1,178 @@
1
+ import { GetQueueUrlCommand, GetQueueAttributesCommand, CreateQueueCommand, SetQueueAttributesCommand, DeleteQueueCommand, QueueAttributeName, } from '@aws-sdk/client-sqs';
2
+ import { BaseBuilder } from '../../core/resource.js';
3
+ import { getSQSClient } from './api.js';
4
+ export class SQSBuilder extends BaseBuilder {
5
+ _fifo = false;
6
+ _deduplication = false;
7
+ _visibilityTimeout = 30;
8
+ _retentionDays = 4;
9
+ _delaySeconds = 0;
10
+ _dlqName;
11
+ _dlqMaxReceives = 3;
12
+ resolvedUrl = null;
13
+ resolvedArn = null;
14
+ resolvedDlqUrl = null;
15
+ resolvedDlqArn = null;
16
+ constructor(name) {
17
+ super(name);
18
+ this.discoveryPromise = this.discoverQueue(name);
19
+ }
20
+ fifo(enabled = true) { this._fifo = enabled; return this; }
21
+ deduplication(enabled = true) { this._deduplication = enabled; return this; }
22
+ timeout(seconds) { this._visibilityTimeout = seconds; return this; }
23
+ retention(days) { this._retentionDays = days; return this; }
24
+ delay(seconds) { this._delaySeconds = seconds; return this; }
25
+ dlq(name, maxReceives = 3) {
26
+ this._dlqName = name;
27
+ this._dlqMaxReceives = maxReceives;
28
+ return this;
29
+ }
30
+ queueName() {
31
+ const base = this.name;
32
+ if (this._fifo && !base.endsWith('.fifo'))
33
+ return `${base}.fifo`;
34
+ return base;
35
+ }
36
+ async discoverQueue(name) {
37
+ const sqs = getSQSClient();
38
+ const qName = this._fifo && !name.endsWith('.fifo') ? `${name}.fifo` : name;
39
+ try {
40
+ const urlResult = await sqs.send(new GetQueueUrlCommand({ QueueName: qName }));
41
+ this.resolvedUrl = urlResult.QueueUrl;
42
+ const attrResult = await sqs.send(new GetQueueAttributesCommand({
43
+ QueueUrl: this.resolvedUrl,
44
+ AttributeNames: [QueueAttributeName.QueueArn],
45
+ }));
46
+ this.resolvedArn = attrResult.Attributes?.QueueArn ?? null;
47
+ return { url: this.resolvedUrl, arn: this.resolvedArn };
48
+ }
49
+ catch (e) {
50
+ if (e.name === 'QueueDoesNotExist' || e.name === 'AWS.SimpleQueueService.NonExistentQueue')
51
+ return null;
52
+ if (e.name === 'CredentialsProviderError')
53
+ return null;
54
+ throw e;
55
+ }
56
+ }
57
+ async ensureQueue(queueName, attrs) {
58
+ const sqs = getSQSClient();
59
+ try {
60
+ const urlResult = await sqs.send(new GetQueueUrlCommand({ QueueName: queueName }));
61
+ const url = urlResult.QueueUrl;
62
+ const attrResult = await sqs.send(new GetQueueAttributesCommand({
63
+ QueueUrl: url,
64
+ AttributeNames: [QueueAttributeName.QueueArn],
65
+ }));
66
+ return { url, arn: attrResult.Attributes?.QueueArn };
67
+ }
68
+ catch (e) {
69
+ if (e.name !== 'QueueDoesNotExist' && e.name !== 'AWS.SimpleQueueService.NonExistentQueue')
70
+ throw e;
71
+ }
72
+ const created = await sqs.send(new CreateQueueCommand({ QueueName: queueName, Attributes: attrs }));
73
+ const url = created.QueueUrl;
74
+ const attrResult = await sqs.send(new GetQueueAttributesCommand({
75
+ QueueUrl: url,
76
+ AttributeNames: [QueueAttributeName.QueueArn],
77
+ }));
78
+ return { url, arn: attrResult.Attributes?.QueueArn };
79
+ }
80
+ buildAttributes(redrivePolicy) {
81
+ const attrs = {
82
+ VisibilityTimeout: String(this._visibilityTimeout),
83
+ MessageRetentionPeriod: String(this._retentionDays * 86400),
84
+ DelaySeconds: String(this._delaySeconds),
85
+ };
86
+ if (this._fifo) {
87
+ attrs.FifoQueue = 'true';
88
+ if (this._deduplication)
89
+ attrs.ContentBasedDeduplication = 'true';
90
+ }
91
+ if (redrivePolicy)
92
+ attrs.RedrivePolicy = redrivePolicy;
93
+ return attrs;
94
+ }
95
+ async deploy() {
96
+ const dryRun = this.isDryRunActive();
97
+ const existing = await this.discoveryPromise;
98
+ const queueName = this.queueName();
99
+ console.log(`\n⚔ Finalizing SQS Queue "${queueName}"...`);
100
+ if (dryRun) {
101
+ const dlqName = this._dlqName ? (this._fifo && !this._dlqName.endsWith('.fifo') ? `${this._dlqName}.fifo` : this._dlqName) : undefined;
102
+ console.log(` šŸ“ [PLAN] ${existing ? 'Update' : 'Create'} queue "${queueName}"`);
103
+ console.log(` └─ Type: ${this._fifo ? 'FIFO' : 'Standard'}`);
104
+ console.log(` └─ Visibility timeout: ${this._visibilityTimeout}s | Retention: ${this._retentionDays}d`);
105
+ if (this._delaySeconds)
106
+ console.log(` └─ Delivery delay: ${this._delaySeconds}s`);
107
+ if (dlqName)
108
+ console.log(` └─ DLQ: ${dlqName} (after ${this._dlqMaxReceives} receives)`);
109
+ this.resolvedUrl = `https://sqs.DRYRUN.amazonaws.com/000000000000/${queueName}`;
110
+ this.resolvedArn = `arn:aws:sqs:DRYRUN:000000000000:${queueName}`;
111
+ return { name: queueName, url: this.resolvedUrl, arn: this.resolvedArn };
112
+ }
113
+ const sqs = getSQSClient();
114
+ // Create DLQ first so we have its ARN for the redrive policy
115
+ let redrivePolicy;
116
+ if (this._dlqName) {
117
+ const dlqQueueName = this._fifo && !this._dlqName.endsWith('.fifo')
118
+ ? `${this._dlqName}.fifo`
119
+ : this._dlqName;
120
+ const dlqAttrs = {
121
+ VisibilityTimeout: String(this._visibilityTimeout),
122
+ MessageRetentionPeriod: String(this._retentionDays * 86400),
123
+ };
124
+ if (this._fifo)
125
+ dlqAttrs.FifoQueue = 'true';
126
+ const dlq = await this.ensureQueue(dlqQueueName, dlqAttrs);
127
+ this.resolvedDlqUrl = dlq.url;
128
+ this.resolvedDlqArn = dlq.arn;
129
+ redrivePolicy = JSON.stringify({ deadLetterTargetArn: dlq.arn, maxReceiveCount: this._dlqMaxReceives });
130
+ console.log(` āœ… DLQ ready: ${dlqQueueName}`);
131
+ }
132
+ const attrs = this.buildAttributes(redrivePolicy);
133
+ if (existing) {
134
+ // FIFO queues reject attribute updates that change queue type — only update mutable attrs
135
+ const mutableAttrs = {
136
+ VisibilityTimeout: attrs.VisibilityTimeout,
137
+ MessageRetentionPeriod: attrs.MessageRetentionPeriod,
138
+ DelaySeconds: attrs.DelaySeconds,
139
+ };
140
+ if (redrivePolicy)
141
+ mutableAttrs.RedrivePolicy = redrivePolicy;
142
+ await sqs.send(new SetQueueAttributesCommand({
143
+ QueueUrl: this.resolvedUrl,
144
+ Attributes: mutableAttrs,
145
+ }));
146
+ console.log(` āœ… Updated queue "${queueName}"`);
147
+ }
148
+ else {
149
+ const created = await sqs.send(new CreateQueueCommand({ QueueName: queueName, Attributes: attrs }));
150
+ this.resolvedUrl = created.QueueUrl;
151
+ const arnResult = await sqs.send(new GetQueueAttributesCommand({
152
+ QueueUrl: this.resolvedUrl,
153
+ AttributeNames: [QueueAttributeName.QueueArn],
154
+ }));
155
+ this.resolvedArn = arnResult.Attributes?.QueueArn ?? null;
156
+ console.log(`šŸš€ Created queue "${queueName}"`);
157
+ }
158
+ console.log(` šŸ”— URL: ${this.resolvedUrl}`);
159
+ await this.deploySidecars();
160
+ return { name: queueName, url: this.resolvedUrl, arn: this.resolvedArn };
161
+ }
162
+ async destroy() {
163
+ const dryRun = this.isDryRunActive();
164
+ const existing = await this.discoveryPromise;
165
+ console.log(`\nšŸ—‘ļø Destroying SQS Queue "${this.queueName()}"...`);
166
+ if (!existing) {
167
+ console.log(` āœ… Queue "${this.queueName()}" does not exist — nothing to do`);
168
+ return { destroyed: this.name };
169
+ }
170
+ if (dryRun) {
171
+ console.log(` šŸ“ [PLAN] Delete queue "${this.queueName()}"`);
172
+ return { destroyed: this.name };
173
+ }
174
+ await getSQSClient().send(new DeleteQueueCommand({ QueueUrl: this.resolvedUrl }));
175
+ console.log(` āœ… Deleted queue "${this.queueName()}"`);
176
+ return { destroyed: this.name };
177
+ }
178
+ }
@@ -0,0 +1,11 @@
1
+ export declare class DoApiClient {
2
+ private token;
3
+ private static readonly BASE;
4
+ constructor(token: string);
5
+ private get authHeaders();
6
+ get<T>(path: string): Promise<T>;
7
+ post<T>(path: string, body: unknown): Promise<T>;
8
+ put<T>(path: string, body: unknown): Promise<T>;
9
+ delete(path: string): Promise<void>;
10
+ }
11
+ export declare function getDoApi(): DoApiClient;
@@ -0,0 +1,52 @@
1
+ import { Config } from '../../core/config.js';
2
+ export class DoApiClient {
3
+ token;
4
+ static BASE = 'https://api.digitalocean.com/v2';
5
+ constructor(token) {
6
+ this.token = token;
7
+ }
8
+ get authHeaders() {
9
+ return { Authorization: `Bearer ${this.token}`, 'Content-Type': 'application/json' };
10
+ }
11
+ async get(path) {
12
+ const res = await fetch(`${DoApiClient.BASE}${path}`, { headers: this.authHeaders });
13
+ if (!res.ok)
14
+ throw new Error(`DO API GET ${path}: ${res.status} ${await res.text()}`);
15
+ return res.json();
16
+ }
17
+ async post(path, body) {
18
+ const res = await fetch(`${DoApiClient.BASE}${path}`, {
19
+ method: 'POST',
20
+ headers: this.authHeaders,
21
+ body: JSON.stringify(body),
22
+ });
23
+ if (!res.ok)
24
+ throw new Error(`DO API POST ${path}: ${res.status} ${await res.text()}`);
25
+ return res.json();
26
+ }
27
+ async put(path, body) {
28
+ const res = await fetch(`${DoApiClient.BASE}${path}`, {
29
+ method: 'PUT',
30
+ headers: this.authHeaders,
31
+ body: JSON.stringify(body),
32
+ });
33
+ if (!res.ok)
34
+ throw new Error(`DO API PUT ${path}: ${res.status} ${await res.text()}`);
35
+ return res.json();
36
+ }
37
+ async delete(path) {
38
+ const res = await fetch(`${DoApiClient.BASE}${path}`, {
39
+ method: 'DELETE',
40
+ headers: this.authHeaders,
41
+ });
42
+ if (!res.ok && res.status !== 404) {
43
+ throw new Error(`DO API DELETE ${path}: ${res.status} ${await res.text()}`);
44
+ }
45
+ }
46
+ }
47
+ export function getDoApi() {
48
+ const token = Config.get().providers.do?.token;
49
+ if (!token)
50
+ throw new Error('DO token not configured. Call DO.init({ token: "..." })');
51
+ return new DoApiClient(token);
52
+ }
@@ -0,0 +1,7 @@
1
+ import { BaseBuilder } from '../../core/resource.js';
2
+ export declare class CertificateBuilder extends BaseBuilder {
3
+ domainName: string;
4
+ constructor(domainName: string);
5
+ private discoverCertificate;
6
+ deploy(): Promise<any>;
7
+ }
@@ -0,0 +1,36 @@
1
+ import { BaseBuilder } from '../../core/resource.js';
2
+ import { getDoApi } from './api.js';
3
+ export class CertificateBuilder extends BaseBuilder {
4
+ domainName;
5
+ constructor(domainName) {
6
+ super(`ssl-${domainName}`);
7
+ this.domainName = domainName;
8
+ this.discoveryPromise = this.discoverCertificate(this.name);
9
+ }
10
+ async discoverCertificate(name) {
11
+ const api = getDoApi();
12
+ const data = await api.get('/certificates?per_page=200');
13
+ return data.certificates.find(c => c.name === name) ?? null;
14
+ }
15
+ async deploy() {
16
+ const dryRun = this.isDryRunActive();
17
+ const existing = await this.discoveryPromise;
18
+ console.log(`\nšŸ” Finalizing SSL certificate for "${this.domainName}"...`);
19
+ if (existing) {
20
+ console.log(` āœ… Certificate ${this.name} already exists (state=${existing.state}).`);
21
+ return existing;
22
+ }
23
+ if (dryRun) {
24
+ console.log(` šŸ“ [PLAN] Create Let's Encrypt certificate for *.${this.domainName}`);
25
+ return { name: this.name };
26
+ }
27
+ const api = getDoApi();
28
+ const result = await api.post('/certificates', {
29
+ name: this.name,
30
+ type: 'lets_encrypt',
31
+ dns_names: [`*.${this.domainName}`, this.domainName],
32
+ });
33
+ console.log(`šŸš€ Requested certificate ${this.name} (id=${result.certificate.id})`);
34
+ return result.certificate;
35
+ }
36
+ }
@@ -0,0 +1,21 @@
1
+ import { BaseBuilder } from '../../core/resource.js';
2
+ import { Output } from '../../core/output.js';
3
+ import { DropletBuilder } from './droplet.js';
4
+ export interface DNSRecord {
5
+ type: 'A' | 'CNAME' | 'TXT' | 'MX';
6
+ name: string;
7
+ value: string | DropletBuilder | Output<string>;
8
+ }
9
+ export declare class DomainBuilder extends BaseBuilder {
10
+ domainName: string;
11
+ private records;
12
+ constructor(domainName: string);
13
+ private discoverDomain;
14
+ withSSL(): this;
15
+ pointer(name: string, target: DropletBuilder | Output<string> | string): this;
16
+ cname(name: string, target: string): this;
17
+ deploy(): Promise<{
18
+ domain: string;
19
+ records: DNSRecord[];
20
+ }>;
21
+ }
@@ -0,0 +1,81 @@
1
+ import { BaseBuilder } from '../../core/resource.js';
2
+ import { Output } from '../../core/output.js';
3
+ import { DropletBuilder } from './droplet.js';
4
+ import { CertificateBuilder } from './certificate.js';
5
+ import { getDoApi } from './api.js';
6
+ export class DomainBuilder extends BaseBuilder {
7
+ domainName;
8
+ records = [];
9
+ constructor(domainName) {
10
+ super(domainName);
11
+ this.domainName = domainName;
12
+ this.discoveryPromise = this.discoverDomain(domainName);
13
+ }
14
+ async discoverDomain(name) {
15
+ const api = getDoApi();
16
+ try {
17
+ return await api.get(`/domains/${name}`).then(d => d.domain);
18
+ }
19
+ catch {
20
+ return null;
21
+ }
22
+ }
23
+ withSSL() {
24
+ const cert = new CertificateBuilder(this.domainName);
25
+ this.sidecars.push(cert);
26
+ return this;
27
+ }
28
+ pointer(name, target) {
29
+ this.records.push({ type: 'A', name, value: target });
30
+ return this;
31
+ }
32
+ cname(name, target) {
33
+ this.records.push({ type: 'CNAME', name, value: target });
34
+ return this;
35
+ }
36
+ async deploy() {
37
+ const dryRun = this.isDryRunActive();
38
+ const existing = await this.discoveryPromise;
39
+ const api = getDoApi();
40
+ console.log(`\n🌐 Finalizing DNS for "${this.domainName}"...`);
41
+ if (!existing) {
42
+ if (dryRun) {
43
+ console.log(` šŸ“ [PLAN] Create domain ${this.domainName}`);
44
+ }
45
+ else {
46
+ await api.post('/domains', { name: this.domainName });
47
+ console.log(`šŸš€ Created domain ${this.domainName}`);
48
+ }
49
+ }
50
+ for (const record of this.records) {
51
+ let data;
52
+ if (record.value instanceof Output) {
53
+ data = await record.value.get();
54
+ }
55
+ else if (record.value instanceof DropletBuilder) {
56
+ data = (await record.value.getPublicIp()) ?? `[IP of ${record.value.name} — not found]`;
57
+ }
58
+ else {
59
+ data = record.value;
60
+ }
61
+ if (dryRun) {
62
+ console.log(` šŸ“ [PLAN] ${record.type} ${record.name}.${this.domainName} → ${data}`);
63
+ continue;
64
+ }
65
+ // Delete existing record with same type+name before creating
66
+ const existing_records = await api.get(`/domains/${this.domainName}/records?per_page=200`);
67
+ const dupe = existing_records.domain_records.find(r => r.type === record.type && r.name === record.name);
68
+ if (dupe)
69
+ await api.delete(`/domains/${this.domainName}/records/${dupe.id}`);
70
+ await api.post(`/domains/${this.domainName}/records`, {
71
+ type: record.type,
72
+ name: record.name,
73
+ data,
74
+ ttl: 3600,
75
+ });
76
+ console.log(` āœ… ${record.type} ${record.name}.${this.domainName} → ${data}`);
77
+ }
78
+ await this.deploySidecars();
79
+ return { domain: this.domainName, records: this.records };
80
+ }
81
+ }
@@ -0,0 +1,35 @@
1
+ import { OS, REGION, SIZE } from '../../types/do.js';
2
+ import { BaseBuilder } from '../../core/resource.js';
3
+ import { Output } from '../../core/output.js';
4
+ import { DomainBuilder } from './domain.js';
5
+ import { LoadBalancerBuilder } from './load_balancer.js';
6
+ export declare class DropletBuilder extends BaseBuilder {
7
+ readonly out: {
8
+ ip: Output<string>;
9
+ id: Output<number>;
10
+ };
11
+ config: any;
12
+ private dropletId?;
13
+ private resolvedIp?;
14
+ private sshKeyPath?;
15
+ constructor(name: string);
16
+ private discoverDroplet;
17
+ getPublicIp(): Promise<string | undefined>;
18
+ allowPublicWeb(sources?: string[]): this;
19
+ image(image: (typeof OS)[keyof typeof OS] | string): this;
20
+ region(region: (typeof REGION)[keyof typeof REGION] | string): this;
21
+ size(size: (typeof SIZE)[keyof typeof SIZE] | string): this;
22
+ sslKey(keyPath: string): this;
23
+ private resolveOrRegisterSshKey;
24
+ deploy(): Promise<any>;
25
+ destroy(): Promise<any>;
26
+ }
27
+ export declare const DO: {
28
+ init: (opts: {
29
+ token: string;
30
+ defaultRegion?: string;
31
+ }) => void;
32
+ Droplet: (name: string) => DropletBuilder;
33
+ Domain: (name: string) => DomainBuilder;
34
+ LoadBalancer: (name: string) => LoadBalancerBuilder;
35
+ };
@@ -0,0 +1,180 @@
1
+ import { readFileSync } from 'fs';
2
+ import { homedir } from 'os';
3
+ import { OS, REGION, SIZE, NETWORK } from '../../types/do.js';
4
+ import { Config } from '../../core/config.js';
5
+ import { BaseBuilder } from '../../core/resource.js';
6
+ import { Output } from '../../core/output.js';
7
+ import { FirewallBuilder } from './firewall.js';
8
+ import { DomainBuilder } from './domain.js';
9
+ import { LoadBalancerBuilder } from './load_balancer.js';
10
+ import { getDoApi } from './api.js';
11
+ export class DropletBuilder extends BaseBuilder {
12
+ out = {
13
+ ip: new Output(),
14
+ id: new Output(),
15
+ };
16
+ config = {
17
+ image: OS.UBUNTU_22_04,
18
+ region: Config.get().providers.do?.defaultRegion || REGION.NYC,
19
+ size: SIZE.SMALL,
20
+ };
21
+ dropletId;
22
+ resolvedIp;
23
+ sshKeyPath;
24
+ constructor(name) {
25
+ super(name);
26
+ this.discoveryPromise = this.discoverDroplet(name);
27
+ }
28
+ async discoverDroplet(name) {
29
+ const api = getDoApi();
30
+ const data = await api.get(`/droplets?name=${encodeURIComponent(name)}&per_page=200`);
31
+ const match = data.droplets.find(d => d.name === name) ?? null;
32
+ if (match) {
33
+ this.dropletId = match.id;
34
+ const pub = (match.networks?.v4 ?? []).find((n) => n.type === 'public');
35
+ this.resolvedIp = pub?.ip_address;
36
+ if (this.dropletId)
37
+ this.out.id.resolve(this.dropletId);
38
+ if (this.resolvedIp)
39
+ this.out.ip.resolve(this.resolvedIp);
40
+ }
41
+ return match;
42
+ }
43
+ async getPublicIp() {
44
+ await this.discoveryPromise;
45
+ return this.resolvedIp;
46
+ }
47
+ allowPublicWeb(sources = [NETWORK.ANY, NETWORK.ANY_V6]) {
48
+ const fw = new FirewallBuilder(`${this.name}-web-fw`)
49
+ .ingress('tcp', 80, sources)
50
+ .ingress('tcp', 443, sources)
51
+ .egress('tcp', 'all', [NETWORK.ANY, NETWORK.ANY_V6])
52
+ .attachTo(this.name);
53
+ this.sidecars.push(fw);
54
+ return this;
55
+ }
56
+ image(image) {
57
+ this.config.image = image;
58
+ return this;
59
+ }
60
+ region(region) {
61
+ this.config.region = region;
62
+ return this;
63
+ }
64
+ size(size) {
65
+ this.config.size = size;
66
+ return this;
67
+ }
68
+ sslKey(keyPath) {
69
+ this.sshKeyPath = keyPath.replace('~', homedir());
70
+ return this;
71
+ }
72
+ async resolveOrRegisterSshKey(api) {
73
+ const pubPath = this.sshKeyPath.replace(/\.pub$/, '') + '.pub';
74
+ const pubKey = readFileSync(pubPath, 'utf8').trim();
75
+ const { ssh_keys } = await api.get('/account/keys?per_page=200');
76
+ const existing = ssh_keys.find(k => k.public_key.trim() === pubKey);
77
+ if (existing)
78
+ return existing.id;
79
+ const keyName = pubPath.split('/').pop().replace('.pub', '');
80
+ const result = await api.post('/account/keys', { name: keyName, public_key: pubKey });
81
+ console.log(` šŸ”‘ Registered SSH key "${keyName}" (id=${result.ssh_key.id})`);
82
+ return result.ssh_key.id;
83
+ }
84
+ async deploy() {
85
+ const dryRun = this.isDryRunActive();
86
+ const existing = await this.discoveryPromise;
87
+ const api = getDoApi();
88
+ const hasChanges = existing
89
+ ? existing.size_slug !== this.config.size || existing.region.slug !== this.config.region
90
+ : true;
91
+ if (await this.checkProtection(hasChanges))
92
+ return null;
93
+ if (dryRun) {
94
+ console.log(`\nšŸ” [DRY RUN] "${this.name}"...`);
95
+ if (!existing) {
96
+ const keyHint = this.sshKeyPath ? ` + key ${this.sshKeyPath.split('/').pop()}` : '';
97
+ console.log(` šŸ“ Plan: Create droplet ${this.name} (${this.config.size} in ${this.config.region}${keyHint})`);
98
+ this.out.id.resolve(-1);
99
+ this.out.ip.resolve('0.0.0.0');
100
+ }
101
+ else if (hasChanges) {
102
+ console.log(` šŸ“ Plan: Resize ${this.name} → ${this.config.size}`);
103
+ }
104
+ else {
105
+ console.log(` āœ… ${this.name} is up to date.`);
106
+ }
107
+ for (const sidecar of this.sidecars)
108
+ await sidecar.deploy();
109
+ return this.config;
110
+ }
111
+ console.log(`\nā³ Finalizing "${this.name}"...`);
112
+ if (!existing) {
113
+ const sshKeyIds = this.sshKeyPath ? [await this.resolveOrRegisterSshKey(api)] : [];
114
+ const result = await api.post('/droplets', {
115
+ name: this.name,
116
+ region: this.config.region,
117
+ size: this.config.size,
118
+ image: this.config.image,
119
+ ...(sshKeyIds.length && { ssh_keys: sshKeyIds }),
120
+ });
121
+ this.dropletId = result.droplet.id;
122
+ this.out.id.resolve(this.dropletId);
123
+ console.log(`šŸš€ Created droplet ${this.name} (id=${this.dropletId})`);
124
+ await this.waitFor('droplet to become active', async () => {
125
+ const d = await api.get(`/droplets/${this.dropletId}`);
126
+ if (d.droplet.status === 'active') {
127
+ const pub = (d.droplet.networks?.v4 ?? []).find((n) => n.type === 'public');
128
+ this.resolvedIp = pub?.ip_address;
129
+ return true;
130
+ }
131
+ return false;
132
+ });
133
+ if (this.resolvedIp) {
134
+ this.out.ip.resolve(this.resolvedIp);
135
+ console.log(` 🌐 Public IP: ${this.resolvedIp}`);
136
+ }
137
+ }
138
+ else if (hasChanges) {
139
+ console.log(`✨ Resizing ${this.name} → ${this.config.size}...`);
140
+ await api.post(`/droplets/${this.dropletId}/actions`, { type: 'resize', size: this.config.size });
141
+ }
142
+ else {
143
+ console.log(`āœ… ${this.name} is up to date.`);
144
+ }
145
+ for (const sidecar of this.sidecars)
146
+ await sidecar.deploy();
147
+ return this.config;
148
+ }
149
+ async destroy() {
150
+ const dryRun = this.isDryRunActive();
151
+ await this.discoveryPromise;
152
+ if (!this.dropletId) {
153
+ console.log(`\nšŸ—‘ļø "${this.name}" not found, nothing to destroy.`);
154
+ return { destroyed: null };
155
+ }
156
+ console.log(`\nšŸ—‘ļø Destroying "${this.name}" (id=${this.dropletId})...`);
157
+ if (dryRun) {
158
+ console.log(` šŸ“ [PLAN] Would delete droplet id=${this.dropletId}`);
159
+ }
160
+ else {
161
+ await getDoApi().delete(`/droplets/${this.dropletId}`);
162
+ console.log(` āœ… Deleted.`);
163
+ }
164
+ await this.destroySidecars();
165
+ return { destroyed: this.name };
166
+ }
167
+ }
168
+ export const DO = {
169
+ init: (opts) => {
170
+ Config.set({
171
+ providers: {
172
+ ...Config.get().providers,
173
+ do: opts,
174
+ },
175
+ });
176
+ },
177
+ Droplet: (name) => new DropletBuilder(name),
178
+ Domain: (name) => new DomainBuilder(name),
179
+ LoadBalancer: (name) => new LoadBalancerBuilder(name),
180
+ };
@@ -0,0 +1,23 @@
1
+ import { BaseBuilder } from '../../core/resource.js';
2
+ export interface FirewallRule {
3
+ type: 'ingress' | 'egress';
4
+ protocol: 'tcp' | 'udp' | 'icmp';
5
+ port: number | string;
6
+ sources?: string[];
7
+ destinations?: string[];
8
+ }
9
+ export declare class FirewallBuilder extends BaseBuilder {
10
+ private rules;
11
+ private dropletNames;
12
+ constructor(name: string);
13
+ private discoverFirewall;
14
+ ingress(protocol: 'tcp' | 'udp' | 'icmp', port: number | string, sources: string[]): this;
15
+ egress(protocol: 'tcp' | 'udp' | 'icmp', port: number | string, destinations: string[]): this;
16
+ attachTo(dropletName: string): this;
17
+ private resolveDropletIds;
18
+ private buildApiRules;
19
+ deploy(): Promise<{
20
+ name: string;
21
+ rules: FirewallRule[];
22
+ }>;
23
+ }