@lenne.tech/cli 1.22.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.
- package/build/commands/dev/dev.js +8 -6
- package/build/commands/dev/doctor.js +41 -7
- package/build/commands/dev/install.js +107 -48
- package/build/commands/dev/status.js +1 -1
- package/build/commands/dev/test.js +1 -1
- package/build/commands/dev/tunnel.js +142 -0
- package/build/commands/dev/uninstall.js +92 -0
- package/build/commands/dev/up.js +1 -1
- package/build/lib/caddy.js +15 -1
- package/build/lib/cloudflared.js +129 -0
- package/build/lib/dev-env-bridge.js +3 -0
- package/build/lib/dev-env.js +33 -3
- package/build/lib/dev-service.js +414 -0
- package/docs/commands.md +65 -5
- package/package.json +1 -1
|
@@ -13,12 +13,14 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
13
13
|
* Local development orchestration commands (`lt dev <subcommand>`).
|
|
14
14
|
*
|
|
15
15
|
* Subcommands:
|
|
16
|
-
* - `install`
|
|
17
|
-
* - `
|
|
18
|
-
* - `
|
|
19
|
-
* - `
|
|
20
|
-
* - `
|
|
21
|
-
* - `
|
|
16
|
+
* - `install` — one-time per-machine setup: caddy + LaunchAgent/systemd unit
|
|
17
|
+
* - `uninstall` — remove the lt-dev service (symmetric to install)
|
|
18
|
+
* - `migrate` — register an existing project (idempotent ENV patches)
|
|
19
|
+
* - `up` — start API + App behind Caddy with project-specific URLs
|
|
20
|
+
* - `down` — stop the detached processes + remove the Caddy block
|
|
21
|
+
* - `status` — show what is running
|
|
22
|
+
* - `doctor` — diagnose Caddy/CA/DNS/port issues
|
|
23
|
+
* - `tunnel` — Cloudflare quick tunnel to expose a running project publicly
|
|
22
24
|
*/
|
|
23
25
|
module.exports = {
|
|
24
26
|
alias: ['d'],
|
|
@@ -12,6 +12,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
12
12
|
const child_process_1 = require("child_process");
|
|
13
13
|
const caddy_1 = require("../../lib/caddy");
|
|
14
14
|
const dev_process_1 = require("../../lib/dev-process");
|
|
15
|
+
const dev_service_1 = require("../../lib/dev-service");
|
|
15
16
|
const dev_state_1 = require("../../lib/dev-state");
|
|
16
17
|
/**
|
|
17
18
|
* Diagnose Caddy / CA / DNS / port issues for `lt dev`.
|
|
@@ -19,6 +20,9 @@ const dev_state_1 = require("../../lib/dev-state");
|
|
|
19
20
|
* Categorical output (OK / WARN / FAIL) so developers can quickly see
|
|
20
21
|
* what is missing on a fresh machine. Exit code 0 = all green,
|
|
21
22
|
* 1 = at least one FAIL.
|
|
23
|
+
*
|
|
24
|
+
* Checks our OWN LaunchAgent / systemd-user unit — not
|
|
25
|
+
* `brew services caddy`. The latter cannot host our Caddyfile.
|
|
22
26
|
*/
|
|
23
27
|
const DoctorCommand = {
|
|
24
28
|
alias: ['doc'],
|
|
@@ -36,20 +40,40 @@ const DoctorCommand = {
|
|
|
36
40
|
if (hasCaddy)
|
|
37
41
|
line('OK', colors.green, 'caddy on PATH');
|
|
38
42
|
else {
|
|
39
|
-
line('FAIL', colors.red, 'caddy not installed — run `brew install caddy`');
|
|
43
|
+
line('FAIL', colors.red, 'caddy not installed — run `brew install caddy` then `lt dev install`');
|
|
40
44
|
fails++;
|
|
41
45
|
}
|
|
42
|
-
// 2.
|
|
46
|
+
// 2. Service installed (LaunchAgent / systemd-user)
|
|
47
|
+
const plat = (0, dev_service_1.platformSupported)();
|
|
48
|
+
if (plat === 'unsupported') {
|
|
49
|
+
line('WARN', colors.yellow, `service management not supported on ${process.platform} — run caddy manually`);
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
const svc = yield (0, dev_service_1.getServiceStatus)();
|
|
53
|
+
const servicePaths = (0, dev_service_1.getServicePaths)();
|
|
54
|
+
if (svc.installed && svc.loaded) {
|
|
55
|
+
line('OK', colors.green, `lt-dev service loaded (${servicePaths.unitFile})`);
|
|
56
|
+
}
|
|
57
|
+
else if (svc.installed && !svc.loaded) {
|
|
58
|
+
line('FAIL', colors.red, `service file exists but is not loaded — run \`lt dev install\``);
|
|
59
|
+
fails++;
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
line('FAIL', colors.red, `lt-dev service not installed — run \`lt dev install\``);
|
|
63
|
+
fails++;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// 3. Caddy daemon admin endpoint
|
|
43
67
|
if (hasCaddy) {
|
|
44
68
|
const daemon = yield (0, caddy_1.caddyDaemonRunning)();
|
|
45
69
|
if (daemon)
|
|
46
|
-
line('OK', colors.green, 'caddy
|
|
70
|
+
line('OK', colors.green, 'caddy admin (:2019) reachable');
|
|
47
71
|
else {
|
|
48
|
-
line('FAIL', colors.red, 'caddy
|
|
72
|
+
line('FAIL', colors.red, 'caddy admin (:2019) unreachable — run `lt dev install`');
|
|
49
73
|
fails++;
|
|
50
74
|
}
|
|
51
75
|
}
|
|
52
|
-
//
|
|
76
|
+
// 4. Caddyfile validates
|
|
53
77
|
if (hasCaddy) {
|
|
54
78
|
const v = yield (0, caddy_1.validateCaddyfile)();
|
|
55
79
|
if (v.ok)
|
|
@@ -94,12 +118,22 @@ const DoctorCommand = {
|
|
|
94
118
|
}
|
|
95
119
|
}),
|
|
96
120
|
};
|
|
97
|
-
/**
|
|
121
|
+
/**
|
|
122
|
+
* Probe DNS — RFC 6761 mandates *.localhost MUST resolve to loopback.
|
|
123
|
+
*
|
|
124
|
+
* On macOS the resolver returns `::1` first (IPv6 loopback); on Linux
|
|
125
|
+
* `127.0.0.1` (IPv4) is more common. Both are valid loopback addresses
|
|
126
|
+
* and Caddy listens on both, so we accept either.
|
|
127
|
+
*/
|
|
98
128
|
function dnsResolvesLocalhost(host) {
|
|
99
129
|
return new Promise((resolve) => {
|
|
100
130
|
const child = (0, child_process_1.spawn)('node', [
|
|
101
131
|
'-e',
|
|
102
|
-
`require('dns').lookup(${JSON.stringify(host)}, (e,
|
|
132
|
+
`require('dns').lookup(${JSON.stringify(host)}, { all: true }, (e, addrs) => {
|
|
133
|
+
if (e) process.exit(1);
|
|
134
|
+
const loopback = (addrs || []).some(a => a.address === '127.0.0.1' || a.address === '::1');
|
|
135
|
+
process.exit(loopback ? 0 : 1);
|
|
136
|
+
});`,
|
|
103
137
|
], {
|
|
104
138
|
stdio: ['ignore', 'ignore', 'ignore'],
|
|
105
139
|
});
|
|
@@ -9,25 +9,33 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
9
9
|
});
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
-
exports.runShell = runShell;
|
|
13
|
-
const child_process_1 = require("child_process");
|
|
14
12
|
const caddy_1 = require("../../lib/caddy");
|
|
13
|
+
const dev_service_1 = require("../../lib/dev-service");
|
|
15
14
|
/**
|
|
16
15
|
* One-time per-machine setup for `lt dev`.
|
|
17
16
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
17
|
+
* Owns the full Caddy lifecycle through a dedicated LaunchAgent
|
|
18
|
+
* (macOS) / systemd-user unit (Linux). This intentionally bypasses
|
|
19
|
+
* `brew services caddy`, whose plist hardcodes
|
|
20
|
+
* `--config /opt/homebrew/etc/Caddyfile` and would crash-loop against
|
|
21
|
+
* our `~/.lenneTech/Caddyfile` location — which is the bug that
|
|
22
|
+
* blocked the first real install.
|
|
20
23
|
*
|
|
21
|
-
* Steps:
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
24
|
+
* Steps (each idempotent — safe to re-run):
|
|
25
|
+
* 1. Verify `caddy` is on PATH (suggest install instructions otherwise)
|
|
26
|
+
* 2. Ensure `~/.lenneTech/Caddyfile` exists (stub written if missing)
|
|
27
|
+
* 3. Detect conflicting `brew services caddy` and tell the user to
|
|
28
|
+
* stop it — it would fight us for ports 80/443
|
|
29
|
+
* 4. Install + bootstrap our LaunchAgent/systemd unit
|
|
30
|
+
* 5. Wait for the Caddy admin endpoint to respond
|
|
31
|
+
* 6. Validate the Caddyfile
|
|
32
|
+
* 7. Surface the `sudo -E HOME="$HOME" caddy trust` instruction for
|
|
33
|
+
* installing the local CA (HOME must survive sudo so caddy can
|
|
34
|
+
* find its CA files under the *user* profile, not /var/root)
|
|
27
35
|
*/
|
|
28
36
|
const InstallCommand = {
|
|
29
37
|
alias: ['i'],
|
|
30
|
-
description: '
|
|
38
|
+
description: 'Setup Caddy service for lt dev',
|
|
31
39
|
hidden: false,
|
|
32
40
|
name: 'install',
|
|
33
41
|
run: (toolbox) => __awaiter(void 0, void 0, void 0, function* () {
|
|
@@ -35,53 +43,95 @@ const InstallCommand = {
|
|
|
35
43
|
info('');
|
|
36
44
|
info(colors.bold('lt dev install — one-time per-machine setup'));
|
|
37
45
|
info(colors.dim('─'.repeat(60)));
|
|
46
|
+
const plat = (0, dev_service_1.platformSupported)();
|
|
47
|
+
if (plat === 'unsupported') {
|
|
48
|
+
error(`Service management is not supported on ${process.platform}. Only macOS and Linux are covered.`);
|
|
49
|
+
info(` Workaround: run \`${colors.cyan(`caddy run --config ${caddy_1.paths.caddyfile}`)}\` manually.`);
|
|
50
|
+
if (!parameters.options.fromGluegunMenu)
|
|
51
|
+
process.exit(1);
|
|
52
|
+
return 'dev install: unsupported platform';
|
|
53
|
+
}
|
|
38
54
|
let blocked = false;
|
|
39
|
-
// 1.
|
|
55
|
+
// 1. caddy on PATH
|
|
40
56
|
const hasCaddy = yield (0, caddy_1.caddyAvailable)();
|
|
41
57
|
if (hasCaddy) {
|
|
42
58
|
success('caddy is on PATH');
|
|
43
59
|
}
|
|
44
60
|
else {
|
|
45
61
|
warning('caddy is not installed.');
|
|
46
|
-
info(` →
|
|
47
|
-
info(` → Linux:
|
|
62
|
+
info(` → macOS: ${colors.cyan('brew install caddy')}`);
|
|
63
|
+
info(` → Linux: ${colors.cyan('https://caddyserver.com/docs/install')}`);
|
|
64
|
+
info(' (Do NOT start it via `brew services` — `lt dev install` runs its own service.)');
|
|
48
65
|
blocked = true;
|
|
49
66
|
}
|
|
50
67
|
// 2. Caddyfile stub
|
|
51
68
|
(0, caddy_1.writeCaddyfile)('# lt dev — managed Caddyfile\n# Add per-project blocks via `lt dev up`.\n');
|
|
52
69
|
success(`Caddyfile present at ${caddy_1.paths.caddyfile}`);
|
|
53
70
|
if (!hasCaddy) {
|
|
54
|
-
|
|
71
|
+
info('');
|
|
72
|
+
error('Cannot continue setup until Caddy is installed. Re-run `lt dev install` afterwards.');
|
|
55
73
|
if (!parameters.options.fromGluegunMenu)
|
|
56
74
|
process.exit(blocked ? 1 : 0);
|
|
57
75
|
return 'dev install: caddy missing';
|
|
58
76
|
}
|
|
59
|
-
// 3.
|
|
60
|
-
const
|
|
61
|
-
if (
|
|
62
|
-
|
|
77
|
+
// 3. brew services conflict warning
|
|
78
|
+
const brewConflict = yield detectBrewCaddyConflict();
|
|
79
|
+
if (brewConflict) {
|
|
80
|
+
warning('A `brew services caddy` instance is registered.');
|
|
81
|
+
info(` Stop it (it crash-loops against our Caddyfile): ${colors.cyan('brew services stop caddy')}`);
|
|
82
|
+
info(' `lt dev install` runs its own service — the brew one is no longer needed.');
|
|
63
83
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
84
|
+
// 4. Install our LaunchAgent / systemd unit
|
|
85
|
+
const paths = (0, dev_service_1.getServicePaths)();
|
|
86
|
+
info('');
|
|
87
|
+
info(`Installing ${plat === 'darwin' ? 'LaunchAgent' : 'systemd-user unit'} at:`);
|
|
88
|
+
info(colors.dim(` ${paths.unitFile}`));
|
|
89
|
+
const installResult = yield (0, dev_service_1.installService)();
|
|
90
|
+
if (!installResult.ok) {
|
|
91
|
+
error(installResult.message);
|
|
68
92
|
blocked = true;
|
|
69
93
|
}
|
|
70
|
-
// 4. Validate Caddyfile (catches config issues early)
|
|
71
|
-
const validation = yield (0, caddy_1.validateCaddyfile)();
|
|
72
|
-
if (validation.ok) {
|
|
73
|
-
success('Caddyfile validates');
|
|
74
|
-
}
|
|
75
94
|
else {
|
|
76
|
-
|
|
77
|
-
|
|
95
|
+
if (installResult.created)
|
|
96
|
+
success(installResult.message);
|
|
97
|
+
else
|
|
98
|
+
info(colors.dim(installResult.message));
|
|
78
99
|
}
|
|
79
|
-
// 5.
|
|
100
|
+
// 5. Wait for admin endpoint
|
|
101
|
+
if (installResult.ok) {
|
|
102
|
+
info(colors.dim('Waiting for Caddy admin endpoint (:2019) ...'));
|
|
103
|
+
const ready = yield (0, dev_service_1.waitForServiceReady)(8000);
|
|
104
|
+
const status = yield (0, dev_service_1.getServiceStatus)();
|
|
105
|
+
if (ready && status.daemonReachable) {
|
|
106
|
+
success(`Caddy daemon ready${status.pid ? ` (pid ${status.pid})` : ''}.`);
|
|
107
|
+
}
|
|
108
|
+
else if (status.loaded && !status.daemonReachable) {
|
|
109
|
+
warning('Service is loaded but admin endpoint did not respond within 8s.');
|
|
110
|
+
info(colors.dim(` Logs: ${paths.logFile} / ${paths.errFile}`));
|
|
111
|
+
blocked = true;
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
warning('Caddy daemon did not start. See logs:');
|
|
115
|
+
info(colors.dim(` ${paths.logFile}`));
|
|
116
|
+
info(colors.dim(` ${paths.errFile}`));
|
|
117
|
+
blocked = true;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// 6. Validate Caddyfile
|
|
121
|
+
if (installResult.ok) {
|
|
122
|
+
const validation = yield (0, caddy_1.validateCaddyfile)();
|
|
123
|
+
if (validation.ok)
|
|
124
|
+
success('Caddyfile validates');
|
|
125
|
+
else
|
|
126
|
+
warning(`Caddyfile validation: ${validation.stderr.split('\n').slice(0, 2).join(' / ')}`);
|
|
127
|
+
}
|
|
128
|
+
// 7. CA trust
|
|
80
129
|
info('');
|
|
81
|
-
info(colors.bold('Local CA'));
|
|
82
|
-
info(' Caddy
|
|
83
|
-
info('
|
|
84
|
-
info(
|
|
130
|
+
info(colors.bold('Local CA trust'));
|
|
131
|
+
info(' Caddy creates its local CA on first run. To trust it system-wide,');
|
|
132
|
+
info(' run this once (HOME must be preserved so sudo keeps the user-scoped');
|
|
133
|
+
info(' CA, otherwise caddy looks in /var/root and fails):');
|
|
134
|
+
info(` ${colors.cyan('sudo -E HOME="$HOME" caddy trust')}`);
|
|
85
135
|
info(` Browsers will then accept ${colors.cyan('https://*.localhost')} without warnings.`);
|
|
86
136
|
info('');
|
|
87
137
|
if (blocked) {
|
|
@@ -95,19 +145,28 @@ const InstallCommand = {
|
|
|
95
145
|
return blocked ? 'dev install: incomplete' : 'dev install: ok';
|
|
96
146
|
}),
|
|
97
147
|
};
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
148
|
+
/**
|
|
149
|
+
* Quick `brew services list` scan for a registered caddy service.
|
|
150
|
+
* Returns true on macOS if any entry contains "caddy" — error/started
|
|
151
|
+
* alike, both are conflicts. Always returns false on non-darwin or
|
|
152
|
+
* when `brew` is unavailable (no false positives).
|
|
153
|
+
*/
|
|
154
|
+
function detectBrewCaddyConflict() {
|
|
155
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
156
|
+
if (process.platform !== 'darwin')
|
|
157
|
+
return false;
|
|
158
|
+
return new Promise((resolve) => {
|
|
159
|
+
var _a;
|
|
160
|
+
const { spawn } = require('child_process');
|
|
161
|
+
const child = spawn('brew', ['services', 'list'], { stdio: ['ignore', 'pipe', 'ignore'] });
|
|
162
|
+
let out = '';
|
|
163
|
+
(_a = child.stdout) === null || _a === void 0 ? void 0 : _a.on('data', (b) => (out += String(b)));
|
|
164
|
+
child.on('error', () => resolve(false));
|
|
165
|
+
child.on('close', () => {
|
|
166
|
+
const conflict = /\bcaddy\b/.test(out) && !/^caddy\s+none\b/m.test(out);
|
|
167
|
+
resolve(conflict);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
111
170
|
});
|
|
112
171
|
}
|
|
113
172
|
module.exports = InstallCommand;
|
|
@@ -115,7 +115,7 @@ const StatusCommand = {
|
|
|
115
115
|
warning(' Caddy not installed — run `lt dev install` first.');
|
|
116
116
|
}
|
|
117
117
|
else if (!daemonOk) {
|
|
118
|
-
warning(' Caddy daemon not running — `
|
|
118
|
+
warning(' Caddy daemon not running — run `lt dev install` to (re)start the lt-dev service.');
|
|
119
119
|
}
|
|
120
120
|
else {
|
|
121
121
|
info(colors.dim(' Caddy: ready'));
|
|
@@ -77,7 +77,7 @@ const TestCommand = {
|
|
|
77
77
|
return 'dev test: caddy missing';
|
|
78
78
|
}
|
|
79
79
|
if (!(yield (0, caddy_1.caddyDaemonRunning)())) {
|
|
80
|
-
error('caddy daemon not running. `
|
|
80
|
+
error('caddy daemon not running. Run `lt dev install` first.');
|
|
81
81
|
if (!parameters.options.fromGluegunMenu)
|
|
82
82
|
process.exit(1);
|
|
83
83
|
return 'dev test: caddy daemon down';
|
|
@@ -0,0 +1,142 @@
|
|
|
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 caddy_1 = require("../../lib/caddy");
|
|
13
|
+
const cloudflared_1 = require("../../lib/cloudflared");
|
|
14
|
+
const dev_identity_1 = require("../../lib/dev-identity");
|
|
15
|
+
const dev_project_1 = require("../../lib/dev-project");
|
|
16
|
+
const dev_state_1 = require("../../lib/dev-state");
|
|
17
|
+
/**
|
|
18
|
+
* Expose a running `lt dev up` project to the public internet via a
|
|
19
|
+
* Cloudflare Quick Tunnel.
|
|
20
|
+
*
|
|
21
|
+
* Quick tunnel = no Cloudflare account, ephemeral `*.trycloudflare.com`
|
|
22
|
+
* URL, runs in the foreground until Ctrl-C. Designed for ad-hoc work:
|
|
23
|
+
* - mobile / tablet preview from outside the LAN
|
|
24
|
+
* - sharing a feature with a teammate during review
|
|
25
|
+
* - landing webhooks from external services
|
|
26
|
+
*
|
|
27
|
+
* Caveats (printed at runtime):
|
|
28
|
+
* - Auth cookies set on `<slug>.localhost` are NOT valid on the
|
|
29
|
+
* `*.trycloudflare.com` domain — log in again on the public URL.
|
|
30
|
+
* - The default tunnel exposes ONLY the App subdomain (the API stays
|
|
31
|
+
* on `*.localhost`). Use `--api` to tunnel the API instead, or
|
|
32
|
+
* start a second `lt dev tunnel --api` in parallel.
|
|
33
|
+
* - Better-Auth's `trustedOrigins` won't include the random tunnel
|
|
34
|
+
* URL — login flows that validate the origin will reject the
|
|
35
|
+
* request unless you add the URL explicitly to your API config.
|
|
36
|
+
*
|
|
37
|
+
* Not yet supported (deliberate, separate command if needed later):
|
|
38
|
+
* - named tunnels with persistent URL (`cloudflared tunnel create`)
|
|
39
|
+
* - parallel multi-host tunnels in one process
|
|
40
|
+
* - background/detached mode (use a separate shell for now)
|
|
41
|
+
*/
|
|
42
|
+
const TunnelCommand = {
|
|
43
|
+
alias: ['tun'],
|
|
44
|
+
description: 'Cloudflare quick-tunnel to a running app',
|
|
45
|
+
hidden: false,
|
|
46
|
+
name: 'tunnel',
|
|
47
|
+
run: (toolbox) => __awaiter(void 0, void 0, void 0, function* () {
|
|
48
|
+
var _a, _b;
|
|
49
|
+
const { filesystem, parameters, print: { colors, error, info, success, warning }, } = toolbox;
|
|
50
|
+
const layout = (0, dev_project_1.resolveLayout)(filesystem.cwd(), filesystem);
|
|
51
|
+
if (!layout.apiDir && !layout.appDir) {
|
|
52
|
+
error('No API or App project detected at this path. Run `lt dev migrate` first.');
|
|
53
|
+
if (!parameters.options.fromGluegunMenu)
|
|
54
|
+
process.exit(1);
|
|
55
|
+
return 'dev tunnel: not a project';
|
|
56
|
+
}
|
|
57
|
+
const apiMode = Boolean(parameters.options.api);
|
|
58
|
+
const identity = (0, dev_identity_1.buildIdentity)(layout.root);
|
|
59
|
+
const targetSub = apiMode ? identity.subdomains.api : identity.subdomains.app;
|
|
60
|
+
if (!targetSub) {
|
|
61
|
+
error(`No ${apiMode ? 'API' : 'App'} subdomain configured for "${identity.slug}".`);
|
|
62
|
+
if (!parameters.options.fromGluegunMenu)
|
|
63
|
+
process.exit(1);
|
|
64
|
+
return 'dev tunnel: no target';
|
|
65
|
+
}
|
|
66
|
+
// Pre-flight: cloudflared installed
|
|
67
|
+
const available = yield (0, cloudflared_1.cloudflaredAvailable)();
|
|
68
|
+
if (!available.installed) {
|
|
69
|
+
error('cloudflared is not installed.');
|
|
70
|
+
info(` → ${colors.cyan('brew install cloudflared')} (macOS)`);
|
|
71
|
+
info(` → ${colors.cyan('https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/')} (Linux/Windows)`);
|
|
72
|
+
if (!parameters.options.fromGluegunMenu)
|
|
73
|
+
process.exit(1);
|
|
74
|
+
return 'dev tunnel: cloudflared missing';
|
|
75
|
+
}
|
|
76
|
+
// Pre-flight: Caddy must be up — without it cloudflared would forward
|
|
77
|
+
// to a dead upstream and the public URL would 502.
|
|
78
|
+
if (!(yield (0, caddy_1.caddyDaemonRunning)())) {
|
|
79
|
+
error('Caddy daemon is not running — run `lt dev install` first.');
|
|
80
|
+
if (!parameters.options.fromGluegunMenu)
|
|
81
|
+
process.exit(1);
|
|
82
|
+
return 'dev tunnel: caddy down';
|
|
83
|
+
}
|
|
84
|
+
const registry = (0, dev_state_1.loadRegistry)();
|
|
85
|
+
if (!registry.projects[identity.slug]) {
|
|
86
|
+
warning(`Project "${identity.slug}" is not in the lt-dev registry. Run \`lt dev up\` first.`);
|
|
87
|
+
info(' (Continuing anyway — Caddy may have a stale block.)');
|
|
88
|
+
}
|
|
89
|
+
const upstreamUrl = `https://${targetSub.hostname}`;
|
|
90
|
+
info('');
|
|
91
|
+
info(colors.bold(`lt dev tunnel — Cloudflare Quick Tunnel`));
|
|
92
|
+
info(colors.dim('─'.repeat(60)));
|
|
93
|
+
info(` Upstream: ${colors.cyan(upstreamUrl)}`);
|
|
94
|
+
info(colors.dim(` cloudflared ${available.version || 'unknown'}`));
|
|
95
|
+
info('');
|
|
96
|
+
info(colors.dim('Starting tunnel — this typically takes 5-10 seconds ...'));
|
|
97
|
+
const tunnel = (0, cloudflared_1.spawnQuickTunnel)({ hostHeader: targetSub.hostname, upstreamUrl });
|
|
98
|
+
// Wire stderr through so users see cloudflared progress / errors.
|
|
99
|
+
(_a = tunnel.child.stderr) === null || _a === void 0 ? void 0 : _a.on('data', (chunk) => {
|
|
100
|
+
process.stderr.write(String(chunk));
|
|
101
|
+
});
|
|
102
|
+
let publicUrl;
|
|
103
|
+
try {
|
|
104
|
+
publicUrl = yield tunnel.publicUrl;
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
error(`Tunnel failed to start: ${err.message}`);
|
|
108
|
+
if (!parameters.options.fromGluegunMenu)
|
|
109
|
+
process.exit(1);
|
|
110
|
+
return 'dev tunnel: failed';
|
|
111
|
+
}
|
|
112
|
+
info('');
|
|
113
|
+
success(`Public URL: ${colors.cyan(publicUrl)}`);
|
|
114
|
+
info('');
|
|
115
|
+
info(colors.bold('Heads up:'));
|
|
116
|
+
info(' • Auth cookies set on the localhost domain are NOT valid on the tunnel URL.');
|
|
117
|
+
info(` • Better-Auth's trustedOrigins must include ${colors.cyan(publicUrl)} for login to succeed.`);
|
|
118
|
+
if (!apiMode) {
|
|
119
|
+
info(` • This tunnel exposes ONLY the App. The API stays on \`${((_b = identity.subdomains.api) === null || _b === void 0 ? void 0 : _b.hostname) || '—'}\`.`);
|
|
120
|
+
info(' For full external usage, start a second `lt dev tunnel --api` in another shell.');
|
|
121
|
+
}
|
|
122
|
+
info('');
|
|
123
|
+
info(colors.dim('Stop with Ctrl-C.'));
|
|
124
|
+
// Keep the foreground alive until the child exits (Ctrl-C → SIGINT
|
|
125
|
+
// is forwarded to the child by the terminal, child exits, we exit).
|
|
126
|
+
const exitCode = yield new Promise((resolve) => {
|
|
127
|
+
tunnel.child.on('close', resolve);
|
|
128
|
+
const forward = (sig) => {
|
|
129
|
+
if (!tunnel.child.killed)
|
|
130
|
+
tunnel.child.kill(sig);
|
|
131
|
+
};
|
|
132
|
+
process.on('SIGINT', forward);
|
|
133
|
+
process.on('SIGTERM', forward);
|
|
134
|
+
});
|
|
135
|
+
info('');
|
|
136
|
+
info(colors.dim(`cloudflared exited (code ${exitCode !== null && exitCode !== void 0 ? exitCode : 'unknown'}).`));
|
|
137
|
+
if (!parameters.options.fromGluegunMenu)
|
|
138
|
+
process.exit(exitCode !== null && exitCode !== void 0 ? exitCode : 0);
|
|
139
|
+
return `dev tunnel: exit=${exitCode}`;
|
|
140
|
+
}),
|
|
141
|
+
};
|
|
142
|
+
module.exports = TunnelCommand;
|
|
@@ -0,0 +1,92 @@
|
|
|
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_service_1 = require("../../lib/dev-service");
|
|
16
|
+
/**
|
|
17
|
+
* Symmetric counterpart to `lt dev install`.
|
|
18
|
+
*
|
|
19
|
+
* Removes the LaunchAgent / systemd-user unit, stops the running Caddy
|
|
20
|
+
* daemon, and optionally purges all lt-dev state (Caddyfile, logs).
|
|
21
|
+
*
|
|
22
|
+
* What it does NOT touch:
|
|
23
|
+
* - the caddy binary itself (`brew uninstall caddy` remains the
|
|
24
|
+
* user's choice — we don't presume they want to drop the tool)
|
|
25
|
+
* - per-project state under `<project>/.lt-dev/` (use `lt dev down`)
|
|
26
|
+
* - the trusted CA in the system keychain (use
|
|
27
|
+
* `sudo -E HOME="$HOME" caddy untrust` if desired)
|
|
28
|
+
*
|
|
29
|
+
* Flags:
|
|
30
|
+
* --purge — also remove ~/.lenneTech/Caddyfile and caddy logs
|
|
31
|
+
* --noConfirm — skip the purge confirmation prompt
|
|
32
|
+
*/
|
|
33
|
+
const UninstallCommand = {
|
|
34
|
+
alias: ['un'],
|
|
35
|
+
description: 'Remove lt dev Caddy service',
|
|
36
|
+
hidden: false,
|
|
37
|
+
name: 'uninstall',
|
|
38
|
+
run: (toolbox) => __awaiter(void 0, void 0, void 0, function* () {
|
|
39
|
+
const { parameters, print: { colors, error, info, success, warning }, prompt: { confirm }, } = toolbox;
|
|
40
|
+
info('');
|
|
41
|
+
info(colors.bold('lt dev uninstall — remove the lt-dev Caddy service'));
|
|
42
|
+
info(colors.dim('─'.repeat(60)));
|
|
43
|
+
const plat = (0, dev_service_1.platformSupported)();
|
|
44
|
+
if (plat === 'unsupported') {
|
|
45
|
+
info('No managed service to remove on this platform.');
|
|
46
|
+
if (!parameters.options.fromGluegunMenu)
|
|
47
|
+
process.exit(0);
|
|
48
|
+
return 'dev uninstall: nothing to do';
|
|
49
|
+
}
|
|
50
|
+
const paths = (0, dev_service_1.getServicePaths)();
|
|
51
|
+
const result = yield (0, dev_service_1.uninstallService)();
|
|
52
|
+
if (!result.ok) {
|
|
53
|
+
error(result.message);
|
|
54
|
+
if (!parameters.options.fromGluegunMenu)
|
|
55
|
+
process.exit(1);
|
|
56
|
+
return 'dev uninstall: failed';
|
|
57
|
+
}
|
|
58
|
+
if (result.removed.length === 0) {
|
|
59
|
+
info(colors.dim('Service was not installed.'));
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
success(`Removed: ${result.removed.join(', ')}`);
|
|
63
|
+
}
|
|
64
|
+
// Optional purge of related state files.
|
|
65
|
+
const purge = Boolean(parameters.options.purge) ||
|
|
66
|
+
(!parameters.options.noConfirm && (yield confirm('Also remove the Caddyfile and lt-dev caddy logs?', false)));
|
|
67
|
+
if (purge) {
|
|
68
|
+
const toRemove = [caddy_1.paths.caddyfile, paths.logFile, paths.errFile, (0, path_1.join)(paths.logFile, '..', 'ports.json')];
|
|
69
|
+
for (const file of toRemove) {
|
|
70
|
+
if ((0, fs_1.existsSync)(file)) {
|
|
71
|
+
try {
|
|
72
|
+
(0, fs_1.unlinkSync)(file);
|
|
73
|
+
success(`Removed ${file}`);
|
|
74
|
+
}
|
|
75
|
+
catch (e) {
|
|
76
|
+
warning(`Failed to remove ${file}: ${e.message}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
info(colors.dim(`Kept: ${caddy_1.paths.caddyfile}`));
|
|
83
|
+
info(colors.dim(`Kept: ${paths.logFile}`));
|
|
84
|
+
}
|
|
85
|
+
info('');
|
|
86
|
+
info(`To reinstall: ${colors.cyan('lt dev install')}. To also remove caddy itself: ${colors.cyan('brew uninstall caddy')}.`);
|
|
87
|
+
if (!parameters.options.fromGluegunMenu)
|
|
88
|
+
process.exit(0);
|
|
89
|
+
return 'dev uninstall: ok';
|
|
90
|
+
}),
|
|
91
|
+
};
|
|
92
|
+
module.exports = UninstallCommand;
|
package/build/commands/dev/up.js
CHANGED
|
@@ -63,7 +63,7 @@ const UpCommand = {
|
|
|
63
63
|
return 'dev up: caddy missing';
|
|
64
64
|
}
|
|
65
65
|
if (!(yield (0, caddy_1.caddyDaemonRunning)())) {
|
|
66
|
-
error('caddy daemon is not running.
|
|
66
|
+
error('caddy daemon is not running. Run `lt dev install` to start the lt-dev service.');
|
|
67
67
|
if (!parameters.options.fromGluegunMenu)
|
|
68
68
|
process.exit(1);
|
|
69
69
|
return 'dev up: caddy daemon down';
|
package/build/lib/caddy.js
CHANGED
|
@@ -94,7 +94,21 @@ function removeProjectBlock(slug) {
|
|
|
94
94
|
writeCaddyfile(next);
|
|
95
95
|
return true;
|
|
96
96
|
}
|
|
97
|
-
/**
|
|
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
|
+
*/
|
|
98
112
|
function renderProjectBlock(slug, routes) {
|
|
99
113
|
const lines = [`# >>> lt-dev:${slug} >>>`];
|
|
100
114
|
for (const route of routes) {
|