@lenne.tech/cli 1.21.0 → 1.23.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} +14 -10
- package/build/commands/dev/doctor.js +144 -0
- package/build/commands/dev/down.js +76 -0
- package/build/commands/dev/install.js +172 -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/tunnel.js +142 -0
- package/build/commands/dev/uninstall.js +92 -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 +183 -0
- package/build/lib/cloudflared.js +129 -0
- package/build/lib/dev-env-bridge.js +112 -0
- package/build/lib/dev-env.js +65 -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-service.js +414 -0
- package/build/lib/dev-state.js +152 -0
- package/docs/commands.md +167 -94
- 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,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
|
-
}
|
|
@@ -1,304 +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
|
-
exports.SLOT_PORT_RANGE_END = exports.SLOT_MAX = exports.SLOT_STEP = exports.SLOT_BASE_API = void 0;
|
|
13
|
-
exports.allocatedSlots = allocatedSlots;
|
|
14
|
-
exports.allocateSlot = allocateSlot;
|
|
15
|
-
exports.checkPortInUse = checkPortInUse;
|
|
16
|
-
exports.clearLocalState = clearLocalState;
|
|
17
|
-
exports.isPidAlive = isPidAlive;
|
|
18
|
-
exports.isValidPid = isValidPid;
|
|
19
|
-
exports.listenSnapshot = listenSnapshot;
|
|
20
|
-
exports.loadLocalState = loadLocalState;
|
|
21
|
-
exports.loadRegistry = loadRegistry;
|
|
22
|
-
exports.localStatePath = localStatePath;
|
|
23
|
-
exports.portsForSlot = portsForSlot;
|
|
24
|
-
exports.projectSlug = projectSlug;
|
|
25
|
-
exports.registryPath = registryPath;
|
|
26
|
-
exports.saveLocalState = saveLocalState;
|
|
27
|
-
exports.saveRegistry = saveRegistry;
|
|
28
|
-
exports.slotFromSlug = slotFromSlug;
|
|
29
|
-
/**
|
|
30
|
-
* Port registry helpers for `lt local` and `lt ports`.
|
|
31
|
-
*
|
|
32
|
-
* Provides:
|
|
33
|
-
* - Deterministic slot allocation from a project slug (hash-based, reproducible across machines)
|
|
34
|
-
* - Persistent registry at ~/.lenneTech/ports.json
|
|
35
|
-
* - Live port introspection via `lsof` (single-call snapshot for batch checks)
|
|
36
|
-
* - Process state tracking under <project>/.lt-local/state.json
|
|
37
|
-
*/
|
|
38
|
-
const child_process_1 = require("child_process");
|
|
39
|
-
const fs_1 = require("fs");
|
|
40
|
-
const os_1 = require("os");
|
|
41
|
-
const path_1 = require("path");
|
|
42
|
-
/** Lowest API port: slot 0 maps to 3000/3001, slot 1 to 3010/3011, … */
|
|
43
|
-
exports.SLOT_BASE_API = 3000;
|
|
44
|
-
/** Distance between two adjacent slots' API ports. */
|
|
45
|
-
exports.SLOT_STEP = 10;
|
|
46
|
-
/** Number of slots [0..SLOT_MAX). API range = [SLOT_BASE_API, SLOT_BASE_API + SLOT_MAX*SLOT_STEP). */
|
|
47
|
-
exports.SLOT_MAX = 90;
|
|
48
|
-
/** Highest port (exclusive) covered by the slot range. Useful for live-port sweeps. */
|
|
49
|
-
exports.SLOT_PORT_RANGE_END = exports.SLOT_BASE_API + exports.SLOT_MAX * exports.SLOT_STEP;
|
|
50
|
-
/** All currently allocated slots in the registry. */
|
|
51
|
-
function allocatedSlots(registry) {
|
|
52
|
-
return new Set(Object.values(registry.projects).map((p) => p.slot));
|
|
53
|
-
}
|
|
54
|
-
/**
|
|
55
|
-
* Allocate a slot for a project. Returns deterministic slot from slug if free,
|
|
56
|
-
* otherwise scans linearly until a free slot is found. Throws if all are taken.
|
|
57
|
-
*
|
|
58
|
-
* The linear scan loops `i in [1, SLOT_MAX)` (not `<=`) because `i = 0` is the
|
|
59
|
-
* preferred slot already checked in the fast path above.
|
|
60
|
-
*/
|
|
61
|
-
function allocateSlot(slug, registry) {
|
|
62
|
-
const taken = allocatedSlots(registry);
|
|
63
|
-
const preferred = slotFromSlug(slug);
|
|
64
|
-
if (!taken.has(preferred)) {
|
|
65
|
-
return preferred;
|
|
66
|
-
}
|
|
67
|
-
for (let i = 1; i < exports.SLOT_MAX; i++) {
|
|
68
|
-
const candidate = (preferred + i) % exports.SLOT_MAX;
|
|
69
|
-
if (!taken.has(candidate)) {
|
|
70
|
-
return candidate;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
throw new Error('No free port slot available (all 90 slots are taken).');
|
|
74
|
-
}
|
|
75
|
-
/**
|
|
76
|
-
* Check via `lsof` whether a single TCP port is currently bound by a LISTEN socket.
|
|
77
|
-
*
|
|
78
|
-
* For multi-port checks prefer {@link listenSnapshot} — it issues a single `lsof`
|
|
79
|
-
* call instead of one per port (~50ms vs ~50ms × N).
|
|
80
|
-
*
|
|
81
|
-
* Note: `-iTCP:<port>` selects connections by *service port* — both LISTEN
|
|
82
|
-
* sockets and remote endpoints. We filter explicitly to LISTEN via
|
|
83
|
-
* `-sTCP:LISTEN` AND post-filter the NAME column for `*:<port>` /
|
|
84
|
-
* `<addr>:<port>` (LISTEN) so outgoing connections whose remote port is
|
|
85
|
-
* `<port>` don't trigger a false positive.
|
|
86
|
-
*
|
|
87
|
-
* Returns null if lsof is unavailable.
|
|
88
|
-
*/
|
|
89
|
-
function checkPortInUse(port) {
|
|
90
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
91
|
-
return new Promise((resolve) => {
|
|
92
|
-
var _a;
|
|
93
|
-
const child = (0, child_process_1.spawn)('lsof', ['-iTCP', `-sTCP:LISTEN`, '-nP', `-iTCP:${port}`], {
|
|
94
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
95
|
-
});
|
|
96
|
-
let out = '';
|
|
97
|
-
(_a = child.stdout) === null || _a === void 0 ? void 0 : _a.on('data', (chunk) => (out += chunk.toString()));
|
|
98
|
-
child.on('error', () => resolve(null));
|
|
99
|
-
child.on('close', () => {
|
|
100
|
-
const lines = out.trim().split('\n').slice(1); // skip header
|
|
101
|
-
const portRe = new RegExp(`:${port}\\s+\\(LISTEN\\)\\s*$`);
|
|
102
|
-
const match = lines.find((l) => portRe.test(l));
|
|
103
|
-
if (!match) {
|
|
104
|
-
resolve({ inUse: false });
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
const cols = match.split(/\s+/);
|
|
108
|
-
resolve({ command: cols[0], inUse: true, pid: Number(cols[1]) });
|
|
109
|
-
});
|
|
110
|
-
});
|
|
111
|
-
});
|
|
112
|
-
}
|
|
113
|
-
/** Reset state file to an empty record. Called by `lt local down` after stopping processes. */
|
|
114
|
-
function clearLocalState(projectPath) {
|
|
115
|
-
const path = localStatePath(projectPath);
|
|
116
|
-
if ((0, fs_1.existsSync)(path)) {
|
|
117
|
-
(0, fs_1.writeFileSync)(path, `${JSON.stringify({ pids: {}, ports: { api: 0, app: 0 }, startedAt: '' }, null, 2)}\n`, 'utf8');
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
/**
|
|
121
|
-
* Check whether a PID is still alive without sending any signal.
|
|
122
|
-
* `process.kill(pid, 0)` performs a permission check; ESRCH means dead.
|
|
123
|
-
*
|
|
124
|
-
* Refuses non-positive / non-integer PIDs to prevent accidental probes
|
|
125
|
-
* of process groups (negative PID) or every user-owned process (PID 0/-1).
|
|
126
|
-
*/
|
|
127
|
-
function isPidAlive(pid) {
|
|
128
|
-
if (!Number.isInteger(pid) || pid <= 0)
|
|
129
|
-
return false;
|
|
130
|
-
try {
|
|
131
|
-
process.kill(pid, 0);
|
|
132
|
-
return true;
|
|
133
|
-
}
|
|
134
|
-
catch (e) {
|
|
135
|
-
return e.code === 'EPERM';
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
/**
|
|
139
|
-
* Validate a value parsed from `state.json` as a plausible PID.
|
|
140
|
-
*
|
|
141
|
-
* Accepts: positive integer in [100, 2^31 - 1] or undefined.
|
|
142
|
-
* The lower bound 100 excludes init / kernel / login PIDs that should
|
|
143
|
-
* never be the result of a `pnpm start` spawn.
|
|
144
|
-
*/
|
|
145
|
-
function isValidPid(value) {
|
|
146
|
-
if (value === undefined)
|
|
147
|
-
return true;
|
|
148
|
-
if (typeof value !== 'number')
|
|
149
|
-
return false;
|
|
150
|
-
return Number.isInteger(value) && value >= 100 && value <= 0x7fffffff;
|
|
151
|
-
}
|
|
152
|
-
/**
|
|
153
|
-
* One-shot listener snapshot for an arbitrary set of ports.
|
|
154
|
-
*
|
|
155
|
-
* Issues a single `lsof -iTCP -sTCP:LISTEN -nP` call and filters in memory.
|
|
156
|
-
* ~50ms total regardless of port count, vs ~50ms × N for sequential
|
|
157
|
-
* {@link checkPortInUse} calls.
|
|
158
|
-
*
|
|
159
|
-
* Returns an empty Map if `lsof` is unavailable.
|
|
160
|
-
*/
|
|
161
|
-
function listenSnapshot(ports) {
|
|
162
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
163
|
-
return new Promise((resolve) => {
|
|
164
|
-
var _a;
|
|
165
|
-
const child = (0, child_process_1.spawn)('lsof', ['-iTCP', '-sTCP:LISTEN', '-nP'], { stdio: ['ignore', 'pipe', 'ignore'] });
|
|
166
|
-
let out = '';
|
|
167
|
-
(_a = child.stdout) === null || _a === void 0 ? void 0 : _a.on('data', (chunk) => (out += chunk.toString()));
|
|
168
|
-
child.on('error', () => resolve(new Map()));
|
|
169
|
-
child.on('close', () => {
|
|
170
|
-
const wanted = new Set(ports);
|
|
171
|
-
const result = new Map();
|
|
172
|
-
const lines = out.trim().split('\n').slice(1);
|
|
173
|
-
const re = /:(\d+)\s+\(LISTEN\)\s*$/;
|
|
174
|
-
for (const line of lines) {
|
|
175
|
-
const m = re.exec(line);
|
|
176
|
-
if (!m)
|
|
177
|
-
continue;
|
|
178
|
-
const port = Number(m[1]);
|
|
179
|
-
if (!wanted.has(port))
|
|
180
|
-
continue;
|
|
181
|
-
if (result.has(port))
|
|
182
|
-
continue; // first hit wins (IPv4 before IPv6)
|
|
183
|
-
const cols = line.split(/\s+/);
|
|
184
|
-
result.set(port, { command: cols[0], pid: Number(cols[1]) });
|
|
185
|
-
}
|
|
186
|
-
resolve(result);
|
|
187
|
-
});
|
|
188
|
-
});
|
|
189
|
-
});
|
|
190
|
-
}
|
|
191
|
-
/**
|
|
192
|
-
* Load the local state JSON for a project.
|
|
193
|
-
*
|
|
194
|
-
* Returns null when the file is missing, unreadable, malformed, or contains
|
|
195
|
-
* structurally invalid data (see {@link isValidPid}). The schema validation
|
|
196
|
-
* here is the authoritative gate: it prevents `process.kill(-pid, …)` in
|
|
197
|
-
* `lt local down` from receiving anything but a plausible PID we ourselves
|
|
198
|
-
* could have written.
|
|
199
|
-
*/
|
|
200
|
-
function loadLocalState(projectPath) {
|
|
201
|
-
const path = localStatePath(projectPath);
|
|
202
|
-
if (!(0, fs_1.existsSync)(path))
|
|
203
|
-
return null;
|
|
204
|
-
try {
|
|
205
|
-
const parsed = JSON.parse((0, fs_1.readFileSync)(path, 'utf8'));
|
|
206
|
-
if (!parsed || typeof parsed !== 'object')
|
|
207
|
-
return null;
|
|
208
|
-
const obj = parsed;
|
|
209
|
-
const pids = obj.pids;
|
|
210
|
-
const ports = obj.ports;
|
|
211
|
-
if (!pids || typeof pids !== 'object' || !ports || typeof ports !== 'object')
|
|
212
|
-
return null;
|
|
213
|
-
if (!isValidPid(pids.api) || !isValidPid(pids.app))
|
|
214
|
-
return null;
|
|
215
|
-
if (typeof ports.api !== 'number' || typeof ports.app !== 'number')
|
|
216
|
-
return null;
|
|
217
|
-
if (typeof obj.startedAt !== 'string')
|
|
218
|
-
return null;
|
|
219
|
-
return {
|
|
220
|
-
pids: { api: pids.api, app: pids.app },
|
|
221
|
-
ports: { api: ports.api, app: ports.app },
|
|
222
|
-
startedAt: obj.startedAt,
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
catch (_a) {
|
|
226
|
-
return null;
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
/**
|
|
230
|
-
* Load registry; returns empty if missing or corrupt.
|
|
231
|
-
*
|
|
232
|
-
* Prints a warning when a corrupt or schema-incompatible file is encountered
|
|
233
|
-
* so the user notices the silent reset rather than discovering stale port
|
|
234
|
-
* allocations later.
|
|
235
|
-
*/
|
|
236
|
-
function loadRegistry() {
|
|
237
|
-
const path = registryPath();
|
|
238
|
-
if (!(0, fs_1.existsSync)(path)) {
|
|
239
|
-
return { projects: {}, version: 1 };
|
|
240
|
-
}
|
|
241
|
-
try {
|
|
242
|
-
const raw = (0, fs_1.readFileSync)(path, 'utf8');
|
|
243
|
-
const parsed = JSON.parse(raw);
|
|
244
|
-
if ((parsed === null || parsed === void 0 ? void 0 : parsed.version) !== 1 || typeof (parsed === null || parsed === void 0 ? void 0 : parsed.projects) !== 'object') {
|
|
245
|
-
console.warn(`[lt] ports.json has wrong schema (got version=${parsed === null || parsed === void 0 ? void 0 : parsed.version}); starting with empty registry.`);
|
|
246
|
-
return { projects: {}, version: 1 };
|
|
247
|
-
}
|
|
248
|
-
return parsed;
|
|
249
|
-
}
|
|
250
|
-
catch (e) {
|
|
251
|
-
console.warn(`[lt] ports.json was unreadable (${e.message}); starting with empty registry.`);
|
|
252
|
-
return { projects: {}, version: 1 };
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
/** Path to the local state file inside a project. */
|
|
256
|
-
function localStatePath(projectPath) {
|
|
257
|
-
return (0, path_1.join)(projectPath, '.lt-local', 'state.json');
|
|
258
|
-
}
|
|
259
|
-
/** Convert a slot to its API+App port pair. */
|
|
260
|
-
function portsForSlot(slot) {
|
|
261
|
-
const api = exports.SLOT_BASE_API + slot * exports.SLOT_STEP;
|
|
262
|
-
return { api, app: api + 1 };
|
|
263
|
-
}
|
|
264
|
-
/** Convert any project path → a stable slug (basename, lowercase, alpha-num + dashes). */
|
|
265
|
-
function projectSlug(projectPath) {
|
|
266
|
-
const base = projectPath.replace(/\/+$/, '').split('/').pop() || 'project';
|
|
267
|
-
return base
|
|
268
|
-
.toLowerCase()
|
|
269
|
-
.replace(/[^a-z0-9]+/g, '-')
|
|
270
|
-
.replace(/^-+|-+$/g, '');
|
|
271
|
-
}
|
|
272
|
-
/**
|
|
273
|
-
* Path to the central registry.
|
|
274
|
-
*
|
|
275
|
-
* Honors `LT_PORTS_REGISTRY_PATH` for tests / non-default workspaces;
|
|
276
|
-
* falls back to `~/.lenneTech/ports.json`.
|
|
277
|
-
*/
|
|
278
|
-
function registryPath() {
|
|
279
|
-
return process.env.LT_PORTS_REGISTRY_PATH || (0, path_1.join)((0, os_1.homedir)(), '.lenneTech', 'ports.json');
|
|
280
|
-
}
|
|
281
|
-
/** Persist local state to <project>/.lt-local/state.json (creates the parent directory if needed). */
|
|
282
|
-
function saveLocalState(projectPath, state) {
|
|
283
|
-
const path = localStatePath(projectPath);
|
|
284
|
-
(0, fs_1.mkdirSync)((0, path_1.dirname)(path), { recursive: true });
|
|
285
|
-
(0, fs_1.writeFileSync)(path, `${JSON.stringify(state, null, 2)}\n`, 'utf8');
|
|
286
|
-
}
|
|
287
|
-
/** Save registry, creating parent directory if needed. */
|
|
288
|
-
function saveRegistry(registry) {
|
|
289
|
-
const path = registryPath();
|
|
290
|
-
(0, fs_1.mkdirSync)((0, path_1.dirname)(path), { recursive: true });
|
|
291
|
-
(0, fs_1.writeFileSync)(path, `${JSON.stringify(registry, null, 2)}\n`, 'utf8');
|
|
292
|
-
}
|
|
293
|
-
/**
|
|
294
|
-
* Deterministic slot from project slug — same slug yields the same slot on every machine.
|
|
295
|
-
* Uses the lower 32 bits of FNV-1a then modulo SLOT_MAX.
|
|
296
|
-
*/
|
|
297
|
-
function slotFromSlug(slug) {
|
|
298
|
-
let hash = 2166136261; // FNV-1a 32-bit offset basis
|
|
299
|
-
for (let i = 0; i < slug.length; i++) {
|
|
300
|
-
hash ^= slug.charCodeAt(i);
|
|
301
|
-
hash = Math.imul(hash, 16777619);
|
|
302
|
-
}
|
|
303
|
-
return Math.abs(hash | 0) % exports.SLOT_MAX;
|
|
304
|
-
}
|