@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.
- package/README.md +16 -10
- package/build/commands/{local/local.js → dev/dev.js} +11 -9
- package/build/commands/dev/doctor.js +110 -0
- package/build/commands/dev/down.js +76 -0
- package/build/commands/dev/install.js +113 -0
- package/build/commands/dev/migrate.js +96 -0
- package/build/commands/dev/status.js +153 -0
- package/build/commands/dev/test.js +191 -0
- package/build/commands/dev/up.js +228 -0
- package/build/commands/fullstack/init.js +36 -2
- package/build/commands/status.js +16 -16
- package/build/lib/caddy.js +169 -0
- package/build/lib/dev-env-bridge.js +109 -0
- package/build/lib/dev-env.js +35 -0
- package/build/lib/dev-identity.js +100 -0
- package/build/lib/dev-migrate-helper.js +81 -0
- package/build/lib/dev-patches.js +190 -0
- package/build/lib/dev-process.js +152 -0
- package/build/lib/{local-project.js → dev-project.js} +23 -38
- package/build/lib/dev-state.js +152 -0
- package/docs/commands.md +111 -98
- package/package.json +1 -1
- package/build/commands/local/down.js +0 -71
- package/build/commands/local/init.js +0 -162
- package/build/commands/local/status.js +0 -69
- package/build/commands/local/up.js +0 -148
- package/build/commands/ports/ports.js +0 -118
- package/build/commands/ports/scan.js +0 -131
- package/build/lib/local-patches.js +0 -175
- package/build/lib/port-registry.js +0 -304
|
@@ -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
|
-
}
|