@lenne.tech/cli 1.41.1 → 1.41.2

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.
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.addToGitignore = addToGitignore;
4
4
  exports.autoPatch = autoPatch;
5
+ exports.canonicaliseBridgeSpan = canonicaliseBridgeSpan;
5
6
  exports.patchApiConfig = patchApiConfig;
6
7
  exports.patchClaudeMd = patchClaudeMd;
7
8
  exports.patchNuxtConfig = patchNuxtConfig;
@@ -13,10 +14,33 @@ exports.patchPlaywrightConfig = patchPlaywrightConfig;
13
14
  * defaults and make it env-aware so it can be served behind Caddy
14
15
  * under `https://<slug>.localhost`.
15
16
  *
16
- * Each patch is a regex-based replace that matches only the legacy
17
- * form. Already-patched files are no-ops.
17
+ * Most patches are regex-based replaces that match only the legacy
18
+ * form; the marker-bracketed `lt-dev:bridge` block in
19
+ * playwright.config.ts is located by its markers instead. Already-patched
20
+ * files are no-ops — including when the consumer's own formatter has since
21
+ * restyled an injected block: that block is compared SEMANTICALLY (see
22
+ * `BRIDGE_VERSION` / `normaliseBridgeBlock`), not byte-for-byte.
18
23
  */
19
24
  const fs_1 = require("fs");
25
+ /**
26
+ * Version of the `lt-dev:bridge` block emitted into a consumer's
27
+ * `playwright.config.ts`. It travels IN the markers
28
+ * (`// >>> lt-dev:bridge v2 >>>`), which is what makes a genuine upgrade
29
+ * detectable independently of formatting.
30
+ *
31
+ * **Bump this on every change to `bridgeBlock`** — including a
32
+ * formatting-only one. The content comparison deliberately ignores quote
33
+ * style and whitespace (the consumer's formatter owns that file), so
34
+ * without a bump a cosmetic fix would never reach already-patched
35
+ * projects. That is exactly what happened to 1.32.1's `"utf8"` → `'utf8'`
36
+ * fix, which is why the version exists.
37
+ */
38
+ const BRIDGE_VERSION = 2;
39
+ /** Matches any bridge marker, versioned or not — v1 shipped without one. */
40
+ const BRIDGE_START_RE = /\/\/ >>> lt-dev:bridge(?: v(\d+))? >>>/;
41
+ const BRIDGE_END_RE = /\/\/ <<< lt-dev:bridge(?: v(\d+))? <<</;
42
+ /** Imports the bridge block owns; a stray copy outside it must be dropped. */
43
+ const BRIDGE_IMPORT_RE = /^\s*import \{[^}]*__ltDev(?:Exists|Read|Dirname|Resolve)[^}]*\} from '[^']*';?\s*$/gm;
20
44
  /** Append entry to .gitignore if not already present. */
