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.
- package/README.md +77 -34
- package/package.json +12 -4
- package/src/cli.js +305 -1
- package/src/commands/apps.js +128 -0
- package/src/commands/auth.js +75 -4
- package/src/commands/config.js +282 -0
- package/src/commands/cost.js +593 -0
- package/src/commands/db.js +531 -0
- package/src/commands/deploy.js +298 -0
- package/src/commands/doctor.js +41 -9
- 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/lib/clouds/aws.js +309 -35
- package/src/lib/clouds/cf.js +401 -2
- package/src/lib/clouds/gcp.js +234 -3
- package/src/lib/clouds/slicervm.js +139 -0
- package/src/lib/config.js +40 -0
- package/src/lib/docker.js +34 -0
- package/src/lib/link.js +20 -5
- package/src/lib/providers/aws/app.js +481 -0
- package/src/lib/providers/aws/db.js +513 -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 +279 -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 +457 -0
- package/src/lib/providers/gcp/dns.js +166 -0
- package/src/lib/providers/gcp/registry.js +30 -0
- package/src/lib/providers/resolve.js +49 -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,128 @@
|
|
|
1
|
+
import { success, fatal, hint, fmt, table } from "../lib/output.js";
|
|
2
|
+
import { resolveAppName, readLink, unlinkApp } from "../lib/link.js";
|
|
3
|
+
import { resolveCloudId, getCloudCfg, getProvider } from "../lib/providers/resolve.js";
|
|
4
|
+
import { createInterface } from "readline";
|
|
5
|
+
|
|
6
|
+
export async function appsList(options) {
|
|
7
|
+
var cloud = resolveCloudId(options.cloud);
|
|
8
|
+
var cfg = getCloudCfg(cloud);
|
|
9
|
+
var appProvider = await getProvider(cloud, "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 cloud = resolveCloudId(options.cloud);
|
|
39
|
+
var cfg = getCloudCfg(cloud);
|
|
40
|
+
var appProvider = await getProvider(cloud, "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 cloud = resolveCloudId(options.cloud);
|
|
92
|
+
var cfg = getCloudCfg(cloud);
|
|
93
|
+
var appProvider = await getProvider(cloud, "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
|
+
}
|
package/src/commands/auth.js
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
verifyProject,
|
|
17
17
|
} from "../lib/clouds/gcp.js";
|
|
18
18
|
import { verifyCredentials as awsVerify } from "../lib/clouds/aws.js";
|
|
19
|
+
import { verifyConnection as slicerVerify } from "../lib/clouds/slicervm.js";
|
|
19
20
|
import kleur from "kleur";
|
|
20
21
|
|
|
21
22
|
function prompt(rl, question) {
|
|
@@ -72,6 +73,9 @@ export async function auth(options) {
|
|
|
72
73
|
case "aws":
|
|
73
74
|
cloudConfig = await authAWS(rl);
|
|
74
75
|
break;
|
|
76
|
+
case "slicervm":
|
|
77
|
+
cloudConfig = await authSlicerVM(rl);
|
|
78
|
+
break;
|
|
75
79
|
}
|
|
76
80
|
|
|
77
81
|
rl.close();
|
|
@@ -105,6 +109,8 @@ function normalizeCompute(input) {
|
|
|
105
109
|
aws: "aws",
|
|
106
110
|
amazon: "aws",
|
|
107
111
|
"app-runner": "aws",
|
|
112
|
+
slicervm: "slicervm",
|
|
113
|
+
slicer: "slicervm",
|
|
108
114
|
};
|
|
109
115
|
return aliases[input.toLowerCase()] || input.toLowerCase();
|
|
110
116
|
}
|
|
@@ -168,17 +174,21 @@ async function authGCP(rl) {
|
|
|
168
174
|
var SA_URL =
|
|
169
175
|
"https://console.cloud.google.com/iam-admin/serviceaccounts/create";
|
|
170
176
|
var ENABLE_APIS =
|
|
171
|
-
"https://console.cloud.google.com/apis/enableflow?apiid=run.googleapis.com,artifactregistry.googleapis.com";
|
|
177
|
+
"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
178
|
|
|
173
179
|
process.stderr.write(`\n ${kleur.bold("Setup")}\n\n`);
|
|
174
|
-
process.stderr.write(` 1. Enable the
|
|
180
|
+
process.stderr.write(` 1. Enable the required APIs:\n`);
|
|
175
181
|
process.stderr.write(` ${fmt.url(ENABLE_APIS)}\n\n`);
|
|
176
182
|
process.stderr.write(` 2. Create a service account:\n`);
|
|
177
183
|
process.stderr.write(` ${fmt.url(SA_URL)}\n\n`);
|
|
178
184
|
process.stderr.write(` Name it ${fmt.val("relight")} and grant these roles:\n`);
|
|
179
185
|
process.stderr.write(` ${fmt.val("Cloud Run Admin")}\n`);
|
|
180
186
|
process.stderr.write(` ${fmt.val("Artifact Registry Admin")}\n`);
|
|
181
|
-
process.stderr.write(` ${fmt.val("Service Account User")}\n
|
|
187
|
+
process.stderr.write(` ${fmt.val("Service Account User")}\n`);
|
|
188
|
+
process.stderr.write(` ${fmt.val("Cloud SQL Admin")}\n`);
|
|
189
|
+
process.stderr.write(` ${fmt.val("DNS Administrator")}\n`);
|
|
190
|
+
process.stderr.write(` ${fmt.val("Logs Viewer")}\n`);
|
|
191
|
+
process.stderr.write(` ${fmt.val("Monitoring Viewer")}\n\n`);
|
|
182
192
|
process.stderr.write(` 3. Go to the service account → ${kleur.bold("Keys")} tab\n`);
|
|
183
193
|
process.stderr.write(` 4. ${kleur.bold("Add Key")} → ${kleur.bold("Create new key")} → ${kleur.bold("JSON")}\n`);
|
|
184
194
|
process.stderr.write(` 5. Save the downloaded file\n\n`);
|
|
@@ -237,7 +247,12 @@ async function authAWS(rl) {
|
|
|
237
247
|
process.stderr.write(` ${fmt.url(IAM_CONSOLE)}\n\n`);
|
|
238
248
|
process.stderr.write(` 2. Create a user and attach these policies:\n`);
|
|
239
249
|
process.stderr.write(` ${fmt.val("AWSAppRunnerFullAccess")}\n`);
|
|
240
|
-
process.stderr.write(` ${fmt.val("AmazonEC2ContainerRegistryFullAccess")}\n
|
|
250
|
+
process.stderr.write(` ${fmt.val("AmazonEC2ContainerRegistryFullAccess")}\n`);
|
|
251
|
+
process.stderr.write(` ${fmt.val("AmazonRDSFullAccess")}\n`);
|
|
252
|
+
process.stderr.write(` ${fmt.val("AmazonRoute53FullAccess")}\n`);
|
|
253
|
+
process.stderr.write(` ${fmt.val("AmazonEC2ReadOnlyAccess")}\n`);
|
|
254
|
+
process.stderr.write(` ${fmt.val("CloudWatchLogsReadOnlyAccess")}\n`);
|
|
255
|
+
process.stderr.write(` ${fmt.val("IAMFullAccess")}\n\n`);
|
|
241
256
|
process.stderr.write(` 3. Go to the user's ${kleur.bold("Security credentials")} tab\n`);
|
|
242
257
|
process.stderr.write(` 4. Click ${kleur.bold("Create access key")} → choose ${kleur.bold("Command Line Interface")}\n`);
|
|
243
258
|
process.stderr.write(` 5. Copy the Access Key ID and Secret Access Key\n\n`);
|
|
@@ -287,6 +302,62 @@ async function authAWS(rl) {
|
|
|
287
302
|
return { accessKeyId, secretAccessKey, region };
|
|
288
303
|
}
|
|
289
304
|
|
|
305
|
+
// --- SlicerVM ---
|
|
306
|
+
|
|
307
|
+
async function authSlicerVM(rl) {
|
|
308
|
+
process.stderr.write(`\n ${kleur.bold("Connection mode")}\n\n`);
|
|
309
|
+
process.stderr.write(` ${kleur.bold("[1]")} Unix socket (local dev)\n`);
|
|
310
|
+
process.stderr.write(` ${kleur.bold("[2]")} HTTP API (remote)\n\n`);
|
|
311
|
+
|
|
312
|
+
var modeChoice = await prompt(rl, "Select [1-2]: ");
|
|
313
|
+
var useSocket = modeChoice.trim() === "1";
|
|
314
|
+
|
|
315
|
+
var config = {};
|
|
316
|
+
|
|
317
|
+
if (useSocket) {
|
|
318
|
+
var defaultSocket = "/var/run/slicer/slicer.sock";
|
|
319
|
+
var socketPath = await prompt(rl, `Socket path [${defaultSocket}]: `);
|
|
320
|
+
socketPath = (socketPath || "").trim() || defaultSocket;
|
|
321
|
+
config.socketPath = socketPath;
|
|
322
|
+
} else {
|
|
323
|
+
var apiUrl = await prompt(rl, "Slicer API URL (e.g. https://slicer.example.com:8080): ");
|
|
324
|
+
apiUrl = (apiUrl || "").trim().replace(/\/+$/, "");
|
|
325
|
+
if (!apiUrl) fatal("No API URL provided.");
|
|
326
|
+
config.apiUrl = apiUrl;
|
|
327
|
+
|
|
328
|
+
var token = await prompt(rl, "API token: ");
|
|
329
|
+
token = (token || "").trim();
|
|
330
|
+
if (!token) fatal("No token provided.");
|
|
331
|
+
config.token = token;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
var hostGroup = await prompt(rl, "Host group [apps]: ");
|
|
335
|
+
config.hostGroup = (hostGroup || "").trim() || "apps";
|
|
336
|
+
|
|
337
|
+
var baseDomain = await prompt(rl, "Base domain (e.g. apps.example.com) [localhost]: ");
|
|
338
|
+
config.baseDomain = (baseDomain || "").trim() || "localhost";
|
|
339
|
+
|
|
340
|
+
process.stderr.write("\nVerifying...\n");
|
|
341
|
+
var verifyCfg = useSocket
|
|
342
|
+
? { socketPath: config.socketPath }
|
|
343
|
+
: { apiUrl: config.apiUrl, apiToken: config.token };
|
|
344
|
+
try {
|
|
345
|
+
await slicerVerify(verifyCfg);
|
|
346
|
+
} catch (e) {
|
|
347
|
+
fatal("Connection failed.", e.message);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (useSocket) {
|
|
351
|
+
process.stderr.write(` Socket: ${fmt.bold(config.socketPath)}\n`);
|
|
352
|
+
} else {
|
|
353
|
+
process.stderr.write(` API: ${fmt.bold(config.apiUrl)}\n`);
|
|
354
|
+
}
|
|
355
|
+
process.stderr.write(` Host group: ${fmt.dim(config.hostGroup)}\n`);
|
|
356
|
+
process.stderr.write(` Base domain: ${fmt.dim(config.baseDomain)}\n`);
|
|
357
|
+
|
|
358
|
+
return config;
|
|
359
|
+
}
|
|
360
|
+
|
|
290
361
|
// --- Helpers ---
|
|
291
362
|
|
|
292
363
|
function tryExec(cmd) {
|
|
@@ -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 { resolveCloudId, getCloudCfg, getProvider } 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 cloud = resolveCloudId(options.cloud);
|
|
23
|
+
var cfg = getCloudCfg(cloud);
|
|
24
|
+
var appProvider = await getProvider(cloud, "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 cloud = resolveCloudId(options.cloud);
|
|
74
|
+
var cfg = getCloudCfg(cloud);
|
|
75
|
+
var appProvider = await getProvider(cloud, "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 cloud = resolveCloudId(options.cloud);
|
|
130
|
+
var cfg = getCloudCfg(cloud);
|
|
131
|
+
var appProvider = await getProvider(cloud, "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 cloud = resolveCloudId(options.cloud);
|
|
177
|
+
var cfg = getCloudCfg(cloud);
|
|
178
|
+
var appProvider = await getProvider(cloud, "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 cloud = resolveCloudId(options.cloud);
|
|
210
|
+
var cfg = getCloudCfg(cloud);
|
|
211
|
+
var appProvider = await getProvider(cloud, "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
|
+
}
|