@codyswann/lisa 4.4.21 → 4.6.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.
Files changed (62) hide show
  1. package/all/copy-overwrite/scripts/lisa-gates.mjs +125 -33
  2. package/all/copy-overwrite/scripts/lisa-run-gates.mjs +112 -4
  3. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  4. package/dist/core/lisa-owned-hash-ledger.js +3 -0
  5. package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
  6. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  7. package/dist/core/upstream-evidence-manifest.js +8 -4
  8. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  9. package/package.json +1 -1
  10. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  11. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  12. package/plugins/lisa-agy/plugin.json +1 -1
  13. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  14. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  15. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  16. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  17. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  18. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  19. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  20. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  21. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  22. package/plugins/lisa-expo-agy/plugin.json +1 -1
  23. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  24. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  25. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  26. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  27. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  28. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  29. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  30. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  31. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  32. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  33. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  34. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  35. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  36. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  37. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  38. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  39. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  40. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  41. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  42. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  43. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  44. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  45. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  46. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  47. package/plugins/lisa-rails-agy/plugin.json +1 -1
  48. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  49. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  50. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  51. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  52. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  53. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  54. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  55. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  56. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  57. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  58. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  59. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  60. package/scripts/check-workflow-package-paths.mjs +218 -57
  61. package/scripts/lib/workflow-contract-probe.mjs +445 -0
  62. package/typescript/copy-overwrite/scripts/lisa-mutation.mjs +75 -1
@@ -37,27 +37,39 @@
37
37
  * and are never extracted. A host-relative fallback is exactly what made #2951
38
38
  * invisible here, so it cannot be what rescues a step from this check.
39
39
  *
40
- * ## What this does NOT prove
40
+ * ## Existence is not the contract (#2982)
41
41
  *
42
- * Existence is necessary and NOT sufficient, and this script proves only
43
- * existence. A file can keep its path and change its contract, or move and take
44
- * its contract with it: greening the path in #2951 produced a prover that
42
+ * Existence is necessary and NOT sufficient. A file can keep its path and
43
+ * change its contract: greening the path in #2951 produced a prover that
45
44
  * scanned zero tracked files and reported success, which was strictly worse
46
- * than the red it replaced. Only the PATH is legible from a workflow, so a
47
- * contract probe needs a declaration this gate does not have and does not
48
- * invent. **Do not read a pass as "the released artifact still does its job."**
49
- * Tracked separately rather than implied here.
45
+ * than the red it replaced. Only the PATH is legible from a workflow, so the
46
+ * contract needs a DECLARATION which #2960 said it did not have and would not
47
+ * invent, and which #2982 added alongside the floor in the same file.
48
+ *
49
+ * Every package path a step resolves now declares what the artifact there is.
50
+ * `reference` means the step does not run it (a directory named in error
51
+ * prose), and existence is the whole claim. `executed` means the step runs it,
52
+ * and carries at least one probe: an invocation of the RELEASED artifact — out
53
+ * of the tarball, never this checkout, because running the local copy
54
+ * reproduces exactly the blindness #2951 had — plus a signal that proves the
55
+ * run was not vacuous. An undeclared path FAILS, so a new one forces a decision
56
+ * instead of inheriting silence. See `scripts/lib/workflow-contract-probe.mjs`
57
+ * for the two probe shapes and what each one's count means.
50
58
  *
51
59
  * ## Refusing to pass on nothing
52
60
  *
53
- * Discovering zero workflows, or extracting zero package paths, is exit 2 and
54
- * never a clean pass. So is a registry it could not reach. A gate that passes
55
- * because it did nothing is the failure this file exists to prevent, and it
56
- * would be perverse to reproduce it here.
61
+ * Discovering zero workflows, extracting zero package paths, or wherever
62
+ * released artifacts are available to run executing zero contract probes is
63
+ * exit 2 and never a clean pass. So is a registry it could not reach, and so is
64
+ * a probe killed at its deadline, because a killed child returns empty streams
65
+ * and would otherwise read as an artifact that emitted no signal. A gate that
66
+ * passes because it did nothing is the failure this file exists to prevent, and
67
+ * it would be perverse to reproduce it here. Every run prints how many paths it
68
+ * examined and how many probes it executed.
57
69
  *
58
70
  * CLI:
