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,181 @@
1
+ import {
2
+ createD1Database,
3
+ deleteD1Database,
4
+ getD1Database,
5
+ queryD1,
6
+ exportD1,
7
+ importD1,
8
+ getWorkersSubdomain,
9
+ } from "../../clouds/cf.js";
10
+ import { randomBytes } from "crypto";
11
+
12
+ export async function createDatabase(cfg, name, opts = {}) {
13
+ var d1Name = `relight-${name}`;
14
+ var result = await createD1Database(cfg.accountId, cfg.apiToken, d1Name, {
15
+ locationHint: opts.location,
16
+ jurisdiction: opts.jurisdiction,
17
+ });
18
+
19
+ var subdomain = await getWorkersSubdomain(cfg.accountId, cfg.apiToken);
20
+ var connectionUrl = subdomain
21
+ ? `https://relight-${name}.${subdomain}.workers.dev`
22
+ : null;
23
+
24
+ var dbToken = randomBytes(32).toString("hex");
25
+
26
+ return {
27
+ dbId: result.uuid,
28
+ dbName: d1Name,
29
+ dbToken,
30
+ connectionUrl,
31
+ };
32
+ }
33
+
34
+ export async function destroyDatabase(cfg, name, opts = {}) {
35
+ if (!opts.dbId) {
36
+ throw new Error("dbId is required to destroy a CF database.");
37
+ }
38
+ await deleteD1Database(cfg.accountId, cfg.apiToken, opts.dbId);
39
+ }
40
+
41
+ export async function getDatabaseInfo(cfg, name, opts = {}) {
42
+ if (!opts.dbId) {
43
+ throw new Error("dbId is required to get CF database info.");
44
+ }
45
+
46
+ var dbDetails = await getD1Database(cfg.accountId, cfg.apiToken, opts.dbId);
47
+ var subdomain = await getWorkersSubdomain(cfg.accountId, cfg.apiToken);
48
+ var connectionUrl = subdomain
49
+ ? `https://relight-${name}.${subdomain}.workers.dev`
50
+ : null;
51
+
52
+ return {
53
+ dbId: opts.dbId,
54
+ dbName: dbDetails.name || `relight-${name}`,
55
+ connectionUrl,
56
+ size: dbDetails.file_size,
57
+ numTables: dbDetails.num_tables,
58
+ createdAt: dbDetails.created_at,
59
+ };
60
+ }
61
+
62
+ export async function queryDatabase(cfg, name, sql, params, opts = {}) {
63
+ if (!opts.dbId) {
64
+ throw new Error("dbId is required to query a CF database.");
65
+ }
66
+ return queryD1(cfg.accountId, cfg.apiToken, opts.dbId, sql, params);
67
+ }
68
+
69
+ export async function importDatabase(cfg, name, sqlContent, opts = {}) {
70
+ if (!opts.dbId) {
71
+ throw new Error("dbId is required to import into a CF database.");
72
+ }
73
+ var dbId = opts.dbId;
74
+
75
+ // Step 1: Init import
76
+ var initRes = await importD1(cfg.accountId, cfg.apiToken, dbId, {
77
+ action: "init",
78
+ });
79
+ var initResult = initRes.result || initRes;
80
+
81
+ if (!initResult.filename || !initResult.upload_url) {
82
+ throw new Error("Import init failed - no upload URL returned.");
83
+ }
84
+
85
+ // Step 2: Upload to signed URL
86
+ var uploadRes = await fetch(initResult.upload_url, {
87
+ method: "PUT",
88
+ headers: { "Content-Type": "application/octet-stream" },
89
+ body: sqlContent,
90
+ });
91
+ if (!uploadRes.ok) {
92
+ throw new Error(`Upload failed: ${uploadRes.status} ${await uploadRes.text()}`);
93
+ }
94
+
95
+ // Step 3: Ingest
96
+ var ingestRes = await importD1(cfg.accountId, cfg.apiToken, dbId, {
97
+ action: "ingest",
98
+ filename: initResult.filename,
99
+ });
100
+
101
+ // Step 4: Poll until complete
102
+ var polling = true;
103
+ while (polling) {
104
+ await new Promise((r) => setTimeout(r, 2000));
105
+ var pollRes = await importD1(cfg.accountId, cfg.apiToken, dbId, {
106
+ action: "poll",
107
+ current_bookmark: (ingestRes.result || ingestRes).at_bookmark,
108
+ });
109
+ var pollResult = pollRes.result || pollRes;
110
+ if (pollResult.status === "complete" || pollResult.type === "done") {
111
+ polling = false;
112
+ } else if (pollResult.status === "error" || pollResult.error) {
113
+ throw new Error(pollResult.error || "Unknown error during ingest.");
114
+ }
115
+ }
116
+ }
117
+
118
+ export async function exportDatabase(cfg, name, opts = {}) {
119
+ if (!opts.dbId) {
120
+ throw new Error("dbId is required to export a CF database.");
121
+ }
122
+ var dbId = opts.dbId;
123
+
124
+ var exportRes = await exportD1(cfg.accountId, cfg.apiToken, dbId, {
125
+ output_format: "polling",
126
+ });
127
+
128
+ var signedUrl = null;
129
+ while (!signedUrl) {
130
+ var exportResult = exportRes.result || exportRes;
131
+ if (exportResult.status === "complete" && exportResult.signed_url) {
132
+ signedUrl = exportResult.signed_url;
133
+ } else if (exportResult.status === "error") {
134
+ throw new Error(exportResult.error || "Unknown error.");
135
+ } else {
136
+ await new Promise((r) => setTimeout(r, 2000));
137
+ exportRes = await exportD1(cfg.accountId, cfg.apiToken, dbId, {
138
+ output_format: "polling",
139
+ current_bookmark: exportResult.at_bookmark,
140
+ });
141
+ }
142
+ }
143
+
144
+ // Download
145
+ var dumpRes = await fetch(signedUrl);
146
+ if (!dumpRes.ok) {
147
+ throw new Error(`Download failed: ${dumpRes.status}`);
148
+ }
149
+ return dumpRes.text();
150
+ }
151
+
152
+ export async function rotateToken(cfg, name, opts = {}) {
153
+ var subdomain = await getWorkersSubdomain(cfg.accountId, cfg.apiToken);
154
+ var connectionUrl = subdomain
155
+ ? `https://relight-${name}.${subdomain}.workers.dev`
156
+ : null;
157
+
158
+ var dbToken = randomBytes(32).toString("hex");
159
+
160
+ return { dbToken, connectionUrl };
161
+ }
162
+
163
+ export async function resetDatabase(cfg, name, opts = {}) {
164
+ if (!opts.dbId) {
165
+ throw new Error("dbId is required to reset a CF database.");
166
+ }
167
+ var dbId = opts.dbId;
168
+
169
+ var results = await queryD1(
170
+ cfg.accountId, cfg.apiToken, dbId,
171
+ "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name NOT LIKE '_cf_%'"
172
+ );
173
+ var result = Array.isArray(results) ? results[0] : results;
174
+ var tables = (result && result.results) ? result.results.map((r) => r.name) : [];
175
+
176
+ for (var t of tables) {
177
+ await queryD1(cfg.accountId, cfg.apiToken, dbId, `DROP TABLE IF EXISTS "${t}"`);
178
+ }
179
+
180
+ return tables;
181
+ }
@@ -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
+ }