relight-cli 0.1.0 → 0.3.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 (45) hide show
  1. package/README.md +77 -34
  2. package/package.json +12 -4
  3. package/src/cli.js +350 -1
  4. package/src/commands/apps.js +128 -0
  5. package/src/commands/auth.js +13 -4
  6. package/src/commands/config.js +282 -0
  7. package/src/commands/cost.js +593 -0
  8. package/src/commands/db.js +775 -0
  9. package/src/commands/deploy.js +264 -0
  10. package/src/commands/doctor.js +69 -13
  11. package/src/commands/domains.js +223 -0
  12. package/src/commands/logs.js +111 -0
  13. package/src/commands/open.js +42 -0
  14. package/src/commands/ps.js +121 -0
  15. package/src/commands/scale.js +132 -0
  16. package/src/commands/service.js +227 -0
  17. package/src/lib/clouds/aws.js +309 -35
  18. package/src/lib/clouds/cf.js +401 -2
  19. package/src/lib/clouds/gcp.js +255 -4
  20. package/src/lib/clouds/neon.js +147 -0
  21. package/src/lib/clouds/slicervm.js +139 -0
  22. package/src/lib/config.js +200 -2
  23. package/src/lib/docker.js +34 -0
  24. package/src/lib/link.js +31 -5
  25. package/src/lib/providers/aws/app.js +481 -0
  26. package/src/lib/providers/aws/db.js +504 -0
  27. package/src/lib/providers/aws/dns.js +232 -0
  28. package/src/lib/providers/aws/registry.js +59 -0
  29. package/src/lib/providers/cf/app.js +596 -0
  30. package/src/lib/providers/cf/bundle.js +70 -0
  31. package/src/lib/providers/cf/db.js +181 -0
  32. package/src/lib/providers/cf/dns.js +148 -0
  33. package/src/lib/providers/cf/registry.js +17 -0
  34. package/src/lib/providers/gcp/app.js +429 -0
  35. package/src/lib/providers/gcp/db.js +372 -0
  36. package/src/lib/providers/gcp/dns.js +166 -0
  37. package/src/lib/providers/gcp/registry.js +30 -0
  38. package/src/lib/providers/neon/db.js +306 -0
  39. package/src/lib/providers/resolve.js +79 -0
  40. package/src/lib/providers/slicervm/app.js +396 -0
  41. package/src/lib/providers/slicervm/db.js +33 -0
  42. package/src/lib/providers/slicervm/dns.js +58 -0
  43. package/src/lib/providers/slicervm/registry.js +7 -0
  44. package/worker-template/package.json +10 -0
  45. package/worker-template/src/index.js +260 -0
