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.
- package/README.md +77 -34
- package/package.json +12 -4
- package/src/cli.js +350 -1
- package/src/commands/apps.js +128 -0
- package/src/commands/auth.js +13 -4
- package/src/commands/config.js +282 -0
- package/src/commands/cost.js +593 -0
- package/src/commands/db.js +775 -0
- package/src/commands/deploy.js +264 -0
- package/src/commands/doctor.js +69 -13
- package/src/commands/domains.js +223 -0
- package/src/commands/logs.js +111 -0
- package/src/commands/open.js +42 -0
- package/src/commands/ps.js +121 -0
- package/src/commands/scale.js +132 -0
- package/src/commands/service.js +227 -0
- package/src/lib/clouds/aws.js +309 -35
- package/src/lib/clouds/cf.js +401 -2
- package/src/lib/clouds/gcp.js +255 -4
- package/src/lib/clouds/neon.js +147 -0
- package/src/lib/clouds/slicervm.js +139 -0
- package/src/lib/config.js +200 -2
- package/src/lib/docker.js +34 -0
- package/src/lib/link.js +31 -5
- package/src/lib/providers/aws/app.js +481 -0
- package/src/lib/providers/aws/db.js +504 -0
- package/src/lib/providers/aws/dns.js +232 -0
- package/src/lib/providers/aws/registry.js +59 -0
- package/src/lib/providers/cf/app.js +596 -0
- package/src/lib/providers/cf/bundle.js +70 -0
- package/src/lib/providers/cf/db.js +181 -0
- package/src/lib/providers/cf/dns.js +148 -0
- package/src/lib/providers/cf/registry.js +17 -0
- package/src/lib/providers/gcp/app.js +429 -0
- package/src/lib/providers/gcp/db.js +372 -0
- package/src/lib/providers/gcp/dns.js +166 -0
- package/src/lib/providers/gcp/registry.js +30 -0
- package/src/lib/providers/neon/db.js +306 -0
- package/src/lib/providers/resolve.js +79 -0
- package/src/lib/providers/slicervm/app.js +396 -0
- package/src/lib/providers/slicervm/db.js +33 -0
- package/src/lib/providers/slicervm/dns.js +58 -0
- package/src/lib/providers/slicervm/registry.js +7 -0
- package/worker-template/package.json +10 -0
- package/worker-template/src/index.js +260 -0
|
@@ -0,0 +1,775 @@
|
|
|
1
|
+
import { phase, status, success, fatal, hint, fmt, table } from "../lib/output.js";
|
|
2
|
+
import { resolveAppName, readLink, linkApp } from "../lib/link.js";
|
|
3
|
+
import { resolveCloudId, getCloudCfg, getProvider } from "../lib/providers/resolve.js";
|
|
4
|
+
import {
|
|
5
|
+
getDatabaseConfig, saveDatabaseConfig, removeDatabaseConfig, listDatabases,
|
|
6
|
+
tryGetServiceConfig, normalizeServiceConfig, CLOUD_IDS,
|
|
7
|
+
} from "../lib/config.js";
|
|
8
|
+
import { createInterface } from "readline";
|
|
9
|
+
import { readFileSync, writeFileSync } from "fs";
|
|
10
|
+
|
|
11
|
+
// --- Helpers ---
|
|
12
|
+
|
|
13
|
+
function resolveDatabase(name) {
|
|
14
|
+
if (!name) {
|
|
15
|
+
var linked = readLink();
|
|
16
|
+
name = linked?.db;
|
|
17
|
+
}
|
|
18
|
+
if (!name) fatal("No database specified.");
|
|
19
|
+
var entry = getDatabaseConfig(name);
|
|
20
|
+
if (!entry) fatal(`Database '${name}' not found. Run ${fmt.cmd("relight db list")} to see databases.`);
|
|
21
|
+
return { name, entry };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function loadProvider(entry) {
|
|
25
|
+
var providerId = entry.provider;
|
|
26
|
+
|
|
27
|
+
// Check if it's a service
|
|
28
|
+
var service = tryGetServiceConfig(providerId);
|
|
29
|
+
if (service && service.layer === "db") {
|
|
30
|
+
var provider = await import(`../lib/providers/${service.type}/db.js`);
|
|
31
|
+
var cfg = { ...normalizeServiceConfig(service), serviceName: providerId };
|
|
32
|
+
return { provider, cfg };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// It's a cloud
|
|
36
|
+
var provider = await getProvider(providerId, "db");
|
|
37
|
+
var cfg = getCloudCfg(providerId);
|
|
38
|
+
return { provider, cfg };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function resolveProvider(options) {
|
|
42
|
+
var provider = options.provider;
|
|
43
|
+
if (!provider) {
|
|
44
|
+
var linked = readLink();
|
|
45
|
+
// Try to infer from linked cloud
|
|
46
|
+
if (linked?.cloud) provider = linked.cloud;
|
|
47
|
+
}
|
|
48
|
+
if (!provider) {
|
|
49
|
+
fatal(
|
|
50
|
+
"No provider specified.",
|
|
51
|
+
`Use ${fmt.cmd("--provider <cf|gcp|aws|service-name>")} to specify the database provider.`
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
return provider;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// --- Commands ---
|
|
58
|
+
|
|
59
|
+
export async function dbCreate(name, options) {
|
|
60
|
+
if (!name) fatal("Database name is required.", `Usage: relight db create <name> --provider <provider>`);
|
|
61
|
+
|
|
62
|
+
// Check if already exists
|
|
63
|
+
if (getDatabaseConfig(name)) {
|
|
64
|
+
fatal(`Database '${name}' already exists.`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
var providerId = resolveProvider(options);
|
|
68
|
+
|
|
69
|
+
// Determine if this is a service or cloud
|
|
70
|
+
var service = tryGetServiceConfig(providerId);
|
|
71
|
+
var isService = service && service.layer === "db";
|
|
72
|
+
var isPostgres;
|
|
73
|
+
var provider;
|
|
74
|
+
var cfg;
|
|
75
|
+
|
|
76
|
+
if (isService) {
|
|
77
|
+
provider = await import(`../lib/providers/${service.type}/db.js`);
|
|
78
|
+
cfg = { ...normalizeServiceConfig(service), serviceName: providerId };
|
|
79
|
+
isPostgres = true;
|
|
80
|
+
} else {
|
|
81
|
+
if (!CLOUD_IDS.includes(providerId)) {
|
|
82
|
+
fatal(
|
|
83
|
+
`Unknown provider: ${providerId}`,
|
|
84
|
+
`Supported: ${CLOUD_IDS.join(", ")} or a registered db service name.`
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
provider = await getProvider(providerId, "db");
|
|
88
|
+
cfg = getCloudCfg(providerId);
|
|
89
|
+
isPostgres = providerId !== "cf";
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
phase("Creating database");
|
|
93
|
+
if (options.jurisdiction) status(`${name} (jurisdiction: ${options.jurisdiction})...`);
|
|
94
|
+
else if (options.location) status(`${name} (location: ${options.location})...`);
|
|
95
|
+
else status(`${name}...`);
|
|
96
|
+
|
|
97
|
+
var result;
|
|
98
|
+
try {
|
|
99
|
+
result = await provider.createDatabase(cfg, name, {
|
|
100
|
+
location: options.location,
|
|
101
|
+
jurisdiction: options.jurisdiction,
|
|
102
|
+
});
|
|
103
|
+
} catch (e) {
|
|
104
|
+
fatal(e.message);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Save to database registry
|
|
108
|
+
saveDatabaseConfig(name, {
|
|
109
|
+
provider: providerId,
|
|
110
|
+
dbId: result.dbId,
|
|
111
|
+
dbName: result.dbName,
|
|
112
|
+
dbUser: result.dbUser || null,
|
|
113
|
+
dbToken: result.dbToken,
|
|
114
|
+
connectionUrl: result.connectionUrl,
|
|
115
|
+
isPostgres,
|
|
116
|
+
apps: [],
|
|
117
|
+
createdAt: new Date().toISOString(),
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
if (options.json) {
|
|
121
|
+
console.log(JSON.stringify({
|
|
122
|
+
name,
|
|
123
|
+
provider: providerId,
|
|
124
|
+
dbId: result.dbId,
|
|
125
|
+
dbName: result.dbName,
|
|
126
|
+
dbToken: result.dbToken,
|
|
127
|
+
connectionUrl: result.connectionUrl,
|
|
128
|
+
}, null, 2));
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
success(`Database ${fmt.app(name)} created!`);
|
|
133
|
+
console.log(` ${fmt.bold("Provider:")} ${providerId}`);
|
|
134
|
+
console.log(` ${fmt.bold("DB ID:")} ${result.dbId}`);
|
|
135
|
+
console.log(` ${fmt.bold("DB Name:")} ${result.dbName}`);
|
|
136
|
+
if (result.connectionUrl) {
|
|
137
|
+
console.log(` ${fmt.bold("DB URL:")} ${fmt.url(result.connectionUrl)}`);
|
|
138
|
+
}
|
|
139
|
+
console.log(` ${fmt.bold("Token:")} ${result.dbToken}`);
|
|
140
|
+
hint("Next", `relight db attach ${name} <app>`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export async function dbDestroy(name, options) {
|
|
144
|
+
var resolved = resolveDatabase(name);
|
|
145
|
+
name = resolved.name;
|
|
146
|
+
var entry = resolved.entry;
|
|
147
|
+
|
|
148
|
+
if (options.confirm !== name) {
|
|
149
|
+
if (process.stdin.isTTY) {
|
|
150
|
+
var rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
151
|
+
var answer = await new Promise((resolve) =>
|
|
152
|
+
rl.question(`Type "${name}" to confirm database destruction: `, resolve)
|
|
153
|
+
);
|
|
154
|
+
rl.close();
|
|
155
|
+
if (answer.trim() !== name) {
|
|
156
|
+
fatal("Confirmation did not match. Aborting.");
|
|
157
|
+
}
|
|
158
|
+
} else {
|
|
159
|
+
fatal(
|
|
160
|
+
`Destroying database requires confirmation.`,
|
|
161
|
+
`Run: relight db destroy ${name} --confirm ${name}`
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Auto-detach from all attached apps
|
|
167
|
+
if (entry.apps && entry.apps.length > 0) {
|
|
168
|
+
for (var appName of entry.apps) {
|
|
169
|
+
process.stderr.write(` Detaching from ${fmt.app(appName)}...\n`);
|
|
170
|
+
try {
|
|
171
|
+
await detachFromApp(entry, appName);
|
|
172
|
+
} catch (e) {
|
|
173
|
+
process.stderr.write(` ${fmt.dim(`Warning: could not detach from ${appName}: ${e.message}`)}\n`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
phase("Destroying database");
|
|
179
|
+
|
|
180
|
+
var { provider, cfg } = await loadProvider(entry);
|
|
181
|
+
try {
|
|
182
|
+
await provider.destroyDatabase(cfg, name, { dbId: entry.dbId });
|
|
183
|
+
} catch (e) {
|
|
184
|
+
fatal(e.message);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
removeDatabaseConfig(name);
|
|
188
|
+
success(`Database ${fmt.app(name)} destroyed.`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export async function dbList(options) {
|
|
192
|
+
var databases = listDatabases();
|
|
193
|
+
|
|
194
|
+
if (options.json) {
|
|
195
|
+
console.log(JSON.stringify(databases, null, 2));
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (databases.length === 0) {
|
|
200
|
+
console.log(fmt.dim("\n No databases. Create one with: relight db create <name> --provider <provider>\n"));
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
var cols = ["NAME", "PROVIDER", "DB NAME", "APPS", "CREATED"];
|
|
205
|
+
var rows = databases.map((db) => [
|
|
206
|
+
db.name,
|
|
207
|
+
db.provider,
|
|
208
|
+
db.dbName || "-",
|
|
209
|
+
(db.apps || []).join(", ") || "-",
|
|
210
|
+
db.createdAt ? db.createdAt.split("T")[0] : "-",
|
|
211
|
+
]);
|
|
212
|
+
|
|
213
|
+
console.log(table(cols, rows));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export async function dbAttach(name, appName, options) {
|
|
217
|
+
var resolved = resolveDatabase(name);
|
|
218
|
+
name = resolved.name;
|
|
219
|
+
var entry = resolved.entry;
|
|
220
|
+
|
|
221
|
+
appName = resolveAppName(appName);
|
|
222
|
+
|
|
223
|
+
// Check not already attached
|
|
224
|
+
if (entry.apps && entry.apps.includes(appName)) {
|
|
225
|
+
fatal(`Database '${name}' is already attached to '${appName}'.`);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Resolve app's cloud/compute
|
|
229
|
+
var appCloud = resolveCloudId(options.cloud);
|
|
230
|
+
var appCfg = getCloudCfg(appCloud);
|
|
231
|
+
var appProvider = await getProvider(appCloud, "app");
|
|
232
|
+
|
|
233
|
+
// Check if compute service
|
|
234
|
+
if (options.compute) {
|
|
235
|
+
var computeService = tryGetServiceConfig(options.compute);
|
|
236
|
+
if (computeService) {
|
|
237
|
+
appProvider = await import(`../lib/providers/${computeService.type}/app.js`);
|
|
238
|
+
appCfg = normalizeServiceConfig(computeService);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
phase("Attaching database");
|
|
243
|
+
status(`${name} -> ${appName}...`);
|
|
244
|
+
|
|
245
|
+
var appConfig = await appProvider.getAppConfig(appCfg, appName);
|
|
246
|
+
if (!appConfig) {
|
|
247
|
+
fatal(`App ${appName} not found.`);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (!appConfig.envKeys) appConfig.envKeys = [];
|
|
251
|
+
if (!appConfig.secretKeys) appConfig.secretKeys = [];
|
|
252
|
+
if (!appConfig.env) appConfig.env = {};
|
|
253
|
+
|
|
254
|
+
// Inject env vars
|
|
255
|
+
if (entry.isPostgres) {
|
|
256
|
+
if (entry.connectionUrl) {
|
|
257
|
+
appConfig.env["DATABASE_URL"] = entry.connectionUrl;
|
|
258
|
+
if (!appConfig.envKeys.includes("DATABASE_URL")) appConfig.envKeys.push("DATABASE_URL");
|
|
259
|
+
}
|
|
260
|
+
} else {
|
|
261
|
+
// CF D1
|
|
262
|
+
appConfig.dbId = entry.dbId;
|
|
263
|
+
appConfig.dbName = entry.dbName;
|
|
264
|
+
if (entry.connectionUrl) {
|
|
265
|
+
appConfig.env["DB_URL"] = entry.connectionUrl;
|
|
266
|
+
if (!appConfig.envKeys.includes("DB_URL")) appConfig.envKeys.push("DB_URL");
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
appConfig.env["DB_TOKEN"] = "[hidden]";
|
|
271
|
+
appConfig.secretKeys = appConfig.secretKeys.filter((k) => k !== "DB_TOKEN");
|
|
272
|
+
appConfig.secretKeys.push("DB_TOKEN");
|
|
273
|
+
appConfig.envKeys = appConfig.envKeys.filter((k) => k !== "DB_TOKEN");
|
|
274
|
+
|
|
275
|
+
if (entry.dbUser) appConfig.dbUser = entry.dbUser;
|
|
276
|
+
|
|
277
|
+
await appProvider.pushAppConfig(appCfg, appName, appConfig, {
|
|
278
|
+
newSecrets: { DB_TOKEN: entry.dbToken },
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// Update registry: add app to entry.apps
|
|
282
|
+
if (!entry.apps) entry.apps = [];
|
|
283
|
+
entry.apps.push(appName);
|
|
284
|
+
saveDatabaseConfig(name, entry);
|
|
285
|
+
|
|
286
|
+
// Update .relight.yaml: set db to database name
|
|
287
|
+
var linked = readLink();
|
|
288
|
+
if (linked && linked.app === appName) {
|
|
289
|
+
linkApp(linked.app, linked.cloud, linked.dns, name, linked.compute);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
success(`Database ${fmt.app(name)} attached to ${fmt.app(appName)}.`);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Helper to detach a database from an app (used by dbDetach and dbDestroy)
|
|
296
|
+
async function detachFromApp(entry, appName, options = {}) {
|
|
297
|
+
var appCloud = options.cloud ? resolveCloudId(options.cloud) : null;
|
|
298
|
+
if (!appCloud) {
|
|
299
|
+
var linked = readLink();
|
|
300
|
+
appCloud = linked?.cloud;
|
|
301
|
+
}
|
|
302
|
+
if (!appCloud) {
|
|
303
|
+
// Try to infer from entry.provider if it's a cloud
|
|
304
|
+
if (CLOUD_IDS.includes(entry.provider)) {
|
|
305
|
+
appCloud = entry.provider;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
if (!appCloud) {
|
|
309
|
+
throw new Error("Cannot determine app cloud. Use --cloud to specify.");
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
var appCfg = getCloudCfg(appCloud);
|
|
313
|
+
var appProvider = await getProvider(appCloud, "app");
|
|
314
|
+
|
|
315
|
+
if (options.compute) {
|
|
316
|
+
var computeService = tryGetServiceConfig(options.compute);
|
|
317
|
+
if (computeService) {
|
|
318
|
+
appProvider = await import(`../lib/providers/${computeService.type}/app.js`);
|
|
319
|
+
appCfg = normalizeServiceConfig(computeService);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
var appConfig = await appProvider.getAppConfig(appCfg, appName);
|
|
324
|
+
if (!appConfig) return;
|
|
325
|
+
|
|
326
|
+
// Remove DB env vars
|
|
327
|
+
delete appConfig.dbId;
|
|
328
|
+
delete appConfig.dbName;
|
|
329
|
+
delete appConfig.dbUser;
|
|
330
|
+
|
|
331
|
+
if (appConfig.env) {
|
|
332
|
+
delete appConfig.env["DB_URL"];
|
|
333
|
+
delete appConfig.env["DB_TOKEN"];
|
|
334
|
+
delete appConfig.env["DATABASE_URL"];
|
|
335
|
+
}
|
|
336
|
+
if (appConfig.envKeys) {
|
|
337
|
+
appConfig.envKeys = appConfig.envKeys.filter((k) => k !== "DB_URL" && k !== "DATABASE_URL");
|
|
338
|
+
}
|
|
339
|
+
if (appConfig.secretKeys) {
|
|
340
|
+
appConfig.secretKeys = appConfig.secretKeys.filter((k) => k !== "DB_TOKEN");
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
await appProvider.pushAppConfig(appCfg, appName, appConfig);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
export async function dbDetach(appName, options) {
|
|
347
|
+
appName = resolveAppName(appName);
|
|
348
|
+
|
|
349
|
+
// Find which database is attached to this app
|
|
350
|
+
var databases = listDatabases();
|
|
351
|
+
var attached = null;
|
|
352
|
+
var attachedName = null;
|
|
353
|
+
|
|
354
|
+
// Check .relight.yaml first
|
|
355
|
+
var linked = readLink();
|
|
356
|
+
if (linked?.db) {
|
|
357
|
+
var entry = getDatabaseConfig(linked.db);
|
|
358
|
+
if (entry && entry.apps && entry.apps.includes(appName)) {
|
|
359
|
+
attached = entry;
|
|
360
|
+
attachedName = linked.db;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Search registry
|
|
365
|
+
if (!attached) {
|
|
366
|
+
for (var db of databases) {
|
|
367
|
+
if (db.apps && db.apps.includes(appName)) {
|
|
368
|
+
attached = db;
|
|
369
|
+
attachedName = db.name;
|
|
370
|
+
break;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (!attached) {
|
|
376
|
+
fatal(`No database found attached to '${appName}'.`);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
phase("Detaching database");
|
|
380
|
+
status(`${attachedName} from ${appName}...`);
|
|
381
|
+
|
|
382
|
+
try {
|
|
383
|
+
await detachFromApp(attached, appName, options);
|
|
384
|
+
} catch (e) {
|
|
385
|
+
fatal(e.message);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Update registry: remove app from entry.apps
|
|
389
|
+
attached.apps = (attached.apps || []).filter((a) => a !== appName);
|
|
390
|
+
// Remove extra fields added by listDatabases() (like 'name')
|
|
391
|
+
var cleanEntry = getDatabaseConfig(attachedName);
|
|
392
|
+
cleanEntry.apps = attached.apps;
|
|
393
|
+
saveDatabaseConfig(attachedName, cleanEntry);
|
|
394
|
+
|
|
395
|
+
success(`Database ${fmt.app(attachedName)} detached from ${fmt.app(appName)}.`);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
export async function dbInfo(name, options) {
|
|
399
|
+
var resolved = resolveDatabase(name);
|
|
400
|
+
name = resolved.name;
|
|
401
|
+
var entry = resolved.entry;
|
|
402
|
+
|
|
403
|
+
var { provider, cfg } = await loadProvider(entry);
|
|
404
|
+
|
|
405
|
+
var info;
|
|
406
|
+
try {
|
|
407
|
+
info = await provider.getDatabaseInfo(cfg, name, {
|
|
408
|
+
dbId: entry.dbId,
|
|
409
|
+
connectionUrl: entry.connectionUrl,
|
|
410
|
+
});
|
|
411
|
+
} catch (e) {
|
|
412
|
+
fatal(e.message);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (options.json) {
|
|
416
|
+
console.log(JSON.stringify({
|
|
417
|
+
name,
|
|
418
|
+
provider: entry.provider,
|
|
419
|
+
dbId: info.dbId,
|
|
420
|
+
dbName: info.dbName,
|
|
421
|
+
connectionUrl: info.connectionUrl,
|
|
422
|
+
size: info.size,
|
|
423
|
+
numTables: info.numTables,
|
|
424
|
+
apps: entry.apps || [],
|
|
425
|
+
createdAt: info.createdAt || entry.createdAt,
|
|
426
|
+
}, null, 2));
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
console.log("");
|
|
431
|
+
console.log(`${fmt.bold("Database:")} ${fmt.app(name)}`);
|
|
432
|
+
console.log(`${fmt.bold("Provider:")} ${entry.provider}`);
|
|
433
|
+
console.log(`${fmt.bold("DB ID:")} ${info.dbId}`);
|
|
434
|
+
console.log(`${fmt.bold("DB Name:")} ${info.dbName}`);
|
|
435
|
+
if (info.size != null) {
|
|
436
|
+
var sizeKb = (info.size / 1024).toFixed(1);
|
|
437
|
+
console.log(`${fmt.bold("Size:")} ${sizeKb} KB`);
|
|
438
|
+
}
|
|
439
|
+
if (info.numTables != null) {
|
|
440
|
+
console.log(`${fmt.bold("Tables:")} ${info.numTables}`);
|
|
441
|
+
}
|
|
442
|
+
if (info.connectionUrl) {
|
|
443
|
+
console.log(`${fmt.bold("DB URL:")} ${fmt.url(info.connectionUrl)}`);
|
|
444
|
+
}
|
|
445
|
+
console.log(`${fmt.bold("Token:")} ${fmt.dim("[hidden]")}`);
|
|
446
|
+
if (entry.apps && entry.apps.length > 0) {
|
|
447
|
+
console.log(`${fmt.bold("Apps:")} ${entry.apps.join(", ")}`);
|
|
448
|
+
}
|
|
449
|
+
if (info.createdAt || entry.createdAt) {
|
|
450
|
+
console.log(`${fmt.bold("Created:")} ${info.createdAt || entry.createdAt}`);
|
|
451
|
+
}
|
|
452
|
+
console.log("");
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
export async function dbShell(name, options) {
|
|
456
|
+
var resolved = resolveDatabase(name);
|
|
457
|
+
name = resolved.name;
|
|
458
|
+
var entry = resolved.entry;
|
|
459
|
+
|
|
460
|
+
var { provider, cfg } = await loadProvider(entry);
|
|
461
|
+
|
|
462
|
+
// Verify database exists
|
|
463
|
+
try {
|
|
464
|
+
await provider.getDatabaseInfo(cfg, name, {
|
|
465
|
+
dbId: entry.dbId,
|
|
466
|
+
connectionUrl: entry.connectionUrl,
|
|
467
|
+
});
|
|
468
|
+
} catch (e) {
|
|
469
|
+
fatal(e.message);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
var isPostgres = entry.isPostgres;
|
|
473
|
+
|
|
474
|
+
var rl = createInterface({
|
|
475
|
+
input: process.stdin,
|
|
476
|
+
output: process.stderr,
|
|
477
|
+
prompt: "sql> ",
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
process.stderr.write(`Connected to ${fmt.app(name)}. Type .exit to quit.\n\n`);
|
|
481
|
+
rl.prompt();
|
|
482
|
+
|
|
483
|
+
rl.on("line", async (line) => {
|
|
484
|
+
line = line.trim();
|
|
485
|
+
if (!line) {
|
|
486
|
+
rl.prompt();
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
if (line === ".exit" || line === ".quit") {
|
|
491
|
+
rl.close();
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
try {
|
|
496
|
+
var sql;
|
|
497
|
+
if (line === ".tables") {
|
|
498
|
+
if (isPostgres) {
|
|
499
|
+
sql = "SELECT tablename AS name FROM pg_tables WHERE schemaname = 'public' ORDER BY tablename";
|
|
500
|
+
} else {
|
|
501
|
+
sql = "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name NOT LIKE '_cf_%' ORDER BY name";
|
|
502
|
+
}
|
|
503
|
+
} else if (line.startsWith(".schema")) {
|
|
504
|
+
var tableName = line.split(/\s+/)[1];
|
|
505
|
+
if (!tableName) {
|
|
506
|
+
process.stderr.write("Usage: .schema <table>\n");
|
|
507
|
+
rl.prompt();
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
if (isPostgres) {
|
|
511
|
+
sql = `SELECT column_name, data_type, is_nullable FROM information_schema.columns WHERE table_name = '${tableName}' AND table_schema = 'public' ORDER BY ordinal_position`;
|
|
512
|
+
} else {
|
|
513
|
+
sql = `SELECT sql FROM sqlite_master WHERE name='${tableName}'`;
|
|
514
|
+
}
|
|
515
|
+
} else {
|
|
516
|
+
sql = line;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
var results = await provider.queryDatabase(cfg, name, sql, undefined, {
|
|
520
|
+
dbId: entry.dbId,
|
|
521
|
+
connectionUrl: entry.connectionUrl,
|
|
522
|
+
});
|
|
523
|
+
var result = Array.isArray(results) ? results[0] : results;
|
|
524
|
+
|
|
525
|
+
if (result && result.results && result.results.length > 0) {
|
|
526
|
+
var cols = Object.keys(result.results[0]);
|
|
527
|
+
var rows = result.results.map((r) => cols.map((c) => String(r[c] ?? "")));
|
|
528
|
+
console.log(table(cols, rows));
|
|
529
|
+
} else if (result && result.meta) {
|
|
530
|
+
process.stderr.write(
|
|
531
|
+
fmt.dim(`OK. ${result.meta.changes || 0} changes, ${result.meta.rows_read || 0} rows read.\n`)
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
} catch (e) {
|
|
535
|
+
process.stderr.write(`${fmt.dim("Error:")} ${e.message}\n`);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
rl.prompt();
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
rl.on("close", () => {
|
|
542
|
+
process.stderr.write("\n");
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
await new Promise((resolve) => rl.on("close", resolve));
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
export async function dbQuery(args, options) {
|
|
549
|
+
var name;
|
|
550
|
+
var sql;
|
|
551
|
+
var joined = args.join(" ");
|
|
552
|
+
|
|
553
|
+
var sqlKeywords = /^(SELECT|INSERT|UPDATE|DELETE|CREATE|DROP|ALTER|PRAGMA|WITH|EXPLAIN|BEGIN|COMMIT|ROLLBACK|REPLACE|VACUUM|REINDEX|ATTACH|DETACH)\b/i;
|
|
554
|
+
if (args.length >= 2 && !args[0].includes(" ") && !sqlKeywords.test(args[0])) {
|
|
555
|
+
name = args[0];
|
|
556
|
+
sql = args.slice(1).join(" ");
|
|
557
|
+
} else {
|
|
558
|
+
sql = joined;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
var resolved = resolveDatabase(name);
|
|
562
|
+
name = resolved.name;
|
|
563
|
+
var entry = resolved.entry;
|
|
564
|
+
|
|
565
|
+
var { provider, cfg } = await loadProvider(entry);
|
|
566
|
+
|
|
567
|
+
var results;
|
|
568
|
+
try {
|
|
569
|
+
results = await provider.queryDatabase(cfg, name, sql, undefined, {
|
|
570
|
+
dbId: entry.dbId,
|
|
571
|
+
connectionUrl: entry.connectionUrl,
|
|
572
|
+
});
|
|
573
|
+
} catch (e) {
|
|
574
|
+
fatal(e.message);
|
|
575
|
+
}
|
|
576
|
+
var result = Array.isArray(results) ? results[0] : results;
|
|
577
|
+
|
|
578
|
+
if (options.json) {
|
|
579
|
+
console.log(JSON.stringify(result, null, 2));
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
if (result && result.results && result.results.length > 0) {
|
|
584
|
+
var cols = Object.keys(result.results[0]);
|
|
585
|
+
var rows = result.results.map((r) => cols.map((c) => String(r[c] ?? "")));
|
|
586
|
+
console.log(table(cols, rows));
|
|
587
|
+
} else if (result && result.meta) {
|
|
588
|
+
process.stderr.write(
|
|
589
|
+
fmt.dim(`OK. ${result.meta.changes || 0} changes, ${result.meta.rows_read || 0} rows read.\n`)
|
|
590
|
+
);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
export async function dbImport(args, options) {
|
|
595
|
+
var name;
|
|
596
|
+
var filepath;
|
|
597
|
+
if (args.length >= 2) {
|
|
598
|
+
name = args[0];
|
|
599
|
+
filepath = args[1];
|
|
600
|
+
} else if (args.length === 1) {
|
|
601
|
+
filepath = args[0];
|
|
602
|
+
} else {
|
|
603
|
+
fatal("Usage: relight db import <name> <path>");
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
var resolved = resolveDatabase(name);
|
|
607
|
+
name = resolved.name;
|
|
608
|
+
var entry = resolved.entry;
|
|
609
|
+
|
|
610
|
+
var { provider, cfg } = await loadProvider(entry);
|
|
611
|
+
|
|
612
|
+
var sqlContent;
|
|
613
|
+
try {
|
|
614
|
+
sqlContent = readFileSync(filepath, "utf-8");
|
|
615
|
+
} catch (e) {
|
|
616
|
+
fatal(`Could not read file: ${filepath}`, e.message);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
phase("Importing SQL");
|
|
620
|
+
status(`File: ${filepath} (${(sqlContent.length / 1024).toFixed(1)} KB)`);
|
|
621
|
+
|
|
622
|
+
try {
|
|
623
|
+
await provider.importDatabase(cfg, name, sqlContent, {
|
|
624
|
+
dbId: entry.dbId,
|
|
625
|
+
connectionUrl: entry.connectionUrl,
|
|
626
|
+
});
|
|
627
|
+
} catch (e) {
|
|
628
|
+
fatal(e.message);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
success(`Imported ${filepath} into ${fmt.app(name)}`);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
export async function dbExport(name, options) {
|
|
635
|
+
var resolved = resolveDatabase(name);
|
|
636
|
+
name = resolved.name;
|
|
637
|
+
var entry = resolved.entry;
|
|
638
|
+
|
|
639
|
+
var { provider, cfg } = await loadProvider(entry);
|
|
640
|
+
|
|
641
|
+
phase("Exporting database");
|
|
642
|
+
status("Initiating export...");
|
|
643
|
+
|
|
644
|
+
var dump;
|
|
645
|
+
try {
|
|
646
|
+
dump = await provider.exportDatabase(cfg, name, {
|
|
647
|
+
dbId: entry.dbId,
|
|
648
|
+
connectionUrl: entry.connectionUrl,
|
|
649
|
+
});
|
|
650
|
+
} catch (e) {
|
|
651
|
+
fatal(e.message);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
if (options.output) {
|
|
655
|
+
writeFileSync(options.output, dump);
|
|
656
|
+
success(`Exported to ${options.output}`);
|
|
657
|
+
} else {
|
|
658
|
+
process.stdout.write(dump);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
export async function dbToken(name, options) {
|
|
663
|
+
var resolved = resolveDatabase(name);
|
|
664
|
+
name = resolved.name;
|
|
665
|
+
var entry = resolved.entry;
|
|
666
|
+
|
|
667
|
+
if (options.rotate) {
|
|
668
|
+
var { provider, cfg } = await loadProvider(entry);
|
|
669
|
+
|
|
670
|
+
var result;
|
|
671
|
+
try {
|
|
672
|
+
result = await provider.rotateToken(cfg, name, { dbId: entry.dbId });
|
|
673
|
+
} catch (e) {
|
|
674
|
+
fatal(e.message);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// Update registry with new token and connection URL
|
|
678
|
+
entry.dbToken = result.dbToken;
|
|
679
|
+
if (result.connectionUrl) entry.connectionUrl = result.connectionUrl;
|
|
680
|
+
saveDatabaseConfig(name, entry);
|
|
681
|
+
|
|
682
|
+
// Update all attached apps
|
|
683
|
+
if (entry.apps && entry.apps.length > 0) {
|
|
684
|
+
for (var appName of entry.apps) {
|
|
685
|
+
status(`Updating ${appName}...`);
|
|
686
|
+
try {
|
|
687
|
+
// Re-attach to update the token in the app
|
|
688
|
+
var appCloud = resolveCloudId(null);
|
|
689
|
+
var appCfg = getCloudCfg(appCloud);
|
|
690
|
+
var appProvider = await getProvider(appCloud, "app");
|
|
691
|
+
var appConfig = await appProvider.getAppConfig(appCfg, appName);
|
|
692
|
+
|
|
693
|
+
if (appConfig) {
|
|
694
|
+
if (!appConfig.envKeys) appConfig.envKeys = [];
|
|
695
|
+
if (!appConfig.secretKeys) appConfig.secretKeys = [];
|
|
696
|
+
if (!appConfig.env) appConfig.env = {};
|
|
697
|
+
|
|
698
|
+
appConfig.env["DB_TOKEN"] = "[hidden]";
|
|
699
|
+
if (!appConfig.secretKeys.includes("DB_TOKEN")) appConfig.secretKeys.push("DB_TOKEN");
|
|
700
|
+
appConfig.envKeys = appConfig.envKeys.filter((k) => k !== "DB_TOKEN");
|
|
701
|
+
|
|
702
|
+
if (result.connectionUrl) {
|
|
703
|
+
var urlKey = entry.isPostgres ? "DATABASE_URL" : "DB_URL";
|
|
704
|
+
appConfig.env[urlKey] = result.connectionUrl;
|
|
705
|
+
if (!appConfig.envKeys.includes(urlKey)) appConfig.envKeys.push(urlKey);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
await appProvider.pushAppConfig(appCfg, appName, appConfig, {
|
|
709
|
+
newSecrets: { DB_TOKEN: result.dbToken },
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
} catch (e) {
|
|
713
|
+
process.stderr.write(` ${fmt.dim(`Warning: could not update ${appName}: ${e.message}`)}\n`);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
success("Token rotated.");
|
|
719
|
+
console.log(`${fmt.bold("Token:")} ${result.dbToken}`);
|
|
720
|
+
if (result.connectionUrl) {
|
|
721
|
+
console.log(`${fmt.bold("DB URL:")} ${fmt.url(result.connectionUrl)}`);
|
|
722
|
+
}
|
|
723
|
+
} else {
|
|
724
|
+
console.log(`${fmt.bold("Token:")} ${fmt.dim("[hidden] - use --rotate to generate a new token")}`);
|
|
725
|
+
if (entry.connectionUrl) {
|
|
726
|
+
console.log(`${fmt.bold("DB URL:")} ${fmt.url(entry.connectionUrl)}`);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
export async function dbReset(name, options) {
|
|
732
|
+
var resolved = resolveDatabase(name);
|
|
733
|
+
name = resolved.name;
|
|
734
|
+
var entry = resolved.entry;
|
|
735
|
+
|
|
736
|
+
if (options.confirm !== name) {
|
|
737
|
+
if (process.stdin.isTTY) {
|
|
738
|
+
var rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
739
|
+
var answer = await new Promise((resolve) =>
|
|
740
|
+
rl.question(`Type "${name}" to confirm database reset: `, resolve)
|
|
741
|
+
);
|
|
742
|
+
rl.close();
|
|
743
|
+
if (answer.trim() !== name) {
|
|
744
|
+
fatal("Confirmation did not match. Aborting.");
|
|
745
|
+
}
|
|
746
|
+
} else {
|
|
747
|
+
fatal(
|
|
748
|
+
`Resetting database requires confirmation.`,
|
|
749
|
+
`Run: relight db reset ${name} --confirm ${name}`
|
|
750
|
+
);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
var { provider, cfg } = await loadProvider(entry);
|
|
755
|
+
|
|
756
|
+
phase("Resetting database");
|
|
757
|
+
status("Listing tables...");
|
|
758
|
+
|
|
759
|
+
var tables;
|
|
760
|
+
try {
|
|
761
|
+
tables = await provider.resetDatabase(cfg, name, {
|
|
762
|
+
dbId: entry.dbId,
|
|
763
|
+
connectionUrl: entry.connectionUrl,
|
|
764
|
+
});
|
|
765
|
+
} catch (e) {
|
|
766
|
+
fatal(e.message);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
if (tables.length === 0) {
|
|
770
|
+
process.stderr.write("No user tables found.\n");
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
success(`Dropped ${tables.length} table${tables.length === 1 ? "" : "s"}.`);
|
|
775
|
+
}
|