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,279 @@
1
+ import {
2
+ createD1Database,
3
+ deleteD1Database,
4
+ getD1Database,
5
+ queryD1,
6
+ exportD1,
7
+ importD1,
8
+ getWorkersSubdomain,
9
+ } from "../../clouds/cf.js";
10
+ import { getAppConfig, pushAppConfig } from "./app.js";
11
+ import { randomBytes } from "crypto";
12
+
13
+ export async function createDatabase(cfg, appName, opts = {}) {
14
+ if (!opts.skipAppConfig) {
15
+ var appConfig = await getAppConfig(cfg, appName);
16
+ if (!appConfig) {
17
+ throw new Error(`App ${appName} not found.`);
18
+ }
19
+ if (appConfig.dbId) {
20
+ throw new Error(`App ${appName} already has a database: ${appConfig.dbName}`);
21
+ }
22
+ }
23
+
24
+ var dbName = `relight-${appName}`;
25
+ var result = await createD1Database(cfg.accountId, cfg.apiToken, dbName, {
26
+ locationHint: opts.location,
27
+ jurisdiction: opts.jurisdiction,
28
+ });
29
+
30
+ var subdomain = await getWorkersSubdomain(cfg.accountId, cfg.apiToken);
31
+ var connectionUrl = subdomain
32
+ ? `https://relight-${appName}.${subdomain}.workers.dev`
33
+ : null;
34
+
35
+ var dbToken = randomBytes(32).toString("hex");
36
+
37
+ if (!opts.skipAppConfig) {
38
+ appConfig.dbId = result.uuid;
39
+ appConfig.dbName = dbName;
40
+
41
+ if (!appConfig.envKeys) appConfig.envKeys = [];
42
+ if (!appConfig.secretKeys) appConfig.secretKeys = [];
43
+ if (!appConfig.env) appConfig.env = {};
44
+
45
+ if (connectionUrl) {
46
+ appConfig.env["DB_URL"] = connectionUrl;
47
+ if (!appConfig.envKeys.includes("DB_URL")) appConfig.envKeys.push("DB_URL");
48
+ }
49
+
50
+ appConfig.env["DB_TOKEN"] = "[hidden]";
51
+ appConfig.secretKeys = appConfig.secretKeys.filter((k) => k !== "DB_TOKEN");
52
+ appConfig.secretKeys.push("DB_TOKEN");
53
+ appConfig.envKeys = appConfig.envKeys.filter((k) => k !== "DB_TOKEN");
54
+
55
+ var newSecrets = { DB_TOKEN: dbToken };
56
+
57
+ await pushAppConfig(cfg, appName, appConfig, { newSecrets });
58
+ }
59
+
60
+ return {
61
+ dbId: result.uuid,
62
+ dbName,
63
+ dbToken,
64
+ connectionUrl,
65
+ };
66
+ }
67
+
68
+ export async function destroyDatabase(cfg, appName, opts = {}) {
69
+ var dbId = opts.dbId;
70
+ if (!dbId) {
71
+ var appConfig = await getAppConfig(cfg, appName);
72
+ if (!appConfig || !appConfig.dbId) {
73
+ throw new Error(`App ${appName} does not have a database.`);
74
+ }
75
+ dbId = appConfig.dbId;
76
+ }
77
+
78
+ await deleteD1Database(cfg.accountId, cfg.apiToken, dbId);
79
+
80
+ if (!opts.dbId) {
81
+ delete appConfig.dbId;
82
+ delete appConfig.dbName;
83
+
84
+ if (appConfig.env) {
85
+ delete appConfig.env["DB_URL"];
86
+ delete appConfig.env["DB_TOKEN"];
87
+ }
88
+ if (appConfig.envKeys) appConfig.envKeys = appConfig.envKeys.filter((k) => k !== "DB_URL");
89
+ if (appConfig.secretKeys) appConfig.secretKeys = appConfig.secretKeys.filter((k) => k !== "DB_TOKEN");
90
+
91
+ await pushAppConfig(cfg, appName, appConfig);
92
+ }
93
+ }
94
+
95
+ export async function getDatabaseInfo(cfg, appName, opts = {}) {
96
+ var dbId = opts.dbId;
97
+ if (!dbId) {
98
+ var appConfig = await getAppConfig(cfg, appName);
99
+ if (!appConfig || !appConfig.dbId) {
100
+ throw new Error(`App ${appName} does not have a database.`);
101
+ }
102
+ dbId = appConfig.dbId;
103
+ }
104
+
105
+ var dbDetails = await getD1Database(cfg.accountId, cfg.apiToken, dbId);
106
+ var subdomain = await getWorkersSubdomain(cfg.accountId, cfg.apiToken);
107
+ var connectionUrl = subdomain
108
+ ? `https://relight-${appName}.${subdomain}.workers.dev`
109
+ : null;
110
+
111
+ return {
112
+ dbId,
113
+ dbName: dbDetails.name || `relight-${appName}`,
114
+ connectionUrl,
115
+ size: dbDetails.file_size,
116
+ numTables: dbDetails.num_tables,
117
+ createdAt: dbDetails.created_at,
118
+ };
119
+ }
120
+
121
+ export async function queryDatabase(cfg, appName, sql, params, opts = {}) {
122
+ var dbId = opts.dbId;
123
+ if (!dbId) {
124
+ var appConfig = await getAppConfig(cfg, appName);
125
+ if (!appConfig || !appConfig.dbId) {
126
+ throw new Error(`App ${appName} does not have a database.`);
127
+ }
128
+ dbId = appConfig.dbId;
129
+ }
130
+ return queryD1(cfg.accountId, cfg.apiToken, dbId, sql, params);
131
+ }
132
+
133
+ export async function importDatabase(cfg, appName, sqlContent, opts = {}) {
134
+ var dbId = opts.dbId;
135
+ if (!dbId) {
136
+ var appConfig = await getAppConfig(cfg, appName);
137
+ if (!appConfig || !appConfig.dbId) {
138
+ throw new Error(`App ${appName} does not have a database.`);
139
+ }
140
+ dbId = appConfig.dbId;
141
+ }
142
+
143
+ // Step 1: Init import
144
+ var initRes = await importD1(cfg.accountId, cfg.apiToken, dbId, {
145
+ action: "init",
146
+ });
147
+ var initResult = initRes.result || initRes;
148
+
149
+ if (!initResult.filename || !initResult.upload_url) {
150
+ throw new Error("Import init failed - no upload URL returned.");
151
+ }
152
+
153
+ // Step 2: Upload to signed URL
154
+ var uploadRes = await fetch(initResult.upload_url, {
155
+ method: "PUT",
156
+ headers: { "Content-Type": "application/octet-stream" },
157
+ body: sqlContent,
158
+ });
159
+ if (!uploadRes.ok) {
160
+ throw new Error(`Upload failed: ${uploadRes.status} ${await uploadRes.text()}`);
161
+ }
162
+
163
+ // Step 3: Ingest
164
+ var ingestRes = await importD1(cfg.accountId, cfg.apiToken, dbId, {
165
+ action: "ingest",
166
+ filename: initResult.filename,
167
+ });
168
+
169
+ // Step 4: Poll until complete
170
+ var polling = true;
171
+ while (polling) {
172
+ await new Promise((r) => setTimeout(r, 2000));
173
+ var pollRes = await importD1(cfg.accountId, cfg.apiToken, dbId, {
174
+ action: "poll",
175
+ current_bookmark: (ingestRes.result || ingestRes).at_bookmark,
176
+ });
177
+ var pollResult = pollRes.result || pollRes;
178
+ if (pollResult.status === "complete" || pollResult.type === "done") {
179
+ polling = false;
180
+ } else if (pollResult.status === "error" || pollResult.error) {
181
+ throw new Error(pollResult.error || "Unknown error during ingest.");
182
+ }
183
+ }
184
+ }
185
+
186
+ export async function exportDatabase(cfg, appName, opts = {}) {
187
+ var dbId = opts.dbId;
188
+ if (!dbId) {
189
+ var appConfig = await getAppConfig(cfg, appName);
190
+ if (!appConfig || !appConfig.dbId) {
191
+ throw new Error(`App ${appName} does not have a database.`);
192
+ }
193
+ dbId = appConfig.dbId;
194
+ }
195
+
196
+ var exportRes = await exportD1(cfg.accountId, cfg.apiToken, dbId, {
197
+ output_format: "polling",
198
+ });
199
+
200
+ var signedUrl = null;
201
+ while (!signedUrl) {
202
+ var exportResult = exportRes.result || exportRes;
203
+ if (exportResult.status === "complete" && exportResult.signed_url) {
204
+ signedUrl = exportResult.signed_url;
205
+ } else if (exportResult.status === "error") {
206
+ throw new Error(exportResult.error || "Unknown error.");
207
+ } else {
208
+ await new Promise((r) => setTimeout(r, 2000));
209
+ exportRes = await exportD1(cfg.accountId, cfg.apiToken, dbId, {
210
+ output_format: "polling",
211
+ current_bookmark: exportResult.at_bookmark,
212
+ });
213
+ }
214
+ }
215
+
216
+ // Download
217
+ var dumpRes = await fetch(signedUrl);
218
+ if (!dumpRes.ok) {
219
+ throw new Error(`Download failed: ${dumpRes.status}`);
220
+ }
221
+ return dumpRes.text();
222
+ }
223
+
224
+ export async function rotateToken(cfg, appName, opts = {}) {
225
+ var subdomain = await getWorkersSubdomain(cfg.accountId, cfg.apiToken);
226
+ var connectionUrl = subdomain
227
+ ? `https://relight-${appName}.${subdomain}.workers.dev`
228
+ : null;
229
+
230
+ var dbToken = randomBytes(32).toString("hex");
231
+
232
+ if (!opts.skipAppConfig) {
233
+ var appConfig = await getAppConfig(cfg, appName);
234
+ if (!appConfig || !appConfig.dbId) {
235
+ throw new Error(`App ${appName} does not have a database.`);
236
+ }
237
+
238
+ if (!appConfig.envKeys) appConfig.envKeys = [];
239
+ if (!appConfig.secretKeys) appConfig.secretKeys = [];
240
+ if (!appConfig.env) appConfig.env = {};
241
+
242
+ appConfig.env["DB_TOKEN"] = "[hidden]";
243
+ if (!appConfig.secretKeys.includes("DB_TOKEN")) appConfig.secretKeys.push("DB_TOKEN");
244
+ appConfig.envKeys = appConfig.envKeys.filter((k) => k !== "DB_TOKEN");
245
+
246
+ if (connectionUrl) {
247
+ appConfig.env["DB_URL"] = connectionUrl;
248
+ if (!appConfig.envKeys.includes("DB_URL")) appConfig.envKeys.push("DB_URL");
249
+ }
250
+
251
+ await pushAppConfig(cfg, appName, appConfig, { newSecrets: { DB_TOKEN: dbToken } });
252
+ }
253
+
254
+ return { dbToken, connectionUrl };
255
+ }
256
+
257
+ export async function resetDatabase(cfg, appName, opts = {}) {
258
+ var dbId = opts.dbId;
259
+ if (!dbId) {
260
+ var appConfig = await getAppConfig(cfg, appName);
261
+ if (!appConfig || !appConfig.dbId) {
262
+ throw new Error(`App ${appName} does not have a database.`);
263
+ }
264
+ dbId = appConfig.dbId;
265
+ }
266
+
267
+ var results = await queryD1(
268
+ cfg.accountId, cfg.apiToken, dbId,
269
+ "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name NOT LIKE '_cf_%'"
270
+ );
271
+ var result = Array.isArray(results) ? results[0] : results;
272
+ var tables = (result && result.results) ? result.results.map((r) => r.name) : [];
273
+
274
+ for (var t of tables) {
275
+ await queryD1(cfg.accountId, cfg.apiToken, dbId, `DROP TABLE IF EXISTS "${t}"`);
276
+ }
277
+
278
+ return tables;
279
+ }
@@ -0,0 +1,148 @@
1
+ import {
2
+ listZones,
3
+ findZoneForHostname,
4
+ listDnsRecords,
5
+ createDnsRecord,
6
+ deleteDnsRecord,
7
+ addWorkerDomain,
8
+ removeWorkerDomain,
9
+ listWorkerDomainsForService,
10
+ getWorkersSubdomain,
11
+ } from "../../clouds/cf.js";
12
+ import { getAppConfig, pushAppConfig } from "./app.js";
13
+
14
+ export async function listDomains(cfg, appName) {
15
+ var scriptName = `relight-${appName}`;
16
+ var subdomain = await getWorkersSubdomain(cfg.accountId, cfg.apiToken);
17
+ var defaultDomain = subdomain
18
+ ? `relight-${appName}.${subdomain}.workers.dev`
19
+ : null;
20
+
21
+ var domains = await listWorkerDomainsForService(cfg.accountId, cfg.apiToken, scriptName);
22
+
23
+ return {
24
+ default: defaultDomain,
25
+ custom: domains.map((d) => d.hostname),
26
+ };
27
+ }
28
+
29
+ export async function addDomain(cfg, appName, domain, { zone, zones }) {
30
+ var scriptName = `relight-${appName}`;
31
+
32
+ // Check for existing DNS records
33
+ var existing = await listDnsRecords(cfg.accountId, cfg.apiToken, zone.id, { name: domain });
34
+ if (existing.length > 0) {
35
+ var types = existing.map((r) => `${r.type} -> ${r.content}`).join("\n ");
36
+ throw new Error(
37
+ `DNS record already exists for ${domain}.\nExisting records:\n ${types}\n\nRemove the existing record first, or choose a different domain.`
38
+ );
39
+ }
40
+
41
+ // Attach domain to worker
42
+ try {
43
+ await addWorkerDomain(cfg.accountId, cfg.apiToken, scriptName, domain, zone.id);
44
+ } catch (e) {
45
+ if (e.message.includes("already has externally managed DNS records")) {
46
+ throw new Error(
47
+ `DNS record already exists for ${domain} (externally managed). Remove the existing DNS record first, or choose a different domain.`
48
+ );
49
+ }
50
+ throw e;
51
+ }
52
+
53
+ // Create CNAME record
54
+ var subdomain = await getWorkersSubdomain(cfg.accountId, cfg.apiToken);
55
+ var target = subdomain ? `relight-${appName}.${subdomain}.workers.dev` : null;
56
+
57
+ if (target) {
58
+ try {
59
+ await createDnsRecord(cfg.accountId, cfg.apiToken, zone.id, {
60
+ type: "CNAME",
61
+ name: domain,
62
+ content: target,
63
+ proxied: true,
64
+ });
65
+ } catch (e) {
66
+ if (!e.message.includes("already exists")) {
67
+ // Not fatal - worker domain is already attached
68
+ }
69
+ }
70
+ }
71
+
72
+ // Update app config metadata
73
+ var appConfig = await getAppConfig(cfg, appName);
74
+ if (appConfig) {
75
+ if (!appConfig.domains) appConfig.domains = [];
76
+ if (!appConfig.domains.includes(domain)) {
77
+ appConfig.domains.push(domain);
78
+ await pushAppConfig(cfg, appName, appConfig);
79
+ }
80
+ }
81
+ }
82
+
83
+ export async function removeDomain(cfg, appName, domain) {
84
+ // Remove Worker Domain route
85
+ await removeWorkerDomain(cfg.accountId, cfg.apiToken, domain);
86
+
87
+ // Remove CNAME record if it exists
88
+ var cfZones = await listZones(cfg.accountId, cfg.apiToken);
89
+ var zone = findZoneForHostname(cfZones, domain);
90
+ if (zone) {
91
+ var records = await listDnsRecords(cfg.accountId, cfg.apiToken, zone.id, {
92
+ type: "CNAME",
93
+ name: domain,
94
+ });
95
+ for (var record of records) {
96
+ await deleteDnsRecord(cfg.accountId, cfg.apiToken, zone.id, record.id);
97
+ }
98
+ }
99
+
100
+ // Update app config metadata
101
+ var appConfig = await getAppConfig(cfg, appName);
102
+ if (appConfig) {
103
+ appConfig.domains = (appConfig.domains || []).filter((d) => d !== domain);
104
+ await pushAppConfig(cfg, appName, appConfig);
105
+ }
106
+ }
107
+
108
+ // --- Pure DNS record operations (for cross-cloud use) ---
109
+
110
+ export async function addDnsRecord(cfg, domain, target, zone) {
111
+ // Check for existing records
112
+ var existing = await listDnsRecords(cfg.accountId, cfg.apiToken, zone.id, { name: domain });
113
+ if (existing.length > 0) {
114
+ var types = existing.map((r) => `${r.type} -> ${r.content}`).join("\n ");
115
+ throw new Error(
116
+ `DNS record already exists for ${domain}.\nExisting records:\n ${types}\n\nRemove the existing record first, or choose a different domain.`
117
+ );
118
+ }
119
+
120
+ // Create CNAME: domain -> target, proxied
121
+ await createDnsRecord(cfg.accountId, cfg.apiToken, zone.id, {
122
+ type: "CNAME",
123
+ name: domain,
124
+ content: target,
125
+ proxied: true,
126
+ });
127
+ }
128
+
129
+ export async function removeDnsRecord(cfg, domain) {
130
+ var cfZones = await listZones(cfg.accountId, cfg.apiToken);
131
+ var zone = findZoneForHostname(cfZones, domain);
132
+ if (!zone) return;
133
+
134
+ var records = await listDnsRecords(cfg.accountId, cfg.apiToken, zone.id, {
135
+ type: "CNAME",
136
+ name: domain,
137
+ });
138
+ for (var record of records) {
139
+ await deleteDnsRecord(cfg.accountId, cfg.apiToken, zone.id, record.id);
140
+ }
141
+ }
142
+
143
+ // Re-export zone utilities for the domains command's interactive flow
144
+ export async function getZones(cfg) {
145
+ return listZones(cfg.accountId, cfg.apiToken);
146
+ }
147
+
148
+ export { findZoneForHostname };
@@ -0,0 +1,17 @@
1
+ import {
2
+ CF_REGISTRY,
3
+ getRegistryCredentials,
4
+ } from "../../clouds/cf.js";
5
+
6
+ export async function getCredentials(cfg) {
7
+ var creds = await getRegistryCredentials(cfg.accountId, cfg.apiToken);
8
+ return {
9
+ registry: CF_REGISTRY,
10
+ username: creds.username,
11
+ password: creds.password,
12
+ };
13
+ }
14
+
15
+ export function getImageTag(cfg, appName, tag) {
16
+ return `${CF_REGISTRY}/${cfg.accountId}/relight-${appName}:${tag}`;
17
+ }