59
71
  * node scripts/check-workflow-package-paths.mjs [--root <dir>]
60
- * [--releases <n>] [--listing <file>] [--json]
72
+ * [--releases <n>] [--listing <file>] [--extracted <dir>] [--json]
61
73
  *
62
74
  * With no flags it reads the floor declared in
63
75
  * `.github/workflow-package-floor.json`, the latest release, and one midpoint
@@ -65,15 +77,23 @@
65
77
  * ad-hoc sweep rather than the promise CI enforces.
66
78
  *
67
79
  * `--listing` supplies release file listings from a JSON file instead of the
68
- * registry (`{ "<version>": ["package/a", ...] }`), so the unit suite can drive
69
- * every branch offline. The networked path is what CI runs.
80
+ * registry (`{ "<version>": ["package/a", ...] }`). It drives the path half
81
+ * offline and supplies no artifacts, so it runs no probes. `--extracted <dir>`
82
+ * supplies already-unpacked releases at `<dir>/<version>/package/...` and drives
83
+ * BOTH halves offline. The networked path is what CI runs, and it always
84
+ * probes.
70
85
  *
71
86
  * Exit codes:
72
- * 0 — every step's package paths resolve in every release in the window.
73
- * 1 a step has no resolvable package path in some release.
87
+ * 0 — every step's package paths resolve, and every released artifact a step
88
+ * executes still honours its declared contract.
89
+ * 1 — a step has no resolvable package path in some release, a resolved path
90
+ * declares no contract, a declaration names a path nothing references, or
91
+ * a released artifact violated its declared contract.
74
92
  * 2 — operational: unknown flag, a flag missing its value, `--root` absent or
75
93
  * not a git repository, git unavailable, zero workflows discovered, zero
76
- * package paths extracted, or the registry could not be read.
94
+ * package paths extracted, a malformed declaration, a probe killed at its
95
+ * deadline, zero probes executed where artifacts were available, or the
96
+ * registry could not be read.
77
97
  *
78
98
  * @module scripts/check-workflow-package-paths
79
99
  */
@@ -84,12 +104,17 @@ import {
84
104
  readFileSync,
85
105
  writeFileSync,
86
106
  } from "node:fs";
87
- import { mkdtempSync } from "node:fs";
107
+ import { mkdtempSync, statSync } from "node:fs";
88
108
  import { tmpdir } from "node:os";
89
109
  import path from "node:path";
90
110
 
91
111
  import { boundedExecFileSync } from "./lib/bounded-spawn.mjs";
92
112
  import { invokedAsScript } from "./lib/invoked-as-script.mjs";
113
+ import {
114
+ declarationGaps,
115
+ probeReleases,
116
+ readContractDeclaration,
117
+ } from "./lib/workflow-contract-probe.mjs";
93
118
 
94
119
  /** The package whose released layout a workflow may make claims about. */
95
120
  const PACKAGE_NAME = "@codyswann/lisa";
@@ -130,7 +155,7 @@ const EXIT_OPERATIONAL = 2;
130
155
  */
131
156
  function usage(message) {
132
157
  process.stderr.write(
133
- `${message}\n\nUsage: node scripts/check-workflow-package-paths.mjs [--root <dir>] [--releases <n>] [--listing <file>] [--json]\n`
158
+ `${message}\n\nUsage: node scripts/check-workflow-package-paths.mjs [--root <dir>] [--releases <n>] [--listing <file>] [--extracted <dir>] [--json]\n`
134
159
  );
135
160
  process.exit(EXIT_OPERATIONAL);
136
161
  }
