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,306 @@
|
|
|
1
|
+
import {
|
|
2
|
+
listProjects,
|
|
3
|
+
createProject,
|
|
4
|
+
deleteProject,
|
|
5
|
+
listBranches,
|
|
6
|
+
listDatabases,
|
|
7
|
+
createDatabase as neonCreateDb,
|
|
8
|
+
deleteDatabase as neonDeleteDb,
|
|
9
|
+
createRole,
|
|
10
|
+
deleteRole,
|
|
11
|
+
getRolePassword,
|
|
12
|
+
resetRolePassword,
|
|
13
|
+
getConnectionUri,
|
|
14
|
+
} from "../../clouds/neon.js";
|
|
15
|
+
import { getServiceMeta, setServiceMeta } from "../../config.js";
|
|
16
|
+
|
|
17
|
+
function userName(appName) {
|
|
18
|
+
return `app_${appName.replace(/-/g, "_")}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function dbName(appName) {
|
|
22
|
+
return `relight_${appName.replace(/-/g, "_")}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function connectPg(connectionUrl) {
|
|
26
|
+
var pg = await import("pg");
|
|
27
|
+
var Client = pg.default?.Client || pg.Client;
|
|
28
|
+
var client = new Client({ connectionString: connectionUrl });
|
|
29
|
+
await client.connect();
|
|
30
|
+
return client;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function getOrCreateSharedProject(cfg) {
|
|
34
|
+
var meta = getServiceMeta(cfg.serviceName, "sharedProject");
|
|
35
|
+
|
|
36
|
+
if (meta && meta.projectId) {
|
|
37
|
+
// Verify project still exists
|
|
38
|
+
try {
|
|
39
|
+
var projects = await listProjects(cfg.apiKey);
|
|
40
|
+
var found = projects.find((p) => p.id === meta.projectId);
|
|
41
|
+
if (found) return meta;
|
|
42
|
+
} catch {
|
|
43
|
+
// Fall through to create
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Find existing "relight" project or create one
|
|
48
|
+
var projects = await listProjects(cfg.apiKey);
|
|
49
|
+
var existing = projects.find((p) => p.name === "relight");
|
|
50
|
+
|
|
51
|
+
var projectId;
|
|
52
|
+
var branchId;
|
|
53
|
+
|
|
54
|
+
if (existing) {
|
|
55
|
+
projectId = existing.id;
|
|
56
|
+
var branches = await listBranches(cfg.apiKey, projectId);
|
|
57
|
+
branchId = branches.find((b) => b.primary)?.id || branches[0]?.id;
|
|
58
|
+
} else {
|
|
59
|
+
process.stderr.write(" Creating shared Neon project...\n");
|
|
60
|
+
var result = await createProject(cfg.apiKey, { name: "relight" });
|
|
61
|
+
projectId = result.project.id;
|
|
62
|
+
branchId = result.branch?.id;
|
|
63
|
+
if (!branchId) {
|
|
64
|
+
var branches = await listBranches(cfg.apiKey, projectId);
|
|
65
|
+
branchId = branches.find((b) => b.primary)?.id || branches[0]?.id;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
meta = { projectId, branchId };
|
|
70
|
+
setServiceMeta(cfg.serviceName, "sharedProject", meta);
|
|
71
|
+
return meta;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function destroySharedProjectIfEmpty(cfg) {
|
|
75
|
+
var meta = getServiceMeta(cfg.serviceName, "sharedProject");
|
|
76
|
+
if (!meta) return false;
|
|
77
|
+
|
|
78
|
+
var databases = await listDatabases(cfg.apiKey, meta.projectId, meta.branchId);
|
|
79
|
+
var relightDbs = databases.filter((d) => d.name.startsWith("relight_"));
|
|
80
|
+
if (relightDbs.length > 0) return false;
|
|
81
|
+
|
|
82
|
+
// No relight databases remain - destroy the project
|
|
83
|
+
await deleteProject(cfg.apiKey, meta.projectId);
|
|
84
|
+
setServiceMeta(cfg.serviceName, "sharedProject", undefined);
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// --- Public API ---
|
|
89
|
+
|
|
90
|
+
export async function createDatabase(cfg, appName, opts = {}) {
|
|
91
|
+
var meta = await getOrCreateSharedProject(cfg);
|
|
92
|
+
var database = dbName(appName);
|
|
93
|
+
var user = userName(appName);
|
|
94
|
+
|
|
95
|
+
// Create role (Neon auto-generates password)
|
|
96
|
+
var roleResult = await createRole(cfg.apiKey, meta.projectId, meta.branchId, user);
|
|
97
|
+
var password = roleResult.role?.password;
|
|
98
|
+
|
|
99
|
+
// Create database owned by the role
|
|
100
|
+
await neonCreateDb(cfg.apiKey, meta.projectId, meta.branchId, database, user);
|
|
101
|
+
|
|
102
|
+
// Get connection URI
|
|
103
|
+
var connectionUrl = await getConnectionUri(cfg.apiKey, meta.projectId, database, user);
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
dbId: meta.projectId,
|
|
107
|
+
dbName: database,
|
|
108
|
+
dbUser: user,
|
|
109
|
+
dbToken: password,
|
|
110
|
+
connectionUrl,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export async function destroyDatabase(cfg, appName, opts = {}) {
|
|
115
|
+
var meta = getServiceMeta(cfg.serviceName, "sharedProject");
|
|
116
|
+
if (!meta) throw new Error("No shared Neon project found.");
|
|
117
|
+
|
|
118
|
+
var database = dbName(appName);
|
|
119
|
+
var user = userName(appName);
|
|
120
|
+
|
|
121
|
+
// Delete database then role
|
|
122
|
+
try {
|
|
123
|
+
await neonDeleteDb(cfg.apiKey, meta.projectId, meta.branchId, database);
|
|
124
|
+
} catch (e) {
|
|
125
|
+
if (!e.message.includes("404")) throw e;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
await deleteRole(cfg.apiKey, meta.projectId, meta.branchId, user);
|
|
130
|
+
} catch (e) {
|
|
131
|
+
if (!e.message.includes("404")) throw e;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Check if shared project should be destroyed
|
|
135
|
+
await destroySharedProjectIfEmpty(cfg);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export async function getDatabaseInfo(cfg, appName, opts = {}) {
|
|
139
|
+
var meta = getServiceMeta(cfg.serviceName, "sharedProject");
|
|
140
|
+
if (!meta) throw new Error("No shared Neon project found.");
|
|
141
|
+
|
|
142
|
+
var database = dbName(appName);
|
|
143
|
+
var user = userName(appName);
|
|
144
|
+
|
|
145
|
+
var connectionUrl;
|
|
146
|
+
try {
|
|
147
|
+
connectionUrl = await getConnectionUri(cfg.apiKey, meta.projectId, database, user);
|
|
148
|
+
// Mask password in display URL
|
|
149
|
+
connectionUrl = connectionUrl.replace(/:([^@]+)@/, ":****@");
|
|
150
|
+
} catch {
|
|
151
|
+
connectionUrl = null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
dbId: meta.projectId,
|
|
156
|
+
dbName: database,
|
|
157
|
+
connectionUrl,
|
|
158
|
+
size: null,
|
|
159
|
+
numTables: null,
|
|
160
|
+
createdAt: null,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export async function queryDatabase(cfg, appName, sql, params, opts = {}) {
|
|
165
|
+
var meta = getServiceMeta(cfg.serviceName, "sharedProject");
|
|
166
|
+
if (!meta) throw new Error("No shared Neon project found.");
|
|
167
|
+
|
|
168
|
+
var database = dbName(appName);
|
|
169
|
+
var user = userName(appName);
|
|
170
|
+
|
|
171
|
+
var connectionUrl = await getConnectionUri(cfg.apiKey, meta.projectId, database, user);
|
|
172
|
+
var client = await connectPg(connectionUrl);
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
var result = await client.query(sql, params || []);
|
|
176
|
+
return {
|
|
177
|
+
results: result.rows,
|
|
178
|
+
meta: { changes: result.rowCount, rows_read: result.rows.length },
|
|
179
|
+
};
|
|
180
|
+
} finally {
|
|
181
|
+
await client.end();
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export async function importDatabase(cfg, appName, sqlContent, opts = {}) {
|
|
186
|
+
var meta = getServiceMeta(cfg.serviceName, "sharedProject");
|
|
187
|
+
if (!meta) throw new Error("No shared Neon project found.");
|
|
188
|
+
|
|
189
|
+
var database = dbName(appName);
|
|
190
|
+
var user = userName(appName);
|
|
191
|
+
|
|
192
|
+
var connectionUrl = await getConnectionUri(cfg.apiKey, meta.projectId, database, user);
|
|
193
|
+
var client = await connectPg(connectionUrl);
|
|
194
|
+
|
|
195
|
+
try {
|
|
196
|
+
await client.query(sqlContent);
|
|
197
|
+
} finally {
|
|
198
|
+
await client.end();
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export async function exportDatabase(cfg, appName, opts = {}) {
|
|
203
|
+
var meta = getServiceMeta(cfg.serviceName, "sharedProject");
|
|
204
|
+
if (!meta) throw new Error("No shared Neon project found.");
|
|
205
|
+
|
|
206
|
+
var database = dbName(appName);
|
|
207
|
+
var user = userName(appName);
|
|
208
|
+
|
|
209
|
+
var connectionUrl = await getConnectionUri(cfg.apiKey, meta.projectId, database, user);
|
|
210
|
+
var client = await connectPg(connectionUrl);
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
var tablesRes = await client.query(
|
|
214
|
+
"SELECT tablename FROM pg_tables WHERE schemaname = 'public' ORDER BY tablename"
|
|
215
|
+
);
|
|
216
|
+
var tables = tablesRes.rows.map((r) => r.tablename);
|
|
217
|
+
|
|
218
|
+
var dump = [];
|
|
219
|
+
dump.push("-- PostgreSQL dump generated by relight");
|
|
220
|
+
dump.push(`-- Database: ${database}`);
|
|
221
|
+
dump.push(`-- Date: ${new Date().toISOString()}`);
|
|
222
|
+
dump.push("");
|
|
223
|
+
|
|
224
|
+
for (var t of tables) {
|
|
225
|
+
var colsRes = await client.query(
|
|
226
|
+
`SELECT column_name, data_type, is_nullable, column_default
|
|
227
|
+
FROM information_schema.columns
|
|
228
|
+
WHERE table_name = $1 AND table_schema = 'public'
|
|
229
|
+
ORDER BY ordinal_position`,
|
|
230
|
+
[t]
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
var cols = colsRes.rows.map((c) => {
|
|
234
|
+
var def = ` "${c.column_name}" ${c.data_type}`;
|
|
235
|
+
if (c.column_default) def += ` DEFAULT ${c.column_default}`;
|
|
236
|
+
if (c.is_nullable === "NO") def += " NOT NULL";
|
|
237
|
+
return def;
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
dump.push(`CREATE TABLE IF NOT EXISTS "${t}" (`);
|
|
241
|
+
dump.push(cols.join(",\n"));
|
|
242
|
+
dump.push(");");
|
|
243
|
+
dump.push("");
|
|
244
|
+
|
|
245
|
+
var dataRes = await client.query(`SELECT * FROM "${t}"`);
|
|
246
|
+
for (var row of dataRes.rows) {
|
|
247
|
+
var values = Object.values(row).map((v) => {
|
|
248
|
+
if (v === null) return "NULL";
|
|
249
|
+
if (typeof v === "number") return String(v);
|
|
250
|
+
if (typeof v === "boolean") return v ? "TRUE" : "FALSE";
|
|
251
|
+
return "'" + String(v).replace(/'/g, "''") + "'";
|
|
252
|
+
});
|
|
253
|
+
var colNames = Object.keys(row).map((c) => `"${c}"`).join(", ");
|
|
254
|
+
dump.push(`INSERT INTO "${t}" (${colNames}) VALUES (${values.join(", ")});`);
|
|
255
|
+
}
|
|
256
|
+
dump.push("");
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return dump.join("\n");
|
|
260
|
+
} finally {
|
|
261
|
+
await client.end();
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export async function rotateToken(cfg, appName, opts = {}) {
|
|
266
|
+
var meta = getServiceMeta(cfg.serviceName, "sharedProject");
|
|
267
|
+
if (!meta) throw new Error("No shared Neon project found.");
|
|
268
|
+
|
|
269
|
+
var database = dbName(appName);
|
|
270
|
+
var user = userName(appName);
|
|
271
|
+
|
|
272
|
+
// Reset password via Neon API
|
|
273
|
+
var result = await resetRolePassword(cfg.apiKey, meta.projectId, meta.branchId, user);
|
|
274
|
+
var newPassword = result;
|
|
275
|
+
|
|
276
|
+
// Get updated connection URI
|
|
277
|
+
var connectionUrl = await getConnectionUri(cfg.apiKey, meta.projectId, database, user);
|
|
278
|
+
|
|
279
|
+
return { dbToken: newPassword, connectionUrl };
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export async function resetDatabase(cfg, appName, opts = {}) {
|
|
283
|
+
var meta = getServiceMeta(cfg.serviceName, "sharedProject");
|
|
284
|
+
if (!meta) throw new Error("No shared Neon project found.");
|
|
285
|
+
|
|
286
|
+
var database = dbName(appName);
|
|
287
|
+
var user = userName(appName);
|
|
288
|
+
|
|
289
|
+
var connectionUrl = await getConnectionUri(cfg.apiKey, meta.projectId, database, user);
|
|
290
|
+
var client = await connectPg(connectionUrl);
|
|
291
|
+
|
|
292
|
+
try {
|
|
293
|
+
var tablesRes = await client.query(
|
|
294
|
+
"SELECT tablename FROM pg_tables WHERE schemaname = 'public'"
|
|
295
|
+
);
|
|
296
|
+
var tables = tablesRes.rows.map((r) => r.tablename);
|
|
297
|
+
|
|
298
|
+
for (var t of tables) {
|
|
299
|
+
await client.query(`DROP TABLE IF EXISTS "${t}" CASCADE`);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return tables;
|
|
303
|
+
} finally {
|
|
304
|
+
await client.end();
|
|
305
|
+
}
|
|
306
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { resolveCloud } from "../link.js";
|
|
2
|
+
import { resolveCompute } from "../link.js";
|
|
3
|
+
import { getDefaultCloud, resolveCloudConfig, CLOUD_NAMES, tryGetServiceConfig, normalizeServiceConfig } from "../config.js";
|
|
4
|
+
import { fatal, fmt } from "../output.js";
|
|
5
|
+
|
|
6
|
+
var LAYERS = ["app", "dns", "db", "registry"];
|
|
7
|
+
|
|
8
|
+
export function getProvider(cloudId, layer) {
|
|
9
|
+
if (!LAYERS.includes(layer)) {
|
|
10
|
+
throw new Error(`Unknown provider layer: ${layer}`);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
var providers = {
|
|
14
|
+
cf: () => import(`./cf/${layer}.js`),
|
|
15
|
+
gcp: () => import(`./gcp/${layer}.js`),
|
|
16
|
+
aws: () => import(`./aws/${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>")} 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
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function resolveTarget(options) {
|
|
52
|
+
// Check if --compute points to a service
|
|
53
|
+
var computeName = options.compute || resolveCompute();
|
|
54
|
+
if (computeName) {
|
|
55
|
+
var service = tryGetServiceConfig(computeName);
|
|
56
|
+
if (service) {
|
|
57
|
+
var serviceType = service.type;
|
|
58
|
+
return {
|
|
59
|
+
kind: "service",
|
|
60
|
+
id: computeName,
|
|
61
|
+
layer: service.layer,
|
|
62
|
+
type: serviceType,
|
|
63
|
+
cfg: normalizeServiceConfig(service),
|
|
64
|
+
provider: (layer) => import(`./${serviceType}/${layer}.js`),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Fall back to cloud
|
|
70
|
+
var cloud = resolveCloudId(options.cloud);
|
|
71
|
+
var cfg = resolveCloudConfig(cloud);
|
|
72
|
+
return {
|
|
73
|
+
kind: "cloud",
|
|
74
|
+
id: cloud,
|
|
75
|
+
type: cloud,
|
|
76
|
+
cfg,
|
|
77
|
+
provider: (layer) => getProvider(cloud, layer),
|
|
78
|
+
};
|
|
79
|
+
}
|