UNPKG

@lenne.tech/cli

Version:

lenne.Tech CLI: lt

171 lines (170 loc) 8.56 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 }); const child_process_1 = require("child_process"); const caddy_1 = require("../../lib/caddy"); const dev_process_1 = require("../../lib/dev-process"); const dev_project_1 = require("../../lib/dev-project"); const dev_service_1 = require("../../lib/dev-service"); const dev_state_1 = require("../../lib/dev-state"); const dev_ticket_1 = require("../../lib/dev-ticket"); /** * Diagnose Caddy / CA / DNS / port issues for `lt dev`. * * Categorical output (OK / WARN / FAIL) so developers can quickly see * what is missing on a fresh machine. Exit code 0 = all green, * 1 = at least one FAIL. * * Checks our OWN LaunchAgent / systemd-user unit — not * `brew services caddy`. The latter cannot host our Caddyfile. */ const DoctorCommand = { alias: ['doc'], description: 'Diagnose Caddy/CA/DNS/port issues', hidden: false, name: 'doctor', run: (toolbox) => __awaiter(void 0, void 0, void 0, function* () { const { filesystem, parameters, print: { colors, info }, } = toolbox; info(''); info(colors.bold('lt dev doctor')); info(colors.dim('─'.repeat(60))); let fails = 0; // 1. Caddy installed const hasCaddy = yield (0, caddy_1.caddyAvailable)(); if (hasCaddy) line('OK', colors.green, 'caddy on PATH'); else { line('FAIL', colors.red, 'caddy not installed — run `brew install caddy` then `lt dev install`'); fails++; } // 2. Service installed (LaunchAgent / systemd-user) const plat = (0, dev_service_1.platformSupported)(); if (plat === 'unsupported') { line('WARN', colors.yellow, `service management not supported on ${process.platform} — run caddy manually`); } else { const svc = yield (0, dev_service_1.getServiceStatus)(); const servicePaths = (0, dev_service_1.getServicePaths)(); if (svc.installed && svc.loaded) { line('OK', colors.green, `lt-dev service loaded (${servicePaths.unitFile})`); } else if (svc.installed && !svc.loaded) { line('FAIL', colors.red, `service file exists but is not loaded — run \`lt dev install\``); fails++; } else { line('FAIL', colors.red, `lt-dev service not installed — run \`lt dev install\``); fails++; } } // 3. Caddy daemon admin endpoint if (hasCaddy) { const daemon = yield (0, caddy_1.caddyDaemonRunning)(); if (daemon) line('OK', colors.green, 'caddy admin (:2019) reachable'); else { line('FAIL', colors.red, 'caddy admin (:2019) unreachable — run `lt dev install`'); fails++; } } // 4. Caddyfile validates if (hasCaddy) { const v = yield (0, caddy_1.validateCaddyfile)(); if (v.ok) line('OK', colors.green, `Caddyfile valid (${caddy_1.paths.caddyfile})`); else line('WARN', colors.yellow, `Caddyfile validation: ${v.stderr.split('\n')[0]}`); } // 4. Port 80 / 443 free or held by Caddy for (const port of [80, 443]) { const r = yield (0, dev_process_1.checkPortInUse)(port); if (r === null) line('WARN', colors.yellow, `lsof unavailable — cannot probe port ${port}`); else if (!r.inUse) line('OK', colors.green, `port ${port} free`); else if (r.command === 'caddy') line('OK', colors.green, `port ${port} held by caddy (pid ${r.pid})`); else { line('FAIL', colors.red, `port ${port} held by ${r.command} (pid ${r.pid}) — Caddy cannot bind`); fails++; } } // 5. *.localhost resolves to 127.0.0.1 const dnsOk = yield dnsResolvesLocalhost('lt-dev-doctor.localhost'); if (dnsOk) line('OK', colors.green, '*.localhost resolves to 127.0.0.1 (RFC 6761)'); else line('WARN', colors.yellow, '*.localhost may not resolve — check /etc/hosts or system resolver'); // 6. Registry const reg = (0, dev_state_1.loadRegistry)(); const count = Object.keys(reg.projects).length; line('OK', colors.green, `registry: ${count} project(s) at ${dev_state_1.paths.registry}`); // 7. Project-level (only when run inside a project): is a DB-wiping // Playwright global-setup ticket/shard-safe? WARN (never auto-edit) if a // bespoke allow-list would reject the per-ticket/shard `<base>-<id>-test` // DBs that `lt ticket` / `lt dev test --shard` create. const layout = (0, dev_project_1.resolveLayout)(filesystem.cwd(), filesystem); if (layout.apiDir || layout.appDir) { const gs = (0, dev_ticket_1.checkGlobalSetupTicketSafe)(layout); if (gs.file && gs.hasDbReset && !gs.ticketSafe) { line('WARN', colors.yellow, 'global-setup allow-list rejects per-ticket/shard test DBs — `lt ticket` / `--shard` E2E cannot reset its DB'); line('WARN', colors.yellow, ` ${gs.file}: widen isAllowedDb → /^<base>-(?:[a-z0-9-]+-)?test(?:-\\d+)?$/ (svl is the reference)`); } else if (gs.file && gs.hasDbReset) { line('OK', colors.green, 'global-setup allow-list is ticket + shard safe'); } // 8. Slug ↔ path: is this project's slug registered to a DIFFERENT checkout? // Two clones of the same project (same package.json "name") share the // slug → Caddy block / ports / DB and collide. Surface it proactively. const { identity } = (0, dev_ticket_1.resolveDevIdentity)(layout); const conflict = (0, dev_state_1.detectSlugConflict)(identity.slug, layout.root); if (conflict) { line('WARN', colors.yellow, `slug "${identity.slug}" is also registered to another checkout${conflict.otherSessionAlive ? ' (currently RUNNING)' : ''}: ${conflict.otherPath}`); line('WARN', colors.yellow, ' two clones of the same project collide on URLs/ports/DB — rename one package.json "name", or run only one.'); } } info(''); if (fails > 0) info(colors.red(`✗ ${fails} fail(s) — see above`)); else info(colors.green('✓ all checks passed')); if (!parameters.options.fromGluegunMenu) process.exit(fails > 0 ? 1 : 0); return fails > 0 ? `dev doctor: ${fails} fails` : 'dev doctor: ok'; function line(tag, color, msg) { info(` ${color(`[${tag.padEnd(4)}]`)} ${msg}`); } }), }; /** * Probe DNS — RFC 6761 mandates *.localhost MUST resolve to loopback. * * On macOS the resolver returns `::1` first (IPv6 loopback); on Linux * `127.0.0.1` (IPv4) is more common. Both are valid loopback addresses * and Caddy listens on both, so we accept either. */ function dnsResolvesLocalhost(host) { return new Promise((resolve) => { const child = (0, child_process_1.spawn)('node', [ '-e', `require('dns').lookup(${JSON.stringify(host)}, { all: true }, (e, addrs) => { if (e) process.exit(1); const loopback = (addrs || []).some(a => a.address === '127.0.0.1' || a.address === '::1'); process.exit(loopback ? 0 : 1); });`, ], { stdio: ['ignore', 'ignore', 'ignore'], }); child.on('error', () => resolve(false)); child.on('close', (code) => resolve(code === 0)); }); } module.exports = DoctorCommand;