@lenne.tech/cli 1.20.0 → 1.21.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,131 @@
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 port_registry_1 = require("../../lib/port-registry");
15
+ /**
16
+ * Rebuild the port registry from the filesystem.
17
+ *
18
+ * Walks the given directory (default: cwd) up to depth 3, looking for
19
+ * `lt.config.json` + `package.json` pairs or workspace markers. Re-allocates
20
+ * a slot for each new project found; preserves slots for projects whose
21
+ * names already exist in the registry.
22
+ */
23
+ const ScanCommand = {
24
+ alias: [],
25
+ description: 'Rebuild port registry',
26
+ hidden: false,
27
+ name: 'scan',
28
+ run: (toolbox) => __awaiter(void 0, void 0, void 0, function* () {
29
+ const { filesystem, parameters, print: { colors, info, success }, } = toolbox;
30
+ const startDir = parameters.first ? filesystem.path(parameters.first) : filesystem.cwd();
31
+ info('');
32
+ info(colors.bold(`Scanning for projects under ${startDir} ...`));
33
+ const found = walkForProjects(startDir, 0, 3);
34
+ info(colors.dim(`Found ${found.length} candidate project(s)`));
35
+ const registry = (0, port_registry_1.loadRegistry)();
36
+ let added = 0;
37
+ let kept = 0;
38
+ let dirty = false;
39
+ for (const project of found) {
40
+ const slug = (0, port_registry_1.projectSlug)(project.path);
41
+ const existing = registry.projects[slug];
42
+ if (existing) {
43
+ if (existing.path !== project.path) {
44
+ existing.path = project.path;
45
+ dirty = true;
46
+ }
47
+ kept++;
48
+ continue;
49
+ }
50
+ const slot = (0, port_registry_1.allocateSlot)(slug, registry);
51
+ registry.projects[slug] = { path: project.path, ports: (0, port_registry_1.portsForSlot)(slot), slot };
52
+ added++;
53
+ dirty = true;
54
+ }
55
+ if (dirty)
56
+ (0, port_registry_1.saveRegistry)(registry);
57
+ success(`Registry updated: ${added} new, ${kept} kept (total: ${Object.keys(registry.projects).length})`);
58
+ if (!parameters.options.fromGluegunMenu) {
59
+ process.exit();
60
+ }
61
+ return `ports scan: ${added} new`;
62
+ }),
63
+ };
64
+ /** A directory looks like a project if it has `package.json` with a non-empty `name` field. */
65
+ function looksLikeProject(dir) {
66
+ if (!(0, fs_1.existsSync)((0, path_1.join)(dir, 'package.json')))
67
+ return false;
68
+ try {
69
+ const pkg = JSON.parse((0, fs_1.readFileSync)((0, path_1.join)(dir, 'package.json'), 'utf8'));
70
+ return Boolean(pkg.name);
71
+ }
72
+ catch (_a) {
73
+ return false;
74
+ }
75
+ }
76
+ /**
77
+ * Recursive project discovery.
78
+ *
79
+ * Stops descending into a directory as soon as a project marker is detected
80
+ * (lt.config.json with package.json, pnpm-workspace.yaml, or `projects/`).
81
+ * Skips dotdirs and `node_modules`. Skips symlinks (lstatSync) to avoid
82
+ * traversal loops on pathological filesystems.
83
+ */
84
+ function walkForProjects(dir, depth, maxDepth) {
85
+ if (depth > maxDepth)
86
+ return [];
87
+ if (!(0, fs_1.existsSync)(dir))
88
+ return [];
89
+ const out = [];
90
+ // A project is detected if EITHER an lt.config.json exists OR a workspace marker is found.
91
+ if ((0, fs_1.existsSync)((0, path_1.join)(dir, 'lt.config.json'))) {
92
+ if (looksLikeProject(dir)) {
93
+ out.push({ path: dir });
94
+ return out; // don't recurse into a detected project
95
+ }
96
+ }
97
+ if ((0, fs_1.existsSync)((0, path_1.join)(dir, 'pnpm-workspace.yaml')) || (0, fs_1.existsSync)((0, path_1.join)(dir, 'projects'))) {
98
+ out.push({ path: dir });
99
+ return out;
100
+ }
101
+ let entries;
102
+ try {
103
+ entries = (0, fs_1.readdirSync)(dir, { withFileTypes: true });
104
+ }
105
+ catch (_a) {
106
+ return [];
107
+ }
108
+ for (const entry of entries) {
109
+ if (entry.name.startsWith('.') || entry.name === 'node_modules')
110
+ continue;
111
+ // Use the cheap Dirent check first — it doesn't need an extra syscall.
112
+ if (!entry.isDirectory()) {
113
+ // Could still be a symlink that points to a directory. Skip those to
114
+ // avoid traversal loops.
115
+ if (!entry.isSymbolicLink())
116
+ continue;
117
+ let s;
118
+ try {
119
+ s = (0, fs_1.lstatSync)((0, path_1.join)(dir, entry.name));
120
+ }
121
+ catch (_b) {
122
+ continue;
123
+ }
124
+ if (s.isSymbolicLink())
125
+ continue;
126
+ }
127
+ out.push(...walkForProjects((0, path_1.join)(dir, entry.name), depth + 1, maxDepth));
128
+ }
129
+ return out;
130
+ }
131
+ module.exports = ScanCommand;
@@ -12,6 +12,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
12
12
  const path_1 = require("path");
