relight-cli 0.2.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/package.json +1 -1
- package/src/cli.js +94 -49
- package/src/commands/apps.js +10 -10
- package/src/commands/auth.js +0 -62
- package/src/commands/config.js +16 -16
- package/src/commands/cost.js +6 -6
- package/src/commands/db.js +434 -190
- package/src/commands/deploy.js +38 -72
- package/src/commands/doctor.js +28 -4
- package/src/commands/domains.js +22 -22
- package/src/commands/logs.js +4 -4
- package/src/commands/open.js +4 -4
- package/src/commands/ps.js +4 -4
- package/src/commands/scale.js +4 -4
- package/src/commands/service.js +227 -0
- package/src/lib/clouds/gcp.js +21 -1
- package/src/lib/clouds/neon.js +147 -0
- package/src/lib/config.js +169 -11
- package/src/lib/link.js +13 -2
- package/src/lib/providers/aws/db.js +217 -226
- package/src/lib/providers/cf/db.js +33 -131
- package/src/lib/providers/gcp/db.js +169 -254
- package/src/lib/providers/neon/db.js +306 -0
- package/src/lib/providers/resolve.js +33 -3
package/src/commands/deploy.js
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import { createInterface } from "readline";
|
|
2
|
-
import { randomBytes } from "crypto";
|
|
3
2
|
import { phase, status, success, hint, fatal, fmt, generateAppName } from "../lib/output.js";
|
|
4
3
|
import { readLink, linkApp, resolveAppName } from "../lib/link.js";
|
|
5
|
-
import {
|
|
4
|
+
import { resolveTarget } from "../lib/providers/resolve.js";
|
|
6
5
|
import { dockerBuild, dockerTag, dockerPush, dockerLogin } from "../lib/docker.js";
|
|
7
6
|
|
|
8
7
|
export async function deploy(nameOrPath, path, options) {
|
|
9
|
-
var
|
|
10
|
-
var cfg =
|
|
8
|
+
var target = await resolveTarget(options);
|
|
9
|
+
var cfg = target.cfg;
|
|
11
10
|
|
|
12
11
|
// Smart arg parsing: if first arg looks like a path, shift args
|
|
13
12
|
var name;
|
|
@@ -26,12 +25,12 @@ export async function deploy(nameOrPath, path, options) {
|
|
|
26
25
|
var tag = options.tag || `${Date.now()}`;
|
|
27
26
|
|
|
28
27
|
// Get registry credentials and image tag
|
|
29
|
-
var registry = await
|
|
28
|
+
var registry = await target.provider("registry");
|
|
30
29
|
var remoteTag = await registry.getImageTag(cfg, name, tag);
|
|
31
30
|
var localTag = `relight-${name}:${tag}`;
|
|
32
31
|
|
|
33
32
|
// Load existing config from deployed worker (null on first deploy)
|
|
34
|
-
var appProvider = await
|
|
33
|
+
var appProvider = await target.provider("app");
|
|
35
34
|
var appConfig;
|
|
36
35
|
try {
|
|
37
36
|
appConfig = await appProvider.getAppConfig(cfg, name);
|
|
@@ -41,7 +40,7 @@ export async function deploy(nameOrPath, path, options) {
|
|
|
41
40
|
|
|
42
41
|
var isFirstDeploy = !appConfig;
|
|
43
42
|
|
|
44
|
-
// Get valid regions for this cloud
|
|
43
|
+
// Get valid regions for this cloud/service
|
|
45
44
|
var validRegions = appProvider.getRegions();
|
|
46
45
|
var validCodes = validRegions.map((r) => r.code);
|
|
47
46
|
|
|
@@ -89,12 +88,13 @@ export async function deploy(nameOrPath, path, options) {
|
|
|
89
88
|
}
|
|
90
89
|
}
|
|
91
90
|
|
|
92
|
-
var
|
|
91
|
+
var isService = target.kind === "service";
|
|
92
|
+
var defaultRegion = isService ? "self-hosted" : target.type === "gcp" ? "us-central1" : target.type === "aws" ? "us-east-1" : "enam";
|
|
93
93
|
var regions;
|
|
94
94
|
|
|
95
95
|
if (options.regions) {
|
|
96
96
|
regions = options.regions.split(",").map((r) => r.trim());
|
|
97
|
-
} else if ((
|
|
97
|
+
} else if (!isService && (target.type === "gcp" || target.type === "aws") && process.stdin.isTTY) {
|
|
98
98
|
// Interactive region picker for GCP/AWS first deploy
|
|
99
99
|
var { createInterface: createRL } = await import("readline");
|
|
100
100
|
var rl = createRL({ input: process.stdin, output: process.stderr });
|
|
@@ -130,10 +130,10 @@ export async function deploy(nameOrPath, path, options) {
|
|
|
130
130
|
appConfig = {
|
|
131
131
|
name,
|
|
132
132
|
regions,
|
|
133
|
-
instances: options.instances || (
|
|
133
|
+
instances: options.instances || (isService ? 1 : 2),
|
|
134
134
|
port: options.port || 8080,
|
|
135
135
|
sleepAfter: options.sleep || "30s",
|
|
136
|
-
instanceType: options.instanceType || (
|
|
136
|
+
instanceType: options.instanceType || (isService || target.type === "gcp" || target.type === "aws" ? undefined : "lite"),
|
|
137
137
|
vcpu: options.vcpu || undefined,
|
|
138
138
|
memory: options.memory || undefined,
|
|
139
139
|
disk: options.disk || undefined,
|
|
@@ -147,48 +147,7 @@ export async function deploy(nameOrPath, path, options) {
|
|
|
147
147
|
};
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
-
// --- D1 database (--db flag, CF only) ---
|
|
151
150
|
var newSecrets = {};
|
|
152
|
-
if (options.db && !appConfig.dbId) {
|
|
153
|
-
if (cloud === "gcp") {
|
|
154
|
-
process.stderr.write(
|
|
155
|
-
`\n${fmt.dim("Cloud SQL provisioning takes 3-10 minutes. Use 'relight db create' separately after deploy.")}\n\n`
|
|
156
|
-
);
|
|
157
|
-
} else if (cloud === "aws") {
|
|
158
|
-
process.stderr.write(
|
|
159
|
-
`\n${fmt.dim("RDS provisioning takes 5-15 minutes. Use 'relight db create' separately after deploy.")}\n\n`
|
|
160
|
-
);
|
|
161
|
-
} else if (cloud === "cf") {
|
|
162
|
-
var { createD1Database, getWorkersSubdomain } = await import("../lib/clouds/cf.js");
|
|
163
|
-
var dbResult = await createD1Database(cfg.accountId, cfg.apiToken, `relight-${name}`, {
|
|
164
|
-
locationHint: options.dbLocation,
|
|
165
|
-
jurisdiction: options.dbJurisdiction,
|
|
166
|
-
});
|
|
167
|
-
appConfig.dbId = dbResult.uuid;
|
|
168
|
-
appConfig.dbName = `relight-${name}`;
|
|
169
|
-
|
|
170
|
-
if (!appConfig.envKeys) appConfig.envKeys = [];
|
|
171
|
-
if (!appConfig.secretKeys) appConfig.secretKeys = [];
|
|
172
|
-
if (!appConfig.env) appConfig.env = {};
|
|
173
|
-
|
|
174
|
-
var subdomain = await getWorkersSubdomain(cfg.accountId, cfg.apiToken);
|
|
175
|
-
var dbUrl = subdomain
|
|
176
|
-
? `https://relight-${name}.${subdomain}.workers.dev`
|
|
177
|
-
: null;
|
|
178
|
-
|
|
179
|
-
if (dbUrl) {
|
|
180
|
-
appConfig.env["DB_URL"] = dbUrl;
|
|
181
|
-
if (!appConfig.envKeys.includes("DB_URL")) appConfig.envKeys.push("DB_URL");
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
appConfig.env["DB_TOKEN"] = "[hidden]";
|
|
185
|
-
appConfig.secretKeys = appConfig.secretKeys.filter((k) => k !== "DB_TOKEN");
|
|
186
|
-
appConfig.secretKeys.push("DB_TOKEN");
|
|
187
|
-
appConfig.envKeys = appConfig.envKeys.filter((k) => k !== "DB_TOKEN");
|
|
188
|
-
|
|
189
|
-
newSecrets.DB_TOKEN = randomBytes(32).toString("hex");
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
151
|
|
|
193
152
|
// --- Summary & confirmation ---
|
|
194
153
|
var instanceDesc = appConfig.vcpu
|
|
@@ -198,7 +157,11 @@ export async function deploy(nameOrPath, path, options) {
|
|
|
198
157
|
process.stderr.write(`\n${fmt.bold("Deploy summary")}\n`);
|
|
199
158
|
process.stderr.write(`${fmt.dim("-".repeat(40))}\n`);
|
|
200
159
|
process.stderr.write(` ${fmt.bold("App:")} ${fmt.app(name)}${isFirstDeploy ? fmt.dim(" (new)") : ""}\n`);
|
|
201
|
-
|
|
160
|
+
if (target.kind === "service") {
|
|
161
|
+
process.stderr.write(` ${fmt.bold("Service:")} ${fmt.cloud(target.id)} ${fmt.dim(`(${target.type})`)}\n`);
|
|
162
|
+
} else {
|
|
163
|
+
process.stderr.write(` ${fmt.bold("Cloud:")} ${fmt.cloud(target.id)}\n`);
|
|
164
|
+
}
|
|
202
165
|
process.stderr.write(` ${fmt.bold("Path:")} ${dockerPath}\n`);
|
|
203
166
|
process.stderr.write(` ${fmt.bold("Image:")} ${remoteTag}\n`);
|
|
204
167
|
process.stderr.write(` ${fmt.bold("Regions:")} ${appConfig.regions.join(", ")}\n`);
|
|
@@ -225,7 +188,7 @@ export async function deploy(nameOrPath, path, options) {
|
|
|
225
188
|
|
|
226
189
|
// 1. Build Docker image
|
|
227
190
|
var platform = "linux/amd64";
|
|
228
|
-
if (
|
|
191
|
+
if (target.type === "slicervm") {
|
|
229
192
|
// Match VM architecture
|
|
230
193
|
var { listNodes } = await import("../lib/clouds/slicervm.js");
|
|
231
194
|
var nodes = await listNodes(cfg);
|
|
@@ -236,8 +199,8 @@ export async function deploy(nameOrPath, path, options) {
|
|
|
236
199
|
status(`${localTag} for ${platform}`);
|
|
237
200
|
dockerBuild(dockerPath, localTag, { platform });
|
|
238
201
|
|
|
239
|
-
if (
|
|
240
|
-
//
|
|
202
|
+
if (target.kind === "service") {
|
|
203
|
+
// Service: skip registry push - deploy extracts and uploads the image directly
|
|
241
204
|
phase("Deploying");
|
|
242
205
|
await appProvider.deploy(cfg, name, localTag, {
|
|
243
206
|
appConfig,
|
|
@@ -268,21 +231,20 @@ export async function deploy(nameOrPath, path, options) {
|
|
|
268
231
|
var url = await appProvider.getAppUrl(cfg, name);
|
|
269
232
|
|
|
270
233
|
if (options.json) {
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
);
|
|
234
|
+
var result = {
|
|
235
|
+
name,
|
|
236
|
+
image: remoteTag,
|
|
237
|
+
url,
|
|
238
|
+
regions: appConfig.regions,
|
|
239
|
+
instances: appConfig.instances,
|
|
240
|
+
firstDeploy: isFirstDeploy,
|
|
241
|
+
};
|
|
242
|
+
if (target.kind === "service") {
|
|
243
|
+
result.service = target.id;
|
|
244
|
+
} else {
|
|
245
|
+
result.cloud = target.id;
|
|
246
|
+
}
|
|
247
|
+
console.log(JSON.stringify(result, null, 2));
|
|
286
248
|
} else {
|
|
287
249
|
success(`App ${fmt.app(name)} deployed!`);
|
|
288
250
|
process.stderr.write(` ${fmt.bold("Name:")} ${fmt.app(name)}\n`);
|
|
@@ -294,5 +256,9 @@ export async function deploy(nameOrPath, path, options) {
|
|
|
294
256
|
}
|
|
295
257
|
|
|
296
258
|
// Link this directory to the app
|
|
297
|
-
|
|
259
|
+
if (target.kind === "service") {
|
|
260
|
+
linkApp(name, null, options.dns, null, target.id);
|
|
261
|
+
} else {
|
|
262
|
+
linkApp(name, target.id, options.dns);
|
|
263
|
+
}
|
|
298
264
|
}
|
package/src/commands/doctor.js
CHANGED
|
@@ -4,10 +4,14 @@ import {
|
|
|
4
4
|
tryGetConfig,
|
|
5
5
|
CONFIG_PATH,
|
|
6
6
|
CLOUD_NAMES,
|
|
7
|
+
SERVICE_TYPES,
|
|
8
|
+
getRegisteredServices,
|
|
9
|
+
normalizeServiceConfig,
|
|
7
10
|
} from "../lib/config.js";
|
|
8
11
|
import { verifyToken as cfVerify, getWorkersSubdomain } from "../lib/clouds/cf.js";
|
|
9
12
|
import { mintAccessToken, verifyProject as gcpVerifyProject, listRegions as gcpListRegions, gcpApi, AR_API, SQLADMIN_API, DNS_API } from "../lib/clouds/gcp.js";
|
|
10
13
|
import { verifyCredentials as awsVerify, checkAppRunner, awsJsonApi, awsQueryApi, awsRestXmlApi } from "../lib/clouds/aws.js";
|
|
14
|
+
import { verifyConnection as slicerVerify } from "../lib/clouds/slicervm.js";
|
|
11
15
|
import kleur from "kleur";
|
|
12
16
|
|
|
13
17
|
var PASS = kleur.green("[ok]");
|
|
@@ -16,7 +20,7 @@ var SKIP = kleur.yellow("[--]");
|
|
|
16
20
|
|
|
17
21
|
export async function doctor() {
|
|
18
22
|
process.stderr.write(`\n${kleur.bold("relight doctor")}\n`);
|
|
19
|
-
process.stderr.write(`${kleur.dim("
|
|
23
|
+
process.stderr.write(`${kleur.dim("-".repeat(50))}\n\n`);
|
|
20
24
|
var allGood = true;
|
|
21
25
|
|
|
22
26
|
// --- General checks ---
|
|
@@ -73,9 +77,29 @@ export async function doctor() {
|
|
|
73
77
|
}
|
|
74
78
|
}
|
|
75
79
|
|
|
80
|
+
// --- Services ---
|
|
81
|
+
|
|
82
|
+
var services = getRegisteredServices();
|
|
83
|
+
if (services.length > 0) {
|
|
84
|
+
process.stderr.write(`\n${kleur.bold(" Services")}\n`);
|
|
85
|
+
|
|
86
|
+
for (var service of services) {
|
|
87
|
+
var typeName = SERVICE_TYPES[service.type]?.name || service.type;
|
|
88
|
+
var endpoint = service.socketPath || service.apiUrl || "unknown";
|
|
89
|
+
|
|
90
|
+
allGood =
|
|
91
|
+
(await asyncCheck(`${service.name} (${typeName} - ${endpoint})`, async () => {
|
|
92
|
+
if (service.type === "slicervm") {
|
|
93
|
+
var cfg = normalizeServiceConfig(service);
|
|
94
|
+
await slicerVerify(cfg);
|
|
95
|
+
}
|
|
96
|
+
})) && allGood;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
76
100
|
// --- Summary ---
|
|
77
101
|
|
|
78
|
-
process.stderr.write(`\n${kleur.dim("
|
|
102
|
+
process.stderr.write(`\n${kleur.dim("-".repeat(50))}\n`);
|
|
79
103
|
if (allGood) {
|
|
80
104
|
process.stderr.write(kleur.green("All checks passed.\n\n"));
|
|
81
105
|
} else {
|
|
@@ -204,7 +228,7 @@ function check(label, fn) {
|
|
|
204
228
|
return true;
|
|
205
229
|
} catch (e) {
|
|
206
230
|
process.stderr.write(` ${FAIL} ${label}`);
|
|
207
|
-
if (e.message) process.stderr.write(kleur.dim(`
|
|
231
|
+
if (e.message) process.stderr.write(kleur.dim(` - ${e.message}`));
|
|
208
232
|
process.stderr.write("\n");
|
|
209
233
|
return false;
|
|
210
234
|
}
|
|
@@ -217,7 +241,7 @@ async function asyncCheck(label, fn) {
|
|
|
217
241
|
return true;
|
|
218
242
|
} catch (e) {
|
|
219
243
|
process.stderr.write(` ${FAIL} ${label}`);
|
|
220
|
-
if (e.message) process.stderr.write(kleur.dim(`
|
|
244
|
+
if (e.message) process.stderr.write(kleur.dim(` - ${truncate(e.message, 80)}`));
|
|
221
245
|
process.stderr.write("\n");
|
|
222
246
|
return false;
|
|
223
247
|
}
|
package/src/commands/domains.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createInterface } from "readline";
|
|
2
2
|
import { phase, status, success, fatal, hint, fmt } from "../lib/output.js";
|
|
3
3
|
import { resolveAppName, resolveDns, readLink, linkApp } from "../lib/link.js";
|
|
4
|
-
import { resolveCloudId, getCloudCfg, getProvider } from "../lib/providers/resolve.js";
|
|
4
|
+
import { resolveTarget, resolveCloudId, getCloudCfg, getProvider } from "../lib/providers/resolve.js";
|
|
5
5
|
import kleur from "kleur";
|
|
6
6
|
|
|
7
7
|
function prompt(rl, question) {
|
|
@@ -10,9 +10,9 @@ function prompt(rl, question) {
|
|
|
10
10
|
|
|
11
11
|
export async function domainsList(name, options) {
|
|
12
12
|
name = resolveAppName(name);
|
|
13
|
-
var
|
|
14
|
-
var cfg =
|
|
15
|
-
var dnsProvider = await
|
|
13
|
+
var target = await resolveTarget(options);
|
|
14
|
+
var cfg = target.cfg;
|
|
15
|
+
var dnsProvider = await target.provider("dns");
|
|
16
16
|
|
|
17
17
|
var result = await dnsProvider.listDomains(cfg, name);
|
|
18
18
|
|
|
@@ -58,16 +58,16 @@ export async function domainsAdd(args, options) {
|
|
|
58
58
|
domain = null;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
var
|
|
62
|
-
var appCfg =
|
|
63
|
-
var appProvider = await
|
|
61
|
+
var target = await resolveTarget(options);
|
|
62
|
+
var appCfg = target.cfg;
|
|
63
|
+
var appProvider = await target.provider("app");
|
|
64
64
|
|
|
65
65
|
// Cross-cloud DNS: --dns flag or .relight dns field specifies a different cloud for DNS records
|
|
66
66
|
var dnsFlag = options.dns || resolveDns();
|
|
67
|
-
var crossCloud = dnsFlag && resolveCloudId(dnsFlag) !==
|
|
68
|
-
var dnsCloud = crossCloud ? resolveCloudId(dnsFlag) :
|
|
67
|
+
var crossCloud = dnsFlag && (target.kind === "service" || resolveCloudId(dnsFlag) !== target.id);
|
|
68
|
+
var dnsCloud = crossCloud ? resolveCloudId(dnsFlag) : (target.kind === "cloud" ? target.id : null);
|
|
69
69
|
var dnsCfg = crossCloud ? getCloudCfg(dnsCloud) : appCfg;
|
|
70
|
-
var dnsProvider = await getProvider(dnsCloud, "dns");
|
|
70
|
+
var dnsProvider = crossCloud ? await getProvider(dnsCloud, "dns") : await target.provider("dns");
|
|
71
71
|
|
|
72
72
|
var appConfig = await appProvider.getAppConfig(appCfg, name);
|
|
73
73
|
if (!appConfig) {
|
|
@@ -147,15 +147,15 @@ export async function domainsAdd(args, options) {
|
|
|
147
147
|
// Cross-cloud: DNS record on one cloud, app config on another
|
|
148
148
|
status(`Creating DNS record for ${domain}...`);
|
|
149
149
|
var appUrl = await appProvider.getAppUrl(appCfg, name);
|
|
150
|
-
var
|
|
150
|
+
var dnsTarget = new URL(appUrl).hostname;
|
|
151
151
|
try {
|
|
152
|
-
await dnsProvider.addDnsRecord(dnsCfg, domain,
|
|
152
|
+
await dnsProvider.addDnsRecord(dnsCfg, domain, dnsTarget, zone);
|
|
153
153
|
} catch (e) {
|
|
154
154
|
fatal(e.message);
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
-
// Update app config on the app cloud
|
|
158
|
-
status(`Updating app config
|
|
157
|
+
// Update app config on the app cloud/service
|
|
158
|
+
status(`Updating app config...`);
|
|
159
159
|
if (!appConfig.domains) appConfig.domains = [];
|
|
160
160
|
if (!appConfig.domains.includes(domain)) {
|
|
161
161
|
appConfig.domains.push(domain);
|
|
@@ -165,7 +165,7 @@ export async function domainsAdd(args, options) {
|
|
|
165
165
|
// Persist dns cloud in .relight so future commands don't need --dns
|
|
166
166
|
var linked = readLink();
|
|
167
167
|
if (linked && !linked.dns) {
|
|
168
|
-
linkApp(linked.app, linked.cloud, dnsCloud);
|
|
168
|
+
linkApp(linked.app, linked.cloud, dnsCloud, undefined, linked.compute);
|
|
169
169
|
}
|
|
170
170
|
} else {
|
|
171
171
|
// Same-cloud: existing flow
|
|
@@ -193,22 +193,22 @@ export async function domainsRemove(args, options) {
|
|
|
193
193
|
fatal("Usage: relight domains remove [name] <domain>");
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
-
var
|
|
197
|
-
var appCfg =
|
|
196
|
+
var target = await resolveTarget(options);
|
|
197
|
+
var appCfg = target.cfg;
|
|
198
198
|
|
|
199
199
|
var dnsFlag = options.dns || resolveDns();
|
|
200
|
-
var crossCloud = dnsFlag && resolveCloudId(dnsFlag) !==
|
|
201
|
-
var dnsCloud = crossCloud ? resolveCloudId(dnsFlag) :
|
|
200
|
+
var crossCloud = dnsFlag && (target.kind === "service" || resolveCloudId(dnsFlag) !== target.id);
|
|
201
|
+
var dnsCloud = crossCloud ? resolveCloudId(dnsFlag) : (target.kind === "cloud" ? target.id : null);
|
|
202
202
|
var dnsCfg = crossCloud ? getCloudCfg(dnsCloud) : appCfg;
|
|
203
|
-
var dnsProvider = await getProvider(dnsCloud, "dns");
|
|
203
|
+
var dnsProvider = crossCloud ? await getProvider(dnsCloud, "dns") : await target.provider("dns");
|
|
204
204
|
|
|
205
205
|
status(`Removing ${domain}...`);
|
|
206
206
|
|
|
207
207
|
if (crossCloud) {
|
|
208
|
-
// Cross-cloud: remove DNS record from dns cloud, update app config on app cloud
|
|
208
|
+
// Cross-cloud: remove DNS record from dns cloud, update app config on app cloud/service
|
|
209
209
|
await dnsProvider.removeDnsRecord(dnsCfg, domain);
|
|
210
210
|
|
|
211
|
-
var appProvider = await
|
|
211
|
+
var appProvider = await target.provider("app");
|
|
212
212
|
var appConfig = await appProvider.getAppConfig(appCfg, name);
|
|
213
213
|
if (appConfig) {
|
|
214
214
|
appConfig.domains = (appConfig.domains || []).filter((d) => d !== domain);
|
package/src/commands/logs.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { fatal, fmt } from "../lib/output.js";
|
|
2
2
|
import { resolveAppName } from "../lib/link.js";
|
|
3
|
-
import {
|
|
3
|
+
import { resolveTarget } from "../lib/providers/resolve.js";
|
|
4
4
|
import kleur from "kleur";
|
|
5
5
|
|
|
6
6
|
export async function logs(name, options) {
|
|
7
7
|
name = resolveAppName(name);
|
|
8
|
-
var
|
|
9
|
-
var cfg =
|
|
10
|
-
var appProvider = await
|
|
8
|
+
var target = await resolveTarget(options);
|
|
9
|
+
var cfg = target.cfg;
|
|
10
|
+
var appProvider = await target.provider("app");
|
|
11
11
|
|
|
12
12
|
process.stderr.write(
|
|
13
13
|
`Tailing logs for ${fmt.app(name)}... ${fmt.dim("(ctrl+c to stop)")}\n\n`
|
package/src/commands/open.js
CHANGED
|
@@ -2,13 +2,13 @@ import { execSync } from "child_process";
|
|
|
2
2
|
import { platform } from "os";
|
|
3
3
|
import { fatal, fmt } from "../lib/output.js";
|
|
4
4
|
import { resolveAppName } from "../lib/link.js";
|
|
5
|
-
import {
|
|
5
|
+
import { resolveTarget } from "../lib/providers/resolve.js";
|
|
6
6
|
|
|
7
7
|
export async function open(name, options) {
|
|
8
8
|
name = resolveAppName(name);
|
|
9
|
-
var
|
|
10
|
-
var cfg =
|
|
11
|
-
var appProvider = await
|
|
9
|
+
var target = await resolveTarget(options);
|
|
10
|
+
var cfg = target.cfg;
|
|
11
|
+
var appProvider = await target.provider("app");
|
|
12
12
|
|
|
13
13
|
var url = await appProvider.getAppUrl(cfg, name);
|
|
14
14
|
|
package/src/commands/ps.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { fatal, fmt, table } from "../lib/output.js";
|
|
2
2
|
import { resolveAppName } from "../lib/link.js";
|
|
3
|
-
import {
|
|
3
|
+
import { resolveTarget } from "../lib/providers/resolve.js";
|
|
4
4
|
import kleur from "kleur";
|
|
5
5
|
|
|
6
6
|
export async function ps(name, options) {
|
|
7
7
|
name = resolveAppName(name);
|
|
8
|
-
var
|
|
9
|
-
var cfg =
|
|
10
|
-
var appProvider = await
|
|
8
|
+
var target = await resolveTarget(options);
|
|
9
|
+
var cfg = target.cfg;
|
|
10
|
+
var appProvider = await target.provider("app");
|
|
11
11
|
|
|
12
12
|
var appConfig = await appProvider.getAppConfig(cfg, name);
|
|
13
13
|
|
package/src/commands/scale.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { success, fatal, fmt } from "../lib/output.js";
|
|
2
2
|
import { resolveAppName } from "../lib/link.js";
|
|
3
|
-
import {
|
|
3
|
+
import { resolveTarget } from "../lib/providers/resolve.js";
|
|
4
4
|
|
|
5
5
|
export async function scale(name, options) {
|
|
6
6
|
name = resolveAppName(name);
|
|
7
|
-
var
|
|
8
|
-
var cfg =
|
|
9
|
-
var appProvider = await
|
|
7
|
+
var target = await resolveTarget(options);
|
|
8
|
+
var cfg = target.cfg;
|
|
9
|
+
var appProvider = await target.provider("app");
|
|
10
10
|
|
|
11
11
|
var appConfig = await appProvider.getAppConfig(cfg, name);
|
|
12
12
|
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { createInterface } from "readline";
|
|
2
|
+
import { success, fatal, fmt, table } from "../lib/output.js";
|
|
3
|
+
import {
|
|
4
|
+
SERVICE_TYPES,
|
|
5
|
+
getRegisteredServices,
|
|
6
|
+
saveServiceConfig,
|
|
7
|
+
removeServiceConfig,
|
|
8
|
+
normalizeServiceConfig,
|
|
9
|
+
tryGetServiceConfig,
|
|
10
|
+
} from "../lib/config.js";
|
|
11
|
+
import { verifyConnection } from "../lib/clouds/slicervm.js";
|
|
12
|
+
import { verifyApiKey } from "../lib/clouds/neon.js";
|
|
13
|
+
import kleur from "kleur";
|
|
14
|
+
|
|
15
|
+
function prompt(rl, question) {
|
|
16
|
+
return new Promise((resolve) => rl.question(question, resolve));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function serviceList() {
|
|
20
|
+
var services = getRegisteredServices();
|
|
21
|
+
|
|
22
|
+
if (services.length === 0) {
|
|
23
|
+
process.stderr.write("No services registered.\n");
|
|
24
|
+
process.stderr.write(
|
|
25
|
+
`\n${fmt.dim("Hint:")} ${fmt.cmd("relight service add")} to register one.\n`
|
|
26
|
+
);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
var headers = ["NAME", "LAYER", "TYPE", "ENDPOINT"];
|
|
31
|
+
var rows = services.map((a) => [
|
|
32
|
+
fmt.bold(a.name),
|
|
33
|
+
a.layer,
|
|
34
|
+
SERVICE_TYPES[a.type]?.name || a.type,
|
|
35
|
+
a.socketPath || a.apiUrl || (a.type === "neon" ? "console.neon.tech" : "-"),
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
console.log(table(headers, rows));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function serviceAdd(name) {
|
|
42
|
+
var rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
43
|
+
|
|
44
|
+
// 1. Pick layer
|
|
45
|
+
var layers = ["compute", "db"];
|
|
46
|
+
process.stderr.write(`\n${kleur.bold("Register a service")}\n\n`);
|
|
47
|
+
process.stderr.write(` ${kleur.bold("Layer:")}\n\n`);
|
|
48
|
+
for (var i = 0; i < layers.length; i++) {
|
|
49
|
+
process.stderr.write(` ${kleur.bold(`[${i + 1}]`)} ${layers[i]}\n`);
|
|
50
|
+
}
|
|
51
|
+
process.stderr.write("\n");
|
|
52
|
+
|
|
53
|
+
var layerChoice = await prompt(rl, `Select layer [1-${layers.length}]: `);
|
|
54
|
+
var layerIdx = parseInt(layerChoice, 10) - 1;
|
|
55
|
+
if (isNaN(layerIdx) || layerIdx < 0 || layerIdx >= layers.length) {
|
|
56
|
+
rl.close();
|
|
57
|
+
fatal("Invalid selection.");
|
|
58
|
+
}
|
|
59
|
+
var layer = layers[layerIdx];
|
|
60
|
+
|
|
61
|
+
// 2. Pick type (contextual to layer)
|
|
62
|
+
var types = Object.entries(SERVICE_TYPES)
|
|
63
|
+
.filter(([, v]) => v.layer === layer)
|
|
64
|
+
.map(([id, v]) => ({ id, name: v.name }));
|
|
65
|
+
|
|
66
|
+
process.stderr.write(`\n ${kleur.bold("Type:")}\n\n`);
|
|
67
|
+
for (var i = 0; i < types.length; i++) {
|
|
68
|
+
process.stderr.write(
|
|
69
|
+
` ${kleur.bold(`[${i + 1}]`)} ${types[i].name}\n`
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
process.stderr.write("\n");
|
|
73
|
+
|
|
74
|
+
var typeChoice = await prompt(rl, `Select type [1-${types.length}]: `);
|
|
75
|
+
var typeIdx = parseInt(typeChoice, 10) - 1;
|
|
76
|
+
if (isNaN(typeIdx) || typeIdx < 0 || typeIdx >= types.length) {
|
|
77
|
+
rl.close();
|
|
78
|
+
fatal("Invalid selection.");
|
|
79
|
+
}
|
|
80
|
+
var serviceType = types[typeIdx].id;
|
|
81
|
+
|
|
82
|
+
// 3. Connection details (SlicerVM-specific)
|
|
83
|
+
var config = { layer, type: serviceType };
|
|
84
|
+
|
|
85
|
+
if (serviceType === "slicervm") {
|
|
86
|
+
process.stderr.write(`\n ${kleur.bold("Connection mode")}\n\n`);
|
|
87
|
+
process.stderr.write(` ${kleur.bold("[1]")} Unix socket (local dev)\n`);
|
|
88
|
+
process.stderr.write(` ${kleur.bold("[2]")} HTTP API (remote)\n\n`);
|
|
89
|
+
|
|
90
|
+
var modeChoice = await prompt(rl, "Select [1-2]: ");
|
|
91
|
+
var useSocket = modeChoice.trim() === "1";
|
|
92
|
+
|
|
93
|
+
if (useSocket) {
|
|
94
|
+
var defaultSocket = "/var/run/slicer/slicer.sock";
|
|
95
|
+
var socketPath = await prompt(rl, `Socket path [${defaultSocket}]: `);
|
|
96
|
+
socketPath = (socketPath || "").trim() || defaultSocket;
|
|
97
|
+
config.socketPath = socketPath;
|
|
98
|
+
} else {
|
|
99
|
+
var apiUrl = await prompt(
|
|
100
|
+
rl,
|
|
101
|
+
"Slicer API URL (e.g. https://slicer.example.com:8080): "
|
|
102
|
+
);
|
|
103
|
+
apiUrl = (apiUrl || "").trim().replace(/\/+$/, "");
|
|
104
|
+
if (!apiUrl) {
|
|
105
|
+
rl.close();
|
|
106
|
+
fatal("No API URL provided.");
|
|
107
|
+
}
|
|
108
|
+
config.apiUrl = apiUrl;
|
|
109
|
+
|
|
110
|
+
var token = await prompt(rl, "API token: ");
|
|
111
|
+
token = (token || "").trim();
|
|
112
|
+
if (!token) {
|
|
113
|
+
rl.close();
|
|
114
|
+
fatal("No token provided.");
|
|
115
|
+
}
|
|
116
|
+
config.token = token;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
var hostGroup = await prompt(rl, "Host group [apps]: ");
|
|
120
|
+
config.hostGroup = (hostGroup || "").trim() || "apps";
|
|
121
|
+
|
|
122
|
+
var baseDomain = await prompt(
|
|
123
|
+
rl,
|
|
124
|
+
"Base domain (e.g. apps.example.com) [localhost]: "
|
|
125
|
+
);
|
|
126
|
+
config.baseDomain = (baseDomain || "").trim() || "localhost";
|
|
127
|
+
|
|
128
|
+
// 4. Verify connection
|
|
129
|
+
process.stderr.write("\nVerifying...\n");
|
|
130
|
+
var verifyCfg = normalizeServiceConfig(config);
|
|
131
|
+
try {
|
|
132
|
+
await verifyConnection(verifyCfg);
|
|
133
|
+
} catch (e) {
|
|
134
|
+
rl.close();
|
|
135
|
+
fatal("Connection failed.", e.message);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (useSocket) {
|
|
139
|
+
process.stderr.write(` Socket: ${fmt.bold(config.socketPath)}\n`);
|
|
140
|
+
} else {
|
|
141
|
+
process.stderr.write(` API: ${fmt.bold(config.apiUrl)}\n`);
|
|
142
|
+
}
|
|
143
|
+
process.stderr.write(` Host group: ${fmt.dim(config.hostGroup)}\n`);
|
|
144
|
+
process.stderr.write(` Base domain: ${fmt.dim(config.baseDomain)}\n`);
|
|
145
|
+
} else if (serviceType === "neon") {
|
|
146
|
+
process.stderr.write(`\n ${kleur.bold("Neon API key")}\n\n`);
|
|
147
|
+
process.stderr.write(
|
|
148
|
+
` ${fmt.dim("Get your API key at https://console.neon.tech/app/settings/api-keys")}\n\n`
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
var apiKey = await prompt(rl, "API key: ");
|
|
152
|
+
apiKey = (apiKey || "").trim();
|
|
153
|
+
if (!apiKey) {
|
|
154
|
+
rl.close();
|
|
155
|
+
fatal("No API key provided.");
|
|
156
|
+
}
|
|
157
|
+
config.apiKey = apiKey;
|
|
158
|
+
|
|
159
|
+
// Verify connection
|
|
160
|
+
process.stderr.write("\nVerifying...\n");
|
|
161
|
+
try {
|
|
162
|
+
var projects = await verifyApiKey(apiKey);
|
|
163
|
+
process.stderr.write(
|
|
164
|
+
` Authenticated. ${projects.length} existing project${projects.length === 1 ? "" : "s"}.\n`
|
|
165
|
+
);
|
|
166
|
+
} catch (e) {
|
|
167
|
+
rl.close();
|
|
168
|
+
fatal("Authentication failed.", e.message);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// 5. Auto-name if not provided
|
|
173
|
+
if (!name) {
|
|
174
|
+
var existing = getRegisteredServices().filter((a) => a.type === serviceType);
|
|
175
|
+
if (existing.length === 0) {
|
|
176
|
+
name = serviceType;
|
|
177
|
+
} else {
|
|
178
|
+
name = `${serviceType}-${existing.length + 1}`;
|
|
179
|
+
}
|
|
180
|
+
var inputName = await prompt(rl, `Service name [${name}]: `);
|
|
181
|
+
name = (inputName || "").trim() || name;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Check for existing
|
|
185
|
+
if (tryGetServiceConfig(name)) {
|
|
186
|
+
var overwrite = await prompt(
|
|
187
|
+
rl,
|
|
188
|
+
`Service '${name}' already exists. Overwrite? [y/N] `
|
|
189
|
+
);
|
|
190
|
+
if (!overwrite.match(/^y(es)?$/i)) {
|
|
191
|
+
rl.close();
|
|
192
|
+
process.stderr.write("Cancelled.\n");
|
|
193
|
+
process.exit(0);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
rl.close();
|
|
198
|
+
|
|
199
|
+
// 6. Save
|
|
200
|
+
saveServiceConfig(name, config);
|
|
201
|
+
|
|
202
|
+
success(`Service ${fmt.bold(name)} registered!`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export async function serviceRemove(name) {
|
|
206
|
+
if (!name) {
|
|
207
|
+
fatal("Usage: relight service remove <name>");
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (!tryGetServiceConfig(name)) {
|
|
211
|
+
fatal(`Service '${name}' not found.`);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
var rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
215
|
+
var answer = await new Promise((resolve) =>
|
|
216
|
+
rl.question(`Remove service '${name}'? [y/N] `, resolve)
|
|
217
|
+
);
|
|
218
|
+
rl.close();
|
|
219
|
+
|
|
220
|
+
if (!answer.match(/^y(es)?$/i)) {
|
|
221
|
+
process.stderr.write("Cancelled.\n");
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
removeServiceConfig(name);
|
|
226
|
+
success(`Service ${fmt.bold(name)} removed.`);
|
|
227
|
+
}
|