@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.
@@ -1,148 +0,0 @@
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 child_process_1 = require("child_process");
13
- const fs_1 = require("fs");
14
- const path_1 = require("path");
15
- const local_project_1 = require("../../lib/local-project");
16
- const port_registry_1 = require("../../lib/port-registry");
17
- /**
18
- * Start API + App with project-specific ports.
19
- *
20
- * Reads the slot from the central registry, exports env vars,
21
- * spawns `pnpm start` (api) and `pnpm dev` (app) detached, and
22
- * persists the PIDs to <root>/.lt-local/state.json.
23
- *
24
- * Environment-variable contract injected into spawned children
25
- * (single source of truth — keep in sync with the CLAUDE.md port
26
- * block in `lib/local-patches.ts#patchClaudeMd`):
27
- *
28
- * PORT — Nest (api) / Nuxt dev server (app); slot-derived
29
- * BASE_URL — nest-server config.env.ts (canonical API base)
30
- * APP_URL — nest-server config.env.ts (frontend origin for redirects/CORS)
31
- * NUXT_API_URL — Nuxt vite-proxy target for /api, /iam, …
32
- * NUXT_PUBLIC_API_URL — public, exposed via useRuntimeConfig().public.apiUrl
33
- * NUXT_PUBLIC_SITE_URL — public, used by useRuntimeConfig().public.siteUrl + Playwright
34
- * NUXT_PUBLIC_STORAGE_PREFIX — namespaces sessionStorage/localStorage so parallel
35
- * projects don't share auth tokens (e.g. "crm-local")
36
- * NSC__MONGOOSE__URI — nest-server-config Mongoose URI (only when dbName known)
37
- */
38
- const UpCommand = {
39
- alias: ['u'],
40
- description: 'Start API + App',
41
- hidden: false,
42
- name: 'up',
43
- run: (toolbox) => __awaiter(void 0, void 0, void 0, function* () {
44
- var _a, _b, _c, _d;
45
- const { filesystem, parameters, print: { colors, error, info, success, warning }, } = toolbox;
46
- const layout = (0, local_project_1.resolveLayout)(filesystem.cwd(), filesystem);
47
- const slug = (0, port_registry_1.projectSlug)(layout.root);
48
- const registry = (0, port_registry_1.loadRegistry)();
49
- const entry = registry.projects[slug];
50
- if (!entry) {
51
- error(`Project "${slug}" not registered. Run \`lt local init\` first.`);
52
- if (!parameters.options.fromGluegunMenu)
53
- process.exit(1);
54
- return 'local up: not registered';
55
- }
56
- const ports = (0, port_registry_1.portsForSlot)(entry.slot);
57
- // Already running?
58
- const existing = (0, port_registry_1.loadLocalState)(layout.root);
59
- if (existing &&
60
- ((existing.pids.api && (0, port_registry_1.isPidAlive)(existing.pids.api)) || (existing.pids.app && (0, port_registry_1.isPidAlive)(existing.pids.app)))) {
61
- warning(`Already running (api pid ${(_a = existing.pids.api) !== null && _a !== void 0 ? _a : '-'}, app pid ${(_b = existing.pids.app) !== null && _b !== void 0 ? _b : '-'}). Run \`lt local down\` first.`);
62
- if (!parameters.options.fromGluegunMenu)
63
- process.exit(1);
64
- return 'local up: already running';
65
- }
66
- // Pre-flight port check (single lsof call covers both ports).
67
- const snapshot = yield (0, port_registry_1.listenSnapshot)([ports.api, ports.app]);
68
- for (const p of [ports.api, ports.app]) {
69
- const r = snapshot.get(p);
70
- if (r) {
71
- error(`Port ${p} already in use by ${r.command} (pid ${r.pid}).`);
72
- if (!parameters.options.fromGluegunMenu)
73
- process.exit(1);
74
- return 'local up: port busy';
75
- }
76
- }
77
- const env = Object.assign(Object.assign(Object.assign({}, process.env), { APP_URL: `http://localhost:${ports.app}`, BASE_URL: `http://localhost:${ports.api}`, NUXT_API_URL: `http://localhost:${ports.api}`, NUXT_PUBLIC_API_URL: `http://localhost:${ports.api}`, NUXT_PUBLIC_SITE_URL: `http://localhost:${ports.app}`, NUXT_PUBLIC_STORAGE_PREFIX: `${slug}-local` }), (entry.dbName ? { NSC__MONGOOSE__URI: `mongodb://127.0.0.1/${entry.dbName}` } : {}));
78
- info('');
79
- info(colors.bold(`Starting "${slug}" on slot ${entry.slot}`));
80
- info(` api: http://localhost:${ports.api}`);
81
- info(` app: http://localhost:${ports.app}`);
82
- if (entry.dbName)
83
- info(` db: mongodb://127.0.0.1/${entry.dbName}`);
84
- info('');
85
- const pids = {};
86
- // Allow corporate / pinned setups to override the binary used for `lt local up`.
87
- const pnpmBin = process.env.LT_PNPM_BIN || 'pnpm';
88
- if (layout.apiDir && (0, fs_1.existsSync)((0, path_1.join)(layout.apiDir, 'package.json'))) {
89
- const apiPid = spawnDetached(pnpmBin, ['start'], {
90
- cwd: layout.apiDir,
91
- env: Object.assign(Object.assign({}, env), { PORT: String(ports.api) }),
92
- logFile: (0, path_1.join)(layout.root, '.lt-local', 'api.log'),
93
- });
94
- if (apiPid)
95
- pids.api = apiPid;
96
- }
97
- if (layout.appDir && (0, fs_1.existsSync)((0, path_1.join)(layout.appDir, 'package.json'))) {
98
- const appPid = spawnDetached(pnpmBin, ['dev'], {
99
- cwd: layout.appDir,
100
- env: Object.assign(Object.assign({}, env), { PORT: String(ports.app) }),
101
- logFile: (0, path_1.join)(layout.root, '.lt-local', 'app.log'),
102
- });
103
- if (appPid)
104
- pids.app = appPid;
105
- }
106
- (0, port_registry_1.saveLocalState)(layout.root, { pids, ports, startedAt: new Date().toISOString() });
107
- success(`Started: api pid=${(_c = pids.api) !== null && _c !== void 0 ? _c : '-'}, app pid=${(_d = pids.app) !== null && _d !== void 0 ? _d : '-'}`);
108
- info(colors.dim('Logs: <root>/.lt-local/api.log, <root>/.lt-local/app.log'));
109
- info(colors.dim('Stop with: lt local down'));
110
- if (!parameters.options.fromGluegunMenu)
111
- process.exit();
112
- return `local up: api=${pids.api}, app=${pids.app}`;
113
- }),
114
- };
115
- /**
116
- * Spawn a detached child whose stdio is redirected to a log file.
117
- *
118
- * The parent's copy of the log file descriptor is closed in the `finally`
119
- * block — the child has already inherited its own fd before `spawn` returns,
120
- * so closing here prevents fd leaks and avoids racing-write artifacts on
121
- * filesystems where O_APPEND is not atomic.
122
- */
123
- function spawnDetached(cmd, args, opts) {
124
- (0, fs_1.mkdirSync)((0, path_1.dirname)(opts.logFile), { recursive: true });
125
- const out = (0, fs_1.openSync)(opts.logFile, 'a');
126
- try {
127
- const child = (0, child_process_1.spawn)(cmd, args, {
128
- cwd: opts.cwd,
129
- detached: true,
130
- env: opts.env,
131
- stdio: ['ignore', out, out],
132
- });
133
- child.unref();
134
- return child.pid;
135
- }
136
- catch (_a) {
137
- return undefined;
138
- }
139
- finally {
140
- try {
141
- (0, fs_1.closeSync)(out);
142
- }
143
- catch (_b) {
144
- /* fd already inherited by child; ignore */
145
- }
146
- }
147
- }
148
- module.exports = UpCommand;
@@ -1,118 +0,0 @@
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 port_registry_1 = require("../../lib/port-registry");
13
- /**
14
- * Show port registry overview + live `lsof` state.
15
- *
16
- * `lt ports` → list all reserved + currently bound dev ports
17
- * `lt ports check 3030` → exit-coded check (0 = free, 1 = in use)
18
- * `lt ports scan <dir>` → rebuild registry from filesystem (subcommand)
19
- *
20
- * The default action issues a single `lsof` call (via {@link listenSnapshot})
21
- * and filters in memory rather than spawning lsof per port — see
22
- * `port-registry.ts` for the rationale.
23
- */
24
- const PortsCommand = {
25
- alias: ['p'],
26
- description: 'Inspect port allocation',
27
- hidden: false,
28
- name: 'ports',
29
- run: (toolbox) => __awaiter(void 0, void 0, void 0, function* () {
30
- var _a, _b;
31
- const { parameters, print: { colors, info, success, warning }, } = toolbox;
32
- // Sub-command "check": fast exit-coded port probe.
33
- if (parameters.first === 'check') {
34
- const port = Number(parameters.second);
35
- const fromMenu = Boolean(parameters.options.fromGluegunMenu);
36
- if (!Number.isFinite(port)) {
37
- warning('Usage: lt ports check <port>');
38
- if (!fromMenu)
39
- process.exit(2);
40
- return 'ports check missing-port';
41
- }
42
- const result = yield (0, port_registry_1.checkPortInUse)(port);
43
- if (result === null) {
44
- warning('lsof not available on this system.');
45
- if (!fromMenu)
46
- process.exit(2);
47
- return 'ports check no-lsof';
48
- }
49
- if (result.inUse) {
50
- info(`port ${port}: in use by ${result.command} (pid ${result.pid})`);
51
- if (!fromMenu)
52
- process.exit(1);
53
- return 'ports check in-use';
54
- }
55
- success(`port ${port}: free`);
56
- if (!fromMenu)
57
- process.exit(0);
58
- return 'ports check free';
59
- }
60
- // Default action: combined list. Single lsof call covers the entire slot
61
- // range plus all registry-allocated ports.
62
- const registry = (0, port_registry_1.loadRegistry)();
63
- const projects = Object.entries(registry.projects);
64
- const wanted = new Set();
65
- for (let p = port_registry_1.SLOT_BASE_API; p < port_registry_1.SLOT_PORT_RANGE_END; p++)
66
- wanted.add(p);
67
- for (const [, entry] of projects) {
68
- const ports = (0, port_registry_1.portsForSlot)(entry.slot);
69
- wanted.add(ports.api);
70
- wanted.add(ports.app);
71
- }
72
- const snapshot = yield (0, port_registry_1.listenSnapshot)(wanted);
73
- info('');
74
- info(colors.bold('Reserved ports (registry)'));
75
- info(colors.dim('─'.repeat(60)));
76
- if (projects.length === 0) {
77
- info(colors.dim(' (none yet — run `lt local init` in a project)'));
78
- }
79
- else {
80
- for (const [name, entry] of projects.sort((a, b) => a[1].slot - b[1].slot)) {
81
- const ports = (0, port_registry_1.portsForSlot)(entry.slot);
82
- const apiTag = snapshot.has(ports.api) ? colors.green('●') : colors.dim('○');
83
- const appTag = snapshot.has(ports.app) ? colors.green('●') : colors.dim('○');
84
- info(` ${name.padEnd(28)} api ${apiTag} ${ports.api} app ${appTag} ${ports.app} ${colors.dim(entry.path)}`);
85
- }
86
- }
87
- info('');
88
- info(colors.bold(`Currently bound dev ports (${port_registry_1.SLOT_BASE_API}-${port_registry_1.SLOT_PORT_RANGE_END - 1})`));
89
- info(colors.dim('─'.repeat(60)));
90
- let anyBound = false;
91
- for (let p = port_registry_1.SLOT_BASE_API; p < port_registry_1.SLOT_PORT_RANGE_END; p++) {
92
- const r = snapshot.get(p);
93
- if (!r)
94
- continue;
95
- anyBound = true;
96
- const owner = registryOwner(registry, p);
97
- info(` ${String(p).padEnd(6)} ${(_b = (_a = r.command) === null || _a === void 0 ? void 0 : _a.padEnd(12)) !== null && _b !== void 0 ? _b : ''} pid ${r.pid}${owner ? ` ${colors.dim(owner)}` : ''}`);
98
- }
99
- if (!anyBound)
100
- info(colors.dim(` (no ports in ${port_registry_1.SLOT_BASE_API}-${port_registry_1.SLOT_PORT_RANGE_END - 1} range bound)`));
101
- info('');
102
- if (!parameters.options.fromGluegunMenu) {
103
- process.exit();
104
- }
105
- return 'ports list';
106
- }),
107
- };
108
- function registryOwner(registry, port) {
109
- for (const [name, entry] of Object.entries(registry.projects)) {
110
- const ports = (0, port_registry_1.portsForSlot)(entry.slot);
111
- if (ports.api === port)
112
- return `→ ${name} (api)`;
113
- if (ports.app === port)
114
- return `→ ${name} (app)`;
115
- }
116
- return null;
117
- }
118
- module.exports = PortsCommand;
@@ -1,131 +0,0 @@
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;
@@ -1,175 +0,0 @@
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
- }