@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.
@@ -0,0 +1,410 @@
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.autoShardCount = autoShardCount;
13
+ exports.bringUpTestSession = bringUpTestSession;
14
+ exports.hasTestSession = hasTestSession;
15
+ exports.resolveTestSession = resolveTestSession;
16
+ exports.runShardedTestSession = runShardedTestSession;
17
+ exports.tearDownAllTestSessions = tearDownAllTestSessions;
18
+ exports.tearDownTestSession = tearDownTestSession;
19
+ /**
20
+ * Ephemeral, isolated test session for `lt dev test`.
21
+ *
22
+ * Brings up a SECOND, fully separate stack (own URLs, own internal ports,
23
+ * own Caddy block, own database) that runs PARALLEL to — and never touches —
24
+ * the developer's `lt dev up` session. Used to run the Playwright E2E suite
25
+ * against a clean, dedicated database so a developer can keep working in their
26
+ * own environment while tests run, and so a test run never pollutes dev data.
27
+ *
28
+ * Topology (for slug `svl`):
29
+ * - dev session : svl.localhost / api.svl.localhost → db `<…>-local`
30
+ * - test session : svl-test.localhost / api.svl-test.… → db `<…>-test`
31
+ *
32
+ * Both halves run BUILT for speed + prod-fidelity: the API COMPILED (`node dist`,
33
+ * ts-node intermittently dies mid-run) and the App as the production Nitro output
34
+ * (`nuxt build` → `node .output/server/index.mjs`, no Vite cold-compile). Each
35
+ * falls back to its dev runner (`pnpm start` / `pnpm dev`) when no build output is
36
+ * found. bringUp waits for a real 2xx on the API `/meta` before returning so the
37
+ * suite never starts against a not-yet-serving API.
38
+ *
39
+ * Lifecycle: `bringUpTestSession` → run Playwright → `tearDownTestSession`.
40
+ * Teardown is idempotent and residue-free (processes, Caddy block, env bridge,
41
+ * session file, registry entry), so a stale session is always safely reclaimed.
42
+ */
43
+ const fs_1 = require("fs");
44
+ const os_1 = require("os");
45
+ const path_1 = require("path");
46
+ const caddy_1 = require("./caddy");
47
+ const dev_env_1 = require("./dev-env");
48
+ const dev_env_bridge_1 = require("./dev-env-bridge");
49
+ const dev_identity_1 = require("./dev-identity");
50
+ const dev_process_1 = require("./dev-process");
51
+ const dev_project_1 = require("./dev-project");
52
+ const dev_state_1 = require("./dev-state");
53
+ const TEST_API_LOG = 'api.test.log';
54
+ const TEST_APP_LOG = 'app.test.log';
55
+ const TEST_BRIDGE_FILE = '.env.test';
56
+ /** Internal port band for the test stack — distinct from the dev band (4000+). */
57
+ const TEST_PORT_BASE = 4500;
58
+ /**
59
+ * Heuristic for the default local shard count (`--shard auto` / bare `--shard`).
60
+ *
61
+ * Unlike CI — where each shard gets its OWN container (CPU + RAM), so N is just
62
+ * the runner-matrix width — local shards all share ONE machine: every shard runs
63
+ * a built Nuxt/Nitro server + headless Chromium + a compiled API, which together
64
+ * peak at ~2 PERFORMANCE cores during SSR render. The catch is headroom: once the
65
+ * shards' peak demand reaches the perf-core count there is nothing left for the
66
+ * OS / mongod / orchestrator, SSR slows 2-3x, and timing-sensitive navigations
67
+ * FAIL no matter how generous their timeout (true over-subscription).
68
+ *
69
+ * Measured on an M2 Max (8 perf + 4 eff cores, 12 logical) on a heavy built-SSR
70
+ * suite: N=2 → 7.4 min, 0 failures (stable); N=3 → 8.7 min, flaky; N=4 → 6-10 min
71
+ * (high variance), flaky. So the stable optimum is ~perfCores/4 — half the perf
72
+ * cores busy, half free as headroom. On Apple silicon ~2/3 of logical cores are
73
+ * perf cores, so `logical/6 ≈ perfCores/4`. This default deliberately FAVOURS a
74
+ * green, repeatable run over the fastest-on-paper N. Cap by RAM (~4 GB/shard),
75
+ * clamp to [2, 8].
76
+ *
77
+ * A LIGHTER suite (no built SSR, fast tests) or a bigger box can take more —
78
+ * override with an explicit `--shard N`. Always measure N vs N±1 (wall-clock AND
79
+ * flakes) to tune. Higher N also needs generous navigation timeouts under load
80
+ * (see the project's shard-aware `LT_DEV_TEST_SHARDS` timeout handling).
81
+ */
82
+ function autoShardCount() {
83
+ const logical = (0, os_1.cpus)().length || 4;
84
+ const byCpu = Math.floor(logical / 6);
85
+ const byRam = Math.floor((0, os_1.totalmem)() / Math.pow(1024, 3) / 4);
86
+ return Math.max(2, Math.min(byCpu, byRam, 8));
87
+ }
88
+ /**
89
+ * Bring up the isolated test stack. Tears down any stale test session first,
90
+ * so this is safe to call even if a previous run crashed.
91
+ */
92
+ function bringUpTestSession(layout_1, baseIdentity_1, log_1) {
93
+ return __awaiter(this, arguments, void 0, function* (layout, baseIdentity, log, opts = {}) {
94
+ const { shardIndex, skipBuild } = opts;
95
+ const names = testStackNames(shardIndex);
96
+ const { dbName, testIdentity } = resolveTestSession(layout, baseIdentity, shardIndex);
97
+ // Always start from a clean slate — reclaim a stale/crashed test session.
98
+ yield tearDownTestSession(layout, baseIdentity, log, { shardIndex, silent: true });
99
+ // Allocate internal ports (avoid every other registry entry incl. the dev
100
+ // session AND already-running sibling shards, plus anything currently
101
+ // listening). Sibling shards are registered before the next one allocates,
102
+ // so each shard lands on its own port pair.
103
+ const reg = (0, dev_state_1.loadRegistry)();
104
+ const taken = (0, dev_state_1.takenInternalPorts)(reg, testIdentity.slug);
105
+ const apiPort = layout.apiDir ? (0, dev_state_1.allocateInternalPort)(TEST_PORT_BASE, taken) : undefined;
106
+ if (apiPort)
107
+ taken.add(apiPort);
108
+ const appPort = layout.appDir ? (0, dev_state_1.allocateInternalPort)(TEST_PORT_BASE, taken) : undefined;
109
+ const portsToCheck = [apiPort, appPort].filter((p) => typeof p === 'number');
110
+ const snap = yield (0, dev_process_1.listenSnapshot)(portsToCheck);
111
+ for (const p of portsToCheck) {
112
+ const r = snap.get(p);
113
+ if (r)
114
+ throw new Error(`test internal port ${p} already in use by ${r.command} (pid ${r.pid}).`);
115
+ }
116
+ // Caddy block for the test URLs.
117
+ const routes = [];
118
+ if (testIdentity.subdomains.api && apiPort)
119
+ routes.push({ hostname: testIdentity.subdomains.api.hostname, upstreamPort: apiPort });
120
+ if (testIdentity.subdomains.app && appPort)
121
+ routes.push({ hostname: testIdentity.subdomains.app.hostname, upstreamPort: appPort });
122
+ if (routes.length === 0)
123
+ throw new Error('test session has no subdomains to expose (need an app project).');
124
+ (0, caddy_1.upsertProjectBlock)(testIdentity.slug, routes);
125
+ const reload = yield (0, caddy_1.reloadCaddy)();
126
+ if (!reload.ok)
127
+ throw new Error(`caddy reload failed:\n${reload.stderr}`);
128
+ const apiUrl = testIdentity.subdomains.api ? `https://${testIdentity.subdomains.api.hostname}` : '';
129
+ const appUrl = testIdentity.subdomains.app ? `https://${testIdentity.subdomains.app.hostname}` : '';
130
+ log.info('');
131
+ log.info(`Starting isolated test stack "${testIdentity.slug}"`);
132
+ if (appUrl)
133
+ log.info(` app: ${appUrl} → 127.0.0.1:${appPort}`);
134
+ if (apiUrl)
135
+ log.info(` api: ${apiUrl} → 127.0.0.1:${apiPort}`);
136
+ log.info(` db: mongodb://127.0.0.1/${dbName} (reset before the suite by Playwright global-setup)`);
137
+ log.info('');
138
+ const devEnv = (0, dev_env_1.buildDevEnv)({
139
+ apiInternalPort: apiPort !== null && apiPort !== void 0 ? apiPort : 0,
140
+ appInternalPort: appPort !== null && appPort !== void 0 ? appPort : 0,
141
+ baseEnv: process.env,
142
+ dbName,
143
+ identity: testIdentity,
144
+ });
145
+ const pnpmBin = process.env.LT_PNPM_BIN || 'pnpm';
146
+ const pids = {};
147
+ // --- API: compiled (`node dist`) for stability; fall back to `pnpm start`.
148
+ // `skipBuild` (sibling shards) reuses the dist the first shard produced. ---
149
+ if (layout.apiDir && apiPort) {
150
+ let build = 0;
151
+ if (!skipBuild) {
152
+ log.info(log.dim('Building API (compiled, for stable long runs) …'));
153
+ build = yield (0, dev_process_1.runChildInherit)(pnpmBin, ['run', 'build'], { cwd: layout.apiDir, env: process.env });
154
+ }
155
+ const entry = ['dist/src/main.js', 'dist/main.js']
156
+ .map((rel) => (0, path_1.join)(layout.apiDir, rel))
157
+ .find((p) => (0, fs_1.existsSync)(p));
158
+ const apiEnv = Object.assign(Object.assign({}, devEnv.api.env), { NODE_ENV: 'local' });
159
+ let apiSpawn;
160
+ if (build === 0 && entry) {
161
+ apiSpawn = (0, dev_process_1.spawnDetached)('node', [entry], {
162
+ cwd: layout.apiDir,
163
+ env: apiEnv,
164
+ logFile: (0, path_1.join)(layout.root, '.lt-dev', names.apiLog),
165
+ });
166
+ }
167
+ else {
168
+ log.warn('compiled API not available — falling back to `pnpm start` (ts-node).');
169
+ apiSpawn = (0, dev_process_1.spawnDetached)(pnpmBin, ['start'], {
170
+ cwd: layout.apiDir,
171
+ env: apiEnv,
172
+ logFile: (0, path_1.join)(layout.root, '.lt-dev', names.apiLog),
173
+ });
174
+ }
175
+ if (apiSpawn)
176
+ pids.api = apiSpawn.pid;
177
+ }
178
+ // --- App: BUILT (`nuxt build` → `node .output/server/index.mjs`) for speed +
179
+ // prod-fidelity; fall back to the Nuxt dev server when no build output exists.
180
+ // The built server has no Vite cold-compile (which dominates a dev-mode suite
181
+ // — ~84% of runtime) and runs the SAME production bundle a deployment ships.
182
+ // buildDevEnv sets NUXT_PUBLIC_API_PROXY=false, so the built app talks
183
+ // cross-origin to the test API exactly like prod (the injected session cookie
184
+ // must be a cross-subdomain DOMAIN cookie — see the project's parseCookieHeader).
185
+ // Rebuilt every run so the suite never hits stale code (no build-skip / reuse). ---
186
+ if (layout.appDir && appPort) {
187
+ let appBuild = 0;
188
+ if (!skipBuild) {
189
+ log.info(log.dim('Building App (nuxt build, for speed + prod-fidelity) …'));
190
+ appBuild = yield (0, dev_process_1.runChildInherit)(pnpmBin, ['run', 'build'], { cwd: layout.appDir, env: devEnv.app.env });
191
+ }
192
+ const appEntry = ['.output/server/index.mjs']
193
+ .map((rel) => (0, path_1.join)(layout.appDir, rel))
194
+ .find((p) => (0, fs_1.existsSync)(p));
195
+ let appSpawn;
196
+ if (appBuild === 0 && appEntry) {
197
+ appSpawn = (0, dev_process_1.spawnDetached)('node', [appEntry], {
198
+ cwd: layout.appDir,
199
+ env: devEnv.app.env,
200
+ logFile: (0, path_1.join)(layout.root, '.lt-dev', names.appLog),
201
+ });
202
+ }
203
+ else {
204
+ log.warn('built app not available — falling back to `pnpm dev` (slower: cold-compiles routes).');
205
+ appSpawn = (0, dev_process_1.spawnDetached)(pnpmBin, ['dev'], {
206
+ cwd: layout.appDir,
207
+ env: devEnv.app.env,
208
+ logFile: (0, path_1.join)(layout.root, '.lt-dev', names.appLog),
209
+ });
210
+ }
211
+ if (appSpawn)
212
+ pids.app = appSpawn.pid;
213
+ }
214
+ // Persist test session + registry entry (so ports are reserved + status sees it).
215
+ (0, dev_state_1.saveSession)(layout.root, { pids, startedAt: new Date().toISOString() }, names.sessionFile);
216
+ const subdomainMap = {};
217
+ for (const [k, v] of Object.entries(testIdentity.subdomains))
218
+ subdomainMap[k] = v.hostname;
219
+ reg.projects[testIdentity.slug] = {
220
+ dbName,
221
+ internalPorts: { api: apiPort, app: appPort },
222
+ lastUsedAt: new Date().toISOString(),
223
+ path: layout.root,
224
+ subdomains: subdomainMap,
225
+ };
226
+ (0, dev_state_1.saveRegistry)(reg);
227
+ // ENV bridge for external tooling (kept separate from the dev `.env`).
228
+ (0, dev_env_bridge_1.writeEnvBridge)(layout.root, devEnv, dbName, names.bridgeFile);
229
+ // Wait for the test App to answer (best-effort).
230
+ if (appUrl) {
231
+ log.info(log.dim(`Waiting for ${appUrl} …`));
232
+ yield (0, dev_process_1.waitForHttp)(appUrl, 90000);
233
+ }
234
+ // Wait for the test API to actually SERVE (real 2xx on /meta) before handing
235
+ // off to Playwright. Previously bringUp only waited for the App, so a compiled
236
+ // API still connecting to Mongo made the first specs skip via the suite's
237
+ // `ensureApiReachableOrSkip` guard (the API-readiness race). A strict 2xx is
238
+ // required: Caddy answers 502 while its upstream is still booting, which the
239
+ // default (lenient) predicate would accept as "up".
240
+ if (apiUrl) {
241
+ log.info(log.dim(`Waiting for ${apiUrl}/meta …`));
242
+ const apiReady = yield (0, dev_process_1.waitForHttp)(`${apiUrl}/meta`, 120000, (status) => status >= 200 && status < 300);
243
+ if (!apiReady)
244
+ log.warn(`Test API did not answer 2xx on ${apiUrl}/meta within 120s — the first specs may skip.`);
245
+ }
246
+ return { apiUrl, appEnv: devEnv.app.env, appUrl, dbName, pids, testIdentity };
247
+ });
248
+ }
249
+ /** True when a test session file exists (used by status/down). */
250
+ function hasTestSession(root) {
251
+ return (0, dev_state_1.loadSession)(root, dev_state_1.TEST_SESSION_FILE) !== null;
252
+ }
253
+ /** Build the dedicated test identity + test DB name for a project. */
254
+ function resolveTestSession(layout, baseIdentity, shardIndex) {
255
+ const names = testStackNames(shardIndex);
256
+ const testIdentity = (0, dev_identity_1.buildTestIdentity)(baseIdentity, names.identitySuffix);
257
+ const dbName = (0, dev_project_1.deriveTestDbName)((0, dev_project_1.deriveDbName)(layout.apiDir, baseIdentity.slug)) + names.dbSuffix;
258
+ return { dbName, testIdentity };
259
+ }
260
+ /**
261
+ * Run the suite SHARDED across `total` fully-isolated stacks in parallel — the
262
+ * local equivalent of the CI `parallel: N` + `--shard=i/N` matrix, but on one
263
+ * machine: each shard gets its own URLs/ports/Caddy block AND its own DB
264
+ * (`<…>-test-<i>`), so there is zero cross-shard data contention (the reason
265
+ * in-process `workers > 1` against a single stack produces false results —
266
+ * `cleanupTestEntities` / "pick any active season" collide).
267
+ *
268
+ * The first shard builds the API + App; siblings reuse that build (`skipBuild`),
269
+ * since the bundles are shard-agnostic (URLs come from runtime env). Stacks are
270
+ * brought up sequentially (builds + Caddy reloads serialise cleanly), then the
271
+ * N Playwright `--shard=i/N` processes run CONCURRENTLY, each against its own
272
+ * stack, output captured to `.lt-dev/shard.<i>.test.log`. Returns 0 iff every
273
+ * shard passed. Teardown is the caller's responsibility (so `--keep` works).
274
+ */
275
+ function runShardedTestSession(layout, baseIdentity, log, opts) {
276
+ return __awaiter(this, void 0, void 0, function* () {
277
+ const total = Math.max(2, Math.floor(opts.total));
278
+ const contexts = [];
279
+ // Bring up the N isolated stacks sequentially (shard 1 builds; 2..N reuse).
280
+ for (let index = 1; index <= total; index++) {
281
+ log.info('');
282
+ log.info(`▶ shard ${index}/${total}: bringing up isolated stack …`);
283
+ const ctx = yield bringUpTestSession(layout, baseIdentity, log, { shardIndex: index, skipBuild: index > 1 });
284
+ contexts.push({ ctx, index });
285
+ }
286
+ // Run the N Playwright shards CONCURRENTLY, each against its own stack/DB.
287
+ log.info('');
288
+ log.info(`Running ${total} Playwright shards in parallel (one isolated stack each) …`);
289
+ const appDir = layout.appDir;
290
+ const results = yield Promise.all(contexts.map((_a) => __awaiter(this, [_a], void 0, function* ({ ctx, index }) {
291
+ // `LT_DEV_TEST_SHARDS` signals to the project's playwright.config that the
292
+ // suite runs under concurrent sharded load, so it can relax navigation /
293
+ // test timeouts (N built SSR servers + N Chromium saturate the CPU and slow
294
+ // every navigation) without loosening them for serial runs.
295
+ const env = Object.assign(Object.assign({}, ctx.appEnv), { LT_DEV_TEST_SHARDS: String(total), MONGO_URI: `mongodb://127.0.0.1/${ctx.dbName}` });
296
+ const logFile = (0, path_1.join)(layout.root, '.lt-dev', `shard.${index}.test.log`);
297
+ // Invoke Playwright DIRECTLY via `pnpm exec` (NOT `pnpm run test:e2e -- …`):
298
+ // forwarding option flags through `pnpm run`'s `--` is unreliable — pnpm
299
+ // passed the separator on to Playwright, which then read `--shard`/
300
+ // `--reporter` as file FILTERS (not options) → every shard ran the whole
301
+ // suite. `pnpm exec` hands args straight to the binary (mirrors CI).
302
+ const args = ['exec', 'playwright', 'test', `--shard=${index}/${total}`, '--reporter=line', ...opts.forwarded];
303
+ const code = yield (0, dev_process_1.runChildToFile)(opts.pnpmBin, args, { cwd: appDir, env, logFile });
304
+ return { code, index, logFile };
305
+ })));
306
+ // Aggregate per-shard exit codes into a single result.
307
+ let failed = 0;
308
+ log.info('');
309
+ for (const r of results.sort((a, b) => a.index - b.index)) {
310
+ const ok = r.code === 0;
311
+ if (!ok)
312
+ failed++;
313
+ log.info(` shard ${r.index}/${total}: ${ok ? 'passed' : `FAILED (exit ${r.code})`} (log: ${r.logFile})`);
314
+ }
315
+ return failed === 0 ? 0 : 1;
316
+ });
317
+ }
318
+ /**
319
+ * Tear down the unsharded test stack AND every sharded stack discovered on disk
320
+ * (`state.test.<i>.json` in `.lt-dev/`). Used by `lt dev test down` so a
321
+ * `--keep`-ed sharded run is fully reclaimed by one command.
322
+ */
323
+ function tearDownAllTestSessions(layout_1, baseIdentity_1, log_1) {
324
+ return __awaiter(this, arguments, void 0, function* (layout, baseIdentity, log, opts = {}) {
325
+ const stopped = [];
326
+ // Unsharded session first.
327
+ const base = yield tearDownTestSession(layout, baseIdentity, log, { silent: opts.silent });
328
+ stopped.push(...base.stopped);
329
+ // Then any sharded sessions still on disk.
330
+ let entries = [];
331
+ try {
332
+ entries = (0, fs_1.readdirSync)((0, path_1.join)(layout.root, '.lt-dev'));
333
+ }
334
+ catch (_a) {
335
+ /* no .lt-dev dir → nothing sharded to reclaim */
336
+ }
337
+ const shardIndices = entries
338
+ .map((f) => f.match(/^state\.test\.(\d+)\.json$/))
339
+ .filter((m) => m !== null)
340
+ .map((m) => Number(m[1]))
341
+ .sort((a, b) => a - b);
342
+ for (const shardIndex of shardIndices) {
343
+ const r = yield tearDownTestSession(layout, baseIdentity, log, { shardIndex, silent: opts.silent });
344
+ stopped.push(...r.stopped);
345
+ }
346
+ return { stopped };
347
+ });
348
+ }
349
+ /**
350
+ * Tear down the test stack: stop processes, remove the Caddy block, clear the
351
+ * env bridge + session file + registry entry. Idempotent + residue-free.
352
+ */
353
+ function tearDownTestSession(layout_1, baseIdentity_1, log_1) {
354
+ return __awaiter(this, arguments, void 0, function* (layout, baseIdentity, log, opts = {}) {
355
+ const names = testStackNames(opts.shardIndex);
356
+ const testIdentity = (0, dev_identity_1.buildTestIdentity)(baseIdentity, names.identitySuffix);
357
+ const stopped = [];
358
+ const session = (0, dev_state_1.loadSession)(layout.root, names.sessionFile);
359
+ if (session) {
360
+ for (const [name, pid] of Object.entries(session.pids)) {
361
+ if (!pid)
362
+ continue;
363
+ if (!(0, dev_state_1.isPidAlive)(pid)) {
364
+ stopped.push(`${name} (pid ${pid}, already dead)`);
365
+ continue;
366
+ }
367
+ // SIGTERM → wait → SIGKILL. A compiled `node dist` API catches SIGTERM
368
+ // for graceful shutdown and can hang on open Mongo connections, so a
369
+ // single SIGTERM would leave it listening + holding the test DB. Escalate
370
+ // to guarantee the residue-free teardown promise.
371
+ const gone = yield (0, dev_process_1.terminateProcessGroup)(pid);
372
+ stopped.push(gone ? `${name} (pid ${pid})` : `${name} (pid ${pid}, SURVIVED SIGKILL!)`);
373
+ }
374
+ (0, dev_state_1.clearSession)(layout.root, names.sessionFile);
375
+ }
376
+ const removed = (0, caddy_1.removeProjectBlock)(testIdentity.slug);
377
+ if (removed) {
378
+ const r = yield (0, caddy_1.reloadCaddy)();
379
+ if (!r.ok && !opts.silent)
380
+ log.warn(`Removed test Caddy block but reload failed: ${r.stderr.split('\n')[0]}`);
381
+ }
382
+ (0, dev_env_bridge_1.clearEnvBridge)(layout.root, names.bridgeFile);
383
+ // Drop the registry entry so the test slug + ports are reclaimed.
384
+ const reg = (0, dev_state_1.loadRegistry)();
385
+ if (reg.projects[testIdentity.slug]) {
386
+ delete reg.projects[testIdentity.slug];
387
+ (0, dev_state_1.saveRegistry)(reg);
388
+ }
389
+ if (!opts.silent && stopped.length > 0)
390
+ log.info(`Stopped test stack: ${stopped.join(', ')}`);
391
+ return { stopped };
392
+ });
393
+ }
394
+ /**
395
+ * Resolve the per-stack file/identity names. For a sharded run (`shardIndex`
396
+ * given) everything gets a `.<i>` / `-<i>` suffix so N stacks coexist without
397
+ * clobbering each other's session file, env bridge, logs, Caddy block or DB.
398
+ * Unsharded (shardIndex undefined) keeps the original single-stack names.
399
+ */
400
+ function testStackNames(shardIndex) {
401
+ const sharded = shardIndex !== undefined;
402
+ return {
403
+ apiLog: sharded ? `api.test.${shardIndex}.log` : TEST_API_LOG,
404
+ appLog: sharded ? `app.test.${shardIndex}.log` : TEST_APP_LOG,
405
+ bridgeFile: sharded ? `${TEST_BRIDGE_FILE}.${shardIndex}` : TEST_BRIDGE_FILE,
406
+ dbSuffix: sharded ? `-${shardIndex}` : '',
407
+ identitySuffix: sharded ? `-test-${shardIndex}` : '-test',
408
+ sessionFile: sharded ? `state.test.${shardIndex}.json` : dev_state_1.TEST_SESSION_FILE,
409
+ };
410
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lenne.tech/cli",
3
- "version": "1.26.0",
3
+ "version": "1.27.0",
4
4
  "description": "lenne.Tech CLI: lt",
5
5
  "keywords": [
6
6
  "lenne.Tech",