@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.
- package/README.md +19 -0
- package/build/commands/local/down.js +71 -0
- package/build/commands/local/init.js +162 -0
- package/build/commands/local/local.js +30 -0
- package/build/commands/local/status.js +69 -0
- package/build/commands/local/up.js +148 -0
- package/build/commands/ports/ports.js +118 -0
- package/build/commands/ports/scan.js +131 -0
- package/build/commands/status.js +37 -6
- package/build/extensions/server.js +14 -4
- package/build/lib/local-patches.js +175 -0
- package/build/lib/local-project.js +101 -0
- package/build/lib/port-registry.js +304 -0
- package/docs/commands.md +189 -0
- package/package.json +7 -10
|
@@ -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;
|
package/build/commands/status.js
CHANGED
|
@@ -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 (
|
|
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 (
|
|
140
|
+
catch (_e) {
|
|
140
141
|
// ignore
|
|
141
142
|
}
|
|
142
143
|
}
|
|
@@ -152,7 +153,7 @@ const StatusCommand = {
|
|
|
152
153
|
});
|
|
153
154
|
}
|
|
154
155
|
}
|
|
155
|
-
catch (
|
|
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 (
|
|
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 (
|
|
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
|
|
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
|
|
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
|
+
}
|