UNPKG

@lenne.tech/cli

Version:

lenne.Tech CLI: lt

184 lines (183 loc) 7.98 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.paths = void 0; exports.caddyAvailable = caddyAvailable; exports.caddyDaemonRunning = caddyDaemonRunning; exports.readCaddyfile = readCaddyfile; exports.reloadCaddy = reloadCaddy; exports.removeProjectBlock = removeProjectBlock; exports.renderProjectBlock = renderProjectBlock; exports.upsertProjectBlock = upsertProjectBlock; exports.validateCaddyfile = validateCaddyfile; exports.writeCaddyfile = writeCaddyfile; /** * Caddy integration for `lt dev`. * * Caddy is the HTTPS engine: it provides automatic local TLS for * `*.localhost` (no /etc/hosts edits needed — RFC 6761), atomic * config reload, and a long-stable Caddyfile format. Compared to * portless / mkcert / nginx, Caddy gives all of this with a single * binary and no sudo daemon. * * Layout: * - Global Caddyfile at `~/.lenneTech/Caddyfile` — one block per * project, marked with `# >>> lt-dev:<slug> >>>` / `# <<<`. * - Atomic reload via `caddy reload --config ~/.lenneTech/Caddyfile`. * * Lifecycle is owned by `lt dev install` (one-time setup) and * `lt dev up`/`lt dev down` (per-project block management). */ const child_process_1 = require("child_process"); const fs_1 = require("fs"); const os_1 = require("os"); const path_1 = require("path"); const CADDYFILE_PATH = process.env.LT_DEV_CADDYFILE || (0, path_1.join)((0, os_1.homedir)(), '.lenneTech', 'Caddyfile'); const HEADER = '# Managed by `lt dev`. Per-project blocks are bounded by `# >>> lt-dev:<slug> >>>` markers.'; /** Detect whether `caddy` is on PATH. */ function caddyAvailable() { return __awaiter(this, void 0, void 0, function* () { const result = yield runCaddy(['version']); return result.ok; }); } /** Detect whether the Caddy admin endpoint is reachable (i.e. a daemon is running). */ function caddyDaemonRunning() { return __awaiter(this, void 0, void 0, function* () { return new Promise((resolve) => { const child = (0, child_process_1.spawn)('curl', ['-fsS', '-o', '/dev/null', 'http://localhost:2019/config/'], { stdio: ['ignore', 'ignore', 'ignore'], }); child.on('error', () => resolve(false)); child.on('close', (code) => resolve(code === 0)); }); }); } /** Read the current Caddyfile (or empty string). */ function readCaddyfile() { if (!(0, fs_1.existsSync)(CADDYFILE_PATH)) return ''; return (0, fs_1.readFileSync)(CADDYFILE_PATH, 'utf8'); } /** * Reload Caddy with the global Caddyfile. Caller is responsible for * starting Caddy in the first place (typically via `lt dev install`). */ function reloadCaddy() { return __awaiter(this, void 0, void 0, function* () { return runCaddy(['reload', '--config', CADDYFILE_PATH, '--adapter', 'caddyfile']); }); } /** * Remove a project block from the Caddyfile. * Returns true if anything was removed. */ function removeProjectBlock(slug) { const current = readCaddyfile(); const startMarker = `# >>> lt-dev:${slug} >>>`; const endMarker = `# <<< lt-dev:${slug} <<<`; const startIdx = current.indexOf(startMarker); const endIdx = current.indexOf(endMarker); if (startIdx === -1 || endIdx === -1 || endIdx <= startIdx) return false; const before = current.slice(0, startIdx).replace(/\n+$/, ''); const after = current.slice(endIdx + endMarker.length).replace(/^\n+/, ''); const next = [before, after].filter((s) => s.length > 0).join('\n\n'); writeCaddyfile(next); return true; } /** * Generate the Caddyfile block for one project's routes. * * Upstream uses `127.0.0.1:<port>` explicitly — paired with * `HOST=127.0.0.1` injected into the dev-server processes (see * `dev-env.ts`). This guarantees a single, unambiguous loopback path: * * - Vite/Nuxt/Nest, when given `HOST=127.0.0.1`, bind exclusively * to IPv4. There is no second IPv6 listener that could shadow * the port (which had been the source of the 502 / hanging * requests when two processes both registered on `[::1]:<port>`). * - `localhost` as Caddy upstream resolves to `::1` first on macOS, * so it would still pick the IPv6 family and miss the IPv4 bind. * Pinning to `127.0.0.1` removes that ambiguity entirely. */ function renderProjectBlock(slug, routes) { const lines = [`# >>> lt-dev:${slug} >>>`]; for (const route of routes) { lines.push(`${route.hostname} {`); lines.push(` reverse_proxy 127.0.0.1:${route.upstreamPort}`); lines.push('}'); } lines.push(`# <<< lt-dev:${slug} <<<`); return lines.join('\n'); } /** * Insert/replace a project block in the Caddyfile. * * Idempotent — re-applying with the same routes is a no-op. * Returns true if the file was modified. */ function upsertProjectBlock(slug, routes) { const current = readCaddyfile(); const block = renderProjectBlock(slug, routes); const startMarker = `# >>> lt-dev:${slug} >>>`; const endMarker = `# <<< lt-dev:${slug} <<<`; const startIdx = current.indexOf(startMarker); const endIdx = current.indexOf(endMarker); let next; if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) { const before = current.slice(0, startIdx).replace(/\n+$/, ''); const after = current.slice(endIdx + endMarker.length).replace(/^\n+/, ''); next = [before, block, after].filter((s) => s.length > 0).join('\n\n'); } else { next = current.length > 0 ? `${current.replace(/\n+$/, '')}\n\n${block}` : block; } if (next === current.replace(/\s+$/, '')) return false; writeCaddyfile(next); return true; } /** Validate the current Caddyfile syntax. */ function validateCaddyfile() { return __awaiter(this, void 0, void 0, function* () { return runCaddy(['validate', '--config', CADDYFILE_PATH, '--adapter', 'caddyfile']); }); } /** Write the Caddyfile, ensuring the parent directory exists. */ function writeCaddyfile(content) { (0, fs_1.mkdirSync)((0, path_1.dirname)(CADDYFILE_PATH), { recursive: true }); const next = content.startsWith('#') ? content : `${HEADER}\n\n${content}`; (0, fs_1.writeFileSync)(CADDYFILE_PATH, next.endsWith('\n') ? next : `${next}\n`, 'utf8'); } /** Run a caddy subcommand and capture stdout/stderr. */ function runCaddy(args) { return new Promise((resolve) => { var _a, _b; const child = (0, child_process_1.spawn)('caddy', args, { stdio: ['ignore', 'pipe', 'pipe'] }); let stdout = ''; let stderr = ''; let errored = false; (_a = child.stdout) === null || _a === void 0 ? void 0 : _a.on('data', (b) => (stdout += String(b))); (_b = child.stderr) === null || _b === void 0 ? void 0 : _b.on('data', (b) => (stderr += String(b))); child.on('error', () => (errored = true)); child.on('close', (code) => { if (errored) resolve({ exitCode: null, ok: false, stderr: 'caddy: command not found', stdout: '' }); else resolve({ exitCode: code, ok: code === 0, stderr, stdout }); }); }); } /** Path constants for tests + status displays. */ exports.paths = { caddyfile: CADDYFILE_PATH, };