@lenne.tech/cli 1.26.0 → 1.27.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/build/commands/claude/plugins.js +13 -10
- package/build/commands/dev/down.js +12 -0
- package/build/commands/dev/status.js +19 -1
- package/build/commands/dev/test.js +197 -118
- package/build/lib/dev-env-bridge.js +6 -6
- package/build/lib/dev-identity.js +18 -0
- package/build/lib/dev-migrate-helper.js +16 -6
- package/build/lib/dev-patches.js +71 -0
- package/build/lib/dev-process.js +154 -0
- package/build/lib/dev-project.js +20 -1
- package/build/lib/dev-state.js +9 -7
- package/build/lib/dev-test-session.js +410 -0
- package/package.json +1 -1
package/build/lib/dev-patches.js
CHANGED
|
@@ -165,6 +165,16 @@ function patchNuxtConfig(file) {
|
|
|
165
165
|
* bracketed by `// >>> lt-dev:bridge >>>` markers.
|
|
166
166
|
* 2. Hardcoded baseURL/host/url for `http://localhost:3001` →
|
|
167
167
|
* `process.env.NUXT_PUBLIC_SITE_URL || 'http://localhost:3001'`.
|
|
168
|
+
* 3. `webServer` wrapped in an `LT_DEV_ACTIVE` guard so Playwright reuses
|
|
169
|
+
* the App already served by `lt dev` / `lt dev test` instead of spawning
|
|
170
|
+
* its own (which would bind the wrong port and miss the isolated stack).
|
|
171
|
+
* 4. `ignoreHTTPSErrors: true` so Playwright's Chromium accepts the lt dev
|
|
172
|
+
* Caddy self-signed cert (required for `lt dev test` over HTTPS).
|
|
173
|
+
* 5. Shard-aware timeouts gated on `LT_DEV_TEST_SHARDS` — a `SHARDED` const
|
|
174
|
+
* plus relaxed `timeout` / `expect` / `navigationTimeout` / `actionTimeout`
|
|
175
|
+
* under sharded load only, so serial + CI keep their tight, fast-failing
|
|
176
|
+
* defaults. Each sub-patch is a graceful no-op on a non-standard config.
|
|
177
|
+
* 6. `slowMo: 10` → `0` (pointless per-action delay, multiplied across shards).
|
|
168
178
|
*/
|
|
169
179
|
function patchPlaywrightConfig(file) {
|
|
170
180
|
if (!(0, fs_1.existsSync)(file))
|
|
@@ -225,6 +235,67 @@ function patchPlaywrightConfig(file) {
|
|
|
225
235
|
count++;
|
|
226
236
|
}
|
|
227
237
|
}
|
|
238
|
+
// 3. Wrap `webServer` in an `LT_DEV_ACTIVE` guard so Playwright does NOT
|
|
239
|
+
// start/manage its own server when the App is already served by
|
|
240
|
+
// `lt dev` / `lt dev test` (both export LT_DEV_ACTIVE + the App URL).
|
|
241
|
+
// The original array/object's closing `]`/`}` becomes the ternary's
|
|
242
|
+
// false branch, so no bracket-matching is needed. Idempotent.
|
|
243
|
+
if (!/webServer:\s*process\.env\.LT_DEV_ACTIVE/.test(after)) {
|
|
244
|
+
after = after.replace(/webServer:\s*([[{])/, (_match, open) => {
|
|
245
|
+
count++;
|
|
246
|
+
return `webServer: process.env.LT_DEV_ACTIVE ? undefined : ${open}`;
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
// 4. ignoreHTTPSErrors — accept the `lt dev` Caddy self-signed cert on
|
|
250
|
+
// `https://*.localhost` (Playwright's bundled Chromium uses its own trust
|
|
251
|
+
// store, so NODE_EXTRA_CA_CERTS alone is not enough). No-op in CI (http).
|
|
252
|
+
// Without this, `lt dev test` fails with ERR_CERT_AUTHORITY_INVALID.
|
|
253
|
+
if (!/ignoreHTTPSErrors/.test(after)) {
|
|
254
|
+
after = after.replace(/(\n(\s*)use:\s*\{)/, (_m, whole, indent) => {
|
|
255
|
+
count++;
|
|
256
|
+
return `${whole}\n${indent} ignoreHTTPSErrors: true,`;
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
// 5. Shard-aware timeouts — `lt dev test --shard N` runs N built stacks +
|
|
260
|
+
// N Chromium concurrently; the CPU saturates and SSR slows 2-3x. Relax
|
|
261
|
+
// timeouts ONLY under that load (the CLI exports LT_DEV_TEST_SHARDS), so
|
|
262
|
+
// serial + CI keep their tight values and fast-failure feedback. Each
|
|
263
|
+
// sub-patch is idempotent + a graceful no-op on non-standard configs.
|
|
264
|
+
if (!/const SHARDED\b/.test(after) && /export default defineConfig/.test(after)) {
|
|
265
|
+
const shardConst = '// `lt dev test --shard N` saturates the CPU (N built SSR servers + N Chromium),\n' +
|
|
266
|
+
'// slowing every navigation. Relax timeouts ONLY under that load — the CLI sets\n' +
|
|
267
|
+
"// LT_DEV_TEST_SHARDS — so serial + CI keep their tight, fast-failing defaults.\n" +
|
|
268
|
+
"const SHARDED = Number(process.env.LT_DEV_TEST_SHARDS || '0') > 1;\n\n";
|
|
269
|
+
after = after.replace(/(export default defineConfig)/, `${shardConst}$1`);
|
|
270
|
+
count++;
|
|
271
|
+
}
|
|
272
|
+
// 5a. per-test timeout (`isWindows ? A : B` form) → add the sharded branch.
|
|
273
|
+
if (/timeout:\s*isWindows\s*\?/.test(after) && !/timeout:\s*isWindows\s*\?[^,\n]*SHARDED/.test(after)) {
|
|
274
|
+
after = after.replace(/timeout:\s*isWindows\s*\?\s*([0-9_]+)\s*:\s*([0-9_]+|undefined)/, (_m, a, b) => {
|
|
275
|
+
count++;
|
|
276
|
+
return `timeout: isWindows ? ${a} : SHARDED ? 180_000 : ${b}`;
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
// 5b. expect.timeout (only when an `expect: { timeout: N }` already exists).
|
|
280
|
+
if (/expect:\s*\{\s*timeout:\s*[0-9_]+\s*\}/.test(after) && !/expect:\s*\{\s*timeout:\s*SHARDED/.test(after)) {
|
|
281
|
+
after = after.replace(/expect:\s*\{\s*timeout:\s*([0-9_]+)\s*\}/, (_m, t) => {
|
|
282
|
+
count++;
|
|
283
|
+
return `expect: { timeout: SHARDED ? 30_000 : ${t} }`;
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
// 5c. navigation/action ceilings under shard (inject into `use` if absent).
|
|
287
|
+
if (!/navigationTimeout/.test(after)) {
|
|
288
|
+
after = after.replace(/(\n(\s*)use:\s*\{)/, (_m, whole, indent) => {
|
|
289
|
+
count++;
|
|
290
|
+
return `${whole}\n${indent} actionTimeout: SHARDED ? 30_000 : undefined,\n${indent} navigationTimeout: SHARDED ? 60_000 : undefined,`;
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
// 6. slowMo: 10 → 0 — an artificial per-action delay, pointless and multiplied
|
|
294
|
+
// across N concurrent sharded browsers.
|
|
295
|
+
after = after.replace(/slowMo:\s*10\b/, () => {
|
|
296
|
+
count++;
|
|
297
|
+
return 'slowMo: 0';
|
|
298
|
+
});
|
|
228
299
|
if (count === 0)
|
|
229
300
|
return { file, patched: false, replacements: 0 };
|
|
230
301
|
(0, fs_1.writeFileSync)(file, after, 'utf8');
|
package/build/lib/dev-process.js
CHANGED
|
@@ -13,7 +13,11 @@ exports.checkPortInUse = checkPortInUse;
|
|
|
13
13
|
exports.killProcessGroup = killProcessGroup;
|
|
14
14
|
exports.listenSnapshot = listenSnapshot;
|
|
15
15
|
exports.rotateLogFile = rotateLogFile;
|
|
16
|
+
exports.runChildInherit = runChildInherit;
|
|
17
|
+
exports.runChildToFile = runChildToFile;
|
|
16
18
|
exports.spawnDetached = spawnDetached;
|
|
19
|
+
exports.terminateProcessGroup = terminateProcessGroup;
|
|
20
|
+
exports.waitForHttp = waitForHttp;
|
|
17
21
|
/**
|
|
18
22
|
* Process + port helpers for `lt dev`.
|
|
19
23
|
*
|
|
@@ -145,6 +149,60 @@ function rotateLogFile(logFile) {
|
|
|
145
149
|
}
|
|
146
150
|
return { archivePath, previousSize, rotated: true };
|
|
147
151
|
}
|
|
152
|
+
/**
|
|
153
|
+
* Run a child to completion with inherited stdio. Resolves with the exit code.
|
|
154
|
+
*
|
|
155
|
+
* Counterpart of `spawnDetached`: foreground, synchronous-feeling, used for
|
|
156
|
+
* commands the user must see live output from (build, test runners).
|
|
157
|
+
* Errors during spawn resolve as exit code `1` so callers can branch on a
|
|
158
|
+
* single integer instead of try/catch.
|
|
159
|
+
*/
|
|
160
|
+
function runChildInherit(cmd, args, opts) {
|
|
161
|
+
return new Promise((resolve) => {
|
|
162
|
+
const child = (0, child_process_1.spawn)(cmd, args, { cwd: opts.cwd, env: opts.env, stdio: 'inherit' });
|
|
163
|
+
child.on('error', () => resolve(1));
|
|
164
|
+
child.on('close', (code) => resolve(code));
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Run a child to completion with stdout+stderr redirected to a log file.
|
|
169
|
+
* Resolves with the exit code (`1` on spawn error). Like `runChildInherit` but
|
|
170
|
+
* non-interleaving — used to run several children CONCURRENTLY (e.g. parallel
|
|
171
|
+
* Playwright shards) without their console output clobbering each other.
|
|
172
|
+
*/
|
|
173
|
+
function runChildToFile(cmd, args, opts) {
|
|
174
|
+
(0, fs_1.mkdirSync)((0, path_1.dirname)(opts.logFile), { recursive: true });
|
|
175
|
+
// Rotate so each run starts with a fresh log (one prior generation kept as
|
|
176
|
+
// `<logFile>.1`) instead of appending run-on-run.
|
|
177
|
+
rotateLogFile(opts.logFile);
|
|
178
|
+
const out = (0, fs_1.openSync)(opts.logFile, 'a');
|
|
179
|
+
const close = () => {
|
|
180
|
+
try {
|
|
181
|
+
(0, fs_1.closeSync)(out);
|
|
182
|
+
}
|
|
183
|
+
catch (_a) {
|
|
184
|
+
/* already closed */
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
return new Promise((resolve) => {
|
|
188
|
+
let child;
|
|
189
|
+
try {
|
|
190
|
+
child = (0, child_process_1.spawn)(cmd, args, { cwd: opts.cwd, env: opts.env, stdio: ['ignore', out, out] });
|
|
191
|
+
}
|
|
192
|
+
catch (_a) {
|
|
193
|
+
close();
|
|
194
|
+
return resolve(1);
|
|
195
|
+
}
|
|
196
|
+
child.on('error', () => {
|
|
197
|
+
close();
|
|
198
|
+
resolve(1);
|
|
199
|
+
});
|
|
200
|
+
child.on('close', (code) => {
|
|
201
|
+
close();
|
|
202
|
+
resolve(code);
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
}
|
|
148
206
|
/**
|
|
149
207
|
* Spawn a detached child whose stdio is redirected to a log file.
|
|
150
208
|
*
|
|
@@ -188,3 +246,99 @@ function spawnDetached(cmd, args, opts) {
|
|
|
188
246
|
}
|
|
189
247
|
}
|
|
190
248
|
}
|
|
249
|
+
/**
|
|
250
|
+
* Terminate a detached process group RELIABLY: SIGTERM the group, wait up to
|
|
251
|
+
* `graceMs` for it to exit, then SIGKILL anything still alive.
|
|
252
|
+
*
|
|
253
|
+
* Needed because a compiled NestJS API (`node dist`) installs shutdown hooks
|
|
254
|
+
* that catch SIGTERM and can hang on open Mongo connections — a single
|
|
255
|
+
* SIGTERM then "succeeds" (the call returns) while the process keeps
|
|
256
|
+
* listening on its port and holding DB connections. `lt dev test`'s
|
|
257
|
+
* residue-free teardown promise depends on the process actually being gone,
|
|
258
|
+
* so we escalate to SIGKILL after a grace period.
|
|
259
|
+
*
|
|
260
|
+
* Polls every 150ms so a process that exits cleanly returns near-instantly
|
|
261
|
+
* (only a hung process waits the full `graceMs`). Returns true if the process
|
|
262
|
+
* is gone by the end, false if it somehow survived even SIGKILL.
|
|
263
|
+
*/
|
|
264
|
+
function terminateProcessGroup(pid_1) {
|
|
265
|
+
return __awaiter(this, arguments, void 0, function* (pid, graceMs = 4000) {
|
|
266
|
+
if (!(0, dev_state_1.isValidPid)(pid))
|
|
267
|
+
return false;
|
|
268
|
+
if (!(0, dev_state_1.isPidAlive)(pid))
|
|
269
|
+
return true;
|
|
270
|
+
// Phase 1 — graceful: SIGTERM the group (single-PID fallback inside).
|
|
271
|
+
killProcessGroup(pid);
|
|
272
|
+
const deadline = Date.now() + Math.max(0, graceMs);
|
|
273
|
+
while (Date.now() < deadline) {
|
|
274
|
+
if (!(0, dev_state_1.isPidAlive)(pid))
|
|
275
|
+
return true;
|
|
276
|
+
yield delay(150);
|
|
277
|
+
}
|
|
278
|
+
// Phase 2 — forced: SIGKILL the group, then the single PID.
|
|
279
|
+
if (!(0, dev_state_1.isPidAlive)(pid))
|
|
280
|
+
return true;
|
|
281
|
+
try {
|
|
282
|
+
process.kill(-pid, 'SIGKILL');
|
|
283
|
+
}
|
|
284
|
+
catch (_a) {
|
|
285
|
+
/* group already gone or pid is not a group leader */
|
|
286
|
+
}
|
|
287
|
+
try {
|
|
288
|
+
process.kill(pid, 'SIGKILL');
|
|
289
|
+
}
|
|
290
|
+
catch (_b) {
|
|
291
|
+
/* already dead */
|
|
292
|
+
}
|
|
293
|
+
yield delay(150);
|
|
294
|
+
return !(0, dev_state_1.isPidAlive)(pid);
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Poll an HTTPS/HTTP URL until a matching response is observed or `timeoutMs`
|
|
299
|
+
* elapses.
|
|
300
|
+
*
|
|
301
|
+
* Used to wait for dev servers to become reachable before the next step
|
|
302
|
+
* (typically running a test suite). By default treats ANY HTTP status (1xx-5xx)
|
|
303
|
+
* as "up" — a 404 means the server is bound and answering, which is usually
|
|
304
|
+
* what we want to know. Pass `ready` to require a stricter status: an API
|
|
305
|
+
* readiness probe wants a real 2xx on `/meta`, because Caddy answers 502 while
|
|
306
|
+
* its upstream is still booting and the default predicate would accept that
|
|
307
|
+
* prematurely (the cause of the test-suite API-readiness race). Uses `curl`
|
|
308
|
+
* because it is universally available and handles HTTPS-with-self-signed-cert
|
|
309
|
+
* (Caddy) via `-k` for free.
|
|
310
|
+
*
|
|
311
|
+
* Resolves `true` on the first matching response, `false` on timeout. Never rejects.
|
|
312
|
+
*/
|
|
313
|
+
function waitForHttp(url, timeoutMs, ready = (status) => status >= 100 && status < 600) {
|
|
314
|
+
const start = Date.now();
|
|
315
|
+
return new Promise((resolve) => {
|
|
316
|
+
const tick = () => {
|
|
317
|
+
var _a;
|
|
318
|
+
const child = (0, child_process_1.spawn)('curl', ['-sk', '-o', '/dev/null', '-w', '%{http_code}', '--max-time', '2', url], {
|
|
319
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
320
|
+
});
|
|
321
|
+
let status = '';
|
|
322
|
+
(_a = child.stdout) === null || _a === void 0 ? void 0 : _a.on('data', (b) => (status += String(b)));
|
|
323
|
+
child.on('close', () => {
|
|
324
|
+
const code = Number(status.trim());
|
|
325
|
+
// `000` (curl could not connect) parses to 0 → never "ready".
|
|
326
|
+
if (Number.isFinite(code) && code > 0 && ready(code))
|
|
327
|
+
return resolve(true);
|
|
328
|
+
if (Date.now() - start > timeoutMs)
|
|
329
|
+
return resolve(false);
|
|
330
|
+
setTimeout(tick, 500);
|
|
331
|
+
});
|
|
332
|
+
child.on('error', () => {
|
|
333
|
+
if (Date.now() - start > timeoutMs)
|
|
334
|
+
return resolve(false);
|
|
335
|
+
setTimeout(tick, 500);
|
|
336
|
+
});
|
|
337
|
+
};
|
|
338
|
+
tick();
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
/** Promise-based delay used by the graceful→forced termination escalation. */
|
|
342
|
+
function delay(ms) {
|
|
343
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
344
|
+
}
|
package/build/lib/dev-project.js
CHANGED
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.apiNeedsPortPatch = apiNeedsPortPatch;
|
|
4
4
|
exports.appNeedsPortPatch = appNeedsPortPatch;
|
|
5
5
|
exports.deriveDbName = deriveDbName;
|
|
6
|
+
exports.deriveTestDbName = deriveTestDbName;
|
|
6
7
|
exports.resolveLayout = resolveLayout;
|
|
7
8
|
const fs_1 = require("fs");
|
|
8
9
|
const path_1 = require("path");
|
|
@@ -33,7 +34,10 @@ function appNeedsPortPatch(appDir) {
|
|
|
33
34
|
/target:\s*'http:\/\/localhost:3000'/.test(c) ||
|
|
34
35
|
/baseURL:\s*'http:\/\/localhost:3001'/.test(c) ||
|
|
35
36
|
/url:\s*'http:\/\/localhost:3001'/.test(c) ||
|
|
36
|
-
/host:\s*'http:\/\/localhost:3001'/.test(c)
|
|
37
|
+
/host:\s*'http:\/\/localhost:3001'/.test(c) ||
|
|
38
|
+
// Unguarded Playwright `webServer` (no LT_DEV_ACTIVE guard) — patch it so
|
|
39
|
+
// `lt dev test`'s isolated stack is reused instead of a stray server.
|
|
40
|
+
(/webServer:\s*[[{]/.test(c) && !/webServer:\s*process\.env\.LT_DEV_ACTIVE/.test(c)));
|
|
37
41
|
});
|
|
38
42
|
}
|
|
39
43
|
/** Read `dbName` from the API config (defaults to `<slug>-local`). */
|
|
@@ -49,6 +53,21 @@ function deriveDbName(apiDir, slug) {
|
|
|
49
53
|
}
|
|
50
54
|
return `${slug}-local`;
|
|
51
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* Derive the dedicated database name for the `lt dev test` stack from the
|
|
58
|
+
* project's dev DB name. Distinct from both `<…>-local` (developer DB) and
|
|
59
|
+
* the API unit-test DB (`<…>-e2e`), so Playwright E2E never touches developer
|
|
60
|
+
* or API-test data.
|
|
61
|
+
*
|
|
62
|
+
* svl-sports-system-local → svl-sports-system-test
|
|
63
|
+
*
|
|
64
|
+
* Uses the `-test` suffix so it passes test-helper guards that only permit
|
|
65
|
+
* local/test databases (name ending in `-local` | `-ci` | `-e2e` | `-test`).
|
|
66
|
+
*/
|
|
67
|
+
function deriveTestDbName(devDbName) {
|
|
68
|
+
const base = devDbName.replace(/-(local|dev)$/i, '');
|
|
69
|
+
return `${base}-test`;
|
|
70
|
+
}
|
|
52
71
|
/**
|
|
53
72
|
* Resolve layout starting from `cwd`. Walks up to find a workspace if
|
|
54
73
|
* cwd is inside `projects/api/` or `projects/app/`.
|
package/build/lib/dev-state.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.paths = void 0;
|
|
3
|
+
exports.paths = exports.TEST_SESSION_FILE = void 0;
|
|
4
4
|
exports.allocateInternalPort = allocateInternalPort;
|
|
5
5
|
exports.clearSession = clearSession;
|
|
6
6
|
exports.isPidAlive = isPidAlive;
|
|
@@ -28,6 +28,8 @@ const path_1 = require("path");
|
|
|
28
28
|
const REGISTRY_PATH = process.env.LT_DEV_REGISTRY_PATH || (0, path_1.join)((0, os_1.homedir)(), '.lenneTech', 'projects.json');
|
|
29
29
|
const SESSION_DIR = '.lt-dev';
|
|
30
30
|
const SESSION_FILE = 'state.json';
|
|
31
|
+
/** Session file for the ephemeral `lt dev test` stack (runs parallel to the dev session). */
|
|
32
|
+
exports.TEST_SESSION_FILE = 'state.test.json';
|
|
31
33
|
/**
|
|
32
34
|
* Allocate a free internal port for a Caddy upstream.
|
|
33
35
|
*
|
|
@@ -44,8 +46,8 @@ function allocateInternalPort(start, taken) {
|
|
|
44
46
|
throw new Error(`No free internal port in range [${start}, ${start + 1000})`);
|
|
45
47
|
}
|
|
46
48
|
/** Remove session state file (called by `lt dev down`). */
|
|
47
|
-
function clearSession(root) {
|
|
48
|
-
const file = (0, path_1.join)(root, SESSION_DIR,
|
|
49
|
+
function clearSession(root, sessionFile = SESSION_FILE) {
|
|
50
|
+
const file = (0, path_1.join)(root, SESSION_DIR, sessionFile);
|
|
49
51
|
if ((0, fs_1.existsSync)(file)) {
|
|
50
52
|
try {
|
|
51
53
|
(0, fs_1.rmSync)(file);
|
|
@@ -87,8 +89,8 @@ function loadRegistry() {
|
|
|
87
89
|
return { projects: {}, version: 1 };
|
|
88
90
|
}
|
|
89
91
|
/** Load session state for a project root. */
|
|
90
|
-
function loadSession(root) {
|
|
91
|
-
const file = (0, path_1.join)(root, SESSION_DIR,
|
|
92
|
+
function loadSession(root, sessionFile = SESSION_FILE) {
|
|
93
|
+
const file = (0, path_1.join)(root, SESSION_DIR, sessionFile);
|
|
92
94
|
if (!(0, fs_1.existsSync)(file))
|
|
93
95
|
return null;
|
|
94
96
|
try {
|
|
@@ -126,10 +128,10 @@ function saveRegistry(reg) {
|
|
|
126
128
|
}
|
|
127
129
|
}
|
|
128
130
|
/** Persist session state for a project root. */
|
|
129
|
-
function saveSession(root, state) {
|
|
131
|
+
function saveSession(root, state, sessionFile = SESSION_FILE) {
|
|
130
132
|
const dir = (0, path_1.join)(root, SESSION_DIR);
|
|
131
133
|
(0, fs_1.mkdirSync)(dir, { recursive: true });
|
|
132
|
-
(0, fs_1.writeFileSync)((0, path_1.join)(dir,
|
|
134
|
+
(0, fs_1.writeFileSync)((0, path_1.join)(dir, sessionFile), JSON.stringify(state, null, 2), 'utf8');
|
|
133
135
|
}
|
|
134
136
|
/** Collect all internal ports already claimed across the registry. */
|
|
135
137
|
function takenInternalPorts(reg, excludeSlug) {
|