@lenne.tech/cli 1.21.0 → 1.22.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,169 @@
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
+ /** Generate the Caddyfile block for one project's routes. */
98
+ function renderProjectBlock(slug, routes) {
99
+ const lines = [`# >>> lt-dev:${slug} >>>`];
100
+ for (const route of routes) {
101
+ lines.push(`${route.hostname} {`);
102
+ lines.push(` reverse_proxy 127.0.0.1:${route.upstreamPort}`);
103
+ lines.push('}');
104
+ }
105
+ lines.push(`# <<< lt-dev:${slug} <<<`);
106
+ return lines.join('\n');
107
+ }
108
+ /**
109
+ * Insert/replace a project block in the Caddyfile.
110
+ *
111
+ * Idempotent — re-applying with the same routes is a no-op.
112
+ * Returns true if the file was modified.
113
+ */
114
+ function upsertProjectBlock(slug, routes) {
115
+ const current = readCaddyfile();
116
+ const block = renderProjectBlock(slug, routes);
117
+ const startMarker = `# >>> lt-dev:${slug} >>>`;
118
+ const endMarker = `# <<< lt-dev:${slug} <<<`;
119
+ const startIdx = current.indexOf(startMarker);
120
+ const endIdx = current.indexOf(endMarker);
121
+ let next;
122
+ if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
123
+ const before = current.slice(0, startIdx).replace(/\n+$/, '');
124
+ const after = current.slice(endIdx + endMarker.length).replace(/^\n+/, '');
125
+ next = [before, block, after].filter((s) => s.length > 0).join('\n\n');
126
+ }
127
+ else {
128
+ next = current.length > 0 ? `${current.replace(/\n+$/, '')}\n\n${block}` : block;
129
+ }
130
+ if (next === current.replace(/\s+$/, ''))
131
+ return false;
132
+ writeCaddyfile(next);
133
+ return true;
134
+ }
135
+ /** Validate the current Caddyfile syntax. */
136
+ function validateCaddyfile() {
137
+ return __awaiter(this, void 0, void 0, function* () {
138
+ return runCaddy(['validate', '--config', CADDYFILE_PATH, '--adapter', 'caddyfile']);
139
+ });
140
+ }
141
+ /** Write the Caddyfile, ensuring the parent directory exists. */
142
+ function writeCaddyfile(content) {
143
+ (0, fs_1.mkdirSync)((0, path_1.dirname)(CADDYFILE_PATH), { recursive: true });
144
+ const next = content.startsWith('#') ? content : `${HEADER}\n\n${content}`;
145
+ (0, fs_1.writeFileSync)(CADDYFILE_PATH, next.endsWith('\n') ? next : `${next}\n`, 'utf8');
146
+ }
147
+ /** Run a caddy subcommand and capture stdout/stderr. */
148
+ function runCaddy(args) {
149
+ return new Promise((resolve) => {
150
+ var _a, _b;
151
+ const child = (0, child_process_1.spawn)('caddy', args, { stdio: ['ignore', 'pipe', 'pipe'] });
152
+ let stdout = '';
153
+ let stderr = '';
154
+ let errored = false;
155
+ (_a = child.stdout) === null || _a === void 0 ? void 0 : _a.on('data', (b) => (stdout += String(b)));
156
+ (_b = child.stderr) === null || _b === void 0 ? void 0 : _b.on('data', (b) => (stderr += String(b)));
157
+ child.on('error', () => (errored = true));
158
+ child.on('close', (code) => {
159
+ if (errored)
160
+ resolve({ exitCode: null, ok: false, stderr: 'caddy: command not found', stdout: '' });
161
+ else
162
+ resolve({ exitCode: code, ok: code === 0, stderr, stdout });
163
+ });
164
+ });
165
+ }
166
+ /** Path constants for tests + status displays. */
167
+ exports.paths = {
168
+ caddyfile: CADDYFILE_PATH,
169
+ };
@@ -0,0 +1,109 @@
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
+ ];
90
+ for (const key of exported) {
91
+ const v = devEnv.app.env[key];
92
+ if (v !== undefined && v !== '')
93
+ lines.push(`${String(key)}=${v}`);
94
+ }
95
+ // Marker so consumers can detect "lt dev mode" reliably.
96
+ lines.push(`LT_DEV_ACTIVE=true`);
97
+ if (dbName)
98
+ lines.push(`LT_DEV_DB_NAME=${dbName}`);
99
+ // Caddy root CA — Playwright/Chromium use NODE_EXTRA_CA_CERTS to trust it.
100
+ const caPath = detectCaddyRootCa();
101
+ if (caPath)
102
+ lines.push(`NODE_EXTRA_CA_CERTS=${caPath}`);
103
+ const content = `${HEADER}${lines.join('\n')}\n`;
104
+ if ((0, fs_1.existsSync)(file) && (0, fs_1.readFileSync)(file, 'utf8') === content)
105
+ return file;
106
+ (0, fs_1.mkdirSync)((0, path_1.dirname)(file), { recursive: true });
107
+ (0, fs_1.writeFileSync)(file, content, 'utf8');
108
+ return file;
109
+ }
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildDevEnv = buildDevEnv;
4
+ /**
5
+ * Build the environment maps for both API and App processes.
6
+ *
7
+ * Both processes inherit `baseEnv` (typically `process.env`) so user-set
8
+ * vars survive. `lt dev`-managed keys win on top.
9
+ */
10
+ function buildDevEnv(input) {
11
+ const { apiInternalPort, appInternalPort, baseEnv = {}, dbName, identity } = input;
12
+ const apiSub = identity.subdomains.api;
13
+ const appSub = identity.subdomains.app;
14
+ const apiUrl = apiSub ? `https://${apiSub.hostname}` : '';
15
+ const appUrl = appSub ? `https://${appSub.hostname}` : '';
16
+ const sharedKeys = 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}` } : {}));
17
+ return {
18
+ api: {
19
+ env: Object.assign(Object.assign(Object.assign({}, baseEnv), sharedKeys), { PORT: String(apiInternalPort) }),
20
+ internalPort: apiInternalPort,
21
+ },
22
+ app: {
23
+ env: Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, baseEnv), sharedKeys), (apiUrl ? { NUXT_API_URL: apiUrl, NUXT_PUBLIC_API_URL: apiUrl } : {})), (appUrl ? { NUXT_PUBLIC_SITE_URL: appUrl } : {})), {
24
+ // Vite-API-Proxy is OFF by default in lt dev mode — Caddy serves
25
+ // both subdomains under HTTPS with shared cookie domain, so
26
+ // same-origin trickery is no longer required.
27
+ NUXT_PUBLIC_API_PROXY: 'false', NUXT_PUBLIC_STORAGE_PREFIX: identity.slug, PORT: String(appInternalPort) }),
28
+ internalPort: appInternalPort,
29
+ },
30
+ };
31
+ }
32
+ /** Postgres convenience URL — used by Postgres-based projects (e.g. nest-base). */
33
+ function buildPostgresUrl(dbName) {
34
+ return `postgresql://${dbName}:${dbName}@localhost:5432/${dbName}`;
35
+ }
@@ -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
+ }