@@ -138,13 +163,14 @@ function usage(message) {
138
163
  /**
139
164
  * Parse argv into options.
140
165
  * @param {readonly string[]} argv - Arguments after the script name
141
- * @returns {{root: string, releases: number|null, listing: string|null, json: boolean}}
166
+ * @returns {{root: string, releases: number|null, listing: string|null, extracted: string|null, json: boolean}}
142
167
  */
143
168
  function parseArgs(argv) {
144
169
  const options = {
145
170
  root: process.cwd(),
146
171
  releases: null,
147
172
  listing: null,
173
+ extracted: null,
148
174
  json: false,
149
175
  };
150
176
  for (let index = 0; index < argv.length; index += 1) {
@@ -154,11 +180,17 @@ function parseArgs(argv) {
154
180
  continue;
155
181
  }
156
182
  const value = argv[index + 1];
157
- if (flag === "--root" || flag === "--releases" || flag === "--listing") {
183
+ if (
184
+ flag === "--root" ||
185
+ flag === "--releases" ||
186
+ flag === "--listing" ||
187
+ flag === "--extracted"
188
+ ) {
158
189
  if (value === undefined) usage(`${flag} needs a value`);
159
190
  index += 1;
160
191
  if (flag === "--root") options.root = path.resolve(value);
161
192
  if (flag === "--listing") options.listing = path.resolve(value);
193
+ if (flag === "--extracted") options.extracted = path.resolve(value);
162
194
  if (flag === "--releases") {
163
195
  const parsed = Number.parseInt(value, 10);
164
196
  if (!Number.isInteger(parsed) || parsed < 1) {
@@ -322,38 +354,89 @@ function chooseReleases(versions, floor, recent) {
322
354
  }
323
355
 
324
356
  /**
325
- * Read the declared compatibility floor.
357
+ * Read the declaration that governs this check: the floor and the contracts.
358
+ * @remarks
359
+ * One file, two properties of the same promise. #2960 declared how far back the
360
+ * workflows keep working; #2982 declared what the artifacts there must still
361
+ * do. Splitting them into two files would give one property two declaration
362
+ * surfaces, which is this repository's recurring defect.
326
363
  * @param {string} root - Repository root
327
- * @returns {string} The floor version
364
+ * @returns {{floor: string, contracts: Record<string, object>, fixtures: Record<string, object>}}
328
365
  * @throws {Error} When the declaration is missing or malformed
329
366
  */
330
- function readFloor(root) {
367
+ function readDeclaration(root) {
331
368
  const file = path.join(root, FLOOR_CONFIG);
332
369
  if (!existsSync(file)) {
333
370
  throw new Error(
334
- `${FLOOR_CONFIG} is missing. It declares the oldest release these workflows promise to keep working against; without it this check would be guessing, and a guessed floor is how a gate ends up measuring nothing.`
371
+ `${FLOOR_CONFIG} is missing. It declares the oldest release these workflows promise to keep working against, and what each artifact they resolve must still do; without it this check would be guessing, and a guessed floor is how a gate ends up measuring nothing.`
335
372
  );
336
373
  }
337
374
  const declared = JSON.parse(readFileSync(file, "utf8"));
338
375
  if (typeof declared.floor !== "string" || declared.floor.length === 0) {
339
376
  throw new Error(`${FLOOR_CONFIG} has no string "floor"`);
340
377
  }
341
- return declared.floor;
378
+ const { contracts, fixtures } = readContractDeclaration(declared);
379
+ return { floor: declared.floor, contracts, fixtures };
380
+ }
381
+
382
+ /**
383
+ * Every file under a directory, as `/`-separated paths relative to it.
384
+ * @param {string} root - Directory to walk
385
+ * @param {string} prefix - Accumulated relative prefix
386
+ * @returns {readonly string[]} Relative file paths
387
+ */
388
+ function walkFiles(root, prefix = "") {
389
+ return readdirSync(path.join(root, prefix), { withFileTypes: true }).flatMap(
390
+ entry => {
391
+ const relative = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
392
+ return entry.isDirectory()
393
+ ? walkFiles(root, relative)
394
+ : [`package/${relative}`];
395
+ }
396
+ );
397
+ }
398
+
399
+ /**
400
+ * Read already-unpacked releases from `<dir>/<version>/package/`.
401
+ * @param {string} dir - Directory holding one subdirectory per version
402
+ * @returns {Record<string, {entries: readonly string[], root: string|null}>}
403
+ */
404
+ function readExtractedReleases(dir) {
405
+ return Object.fromEntries(
406
+ readdirSync(dir, { withFileTypes: true })
407
+ .filter(entry => entry.isDirectory())
408
+ .map(entry => {
409
+ const packageRoot = path.join(dir, entry.name, "package");
410
+ return [
411
+ entry.name,
412
+ { entries: walkFiles(packageRoot), root: packageRoot },
413
+ ];
414
+ })
415
+ );
342
416
  }
343
417
 
344
418
  /**
345
- * Read each chosen release's file listing, memoising downloads.
419
+ * Download and unpack each chosen release, memoising both.
420
+ * @remarks
421
+ * The tarball is UNPACKED, not merely listed, because a contract probe has to
422
+ * run the released artifact. Measured 2026-08-24: three releases in the current
423
+ * window are ~30 MB compressed and ~175 MB unpacked, downloaded once and cached
424
+ * — the same order of cost #2960 already paid for the listings alone.
346
425
  * @param {readonly string[]} chosen - Versions to read
347
- * @param {string} cacheDir - Where downloaded listings are memoised
348
- * @returns {Record<string, readonly string[]>} Version to tarball entry list
426
+ * @param {string} cacheDir - Where releases are memoised
427
+ * @returns {Record<string, {entries: readonly string[], root: string}>}
349
428
  */
350
- function fetchReleaseListings(chosen, cacheDir) {
429
+ function fetchReleases(chosen, cacheDir) {
351
430
  mkdirSync(cacheDir, { recursive: true });
352
- const listings = {};
431
+ const releases = {};
353
432
  for (const version of chosen) {
433
+ const unpacked = path.join(cacheDir, version, "package");
354
434
  const cached = path.join(cacheDir, `${version}.json`);
355
- if (existsSync(cached)) {
356
- listings[version] = JSON.parse(readFileSync(cached, "utf8"));
435
+ if (existsSync(cached) && existsSync(unpacked)) {
436
+ releases[version] = {
437
+ entries: JSON.parse(readFileSync(cached, "utf8")),
438
+ root: unpacked,
439
+ };
357
440
  continue;
358
441
  }
359
442
  const staging = mkdtempSync(path.join(tmpdir(), "lisa-release-"));
@@ -366,20 +449,25 @@ function fetchReleaseListings(chosen, cacheDir) {
366
449
  if (tarball === undefined) {
367
450
  throw new Error(`npm pack produced no tarball for ${version}`);
368
451
  }
369
- const entries = boundedExecFileSync(
370
- "tar",
371
- ["-tzf", path.join(staging, tarball)],
372
- {
373
- encoding: "utf8",
374
- maxBuffer: 64 * 1024 * 1024,
375
- }
376
- )
452
+ const archive = path.join(staging, tarball);
453
+ const entries = boundedExecFileSync("tar", ["-tzf", archive], {
454
+ encoding: "utf8",
455
+ maxBuffer: 64 * 1024 * 1024,
456
+ })
377
457
  .split("\n")
378
458
  .filter(Boolean);
379
- listings[version] = entries;
459
+ const destination = path.join(cacheDir, version);
460
+ mkdirSync(destination, { recursive: true });
461
+ boundedExecFileSync("tar", ["-xzf", archive, "-C", destination], {
462
+ timeout: 300_000,
463
+ });
464
+ if (!existsSync(unpacked) || !statSync(unpacked).isDirectory()) {
465
+ throw new Error(`unpacking ${version} produced no package/ directory`);
466
+ }
380
467
  writeFileSync(cached, JSON.stringify(entries));
468
+ releases[version] = { entries, root: unpacked };
381
469
  }
382
- return listings;
470
+ return releases;
383
471
  }
384
472
 
385
473
  /**
@@ -436,30 +524,62 @@ function main() {
436
524
  );
437
525
  }
438
526
 
439
- let listings;
527
+ let declaration;
528
+ let releases;
440
529
  try {
441
- listings = options.listing
442
- ? JSON.parse(readFileSync(options.listing, "utf8"))
443
- : fetchReleaseListings(
444
- chooseReleases(
445
- publishedVersions(),
446
- readFloor(options.root),
447
- options.releases
448
- ),
449
- path.join(options.root, "node_modules", ".cache", "lisa-releases")
450
- );
530
+ declaration = readDeclaration(options.root);
531
+ releases = readReleases(options, declaration.floor);
451
532
  } catch (error) {
452
533
  usage(
453
534
  `Could not read released package layouts: ${error.message}. Refusing to report a clean scan that never ran.`
454
535
  );
455
536
  }
456
537
 
457
- const breakages = findBreakages(claims.groups, listings);
538
+ const listings = Object.fromEntries(
539
+ Object.entries(releases).map(([version, release]) => [
540
+ version,
541
+ release.entries,
542
+ ])
543
+ );
544
+ const gaps = declarationGaps(claims.groups, declaration.contracts);
545
+ const probeable = Object.entries(releases).filter(
546
+ ([, release]) => release.root !== null
547
+ );
548
+ const contracts =
549
+ gaps.undeclared.length > 0
550
+ ? {
551
+ executed: 0,
552
+ violations: [],
553
+ deferred: [],
554
+ operational: [],
555
+ probed: [],
556
+ }
557
+ : probeReleases({
558
+ groups: claims.groups,
559
+ releases: probeable.map(([version, release]) => ({
560
+ version,
561
+ root: release.root,
562
+ contains: candidate => releaseContains(release.entries, candidate),
563
+ })),
564
+ contracts: declaration.contracts,
565
+ fixtures: declaration.fixtures,
566
+ workRoot: mkdtempSync(path.join(tmpdir(), "lisa-contract-probe-")),
567
+ });
568
+
569
+ const breakages = [
570
+ ...findBreakages(claims.groups, listings),
571
+ ...gaps.undeclared,
572
+ ...gaps.stale,
573
+ ...contracts.violations,
574
+ ];
458
575
  const report = {
459
576
  workflowsExamined: claims.workflowCount,
460
577
  packagePathsExamined: claims.pathCount,
461
578
  stepsExamined: claims.groups.length,
462
579
  releasesExamined: Object.keys(listings),
580
+ contractProbesExecuted: contracts.executed,
581
+ contractProbesDeferred: contracts.deferred,
582
+ contractProbesRun: contracts.probed,
463
583
  unattributed: claims.unattributed,
464
584
  breakages,
465
585
  };
@@ -468,14 +588,55 @@ function main() {
468
588
  process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
469
589
  } else {
470
590
  process.stdout.write(
471
- `Examined ${report.packagePathsExamined} package path(s) across ${report.stepsExamined} step(s) in ${report.workflowsExamined} workflow(s), against release(s) ${report.releasesExamined.join(", ")}.\n`
591
+ `Examined ${report.packagePathsExamined} package path(s) across ${report.stepsExamined} step(s) in ${report.workflowsExamined} workflow(s), against release(s) ${report.releasesExamined.join(", ")}. Executed ${report.contractProbesExecuted} contract probe(s).\n`
472
592
  );
593
+ for (const line of contracts.deferred)
594
+ process.stdout.write(` · ${line}\n`);
473
595
  for (const line of breakages) process.stdout.write(` ✗ ${line}\n`);
474
596
  }
475
597
 
598
+ if (contracts.operational.length > 0) {
599
+ usage(
600
+ `Contract probes could not be performed:\n ${contracts.operational.join("\n ")}`
601
+ );
602
+ }
603
+ if (
604
+ probeable.length > 0 &&
605
+ contracts.executed === 0 &&
606
+ breakages.length === 0
607
+ ) {
608
+ usage(
609
+ `Released artifacts were available and ZERO contract probes ran. An empty inspection and a satisfied contract look identical from here, so this is a failure rather than a pass. Declare a probe on the executed paths in ${FLOOR_CONFIG}.`
610
+ );
611
+ }
612
+
476
613
  process.exit(breakages.length === 0 ? 0 : 1);
477
614
  }
478
615
 
616
+ /**
617
+ * Resolve the releases to judge against, from whichever source was requested.
618
+ * @param {{listing: string|null, extracted: string|null, releases: number|null, root: string}} options - Parsed flags
619
+ * @param {string} floor - The declared compatibility floor
620
+ * @returns {Record<string, {entries: readonly string[], root: string|null}>} Releases
621
+ */
622
+ function readReleases(options, floor) {
623
+ if (options.extracted !== null)
624
+ return readExtractedReleases(options.extracted);
625
+ if (options.listing !== null) {
626
+ const listing = JSON.parse(readFileSync(options.listing, "utf8"));
627
+ return Object.fromEntries(
628
+ Object.entries(listing).map(([version, entries]) => [
629
+ version,
630
+ { entries, root: null },
631
+ ])
632
+ );
633
+ }
634
+ return fetchReleases(
635
+ chooseReleases(publishedVersions(), floor, options.releases),
636
+ path.join(options.root, "node_modules", ".cache", "lisa-releases")
637
+ );
638
+ }
639
+
479
640
  if (invokedAsScript(import.meta.url)) {
480
641
  main();
481
642
  }