relight-cli 0.1.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 ADDED
@@ -0,0 +1,199 @@
1
+ # Relight
2
+
3
+ Deploy Docker containers to your cloud with scale-to-zero. Apps sleep when idle and wake on the next request.
4
+
5
+ ```
6
+ $ relight deploy myapp . --compute gcp
7
+ Building image...
8
+ Pushing to gcr.io/my-project/myapp:v1...
9
+ Deploying to Cloud Run (us-east1)...
10
+
11
+ --> Live at https://myapp-abc123.run.app
12
+ Sleeps after 30s idle. $0 when sleeping.
13
+ ```
14
+
15
+ ## What this is
16
+
17
+ Relight is a CLI that deploys and manages Docker containers across multiple cloud providers. It talks directly to each cloud's API using your own credentials. No vendor infrastructure gets installed in your account.
18
+
19
+ Supported compute backends:
20
+
21
+ | Cloud | Backend | Scale to zero |
22
+ |---|---|---|
23
+ | GCP | Cloud Run | Yes |
24
+ | AWS | App Runner | Yes |
25
+ | Cloudflare | Containers (Workers + Durable Objects) | Yes |
26
+
27
+ More backends planned: Azure Container Apps.
28
+
29
+ ## Install
30
+
31
+ ```sh
32
+ npm install -g relight
33
+ ```
34
+
35
+ Requires Node.js 20+ and Docker.
36
+
37
+ ## Quick start
38
+
39
+ ```sh
40
+ # Authenticate with a cloud provider
41
+ relight auth --compute gcp
42
+
43
+ # Deploy from a Dockerfile
44
+ relight deploy myapp .
45
+
46
+ # Check your apps
47
+ relight apps
48
+
49
+ # Stream logs
50
+ relight logs myapp
51
+
52
+ # Open in browser
53
+ relight open myapp
54
+ ```
55
+
56
+ The first deploy links the current directory to the app name. After that, `relight deploy` is enough.
57
+
58
+ ## Commands
59
+
60
+ ```
61
+ relight auth Authenticate with a cloud provider
62
+ relight deploy [name] [path] Deploy an app from a Dockerfile
63
+ relight apps List all deployed apps across all clouds
64
+ relight apps info [name] Show detailed app info
65
+ relight apps destroy [name] Destroy an app and its cloud resources
66
+ relight ps [name] Show running containers and resource usage
67
+ relight logs [name] Stream live logs
68
+ relight open [name] Open app URL in browser
69
+ relight config show [name] Show environment variables
70
+ relight config set KEY=VALUE Set env vars (applied live, no redeploy)
71
+ relight config unset KEY Remove an env var
72
+ relight config import -f .env Import from .env file
73
+ relight scale [name] Show or adjust instance count and resources
74
+ relight domains list [name] List custom domains
75
+ relight domains add [domain] Add a custom domain with DNS setup
76
+ relight domains remove [domain] Remove a custom domain
77
+ relight regions [--compute <cloud>] List available regions for a cloud
78
+ relight cost [name] Show estimated costs
79
+ relight doctor Check local setup and cloud connectivity
80
+ ```
81
+
82
+ Config changes (`config set`, `config unset`, `scale`, `domains`) are applied live without redeploying the container image.
83
+
84
+ ## How it works
85
+
86
+ 1. `relight auth` stores a scoped API token locally at `~/.relight/config.json`. One token per cloud provider. No cross-account IAM roles, no OAuth flows, no vendor access to your account.
87
+
88
+ 2. `relight deploy` builds a Docker image locally, pushes it to the cloud's container registry (GCR, Cloudflare Registry, ECR), and deploys it using the cloud's native container service.
89
+
90
+ 3. The deployed app sleeps after a configurable idle period (default 30s). The next incoming request wakes it. Cold starts are typically 1-5 seconds depending on the cloud and image size.
91
+
92
+ 4. All app state (config, scaling, domains) lives in the cloud provider's API. The only local files are your auth token and a `.relight` link file in your project directory. You can manage your apps from any machine.
93
+
94
+ ## Fleet view
95
+
96
+ When you authenticate with multiple clouds, `relight apps` shows everything in one table:
97
+
98
+ ```
99
+ $ relight apps
100
+
101
+ NAME CLOUD STATUS INSTANCES COST/MTD LAST ACTIVE
102
+ myapi gcp sleeping 0/3 $0.12 2h ago
103
+ frontend cf active 2/5 $1.84 now
104
+ dashboard gcp sleeping 0/1 $0.00 3d ago
105
+ ───────────────────────────────────────────────────────────────
106
+ TOTAL $1.96
107
+ ```
108
+
109
+ ## BYOC model
110
+
111
+ Relight deploys to cloud accounts you own. You pay the cloud provider directly at their published rates. Relight itself is free for individual use.
112
+
113
+ What this means in practice:
114
+
115
+ - **Your credentials stay local.** The API token is stored on your machine. Relight has no backend service that holds your cloud credentials.
116
+ - **Nothing is installed in your account.** No Kubernetes clusters, no CloudFormation stacks, no VPCs, no agent processes. Relight uses the cloud's existing container services.
117
+ - **You can stop using Relight anytime.** Your apps are standard Cloud Run services / App Runner services / CF Workers. They continue running without Relight. There's nothing to uninstall.
118
+
119
+ ## Scaling
120
+
121
+ ```sh
122
+ # Set instance count
123
+ relight scale myapp -i 4
124
+
125
+ # Set resources
126
+ relight scale myapp --vcpu 1 --memory 512
127
+
128
+ # Set idle timeout
129
+ relight deploy myapp . --sleep-after 60
130
+
131
+ # Multi-region (cloud support varies)
132
+ relight deploy myapp . --regions us-east1,europe-west1
133
+ ```
134
+
135
+ Relight does not autoscale. You set the maximum instance count and the cloud provider handles scaling between 0 and that limit based on traffic.
136
+
137
+ ## Custom domains
138
+
139
+ ```sh
140
+ relight domains add myapp.example.com
141
+
142
+ # Relight creates the DNS record if your domain's DNS is on a supported provider
143
+ # Otherwise it prints the record for you to create manually
144
+ ```
145
+
146
+ ## What Relight doesn't do
147
+
148
+ - **Not a general infrastructure tool.** Relight deploys Docker containers with scale-to-zero. It doesn't manage databases, queues, cron jobs, or other infrastructure. Use Terraform, Pulumi, or cloud consoles for those.
149
+ - **No autoscaling.** You set max instances. The cloud scales between 0 and your max based on traffic. There's no custom autoscaling logic.
150
+ - **No CI/CD.** Relight is a deployment tool, not a build pipeline. Integrate it into your CI by running `relight deploy` in your workflow.
151
+ - **Cold starts.** Sleeping apps take 1-5 seconds to respond to the first request. This is inherent to scale-to-zero and varies by cloud and image size.
152
+
153
+ ## Cloud-specific notes
154
+
155
+ ### GCP (Cloud Run)
156
+
157
+ - Requires a GCP project with Cloud Run and Artifact Registry APIs enabled.
158
+ - Token: service account key or `gcloud auth print-access-token`.
159
+ - Regions: any Cloud Run region. Run `relight regions --compute gcp` to list.
160
+ - Scale-to-zero is native. Minimum instances can be set to 0.
161
+
162
+ ### AWS (App Runner)
163
+
164
+ - Requires an AWS account with App Runner and ECR access.
165
+ - Token: IAM access key with `apprunner:*` and `ecr:*` permissions, or use `aws configure`.
166
+ - Regions: any App Runner region. Run `relight regions --compute aws` to list.
167
+ - Scale-to-zero is native. Services pause when idle and resume on the next request.
168
+
169
+ ### Cloudflare (Containers)
170
+
171
+ - Requires a Cloudflare account with Containers access (paid Workers plan).
172
+ - Token: Cloudflare API token with `workers_scripts:edit` and `containers:edit` permissions.
173
+ - Each app is a Worker backed by Durable Objects running your container. The CLI bundles and uploads the Worker template automatically.
174
+ - Regions use Durable Object `locationHints` — placement is best-effort, not guaranteed.
175
+
176
+ ## Configuration
177
+
178
+ Auth config is stored at `~/.relight/config.json`:
179
+
180
+ ```json
181
+ {
182
+ "clouds": {
183
+ "gcp": { "token": "...", "project": "my-project" },
184
+ "aws": { "accessKeyId": "...", "secretAccessKey": "...", "region": "us-east-1" },
185
+ "cf": { "token": "...", "accountId": "..." }
186
+ },
187
+ "default_compute": "gcp"
188
+ }
189
+ ```
190
+
191
+ Per-project app linking is stored in a `.relight` file in your project directory:
192
+
193
+ ```json
194
+ { "app": "myapp", "compute": "gcp" }
195
+ ```
196
+
197
+ ## License
198
+
199
+ MIT
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "relight-cli",
3
+ "version": "0.1.0",
4
+ "description": "Deploy and manage Docker containers across clouds with scale-to-zero",
5
+ "bin": {
6
+ "relight": "./src/cli.js"
7
+ },
8
+ "type": "module",
9
+ "license": "MIT",
10
+ "files": [
11
+ "src"
12
+ ],
13
+ "keywords": [
14
+ "docker",
15
+ "deploy",
16
+ "paas",
17
+ "cloudflare",
18
+ "gcp",
19
+ "aws",
20
+ "scale-to-zero",
21
+ "multi-cloud"
22
+ ],
23
+ "dependencies": {
24
+ "commander": "^13.0.0",
25
+ "kleur": "^4.1.5"
26
+ }
27
+ }
package/src/cli.js ADDED
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from "commander";
4
+ import { auth } from "./commands/auth.js";
5
+ import { doctor } from "./commands/doctor.js";
6
+
7
+ var program = new Command();
8
+
9
+ program
10
+ .name("relight")
11
+ .description("Deploy and manage Docker containers across clouds with scale-to-zero")
12
+ .version("0.1.0");
13
+
14
+ // --- Auth ---
15
+
16
+ program
17
+ .command("auth")
18
+ .description("Authenticate with a cloud provider")
19
+ .option(
20
+ "-c, --cloud <cloud>",
21
+ "Cloud provider (cf, gcp, aws)"
22
+ )
23
+ .action(auth);
24
+
25
+ // --- Doctor ---
26
+
27
+ program
28
+ .command("doctor")
29
+ .description("Check system setup and cloud connectivity")
30
+ .action(doctor);
31
+
32
+ program.parse();
@@ -0,0 +1,302 @@
1
+ import { createInterface } from "readline";
2
+ import { execSync } from "child_process";
3
+ import { existsSync } from "fs";
4
+ import { resolve } from "path";
5
+ import { success, fatal, hint, fmt } from "../lib/output.js";
6
+ import {
7
+ tryGetConfig,
8
+ saveConfig,
9
+ CLOUD_NAMES,
10
+ CLOUD_IDS,
11
+ } from "../lib/config.js";
12
+ import { TOKEN_URL, verifyToken, listAccounts } from "../lib/clouds/cf.js";
13
+ import {
14
+ readKeyFile,
15
+ mintAccessToken,
16
+ verifyProject,
17
+ } from "../lib/clouds/gcp.js";
18
+ import { verifyCredentials as awsVerify } from "../lib/clouds/aws.js";
19
+ import kleur from "kleur";
20
+
21
+ function prompt(rl, question) {
22
+ return new Promise((resolve) => rl.question(question, resolve));
23
+ }
24
+
25
+ export async function auth(options) {
26
+ var compute = options.cloud;
27
+
28
+ var rl = createInterface({ input: process.stdin, output: process.stderr });
29
+
30
+ if (!compute) {
31
+ process.stderr.write(`\n${kleur.bold("Authenticate with a cloud provider")}\n\n`);
32
+ for (var i = 0; i < CLOUD_IDS.length; i++) {
33
+ process.stderr.write(
34
+ ` ${kleur.bold(`[${i + 1}]`)} ${CLOUD_NAMES[CLOUD_IDS[i]]}\n`
35
+ );
36
+ }
37
+ process.stderr.write("\n");
38
+
39
+ var choice = await prompt(rl, `Select cloud [1-${CLOUD_IDS.length}]: `);
40
+ var idx = parseInt(choice, 10) - 1;
41
+
42
+ if (isNaN(idx) || idx < 0 || idx >= CLOUD_IDS.length) {
43
+ rl.close();
44
+ fatal("Invalid selection.");
45
+ }
46
+
47
+ compute = CLOUD_IDS[idx];
48
+ }
49
+
50
+ compute = normalizeCompute(compute);
51
+ if (!CLOUD_NAMES[compute]) {
52
+ rl.close();
53
+ fatal(
54
+ `Unknown cloud: ${compute}`,
55
+ `Supported: ${CLOUD_IDS.join(", ")}`
56
+ );
57
+ }
58
+
59
+ process.stderr.write(
60
+ `\n${kleur.bold(`Authenticate with ${CLOUD_NAMES[compute]}`)}\n`
61
+ );
62
+
63
+ var cloudConfig;
64
+
65
+ switch (compute) {
66
+ case "cf":
67
+ cloudConfig = await authCloudflare(rl);
68
+ break;
69
+ case "gcp":
70
+ cloudConfig = await authGCP(rl);
71
+ break;
72
+ case "aws":
73
+ cloudConfig = await authAWS(rl);
74
+ break;
75
+ }
76
+
77
+ rl.close();
78
+
79
+ // Merge into existing config
80
+ var config = tryGetConfig() || { clouds: {} };
81
+ if (!config.clouds) config.clouds = {};
82
+ config.clouds[compute] = cloudConfig;
83
+
84
+ if (!config.default_cloud) {
85
+ config.default_cloud = compute;
86
+ }
87
+
88
+ saveConfig(config);
89
+
90
+ success(`Authenticated with ${CLOUD_NAMES[compute]}!`);
91
+
92
+ if (config.default_cloud === compute) {
93
+ hint("Default", `${CLOUD_NAMES[compute]} is your default cloud`);
94
+ }
95
+ hint("Next", `relight deploy <name> .`);
96
+ }
97
+
98
+ function normalizeCompute(input) {
99
+ var aliases = {
100
+ cloudflare: "cf",
101
+ cf: "cf",
102
+ gcp: "gcp",
103
+ "google-cloud": "gcp",
104
+ "cloud-run": "gcp",
105
+ aws: "aws",
106
+ amazon: "aws",
107
+ "app-runner": "aws",
108
+ };
109
+ return aliases[input.toLowerCase()] || input.toLowerCase();
110
+ }
111
+
112
+ // --- Cloudflare ---
113
+
114
+ async function authCloudflare(rl) {
115
+ process.stderr.write(`\n ${kleur.bold("Setup")}\n\n`);
116
+ process.stderr.write(` 1. Log in to the Cloudflare dashboard\n`);
117
+ process.stderr.write(` 2. Open this link to create a pre-filled API token:\n\n`);
118
+ process.stderr.write(` ${fmt.url(TOKEN_URL)}\n\n`);
119
+ process.stderr.write(` 3. Click ${kleur.bold("Continue to summary")} then ${kleur.bold("Create Token")}\n`);
120
+ process.stderr.write(` 4. Copy the token\n\n`);
121
+
122
+ var apiToken = await prompt(rl, "Paste your API token: ");
123
+ apiToken = (apiToken || "").trim();
124
+ if (!apiToken) fatal("No token provided.");
125
+
126
+ process.stderr.write("\nVerifying...\n");
127
+ try {
128
+ await verifyToken(apiToken);
129
+ } catch (e) {
130
+ fatal("Token verification failed.", e.message);
131
+ }
132
+
133
+ var accounts = await listAccounts(apiToken);
134
+ if (accounts.length === 0) {
135
+ fatal("No accounts found for this token.");
136
+ }
137
+
138
+ var account;
139
+ if (accounts.length === 1) {
140
+ account = accounts[0];
141
+ } else {
142
+ process.stderr.write("\nMultiple accounts found:\n\n");
143
+ for (var i = 0; i < accounts.length; i++) {
144
+ process.stderr.write(
145
+ ` ${kleur.bold(`[${i + 1}]`)} ${accounts[i].name} ${fmt.dim(`(${accounts[i].id})`)}\n`
146
+ );
147
+ }
148
+ process.stderr.write("\n");
149
+
150
+ var choice = await prompt(rl, `Select account [1-${accounts.length}]: `);
151
+ var idx = parseInt(choice, 10) - 1;
152
+ if (isNaN(idx) || idx < 0 || idx >= accounts.length) {
153
+ fatal("Invalid selection.");
154
+ }
155
+ account = accounts[idx];
156
+ }
157
+
158
+ process.stderr.write(
159
+ ` Account: ${fmt.bold(account.name)} ${fmt.dim(`(${account.id})`)}\n`
160
+ );
161
+
162
+ return { token: apiToken, accountId: account.id };
163
+ }
164
+
165
+ // --- GCP ---
166
+
167
+ async function authGCP(rl) {
168
+ var SA_URL =
169
+ "https://console.cloud.google.com/iam-admin/serviceaccounts/create";
170
+ var ENABLE_APIS =
171
+ "https://console.cloud.google.com/apis/enableflow?apiid=run.googleapis.com,artifactregistry.googleapis.com";
172
+
173
+ process.stderr.write(`\n ${kleur.bold("Setup")}\n\n`);
174
+ process.stderr.write(` 1. Enable the Cloud Run and Artifact Registry APIs:\n`);
175
+ process.stderr.write(` ${fmt.url(ENABLE_APIS)}\n\n`);
176
+ process.stderr.write(` 2. Create a service account:\n`);
177
+ process.stderr.write(` ${fmt.url(SA_URL)}\n\n`);
178
+ process.stderr.write(` Name it ${fmt.val("relight")} and grant these roles:\n`);
179
+ process.stderr.write(` ${fmt.val("Cloud Run Admin")}\n`);
180
+ process.stderr.write(` ${fmt.val("Artifact Registry Admin")}\n`);
181
+ process.stderr.write(` ${fmt.val("Service Account User")}\n\n`);
182
+ process.stderr.write(` 3. Go to the service account → ${kleur.bold("Keys")} tab\n`);
183
+ process.stderr.write(` 4. ${kleur.bold("Add Key")} → ${kleur.bold("Create new key")} → ${kleur.bold("JSON")}\n`);
184
+ process.stderr.write(` 5. Save the downloaded file\n\n`);
185
+
186
+ var keyPath = await prompt(rl, "Path to service account key JSON: ");
187
+ keyPath = (keyPath || "").trim();
188
+ if (!keyPath) fatal("No key file provided.");
189
+
190
+ keyPath = resolve(keyPath.replace(/^~\//, process.env.HOME + "/"));
191
+ if (!existsSync(keyPath)) {
192
+ fatal(`File not found: ${keyPath}`);
193
+ }
194
+
195
+ var key;
196
+ try {
197
+ key = readKeyFile(keyPath);
198
+ } catch (e) {
199
+ fatal("Failed to read key file.", e.message);
200
+ }
201
+
202
+ var project = key.project;
203
+ if (!project) {
204
+ project = await prompt(rl, "GCP project ID: ");
205
+ project = (project || "").trim();
206
+ if (!project) fatal("No project provided.");
207
+ }
208
+
209
+ process.stderr.write("\nVerifying...\n");
210
+
211
+ var token;
212
+ try {
213
+ token = await mintAccessToken(key.clientEmail, key.privateKey);
214
+ } catch (e) {
215
+ fatal("Failed to mint access token.", e.message);
216
+ }
217
+
218
+ try {
219
+ await verifyProject(token, project);
220
+ } catch (e) {
221
+ fatal("Project verification failed.", e.message);
222
+ }
223
+
224
+ process.stderr.write(` Project: ${fmt.bold(project)}\n`);
225
+ process.stderr.write(` Service account: ${fmt.dim(key.clientEmail)}\n`);
226
+
227
+ return { clientEmail: key.clientEmail, privateKey: key.privateKey, project };
228
+ }
229
+
230
+ // --- AWS ---
231
+
232
+ async function authAWS(rl) {
233
+ var IAM_CONSOLE = "https://console.aws.amazon.com/iam/home#/users";
234
+
235
+ process.stderr.write(`\n ${kleur.bold("Setup")}\n\n`);
236
+ process.stderr.write(` 1. Open the IAM console:\n`);
237
+ process.stderr.write(` ${fmt.url(IAM_CONSOLE)}\n\n`);
238
+ process.stderr.write(` 2. Create a user and attach these policies:\n`);
239
+ process.stderr.write(` ${fmt.val("AWSAppRunnerFullAccess")}\n`);
240
+ process.stderr.write(` ${fmt.val("AmazonEC2ContainerRegistryFullAccess")}\n\n`);
241
+ process.stderr.write(` 3. Go to the user's ${kleur.bold("Security credentials")} tab\n`);
242
+ process.stderr.write(` 4. Click ${kleur.bold("Create access key")} → choose ${kleur.bold("Command Line Interface")}\n`);
243
+ process.stderr.write(` 5. Copy the Access Key ID and Secret Access Key\n\n`);
244
+
245
+ // Detect from env vars as fallback
246
+ var detectedKeyId = process.env.AWS_ACCESS_KEY_ID || null;
247
+ var detectedSecret = process.env.AWS_SECRET_ACCESS_KEY || null;
248
+ var detectedRegion =
249
+ process.env.AWS_DEFAULT_REGION || process.env.AWS_REGION || null;
250
+
251
+ var accessKeyId;
252
+ if (detectedKeyId) {
253
+ var input = await prompt(
254
+ rl,
255
+ `AWS Access Key ID [${detectedKeyId.slice(0, 8)}...]: `
256
+ );
257
+ accessKeyId = (input || "").trim() || detectedKeyId;
258
+ } else {
259
+ accessKeyId = await prompt(rl, "AWS Access Key ID: ");
260
+ accessKeyId = (accessKeyId || "").trim();
261
+ if (!accessKeyId) fatal("No access key provided.");
262
+ }
263
+
264
+ var secretAccessKey;
265
+ if (detectedSecret && accessKeyId === detectedKeyId) {
266
+ var input = await prompt(rl, "AWS Secret Access Key [detected]: ");
267
+ secretAccessKey = (input || "").trim() || detectedSecret;
268
+ } else {
269
+ secretAccessKey = await prompt(rl, "AWS Secret Access Key: ");
270
+ secretAccessKey = (secretAccessKey || "").trim();
271
+ if (!secretAccessKey) fatal("No secret key provided.");
272
+ }
273
+
274
+ var defaultRegion = detectedRegion || "us-east-1";
275
+ var region = await prompt(rl, `AWS Region [${defaultRegion}]: `);
276
+ region = (region || "").trim() || defaultRegion;
277
+
278
+ process.stderr.write("\nVerifying...\n");
279
+ try {
280
+ await awsVerify({ accessKeyId, secretAccessKey }, region);
281
+ } catch (e) {
282
+ fatal("Credential verification failed.", e.message);
283
+ }
284
+
285
+ process.stderr.write(` Region: ${fmt.bold(region)}\n`);
286
+
287
+ return { accessKeyId, secretAccessKey, region };
288
+ }
289
+
290
+ // --- Helpers ---
291
+
292
+ function tryExec(cmd) {
293
+ try {
294
+ return execSync(cmd + " 2>/dev/null", {
295
+ stdio: ["pipe", "pipe", "pipe"],
296
+ encoding: "utf-8",
297
+ timeout: 5000,
298
+ }).trim() || null;
299
+ } catch {
300
+ return null;
301
+ }
302
+ }
@@ -0,0 +1,197 @@
1
+ import { execSync } from "child_process";
2
+ import { existsSync } from "fs";
3
+ import {
4
+ tryGetConfig,
5
+ CONFIG_PATH,
6
+ CLOUD_NAMES,
7
+ } from "../lib/config.js";
8
+ import { verifyToken as cfVerify, getWorkersSubdomain } from "../lib/clouds/cf.js";
9
+ import { mintAccessToken, verifyProject as gcpVerifyProject, listRegions as gcpListRegions } from "../lib/clouds/gcp.js";
10
+ import { verifyCredentials as awsVerify, checkAppRunner } from "../lib/clouds/aws.js";
11
+ import kleur from "kleur";
12
+
13
+ var PASS = kleur.green("[ok]");
14
+ var FAIL = kleur.red("[!!]");
15
+ var SKIP = kleur.yellow("[--]");
16
+
17
+ export async function doctor() {
18
+ process.stderr.write(`\n${kleur.bold("relight doctor")}\n`);
19
+ process.stderr.write(`${kleur.dim("─".repeat(50))}\n\n`);
20
+ var allGood = true;
21
+
22
+ // --- General checks ---
23
+
24
+ process.stderr.write(kleur.bold(" System\n"));
25
+
26
+ allGood =
27
+ check("Docker installed", () => {
28
+ execSync("docker --version", { stdio: "pipe" });
29
+ }) && allGood;
30
+
31
+ allGood =
32
+ check("Docker daemon running", () => {
33
+ execSync("docker info", { stdio: "pipe", timeout: 5000 });
34
+ }) && allGood;
35
+
36
+ check("Node.js >= 20", () => {
37
+ var major = parseInt(process.version.slice(1), 10);
38
+ if (major < 20) throw new Error(`Node ${process.version} (need >= 20)`);
39
+ });
40
+
41
+ allGood =
42
+ check("Auth config exists", () => {
43
+ if (!existsSync(CONFIG_PATH)) throw new Error("Not found");
44
+ }) && allGood;
45
+
46
+ var config = tryGetConfig();
47
+ var clouds = config && config.clouds ? config.clouds : {};
48
+ var authenticatedClouds = Object.keys(clouds).filter(
49
+ (id) => clouds[id] && Object.keys(clouds[id]).length > 0
50
+ );
51
+
52
+ if (authenticatedClouds.length === 0) {
53
+ process.stderr.write(
54
+ `\n ${SKIP} No clouds configured. Run ${kleur.bold().cyan("relight auth")} to get started.\n`
55
+ );
56
+ }
57
+
58
+ // --- Per-cloud checks ---
59
+
60
+ for (var cloudId of authenticatedClouds) {
61
+ process.stderr.write(`\n${kleur.bold(` ${CLOUD_NAMES[cloudId] || cloudId}`)}\n`);
62
+
63
+ switch (cloudId) {
64
+ case "cf":
65
+ allGood = (await checkCloudflare(clouds.cf)) && allGood;
66
+ break;
67
+ case "gcp":
68
+ allGood = (await checkGCP(clouds.gcp)) && allGood;
69
+ break;
70
+ case "aws":
71
+ allGood = (await checkAWS(clouds.aws)) && allGood;
72
+ break;
73
+ }
74
+ }
75
+
76
+ // --- Summary ---
77
+
78
+ process.stderr.write(`\n${kleur.dim("─".repeat(50))}\n`);
79
+ if (allGood) {
80
+ process.stderr.write(kleur.green("All checks passed.\n\n"));
81
+ } else {
82
+ process.stderr.write(
83
+ kleur.yellow("Some checks failed. Fix the issues above and re-run.\n\n")
84
+ );
85
+ }
86
+ }
87
+
88
+ // --- Cloudflare checks ---
89
+
90
+ async function checkCloudflare(cfg) {
91
+ var ok = true;
92
+
93
+ ok =
94
+ (await asyncCheck("API token valid", async () => {
95
+ await cfVerify(cfg.token);
96
+ })) && ok;
97
+
98
+ ok =
99
+ (await asyncCheck("Account accessible", async () => {
100
+ var { listAccounts } = await import("../lib/clouds/cf.js");
101
+ var accounts = await listAccounts(cfg.token);
102
+ if (!accounts.length) throw new Error("No accounts");
103
+ var match = accounts.find((a) => a.id === cfg.accountId);
104
+ if (!match) throw new Error(`Account ${cfg.accountId} not found`);
105
+ })) && ok;
106
+
107
+ ok =
108
+ (await asyncCheck("Workers subdomain configured", async () => {
109
+ var sub = await getWorkersSubdomain(cfg.accountId, cfg.token);
110
+ if (!sub) throw new Error("Not configured");
111
+ })) && ok;
112
+
113
+ return ok;
114
+ }
115
+
116
+ // --- GCP checks ---
117
+
118
+ async function checkGCP(cfg) {
119
+ var ok = true;
120
+
121
+ var token;
122
+ ok =
123
+ (await asyncCheck("Service account key valid", async () => {
124
+ token = await mintAccessToken(cfg.clientEmail, cfg.privateKey);
125
+ })) && ok;
126
+
127
+ if (token) {
128
+ ok =
129
+ (await asyncCheck("Project accessible", async () => {
130
+ await gcpVerifyProject(token, cfg.project);
131
+ })) && ok;
132
+
133
+ ok =
134
+ (await asyncCheck("Cloud Run API reachable", async () => {
135
+ await gcpListRegions(token, cfg.project);
136
+ })) && ok;
137
+ }
138
+
139
+ return ok;
140
+ }
141
+
142
+ // --- AWS checks ---
143
+
144
+ async function checkAWS(cfg) {
145
+ var ok = true;
146
+
147
+ ok =
148
+ (await asyncCheck("Credentials valid (STS)", async () => {
149
+ await awsVerify(
150
+ { accessKeyId: cfg.accessKeyId, secretAccessKey: cfg.secretAccessKey },
151
+ cfg.region
152
+ );
153
+ })) && ok;
154
+
155
+ ok =
156
+ (await asyncCheck("App Runner accessible", async () => {
157
+ await checkAppRunner(
158
+ { accessKeyId: cfg.accessKeyId, secretAccessKey: cfg.secretAccessKey },
159
+ cfg.region
160
+ );
161
+ })) && ok;
162
+
163
+ return ok;
164
+ }
165
+
166
+ // --- Check helpers ---
167
+
168
+ function check(label, fn) {
169
+ try {
170
+ fn();
171
+ process.stderr.write(` ${PASS} ${label}\n`);
172
+ return true;
173
+ } catch (e) {
174
+ process.stderr.write(` ${FAIL} ${label}`);
175
+ if (e.message) process.stderr.write(kleur.dim(` — ${e.message}`));
176
+ process.stderr.write("\n");
177
+ return false;
178
+ }
179
+ }
180
+
181
+ async function asyncCheck(label, fn) {
182
+ try {
183
+ await fn();
184
+ process.stderr.write(` ${PASS} ${label}\n`);
185
+ return true;
186
+ } catch (e) {
187
+ process.stderr.write(` ${FAIL} ${label}`);
188
+ if (e.message) process.stderr.write(kleur.dim(` — ${truncate(e.message, 80)}`));
189
+ process.stderr.write("\n");
190
+ return false;
191
+ }
192
+ }
193
+
194
+ function truncate(str, max) {
195
+ if (str.length <= max) return str;
196
+ return str.slice(0, max - 3) + "...";
197
+ }
@@ -0,0 +1,223 @@
1
+ import { createHmac, createHash } from "crypto";
2
+
3
+ // AWS Signature V4 signing
4
+
5
+ function sha256(data) {
6
+ return createHash("sha256").update(data).digest("hex");
7
+ }
8
+
9
+ function hmac(key, data) {
10
+ return createHmac("sha256", key).update(data).digest();
11
+ }
12
+
13
+ function getSignatureKey(secretKey, dateStamp, region, service) {
14
+ var kDate = hmac("AWS4" + secretKey, dateStamp);
15
+ var kRegion = hmac(kDate, region);
16
+ var kService = hmac(kRegion, service);
17
+ return hmac(kService, "aws4_request");
18
+ }
19
+
20
+ export async function awsApi(method, service, host, path, body, credentials, region) {
21
+ var now = new Date();
22
+ var amzDate = now.toISOString().replace(/[-:]/g, "").replace(/\.\d+/, "");
23
+ var dateStamp = amzDate.slice(0, 8);
24
+
25
+ var bodyStr = body ? JSON.stringify(body) : "";
26
+ var payloadHash = sha256(bodyStr);
27
+
28
+ var canonicalHeaders =
29
+ `content-type:application/x-amz-json-1.0\n` +
30
+ `host:${host}\n` +
31
+ `x-amz-date:${amzDate}\n`;
32
+ var signedHeaders = "content-type;host;x-amz-date";
33
+
34
+ var canonicalRequest = [
35
+ method,
36
+ path,
37
+ "", // query string
38
+ canonicalHeaders,
39
+ signedHeaders,
40
+ payloadHash,
41
+ ].join("\n");
42
+
43
+ var credentialScope = `${dateStamp}/${region}/${service}/aws4_request`;
44
+ var stringToSign = [
45
+ "AWS4-HMAC-SHA256",
46
+ amzDate,
47
+ credentialScope,
48
+ sha256(canonicalRequest),
49
+ ].join("\n");
50
+
51
+ var signingKey = getSignatureKey(
52
+ credentials.secretAccessKey,
53
+ dateStamp,
54
+ region,
55
+ service
56
+ );
57
+ var signature = createHmac("sha256", signingKey)
58
+ .update(stringToSign)
59
+ .digest("hex");
60
+
61
+ var authHeader =
62
+ `AWS4-HMAC-SHA256 ` +
63
+ `Credential=${credentials.accessKeyId}/${credentialScope}, ` +
64
+ `SignedHeaders=${signedHeaders}, ` +
65
+ `Signature=${signature}`;
66
+
67
+ var res = await fetch(`https://${host}${path}`, {
68
+ method,
69
+ headers: {
70
+ "Content-Type": "application/x-amz-json-1.0",
71
+ "X-Amz-Date": amzDate,
72
+ Authorization: authHeader,
73
+ Host: host,
74
+ },
75
+ body: method === "GET" ? undefined : bodyStr || undefined,
76
+ });
77
+
78
+ if (!res.ok) {
79
+ var text = await res.text();
80
+ throw new Error(`AWS ${service} ${method} ${path}: ${res.status} ${text}`);
81
+ }
82
+
83
+ var ct = res.headers.get("content-type") || "";
84
+ if (ct.includes("json")) {
85
+ return res.json();
86
+ }
87
+ return res.text();
88
+ }
89
+
90
+ export async function verifyCredentials(credentials, region) {
91
+ // Use STS GetCallerIdentity to verify credentials
92
+ var host = "sts.amazonaws.com";
93
+ var now = new Date();
94
+ var amzDate = now.toISOString().replace(/[-:]/g, "").replace(/\.\d+/, "");
95
+ var dateStamp = amzDate.slice(0, 8);
96
+
97
+ var queryParams = new URLSearchParams({
98
+ Action: "GetCallerIdentity",
99
+ Version: "2011-06-15",
100
+ });
101
+ var queryString = queryParams.toString();
102
+
103
+ var payloadHash = sha256("");
104
+ var canonicalHeaders = `host:${host}\nx-amz-date:${amzDate}\n`;
105
+ var signedHeaders = "host;x-amz-date";
106
+
107
+ var canonicalRequest = [
108
+ "GET",
109
+ "/",
110
+ queryString,
111
+ canonicalHeaders,
112
+ signedHeaders,
113
+ payloadHash,
114
+ ].join("\n");
115
+
116
+ var credentialScope = `${dateStamp}/us-east-1/sts/aws4_request`;
117
+ var stringToSign = [
118
+ "AWS4-HMAC-SHA256",
119
+ amzDate,
120
+ credentialScope,
121
+ sha256(canonicalRequest),
122
+ ].join("\n");
123
+
124
+ var signingKey = getSignatureKey(
125
+ credentials.secretAccessKey,
126
+ dateStamp,
127
+ "us-east-1",
128
+ "sts"
129
+ );
130
+ var signature = createHmac("sha256", signingKey)
131
+ .update(stringToSign)
132
+ .digest("hex");
133
+
134
+ var authHeader =
135
+ `AWS4-HMAC-SHA256 ` +
136
+ `Credential=${credentials.accessKeyId}/${credentialScope}, ` +
137
+ `SignedHeaders=${signedHeaders}, ` +
138
+ `Signature=${signature}`;
139
+
140
+ var res = await fetch(`https://${host}/?${queryString}`, {
141
+ method: "GET",
142
+ headers: {
143
+ "X-Amz-Date": amzDate,
144
+ Authorization: authHeader,
145
+ },
146
+ });
147
+
148
+ if (!res.ok) {
149
+ var text = await res.text();
150
+ throw new Error(`STS GetCallerIdentity failed: ${res.status} ${text}`);
151
+ }
152
+
153
+ return res.text();
154
+ }
155
+
156
+ export async function checkAppRunner(credentials, region) {
157
+ var host = `apprunner.${region}.amazonaws.com`;
158
+ var now = new Date();
159
+ var amzDate = now.toISOString().replace(/[-:]/g, "").replace(/\.\d+/, "");
160
+ var dateStamp = amzDate.slice(0, 8);
161
+
162
+ var bodyStr = JSON.stringify({});
163
+ var payloadHash = sha256(bodyStr);
164
+
165
+ var target = "AppRunner.ListServices";
166
+ var canonicalHeaders =
167
+ `content-type:application/x-amz-json-1.0\n` +
168
+ `host:${host}\n` +
169
+ `x-amz-date:${amzDate}\n` +
170
+ `x-amz-target:${target}\n`;
171
+ var signedHeaders = "content-type;host;x-amz-date;x-amz-target";
172
+
173
+ var canonicalRequest = [
174
+ "POST",
175
+ "/",
176
+ "",
177
+ canonicalHeaders,
178
+ signedHeaders,
179
+ payloadHash,
180
+ ].join("\n");
181
+
182
+ var credentialScope = `${dateStamp}/${region}/apprunner/aws4_request`;
183
+ var stringToSign = [
184
+ "AWS4-HMAC-SHA256",
185
+ amzDate,
186
+ credentialScope,
187
+ sha256(canonicalRequest),
188
+ ].join("\n");
189
+
190
+ var signingKey = getSignatureKey(
191
+ credentials.secretAccessKey,
192
+ dateStamp,
193
+ region,
194
+ "apprunner"
195
+ );
196
+ var signature = createHmac("sha256", signingKey)
197
+ .update(stringToSign)
198
+ .digest("hex");
199
+
200
+ var authHeader =
201
+ `AWS4-HMAC-SHA256 ` +
202
+ `Credential=${credentials.accessKeyId}/${credentialScope}, ` +
203
+ `SignedHeaders=${signedHeaders}, ` +
204
+ `Signature=${signature}`;
205
+
206
+ var res = await fetch(`https://${host}/`, {
207
+ method: "POST",
208
+ headers: {
209
+ "Content-Type": "application/x-amz-json-1.0",
210
+ "X-Amz-Date": amzDate,
211
+ "X-Amz-Target": target,
212
+ Authorization: authHeader,
213
+ },
214
+ body: bodyStr,
215
+ });
216
+
217
+ if (!res.ok) {
218
+ var text = await res.text();
219
+ throw new Error(`App Runner ListServices failed: ${res.status} ${text}`);
220
+ }
221
+
222
+ return res.json();
223
+ }
@@ -0,0 +1,65 @@
1
+ var CF_API = "https://api.cloudflare.com/client/v4";
2
+
3
+ export var TOKEN_URL =
4
+ "https://dash.cloudflare.com/profile/api-tokens?" +
5
+ "permissionGroupKeys=" +
6
+ encodeURIComponent(
7
+ JSON.stringify([
8
+ { key: "workers_scripts", type: "edit" },
9
+ { key: "containers", type: "edit" },
10
+ { key: "zone", type: "read" },
11
+ { key: "dns", type: "edit" },
12
+ ])
13
+ ) +
14
+ "&name=relight-cli";
15
+
16
+ export async function cfApi(method, path, body, apiToken) {
17
+ var headers = {
18
+ Authorization: `Bearer ${apiToken}`,
19
+ };
20
+
21
+ if (body && typeof body === "object") {
22
+ headers["Content-Type"] = "application/json";
23
+ body = JSON.stringify(body);
24
+ }
25
+
26
+ var res = await fetch(`${CF_API}${path}`, {
27
+ method,
28
+ headers,
29
+ body: method === "GET" ? undefined : body,
30
+ });
31
+
32
+ if (!res.ok) {
33
+ var text = await res.text();
34
+ throw new Error(`CF API ${method} ${path}: ${res.status} ${text}`);
35
+ }
36
+
37
+ var ct = res.headers.get("content-type") || "";
38
+ if (ct.includes("application/json")) {
39
+ return res.json();
40
+ }
41
+ return res.text();
42
+ }
43
+
44
+ export async function verifyToken(apiToken) {
45
+ return cfApi("GET", "/user/tokens/verify", null, apiToken);
46
+ }
47
+
48
+ export async function listAccounts(apiToken) {
49
+ var res = await cfApi("GET", "/accounts", null, apiToken);
50
+ return res.result || [];
51
+ }
52
+
53
+ export async function getWorkersSubdomain(accountId, apiToken) {
54
+ try {
55
+ var res = await cfApi(
56
+ "GET",
57
+ `/accounts/${accountId}/workers/subdomain`,
58
+ null,
59
+ apiToken
60
+ );
61
+ return res.result?.subdomain || null;
62
+ } catch {
63
+ return null;
64
+ }
65
+ }
@@ -0,0 +1,120 @@
1
+ import { createSign } from "crypto";
2
+ import { readFileSync } from "fs";
3
+
4
+ var RUN_API = "https://run.googleapis.com/v2";
5
+ var CRM_API = "https://cloudresourcemanager.googleapis.com/v1";
6
+ var TOKEN_URI = "https://oauth2.googleapis.com/token";
7
+ var SCOPE = "https://www.googleapis.com/auth/cloud-platform";
8
+
9
+ // --- Service account key file ---
10
+
11
+ export function readKeyFile(path) {
12
+ var raw = readFileSync(path, "utf-8");
13
+ var key = JSON.parse(raw);
14
+
15
+ if (!key.client_email || !key.private_key) {
16
+ throw new Error(
17
+ "Invalid service account key file. Expected client_email and private_key fields."
18
+ );
19
+ }
20
+
21
+ return {
22
+ clientEmail: key.client_email,
23
+ privateKey: key.private_key,
24
+ project: key.project_id || null,
25
+ };
26
+ }
27
+
28
+ // --- JWT → access token ---
29
+
30
+ export async function mintAccessToken(clientEmail, privateKey) {
31
+ var now = Math.floor(Date.now() / 1000);
32
+
33
+ var header = { alg: "RS256", typ: "JWT" };
34
+ var payload = {
35
+ iss: clientEmail,
36
+ scope: SCOPE,
37
+ aud: TOKEN_URI,
38
+ iat: now,
39
+ exp: now + 3600,
40
+ };
41
+
42
+ var segments = [
43
+ base64url(JSON.stringify(header)),
44
+ base64url(JSON.stringify(payload)),
45
+ ];
46
+
47
+ var sign = createSign("RSA-SHA256");
48
+ sign.update(segments.join("."));
49
+ var signature = sign.sign(privateKey, "base64url");
50
+
51
+ var jwt = segments.join(".") + "." + signature;
52
+
53
+ var res = await fetch(TOKEN_URI, {
54
+ method: "POST",
55
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
56
+ body: `grant_type=${encodeURIComponent("urn:ietf:params:oauth:grant_type:jwt-bearer")}&assertion=${encodeURIComponent(jwt)}`,
57
+ });
58
+
59
+ if (!res.ok) {
60
+ var text = await res.text();
61
+ throw new Error(`Token exchange failed: ${res.status} ${text}`);
62
+ }
63
+
64
+ var data = await res.json();
65
+ return data.access_token;
66
+ }
67
+
68
+ function base64url(str) {
69
+ return Buffer.from(str).toString("base64url");
70
+ }
71
+
72
+ // --- GCP API ---
73
+
74
+ export async function gcpApi(method, url, body, token) {
75
+ var headers = {
76
+ Authorization: `Bearer ${token}`,
77
+ };
78
+
79
+ if (body && typeof body === "object") {
80
+ headers["Content-Type"] = "application/json";
81
+ body = JSON.stringify(body);
82
+ }
83
+
84
+ var res = await fetch(url, {
85
+ method,
86
+ headers,
87
+ body: method === "GET" ? undefined : body,
88
+ });
89
+
90
+ if (!res.ok) {
91
+ var text = await res.text();
92
+ throw new Error(`GCP API ${method} ${url}: ${res.status} ${text}`);
93
+ }
94
+
95
+ return res.json();
96
+ }
97
+
98
+ export async function verifyProject(token, project) {
99
+ return gcpApi("GET", `${CRM_API}/projects/${project}`, null, token);
100
+ }
101
+
102
+ export async function listRegions(token, project) {
103
+ var res = await gcpApi(
104
+ "GET",
105
+ `${RUN_API}/projects/${project}/locations`,
106
+ null,
107
+ token
108
+ );
109
+ return res.locations || [];
110
+ }
111
+
112
+ export async function listServices(token, project, region) {
113
+ var res = await gcpApi(
114
+ "GET",
115
+ `${RUN_API}/projects/${project}/locations/${region}/services`,
116
+ null,
117
+ token
118
+ );
119
+ return res.services || [];
120
+ }
@@ -0,0 +1,58 @@
1
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
2
+ import { join } from "path";
3
+ import { homedir } from "os";
4
+
5
+ var CONFIG_DIR = join(homedir(), ".relight");
6
+ var CONFIG_PATH = join(CONFIG_DIR, "config.json");
7
+
8
+ export { CONFIG_DIR, CONFIG_PATH };
9
+
10
+ export var CLOUD_NAMES = {
11
+ cf: "Cloudflare",
12
+ gcp: "GCP",
13
+ aws: "AWS",
14
+ };
15
+
16
+ export var CLOUD_IDS = Object.keys(CLOUD_NAMES);
17
+
18
+ export function getConfig() {
19
+ if (!existsSync(CONFIG_PATH)) {
20
+ console.error("Not authenticated. Run `relight auth` first.");
21
+ process.exit(1);
22
+ }
23
+ return JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
24
+ }
25
+
26
+ export function tryGetConfig() {
27
+ if (!existsSync(CONFIG_PATH)) return null;
28
+ try {
29
+ return JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
30
+ } catch {
31
+ return null;
32
+ }
33
+ }
34
+
35
+ export function saveConfig(config) {
36
+ if (!existsSync(CONFIG_DIR)) mkdirSync(CONFIG_DIR, { recursive: true });
37
+ writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + "\n");
38
+ }
39
+
40
+ export function getCloudConfig(cloudId) {
41
+ var config = getConfig();
42
+ var cloud = config.clouds && config.clouds[cloudId];
43
+ if (!cloud) {
44
+ console.error(
45
+ `Not authenticated with ${CLOUD_NAMES[cloudId] || cloudId}. Run \`relight auth --cloud ${cloudId}\` first.`
46
+ );
47
+ process.exit(1);
48
+ }
49
+ return cloud;
50
+ }
51
+
52
+ export function getAuthenticatedClouds() {
53
+ var config = tryGetConfig();
54
+ if (!config || !config.clouds) return [];
55
+ return Object.keys(config.clouds).filter(
56
+ (id) => config.clouds[id] && Object.keys(config.clouds[id]).length > 0
57
+ );
58
+ }
@@ -0,0 +1,40 @@
1
+ import { readFileSync, writeFileSync, unlinkSync } from "fs";
2
+ import { fatal, fmt } from "./output.js";
3
+
4
+ var LINK_FILE = ".relight";
5
+
6
+ export function readLink() {
7
+ try {
8
+ var data = JSON.parse(readFileSync(LINK_FILE, "utf-8"));
9
+ return data;
10
+ } catch {
11
+ return null;
12
+ }
13
+ }
14
+
15
+ export function linkApp(name, cloud) {
16
+ writeFileSync(LINK_FILE, JSON.stringify({ app: name, cloud }) + "\n");
17
+ }
18
+
19
+ export function unlinkApp() {
20
+ try {
21
+ unlinkSync(LINK_FILE);
22
+ } catch {}
23
+ }
24
+
25
+ export function resolveAppName(name) {
26
+ if (name) return name;
27
+ var linked = readLink();
28
+ if (linked && linked.app) return linked.app;
29
+ fatal(
30
+ "No app specified.",
31
+ `Provide an app name or run ${fmt.cmd("relight deploy")} in this directory first.`
32
+ );
33
+ }
34
+
35
+ export function resolveCloud(cloud) {
36
+ if (cloud) return cloud;
37
+ var linked = readLink();
38
+ if (linked && linked.cloud) return linked.cloud;
39
+ return null;
40
+ }
@@ -0,0 +1,103 @@
1
+ import kleur from "kleur";
2
+
3
+ export function phase(msg) {
4
+ process.stderr.write(`\n${kleur.bold().cyan("==>")} ${kleur.bold(msg)}\n`);
5
+ }
6
+
7
+ export function status(msg) {
8
+ process.stderr.write(` ${msg}\n`);
9
+ }
10
+
11
+ export function error(msg, fix) {
12
+ process.stderr.write(`\n${kleur.red("Error:")} ${msg}\n`);
13
+ if (fix) process.stderr.write(` ${fix}\n`);
14
+ }
15
+
16
+ export function fatal(msg, fix) {
17
+ error(msg, fix);
18
+ process.exit(1);
19
+ }
20
+
21
+ export function success(msg) {
22
+ process.stderr.write(`\n${kleur.green("-->")} ${msg}\n`);
23
+ }
24
+
25
+ export function hint(label, msg) {
26
+ process.stderr.write(`\n${kleur.dim(label + ":")} ${msg}\n`);
27
+ }
28
+
29
+ export var fmt = {
30
+ app: (name) => kleur.cyan(name),
31
+ cmd: (cmd) => kleur.bold().cyan(cmd),
32
+ key: (k) => kleur.green(k),
33
+ val: (v) => kleur.yellow(v),
34
+ dim: (t) => kleur.dim(t),
35
+ bold: (t) => kleur.bold(t),
36
+ url: (u) => kleur.underline().cyan(u),
37
+ cloud: (c) => kleur.magenta(c),
38
+ };
39
+
40
+ function stripAnsi(str) {
41
+ return String(str).replace(/\x1B\[[0-9;]*m/g, "");
42
+ }
43
+
44
+ function padEnd(str, len) {
45
+ var visible = stripAnsi(str).length;
46
+ return str + " ".repeat(Math.max(0, len - visible));
47
+ }
48
+
49
+ var ADJECTIVES = [
50
+ "autumn", "bold", "calm", "crimson", "dawn", "dark", "dry", "dusk",
51
+ "fading", "flat", "floral", "fragrant", "frosty", "gentle", "green",
52
+ "hazy", "hidden", "icy", "lively", "long", "misty", "morning", "muddy",
53
+ "nameless", "old", "patient", "plain", "polished", "proud", "purple",
54
+ "quiet", "rapid", "red", "restless", "rough", "shy", "silent", "small",
55
+ "snowy", "solitary", "sparkling", "spring", "still", "summer", "twilight",
56
+ "wandering", "weathered", "white", "wild", "winter", "withered", "young",
57
+ ];
58
+
59
+ var NOUNS = [
60
+ "bird", "brook", "bush", "cloud", "creek", "dew", "dream", "dust",
61
+ "field", "fire", "flower", "fog", "forest", "frost", "gale", "gate",
62
+ "glacier", "grass", "grove", "haze", "hill", "lake", "leaf", "light",
63
+ "meadow", "moon", "moss", "night", "paper", "path", "peak", "pine",
64
+ "pond", "rain", "reef", "ridge", "river", "rock", "rose", "sea",
65
+ "shadow", "shore", "sky", "smoke", "snow", "sound", "star", "stone",
66
+ "stream", "sun", "surf", "thunder", "tree", "violet", "water", "wave",
67
+ "wind", "wood",
68
+ ];
69
+
70
+ export function generateAppName() {
71
+ var adj = ADJECTIVES[Math.floor(Math.random() * ADJECTIVES.length)];
72
+ var noun = NOUNS[Math.floor(Math.random() * NOUNS.length)];
73
+ var num = Math.floor(1000 + Math.random() * 9000);
74
+ return `${adj}-${noun}-${num}`;
75
+ }
76
+
77
+ export function table(headers, rows) {
78
+ if (rows.length === 0) return "";
79
+
80
+ var allRows = [headers, ...rows];
81
+ var widths = [];
82
+ for (var row of allRows) {
83
+ for (var i = 0; i < row.length; i++) {
84
+ var len = stripAnsi(String(row[i] || "")).length;
85
+ if (!widths[i] || len > widths[i]) widths[i] = len;
86
+ }
87
+ }
88
+
89
+ var lines = [];
90
+ lines.push(
91
+ headers
92
+ .map((h, i) => kleur.bold(padEnd(String(h), widths[i] + 2)))
93
+ .join("")
94
+ );
95
+ lines.push(kleur.dim(widths.map((w) => "─".repeat(w)).join(" ")));
96
+ for (var row of rows) {
97
+ lines.push(
98
+ row.map((cell, i) => padEnd(String(cell || ""), widths[i] + 2)).join("")
99
+ );
100
+ }
101
+
102
+ return lines.join("\n");
103
+ }