@lenne.tech/cli
Version:
lenne.Tech CLI: lt
143 lines (142 loc) • 7.44 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 caddy_1 = require("../../lib/caddy");
const cloudflared_1 = require("../../lib/cloudflared");
const dev_identity_1 = require("../../lib/dev-identity");
const dev_project_1 = require("../../lib/dev-project");
const dev_state_1 = require("../../lib/dev-state");
/**
* Expose a running `lt dev up` project to the public internet via a
* Cloudflare Quick Tunnel.
*
* Quick tunnel = no Cloudflare account, ephemeral `*.trycloudflare.com`
* URL, runs in the foreground until Ctrl-C. Designed for ad-hoc work:
* - mobile / tablet preview from outside the LAN
* - sharing a feature with a teammate during review
* - landing webhooks from external services
*
* Caveats (printed at runtime):
* - Auth cookies set on `<slug>.localhost` are NOT valid on the
* `*.trycloudflare.com` domain — log in again on the public URL.
* - The default tunnel exposes ONLY the App subdomain (the API stays
* on `*.localhost`). Use `--api` to tunnel the API instead, or
* start a second `lt dev tunnel --api` in parallel.
* - Better-Auth's `trustedOrigins` won't include the random tunnel
* URL — login flows that validate the origin will reject the
* request unless you add the URL explicitly to your API config.
*
* Not yet supported (deliberate, separate command if needed later):
* - named tunnels with persistent URL (`cloudflared tunnel create`)
* - parallel multi-host tunnels in one process
* - background/detached mode (use a separate shell for now)
*/
const TunnelCommand = {
alias: ['tun'],
description: 'Cloudflare quick-tunnel to a running app',
hidden: false,
name: 'tunnel',
run: (toolbox) => __awaiter(void 0, void 0, void 0, function* () {
var _a, _b;
const { filesystem, parameters, print: { colors, error, info, success, warning }, } = toolbox;
const layout = (0, dev_project_1.resolveLayout)(filesystem.cwd(), filesystem);
if (!layout.apiDir && !layout.appDir) {
error('No API or App project detected at this path. Run `lt dev init` first.');
if (!parameters.options.fromGluegunMenu)
process.exit(1);
return 'dev tunnel: not a project';
}
const apiMode = Boolean(parameters.options.api);
const identity = (0, dev_identity_1.buildIdentity)(layout.root);
const targetSub = apiMode ? identity.subdomains.api : identity.subdomains.app;
if (!targetSub) {
error(`No ${apiMode ? 'API' : 'App'} subdomain configured for "${identity.slug}".`);
if (!parameters.options.fromGluegunMenu)
process.exit(1);
return 'dev tunnel: no target';
}
// Pre-flight: cloudflared installed
const available = yield (0, cloudflared_1.cloudflaredAvailable)();
if (!available.installed) {
error('cloudflared is not installed.');
info(` → ${colors.cyan('brew install cloudflared')} (macOS)`);
info(` → ${colors.cyan('https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/')} (Linux/Windows)`);
if (!parameters.options.fromGluegunMenu)
process.exit(1);
return 'dev tunnel: cloudflared missing';
}
// Pre-flight: Caddy must be up — without it cloudflared would forward
// to a dead upstream and the public URL would 502.
if (!(yield (0, caddy_1.caddyDaemonRunning)())) {
error('Caddy daemon is not running — run `lt dev install` first.');
if (!parameters.options.fromGluegunMenu)
process.exit(1);
return 'dev tunnel: caddy down';
}
const registry = (0, dev_state_1.loadRegistry)();
if (!registry.projects[identity.slug]) {
warning(`Project "${identity.slug}" is not in the lt-dev registry. Run \`lt dev up\` first.`);
info(' (Continuing anyway — Caddy may have a stale block.)');
}
const upstreamUrl = `https://${targetSub.hostname}`;
info('');
info(colors.bold(`lt dev tunnel — Cloudflare Quick Tunnel`));
info(colors.dim('─'.repeat(60)));
info(` Upstream: ${colors.cyan(upstreamUrl)}`);
info(colors.dim(` cloudflared ${available.version || 'unknown'}`));
info('');
info(colors.dim('Starting tunnel — this typically takes 5-10 seconds ...'));
const tunnel = (0, cloudflared_1.spawnQuickTunnel)({ hostHeader: targetSub.hostname, upstreamUrl });
// Wire stderr through so users see cloudflared progress / errors.
(_a = tunnel.child.stderr) === null || _a === void 0 ? void 0 : _a.on('data', (chunk) => {
process.stderr.write(String(chunk));
});
let publicUrl;
try {
publicUrl = yield tunnel.publicUrl;
}
catch (err) {
error(`Tunnel failed to start: ${err.message}`);
if (!parameters.options.fromGluegunMenu)
process.exit(1);
return 'dev tunnel: failed';
}
info('');
success(`Public URL: ${colors.cyan(publicUrl)}`);
info('');
info(colors.bold('Heads up:'));
info(' • Auth cookies set on the localhost domain are NOT valid on the tunnel URL.');
info(` • Better-Auth's trustedOrigins must include ${colors.cyan(publicUrl)} for login to succeed.`);
if (!apiMode) {
info(` • This tunnel exposes ONLY the App. The API stays on \`${((_b = identity.subdomains.api) === null || _b === void 0 ? void 0 : _b.hostname) || '—'}\`.`);
info(' For full external usage, start a second `lt dev tunnel --api` in another shell.');
}
info('');
info(colors.dim('Stop with Ctrl-C.'));
// Keep the foreground alive until the child exits (Ctrl-C → SIGINT
// is forwarded to the child by the terminal, child exits, we exit).
const exitCode = yield new Promise((resolve) => {
tunnel.child.on('close', resolve);
const forward = (sig) => {
if (!tunnel.child.killed)
tunnel.child.kill(sig);
};
process.on('SIGINT', forward);
process.on('SIGTERM', forward);
});
info('');
info(colors.dim(`cloudflared exited (code ${exitCode !== null && exitCode !== void 0 ? exitCode : 'unknown'}).`));
if (!parameters.options.fromGluegunMenu)
process.exit(exitCode !== null && exitCode !== void 0 ? exitCode : 0);
return `dev tunnel: exit=${exitCode}`;
}),
};
module.exports = TunnelCommand;