Skip to content

Commit 4e6446c

Browse files
committed
feat: interactive TLS configure and shell autocomplete
Make `ecloud compute app configure tls` interactive — prompts for domain, app port, ACME staging, and Caddy logs, then appends values directly to .env and placeholders to .env.example. Removes the intermediate .env.example.tls file. Add @oclif/plugin-autocomplete so `ecloud <TAB>` completes commands and flags instead of showing file completions.
1 parent 358194d commit 4e6446c

5 files changed

Lines changed: 163 additions & 60 deletions

File tree

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,5 +28,6 @@
2828
},
2929
"prettier": {
3030
"printWidth": 100
31-
}
31+
},
32+
"packageManager": "pnpm@9.15.0+sha512.76e2379760a4328ec4415815bcd6628dee727af3779aaa4c914e3944156c4299921a89f976381ee107d41f12cfa4b66681ca9c718f0668fa0831ed4c6d8ba56c"
3233
}

packages/cli/package.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,15 @@
99
"README.md"
1010
],
1111
"bin": {
12-
"ecloud": "./bin/run.js"
12+
"ecloud": "./bin/run.js",
13+
"ecloud-dev": "./bin/run.js"
1314
},
1415
"dependencies": {
1516
"@inquirer/prompts": "^7.10.1",
1617
"@layr-labs/ecloud-sdk": "workspace:*",
1718
"@napi-rs/keyring": "^1.0.5",
1819
"@oclif/core": "^4.8.0",
20+
"@oclif/plugin-autocomplete": "^3.2.40",
1921
"axios": "^1.13.2",
2022
"chalk": "^5.6.2",
2123
"cli-table3": "^0.6.5",
@@ -43,6 +45,9 @@
4345
"commands": "./dist/commands",
4446
"dirname": "",
4547
"topicSeparator": " ",
48+
"plugins": [
49+
"@oclif/plugin-autocomplete"
50+
],
4651
"topics": {
4752
"auth": {
4853
"description": "Manage authentication with private keys stored in OS keyring"

packages/cli/src/commands/compute/app/configure/tls.ts

Lines changed: 111 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -2,74 +2,147 @@ import { Command } from "@oclif/core";
22
import * as fs from "fs";
33
import * as path from "path";
44
import chalk from "chalk";
5-
import { getCaddyfileTemplate, ENV_EXAMPLE_TLS } from "../../../../templates/tls/templates.js";
5+
import { input, confirm } from "@inquirer/prompts";
6+
import {
7+
getCaddyfileTemplate,
8+
getTlsEnvBlock,
9+
TLS_ENV_EXAMPLE_BLOCK,
10+
} from "../../../../templates/tls/templates.js";
11+
12+
function envFileHasTlsConfig(filePath: string): boolean {
13+
if (!fs.existsSync(filePath)) return false;
14+
const content = fs.readFileSync(filePath, "utf-8");
15+
return /^DOMAIN=/m.test(content);
16+
}
617

718
export default class ConfigureTLS extends Command {
819
static description = "Configure TLS for your application";
920

10-
static summary = `Adds TLS configuration to your EigenCloud application.
21+
static summary = `Interactively configures TLS for your EigenCloud application.
1122
12-
This command creates:
13-
- Caddyfile: Reverse proxy configuration for automatic HTTPS
14-
- .env.example.tls: Example environment variables for TLS
23+
Prompts for domain and TLS settings, then:
24+
- Creates a Caddyfile for automatic HTTPS via Caddy reverse proxy
25+
- Appends TLS variables to .env with your values
26+
- Appends TLS placeholders to .env.example
1527
1628
TLS certificates are automatically obtained via Let's Encrypt using the tls-keygen tool.`;
1729

1830
async run() {
1931
const cwd = process.cwd();
2032

33+
// Check if TLS is already configured in .env
34+
const envPath = path.join(cwd, ".env");
35+
if (envFileHasTlsConfig(envPath)) {
36+
this.warn("TLS is already configured in .env (DOMAIN is set). Skipping.");
37+
return;
38+
}
39+
2140
// Write Caddyfile
2241
const caddyfilePath = path.join(cwd, "Caddyfile");
2342
if (fs.existsSync(caddyfilePath)) {
24-
this.warn("Caddyfile already exists. Skipping...");
43+
this.log("Caddyfile already exists, keeping existing file.");
2544
} else {
2645
const caddyfileContent = getCaddyfileTemplate();
2746
fs.writeFileSync(caddyfilePath, caddyfileContent, { mode: 0o644 });
2847
this.log("Created Caddyfile");
2948
}
3049

31-
// Write .env.example.tls
32-
const envTLSPath = path.join(cwd, ".env.example.tls");
33-
if (fs.existsSync(envTLSPath)) {
34-
this.warn(".env.example.tls already exists. Skipping...");
35-
} else {
36-
fs.writeFileSync(envTLSPath, ENV_EXAMPLE_TLS, { mode: 0o644 });
37-
this.log("Created .env.example.tls");
38-
}
39-
40-
// Print success message and instructions
41-
this.log("");
42-
this.log(chalk.green("TLS configuration added successfully"));
43-
this.log("");
44-
this.log("Created:");
45-
this.log(" - Caddyfile");
46-
this.log(" - .env.example.tls");
4750
this.log("");
4851

49-
this.log("To enable TLS:");
52+
// Prompt for TLS variables
53+
const domain = await input({
54+
message: "Domain name:",
55+
validate: (value) => {
56+
const trimmed = value.trim();
57+
if (!trimmed) return "Domain is required";
58+
if (trimmed.toLowerCase() === "localhost") return "Domain cannot be localhost";
59+
if (!/^[a-zA-Z0-9]([a-zA-Z0-9-]*\.)+[a-zA-Z]{2,}$/.test(trimmed))
60+
return "Enter a valid domain (e.g. myapp.example.com)";
61+
return true;
62+
},
63+
});
64+
65+
const appPort = await input({
66+
message: "App port:",
67+
default: "3000",
68+
validate: (value) => {
69+
const num = Number(value.trim());
70+
if (!Number.isInteger(num) || num < 1 || num > 65535) return "Enter a valid port (1-65535)";
71+
return true;
72+
},
73+
});
74+
75+
const acmeStaging = await confirm({
76+
message: "Use Let's Encrypt staging? (recommended for first deploy to avoid rate limits)",
77+
default: true,
78+
});
79+
80+
const enableCaddyLogs = await confirm({
81+
message: "Enable Caddy debug logs?",
82+
default: false,
83+
});
84+
85+
// Show summary
5086
this.log("");
51-
this.log("1. Add TLS variables to .env:");
52-
this.log(" cat .env.example.tls >> .env");
87+
this.log(chalk.bold("TLS Configuration:"));
88+
this.log(` Domain: ${domain.trim()}`);
89+
this.log(` App port: ${appPort.trim()}`);
90+
this.log(` ACME staging: ${acmeStaging}`);
91+
this.log(` Caddy logs: ${enableCaddyLogs}`);
5392
this.log("");
5493

55-
this.log("2. Configure required variables:");
56-
this.log(" DOMAIN=yourdomain.com");
94+
const confirmed = await confirm({
95+
message: "Write these settings to .env?",
96+
default: true,
97+
});
98+
99+
if (!confirmed) {
100+
this.log("Cancelled.");
101+
return;
102+
}
103+
104+
const vars = {
105+
domain: domain.trim(),
106+
appPort: appPort.trim(),
107+
acmeStaging,
108+
enableCaddyLogs,
109+
};
110+
111+
// Append to .env
112+
const envBlock = getTlsEnvBlock(vars);
113+
fs.appendFileSync(envPath, envBlock, { mode: 0o644 });
114+
this.log(`Updated .env`);
115+
116+
// Append to .env.example (with placeholders, skip if already has DOMAIN)
117+
const envExamplePath = path.join(cwd, ".env.example");
118+
if (!envFileHasTlsConfig(envExamplePath)) {
119+
fs.appendFileSync(envExamplePath, TLS_ENV_EXAMPLE_BLOCK, { mode: 0o644 });
120+
this.log(`Updated .env.example`);
121+
}
122+
123+
// Print next steps
57124
this.log("");
58-
this.log(" For first deployment (recommended):");
59-
this.log(" ENABLE_CADDY_LOGS=true");
60-
this.log(" ACME_STAGING=true");
125+
this.log(chalk.green("TLS configured successfully"));
61126
this.log("");
62-
63-
this.log("3. Set up DNS A record pointing to instance IP");
127+
this.log("Next steps:");
128+
this.log("");
129+
this.log("1. Set up DNS A record pointing to your instance IP");
64130
this.log(" Run 'ecloud compute app list' to get IP address");
65131
this.log("");
66-
67-
this.log("4. Upgrade:");
68-
this.log(" ecloud compute app upgrade");
132+
this.log("2. Deploy or upgrade:");
133+
this.log(" ecloud compute app deploy # new app");
134+
this.log(" ecloud compute app upgrade # existing app");
69135
this.log("");
70136

71-
this.log("Note: Let's Encrypt rate limit is 5 certificates/week per domain");
72-
this.log(" To switch staging -> production: set ACME_STAGING=false");
73-
this.log(" If cert exists, use ACME_FORCE_ISSUE=true once to replace");
137+
if (acmeStaging) {
138+
this.log(chalk.yellow("Note: ACME_STAGING is enabled (recommended for first deploy)"));
139+
this.log("Once verified, switch to production certs:");
140+
this.log(" 1. Set ACME_STAGING=false in .env");
141+
this.log(" 2. Set ACME_FORCE_ISSUE=true in .env (one-time)");
142+
this.log(" 3. Run: ecloud compute app upgrade");
143+
this.log("");
144+
}
145+
146+
this.log("Let's Encrypt rate limit: 5 certificates/week per domain");
74147
}
75148
}

packages/cli/src/templates/tls/templates.ts

Lines changed: 28 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,27 +11,35 @@ export function getCaddyfileTemplate(): string {
1111
return caddyfileTemplate;
1212
}
1313

14+
export interface TlsEnvVars {
15+
domain: string;
16+
appPort: string;
17+
acmeStaging: boolean;
18+
enableCaddyLogs: boolean;
19+
}
20+
1421
/**
15-
* Embedded .env.example.tls content
16-
* (embedded directly since .env files are gitignored)
22+
* Generate the TLS env block with user-provided values for .env
1723
*/
18-
export const ENV_EXAMPLE_TLS = `# TLS Configuration
19-
# Set these variables to enable TLS for your application
20-
21-
# Your domain name (required for TLS)
22-
DOMAIN=yourdomain.com
23-
24-
# Port your application listens on
25-
APP_PORT=3000
26-
27-
# Enable Caddy debug logs
28-
ENABLE_CADDY_LOGS=false
29-
30-
# Use Let's Encrypt staging environment (for testing)
31-
# Set to true to avoid rate limits during development
32-
ACME_STAGING=false
33-
34-
# Force certificate reissue even if a valid one exists
35-
# Useful when you need to update SANs or force a renewal
24+
export function getTlsEnvBlock(vars: TlsEnvVars): string {
25+
return `
26+
# TLS Configuration
27+
DOMAIN=${vars.domain}
28+
APP_PORT=${vars.appPort}
29+
ENABLE_CADDY_LOGS=${vars.enableCaddyLogs}
30+
ACME_STAGING=${vars.acmeStaging}
3631
ACME_FORCE_ISSUE=false
3732
`;
33+
}
34+
35+
/**
36+
* Placeholder TLS block for .env.example
37+
*/
38+
export const TLS_ENV_EXAMPLE_BLOCK = `
39+
# TLS Configuration
40+
# DOMAIN=yourdomain.com
41+
# APP_PORT=3000
42+
# ENABLE_CADDY_LOGS=false
43+
# ACME_STAGING=false
44+
# ACME_FORCE_ISSUE=false
45+
`;

pnpm-lock.yaml

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)