relight-cli 0.1.0 → 0.2.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 (42) hide show
  1. package/README.md +77 -34
  2. package/package.json +12 -4
  3. package/src/cli.js +305 -1
  4. package/src/commands/apps.js +128 -0
  5. package/src/commands/auth.js +75 -4
  6. package/src/commands/config.js +282 -0
  7. package/src/commands/cost.js +593 -0
  8. package/src/commands/db.js +531 -0
  9. package/src/commands/deploy.js +298 -0
  10. package/src/commands/doctor.js +41 -9
  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/lib/clouds/aws.js +309 -35
  17. package/src/lib/clouds/cf.js +401 -2
  18. package/src/lib/clouds/gcp.js +234 -3
  19. package/src/lib/clouds/slicervm.js +139 -0
  20. package/src/lib/config.js +40 -0
  21. package/src/lib/docker.js +34 -0
  22. package/src/lib/link.js +20 -5
  23. package/src/lib/providers/aws/app.js +481 -0
  24. package/src/lib/providers/aws/db.js +513 -0
  25. package/src/lib/providers/aws/dns.js +232 -0
  26. package/src/lib/providers/aws/registry.js +59 -0
  27. package/src/lib/providers/cf/app.js +596 -0
  28. package/src/lib/providers/cf/bundle.js +70 -0
  29. package/src/lib/providers/cf/db.js +279 -0
  30. package/src/lib/providers/cf/dns.js +148 -0
  31. package/src/lib/providers/cf/registry.js +17 -0
  32. package/src/lib/providers/gcp/app.js +429 -0
  33. package/src/lib/providers/gcp/db.js +457 -0
  34. package/src/lib/providers/gcp/dns.js +166 -0
  35. package/src/lib/providers/gcp/registry.js +30 -0
  36. package/src/lib/providers/resolve.js +49 -0
  37. package/src/lib/providers/slicervm/app.js +396 -0
  38. package/src/lib/providers/slicervm/db.js +33 -0
  39. package/src/lib/providers/slicervm/dns.js +58 -0
  40. package/src/lib/providers/slicervm/registry.js +7 -0
  41. package/worker-template/package.json +10 -0
  42. package/worker-template/src/index.js +260 -0
