@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.
- package/README.md +16 -10
- package/build/commands/{local/local.js → dev/dev.js} +14 -10
- package/build/commands/dev/doctor.js +144 -0
- package/build/commands/dev/down.js +76 -0
- package/build/commands/dev/install.js +172 -0
- package/build/commands/dev/migrate.js +96 -0
- package/build/commands/dev/status.js +153 -0
- package/build/commands/dev/test.js +191 -0
- package/build/commands/dev/tunnel.js +142 -0
- package/build/commands/dev/uninstall.js +92 -0
- package/build/commands/dev/up.js +228 -0
- package/build/commands/fullstack/init.js +36 -2
- package/build/commands/status.js +16 -16
- package/build/lib/caddy.js +183 -0
- package/build/lib/cloudflared.js +129 -0
- package/build/lib/dev-env-bridge.js +112 -0
- package/build/lib/dev-env.js +65 -0
- package/build/lib/dev-identity.js +100 -0
- package/build/lib/dev-migrate-helper.js +81 -0
- package/build/lib/dev-patches.js +190 -0
- package/build/lib/dev-process.js +152 -0
- package/build/lib/{local-project.js → dev-project.js} +23 -38
- package/build/lib/dev-service.js +414 -0
- package/build/lib/dev-state.js +152 -0
- package/docs/commands.md +167 -94
- package/package.json +1 -1
- package/build/commands/local/down.js +0 -71
- package/build/commands/local/init.js +0 -162
- package/build/commands/local/status.js +0 -69
- package/build/commands/local/up.js +0 -148
- package/build/commands/ports/ports.js +0 -118
- package/build/commands/ports/scan.js +0 -131
- package/build/lib/local-patches.js +0 -175
- package/build/lib/port-registry.js +0 -304
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.clearEnvBridge = clearEnvBridge;
|
|
4
|
+
exports.detectCaddyRootCa = detectCaddyRootCa;
|
|
5
|
+
exports.envBridgePath = envBridgePath;
|
|
6
|
+
exports.writeEnvBridge = writeEnvBridge;
|
|
7
|
+
/**
|
|
8
|
+
* ENV bridge file at `<root>/.lt-dev/.env` — written by `lt dev up`,
|
|
9
|
+
* removed by `lt dev down`. External tools (Playwright, custom scripts,
|
|
10
|
+
* IDE test runners) load this file via dotenv to pick up the URLs and
|
|
11
|
+
* the Caddy root CA path without depending on the parent shell.
|
|
12
|
+
*
|
|
13
|
+
* Why a file (not just inherited env): the typical workflow is
|
|
14
|
+
* 1. `lt dev up` (in shell A)
|
|
15
|
+
* 2. `pnpm test:e2e` (in shell B, IDE, or VS Code task)
|
|
16
|
+
* Shell B does not inherit shell A's exports. Reading a file solves
|
|
17
|
+
* this without polluting global state.
|
|
18
|
+
*
|
|
19
|
+
* The file is gitignored via `.lt-dev/`. It contains only public URLs +
|
|
20
|
+
* the local CA path — no secrets. Format: standard dotenv KEY=VALUE.
|
|
21
|
+
*/
|
|
22
|
+
const fs_1 = require("fs");
|
|
23
|
+
const os_1 = require("os");
|
|
24
|
+
const path_1 = require("path");
|
|
25
|
+
const HEADER = `# Managed by \`lt dev up\` — do NOT edit, will be overwritten.\n# Removed by \`lt dev down\`. Loaded by Playwright + other tools.\n`;
|
|
26
|
+
/** Remove the ENV bridge file. No-op if missing. */
|
|
27
|
+
function clearEnvBridge(projectRoot) {
|
|
28
|
+
const file = envBridgePath(projectRoot);
|
|
29
|
+
if (!(0, fs_1.existsSync)(file))
|
|
30
|
+
return false;
|
|
31
|
+
try {
|
|
32
|
+
(0, fs_1.rmSync)(file);
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
catch (_a) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Detect the local Caddy root CA certificate path.
|
|
41
|
+
*
|
|
42
|
+
* Caddy stores its locally-signed root CA at a platform-specific
|
|
43
|
+
* data dir. We probe the common locations first; if none exists,
|
|
44
|
+
* we fall back to `caddy environ` (slow but authoritative).
|
|
45
|
+
*/
|
|
46
|
+
function detectCaddyRootCa() {
|
|
47
|
+
const candidates = [];
|
|
48
|
+
if ((0, os_1.platform)() === 'darwin') {
|
|
49
|
+
candidates.push((0, path_1.join)((0, os_1.homedir)(), 'Library/Application Support/Caddy/pki/authorities/local/root.crt'));
|
|
50
|
+
}
|
|
51
|
+
// Linux + fallback
|
|
52
|
+
candidates.push((0, path_1.join)((0, os_1.homedir)(), '.local/share/caddy/pki/authorities/local/root.crt'));
|
|
53
|
+
// XDG_DATA_HOME override
|
|
54
|
+
if (process.env.XDG_DATA_HOME) {
|
|
55
|
+
candidates.push((0, path_1.join)(process.env.XDG_DATA_HOME, 'caddy/pki/authorities/local/root.crt'));
|
|
56
|
+
}
|
|
57
|
+
for (const c of candidates) {
|
|
58
|
+
if ((0, fs_1.existsSync)(c))
|
|
59
|
+
return c;
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
/** Resolve the path to the ENV bridge file for a project. */
|
|
64
|
+
function envBridgePath(projectRoot) {
|
|
65
|
+
return (0, path_1.join)(projectRoot, '.lt-dev', '.env');
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Write the ENV bridge file. Idempotent — same content = no rewrite.
|
|
69
|
+
*
|
|
70
|
+
* Returns the absolute path that was written.
|
|
71
|
+
*/
|
|
72
|
+
function writeEnvBridge(projectRoot, devEnv, dbName) {
|
|
73
|
+
const file = envBridgePath(projectRoot);
|
|
74
|
+
const lines = [];
|
|
75
|
+
// The App-side env is the more "external" one (Playwright, browser tools).
|
|
76
|
+
// We expose every URL/storage/api key the App env carries.
|
|
77
|
+
const exported = [
|
|
78
|
+
'BASE_URL',
|
|
79
|
+
'APP_URL',
|
|
80
|
+
'NSC__BASE_URL',
|
|
81
|
+
'NSC__APP_URL',
|
|
82
|
+
'NUXT_API_URL',
|
|
83
|
+
'NUXT_PUBLIC_API_URL',
|
|
84
|
+
'NUXT_PUBLIC_SITE_URL',
|
|
85
|
+
'NUXT_PUBLIC_STORAGE_PREFIX',
|
|
86
|
+
'NUXT_PUBLIC_API_PROXY',
|
|
87
|
+
'NSC__MONGOOSE__URI',
|
|
88
|
+
'DATABASE_URL',
|
|
89
|
+
// Legacy aliases — see dev-env.ts for the rationale.
|
|
90
|
+
'API_URL',
|
|
91
|
+
'SITE_URL',
|
|
92
|
+
];
|
|
93
|
+
for (const key of exported) {
|
|
94
|
+
const v = devEnv.app.env[key];
|
|
95
|
+
if (v !== undefined && v !== '')
|
|
96
|
+
lines.push(`${String(key)}=${v}`);
|
|
97
|
+
}
|
|
98
|
+
// Marker so consumers can detect "lt dev mode" reliably.
|
|
99
|
+
lines.push(`LT_DEV_ACTIVE=true`);
|
|
100
|
+
if (dbName)
|
|
101
|
+
lines.push(`LT_DEV_DB_NAME=${dbName}`);
|
|
102
|
+
// Caddy root CA — Playwright/Chromium use NODE_EXTRA_CA_CERTS to trust it.
|
|
103
|
+
const caPath = detectCaddyRootCa();
|
|
104
|
+
if (caPath)
|
|
105
|
+
lines.push(`NODE_EXTRA_CA_CERTS=${caPath}`);
|
|
106
|
+
const content = `${HEADER}${lines.join('\n')}\n`;
|
|
107
|
+
if ((0, fs_1.existsSync)(file) && (0, fs_1.readFileSync)(file, 'utf8') === content)
|
|
108
|
+
return file;
|
|
109
|
+
(0, fs_1.mkdirSync)((0, path_1.dirname)(file), { recursive: true });
|
|
110
|
+
(0, fs_1.writeFileSync)(file, content, 'utf8');
|
|
111
|
+
return file;
|
|
112
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildDevEnv = buildDevEnv;
|
|
4
|
+
/**
|
|
5
|
+
* Build environment variables for `lt dev up`.
|
|
6
|
+
*
|
|
7
|
+
* URL-first: API and App processes receive complete URLs (not just ports)
|
|
8
|
+
* so they can configure CORS, BetterAuth trusted origins, OpenAPI servers,
|
|
9
|
+
* Vite proxies and storage prefixes consistently — without ever needing
|
|
10
|
+
* to know which internal port Caddy proxies them to.
|
|
11
|
+
*
|
|
12
|
+
* Cross-wiring protection:
|
|
13
|
+
* - `BASE_URL`/`APP_URL` lock the API to its own App origin (CORS + BetterAuth)
|
|
14
|
+
* - `NUXT_PUBLIC_*` lock the App to its own API
|
|
15
|
+
* - `NUXT_PUBLIC_STORAGE_PREFIX` namespaces localStorage/sessionStorage
|
|
16
|
+
* - `NSC__MONGOOSE__URI` / `DATABASE_URL` namespace the database per project
|
|
17
|
+
*
|
|
18
|
+
* CA trust for SSR fetches:
|
|
19
|
+
* - Both API and App receive `NODE_EXTRA_CA_CERTS` pointing at the
|
|
20
|
+
* Caddy local root CA so server-side fetches between the two
|
|
21
|
+
* subdomains succeed. Without this Nuxt SSR fails with "unable to
|
|
22
|
+
* get local issuer certificate" when the app calls its own API.
|
|
23
|
+
*/
|
|
24
|
+
const dev_env_bridge_1 = require("./dev-env-bridge");
|
|
25
|
+
/**
|
|
26
|
+
* Build the environment maps for both API and App processes.
|
|
27
|
+
*
|
|
28
|
+
* Both processes inherit `baseEnv` (typically `process.env`) so user-set
|
|
29
|
+
* vars survive. `lt dev`-managed keys win on top.
|
|
30
|
+
*/
|
|
31
|
+
function buildDevEnv(input) {
|
|
32
|
+
const { apiInternalPort, appInternalPort, baseEnv = {}, dbName, identity } = input;
|
|
33
|
+
const apiSub = identity.subdomains.api;
|
|
34
|
+
const appSub = identity.subdomains.app;
|
|
35
|
+
const apiUrl = apiSub ? `https://${apiSub.hostname}` : '';
|
|
36
|
+
const appUrl = appSub ? `https://${appSub.hostname}` : '';
|
|
37
|
+
const caPath = (0, dev_env_bridge_1.detectCaddyRootCa)();
|
|
38
|
+
const sharedKeys = Object.assign(Object.assign(Object.assign(Object.assign({}, (apiUrl ? { BASE_URL: apiUrl, NSC__BASE_URL: apiUrl } : {})), (appUrl ? { APP_URL: appUrl, NSC__APP_URL: appUrl } : {})), (dbName ? { DATABASE_URL: buildPostgresUrl(dbName), NSC__MONGOOSE__URI: `mongodb://127.0.0.1/${dbName}` } : {})), (caPath ? { NODE_EXTRA_CA_CERTS: caPath } : {}));
|
|
39
|
+
return {
|
|
40
|
+
api: {
|
|
41
|
+
env: Object.assign(Object.assign(Object.assign({}, baseEnv), sharedKeys), {
|
|
42
|
+
// Force IPv4 loopback binding so Caddy's `127.0.0.1` upstream
|
|
43
|
+
// (see `caddy.ts#renderProjectBlock`) always matches the
|
|
44
|
+
// listener. Without this, Nuxt + Nest sometimes bind to
|
|
45
|
+
// `[::1]` only, and Caddy gets connection-refused on IPv4.
|
|
46
|
+
HOST: '127.0.0.1', NITRO_HOST: '127.0.0.1', PORT: String(apiInternalPort) }),
|
|
47
|
+
internalPort: apiInternalPort,
|
|
48
|
+
},
|
|
49
|
+
app: {
|
|
50
|
+
env: Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, baseEnv), sharedKeys), {
|
|
51
|
+
// See API note above: pin the dev server to IPv4 so Caddy's
|
|
52
|
+
// `127.0.0.1` upstream is unambiguous.
|
|
53
|
+
HOST: '127.0.0.1', NITRO_HOST: '127.0.0.1' }), (apiUrl ? { API_URL: apiUrl, NUXT_API_URL: apiUrl, NUXT_PUBLIC_API_URL: apiUrl } : {})), (appUrl ? { NUXT_PUBLIC_SITE_URL: appUrl, SITE_URL: appUrl } : {})), {
|
|
54
|
+
// Vite-API-Proxy is OFF by default in lt dev mode — Caddy serves
|
|
55
|
+
// both subdomains under HTTPS with shared cookie domain, so
|
|
56
|
+
// same-origin trickery is no longer required.
|
|
57
|
+
NUXT_PUBLIC_API_PROXY: 'false', NUXT_PUBLIC_STORAGE_PREFIX: identity.slug, PORT: String(appInternalPort) }),
|
|
58
|
+
internalPort: appInternalPort,
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/** Postgres convenience URL — used by Postgres-based projects (e.g. nest-base). */
|
|
63
|
+
function buildPostgresUrl(dbName) {
|
|
64
|
+
return `postgresql://${dbName}:${dbName}@localhost:5432/${dbName}`;
|
|
65
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildIdentity = buildIdentity;
|
|
4
|
+
exports.projectSlug = projectSlug;
|
|
5
|
+
exports.slugify = slugify;
|
|
6
|
+
/**
|
|
7
|
+
* Project identity for `lt dev`.
|
|
8
|
+
*
|
|
9
|
+
* URL-first: every project has a slug derived from its package.json
|
|
10
|
+
* "name", and a deterministic set of subdomains under `*.localhost`.
|
|
11
|
+
*
|
|
12
|
+
* Convention:
|
|
13
|
+
* - `<slug>.localhost` → primary App
|
|
14
|
+
* - `api.<slug>.localhost` → API
|
|
15
|
+
* - `<other>.<slug>.localhost` → optional additional services
|
|
16
|
+
*
|
|
17
|
+
* The internal port behind each subdomain is opaque — Caddy proxies
|
|
18
|
+
* arbitrary local ports. Developers and Claude only ever see the URL.
|
|
19
|
+
*/
|
|
20
|
+
const fs_1 = require("fs");
|
|
21
|
+
const path_1 = require("path");
|
|
22
|
+
/**
|
|
23
|
+
* Build a complete identity from a project root.
|
|
24
|
+
*
|
|
25
|
+
* Detects monorepo subprojects under `projects/` automatically:
|
|
26
|
+
* - `projects/api` → `api.<slug>.localhost`
|
|
27
|
+
* - `projects/app` → `<slug>.localhost` (primary)
|
|
28
|
+
* - `projects/<other>` → `<other>.<slug>.localhost`
|
|
29
|
+
*
|
|
30
|
+
* For standalone projects (single repo, no `projects/` directory):
|
|
31
|
+
* - API project (config.env.ts present) → `api.<slug>.localhost`
|
|
32
|
+
* - App project (nuxt.config.ts present) → `<slug>.localhost`
|
|
33
|
+
*/
|
|
34
|
+
function buildIdentity(root) {
|
|
35
|
+
const slug = projectSlug(root);
|
|
36
|
+
const subdomains = {};
|
|
37
|
+
const projectsDir = (0, path_1.join)(root, 'projects');
|
|
38
|
+
if ((0, fs_1.existsSync)(projectsDir)) {
|
|
39
|
+
// Monorepo: enumerate projects/* subdirectories.
|
|
40
|
+
const apiDir = (0, path_1.join)(projectsDir, 'api');
|
|
41
|
+
const appDir = (0, path_1.join)(projectsDir, 'app');
|
|
42
|
+
if ((0, fs_1.existsSync)(apiDir)) {
|
|
43
|
+
subdomains.api = {
|
|
44
|
+
hostname: `api.${slug}.localhost`,
|
|
45
|
+
isPrimaryApp: false,
|
|
46
|
+
subdir: 'projects/api',
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
if ((0, fs_1.existsSync)(appDir)) {
|
|
50
|
+
subdomains.app = {
|
|
51
|
+
hostname: `${slug}.localhost`,
|
|
52
|
+
isPrimaryApp: true,
|
|
53
|
+
subdir: 'projects/app',
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
// Standalone — derive from project shape.
|
|
59
|
+
const isApi = (0, fs_1.existsSync)((0, path_1.join)(root, 'src', 'config.env.ts')) || (0, fs_1.existsSync)((0, path_1.join)(root, 'nest-cli.json'));
|
|
60
|
+
const isApp = (0, fs_1.existsSync)((0, path_1.join)(root, 'nuxt.config.ts'));
|
|
61
|
+
if (isApi) {
|
|
62
|
+
subdomains.api = { hostname: `api.${slug}.localhost`, isPrimaryApp: false, subdir: null };
|
|
63
|
+
}
|
|
64
|
+
if (isApp) {
|
|
65
|
+
subdomains.app = { hostname: `${slug}.localhost`, isPrimaryApp: true, subdir: null };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return { root, slug, subdomains };
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Read the bare project name from package.json (scope stripped).
|
|
72
|
+
* Falls back to directory basename if no package.json or no `name`.
|
|
73
|
+
*/
|
|
74
|
+
function projectSlug(root) {
|
|
75
|
+
const fromPkg = readPackageName(root);
|
|
76
|
+
const raw = fromPkg || (0, path_1.basename)(root);
|
|
77
|
+
return slugify(raw);
|
|
78
|
+
}
|
|
79
|
+
/** Lowercase, alphanumerics + dashes only, trimmed dashes. */
|
|
80
|
+
function slugify(input) {
|
|
81
|
+
return input
|
|
82
|
+
.toLowerCase()
|
|
83
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
84
|
+
.replace(/^-+|-+$/g, '');
|
|
85
|
+
}
|
|
86
|
+
/** Read `name` from package.json, scope-stripped (e.g. `@lenne.tech/foo` → `foo`). */
|
|
87
|
+
function readPackageName(dir) {
|
|
88
|
+
const pkgPath = (0, path_1.join)(dir, 'package.json');
|
|
89
|
+
if (!(0, fs_1.existsSync)(pkgPath))
|
|
90
|
+
return null;
|
|
91
|
+
try {
|
|
92
|
+
const pkg = JSON.parse((0, fs_1.readFileSync)(pkgPath, 'utf8'));
|
|
93
|
+
if (!pkg.name)
|
|
94
|
+
return null;
|
|
95
|
+
return pkg.name.includes('/') ? pkg.name.split('/').pop() : pkg.name;
|
|
96
|
+
}
|
|
97
|
+
catch (_a) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runMigrate = runMigrate;
|
|
4
|
+
/**
|
|
5
|
+
* Reusable migrate logic — used by `commands/dev/migrate.ts` (interactive
|
|
6
|
+
* + verbose) and `commands/fullstack/init.ts` (silent best-effort after
|
|
7
|
+
* project creation).
|
|
8
|
+
*
|
|
9
|
+
* Returns a structured result so callers can decide what to print.
|
|
10
|
+
* Idempotent — safe to run multiple times.
|
|
11
|
+
*/
|
|
12
|
+
const fs_1 = require("fs");
|
|
13
|
+
const path_1 = require("path");
|
|
14
|
+
const dev_identity_1 = require("./dev-identity");
|
|
15
|
+
const dev_patches_1 = require("./dev-patches");
|
|
16
|
+
const dev_project_1 = require("./dev-project");
|
|
17
|
+
const dev_state_1 = require("./dev-state");
|
|
18
|
+
/**
|
|
19
|
+
* Run all migration steps for a resolved project.
|
|
20
|
+
*
|
|
21
|
+
* Idempotent — re-running with no changes returns `alreadyMigrated: true`.
|
|
22
|
+
*/
|
|
23
|
+
function runMigrate(input) {
|
|
24
|
+
const { layout } = input;
|
|
25
|
+
const identity = (0, dev_identity_1.buildIdentity)(layout.root);
|
|
26
|
+
const dbName = (0, dev_project_1.deriveDbName)(layout.apiDir, identity.slug);
|
|
27
|
+
// 1. Code patches (config.env.ts, nuxt.config.ts, playwright.config.ts).
|
|
28
|
+
const filesToPatch = [];
|
|
29
|
+
if (layout.apiDir) {
|
|
30
|
+
const f = (0, dev_project_1.apiNeedsPortPatch)(layout.apiDir);
|
|
31
|
+
if (f)
|
|
32
|
+
filesToPatch.push(f);
|
|
33
|
+
}
|
|
34
|
+
if (layout.appDir)
|
|
35
|
+
filesToPatch.push(...(0, dev_project_1.appNeedsPortPatch)(layout.appDir));
|
|
36
|
+
const codePatches = filesToPatch.map((f) => (0, dev_patches_1.autoPatch)(f));
|
|
37
|
+
// 2. CLAUDE.md URL block (root + each subproject — only patches existing files).
|
|
38
|
+
const claudeCandidates = [
|
|
39
|
+
(0, path_1.join)(layout.root, 'CLAUDE.md'),
|
|
40
|
+
...(layout.apiDir ? [(0, path_1.join)(layout.apiDir, 'CLAUDE.md')] : []),
|
|
41
|
+
...(layout.appDir ? [(0, path_1.join)(layout.appDir, 'CLAUDE.md')] : []),
|
|
42
|
+
];
|
|
43
|
+
const claudePatches = claudeCandidates
|
|
44
|
+
.filter((f) => (0, fs_1.existsSync)(f))
|
|
45
|
+
.map((f) => (0, dev_patches_1.patchClaudeMd)(f, { dbName, identity }));
|
|
46
|
+
// 3. Registry — only write when something actually changed.
|
|
47
|
+
const reg = (0, dev_state_1.loadRegistry)();
|
|
48
|
+
const subdomainMap = {};
|
|
49
|
+
for (const [k, v] of Object.entries(identity.subdomains))
|
|
50
|
+
subdomainMap[k] = v.hostname;
|
|
51
|
+
const existing = reg.projects[identity.slug];
|
|
52
|
+
const next = {
|
|
53
|
+
dbName,
|
|
54
|
+
internalPorts: (existing === null || existing === void 0 ? void 0 : existing.internalPorts) || {},
|
|
55
|
+
lastUsedAt: existing === null || existing === void 0 ? void 0 : existing.lastUsedAt,
|
|
56
|
+
path: layout.root,
|
|
57
|
+
subdomains: subdomainMap,
|
|
58
|
+
};
|
|
59
|
+
const registryChanged = !existing ||
|
|
60
|
+
existing.path !== next.path ||
|
|
61
|
+
existing.dbName !== next.dbName ||
|
|
62
|
+
JSON.stringify(existing.subdomains) !== JSON.stringify(next.subdomains);
|
|
63
|
+
if (registryChanged) {
|
|
64
|
+
reg.projects[identity.slug] = next;
|
|
65
|
+
(0, dev_state_1.saveRegistry)(reg);
|
|
66
|
+
}
|
|
67
|
+
// 4. .gitignore
|
|
68
|
+
const addedGitignoreEntry = (0, dev_patches_1.addToGitignore)(layout.root, '.lt-dev/');
|
|
69
|
+
const codePatched = codePatches.filter((r) => r.patched).length > 0;
|
|
70
|
+
const claudePatched = claudePatches.filter((r) => r.patched).length > 0;
|
|
71
|
+
const alreadyMigrated = !codePatched && !claudePatched && !registryChanged && !addedGitignoreEntry;
|
|
72
|
+
return {
|
|
73
|
+
addedGitignoreEntry,
|
|
74
|
+
alreadyMigrated,
|
|
75
|
+
claudePatches,
|
|
76
|
+
codePatches,
|
|
77
|
+
dbName,
|
|
78
|
+
identity,
|
|
79
|
+
registryUpdated: registryChanged,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.addToGitignore = addToGitignore;
|
|
4
|
+
exports.autoPatch = autoPatch;
|
|
5
|
+
exports.patchApiConfig = patchApiConfig;
|
|
6
|
+
exports.patchClaudeMd = patchClaudeMd;
|
|
7
|
+
exports.patchNuxtConfig = patchNuxtConfig;
|
|
8
|
+
exports.patchPlaywrightConfig = patchPlaywrightConfig;
|
|
9
|
+
/**
|
|
10
|
+
* Idempotent patches applied by `lt dev migrate`.
|
|
11
|
+
*
|
|
12
|
+
* Goal: take a project that still has hardcoded `localhost:3000`
|
|
13
|
+
* defaults and make it env-aware so it can be served behind Caddy
|
|
14
|
+
* under `https://<slug>.localhost`.
|
|
15
|
+
*
|
|
16
|
+
* Each patch is a regex-based replace that matches only the legacy
|
|
17
|
+
* form. Already-patched files are no-ops.
|
|
18
|
+
*/
|
|
19
|
+
const fs_1 = require("fs");
|
|
20
|
+
/** Append entry to .gitignore if not already present. */
|
|
21
|
+
function addToGitignore(root, entry) {
|
|
22
|
+
const path = `${root}/.gitignore`;
|
|
23
|
+
let content = '';
|
|
24
|
+
if ((0, fs_1.existsSync)(path))
|
|
25
|
+
content = (0, fs_1.readFileSync)(path, 'utf8');
|
|
26
|
+
const lines = content.split(/\r?\n/);
|
|
27
|
+
if (lines.some((l) => l.trim() === entry || l.trim() === entry.replace(/\/$/, '')))
|
|
28
|
+
return false;
|
|
29
|
+
const ensured = `${(content.endsWith('\n') || content.length === 0 ? content : `${content}\n`) + entry}\n`;
|
|
30
|
+
(0, fs_1.writeFileSync)(path, ensured, 'utf8');
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
/** Run the appropriate patch based on filename. */
|
|
34
|
+
function autoPatch(file) {
|
|
35
|
+
if (file.endsWith('config.env.ts'))
|
|
36
|
+
return patchApiConfig(file);
|
|
37
|
+
if (file.endsWith('nuxt.config.ts'))
|
|
38
|
+
return patchNuxtConfig(file);
|
|
39
|
+
if (file.endsWith('playwright.config.ts'))
|
|
40
|
+
return patchPlaywrightConfig(file);
|
|
41
|
+
return { file, patched: false, replacements: 0 };
|
|
42
|
+
}
|
|
43
|
+
/** API: `port: 3000,` → `port: Number(process.env.PORT) || 3000,`. */
|
|
44
|
+
function patchApiConfig(file) {
|
|
45
|
+
if (!(0, fs_1.existsSync)(file))
|
|
46
|
+
return { file, patched: false, replacements: 0 };
|
|
47
|
+
const before = (0, fs_1.readFileSync)(file, 'utf8');
|
|
48
|
+
let count = 0;
|
|
49
|
+
const after = before.replace(/^(\s*)port:\s*3000\s*,$/gm, (_m, indent) => {
|
|
50
|
+
count++;
|
|
51
|
+
return `${indent}port: Number(process.env.PORT) || 3000,`;
|
|
52
|
+
});
|
|
53
|
+
if (count === 0)
|
|
54
|
+
return { file, patched: false, replacements: 0 };
|
|
55
|
+
(0, fs_1.writeFileSync)(file, after, 'utf8');
|
|
56
|
+
return { file, patched: true, replacements: count };
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Inject a "Local Development (lt dev)" block with the project's
|
|
60
|
+
* concrete URLs into CLAUDE.md. Idempotent — re-running with the same
|
|
61
|
+
* URLs is a no-op; re-running with different URLs replaces the block
|
|
62
|
+
* in place.
|
|
63
|
+
*/
|
|
64
|
+
function patchClaudeMd(file, options) {
|
|
65
|
+
const { dbName, identity } = options;
|
|
66
|
+
const startMarker = '<!-- lt-dev:url-block:start -->';
|
|
67
|
+
const endMarker = '<!-- lt-dev:url-block:end -->';
|
|
68
|
+
const apiSub = identity.subdomains.api;
|
|
69
|
+
const appSub = identity.subdomains.app;
|
|
70
|
+
const lines = [
|
|
71
|
+
startMarker,
|
|
72
|
+
'## Local Development (lt dev)',
|
|
73
|
+
'',
|
|
74
|
+
`This project is registered with \`lt dev\` (slug: \`${identity.slug}\`). Use these commands to run alongside other lt projects without cross-wiring or port collisions:`,
|
|
75
|
+
'',
|
|
76
|
+
'```bash',
|
|
77
|
+
'lt dev up # Start API + App behind Caddy with project-specific URLs',
|
|
78
|
+
'lt dev down # Stop the detached processes + remove Caddy block',
|
|
79
|
+
'lt dev status # Show running PIDs + bound URLs',
|
|
80
|
+
'lt dev doctor # Diagnose Caddy/CA/DNS/port issues',
|
|
81
|
+
'```',
|
|
82
|
+
'',
|
|
83
|
+
'**Active URLs for THIS project:**',
|
|
84
|
+
'',
|
|
85
|
+
];
|
|
86
|
+
if (appSub)
|
|
87
|
+
lines.push(`- App: \`https://${appSub.hostname}\``);
|
|
88
|
+
if (apiSub)
|
|
89
|
+
lines.push(`- API: \`https://${apiSub.hostname}\``);
|
|
90
|
+
if (dbName)
|
|
91
|
+
lines.push(`- DB: \`mongodb://127.0.0.1/${dbName}\``);
|
|
92
|
+
lines.push('');
|
|
93
|
+
lines.push('Env vars set automatically by `lt dev up`: `BASE_URL`, `APP_URL`, `NUXT_API_URL`, `NUXT_PUBLIC_API_URL`, `NUXT_PUBLIC_SITE_URL`, `NUXT_PUBLIC_STORAGE_PREFIX`, `NSC__MONGOOSE__URI`, `DATABASE_URL`. **Never assume `localhost:3000` / `localhost:3001` for this project** — those are the framework defaults, not the active URLs.');
|
|
94
|
+
lines.push('');
|
|
95
|
+
lines.push(endMarker);
|
|
96
|
+
const block = lines.join('\n');
|
|
97
|
+
if (!(0, fs_1.existsSync)(file))
|
|
98
|
+
return { file, patched: false, replacements: 0 };
|
|
99
|
+
const content = (0, fs_1.readFileSync)(file, 'utf8');
|
|
100
|
+
const startIdx = content.indexOf(startMarker);
|
|
101
|
+
const endIdx = content.indexOf(endMarker);
|
|
102
|
+
let next;
|
|
103
|
+
if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
|
|
104
|
+
const before = content.slice(0, startIdx);
|
|
105
|
+
const after = content.slice(endIdx + endMarker.length);
|
|
106
|
+
next = before + block + after;
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
const sep = content.endsWith('\n\n') ? '' : content.endsWith('\n') ? '\n' : '\n\n';
|
|
110
|
+
next = `${content}${sep}${block}\n`;
|
|
111
|
+
}
|
|
112
|
+
if (next === content)
|
|
113
|
+
return { file, patched: false, replacements: 0 };
|
|
114
|
+
(0, fs_1.writeFileSync)(file, next, 'utf8');
|
|
115
|
+
return { file, patched: true, replacements: 1 };
|
|
116
|
+
}
|
|
117
|
+
/** App: hardcoded port + vite-proxy target → env-aware. */
|
|
118
|
+
function patchNuxtConfig(file) {
|
|
119
|
+
if (!(0, fs_1.existsSync)(file))
|
|
120
|
+
return { file, patched: false, replacements: 0 };
|
|
121
|
+
const before = (0, fs_1.readFileSync)(file, 'utf8');
|
|
122
|
+
let count = 0;
|
|
123
|
+
let after = before.replace(/^(\s*)port:\s*3001\s*,$/gm, (_m, indent) => {
|
|
124
|
+
count++;
|
|
125
|
+
return `${indent}port: Number(process.env.PORT) || 3001,`;
|
|
126
|
+
});
|
|
127
|
+
after = after.replace(/target:\s*'http:\/\/localhost:3000'/g, () => {
|
|
128
|
+
count++;
|
|
129
|
+
return `target: process.env.NUXT_API_URL || 'http://localhost:3000'`;
|
|
130
|
+
});
|
|
131
|
+
if (count === 0)
|
|
132
|
+
return { file, patched: false, replacements: 0 };
|
|
133
|
+
(0, fs_1.writeFileSync)(file, after, 'utf8');
|
|
134
|
+
return { file, patched: true, replacements: count };
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Playwright: hardcoded baseURL/host/url → env-aware, plus a top-of-file
|
|
138
|
+
* dotenv-load of `.lt-dev/.env` so external test runners (CLI, IDE, VS
|
|
139
|
+
* Code Playwright Extension) automatically pick up `lt dev up`'s URLs
|
|
140
|
+
* and the local Caddy CA — without requiring the parent shell to inherit
|
|
141
|
+
* any env.
|
|
142
|
+
*
|
|
143
|
+
* Patches applied (each idempotent):
|
|
144
|
+
* 1. Top-of-file: `if (existsSync('.lt-dev/.env')) loadEnv(...)` block,
|
|
145
|
+
* bracketed by `// >>> lt-dev:bridge >>>` markers.
|
|
146
|
+
* 2. Hardcoded baseURL/host/url for `http://localhost:3001` →
|
|
147
|
+
* `process.env.NUXT_PUBLIC_SITE_URL || 'http://localhost:3001'`.
|
|
148
|
+
*/
|
|
149
|
+
function patchPlaywrightConfig(file) {
|
|
150
|
+
if (!(0, fs_1.existsSync)(file))
|
|
151
|
+
return { file, patched: false, replacements: 0 };
|
|
152
|
+
const before = (0, fs_1.readFileSync)(file, 'utf8');
|
|
153
|
+
let count = 0;
|
|
154
|
+
let after = before;
|
|
155
|
+
// 1. URL-Patches.
|
|
156
|
+
for (const key of ['baseURL', 'host', 'url']) {
|
|
157
|
+
after = after.replace(new RegExp(`${key}:\\s*'http://localhost:3001'`, 'g'), () => {
|
|
158
|
+
count++;
|
|
159
|
+
return `${key}: process.env.NUXT_PUBLIC_SITE_URL || 'http://localhost:3001'`;
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
// 2. Top-of-file dotenv bridge — only inject if not already present.
|
|
163
|
+
const bridgeStart = '// >>> lt-dev:bridge >>>';
|
|
164
|
+
const bridgeEnd = '// <<< lt-dev:bridge <<<';
|
|
165
|
+
if (!after.includes(bridgeStart)) {
|
|
166
|
+
const bridgeBlock = [
|
|
167
|
+
bridgeStart,
|
|
168
|
+
'// Auto-load <root>/.lt-dev/.env when `lt dev up` is active so',
|
|
169
|
+
'// external test runners (CLI, IDE, VS Code Playwright Extension)',
|
|
170
|
+
'// pick up project URLs + Caddy CA without inheriting the parent shell.',
|
|
171
|
+
"import { existsSync as __ltDevExists, readFileSync as __ltDevRead } from 'node:fs';",
|
|
172
|
+
"import { resolve as __ltDevResolve } from 'node:path';",
|
|
173
|
+
"const __ltDevEnvFile = __ltDevResolve(process.cwd(), '.lt-dev/.env');",
|
|
174
|
+
'if (__ltDevExists(__ltDevEnvFile)) {',
|
|
175
|
+
' for (const __ln of __ltDevRead(__ltDevEnvFile, "utf8").split(/\\r?\\n/)) {',
|
|
176
|
+
' const __m = __ln.match(/^([A-Z][A-Z0-9_]*)=(.*)$/);',
|
|
177
|
+
' if (__m && process.env[__m[1]] === undefined) process.env[__m[1]] = __m[2];',
|
|
178
|
+
' }',
|
|
179
|
+
'}',
|
|
180
|
+
bridgeEnd,
|
|
181
|
+
'',
|
|
182
|
+
].join('\n');
|
|
183
|
+
after = bridgeBlock + after;
|
|
184
|
+
count++;
|
|
185
|
+
}
|
|
186
|
+
if (count === 0)
|
|
187
|
+
return { file, patched: false, replacements: 0 };
|
|
188
|
+
(0, fs_1.writeFileSync)(file, after, 'utf8');
|
|
189
|
+
return { file, patched: true, replacements: count };
|
|
190
|
+
}
|