@lenne.tech/cli 1.21.0 → 1.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,228 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ const fs_1 = require("fs");
13
+ const path_1 = require("path");
14
+ const caddy_1 = require("../../lib/caddy");
15
+ const dev_env_1 = require("../../lib/dev-env");
16
+ const dev_env_bridge_1 = require("../../lib/dev-env-bridge");
17
+ const dev_identity_1 = require("../../lib/dev-identity");
18
+ const dev_patches_1 = require("../../lib/dev-patches");
19
+ const dev_process_1 = require("../../lib/dev-process");
20
+ const dev_project_1 = require("../../lib/dev-project");
21
+ const dev_state_1 = require("../../lib/dev-state");
22
+ /**
23
+ * Start API + App behind Caddy with project-specific URLs.
24
+ *
25
+ * Pre-flight:
26
+ * - Caddy must be installed and running (otherwise points to install)
27
+ * - No existing `lt dev up` session for THIS project
28
+ * - Internal upstream ports must be free
29
+ *
30
+ * Process:
31
+ * 1. Resolve layout + identity
32
+ * 2. Allocate (or reuse) internal upstream ports
33
+ * 3. Upsert Caddy block + reload
34
+ * 4. Spawn API + App detached, log into `<root>/.lt-dev/{api,app}.log`
35
+ * 5. Persist registry entry + session state
36
+ *
37
+ * Env-vars injected (see lib/dev-env.ts):
38
+ * PORT, BASE_URL, APP_URL, NUXT_API_URL, NUXT_PUBLIC_API_URL,
39
+ * NUXT_PUBLIC_SITE_URL, NUXT_PUBLIC_STORAGE_PREFIX,
40
+ * NSC__MONGOOSE__URI, NSC__BASE_URL, NSC__APP_URL, DATABASE_URL,
41
+ * NUXT_PUBLIC_API_PROXY=false (Caddy makes vite-proxy obsolete).
42
+ */
43
+ const UpCommand = {
44
+ alias: ['u'],
45
+ description: 'Start API + App behind Caddy',
46
+ hidden: false,
47
+ name: 'up',
48
+ run: (toolbox) => __awaiter(void 0, void 0, void 0, function* () {
49
+ var _a, _b, _c, _d, _e, _f;
50
+ const { filesystem, parameters, print: { colors, error, info, success, warning }, } = toolbox;
51
+ const layout = (0, dev_project_1.resolveLayout)(filesystem.cwd(), filesystem);
52
+ if (!layout.apiDir && !layout.appDir) {
53
+ error('No API or App project detected at this path. Run `lt dev migrate` first.');
54
+ if (!parameters.options.fromGluegunMenu)
55
+ process.exit(1);
56
+ return 'dev up: not a project';
57
+ }
58
+ // Pre-flight: Caddy
59
+ if (!(yield (0, caddy_1.caddyAvailable)())) {
60
+ error('caddy is not installed. Run `lt dev install` first.');
61
+ if (!parameters.options.fromGluegunMenu)
62
+ process.exit(1);
63
+ return 'dev up: caddy missing';
64
+ }
65
+ if (!(yield (0, caddy_1.caddyDaemonRunning)())) {
66
+ error('caddy daemon is not running. Run `lt dev install` to start the lt-dev service.');
67
+ if (!parameters.options.fromGluegunMenu)
68
+ process.exit(1);
69
+ return 'dev up: caddy daemon down';
70
+ }
71
+ const identity = (0, dev_identity_1.buildIdentity)(layout.root);
72
+ const dbName = (0, dev_project_1.deriveDbName)(layout.apiDir, identity.slug);
73
+ // Sanft auto-migrate sichere Operationen (ohne Code-Modifikation):
74
+ // CLAUDE.md-URL-Block einfügen + .gitignore ergänzen.
75
+ // Code-Patches (config.env.ts, nuxt.config.ts) bleiben explizit `lt dev migrate`.
76
+ {
77
+ const claudeCandidates = [
78
+ (0, path_1.join)(layout.root, 'CLAUDE.md'),
79
+ ...(layout.apiDir ? [(0, path_1.join)(layout.apiDir, 'CLAUDE.md')] : []),
80
+ ...(layout.appDir ? [(0, path_1.join)(layout.appDir, 'CLAUDE.md')] : []),
81
+ ];
82
+ const patched = claudeCandidates.map((f) => (0, dev_patches_1.patchClaudeMd)(f, { dbName, identity })).filter((r) => r.patched);
83
+ if (patched.length > 0) {
84
+ info(colors.dim(`updated CLAUDE.md URL block in ${patched.length} file(s)`));
85
+ }
86
+ if ((0, dev_patches_1.addToGitignore)(layout.root, '.lt-dev/')) {
87
+ info(colors.dim('added `.lt-dev/` to .gitignore'));
88
+ }
89
+ }
90
+ // Warnung bei Legacy-Code (hardcoded ports) — kein Auto-Patch.
91
+ {
92
+ const legacyFiles = [];
93
+ if (layout.apiDir) {
94
+ const f = (0, dev_project_1.apiNeedsPortPatch)(layout.apiDir);
95
+ if (f)
96
+ legacyFiles.push(f);
97
+ }
98
+ if (layout.appDir)
99
+ legacyFiles.push(...(0, dev_project_1.appNeedsPortPatch)(layout.appDir));
100
+ if (legacyFiles.length > 0) {
101
+ warning('Legacy hardcoded ports detected — Caddy will proxy correctly only after running `lt dev migrate`:');
102
+ legacyFiles.forEach((f) => info(colors.dim(` - ${f}`)));
103
+ info(colors.dim(' (Continuing — env-aware files will work; legacy files may bind on 3000/3001 and miss Caddy.)'));
104
+ }
105
+ }
106
+ // Already running?
107
+ const existingSession = (0, dev_state_1.loadSession)(layout.root);
108
+ if (existingSession) {
109
+ const apiUp = existingSession.pids.api ? (0, dev_state_1.isPidAlive)(existingSession.pids.api) : false;
110
+ const appUp = existingSession.pids.app ? (0, dev_state_1.isPidAlive)(existingSession.pids.app) : false;
111
+ if (apiUp || appUp) {
112
+ warning(`Already running (api pid ${(_a = existingSession.pids.api) !== null && _a !== void 0 ? _a : '-'}, app pid ${(_b = existingSession.pids.app) !== null && _b !== void 0 ? _b : '-'}).`);
113
+ info('Run `lt dev down` first.');
114
+ if (!parameters.options.fromGluegunMenu)
115
+ process.exit(1);
116
+ return 'dev up: already running';
117
+ }
118
+ }
119
+ // Allocate internal ports (reuse existing if registered).
120
+ const reg = (0, dev_state_1.loadRegistry)();
121
+ const entry = reg.projects[identity.slug];
122
+ const taken = (0, dev_state_1.takenInternalPorts)(reg, identity.slug);
123
+ const apiPort = (_c = entry === null || entry === void 0 ? void 0 : entry.internalPorts.api) !== null && _c !== void 0 ? _c : (layout.apiDir ? (0, dev_state_1.allocateInternalPort)(4000, taken) : undefined);
124
+ if (apiPort)
125
+ taken.add(apiPort);
126
+ const appPort = (_d = entry === null || entry === void 0 ? void 0 : entry.internalPorts.app) !== null && _d !== void 0 ? _d : (layout.appDir ? (0, dev_state_1.allocateInternalPort)(4000, taken) : undefined);
127
+ // Pre-flight: internal ports free?
128
+ const portsToCheck = [apiPort, appPort].filter((p) => typeof p === 'number');
129
+ const snap = yield (0, dev_process_1.listenSnapshot)(portsToCheck);
130
+ for (const p of portsToCheck) {
131
+ const r = snap.get(p);
132
+ if (r) {
133
+ error(`Internal port ${p} already in use by ${r.command} (pid ${r.pid}).`);
134
+ if (!parameters.options.fromGluegunMenu)
135
+ process.exit(1);
136
+ return 'dev up: port in use';
137
+ }
138
+ }
139
+ // Caddy block + reload.
140
+ const routes = [];
141
+ if (identity.subdomains.api && apiPort)
142
+ routes.push({ hostname: identity.subdomains.api.hostname, upstreamPort: apiPort });
143
+ if (identity.subdomains.app && appPort)
144
+ routes.push({ hostname: identity.subdomains.app.hostname, upstreamPort: appPort });
145
+ if (routes.length === 0) {
146
+ error('No subdomains to expose (project has neither api nor app).');
147
+ if (!parameters.options.fromGluegunMenu)
148
+ process.exit(1);
149
+ return 'dev up: nothing to expose';
150
+ }
151
+ (0, caddy_1.upsertProjectBlock)(identity.slug, routes);
152
+ const reload = yield (0, caddy_1.reloadCaddy)();
153
+ if (!reload.ok) {
154
+ error(`caddy reload failed:\n${reload.stderr}`);
155
+ if (!parameters.options.fromGluegunMenu)
156
+ process.exit(1);
157
+ return 'dev up: caddy reload failed';
158
+ }
159
+ info('');
160
+ info(colors.bold(`Starting "${identity.slug}"`));
161
+ if (identity.subdomains.app)
162
+ info(` app: https://${identity.subdomains.app.hostname} → 127.0.0.1:${appPort}`);
163
+ if (identity.subdomains.api)
164
+ info(` api: https://${identity.subdomains.api.hostname} → 127.0.0.1:${apiPort}`);
165
+ info(` db: mongodb://127.0.0.1/${dbName}`);
166
+ info('');
167
+ // Build env per process.
168
+ const devEnv = (0, dev_env_1.buildDevEnv)({
169
+ apiInternalPort: apiPort !== null && apiPort !== void 0 ? apiPort : 0,
170
+ appInternalPort: appPort !== null && appPort !== void 0 ? appPort : 0,
171
+ baseEnv: process.env,
172
+ dbName,
173
+ identity,
174
+ });
175
+ const pnpmBin = process.env.LT_PNPM_BIN || 'pnpm';
176
+ const pids = {};
177
+ if (layout.apiDir && (0, fs_1.existsSync)((0, path_1.join)(layout.apiDir, 'package.json')) && apiPort) {
178
+ const apiPid = (0, dev_process_1.spawnDetached)(pnpmBin, ['start'], {
179
+ cwd: layout.apiDir,
180
+ env: devEnv.api.env,
181
+ logFile: (0, path_1.join)(layout.root, '.lt-dev', 'api.log'),
182
+ });
183
+ if (apiPid)
184
+ pids.api = apiPid;
185
+ }
186
+ if (layout.appDir && (0, fs_1.existsSync)((0, path_1.join)(layout.appDir, 'package.json')) && appPort) {
187
+ const appPid = (0, dev_process_1.spawnDetached)(pnpmBin, ['dev'], {
188
+ cwd: layout.appDir,
189
+ env: devEnv.app.env,
190
+ logFile: (0, path_1.join)(layout.root, '.lt-dev', 'app.log'),
191
+ });
192
+ if (appPid)
193
+ pids.app = appPid;
194
+ }
195
+ // Persist.
196
+ const subdomainMap = {};
197
+ for (const [k, v] of Object.entries(identity.subdomains))
198
+ subdomainMap[k] = v.hostname;
199
+ reg.projects[identity.slug] = {
200
+ dbName,
201
+ internalPorts: { api: apiPort, app: appPort },
202
+ lastUsedAt: new Date().toISOString(),
203
+ path: layout.root,
204
+ subdomains: subdomainMap,
205
+ };
206
+ (0, dev_state_1.saveRegistry)(reg);
207
+ (0, dev_state_1.saveSession)(layout.root, { pids, startedAt: new Date().toISOString() });
208
+ // Write the ENV bridge so external tools (Playwright, IDE test runners,
209
+ // custom shell scripts) can pick up the URLs without inheriting our shell.
210
+ const bridgePath = (0, dev_env_bridge_1.writeEnvBridge)(layout.root, devEnv, dbName);
211
+ info(colors.dim(`ENV bridge: ${bridgePath}`));
212
+ success(`Started: api pid=${(_e = pids.api) !== null && _e !== void 0 ? _e : '-'}, app pid=${(_f = pids.app) !== null && _f !== void 0 ? _f : '-'}`);
213
+ info(colors.dim('Logs: <root>/.lt-dev/api.log, <root>/.lt-dev/app.log'));
214
+ info(colors.dim('Stop with: lt dev down'));
215
+ // Best-effort: kill orphaned children if neither spawned (unlikely, but tidy).
216
+ if (Object.keys(pids).length === 0) {
217
+ warning('Nothing was spawned. Check package.json scripts (`start` for api, `dev` for app).');
218
+ if (pids.api)
219
+ (0, dev_process_1.killProcessGroup)(pids.api);
220
+ if (pids.app)
221
+ (0, dev_process_1.killProcessGroup)(pids.app);
222
+ }
223
+ if (!parameters.options.fromGluegunMenu)
224
+ process.exit();
225
+ return `dev up: api=${pids.api}, app=${pids.app}`;
226
+ }),
227
+ };
228
+ module.exports = UpCommand;
@@ -12,6 +12,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
12
12
  return (mod && mod.__esModule) ? mod : { "default": mod };