21
45
  function addToGitignore(root, entry) {
22
46
  const path = `${root}/.gitignore`;
@@ -40,6 +64,29 @@ function autoPatch(file) {
40
64
  return patchPlaywrightConfig(file);
41
65
  return { file, patched: false, replacements: 0 };
42
66
  }
67
+ /**
68
+ * Replace the `lt-dev:bridge` span inside a whole file with its canonical
69
+ * comparison form, leaving every other byte untouched.
70
+ *
71
+ * For callers that must decide "is this file exactly what `autoPatch` would
72
+ * produce, or did a developer edit it?" — most importantly
73
+ * `dev-ticket.ts#isPristineLtDevPatch`, which gates whether `lt ticket stop`
74
+ * may discard a dirty config. Since the patcher deliberately tolerates the
75
+ * consumer formatter restyling the block, such a comparison MUST tolerate it
76
+ * too; otherwise a formatter-touched config reads as real developer work and
77
+ * the worktree removal is refused.
78
+ *
79
+ * Everything outside the markers stays byte-exact on purpose — being lenient
80
+ * there could let genuine work be silently discarded.
81
+ */
82
+ function canonicaliseBridgeSpan(content) {
83
+ const start = BRIDGE_START_RE.exec(content);
84
+ const end = BRIDGE_END_RE.exec(content);
85
+ if (!start || !end || end.index <= start.index)
86
+ return content;
87
+ const endsAt = end.index + end[0].length;
88
+ return (content.slice(0, start.index) + normaliseBridgeBlock(content.slice(start.index, endsAt)) + content.slice(endsAt));
89
+ }
43
90
  /**
44
91
  * API: make the server listen port honour `process.env.PORT` (injected by
45
92
  * `lt dev up` for its Caddy upstream). Handles two patterns found in
@@ -164,8 +211,12 @@ function patchNuxtConfig(file) {
164
211
  * any env.
165
212
  *
166
213
  * Patches applied (each idempotent):
167
- * 1. Top-of-file: `if (existsSync('.lt-dev/.env')) loadEnv(...)` block,
168
- * bracketed by `// >>> lt-dev:bridge >>>` markers.
214
+ * 1. Top-of-file: a dotenv loader that searches UP from cwd for
215
+ * `.lt-dev/.env`, bracketed by `// >>> lt-dev:bridge vN >>>` markers.
216
+ * Re-injected when the marker's version differs from `BRIDGE_VERSION`
217
+ * (a genuine upgrade) or when the block's CODE differs semantically
218
+ * (tampering / corruption) — but NOT when the consumer's formatter
219
+ * merely restyled it, which owns this file and must stay free to.
169
220
  * 2. Hardcoded baseURL/host/url for `http://localhost:3001` →
170
221
  * `process.env.NUXT_PUBLIC_SITE_URL || 'http://localhost:3001'`.
171
222
  * 3. `webServer` wrapped in an `LT_DEV_ACTIVE` guard so Playwright reuses
@@ -180,6 +231,7 @@ function patchNuxtConfig(file) {
180
231
  * 6. `slowMo: 10` → `0` (pointless per-action delay, multiplied across shards).
181
232
  */
182
233
  function patchPlaywrightConfig(file) {
234
+ var _a;
183
235
  if (!(0, fs_1.existsSync)(file))
184
236
  return { file, patched: false, replacements: 0 };
185
237
  const before = (0, fs_1.readFileSync)(file, 'utf8');
@@ -198,8 +250,8 @@ function patchPlaywrightConfig(file) {
198
250
  // a direct `playwright test` run) usually sit in `projects/app`. The
199
251
  // original cwd-only resolve missed it, so direct runs fell back to
200
252
  // `localhost:3001` and could collide with a parallel project.
201
- const bridgeStart = '// >>> lt-dev:bridge >>>';
202
- const bridgeEnd = '// <<< lt-dev:bridge <<<';
253
+ const bridgeStart = `// >>> lt-dev:bridge v${BRIDGE_VERSION} >>>`;
254
+ const bridgeEnd = `// <<< lt-dev:bridge v${BRIDGE_VERSION} <<<`;
203
255
  const bridgeBlock = [
204
256
  bridgeStart,
205
257
  '// Auto-load <root>/.lt-dev/.env when `lt dev up` is active so',
@@ -228,16 +280,47 @@ function patchPlaywrightConfig(file) {
228
280
  '}',
229
281
  bridgeEnd,
230
282
  ].join('\n');
231
- const bridgeStartIdx = after.indexOf(bridgeStart);
232
- const bridgeEndIdx = after.indexOf(bridgeEnd);
233
- if (bridgeStartIdx === -1) {
283
+ // Locate ANY bridge marker, versioned or not — v1 shipped unversioned and
284
+ // must still be recognised (and upgraded) rather than double-injected.
285
+ const startMatch = BRIDGE_START_RE.exec(after);
286
+ const endMatch = BRIDGE_END_RE.exec(after);
287
+ const bridgeStartIdx = startMatch ? startMatch.index : -1;
288
+ const bridgeEndIdx = endMatch ? endMatch.index : -1;
289
+ // A start marker without a well-ordered end marker is a corrupted block: the
290
+ // slice arithmetic below would duplicate everything between the two (and the
291
+ // user's code with it). `patchClaudeMd` already guards its span this way.
292
+ const spanIsSane = bridgeStartIdx !== -1 && bridgeEndIdx > bridgeStartIdx;
293
+ if (bridgeStartIdx === -1 || !spanIsSane) {
294
+ // Markers present but unusable (e.g. reversed). Strip the STRAY MARKERS
295
+ // ONLY — never the text between them: with the markers out of order that
296
+ // text is the user's own code, not our block, so slicing the span out
297
+ // would delete their work (and slicing it in would duplicate it).
298
+ if (!spanIsSane)
299
+ after = after.replace(BRIDGE_START_RE, '').replace(BRIDGE_END_RE, '');
300
+ // A formatter with organize-imports may have hoisted the block's imports
301
+ // out of the markers; leaving them behind would duplicate the `__ltDev*`
302
+ // bindings and break the consumer's config with a duplicate-identifier
303
+ // error. They are ours to own, so it is safe to drop them.
304
+ after = after.replace(BRIDGE_IMPORT_RE, '').replace(/^\n+/, '');
234
305
  after = `${bridgeBlock}\n${after}`;
235
306
  count++;
236
307
  }
237
- else if (bridgeEndIdx !== -1) {
238
- const rebuilt = after.slice(0, bridgeStartIdx) + bridgeBlock + after.slice(bridgeEndIdx + bridgeEnd.length);
239
- if (rebuilt !== after) {
240
- after = rebuilt;
308
+ else {
309
+ const existing = after.slice(bridgeStartIdx, bridgeEndIdx + endMatch[0].length);
310
+ // Two independent reasons to re-inject:
311
+ // (a) VERSION — the marker carries the block's version, so a genuine
312
+ // upgrade is detected regardless of how the consumer formatted it.
313
+ // This is what keeps formatting-only fixes shippable (see
314
+ // BRIDGE_VERSION); an unversioned v1 marker yields `undefined` here.
315
+ // (b) CODE — the block's code differs semantically, i.e. it was tampered
316
+ // with or corrupted. Cosmetic reformatting by the consumer's own
317
+ // formatter is deliberately NOT a reason: rewriting it back on every
318
+ // run is what used to leave playwright.config.ts permanently dirty in
319
+ // the working tree, with formatter and patcher flipping it forever.
320
+ const existingVersion = Number((_a = startMatch[1]) !== null && _a !== void 0 ? _a : 0);
321
+ if (existingVersion !== BRIDGE_VERSION || normaliseBridgeBlock(existing) !== normaliseBridgeBlock(bridgeBlock)) {
322
+ const head = after.slice(0, bridgeStartIdx).replace(BRIDGE_IMPORT_RE, '');
323
+ after = head + bridgeBlock + after.slice(bridgeEndIdx + endMatch[0].length);
241
324
  count++;
242
325
  }
243
326
  }
@@ -307,3 +390,30 @@ function patchPlaywrightConfig(file) {
307
390
  (0, fs_1.writeFileSync)(file, after, 'utf8');
308
391
  return { file, patched: true, replacements: count };
309
392
  }
393
+ /**
394
+ * Canonical form of a bridge block for comparison purposes.
395
+ *
396
+ * Compares only the CODE: comment lines are dropped entirely, so rewording
397
+ * a comment never triggers a rewrite — and, crucially, code that a reflow
398
+ * folded behind a `//` VANISHES from the comparison and is therefore
399
+ * detected as changed. A plain `\s+ → ' '` collapse would erase newlines
400
+ * instead, which makes a fully commented-out (inert) loader normalise
401
+ * identically to a live one: the block would silently never load
402
+ * `.lt-dev/.env` again and Playwright would fall back to `localhost:3001`.
403
+ *
404
+ * Quote style and intra-line whitespace are normalised away because the
405
+ * consumer's formatter owns this file (which direction it flips is
406
+ * project-specific — do not assume). Blind spot to keep in mind: a change
407
+ * that differs ONLY in quotes or whitespace is invisible here, which is
408
+ * what `BRIDGE_VERSION` is for.
409
+ */
410
+ function normaliseBridgeBlock(s) {
411
+ return s
412
+ .split(/\r?\n/)
413
+ .map((l) => l.trim())
414
+ .filter((l) => l !== '' && !l.startsWith('//'))
415
+ .join(' ')
416
+ .replace(/['"]/g, '"')
417
+ .replace(/\s+/g, ' ')
418
+ .trim();
419
+ }
@@ -824,7 +824,17 @@ function isPristineLtDevPatch(worktreePath, relPath) {
824
824
  try {
825
825
  (0, fs_1.writeFileSync)(tmp, head, 'utf8');
826
826
  (0, dev_patches_1.autoPatch)(tmp);
827
- return (0, fs_1.readFileSync)(tmp, 'utf8') === current;
827
+ const derived = (0, fs_1.readFileSync)(tmp, 'utf8');
828
+ if (derived === current)
829
+ return true;
830
+ // The patcher deliberately lets the consumer's formatter restyle the
831
+ // injected `lt-dev:bridge` block (quote style, wrapping) without rewriting
832
+ // it — so a byte-exact comparison would classify a merely reformatted
833
+ // playwright.config.ts as real developer work and make `lt ticket stop`
834
+ // refuse to remove the worktree. Compare the bridge span in its canonical
835
+ // form; everything OUTSIDE the markers stays byte-exact, so genuine edits
836
+ // are still never auto-discarded.
837
+ return (0, dev_patches_1.canonicaliseBridgeSpan)(derived) === (0, dev_patches_1.canonicaliseBridgeSpan)(current);
828
838
  }
829
839
  catch (_c) {
830
840
  return false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lenne.tech/cli",
3
- "version": "1.41.1",
3
+ "version": "1.41.2",
4
4
  "description": "lenne.Tech CLI: lt",
5
5
  "keywords": [
6
6
  "lenne.Tech",
@@ -46,14 +46,10 @@
46
46
  "start:build": "npm run build && node bin/lt --compiled-build",
47
47
  "start:compiled": "node bin/lt --compiled-build",
48
48
  "test": "jest --testTimeout=60000",
49
- "watch": "jest --watch",
50
- "release": "standard-version && git push --follow-tags origin main",
51
- "release:minor": "standard-version --release-as minor && git push --follow-tags origin main",
52
- "release:major": "standard-version --release-as major && git push --follow-tags origin main"
49
+ "watch": "jest --watch"
53
50
  },
54
51
  "files": [
55
52
  "tsconfig.json",
56
- "tslint.json",
57
53
  "build",
58
54
  "LICENSE",
59
55
  "README.md",
@@ -96,7 +92,6 @@
96
92
  "jest": "30.4.2",
97
93
  "prettier": "3.8.3",
98
94
  "rimraf": "6.1.3",
99
- "standard-version": "9.5.0",
100
95
  "ts-jest": "29.4.11"
101
96
  },
102
97
  "//overrides": {