13
13
  const framework_detection_1 = require("../lib/framework-detection");
14
14
  const frontend_framework_detection_1 = require("../lib/frontend-framework-detection");
15
+ const port_registry_1 = require("../lib/port-registry");
15
16
  const workspace_integration_1 = require("../lib/workspace-integration");
16
17
  /**
17
18
  * Show project status and context
@@ -22,7 +23,7 @@ const StatusCommand = {
22
23
  hidden: false,
23
24
  name: 'status',
24
25
  run: (toolbox) => __awaiter(void 0, void 0, void 0, function* () {
25
- var _a, _b;
26
+ var _a, _b, _c;
26
27
  const { filesystem, print: { colors, info, success, warning }, system, } = toolbox;
27
28
  const cwd = filesystem.cwd();
28
29
  info('');
@@ -107,7 +108,7 @@ const StatusCommand = {
107
108
  projectInfo.projectType = 'node';
108
109
  }
109
110
  }
110
- catch (_c) {
111
+ catch (_d) {
111
112
  // Ignore parse errors
112
113
  }
113
114
  }
@@ -136,7 +137,7 @@ const StatusCommand = {
136
137
  });
137
138
  }
138
139
  }
139
- catch (_d) {
140
+ catch (_e) {
140
141
  // ignore
141
142
  }
142
143
  }
@@ -152,7 +153,7 @@ const StatusCommand = {
152
153
  });
153
154
  }
154
155
  }
155
- catch (_e) {
156
+ catch (_f) {
156
157
  // ignore
157
158
  }
158
159
  }
@@ -167,7 +168,7 @@ const StatusCommand = {
167
168
  projectInfo.gitBranch = (branch === null || branch === void 0 ? void 0 : branch.trim()) || null;
168
169
  }
169
170
  }
170
- catch (_f) {
171
+ catch (_g) {
171
172
  // Not a git repository
172
173
  }
173
174
  // Get Node/npm versions
@@ -175,7 +176,7 @@ const StatusCommand = {
175
176
  projectInfo.nodeVersion = ((_a = (yield system.run('node --version 2>/dev/null'))) === null || _a === void 0 ? void 0 : _a.trim()) || null;
176
177
  projectInfo.npmVersion = ((_b = (yield system.run('npm --version 2>/dev/null'))) === null || _b === void 0 ? void 0 : _b.trim()) || null;
177
178
  }
178
- catch (_g) {
179
+ catch (_h) {
179
180
  // Ignore errors
180
181
  }
181
182
  // Display project info
@@ -234,6 +235,36 @@ const StatusCommand = {
234
235
  info(colors.dim(' Hint: `lt fullstack add-app` to integrate a Nuxt or Angular app.'));
235
236
  }
236
237
  }
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?".
241
+ {
242
+ const registryRoot = ((_c = projectInfo.workspaceSubProject) === null || _c === void 0 ? void 0 : _c.root) ||
243
+ (projectInfo.workspaceLayout.hasWorkspace ? projectInfo.workspaceLayout.workspaceDir : cwd);
244
+ if (registryRoot) {
245
+ const slug = (0, port_registry_1.projectSlug)(registryRoot);
246
+ const entry = (0, port_registry_1.loadRegistry)().projects[slug];
247
+ if (entry) {
248
+ 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}`);
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;
259
+ if (apiAlive || appAlive) {
260
+ info(` Running: api ${apiAlive ? colors.green('●') : colors.dim('○')} app ${appAlive ? colors.green('●') : colors.dim('○')}`);
261
+ }
262
+ else {
263
+ info(colors.dim(' Hint: `lt local up` to start API + App with these ports.'));
264
+ }
265
+ }
266
+ }
267
+ }
237
268
  // Show monorepo subprojects if we detected any (typically at monorepo root)
238
269
  if (projectInfo.monorepoSubprojects.length > 0) {
239
270
  info('');
@@ -1975,21 +1975,31 @@ class Server {
1975
1975
  literal.setLiteralValue(secretMap.get(text));
1976
1976
  continue;
1977
1977
  }
1978
- // Replace database names (nest-server-ci -> projectDir-ci)
1978
+ // Replace database names so the project gets project-specific DB
1979
+ // names. Matches BOTH the legacy `nest-server-{env}` form AND the
1980
+ // current `nest-server-starter-{env}` form used by the public
1981
+ // nest-server-starter repo. Examples:
1982
+ // nest-server-starter-local → ${projectDir}-local
1983
+ // nest-server-starter-production → ${projectDir}-production
1984
+ // nest-server-ci → ${projectDir}-ci (legacy)
1985
+ // The optional `(?:starter-)?` non-capturing group is what fixes
1986
+ // the previously-broken case where dbName: 'nest-server-starter-local'
1987
+ // turned into 'svl-sports-system-starter-local' (-starter- stayed).
1979
1988
  if (text.includes('nest-server-')) {
1980
- literal.setLiteralValue(text.replace(/nest-server-/g, `${projectDir}-`));
1989
+ literal.setLiteralValue(text.replace(/nest-server-(?:starter-)?/g, `${projectDir}-`));
1981
1990
  }
1982
1991
  }
1983
1992
  sourceFile.saveSync();
1984
1993
  }
1985
1994
  catch (_a) {
1986
- // Fallback to regex-based approach if ts-morph fails
1995
+ // Fallback to regex-based approach if ts-morph fails. Same matcher
1996
+ // as the AST branch — keep them in sync.
1987
1997
  let content = this.filesystem.read(configPath);
1988
1998
  if (!content) {
1989
1999
  return;
1990
2000
  }
1991
2001
  content = this.replaceSecretOrPrivateKeys(content);
1992
- content = content.replace(/nest-server-(\w+)/g, `${projectDir}-$1`);
2002
+ content = content.replace(/nest-server-(?:starter-)?(\w+)/g, `${projectDir}-$1`);
1993
2003
  this.filesystem.write(configPath, content);
1994
2004
  }
1995
2005
  }
@@ -0,0 +1,175 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.autoPatch = autoPatch;
4
+ exports.patchApiConfig = patchApiConfig;
5
+ exports.patchClaudeMd = patchClaudeMd;
6
+ exports.patchNuxtConfig = patchNuxtConfig;
7
+ exports.patchPlaywrightConfig = patchPlaywrightConfig;
8
+ /**
9
+ * Idempotent patches for legacy projects that still have hardcoded
10
+ * dev ports. Applied by `lt local init --patch`.
11
+ *
12
+ * Each patch is a regex-based replace that matches only the legacy
13
+ * form. Already-patched files are no-ops.
14
+ */
15
+ const fs_1 = require("fs");
16
+ /** Run the appropriate patch based on filename. */
17
+ function autoPatch(file) {
18
+ if (file.endsWith('config.env.ts'))
19
+ return patchApiConfig(file);
20
+ if (file.endsWith('nuxt.config.ts'))
21
+ return patchNuxtConfig(file);
22
+ if (file.endsWith('playwright.config.ts'))
23
+ return patchPlaywrightConfig(file);
24
+ return { file, patched: false, replacements: 0 };
25
+ }
26
+ /**
27
+ * Patch nest-server-starter-style `src/config.env.ts`:
28
+ * - `port: 3000,` → `port: Number(process.env.PORT) || 3000,`
29
+ *
30
+ * Idempotent — files already patched return `patched: false`. Missing
31
+ * files are also a no-op (matches `patchClaudeMd` behavior).
32
+ */
33
+ function patchApiConfig(file) {
34
+ if (!(0, fs_1.existsSync)(file)) {
35
+ return { file, patched: false, replacements: 0 };
36
+ }
37
+ const before = (0, fs_1.readFileSync)(file, 'utf8');
38
+ let count = 0;
39
+ const after = before.replace(/^(\s*)port:\s*3000\s*,$/gm, (_match, indent) => {
40
+ count++;
41
+ return `${indent}port: Number(process.env.PORT) || 3000,`;
42
+ });
43
+ if (count === 0) {
44
+ return { file, patched: false, replacements: 0 };
45
+ }
46
+ (0, fs_1.writeFileSync)(file, after, 'utf8');
47
+ return { file, patched: true, replacements: count };
48
+ }
49
+ /**
50
+ * Inject a "Local Development (Parallel Projects)" block with the
51
+ * project's concrete ports into CLAUDE.md. Idempotent — re-running
52
+ * with the same ports is a no-op; re-running with different ports
53
+ * updates the block in place.
54
+ *
55
+ * The block is delimited by HTML comments so it can be located and
56
+ * replaced reliably.
57
+ */
58
+ function patchClaudeMd(file, options) {
59
+ const { apiPort, appPort, dbName, slug } = options;
60
+ const startMarker = '<!-- lt-local:port-block:start -->';
61
+ const endMarker = '<!-- lt-local:port-block:end -->';
62
+ const dbLine = dbName ? `- DB: \`mongodb://127.0.0.1/${dbName}\`\n` : '';
63
+ const block = [
64
+ startMarker,
65
+ '## Local Development (lt local)',
66
+ '',
67
+ `This project is registered with \`lt local\` (slug: \`${slug}\`). Use these commands to run alongside other lt-projects without port collisions:`,
68
+ '',
69
+ '```bash',
70
+ 'lt local up # Start API + App with project-specific ports',
71
+ 'lt local down # Stop the detached processes',
72
+ 'lt local status # Show running PIDs + bound ports',
73
+ 'lt ports # Inspect all reserved + bound dev ports',
74
+ '```',
75
+ '',
76
+ '**Active ports for THIS project:**',
77
+ '',
78
+ `- API: \`http://localhost:${apiPort}\``,
79
+ `- App: \`http://localhost:${appPort}\``,
80
+ dbLine,
81
+ 'Env vars (set automatically by `lt local up`): `PORT`, `BASE_URL`, `APP_URL`, `NUXT_API_URL`, `NUXT_PUBLIC_API_URL`, `NUXT_PUBLIC_SITE_URL`, `NUXT_PUBLIC_STORAGE_PREFIX`, `NSC__MONGOOSE__URI`. **Never assume ports 3000/3001 for this project** — they may belong to a parallel project.',
82
+ '',
83
+ endMarker,
84
+ ]
85
+ .filter((line) => line !== null && line !== undefined)
86
+ .join('\n');
87
+ let content = '';
88
+ if ((0, fs_1.existsSync)(file)) {
89
+ content = (0, fs_1.readFileSync)(file, 'utf8');
90
+ }
91
+ else {
92
+ // Don't create CLAUDE.md from scratch — only patch if it exists.
93
+ return { file, patched: false, replacements: 0 };
94
+ }
95
+ const startIdx = content.indexOf(startMarker);
96
+ const endIdx = content.indexOf(endMarker);
97
+ let next;
98
+ if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
99
+ // Replace existing block in place — the block does not include
100
+ // surrounding whitespace, so we slice exactly from start to end-of-marker.
101
+ const before = content.slice(0, startIdx);
102
+ const after = content.slice(endIdx + endMarker.length);
103
+ next = before + block + after;
104
+ }
105
+ else {
106
+ // Append at the end with one blank line separator + trailing newline
107
+ const sep = content.endsWith('\n\n') ? '' : content.endsWith('\n') ? '\n' : '\n\n';
108
+ next = `${content}${sep}${block}\n`;
109
+ }
110
+ // Idempotent check: if nothing changed, don't write
111
+ if (next === content) {
112
+ return { file, patched: false, replacements: 0 };
113
+ }
114
+ (0, fs_1.writeFileSync)(file, next, 'utf8');
115
+ return { file, patched: true, replacements: 1 };
116
+ }
117
+ /**
118
+ * Patch nuxt-base-template-style `nuxt.config.ts`:
119
+ * - `port: 3001,` → `port: Number(process.env.PORT) || 3001,`
120
+ * - `target: 'http://localhost:3000'` → `target: process.env.NUXT_API_URL || 'http://localhost:3000'`
121
+ *
122
+ * Missing files are a no-op (matches `patchClaudeMd` behavior).
123
+ */
124
+ function patchNuxtConfig(file) {
125
+ if (!(0, fs_1.existsSync)(file)) {
126
+ return { file, patched: false, replacements: 0 };
127
+ }
128
+ const before = (0, fs_1.readFileSync)(file, 'utf8');
129
+ let count = 0;
130
+ let after = before.replace(/^(\s*)port:\s*3001\s*,$/gm, (_match, indent) => {
131
+ count++;
132
+ return `${indent}port: Number(process.env.PORT) || 3001,`;
133
+ });
134
+ after = after.replace(/target:\s*'http:\/\/localhost:3000'/g, () => {
135
+ count++;
136
+ return `target: process.env.NUXT_API_URL || 'http://localhost:3000'`;
137
+ });
138
+ if (count === 0) {
139
+ return { file, patched: false, replacements: 0 };
140
+ }
141
+ (0, fs_1.writeFileSync)(file, after, 'utf8');
142
+ return { file, patched: true, replacements: count };
143
+ }
144
+ /**
145
+ * Patch nuxt-base-template-style `playwright.config.ts`:
146
+ * - `baseURL: 'http://localhost:3001'` → uses NUXT_PUBLIC_SITE_URL
147
+ * - `host: 'http://localhost:3001'` → uses NUXT_PUBLIC_SITE_URL
148
+ * - `url: 'http://localhost:3001'` → uses NUXT_PUBLIC_SITE_URL
149
+ *
150
+ * Missing files are a no-op (matches `patchClaudeMd` behavior).
151
+ */
152
+ function patchPlaywrightConfig(file) {
153
+ if (!(0, fs_1.existsSync)(file)) {
154
+ return { file, patched: false, replacements: 0 };
155
+ }
156
+ const before = (0, fs_1.readFileSync)(file, 'utf8');
157
+ let count = 0;
158
+ let after = before.replace(/baseURL:\s*'http:\/\/localhost:3001'/g, () => {
159
+ count++;
160
+ return `baseURL: process.env.NUXT_PUBLIC_SITE_URL || 'http://localhost:3001'`;
161
+ });
162
+ after = after.replace(/host:\s*'http:\/\/localhost:3001'/g, () => {
163
+ count++;
164
+ return `host: process.env.NUXT_PUBLIC_SITE_URL || 'http://localhost:3001'`;
165
+ });
166
+ after = after.replace(/url:\s*'http:\/\/localhost:3001'/g, () => {
167
+ count++;
168
+ return `url: process.env.NUXT_PUBLIC_SITE_URL || 'http://localhost:3001'`;
169
+ });
170
+ if (count === 0) {
171
+ return { file, patched: false, replacements: 0 };
172
+ }
173
+ (0, fs_1.writeFileSync)(file, after, 'utf8');
174
+ return { file, patched: true, replacements: count };
175
+ }
@@ -0,0 +1,101 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.apiNeedsPortPatch = apiNeedsPortPatch;
4
+ exports.appNeedsPortPatch = appNeedsPortPatch;
5
+ exports.resolveLayout = resolveLayout;
6
+ const fs_1 = require("fs");
7
+ const path_1 = require("path");
8
+ const workspace_integration_1 = require("./workspace-integration");
9
+ /**
10
+ * Detect whether the API project still has the legacy hardcoded
11
+ * `port: 3000`. Returns the absolute file path if a patch is needed,
12
+ * null if already env-aware (or no file).
13
+ */
14
+ function apiNeedsPortPatch(apiDir) {
15
+ const file = (0, path_1.join)(apiDir, 'src', 'config.env.ts');
16
+ if (!(0, fs_1.existsSync)(file))
17
+ return null;
18
+ const content = (0, fs_1.readFileSync)(file, 'utf8');
19
+ // Match `port: 3000,` exactly (not yet env-wrapped).
20
+ return /port:\s*3000\s*,/.test(content) ? file : null;
21
+ }
22
+ /**
23
+ * Detect whether the App project still has hardcoded `port: 3001` or a
24
+ * hardcoded vite-proxy `target: 'http://localhost:3000'`. Returns an
25
+ * array of absolute file paths that need patching.
26
+ */
27
+ function appNeedsPortPatch(appDir) {
28
+ const candidates = [(0, path_1.join)(appDir, 'nuxt.config.ts'), (0, path_1.join)(appDir, 'playwright.config.ts')];
29
+ return candidates.filter((file) => {
30
+ if (!(0, fs_1.existsSync)(file))
31
+ return false;
32
+ const c = (0, fs_1.readFileSync)(file, 'utf8');
33
+ return (/port:\s*3001\s*,/.test(c) ||
34
+ /target:\s*'http:\/\/localhost:3000'/.test(c) ||
35
+ /baseURL:\s*'http:\/\/localhost:3001'/.test(c) ||
36
+ /url:\s*'http:\/\/localhost:3001'/.test(c) ||
37
+ /host:\s*'http:\/\/localhost:3001'/.test(c));
38
+ });
39
+ }
40
+ /**
41
+ * Resolve the project layout starting from `cwd`. Walks up to find a
42
+ * monorepo workspace if cwd is inside `projects/api/` or `projects/app/`.
43
+ */
44
+ function resolveLayout(cwd, filesystem) {
45
+ // Inside a sub-project? → walk to workspace root.
46
+ const subContext = (0, workspace_integration_1.detectSubProjectContext)(cwd, filesystem);
47
+ if (subContext) {
48
+ return monorepoLayout(subContext.workspaceRoot);
49
+ }
50
+ // Workspace root directly?
51
+ const layout = (0, workspace_integration_1.detectWorkspaceLayout)(cwd, filesystem);
52
+ if (layout.hasWorkspace) {
53
+ return monorepoLayout(layout.workspaceDir);
54
+ }
55
+ // Walk up to find a workspace.
56
+ const workspaceRoot = (0, workspace_integration_1.findWorkspaceRoot)(cwd, filesystem);
57
+ if (workspaceRoot) {
58
+ return monorepoLayout(workspaceRoot);
59
+ }
60
+ // Fall back to standalone — figure out if it's API or App.
61
+ const isApi = (0, fs_1.existsSync)((0, path_1.join)(cwd, 'src', 'config.env.ts')) || (0, fs_1.existsSync)((0, path_1.join)(cwd, 'nest-cli.json'));
62
+ const isApp = (0, fs_1.existsSync)((0, path_1.join)(cwd, 'nuxt.config.ts'));
63
+ return {
64
+ apiDir: isApi ? cwd : null,
65
+ appDir: isApp ? cwd : null,
66
+ name: readPackageName(cwd) || basename(cwd),
67
+ root: cwd,
68
+ workspace: false,
69
+ };
70
+ }
71
+ function basename(p) {
72
+ return p.replace(/\/+$/, '').split('/').pop() || 'project';
73
+ }
74
+ /** Build the layout for an lt-monorepo workspace root. */
75
+ function monorepoLayout(workspaceRoot) {
76
+ const apiDir = (0, path_1.join)(workspaceRoot, 'projects', 'api');
77
+ const appDir = (0, path_1.join)(workspaceRoot, 'projects', 'app');
78
+ return {
79
+ apiDir: (0, fs_1.existsSync)(apiDir) ? apiDir : null,
80
+ appDir: (0, fs_1.existsSync)(appDir) ? appDir : null,
81
+ name: readPackageName(workspaceRoot) || basename(workspaceRoot),
82
+ root: workspaceRoot,
83
+ workspace: true,
84
+ };
85
+ }
86
+ /** Read the `name` field from `package.json`, scrubbed to just the bare name (no scope). */
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
+ // Strip npm scope: @lenne.tech/foo → foo
96
+ return pkg.name.includes('/') ? pkg.name.split('/').pop() : pkg.name;
97
+ }
98
+ catch (_a) {
99
+ return null;
100
+ }
101
+ }