@lenne.tech/cli
Version:
lenne.Tech CLI: lt
171 lines (170 loc) • 8.56 kB
JavaScript
;
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;