@@ -0,0 +1,232 @@
1
+ import { awsRestXmlApi, xmlList, xmlVal, xmlBlock } from "../../clouds/aws.js";
2
+ import { getAppConfig, pushAppConfig, getAppUrl } from "./app.js";
3
+
4
+ export async function getZones(cfg) {
5
+ var cr = { accessKeyId: cfg.accessKeyId, secretAccessKey: cfg.secretAccessKey };
6
+ var xml = await awsRestXmlApi("GET", "/2013-04-01/hostedzone", null, cr);
7
+
8
+ var zones = xmlList(xml, "HostedZone");
9
+ return zones.map((z) => {
10
+ var id = xmlVal(z, "Id");
11
+ var name = xmlVal(z, "Name");
12
+ return {
13
+ id: id ? id.replace("/hostedzone/", "") : null,
14
+ name: name ? name.replace(/\.$/, "") : null,
15
+ };
16
+ });
17
+ }
18
+
19
+ export function findZoneForHostname(zones, hostname) {
20
+ var match = null;
21
+ for (var zone of zones) {
22
+ if (hostname === zone.name || hostname.endsWith("." + zone.name)) {
23
+ if (!match || zone.name.length > match.name.length) {
24
+ match = zone;
25
+ }
26
+ }
27
+ }
28
+ return match;
29
+ }
30
+
31
+ export async function listDomains(cfg, appName) {
32
+ var url = await getAppUrl(cfg, appName);
33
+ var defaultDomain = url ? new URL(url).hostname : null;
34
+
35
+ var appConfig = await getAppConfig(cfg, appName);
36
+ var custom = appConfig?.domains || [];
37
+
38
+ return {
39
+ default: defaultDomain,
40
+ custom,
41
+ };
42
+ }
43
+
44
+ export async function addDomain(cfg, appName, domain, { zone }) {
45
+ var cr = { accessKeyId: cfg.accessKeyId, secretAccessKey: cfg.secretAccessKey };
46
+
47
+ // Get App Runner URL to use as CNAME target
48
+ var url = await getAppUrl(cfg, appName);
49
+ if (!url) throw new Error("Could not determine app URL for CNAME target.");
50
+ var target = new URL(url).hostname;
51
+
52
+ // FQDN with trailing dot for Route 53
53
+ var fqdn = domain.endsWith(".") ? domain : domain + ".";
54
+ var targetFqdn = target.endsWith(".") ? target : target + ".";
55
+
56
+ var xmlBody = `<?xml version="1.0" encoding="UTF-8"?>
57
+ <ChangeResourceRecordSetsRequest xmlns="https://route53.amazonaws.com/doc/2013-04-01/">
58
+ <ChangeBatch>
59
+ <Changes>
60
+ <Change>
61
+ <Action>UPSERT</Action>
62
+ <ResourceRecordSet>
63
+ <Name>${fqdn}</Name>
64
+ <Type>CNAME</Type>
65
+ <TTL>300</TTL>
66
+ <ResourceRecords>
67
+ <ResourceRecord>
68
+ <Value>${targetFqdn}</Value>
69
+ </ResourceRecord>
70
+ </ResourceRecords>
71
+ </ResourceRecordSet>
72
+ </Change>
73
+ </Changes>
74
+ </ChangeBatch>
75
+ </ChangeResourceRecordSetsRequest>`;
76
+
77
+ await awsRestXmlApi("POST", `/2013-04-01/hostedzone/${zone.id}/rrset`, xmlBody, cr);
78
+
79
+ // Update app config
80
+ var appConfig = await getAppConfig(cfg, appName);
81
+ if (appConfig) {
82
+ if (!appConfig.domains) appConfig.domains = [];
83
+ if (!appConfig.domains.includes(domain)) {
84
+ appConfig.domains.push(domain);
85
+ await pushAppConfig(cfg, appName, appConfig);
86
+ }
87
+ }
88
+ }
89
+
90
+ export async function removeDomain(cfg, appName, domain) {
91
+ var cr = { accessKeyId: cfg.accessKeyId, secretAccessKey: cfg.secretAccessKey };
92
+ var fqdn = domain.endsWith(".") ? domain : domain + ".";
93
+
94
+ // Find the zone for this domain
95
+ var zones = await getZones(cfg);
96
+ var zone = findZoneForHostname(zones, domain);
97
+
98
+ if (zone) {
99
+ // List record sets to get current value and TTL for DELETE
100
+ var xml = await awsRestXmlApi("GET", `/2013-04-01/hostedzone/${zone.id}/rrset`, null, cr);
101
+ var recordSets = xmlList(xml, "ResourceRecordSet");
102
+
103
+ var existing = null;
104
+ for (var rs of recordSets) {
105
+ var rsName = xmlVal(rs, "Name");
106
+ var rsType = xmlVal(rs, "Type");
107
+ if (rsName === fqdn && rsType === "CNAME") {
108
+ existing = rs;
109
+ break;
110
+ }
111
+ }
112
+
113
+ if (existing) {
114
+ var ttl = xmlVal(existing, "TTL") || "300";
115
+ var value = xmlVal(xmlBlock(existing, "ResourceRecords") || "", "Value") || "";
116
+
117
+ var xmlBody = `<?xml version="1.0" encoding="UTF-8"?>
118
+ <ChangeResourceRecordSetsRequest xmlns="https://route53.amazonaws.com/doc/2013-04-01/">
119
+ <ChangeBatch>
120
+ <Changes>
121
+ <Change>
122
+ <Action>DELETE</Action>
123
+ <ResourceRecordSet>
124
+ <Name>${fqdn}</Name>
125
+ <Type>CNAME</Type>
126
+ <TTL>${ttl}</TTL>
127
+ <ResourceRecords>
128
+ <ResourceRecord>
129
+ <Value>${value}</Value>
130
+ </ResourceRecord>
131
+ </ResourceRecords>
132
+ </ResourceRecordSet>
133
+ </Change>
134
+ </Changes>
135
+ </ChangeBatch>
136
+ </ChangeResourceRecordSetsRequest>`;
137
+
138
+ await awsRestXmlApi("POST", `/2013-04-01/hostedzone/${zone.id}/rrset`, xmlBody, cr);
139
+ }
140
+ }
141
+
142
+ // Update app config
143
+ var appConfig = await getAppConfig(cfg, appName);
144
+ if (appConfig) {
145
+ appConfig.domains = (appConfig.domains || []).filter((d) => d !== domain);
146
+ await pushAppConfig(cfg, appName, appConfig);
147
+ }
148
+ }
149
+
150
+ // --- Pure DNS record operations (for cross-cloud use) ---
151
+
152
+ export async function addDnsRecord(cfg, domain, target, zone) {
153
+ var cr = { accessKeyId: cfg.accessKeyId, secretAccessKey: cfg.secretAccessKey };
154
+ var fqdn = domain.endsWith(".") ? domain : domain + ".";
155
+ var targetFqdn = target.endsWith(".") ? target : target + ".";
156
+
157
+ // Check for existing CNAME
158
+ var xml = await awsRestXmlApi("GET", `/2013-04-01/hostedzone/${zone.id}/rrset`, null, cr);
159
+ var recordSets = xmlList(xml, "ResourceRecordSet");
160
+ for (var rs of recordSets) {
161
+ if (xmlVal(rs, "Name") === fqdn && xmlVal(rs, "Type") === "CNAME") {
162
+ var value = xmlVal(xmlBlock(rs, "ResourceRecords") || "", "Value") || "";
163
+ throw new Error(`CNAME record already exists for ${domain} -> ${value}`);
164
+ }
165
+ }
166
+
167
+ var xmlBody = `<?xml version="1.0" encoding="UTF-8"?>
168
+ <ChangeResourceRecordSetsRequest xmlns="https://route53.amazonaws.com/doc/2013-04-01/">
169
+ <ChangeBatch>
170
+ <Changes>
171
+ <Change>
172
+ <Action>CREATE</Action>
173
+ <ResourceRecordSet>
174
+ <Name>${fqdn}</Name>
175
+ <Type>CNAME</Type>
176
+ <TTL>300</TTL>
177
+ <ResourceRecords>
178
+ <ResourceRecord>
179
+ <Value>${targetFqdn}</Value>
180
+ </ResourceRecord>
181
+ </ResourceRecords>
182
+ </ResourceRecordSet>
183
+ </Change>
184
+ </Changes>
185
+ </ChangeBatch>
186
+ </ChangeResourceRecordSetsRequest>`;
187
+
188
+ await awsRestXmlApi("POST", `/2013-04-01/hostedzone/${zone.id}/rrset`, xmlBody, cr);
189
+ }
190
+
191
+ export async function removeDnsRecord(cfg, domain) {
192
+ var cr = { accessKeyId: cfg.accessKeyId, secretAccessKey: cfg.secretAccessKey };
193
+ var fqdn = domain.endsWith(".") ? domain : domain + ".";
194
+
195
+ var zones = await getZones(cfg);
196
+ var zone = findZoneForHostname(zones, domain);
197
+ if (!zone) return;
198
+
199
+ var xml = await awsRestXmlApi("GET", `/2013-04-01/hostedzone/${zone.id}/rrset`, null, cr);
200
+ var recordSets = xmlList(xml, "ResourceRecordSet");
201
+
202
+ for (var rs of recordSets) {
203
+ if (xmlVal(rs, "Name") === fqdn && xmlVal(rs, "Type") === "CNAME") {
204
+ var ttl = xmlVal(rs, "TTL") || "300";
205
+ var value = xmlVal(xmlBlock(rs, "ResourceRecords") || "", "Value") || "";
206
+
207
+ var xmlBody = `<?xml version="1.0" encoding="UTF-8"?>
208
+ <ChangeResourceRecordSetsRequest xmlns="https://route53.amazonaws.com/doc/2013-04-01/">
209
+ <ChangeBatch>
210
+ <Changes>
211
+ <Change>
212
+ <Action>DELETE</Action>
213
+ <ResourceRecordSet>
214
+ <Name>${fqdn}</Name>
215
+ <Type>CNAME</Type>
216
+ <TTL>${ttl}</TTL>
217
+ <ResourceRecords>
218
+ <ResourceRecord>
219
+ <Value>${value}</Value>
220
+ </ResourceRecord>
221
+ </ResourceRecords>
222
+ </ResourceRecordSet>
223
+ </Change>
224
+ </Changes>
225
+ </ChangeBatch>
226
+ </ChangeResourceRecordSetsRequest>`;
227
+
228
+ await awsRestXmlApi("POST", `/2013-04-01/hostedzone/${zone.id}/rrset`, xmlBody, cr);
229
+ return;
230
+ }
231
+ }
232
+ }
@@ -0,0 +1,59 @@
1
+ import { awsJsonApi, getAccountId } from "../../clouds/aws.js";
2
+
3
+ export async function getCredentials(cfg) {
4
+ var cr = { accessKeyId: cfg.accessKeyId, secretAccessKey: cfg.secretAccessKey };
5
+
6
+ var res = await awsJsonApi(
7
+ "AmazonEC2ContainerRegistry_V20150921.GetAuthorizationToken",
8
+ {},
9
+ "ecr",
10
+ cr,
11
+ cfg.region,
12
+ `api.ecr.${cfg.region}.amazonaws.com`
13
+ );
14
+
15
+ var authData = res.authorizationData?.[0];
16
+ if (!authData) throw new Error("No ECR authorization data returned.");
17
+
18
+ var decoded = Buffer.from(authData.authorizationToken, "base64").toString();
19
+ var [username, password] = decoded.split(":");
20
+ var registry = authData.proxyEndpoint; // https://{accountId}.dkr.ecr.{region}.amazonaws.com
21
+
22
+ return { registry, username, password };
23
+ }
24
+
25
+ export async function getImageTag(cfg, appName, tag) {
26
+ var cr = { accessKeyId: cfg.accessKeyId, secretAccessKey: cfg.secretAccessKey };
27
+ var accountId = await getAccountId(cr, cfg.region);
28
+ return `${accountId}.dkr.ecr.${cfg.region}.amazonaws.com/relight-${appName}:${tag}`;
29
+ }
30
+
31
+ export async function ensureRepository(cfg, appName) {
32
+ var cr = { accessKeyId: cfg.accessKeyId, secretAccessKey: cfg.secretAccessKey };
33
+ var repoName = `relight-${appName}`;
34
+ var host = `api.ecr.${cfg.region}.amazonaws.com`;
35
+
36
+ // Check if repository exists
37
+ try {
38
+ await awsJsonApi(
39
+ "AmazonEC2ContainerRegistry_V20150921.DescribeRepositories",
40
+ { repositoryNames: [repoName] },
41
+ "ecr",
42
+ cr,
43
+ cfg.region,
44
+ host
45
+ );
46
+ return; // Already exists
47
+ } catch {
48
+ // Repository doesn't exist, create it
49
+ }
50
+
51
+ await awsJsonApi(
52
+ "AmazonEC2ContainerRegistry_V20150921.CreateRepository",
53
+ { repositoryName: repoName },
54
+ "ecr",
55
+ cr,
56
+ cfg.region,
57
+ host
58
+ );
59
+ }