13
13
  };
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
+ const gluegun_1 = require("gluegun");
16
+ const caddy_1 = require("../../lib/caddy");
17
+ const dev_migrate_helper_1 = require("../../lib/dev-migrate-helper");
18
+ const dev_project_1 = require("../../lib/dev-project");
15
19
  const hoist_workspace_pnpm_config_1 = require("../../lib/hoist-workspace-pnpm-config");
16
20
  const workspace_integration_1 = require("../../lib/workspace-integration");
17
21
  const add_api_1 = __importDefault(require("./add-api"));
@@ -27,7 +31,7 @@ const NewCommand = {
27
31
  run: (toolbox) => __awaiter(void 0, void 0, void 0, function* () {
28
32
  var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0;
29
33
  // Retrieve the tools we need
30
- const { config, filesystem, frontendHelper, git, helper, parameters, patching, print: { error, info, spin, success }, prompt: { ask, confirm }, server, strings: { kebabCase }, system, template, } = toolbox;
34
+ const { config, filesystem, frontendHelper, git, helper, parameters, patching, print: { colors, error, info, spin, success }, prompt: { ask, confirm }, server, strings: { kebabCase }, system, template, } = toolbox;
31
35
  // Start timer
32
36
  const timer = system.startTimer();
33
37
  // Info
@@ -637,6 +641,22 @@ const NewCommand = {
637
641
  return;
638
642
  }
639
643
  }
644
+ // Best-effort `lt dev migrate` so the workspace is ready for `lt dev up`
645
+ // out-of-the-box: registers the slug in `~/.lenneTech/projects.json`,
646
+ // injects the URL block into CLAUDE.md, adds `.lt-dev/` to .gitignore.
647
+ // Failures here are non-fatal — `init` itself remains successful.
648
+ let devMigrateOk = false;
649
+ try {
650
+ const layout = (0, dev_project_1.resolveLayout)(projectDir, gluegun_1.filesystem);
651
+ const migrate = (0, dev_migrate_helper_1.runMigrate)({ layout });
652
+ devMigrateOk = true;
653
+ if (!migrate.alreadyMigrated) {
654
+ info(colors.dim(` registered "${migrate.identity.slug}" with \`lt dev\``));
655
+ }
656
+ }
657
+ catch (_1) {
658
+ /* best-effort — never block init */
659
+ }
640
660
  // We're done, so show what to do next
641
661
  info('');
642
662
  success(`Generated fullstack workspace with ${frontend} in ${projectDir} with ${name} app in ${helper.msToMinutesAndSeconds(timer())}m.`);
@@ -652,7 +672,21 @@ const NewCommand = {
652
672
  else {
653
673
  info(` Run ${name}`);
654
674
  info(` $ cd ${projectDir}`);
655
- info(` $ ${toolbox.pm.run('start')}`);
675
+ if (devMigrateOk) {
676
+ // Prefer lt dev up — sets up Caddy + HTTPS URLs + cross-wiring guards.
677
+ const caddyOk = yield (0, caddy_1.caddyAvailable)();
678
+ if (caddyOk) {
679
+ info(' $ lt dev up # start API + App behind Caddy with project-specific URLs');
680
+ }
681
+ else {
682
+ info(' $ lt dev install # one-time per machine: verify Caddy + CA');
683
+ info(' $ lt dev up # then: start API + App behind Caddy');
684
+ }
685
+ info(colors.dim(` (fallback: ${toolbox.pm.run('start')} runs the classic localhost:3000/3001 mode)`));
686
+ }
687
+ else {
688
+ info(` $ ${toolbox.pm.run('start')}`);
689
+ }
656
690
  }
657
691
  info('');
658
692
  if (!toolbox.parameters.options.fromGluegunMenu) {
@@ -10,9 +10,10 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
12
  const path_1 = require("path");
13
+ const dev_identity_1 = require("../lib/dev-identity");
14
+ const dev_state_1 = require("../lib/dev-state");
13
15
  const framework_detection_1 = require("../lib/framework-detection");
14
16
  const frontend_framework_detection_1 = require("../lib/frontend-framework-detection");
15
- const port_registry_1 = require("../lib/port-registry");
16
17
  const workspace_integration_1 = require("../lib/workspace-integration");
17
18
  /**
18
19
  * Show project status and context
@@ -235,32 +236,31 @@ const StatusCommand = {
235
236
  info(colors.dim(' Hint: `lt fullstack add-app` to integrate a Nuxt or Angular app.'));
236
237
  }
237
238
  }
238
- // Local dev orchestration registry — surface slot/ports if registered.
239
- // Helps users discover that `lt local` is set up for this project, and
240
- // gives a quick inline answer to "what ports does this project use?".
239
+ // Local dev orchestration registry — surface URLs if registered.
240
+ // Helps users discover that `lt dev` is set up for this project, and
241
+ // gives a quick inline answer to "what URLs does this project use?".
241
242
  {
242
243
  const registryRoot = ((_c = projectInfo.workspaceSubProject) === null || _c === void 0 ? void 0 : _c.root) ||
243
244
  (projectInfo.workspaceLayout.hasWorkspace ? projectInfo.workspaceLayout.workspaceDir : cwd);
244
245
  if (registryRoot) {
245
- const slug = (0, port_registry_1.projectSlug)(registryRoot);
246
- const entry = (0, port_registry_1.loadRegistry)().projects[slug];
246
+ const slug = (0, dev_identity_1.projectSlug)(registryRoot);
247
+ const entry = (0, dev_state_1.loadRegistry)().projects[slug];
247
248
  if (entry) {
248
249
  info('');
249
- info(colors.bold('Local dev orchestration (lt local):'));
250
- const ports = (0, port_registry_1.portsForSlot)(entry.slot);
251
- info(` Slot: ${entry.slot}`);
252
- info(` API: http://localhost:${ports.api}`);
253
- info(` App: http://localhost:${ports.app}`);
250
+ info(colors.bold('Local dev orchestration (lt dev):'));
251
+ for (const [sub, host] of Object.entries(entry.subdomains)) {
252
+ info(` ${sub.padEnd(6)} https://${host}`);
253
+ }
254
254
  if (entry.dbName)
255
- info(` DB: mongodb://127.0.0.1/${entry.dbName}`);
256
- const state = (0, port_registry_1.loadLocalState)(registryRoot);
257
- const apiAlive = (state === null || state === void 0 ? void 0 : state.pids.api) ? (0, port_registry_1.isPidAlive)(state.pids.api) : false;
258
- const appAlive = (state === null || state === void 0 ? void 0 : state.pids.app) ? (0, port_registry_1.isPidAlive)(state.pids.app) : false;
255
+ info(` db mongodb://127.0.0.1/${entry.dbName}`);
256
+ const session = (0, dev_state_1.loadSession)(registryRoot);
257
+ const apiAlive = (session === null || session === void 0 ? void 0 : session.pids.api) ? (0, dev_state_1.isPidAlive)(session.pids.api) : false;
258
+ const appAlive = (session === null || session === void 0 ? void 0 : session.pids.app) ? (0, dev_state_1.isPidAlive)(session.pids.app) : false;
259
259
  if (apiAlive || appAlive) {
260
260
  info(` Running: api ${apiAlive ? colors.green('●') : colors.dim('○')} app ${appAlive ? colors.green('●') : colors.dim('○')}`);
261
261
  }
262
262
  else {
263
- info(colors.dim(' Hint: `lt local up` to start API + App with these ports.'));
263
+ info(colors.dim(' Hint: `lt dev up` to start API + App behind Caddy.'));
264
264
  }
265
265
  }
266
266
  }
@@ -0,0 +1,183 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.paths = void 0;
13
+ exports.caddyAvailable = caddyAvailable;
14
+ exports.caddyDaemonRunning = caddyDaemonRunning;
15
+ exports.readCaddyfile = readCaddyfile;
16
+ exports.reloadCaddy = reloadCaddy;
17
+ exports.removeProjectBlock = removeProjectBlock;
18
+ exports.renderProjectBlock = renderProjectBlock;
19
+ exports.upsertProjectBlock = upsertProjectBlock;
20
+ exports.validateCaddyfile = validateCaddyfile;
21
+ exports.writeCaddyfile = writeCaddyfile;
22
+ /**
23
+ * Caddy integration for `lt dev`.
24
+ *
25
+ * Caddy is the HTTPS engine: it provides automatic local TLS for
26
+ * `*.localhost` (no /etc/hosts edits needed — RFC 6761), atomic
27
+ * config reload, and a long-stable Caddyfile format. Compared to
28
+ * portless / mkcert / nginx, Caddy gives all of this with a single
29
+ * binary and no sudo daemon.
30
+ *
31
+ * Layout:
32
+ * - Global Caddyfile at `~/.lenneTech/Caddyfile` — one block per
33
+ * project, marked with `# >>> lt-dev:<slug> >>>` / `# <<<`.
34
+ * - Atomic reload via `caddy reload --config ~/.lenneTech/Caddyfile`.
35
+ *
36
+ * Lifecycle is owned by `lt dev install` (one-time setup) and
37
+ * `lt dev up`/`lt dev down` (per-project block management).
38
+ */
39
+ const child_process_1 = require("child_process");
40
+ const fs_1 = require("fs");
41
+ const os_1 = require("os");
42
+ const path_1 = require("path");
43
+ const CADDYFILE_PATH = process.env.LT_DEV_CADDYFILE || (0, path_1.join)((0, os_1.homedir)(), '.lenneTech', 'Caddyfile');
44
+ const HEADER = '# Managed by `lt dev`. Per-project blocks are bounded by `# >>> lt-dev:<slug> >>>` markers.';
45
+ /** Detect whether `caddy` is on PATH. */
46
+ function caddyAvailable() {
47
+ return __awaiter(this, void 0, void 0, function* () {
48
+ const result = yield runCaddy(['version']);
49
+ return result.ok;
50
+ });
51
+ }
52
+ /** Detect whether the Caddy admin endpoint is reachable (i.e. a daemon is running). */
53
+ function caddyDaemonRunning() {
54
+ return __awaiter(this, void 0, void 0, function* () {
55
+ return new Promise((resolve) => {
56
+ const child = (0, child_process_1.spawn)('curl', ['-fsS', '-o', '/dev/null', 'http://localhost:2019/config/'], {
57
+ stdio: ['ignore', 'ignore', 'ignore'],
58
+ });
59
+ child.on('error', () => resolve(false));
60
+ child.on('close', (code) => resolve(code === 0));
61
+ });
62
+ });
63
+ }
64
+ /** Read the current Caddyfile (or empty string). */
65
+ function readCaddyfile() {
66
+ if (!(0, fs_1.existsSync)(CADDYFILE_PATH))
67
+ return '';
68
+ return (0, fs_1.readFileSync)(CADDYFILE_PATH, 'utf8');
69
+ }
70
+ /**
71
+ * Reload Caddy with the global Caddyfile. Caller is responsible for
72
+ * starting Caddy in the first place (typically via `lt dev install`).
73
+ */
74
+ function reloadCaddy() {
75
+ return __awaiter(this, void 0, void 0, function* () {
76
+ return runCaddy(['reload', '--config', CADDYFILE_PATH, '--adapter', 'caddyfile']);
77
+ });
78
+ }
79
+ /**
80
+ * Remove a project block from the Caddyfile.
81
+ * Returns true if anything was removed.
82
+ */
83
+ function removeProjectBlock(slug) {
84
+ const current = readCaddyfile();
85
+ const startMarker = `# >>> lt-dev:${slug} >>>`;
86
+ const endMarker = `# <<< lt-dev:${slug} <<<`;
87
+ const startIdx = current.indexOf(startMarker);
88
+ const endIdx = current.indexOf(endMarker);
89
+ if (startIdx === -1 || endIdx === -1 || endIdx <= startIdx)
90
+ return false;
91
+ const before = current.slice(0, startIdx).replace(/\n+$/, '');
92
+ const after = current.slice(endIdx + endMarker.length).replace(/^\n+/, '');
93
+ const next = [before, after].filter((s) => s.length > 0).join('\n\n');
94
+ writeCaddyfile(next);
95
+ return true;
96
+ }
97
+ /**
98
+ * Generate the Caddyfile block for one project's routes.
99
+ *
100
+ * Upstream uses `127.0.0.1:<port>` explicitly — paired with
101
+ * `HOST=127.0.0.1` injected into the dev-server processes (see
102
+ * `dev-env.ts`). This guarantees a single, unambiguous loopback path:
103
+ *
104
+ * - Vite/Nuxt/Nest, when given `HOST=127.0.0.1`, bind exclusively
105
+ * to IPv4. There is no second IPv6 listener that could shadow
106
+ * the port (which had been the source of the 502 / hanging
107
+ * requests when two processes both registered on `[::1]:<port>`).
108
+ * - `localhost` as Caddy upstream resolves to `::1` first on macOS,
109
+ * so it would still pick the IPv6 family and miss the IPv4 bind.
110
+ * Pinning to `127.0.0.1` removes that ambiguity entirely.
111
+ */
112
+ function renderProjectBlock(slug, routes) {
113
+ const lines = [`# >>> lt-dev:${slug} >>>`];
114
+ for (const route of routes) {
115
+ lines.push(`${route.hostname} {`);
116
+ lines.push(` reverse_proxy 127.0.0.1:${route.upstreamPort}`);
117
+ lines.push('}');
118
+ }
119
+ lines.push(`# <<< lt-dev:${slug} <<<`);
120
+ return lines.join('\n');
121
+ }
122
+ /**
123
+ * Insert/replace a project block in the Caddyfile.
124
+ *
125
+ * Idempotent — re-applying with the same routes is a no-op.
126
+ * Returns true if the file was modified.
127
+ */
128
+ function upsertProjectBlock(slug, routes) {
129
+ const current = readCaddyfile();
130
+ const block = renderProjectBlock(slug, routes);
131
+ const startMarker = `# >>> lt-dev:${slug} >>>`;
132
+ const endMarker = `# <<< lt-dev:${slug} <<<`;
133
+ const startIdx = current.indexOf(startMarker);
134
+ const endIdx = current.indexOf(endMarker);
135
+ let next;
136
+ if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
137
+ const before = current.slice(0, startIdx).replace(/\n+$/, '');
138
+ const after = current.slice(endIdx + endMarker.length).replace(/^\n+/, '');
139
+ next = [before, block, after].filter((s) => s.length > 0).join('\n\n');
140
+ }
141
+ else {
142
+ next = current.length > 0 ? `${current.replace(/\n+$/, '')}\n\n${block}` : block;
143
+ }
144
+ if (next === current.replace(/\s+$/, ''))
145
+ return false;
146
+ writeCaddyfile(next);
147
+ return true;
148
+ }
149
+ /** Validate the current Caddyfile syntax. */
150
+ function validateCaddyfile() {
151
+ return __awaiter(this, void 0, void 0, function* () {
152
+ return runCaddy(['validate', '--config', CADDYFILE_PATH, '--adapter', 'caddyfile']);
153
+ });
154
+ }
155
+ /** Write the Caddyfile, ensuring the parent directory exists. */
156
+ function writeCaddyfile(content) {
157
+ (0, fs_1.mkdirSync)((0, path_1.dirname)(CADDYFILE_PATH), { recursive: true });
158
+ const next = content.startsWith('#') ? content : `${HEADER}\n\n${content}`;
159
+ (0, fs_1.writeFileSync)(CADDYFILE_PATH, next.endsWith('\n') ? next : `${next}\n`, 'utf8');
160
+ }
161
+ /** Run a caddy subcommand and capture stdout/stderr. */
162
+ function runCaddy(args) {
163
+ return new Promise((resolve) => {
164
+ var _a, _b;
165
+ const child = (0, child_process_1.spawn)('caddy', args, { stdio: ['ignore', 'pipe', 'pipe'] });
166
+ let stdout = '';
167
+ let stderr = '';
168
+ let errored = false;
169
+ (_a = child.stdout) === null || _a === void 0 ? void 0 : _a.on('data', (b) => (stdout += String(b)));
170
+ (_b = child.stderr) === null || _b === void 0 ? void 0 : _b.on('data', (b) => (stderr += String(b)));
171
+ child.on('error', () => (errored = true));
172
+ child.on('close', (code) => {
173
+ if (errored)
174
+ resolve({ exitCode: null, ok: false, stderr: 'caddy: command not found', stdout: '' });
175
+ else
176
+ resolve({ exitCode: code, ok: code === 0, stderr, stdout });
177
+ });
178
+ });
179
+ }
180
+ /** Path constants for tests + status displays. */
181
+ exports.paths = {
182
+ caddyfile: CADDYFILE_PATH,
183
+ };
@@ -0,0 +1,129 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.TRYCLOUDFLARE_URL_PATTERN = void 0;
13
+ exports.buildQuickTunnelArgs = buildQuickTunnelArgs;
14
+ exports.cloudflaredAvailable = cloudflaredAvailable;
15
+ exports.extractTrycloudflareUrl = extractTrycloudflareUrl;
16
+ exports.spawnQuickTunnel = spawnQuickTunnel;
17
+ /**
18
+ * Cloudflare Tunnel integration for `lt dev tunnel`.
19
+ *
20
+ * Quick tunnels (no Cloudflare account, ephemeral URL) are the only
21
+ * mode supported here. Named tunnels would require multi-step Cloudflare
22
+ * setup (auth, DNS routing) which belongs in a separate command and is
23
+ * intentionally out of scope for this lib.
24
+ *
25
+ * The Caddy upstream stays unchanged: cloudflared connects to Caddy's
26
+ * HTTPS endpoint and rewrites the `Host` header to the configured
27
+ * `*.localhost` so Caddy's per-project block matches. Without that
28
+ * rewrite Cloudflare's edge would forward the random `*.trycloudflare.com`
29
+ * hostname which Caddy doesn't know.
30
+ */
31
+ const child_process_1 = require("child_process");
32
+ /**
33
+ * Match the trycloudflare URL anywhere in cloudflared's log output.
34
+ *
35
+ * cloudflared prints it in an ASCII-box on stderr (Linux/macOS) — exported
36
+ * here so tests can assert the exact pattern without spawning the binary.
37
+ */
38
+ exports.TRYCLOUDFLARE_URL_PATTERN = /https:\/\/[a-z0-9][a-z0-9-]*\.trycloudflare\.com/i;
39
+ /**
40
+ * Build the argv list for `cloudflared tunnel --url ...`. Pure helper.
41
+ *
42
+ * Why each flag:
43
+ * --url : the local upstream (Caddy's HTTPS endpoint)
44
+ * --http-host-header: tells cloudflared to rewrite Host before forwarding,
45
+ * so Caddy's vhost match works for the public URL
46
+ * --no-tls-verify : Caddy serves a locally-signed cert that cloudflared
47
+ * cannot validate from outside the local trust store;
48
+ * disabling the check is safe because the upstream
49
+ * hop never leaves localhost
50
+ */
51
+ function buildQuickTunnelArgs(opts) {
52
+ return [
53
+ 'tunnel',
54
+ '--no-autoupdate',
55
+ '--no-tls-verify',
56
+ '--http-host-header',
57
+ opts.hostHeader,
58
+ '--url',
59
+ opts.upstreamUrl,
60
+ ];
61
+ }
62
+ /** Detect whether `cloudflared` is on PATH. */
63
+ function cloudflaredAvailable() {
64
+ return __awaiter(this, void 0, void 0, function* () {
65
+ return new Promise((resolve) => {
66
+ var _a;
67
+ const child = (0, child_process_1.spawn)('cloudflared', ['--version'], { stdio: ['ignore', 'pipe', 'pipe'] });
68
+ let stdout = '';
69
+ (_a = child.stdout) === null || _a === void 0 ? void 0 : _a.on('data', (b) => (stdout += String(b)));
70
+ child.on('error', () => resolve({ installed: false }));
71
+ child.on('close', (code) => {
72
+ if (code !== 0)
73
+ return resolve({ installed: false });
74
+ const match = stdout.match(/version\s+(\S+)/i);
75
+ resolve({ binary: 'cloudflared', installed: true, version: match === null || match === void 0 ? void 0 : match[1] });
76
+ });
77
+ });
78
+ });
79
+ }
80
+ /**
81
+ * Extract the trycloudflare URL from a chunk of cloudflared output.
82
+ * Returns the first match (cloudflared logs the URL exactly once).
83
+ */
84
+ function extractTrycloudflareUrl(output) {
85
+ const match = output.match(exports.TRYCLOUDFLARE_URL_PATTERN);
86
+ return match ? match[0] : null;
87
+ }
88
+ /**
89
+ * Spawn a quick tunnel and resolve the public URL once cloudflared logs it.
90
+ *
91
+ * The returned `publicUrl` promise rejects if the child exits before
92
+ * surfacing a URL (timeout: ~30s, then cloudflared usually emits an
93
+ * error message and exits). The caller is expected to keep the
94
+ * process alive (foreground command) and `child.kill()` on Ctrl-C.
95
+ */
96
+ function spawnQuickTunnel(opts) {
97
+ const args = buildQuickTunnelArgs(opts);
98
+ const child = (0, child_process_1.spawn)('cloudflared', args, { stdio: ['ignore', 'pipe', 'pipe'] });
99
+ const publicUrl = new Promise((resolve, reject) => {
100
+ var _a, _b;
101
+ let buffer = '';
102
+ let settled = false;
103
+ const onChunk = (chunk) => {
104
+ if (settled)
105
+ return;
106
+ buffer += String(chunk);
107
+ const url = extractTrycloudflareUrl(buffer);
108
+ if (url) {
109
+ settled = true;
110
+ resolve(url);
111
+ }
112
+ };
113
+ (_a = child.stdout) === null || _a === void 0 ? void 0 : _a.on('data', onChunk);
114
+ (_b = child.stderr) === null || _b === void 0 ? void 0 : _b.on('data', onChunk);
115
+ child.on('error', (err) => {
116
+ if (settled)
117
+ return;
118
+ settled = true;
119
+ reject(err);
120
+ });
121
+ child.on('close', (code) => {
122
+ if (settled)
123
+ return;
124
+ settled = true;
125
+ reject(new Error(`cloudflared exited (code ${code}) before publishing a tunnel URL.\n${buffer.slice(-500)}`));
126
+ });
127
+ });
128
+ return { child, publicUrl };
129
+ }