@@ -0,0 +1,457 @@
1
+ import { randomBytes } from "crypto";
2
+ import {
3
+ mintAccessToken,
4
+ createSqlInstance,
5
+ getSqlInstance,
6
+ deleteSqlInstance,
7
+ createSqlDatabase,
8
+ createSqlUser,
9
+ updateSqlUser,
10
+ } from "../../clouds/gcp.js";
11
+ import { getAppConfig, pushAppConfig } from "./app.js";
12
+
13
+ function instanceName(appName) {
14
+ return `relight-${appName}`;
15
+ }
16
+
17
+ function dbName(appName) {
18
+ return `relight_${appName}`;
19
+ }
20
+
21
+ async function getDbPassword(cfg, appName) {
22
+ var token = await mintAccessToken(cfg.clientEmail, cfg.privateKey);
23
+
24
+ // Read from Cloud Run service env vars
25
+ var { listAllServices } = await import("../../clouds/gcp.js");
26
+ var all = await listAllServices(token, cfg.project);
27
+ var svc = all.find((s) => s.name.split("/").pop() === `relight-${appName}`);
28
+ if (!svc) throw new Error(`Service relight-${appName} not found.`);
29
+
30
+ var envVars = svc.template?.containers?.[0]?.env || [];
31
+ var dbToken = envVars.find((e) => e.name === "DB_TOKEN");
32
+ if (!dbToken) throw new Error("DB_TOKEN not found on service.");
33
+ return dbToken.value;
34
+ }
35
+
36
+ async function connectPg(connectionUrl) {
37
+ var pg = await import("pg");
38
+ var Client = pg.default?.Client || pg.Client;
39
+ var client = new Client({ connectionString: connectionUrl });
40
+ await client.connect();
41
+ return client;
42
+ }
43
+
44
+ export async function createDatabase(cfg, appName, opts = {}) {
45
+ var region = "us-central1";
46
+
47
+ if (!opts.skipAppConfig) {
48
+ var appConfig = await getAppConfig(cfg, appName);
49
+ if (!appConfig) {
50
+ throw new Error(`App ${appName} not found.`);
51
+ }
52
+ if (appConfig.dbId) {
53
+ throw new Error(`App ${appName} already has a database: ${appConfig.dbId}`);
54
+ }
55
+ region = appConfig.regions?.[0] || "us-central1";
56
+ }
57
+
58
+ var token = await mintAccessToken(cfg.clientEmail, cfg.privateKey);
59
+ var instName = instanceName(appName);
60
+
61
+ // Create Cloud SQL instance
62
+ await createSqlInstance(token, cfg.project, {
63
+ name: instName,
64
+ region,
65
+ databaseVersion: "POSTGRES_15",
66
+ settings: {
67
+ tier: "db-f1-micro",
68
+ ipConfiguration: {
69
+ ipv4Enabled: true,
70
+ authorizedNetworks: [
71
+ { name: "all", value: "0.0.0.0/0" },
72
+ ],
73
+ },
74
+ backupConfiguration: { enabled: false },
75
+ },
76
+ });
77
+
78
+ // Create database
79
+ var database = dbName(appName);
80
+ await createSqlDatabase(token, cfg.project, instName, database);
81
+
82
+ // Create user with random password
83
+ var password = randomBytes(24).toString("base64url");
84
+ await createSqlUser(token, cfg.project, instName, "relight", password);
85
+
86
+ // Get public IP
87
+ var instance = await getSqlInstance(token, cfg.project, instName);
88
+ var publicIp = null;
89
+ for (var addr of (instance.ipAddresses || [])) {
90
+ if (addr.type === "PRIMARY") {
91
+ publicIp = addr.ipAddress;
92
+ break;
93
+ }
94
+ }
95
+
96
+ if (!publicIp) throw new Error("No public IP assigned to Cloud SQL instance.");
97
+
98
+ var connectionUrl = `postgresql://relight:${encodeURIComponent(password)}@${publicIp}:5432/${database}`;
99
+
100
+ if (!opts.skipAppConfig) {
101
+ // Store in app config
102
+ appConfig.dbId = instName;
103
+ appConfig.dbName = database;
104
+
105
+ if (!appConfig.envKeys) appConfig.envKeys = [];
106
+ if (!appConfig.secretKeys) appConfig.secretKeys = [];
107
+ if (!appConfig.env) appConfig.env = {};
108
+
109
+ appConfig.env["DATABASE_URL"] = connectionUrl;
110
+ if (!appConfig.envKeys.includes("DATABASE_URL")) appConfig.envKeys.push("DATABASE_URL");
111
+
112
+ appConfig.env["DB_TOKEN"] = "[hidden]";
113
+ appConfig.secretKeys = appConfig.secretKeys.filter((k) => k !== "DB_TOKEN");
114
+ appConfig.secretKeys.push("DB_TOKEN");
115
+ appConfig.envKeys = appConfig.envKeys.filter((k) => k !== "DB_TOKEN");
116
+
117
+ var newSecrets = { DB_TOKEN: password };
118
+ await pushAppConfig(cfg, appName, appConfig, { newSecrets });
119
+ }
120
+
121
+ return {
122
+ dbId: instName,
123
+ dbName: database,
124
+ dbToken: password,
125
+ connectionUrl,
126
+ };
127
+ }
128
+
129
+ export async function destroyDatabase(cfg, appName, opts = {}) {
130
+ var dbId = opts.dbId;
131
+ if (!dbId) {
132
+ var appConfig = await getAppConfig(cfg, appName);
133
+ if (!appConfig || !appConfig.dbId) {
134
+ throw new Error(`App ${appName} does not have a database.`);
135
+ }
136
+ dbId = appConfig.dbId;
137
+ }
138
+
139
+ var token = await mintAccessToken(cfg.clientEmail, cfg.privateKey);
140
+ await deleteSqlInstance(token, cfg.project, dbId);
141
+
142
+ if (!opts.dbId) {
143
+ delete appConfig.dbId;
144
+ delete appConfig.dbName;
145
+
146
+ if (appConfig.env) {
147
+ delete appConfig.env["DATABASE_URL"];
148
+ delete appConfig.env["DB_TOKEN"];
149
+ }
150
+ if (appConfig.envKeys) appConfig.envKeys = appConfig.envKeys.filter((k) => k !== "DATABASE_URL");
151
+ if (appConfig.secretKeys) appConfig.secretKeys = appConfig.secretKeys.filter((k) => k !== "DB_TOKEN");
152
+
153
+ await pushAppConfig(cfg, appName, appConfig);
154
+ }
155
+ }
156
+
157
+ export async function getDatabaseInfo(cfg, appName, opts = {}) {
158
+ var dbId = opts.dbId;
159
+ var dbNameVal;
160
+ if (!dbId) {
161
+ var appConfig = await getAppConfig(cfg, appName);
162
+ if (!appConfig || !appConfig.dbId) {
163
+ throw new Error(`App ${appName} does not have a database.`);
164
+ }
165
+ dbId = appConfig.dbId;
166
+ dbNameVal = appConfig.dbName;
167
+ }
168
+
169
+ var token = await mintAccessToken(cfg.clientEmail, cfg.privateKey);
170
+ var instance = await getSqlInstance(token, cfg.project, dbId);
171
+
172
+ var publicIp = null;
173
+ for (var addr of (instance.ipAddresses || [])) {
174
+ if (addr.type === "PRIMARY") {
175
+ publicIp = addr.ipAddress;
176
+ break;
177
+ }
178
+ }
179
+
180
+ var connectionUrl = publicIp
181
+ ? `postgresql://relight:****@${publicIp}:5432/${appConfig.dbName}`
182
+ : null;
183
+
184
+ return {
185
+ dbId,
186
+ dbName: dbNameVal || dbName(appName),
187
+ connectionUrl,
188
+ size: null,
189
+ numTables: null,
190
+ createdAt: instance.createTime || null,
191
+ };
192
+ }
193
+
194
+ export async function queryDatabase(cfg, appName, sql, params, opts = {}) {
195
+ var dbId = opts.dbId;
196
+ var database;
197
+ if (!dbId) {
198
+ var appConfig = await getAppConfig(cfg, appName);
199
+ if (!appConfig || !appConfig.dbId) {
200
+ throw new Error(`App ${appName} does not have a database.`);
201
+ }
202
+ dbId = appConfig.dbId;
203
+ database = appConfig.dbName;
204
+ } else {
205
+ database = dbName(appName);
206
+ }
207
+
208
+ var password = await getDbPassword(cfg, appName);
209
+ var token = await mintAccessToken(cfg.clientEmail, cfg.privateKey);
210
+ var instance = await getSqlInstance(token, cfg.project, dbId);
211
+
212
+ var publicIp = null;
213
+ for (var addr of (instance.ipAddresses || [])) {
214
+ if (addr.type === "PRIMARY") {
215
+ publicIp = addr.ipAddress;
216
+ break;
217
+ }
218
+ }
219
+
220
+ var connectionUrl = `postgresql://relight:${encodeURIComponent(password)}@${publicIp}:5432/${database}`;
221
+ var client = await connectPg(connectionUrl);
222
+
223
+ try {
224
+ var result = await client.query(sql, params || []);
225
+ return {
226
+ results: result.rows,
227
+ meta: { changes: result.rowCount, rows_read: result.rows.length },
228
+ };
229
+ } finally {
230
+ await client.end();
231
+ }
232
+ }
233
+
234
+ export async function importDatabase(cfg, appName, sqlContent, opts = {}) {
235
+ var dbId = opts.dbId;
236
+ var database;
237
+ if (!dbId) {
238
+ var appConfig = await getAppConfig(cfg, appName);
239
+ if (!appConfig || !appConfig.dbId) {
240
+ throw new Error(`App ${appName} does not have a database.`);
241
+ }
242
+ dbId = appConfig.dbId;
243
+ database = appConfig.dbName;
244
+ } else {
245
+ database = dbName(appName);
246
+ }
247
+
248
+ var password = await getDbPassword(cfg, appName);
249
+ var token = await mintAccessToken(cfg.clientEmail, cfg.privateKey);
250
+ var instance = await getSqlInstance(token, cfg.project, dbId);
251
+
252
+ var publicIp = null;
253
+ for (var addr of (instance.ipAddresses || [])) {
254
+ if (addr.type === "PRIMARY") {
255
+ publicIp = addr.ipAddress;
256
+ break;
257
+ }
258
+ }
259
+
260
+ var connectionUrl = `postgresql://relight:${encodeURIComponent(password)}@${publicIp}:5432/${database}`;
261
+ var client = await connectPg(connectionUrl);
262
+
263
+ try {
264
+ await client.query(sqlContent);
265
+ } finally {
266
+ await client.end();
267
+ }
268
+ }
269
+
270
+ export async function exportDatabase(cfg, appName, opts = {}) {
271
+ var dbId = opts.dbId;
272
+ var database;
273
+ if (!dbId) {
274
+ var appConfig = await getAppConfig(cfg, appName);
275
+ if (!appConfig || !appConfig.dbId) {
276
+ throw new Error(`App ${appName} does not have a database.`);
277
+ }
278
+ dbId = appConfig.dbId;
279
+ database = appConfig.dbName;
280
+ } else {
281
+ database = dbName(appName);
282
+ }
283
+
284
+ var password = await getDbPassword(cfg, appName);
285
+ var token = await mintAccessToken(cfg.clientEmail, cfg.privateKey);
286
+ var instance = await getSqlInstance(token, cfg.project, dbId);
287
+
288
+ var publicIp = null;
289
+ for (var addr of (instance.ipAddresses || [])) {
290
+ if (addr.type === "PRIMARY") {
291
+ publicIp = addr.ipAddress;
292
+ break;
293
+ }
294
+ }
295
+
296
+ var connectionUrl = `postgresql://relight:${encodeURIComponent(password)}@${publicIp}:5432/${database}`;
297
+ var client = await connectPg(connectionUrl);
298
+
299
+ try {
300
+ // Get all user tables
301
+ var tablesRes = await client.query(
302
+ "SELECT tablename FROM pg_tables WHERE schemaname = 'public' ORDER BY tablename"
303
+ );
304
+ var tables = tablesRes.rows.map((r) => r.tablename);
305
+
306
+ var dump = [];
307
+ dump.push("-- PostgreSQL dump generated by relight");
308
+ dump.push(`-- Database: ${database}`);
309
+ dump.push(`-- Date: ${new Date().toISOString()}`);
310
+ dump.push("");
311
+
312
+ for (var t of tables) {
313
+ // Get CREATE TABLE via information_schema
314
+ var colsRes = await client.query(
315
+ `SELECT column_name, data_type, is_nullable, column_default
316
+ FROM information_schema.columns
317
+ WHERE table_name = $1 AND table_schema = 'public'
318
+ ORDER BY ordinal_position`,
319
+ [t]
320
+ );
321
+
322
+ var cols = colsRes.rows.map((c) => {
323
+ var def = ` "${c.column_name}" ${c.data_type}`;
324
+ if (c.column_default) def += ` DEFAULT ${c.column_default}`;
325
+ if (c.is_nullable === "NO") def += " NOT NULL";
326
+ return def;
327
+ });
328
+
329
+ dump.push(`CREATE TABLE IF NOT EXISTS "${t}" (`);
330
+ dump.push(cols.join(",\n"));
331
+ dump.push(");");
332
+ dump.push("");
333
+
334
+ // Dump data
335
+ var dataRes = await client.query(`SELECT * FROM "${t}"`);
336
+ for (var row of dataRes.rows) {
337
+ var values = Object.values(row).map((v) => {
338
+ if (v === null) return "NULL";
339
+ if (typeof v === "number") return String(v);
340
+ if (typeof v === "boolean") return v ? "TRUE" : "FALSE";
341
+ return "'" + String(v).replace(/'/g, "''") + "'";
342
+ });
343
+ var colNames = Object.keys(row).map((c) => `"${c}"`).join(", ");
344
+ dump.push(`INSERT INTO "${t}" (${colNames}) VALUES (${values.join(", ")});`);
345
+ }
346
+ dump.push("");
347
+ }
348
+
349
+ return dump.join("\n");
350
+ } finally {
351
+ await client.end();
352
+ }
353
+ }
354
+
355
+ export async function rotateToken(cfg, appName, opts = {}) {
356
+ var dbId = opts.dbId;
357
+ var database;
358
+ if (!dbId) {
359
+ var appConfig = await getAppConfig(cfg, appName);
360
+ if (!appConfig || !appConfig.dbId) {
361
+ throw new Error(`App ${appName} does not have a database.`);
362
+ }
363
+ dbId = appConfig.dbId;
364
+ database = appConfig.dbName;
365
+ } else {
366
+ database = dbName(appName);
367
+ }
368
+
369
+ var token = await mintAccessToken(cfg.clientEmail, cfg.privateKey);
370
+ var newPassword = randomBytes(24).toString("base64url");
371
+
372
+ // Update SQL user password
373
+ await updateSqlUser(token, cfg.project, dbId, "relight", newPassword);
374
+
375
+ // Get public IP for connection URL
376
+ var instance = await getSqlInstance(token, cfg.project, dbId);
377
+ var publicIp = null;
378
+ for (var addr of (instance.ipAddresses || [])) {
379
+ if (addr.type === "PRIMARY") {
380
+ publicIp = addr.ipAddress;
381
+ break;
382
+ }
383
+ }
384
+
385
+ var connectionUrl = publicIp
386
+ ? `postgresql://relight:${encodeURIComponent(newPassword)}@${publicIp}:5432/${database}`
387
+ : null;
388
+
389
+ if (!opts.skipAppConfig) {
390
+ if (!appConfig) {
391
+ appConfig = await getAppConfig(cfg, appName);
392
+ }
393
+
394
+ // Update app config
395
+ if (!appConfig.envKeys) appConfig.envKeys = [];
396
+ if (!appConfig.secretKeys) appConfig.secretKeys = [];
397
+ if (!appConfig.env) appConfig.env = {};
398
+
399
+ appConfig.env["DB_TOKEN"] = "[hidden]";
400
+ if (!appConfig.secretKeys.includes("DB_TOKEN")) appConfig.secretKeys.push("DB_TOKEN");
401
+ appConfig.envKeys = appConfig.envKeys.filter((k) => k !== "DB_TOKEN");
402
+
403
+ if (connectionUrl) {
404
+ appConfig.env["DATABASE_URL"] = connectionUrl;
405
+ if (!appConfig.envKeys.includes("DATABASE_URL")) appConfig.envKeys.push("DATABASE_URL");
406
+ }
407
+
408
+ await pushAppConfig(cfg, appName, appConfig, { newSecrets: { DB_TOKEN: newPassword } });
409
+ }
410
+
411
+ return { dbToken: newPassword, connectionUrl };
412
+ }
413
+
414
+ export async function resetDatabase(cfg, appName, opts = {}) {
415
+ var dbId = opts.dbId;
416
+ var database;
417
+ if (!dbId) {
418
+ var appConfig = await getAppConfig(cfg, appName);
419
+ if (!appConfig || !appConfig.dbId) {
420
+ throw new Error(`App ${appName} does not have a database.`);
421
+ }
422
+ dbId = appConfig.dbId;
423
+ database = appConfig.dbName;
424
+ } else {
425
+ database = dbName(appName);
426
+ }
427
+
428
+ var password = await getDbPassword(cfg, appName);
429
+ var token = await mintAccessToken(cfg.clientEmail, cfg.privateKey);
430
+ var instance = await getSqlInstance(token, cfg.project, dbId);
431
+
432
+ var publicIp = null;
433
+ for (var addr of (instance.ipAddresses || [])) {
434
+ if (addr.type === "PRIMARY") {
435
+ publicIp = addr.ipAddress;
436
+ break;
437
+ }
438
+ }
439
+
440
+ var connectionUrl = `postgresql://relight:${encodeURIComponent(password)}@${publicIp}:5432/${database}`;
441
+ var client = await connectPg(connectionUrl);
442
+
443
+ try {
444
+ var tablesRes = await client.query(
445
+ "SELECT tablename FROM pg_tables WHERE schemaname = 'public'"
446
+ );
447
+ var tables = tablesRes.rows.map((r) => r.tablename);
448
+
449
+ for (var t of tables) {
450
+ await client.query(`DROP TABLE IF EXISTS "${t}" CASCADE`);
451
+ }
452
+
453
+ return tables;
454
+ } finally {
455
+ await client.end();
456
+ }
457
+ }
@@ -0,0 +1,166 @@
1
+ import {
2
+ mintAccessToken,
3
+ listManagedZones,
4
+ createDnsChange,
5
+ listResourceRecordSets,
6
+ } from "../../clouds/gcp.js";
7
+ import { getAppConfig, pushAppConfig, getAppUrl } from "./app.js";
8
+
9
+ export async function getZones(cfg) {
10
+ var token = await mintAccessToken(cfg.clientEmail, cfg.privateKey);
11
+ var zones = await listManagedZones(token, cfg.project);
12
+ return zones.map((z) => ({
13
+ id: z.name,
14
+ name: z.dnsName.replace(/\.$/, ""),
15
+ }));
16
+ }
17
+
18
+ export function findZoneForHostname(zones, hostname) {
19
+ var match = null;
20
+ for (var zone of zones) {
21
+ if (hostname === zone.name || hostname.endsWith("." + zone.name)) {
22
+ if (!match || zone.name.length > match.name.length) {
23
+ match = zone;
24
+ }
25
+ }
26
+ }
27
+ return match;
28
+ }
29
+
30
+ export async function listDomains(cfg, appName) {
31
+ var token = await mintAccessToken(cfg.clientEmail, cfg.privateKey);
32
+
33
+ // Get Cloud Run service URL as default domain
34
+ var url = await getAppUrl(cfg, appName);
35
+ var defaultDomain = url ? new URL(url).hostname : null;
36
+
37
+ // Get custom domains from app config
38
+ var appConfig = await getAppConfig(cfg, appName);
39
+ var custom = appConfig?.domains || [];
40
+
41
+ return {
42
+ default: defaultDomain,
43
+ custom,
44
+ };
45
+ }
46
+
47
+ export async function addDomain(cfg, appName, domain, { zone }) {
48
+ var token = await mintAccessToken(cfg.clientEmail, cfg.privateKey);
49
+
50
+ // Get Cloud Run URL to use as CNAME target
51
+ var url = await getAppUrl(cfg, appName);
52
+ if (!url) throw new Error("Could not determine app URL for CNAME target.");
53
+ var target = new URL(url).hostname;
54
+
55
+ // FQDN for Cloud DNS (trailing dot)
56
+ var fqdn = domain.endsWith(".") ? domain : domain + ".";
57
+ var targetFqdn = target.endsWith(".") ? target : target + ".";
58
+
59
+ // Check for existing records
60
+ var records = await listResourceRecordSets(token, cfg.project, zone.id);
61
+ var existing = records.find(
62
+ (r) => r.name === fqdn && r.type === "CNAME"
63
+ );
64
+ if (existing) {
65
+ throw new Error(
66
+ `CNAME record already exists for ${domain} -> ${existing.rrdatas.join(", ")}`
67
+ );
68
+ }
69
+
70
+ // Create CNAME record
71
+ await createDnsChange(token, cfg.project, zone.id, {
72
+ additions: [
73
+ {
74
+ name: fqdn,
75
+ type: "CNAME",
76
+ ttl: 300,
77
+ rrdatas: [targetFqdn],
78
+ },
79
+ ],
80
+ });
81
+
82
+ // Update app config
83
+ var appConfig = await getAppConfig(cfg, appName);
84
+ if (appConfig) {
85
+ if (!appConfig.domains) appConfig.domains = [];
86
+ if (!appConfig.domains.includes(domain)) {
87
+ appConfig.domains.push(domain);
88
+ await pushAppConfig(cfg, appName, appConfig);
89
+ }
90
+ }
91
+ }
92
+
93
+ export async function removeDomain(cfg, appName, domain) {
94
+ var token = await mintAccessToken(cfg.clientEmail, cfg.privateKey);
95
+ var fqdn = domain.endsWith(".") ? domain : domain + ".";
96
+
97
+ // Find the zone for this domain
98
+ var zones = await getZones(cfg);
99
+ var zone = findZoneForHostname(zones, domain);
100
+
101
+ if (zone) {
102
+ // Find and delete CNAME record
103
+ var records = await listResourceRecordSets(token, cfg.project, zone.id);
104
+ var existing = records.find(
105
+ (r) => r.name === fqdn && r.type === "CNAME"
106
+ );
107
+ if (existing) {
108
+ await createDnsChange(token, cfg.project, zone.id, {
109
+ deletions: [existing],
110
+ });
111
+ }
112
+ }
113
+
114
+ // Update app config
115
+ var appConfig = await getAppConfig(cfg, appName);
116
+ if (appConfig) {
117
+ appConfig.domains = (appConfig.domains || []).filter((d) => d !== domain);
118
+ await pushAppConfig(cfg, appName, appConfig);
119
+ }
120
+ }
121
+
122
+ // --- Pure DNS record operations (for cross-cloud use) ---
123
+
124
+ export async function addDnsRecord(cfg, domain, target, zone) {
125
+ var token = await mintAccessToken(cfg.clientEmail, cfg.privateKey);
126
+ var fqdn = domain.endsWith(".") ? domain : domain + ".";
127
+ var targetFqdn = target.endsWith(".") ? target : target + ".";
128
+
129
+ // Check for existing CNAME
130
+ var records = await listResourceRecordSets(token, cfg.project, zone.id);
131
+ var existing = records.find((r) => r.name === fqdn && r.type === "CNAME");
132
+ if (existing) {
133
+ throw new Error(
134
+ `CNAME record already exists for ${domain} -> ${existing.rrdatas.join(", ")}`
135
+ );
136
+ }
137
+
138
+ // Create CNAME record
139
+ await createDnsChange(token, cfg.project, zone.id, {
140
+ additions: [
141
+ {
142
+ name: fqdn,
143
+ type: "CNAME",
144
+ ttl: 300,
145
+ rrdatas: [targetFqdn],
146
+ },
147
+ ],
148
+ });
149
+ }
150
+
151
+ export async function removeDnsRecord(cfg, domain) {
152
+ var token = await mintAccessToken(cfg.clientEmail, cfg.privateKey);
153
+ var fqdn = domain.endsWith(".") ? domain : domain + ".";
154
+
155
+ var zones = await getZones(cfg);
156
+ var zone = findZoneForHostname(zones, domain);
157
+ if (!zone) return;
158
+
159
+ var records = await listResourceRecordSets(token, cfg.project, zone.id);
160
+ var existing = records.find((r) => r.name === fqdn && r.type === "CNAME");
161
+ if (existing) {
162
+ await createDnsChange(token, cfg.project, zone.id, {
163
+ deletions: [existing],
164
+ });
165
+ }
166
+ }
@@ -0,0 +1,30 @@
1
+ import {
2
+ mintAccessToken,
3
+ getRepository,
4
+ createRepository,
5
+ } from "../../clouds/gcp.js";
6
+
7
+ var AR_LOCATION = "us";
8
+ var AR_HOST = "us-docker.pkg.dev";
9
+ var REPO_NAME = "relight";
10
+
11
+ export async function getCredentials(cfg) {
12
+ var token = await mintAccessToken(cfg.clientEmail, cfg.privateKey);
13
+
14
+ // Ensure repository exists (idempotent)
15
+ try {
16
+ await getRepository(token, cfg.project, AR_LOCATION, REPO_NAME);
17
+ } catch {
18
+ await createRepository(token, cfg.project, AR_LOCATION, REPO_NAME);
19
+ }
20
+
21
+ return {
22
+ registry: `https://${AR_HOST}`,
23
+ username: "oauth2accesstoken",
24
+ password: token,
25
+ };
26
+ }
27
+
28
+ export function getImageTag(cfg, appName, tag) {
29
+ return `${AR_HOST}/${cfg.project}/${REPO_NAME}/relight-${appName}:${tag}`;
30
+ }
@@ -0,0 +1,49 @@
1
+ import { resolveCloud } from "../link.js";
2
+ import { getDefaultCloud, resolveCloudConfig, CLOUD_NAMES } from "../config.js";
3
+ import { fatal, fmt } from "../output.js";
4
+
5
+ var LAYERS = ["app", "dns", "db", "registry"];
6
+
7
+ export function getProvider(cloudId, layer) {
8
+ if (!LAYERS.includes(layer)) {
9
+ throw new Error(`Unknown provider layer: ${layer}`);
10
+ }
11
+
12
+ var providers = {
13
+ cf: () => import(`./cf/${layer}.js`),
14
+ gcp: () => import(`./gcp/${layer}.js`),
15
+ aws: () => import(`./aws/${layer}.js`),
16
+ slicervm: () => import(`./slicervm/${layer}.js`),
17
+ };
18
+
19
+ if (!providers[cloudId]) {
20
+ fatal(
21
+ `Unknown cloud: ${cloudId}`,
22
+ `Supported: ${Object.keys(CLOUD_NAMES).join(", ")}`
23
+ );
24
+ }
25
+
26
+ return providers[cloudId]();
27
+ }
28
+
29
+ export function resolveCloudId(cloudFlag) {
30
+ // --cloud flag > .relight file > config.default_cloud > fatal
31
+ var cloud = cloudFlag || resolveCloud(null) || getDefaultCloud();
32
+ if (!cloud) {
33
+ fatal(
34
+ "No cloud specified.",
35
+ `Use ${fmt.cmd("--cloud <cf|gcp|aws|slicervm>")} or set a default with ${fmt.cmd("relight auth")}.`
36
+ );
37
+ }
38
+ if (!CLOUD_NAMES[cloud]) {
39
+ fatal(
40
+ `Unknown cloud: ${cloud}`,
41
+ `Supported: ${Object.keys(CLOUD_NAMES).join(", ")}`
42
+ );
43
+ }
44
+ return cloud;
45
+ }
46
+
47
+ export function getCloudCfg(cloudId) {
48
+ return resolveCloudConfig(cloudId);
49
+ }