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,128 @@
1
+ import { success, fatal, hint, fmt, table } from "../lib/output.js";
2
+ import { resolveAppName, readLink, unlinkApp } from "../lib/link.js";
3
+ import { resolveTarget } from "../lib/providers/resolve.js";
4
+ import { createInterface } from "readline";
5
+
6
+ export async function appsList(options) {
7
+ var target = await resolveTarget(options);
8
+ var cfg = target.cfg;
9
+ var appProvider = await target.provider("app");
10
+
11
+ var apps = await appProvider.listApps(cfg);
12
+
13
+ if (apps.length === 0) {
14
+ if (options.json) {
15
+ console.log("[]");
16
+ } else {
17
+ process.stderr.write("No apps deployed.\n");
18
+ hint("Next", "relight deploy");
19
+ }
20
+ return;
21
+ }
22
+
23
+ if (options.json) {
24
+ console.log(JSON.stringify(apps, null, 2));
25
+ return;
26
+ }
27
+
28
+ var rows = apps.map((a) => [
29
+ fmt.app(a.name),
30
+ a.modified ? new Date(a.modified).toISOString() : "-",
31
+ ]);
32
+
33
+ console.log(table(["NAME", "LAST MODIFIED"], rows));
34
+ }
35
+
36
+ export async function appsInfo(name, options) {
37
+ name = resolveAppName(name);
38
+ var target = await resolveTarget(options);
39
+ var cfg = target.cfg;
40
+ var appProvider = await target.provider("app");
41
+
42
+ var info = await appProvider.getAppInfo(cfg, name);
43
+
44
+ if (!info) {
45
+ fatal(
46
+ `App ${fmt.app(name)} not found.`,
47
+ `Run ${fmt.cmd(`relight deploy ${name} .`)} first.`
48
+ );
49
+ }
50
+
51
+ var appConfig = info.appConfig;
52
+
53
+ if (options.json) {
54
+ console.log(JSON.stringify(appConfig, null, 2));
55
+ return;
56
+ }
57
+
58
+ console.log("");
59
+ console.log(`${fmt.bold("App:")} ${fmt.app(name)}`);
60
+ if (info.url) console.log(`${fmt.bold("URL:")} ${fmt.url(info.url)}`);
61
+ console.log(
62
+ `${fmt.bold("Image:")} ${appConfig.image || fmt.dim("(not deployed)")}`
63
+ );
64
+ console.log(`${fmt.bold("Regions:")} ${appConfig.regions.join(", ")}`);
65
+ console.log(`${fmt.bold("Instances:")} ${appConfig.instances} per region`);
66
+ console.log(`${fmt.bold("Port:")} ${appConfig.port}`);
67
+ console.log(
68
+ `${fmt.bold("Domains:")} ${(appConfig.domains || []).join(", ") || fmt.dim("(none)")}`
69
+ );
70
+ var envCount = (appConfig.envKeys || []).length;
71
+ var secretCount = (appConfig.secretKeys || []).length;
72
+ var totalCount = envCount + secretCount;
73
+ if (!appConfig.envKeys && appConfig.env) totalCount = Object.keys(appConfig.env).length;
74
+ var envDisplay = secretCount > 0 ? `${totalCount} (${secretCount} secret)` : `${totalCount}`;
75
+ console.log(
76
+ `${fmt.bold("Env vars:")} ${envDisplay}`
77
+ );
78
+ if (appConfig.dbId) {
79
+ console.log(`${fmt.bold("Database:")} ${appConfig.dbName || appConfig.dbId}`);
80
+ }
81
+ if (appConfig.deployedAt) {
82
+ console.log(`${fmt.bold("Deployed:")} ${appConfig.deployedAt}`);
83
+ }
84
+ if (appConfig.createdAt) {
85
+ console.log(`${fmt.bold("Created:")} ${appConfig.createdAt}`);
86
+ }
87
+ }
88
+
89
+ export async function appsDestroy(name, options) {
90
+ name = resolveAppName(name);
91
+ var target = await resolveTarget(options);
92
+ var cfg = target.cfg;
93
+ var appProvider = await target.provider("app");
94
+
95
+ if (options.confirm !== name) {
96
+ if (process.stdin.isTTY) {
97
+ var rl = createInterface({ input: process.stdin, output: process.stderr });
98
+ var answer = await new Promise((resolve) =>
99
+ rl.question(`Type "${name}" to confirm destruction: `, resolve)
100
+ );
101
+ rl.close();
102
+ if (answer.trim() !== name) {
103
+ fatal("Confirmation did not match. Aborting.");
104
+ }
105
+ } else {
106
+ fatal(
107
+ `Destroying ${fmt.app(name)} requires confirmation.`,
108
+ `Run: relight apps destroy ${name} --confirm ${name}`
109
+ );
110
+ }
111
+ }
112
+
113
+ process.stderr.write(`Destroying ${fmt.app(name)}...\n`);
114
+
115
+ try {
116
+ await appProvider.destroyApp(cfg, name);
117
+ } catch (e) {
118
+ fatal(`Could not destroy ${fmt.app(name)}.`, e.message);
119
+ }
120
+
121
+ // Remove .relight if it points to this app
122
+ var linked = readLink();
123
+ if (linked && linked.app === name) {
124
+ unlinkApp();
125
+ }
126
+
127
+ success(`App ${fmt.app(name)} destroyed.`);
128
+ }
@@ -168,17 +168,21 @@ async function authGCP(rl) {
168
168
  var SA_URL =
169
169
  "https://console.cloud.google.com/iam-admin/serviceaccounts/create";
170
170
  var ENABLE_APIS =
171
- "https://console.cloud.google.com/apis/enableflow?apiid=run.googleapis.com,artifactregistry.googleapis.com";
171
+ "https://console.cloud.google.com/apis/enableflow?apiid=run.googleapis.com,artifactregistry.googleapis.com,sqladmin.googleapis.com,dns.googleapis.com,logging.googleapis.com,monitoring.googleapis.com";
172
172
 
173
173
  process.stderr.write(`\n ${kleur.bold("Setup")}\n\n`);
174
- process.stderr.write(` 1. Enable the Cloud Run and Artifact Registry APIs:\n`);
174
+ process.stderr.write(` 1. Enable the required APIs:\n`);
175
175
  process.stderr.write(` ${fmt.url(ENABLE_APIS)}\n\n`);
176
176
  process.stderr.write(` 2. Create a service account:\n`);
177
177
  process.stderr.write(` ${fmt.url(SA_URL)}\n\n`);
178
178
  process.stderr.write(` Name it ${fmt.val("relight")} and grant these roles:\n`);
179
179
  process.stderr.write(` ${fmt.val("Cloud Run Admin")}\n`);
180
180
  process.stderr.write(` ${fmt.val("Artifact Registry Admin")}\n`);
181
- process.stderr.write(` ${fmt.val("Service Account User")}\n\n`);
181
+ process.stderr.write(` ${fmt.val("Service Account User")}\n`);
182
+ process.stderr.write(` ${fmt.val("Cloud SQL Admin")}\n`);
183
+ process.stderr.write(` ${fmt.val("DNS Administrator")}\n`);
184
+ process.stderr.write(` ${fmt.val("Logs Viewer")}\n`);
185
+ process.stderr.write(` ${fmt.val("Monitoring Viewer")}\n\n`);
182
186
  process.stderr.write(` 3. Go to the service account → ${kleur.bold("Keys")} tab\n`);
183
187
  process.stderr.write(` 4. ${kleur.bold("Add Key")} → ${kleur.bold("Create new key")} → ${kleur.bold("JSON")}\n`);
184
188
  process.stderr.write(` 5. Save the downloaded file\n\n`);
@@ -237,7 +241,12 @@ async function authAWS(rl) {
237
241
  process.stderr.write(` ${fmt.url(IAM_CONSOLE)}\n\n`);
238
242
  process.stderr.write(` 2. Create a user and attach these policies:\n`);
239
243
  process.stderr.write(` ${fmt.val("AWSAppRunnerFullAccess")}\n`);
240
- process.stderr.write(` ${fmt.val("AmazonEC2ContainerRegistryFullAccess")}\n\n`);
244
+ process.stderr.write(` ${fmt.val("AmazonEC2ContainerRegistryFullAccess")}\n`);
245
+ process.stderr.write(` ${fmt.val("AmazonRDSFullAccess")}\n`);
246
+ process.stderr.write(` ${fmt.val("AmazonRoute53FullAccess")}\n`);
247
+ process.stderr.write(` ${fmt.val("AmazonEC2ReadOnlyAccess")}\n`);
248
+ process.stderr.write(` ${fmt.val("CloudWatchLogsReadOnlyAccess")}\n`);
249
+ process.stderr.write(` ${fmt.val("IAMFullAccess")}\n\n`);
241
250
  process.stderr.write(` 3. Go to the user's ${kleur.bold("Security credentials")} tab\n`);
242
251
  process.stderr.write(` 4. Click ${kleur.bold("Create access key")} → choose ${kleur.bold("Command Line Interface")}\n`);
243
252
  process.stderr.write(` 5. Copy the Access Key ID and Secret Access Key\n\n`);
@@ -0,0 +1,282 @@
1
+ import { readFileSync } from "fs";
2
+ import { status, success, fatal, hint, fmt } from "../lib/output.js";
3
+ import { resolveAppName } from "../lib/link.js";
4
+ import { resolveTarget } from "../lib/providers/resolve.js";
5
+
6
+ var RESERVED_NAMES = ["RELIGHT_APP_CONFIG", "APP_CONTAINER", "DB", "DB_URL", "DB_TOKEN"];
7
+
8
+ function validateKeyName(key) {
9
+ if (RESERVED_NAMES.includes(key)) {
10
+ fatal(`${fmt.key(key)} is a reserved binding name and cannot be used as an env var.`);
11
+ }
12
+ }
13
+
14
+ function ensureKeyLists(appConfig) {
15
+ if (!appConfig.envKeys) appConfig.envKeys = [];
16
+ if (!appConfig.secretKeys) appConfig.secretKeys = [];
17
+ if (!appConfig.env) appConfig.env = {};
18
+ }
19
+
20
+ export async function configShow(name, options) {
21
+ name = resolveAppName(name);
22
+ var target = await resolveTarget(options);
23
+ var cfg = target.cfg;
24
+ var appProvider = await target.provider("app");
25
+ var appConfig = await appProvider.getAppConfig(cfg, name);
26
+
27
+ if (!appConfig) {
28
+ fatal(
29
+ `App ${fmt.app(name)} not found.`,
30
+ `Run ${fmt.cmd(`relight deploy ${name} .`)} first.`
31
+ );
32
+ }
33
+
34
+ var env = appConfig.env || {};
35
+ var keys = Object.keys(env);
36
+
37
+ if (keys.length === 0) {
38
+ if (options.json) {
39
+ console.log("{}");
40
+ } else {
41
+ process.stderr.write(`No config vars set for ${fmt.app(name)}.\n`);
42
+ }
43
+ return;
44
+ }
45
+
46
+ if (options.json) {
47
+ console.log(JSON.stringify(env, null, 2));
48
+ return;
49
+ }
50
+
51
+ for (var key of keys) {
52
+ console.log(`${fmt.key(key)}=${env[key]}`);
53
+ }
54
+ }
55
+
56
+ export async function configSet(args, options) {
57
+ var name, vars;
58
+ if (args.length === 0) {
59
+ fatal("No env vars provided.", "Usage: relight config set [name] KEY=VALUE ...");
60
+ }
61
+ if (args[0].includes("=")) {
62
+ name = resolveAppName(null);
63
+ vars = args;
64
+ } else {
65
+ name = args[0];
66
+ vars = args.slice(1);
67
+ }
68
+
69
+ if (vars.length === 0) {
70
+ fatal("No env vars provided.", "Usage: relight config set [name] KEY=VALUE ...");
71
+ }
72
+
73
+ var target = await resolveTarget(options);
74
+ var cfg = target.cfg;
75
+ var appProvider = await target.provider("app");
76
+ var appConfig = await appProvider.getAppConfig(cfg, name);
77
+
78
+ if (!appConfig) {
79
+ fatal(
80
+ `App ${fmt.app(name)} not found.`,
81
+ `Run ${fmt.cmd(`relight deploy ${name} .`)} first.`
82
+ );
83
+ }
84
+
85
+ ensureKeyLists(appConfig);
86
+
87
+ var isSecret = options && options.secret;
88
+ var newSecrets = {};
89
+
90
+ for (var v of vars) {
91
+ var eq = v.indexOf("=");
92
+ if (eq === -1) {
93
+ fatal(`Invalid format: ${v}`, "Use KEY=VALUE format.");
94
+ }
95
+ var key = v.substring(0, eq);
96
+ var value = v.substring(eq + 1);
97
+ validateKeyName(key);
98
+
99
+ if (isSecret) {
100
+ appConfig.envKeys = appConfig.envKeys.filter((k) => k !== key);
101
+ if (!appConfig.secretKeys.includes(key)) appConfig.secretKeys.push(key);
102
+ newSecrets[key] = value;
103
+ appConfig.env[key] = "[hidden]";
104
+ status(`${fmt.key(key)} set ${fmt.dim("(secret)")}`);
105
+ } else {
106
+ appConfig.secretKeys = appConfig.secretKeys.filter((k) => k !== key);
107
+ if (!appConfig.envKeys.includes(key)) appConfig.envKeys.push(key);
108
+ appConfig.env[key] = value;
109
+ status(`${fmt.key(key)} set`);
110
+ }
111
+ }
112
+
113
+ await appProvider.pushAppConfig(cfg, name, appConfig, { newSecrets: Object.keys(newSecrets).length > 0 ? newSecrets : undefined });
114
+ success("Config updated (live).");
115
+ }
116
+
117
+ export async function configGet(args, options) {
118
+ var name, key;
119
+ if (args.length === 2) {
120
+ name = args[0];
121
+ key = args[1];
122
+ } else if (args.length === 1) {
123
+ name = resolveAppName(null);
124
+ key = args[0];
125
+ } else {
126
+ fatal("Usage: relight config get [name] <key>");
127
+ }
128
+
129
+ var target = await resolveTarget(options);
130
+ var cfg = target.cfg;
131
+ var appProvider = await target.provider("app");
132
+ var appConfig = await appProvider.getAppConfig(cfg, name);
133
+
134
+ if (!appConfig) {
135
+ fatal(
136
+ `App ${fmt.app(name)} not found.`,
137
+ `Run ${fmt.cmd(`relight deploy ${name} .`)} first.`
138
+ );
139
+ }
140
+
141
+ var env = appConfig.env || {};
142
+
143
+ if (!(key in env)) {
144
+ fatal(`Key ${fmt.key(key)} is not set on ${fmt.app(name)}.`);
145
+ }
146
+
147
+ if ((appConfig.secretKeys || []).includes(key)) {
148
+ console.log("[hidden]");
149
+ hint("Secret values are write-only", "they cannot be read back after being set.");
150
+ return;
151
+ }
152
+
153
+ console.log(env[key]);
154
+ }
155
+
156
+ export async function configUnset(args, options) {
157
+ var name, keys;
158
+ if (args.length === 0) {
159
+ fatal("No keys provided.", "Usage: relight config unset [name] KEY ...");
160
+ }
161
+ if (args.length === 1) {
162
+ name = resolveAppName(null);
163
+ keys = args;
164
+ } else if (/^[A-Z_][A-Z0-9_]*$/.test(args[0])) {
165
+ name = resolveAppName(null);
166
+ keys = args;
167
+ } else {
168
+ name = args[0];
169
+ keys = args.slice(1);
170
+ }
171
+
172
+ if (keys.length === 0) {
173
+ fatal("No keys provided.", "Usage: relight config unset [name] KEY ...");
174
+ }
175
+
176
+ var target = await resolveTarget(options);
177
+ var cfg = target.cfg;
178
+ var appProvider = await target.provider("app");
179
+ var appConfig = await appProvider.getAppConfig(cfg, name);
180
+
181
+ if (!appConfig) {
182
+ fatal(
183
+ `App ${fmt.app(name)} not found.`,
184
+ `Run ${fmt.cmd(`relight deploy ${name} .`)} first.`
185
+ );
186
+ }
187
+
188
+ ensureKeyLists(appConfig);
189
+
190
+ for (var key of keys) {
191
+ var wasEnv = appConfig.envKeys.includes(key);
192
+ var wasSecret = appConfig.secretKeys.includes(key);
193
+ if (!wasEnv && !wasSecret) {
194
+ status(`${fmt.key(key)} ${fmt.dim("(not set, skipping)")}`);
195
+ continue;
196
+ }
197
+ appConfig.envKeys = appConfig.envKeys.filter((k) => k !== key);
198
+ appConfig.secretKeys = appConfig.secretKeys.filter((k) => k !== key);
199
+ delete appConfig.env[key];
200
+ status(`${fmt.key(key)} removed`);
201
+ }
202
+
203
+ await appProvider.pushAppConfig(cfg, name, appConfig);
204
+ success("Config updated (live).");
205
+ }
206
+
207
+ export async function configImport(name, options) {
208
+ name = resolveAppName(name);
209
+ var target = await resolveTarget(options);
210
+ var cfg = target.cfg;
211
+ var appProvider = await target.provider("app");
212
+ var appConfig = await appProvider.getAppConfig(cfg, name);
213
+
214
+ if (!appConfig) {
215
+ fatal(
216
+ `App ${fmt.app(name)} not found.`,
217
+ `Run ${fmt.cmd(`relight deploy ${name} .`)} first.`
218
+ );
219
+ }
220
+
221
+ ensureKeyLists(appConfig);
222
+
223
+ var input;
224
+ if (options.file) {
225
+ try {
226
+ input = readFileSync(options.file, "utf-8");
227
+ } catch (e) {
228
+ fatal(`Could not read file: ${options.file}`, e.message);
229
+ }
230
+ } else if (process.stdin.isTTY) {
231
+ fatal(
232
+ "No input provided.",
233
+ "Pipe a .env file: cat .env | relight config import\n Or use: relight config import --file .env"
234
+ );
235
+ } else {
236
+ var chunks = [];
237
+ for await (var chunk of process.stdin) {
238
+ chunks.push(chunk);
239
+ }
240
+ input = Buffer.concat(chunks).toString("utf-8");
241
+ }
242
+
243
+ var isSecret = options && options.secret;
244
+ var newSecrets = {};
245
+ var count = 0;
246
+ for (var line of input.split("\n")) {
247
+ line = line.trim();
248
+ if (!line || line.startsWith("#")) continue;
249
+ var eq = line.indexOf("=");
250
+ if (eq === -1) continue;
251
+ var key = line.substring(0, eq).trim();
252
+ var value = line.substring(eq + 1).trim();
253
+ if (
254
+ (value.startsWith('"') && value.endsWith('"')) ||
255
+ (value.startsWith("'") && value.endsWith("'"))
256
+ ) {
257
+ value = value.slice(1, -1);
258
+ }
259
+ validateKeyName(key);
260
+
261
+ if (isSecret) {
262
+ appConfig.envKeys = appConfig.envKeys.filter((k) => k !== key);
263
+ if (!appConfig.secretKeys.includes(key)) appConfig.secretKeys.push(key);
264
+ newSecrets[key] = value;
265
+ appConfig.env[key] = "[hidden]";
266
+ status(`${fmt.key(key)} set ${fmt.dim("(secret)")}`);
267
+ } else {
268
+ appConfig.secretKeys = appConfig.secretKeys.filter((k) => k !== key);
269
+ if (!appConfig.envKeys.includes(key)) appConfig.envKeys.push(key);
270
+ appConfig.env[key] = value;
271
+ status(`${fmt.key(key)} set`);
272
+ }
273
+ count++;
274
+ }
275
+
276
+ if (count === 0) {
277
+ fatal("No variables found in input.", "Use KEY=VALUE format, one per line.");
278
+ }
279
+
280
+ await appProvider.pushAppConfig(cfg, name, appConfig, { newSecrets: Object.keys(newSecrets).length > 0 ? newSecrets : undefined });
281
+ success(`${count} variable${count !== 1 ? "s" : ""} imported (live).`);
282
+ }