@elench/testkit 0.1.145 → 0.1.147

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 (32) hide show
  1. package/README.md +12 -0
  2. package/lib/cli/operations/db/schema/refresh/operation.mjs +11 -1
  3. package/lib/cli/operations/db/schema/verify/operation.mjs +11 -1
  4. package/lib/config-api/auth-fixtures.mjs +41 -10
  5. package/lib/database/admin.mjs +227 -0
  6. package/lib/database/cleanup.mjs +201 -0
  7. package/lib/database/constants.mjs +10 -0
  8. package/lib/database/index.mjs +46 -720
  9. package/lib/database/local-postgres.mjs +158 -0
  10. package/lib/database/locks.mjs +31 -0
  11. package/lib/database/resource-postgres.mjs +72 -0
  12. package/lib/database/state-files.mjs +53 -0
  13. package/lib/ownership/docker.mjs +9 -0
  14. package/lib/runner/default-runtime-runner.mjs +2 -0
  15. package/lib/runner/lifecycle.mjs +1 -1
  16. package/lib/runner/maintenance.mjs +3 -0
  17. package/lib/runner/playwright-runner.mjs +10 -2
  18. package/lib/runner/scheduler/index.mjs +3 -0
  19. package/lib/runtime/index.d.ts +8 -0
  20. package/lib/runtime-src/k6/http.js +9 -2
  21. package/node_modules/@elench/next-analysis/package.json +1 -1
  22. package/node_modules/@elench/testkit-bridge/package.json +2 -2
  23. package/node_modules/@elench/testkit-protocol/package.json +1 -1
  24. package/node_modules/@elench/ts-analysis/package.json +1 -1
  25. package/node_modules/es-toolkit/CHANGELOG.md +801 -0
  26. package/node_modules/es-toolkit/src/compat/_internal/Equals.d.ts +1 -0
  27. package/node_modules/es-toolkit/src/compat/_internal/IsWritable.d.ts +3 -0
  28. package/node_modules/es-toolkit/src/compat/_internal/MutableList.d.ts +4 -0
  29. package/node_modules/es-toolkit/src/compat/_internal/RejectReadonly.d.ts +4 -0
  30. package/node_modules/esprima/ChangeLog +235 -0
  31. package/package.json +6 -6
  32. package/packages/testkit-bridge/node_modules/@elench/testkit-protocol/package.json +1 -1
@@ -0,0 +1,158 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { execa } from "execa";
4
+ import {
5
+ LOCAL_ADMIN_DB,
6
+ LOCAL_IMAGE,
7
+ LOCAL_PASSWORD,
8
+ LOCAL_POLL_INTERVAL_MS,
9
+ LOCAL_READY_TIMEOUT_MS,
10
+ LOCAL_USER,
11
+ } from "./constants.mjs";
12
+ import { buildContainerName } from "./naming.mjs";
13
+ import { getLocalInfraDir, readStateValue } from "./state.mjs";
14
+ import { buildDockerResourceLabels, dockerLabelArgs } from "../ownership/docker.mjs";
15
+ import { writeLocalInfraState } from "./state-files.mjs";
16
+
17
+ export async function ensureLocalContainer(productDir, database = {}) {
18
+ const infraDir = getLocalInfraDir(productDir);
19
+ fs.mkdirSync(infraDir, { recursive: true });
20
+
21
+ const containerName =
22
+ readStateValue(path.join(infraDir, "container_name")) || buildContainerName(productDir);
23
+ const image = database.image || readStateValue(path.join(infraDir, "image")) || LOCAL_IMAGE;
24
+ const user = database.user || readStateValue(path.join(infraDir, "user")) || LOCAL_USER;
25
+ const password =
26
+ database.password || readStateValue(path.join(infraDir, "password")) || LOCAL_PASSWORD;
27
+
28
+ let inspect = await inspectContainer(containerName);
29
+ if (inspect && inspect.Config?.Image !== image) {
30
+ await stopAndRemoveContainer(containerName);
31
+ inspect = null;
32
+ }
33
+
34
+ if (!inspect) {
35
+ const labels = buildLocalPostgresContainerLabels(productDir, containerName);
36
+ await execa("docker", [
37
+ "run",
38
+ "-d",
39
+ "--name",
40
+ containerName,
41
+ ...dockerLabelArgs(labels),
42
+ "-e",
43
+ `POSTGRES_USER=${user}`,
44
+ "-e",
45
+ `POSTGRES_PASSWORD=${password}`,
46
+ "-e",
47
+ `POSTGRES_DB=${LOCAL_ADMIN_DB}`,
48
+ "-p",
49
+ "127.0.0.1::5432",
50
+ image,
51
+ ]);
52
+ inspect = await inspectContainer(containerName);
53
+ } else if (!inspect.State?.Running) {
54
+ await execa("docker", ["start", containerName]);
55
+ inspect = await inspectContainer(containerName);
56
+ }
57
+
58
+ const hostPort = inspect?.NetworkSettings?.Ports?.["5432/tcp"]?.[0]?.HostPort;
59
+ if (!hostPort) {
60
+ throw new Error(`Could not determine published port for local database container ${containerName}`);
61
+ }
62
+
63
+ const infra = {
64
+ containerName,
65
+ containerId: inspect.Id,
66
+ image,
67
+ user,
68
+ password,
69
+ host: "127.0.0.1",
70
+ port: Number(hostPort),
71
+ };
72
+
73
+ await waitForLocalContainerReady(infra);
74
+ writeLocalInfraState(infraDir, infra);
75
+ return infra;
76
+ }
77
+
78
+ export async function loadExistingLocalContainer(productDir) {
79
+ const infraDir = getLocalInfraDir(productDir);
80
+ const containerName = readStateValue(path.join(infraDir, "container_name"));
81
+ if (!containerName) return null;
82
+
83
+ const inspect = await inspectContainer(containerName);
84
+ if (!inspect) return null;
85
+
86
+ if (!inspect.State?.Running) {
87
+ await execa("docker", ["start", containerName]);
88
+ }
89
+
90
+ const nextInspect = await inspectContainer(containerName);
91
+ const hostPort = nextInspect?.NetworkSettings?.Ports?.["5432/tcp"]?.[0]?.HostPort;
92
+ if (!hostPort) return null;
93
+
94
+ const infra = {
95
+ containerName,
96
+ containerId: nextInspect.Id,
97
+ image: nextInspect.Config?.Image || readStateValue(path.join(infraDir, "image")) || LOCAL_IMAGE,
98
+ user: readStateValue(path.join(infraDir, "user")) || LOCAL_USER,
99
+ password: readStateValue(path.join(infraDir, "password")) || LOCAL_PASSWORD,
100
+ host: "127.0.0.1",
101
+ port: Number(hostPort),
102
+ };
103
+ await waitForLocalContainerReady(infra);
104
+ return infra;
105
+ }
106
+
107
+ export async function inspectContainer(containerName) {
108
+ try {
109
+ const { stdout } = await execa("docker", ["inspect", containerName]);
110
+ const parsed = JSON.parse(stdout);
111
+ return parsed[0] || null;
112
+ } catch {
113
+ return null;
114
+ }
115
+ }
116
+
117
+ export async function stopAndRemoveContainer(containerName) {
118
+ try {
119
+ await execa("docker", ["rm", "-f", containerName]);
120
+ } catch {
121
+ // Already gone.
122
+ }
123
+ }
124
+
125
+ async function waitForLocalContainerReady(infra) {
126
+ const startedAt = Date.now();
127
+ while (Date.now() - startedAt < LOCAL_READY_TIMEOUT_MS) {
128
+ try {
129
+ await execa("docker", [
130
+ "exec",
131
+ infra.containerName,
132
+ "pg_isready",
133
+ "-U",
134
+ infra.user,
135
+ "-d",
136
+ LOCAL_ADMIN_DB,
137
+ ]);
138
+ return;
139
+ } catch {
140
+ await sleep(LOCAL_POLL_INTERVAL_MS);
141
+ }
142
+ }
143
+
144
+ throw new Error(`Timed out waiting for local database container ${infra.containerName}`);
145
+ }
146
+
147
+ function buildLocalPostgresContainerLabels(productDir, containerName) {
148
+ return buildDockerResourceLabels(productDir, {
149
+ kind: "postgres-container",
150
+ name: containerName,
151
+ scope: "local-postgres",
152
+ cachePolicy: "product-cache",
153
+ });
154
+ }
155
+
156
+ function sleep(ms) {
157
+ return new Promise((resolve) => setTimeout(resolve, ms));
158
+ }
@@ -0,0 +1,31 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+
4
+ export async function withLock(lockPath, fn) {
5
+ const lockDir = `${lockPath}.dir`;
6
+ const timeoutMs = 60_000;
7
+ const startedAt = Date.now();
8
+
9
+ while (true) {
10
+ try {
11
+ fs.mkdirSync(lockDir, { recursive: false });
12
+ break;
13
+ } catch (error) {
14
+ if (error.code !== "EEXIST") throw error;
15
+ if (Date.now() - startedAt > timeoutMs) {
16
+ throw new Error(`Timed out waiting for lock ${path.basename(lockPath)}`);
17
+ }
18
+ await sleep(200);
19
+ }
20
+ }
21
+
22
+ try {
23
+ return await fn();
24
+ } finally {
25
+ fs.rmSync(lockDir, { recursive: true, force: true });
26
+ }
27
+ }
28
+
29
+ function sleep(ms) {
30
+ return new Promise((resolve) => setTimeout(resolve, ms));
31
+ }
@@ -0,0 +1,72 @@
1
+ import path from "path";
2
+ import { LOCAL_ADMIN_DB } from "./constants.mjs";
3
+ import { readResourceConnectionState } from "./state-files.mjs";
4
+
5
+ export function resolveResourcePostgresInfra(config, processEnv = process.env) {
6
+ const resourceName = String(config.testkit?.database?.resource || "").trim();
7
+ if (!resourceName) {
8
+ throw new Error("Resource-backed Postgres database requires database.resource");
9
+ }
10
+ const connections = parseResourceConnections(processEnv.TESTKIT_RESOURCE_CONNECTIONS_JSON);
11
+ const connection = connections[resourceName];
12
+ if (!connection) {
13
+ const available = Object.keys(connections).sort().join(", ") || "none";
14
+ throw new Error(`Postgres resource "${resourceName}" is not available. Available resources: ${available}`);
15
+ }
16
+ return normalizeResourcePostgresConnection(resourceName, connection);
17
+ }
18
+
19
+ export function resolveResourcePostgresInfraFromState(stateDir, readStateValue, processEnv = process.env) {
20
+ const resourceName = readStateValue(path.join(stateDir, "resource_name"));
21
+ if (!resourceName) return null;
22
+ const connections = parseResourceConnections(processEnv.TESTKIT_RESOURCE_CONNECTIONS_JSON);
23
+ const connection = connections[resourceName] || readResourceConnectionState(stateDir, readStateValue);
24
+ return connection ? normalizeResourcePostgresConnection(resourceName, connection) : null;
25
+ }
26
+
27
+ function parseResourceConnections(raw) {
28
+ if (!raw) return {};
29
+ try {
30
+ const parsed = JSON.parse(raw);
31
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
32
+ } catch (error) {
33
+ throw new Error(`Invalid TESTKIT_RESOURCE_CONNECTIONS_JSON: ${error.message}`);
34
+ }
35
+ }
36
+
37
+ function normalizeResourcePostgresConnection(resourceName, connection) {
38
+ const fromUrl = connection.url ? parsePostgresConnectionUrl(connection.url) : {};
39
+ const host = connection.host || fromUrl.host;
40
+ const port = Number(connection.port || fromUrl.port || 5432);
41
+ const user = connection.user || fromUrl.user;
42
+ const password = connection.password || fromUrl.password || "";
43
+ const adminDatabase = connection.adminDatabase || connection.admin_database || fromUrl.database || LOCAL_ADMIN_DB;
44
+ const sslMode = connection.sslMode || connection.sslmode || fromUrl.sslMode || "disable";
45
+ if (!host) throw new Error(`Postgres resource "${resourceName}" connection is missing host`);
46
+ if (!Number.isInteger(port) || port <= 0) {
47
+ throw new Error(`Postgres resource "${resourceName}" connection has invalid port`);
48
+ }
49
+ if (!user) throw new Error(`Postgres resource "${resourceName}" connection is missing user`);
50
+ return {
51
+ backend: "resource",
52
+ resourceName,
53
+ host,
54
+ port,
55
+ user,
56
+ password,
57
+ adminDatabase,
58
+ sslMode,
59
+ };
60
+ }
61
+
62
+ function parsePostgresConnectionUrl(rawUrl) {
63
+ const parsed = new URL(rawUrl);
64
+ return {
65
+ host: parsed.hostname,
66
+ port: parsed.port ? Number(parsed.port) : 5432,
67
+ database: decodeURIComponent(parsed.pathname.replace(/^\//, "")) || LOCAL_ADMIN_DB,
68
+ user: decodeURIComponent(parsed.username || ""),
69
+ password: decodeURIComponent(parsed.password || ""),
70
+ sslMode: parsed.searchParams.get("sslmode") || undefined,
71
+ };
72
+ }
@@ -0,0 +1,53 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { LOCAL_ADMIN_DB } from "./constants.mjs";
4
+ import { buildProductIdentity } from "../ownership/docker.mjs";
5
+
6
+ export function writeLocalInfraState(infraDir, infra) {
7
+ const product = buildProductIdentity(path.resolve(infraDir, "..", "..", ".."));
8
+ fs.writeFileSync(path.join(infraDir, "database_backend"), "local");
9
+ fs.writeFileSync(path.join(infraDir, "container_name"), infra.containerName);
10
+ fs.writeFileSync(path.join(infraDir, "container_id"), infra.containerId);
11
+ fs.writeFileSync(path.join(infraDir, "ownership_product_id"), product.id);
12
+ fs.writeFileSync(path.join(infraDir, "image"), infra.image);
13
+ fs.writeFileSync(path.join(infraDir, "user"), infra.user);
14
+ fs.writeFileSync(path.join(infraDir, "password"), infra.password);
15
+ fs.writeFileSync(path.join(infraDir, "host"), infra.host);
16
+ fs.writeFileSync(path.join(infraDir, "port"), String(infra.port));
17
+ }
18
+
19
+ export function writeCacheState(cacheDir, config, infra, templateDbName, fingerprint) {
20
+ const backend = config.testkit.database.provider;
21
+ fs.writeFileSync(path.join(cacheDir, "database_backend"), backend);
22
+ fs.writeFileSync(path.join(cacheDir, "template_database_name"), templateDbName);
23
+ fs.writeFileSync(path.join(cacheDir, "template_fingerprint"), fingerprint);
24
+ if (backend === "local") {
25
+ fs.writeFileSync(path.join(cacheDir, "container_name"), infra.containerName);
26
+ } else {
27
+ fs.writeFileSync(path.join(cacheDir, "resource_name"), infra.resourceName);
28
+ writeResourceConnectionState(cacheDir, infra);
29
+ }
30
+ }
31
+
32
+ export function writeResourceConnectionState(stateDir, infra) {
33
+ fs.writeFileSync(path.join(stateDir, "resource_host"), infra.host);
34
+ fs.writeFileSync(path.join(stateDir, "resource_port"), String(infra.port));
35
+ fs.writeFileSync(path.join(stateDir, "resource_user"), infra.user);
36
+ fs.writeFileSync(path.join(stateDir, "resource_password"), infra.password);
37
+ fs.writeFileSync(path.join(stateDir, "resource_admin_database"), infra.adminDatabase || LOCAL_ADMIN_DB);
38
+ fs.writeFileSync(path.join(stateDir, "resource_sslmode"), infra.sslMode || "disable");
39
+ }
40
+
41
+ export function readResourceConnectionState(stateDir, readStateValue) {
42
+ const host = readStateValue(path.join(stateDir, "resource_host"));
43
+ const user = readStateValue(path.join(stateDir, "resource_user"));
44
+ if (!host || !user) return null;
45
+ return {
46
+ host,
47
+ port: Number(readStateValue(path.join(stateDir, "resource_port")) || 5432),
48
+ user,
49
+ password: readStateValue(path.join(stateDir, "resource_password")) || "",
50
+ adminDatabase: readStateValue(path.join(stateDir, "resource_admin_database")) || LOCAL_ADMIN_DB,
51
+ sslMode: readStateValue(path.join(stateDir, "resource_sslmode")) || "disable",
52
+ };
53
+ }
@@ -74,6 +74,15 @@ export async function removeDockerContainer(containerRef) {
74
74
  }
75
75
  }
76
76
 
77
+ export async function stopDockerContainer(containerRef) {
78
+ try {
79
+ await execa("docker", ["stop", "--time", "5", containerRef]);
80
+ return true;
81
+ } catch {
82
+ return false;
83
+ }
84
+ }
85
+
77
86
  export function dockerContainerSummary(container) {
78
87
  const status = container.running ? "running" : "stopped";
79
88
  return `${container.name} (${status})`;
@@ -97,6 +97,8 @@ export async function runDefaultRuntimeTask(
97
97
  lease,
98
98
  {
99
99
  ...buildFileTimeoutEnv(fileTimeoutSeconds, startedAt),
100
+ TESTKIT_TEST_FILE: task.file,
101
+ TESTKIT_TEST_ID: String(task.id),
100
102
  ...(task.scenarioSeed ? { TESTKIT_SCENARIO_SEED: task.scenarioSeed } : {}),
101
103
  },
102
104
  process.env
@@ -189,7 +189,7 @@ export async function cleanupRuns(productDir, { includeActive = false } = {}) {
189
189
  summary.cleaned.push(manifest);
190
190
  }
191
191
 
192
- await cleanupOrphanedLocalInfrastructure(productDir);
192
+ summary.resourceCleanup = await cleanupOrphanedLocalInfrastructure(productDir);
193
193
  return summary;
194
194
  }
195
195
 
@@ -126,6 +126,9 @@ export async function cleanup(productDir, options = {}) {
126
126
  label: "assistant session",
127
127
  dryRun,
128
128
  });
129
+ for (const target of summary.resourceCleanup?.targets || []) {
130
+ lines.push(formatDatabaseResourceCleanupLine(target, dryRun));
131
+ }
129
132
  for (const target of resourceCleanup.targets) {
130
133
  lines.push(formatDatabaseResourceCleanupLine(target, dryRun));
131
134
  }
@@ -1,5 +1,6 @@
1
1
  import path from "path";
2
2
  import fs from "fs";
3
+ import { createRequire } from "module";
3
4
  import { parsePlaywrightJsonResults } from "../reporters/playwright.mjs";
4
5
  import { resolveServiceCwd } from "../config/paths.mjs";
5
6
  import { buildFileTimeoutEnv, formatFileTimeoutBudgetError } from "../shared/file-timeout.mjs";
@@ -10,6 +11,8 @@ import { normalizePathSeparators } from "./state.mjs";
10
11
  import { buildPlaywrightEnv } from "./template.mjs";
11
12
  import { settleManagedSubprocess, startManagedSubprocess } from "./subprocess.mjs";
12
13
 
14
+ const require = createRequire(import.meta.url);
15
+
13
16
  export async function runPlaywrightTask(targetConfig, task, lifecycle, lease, reporter = null) {
14
17
  const local = targetConfig.testkit.local;
15
18
  if (!local?.baseUrl) {
@@ -33,9 +36,10 @@ export async function runPlaywrightTask(targetConfig, task, lifecycle, lease, re
33
36
  { globalTimeoutMs: fileTimeoutSeconds * 1000 }
34
37
  );
35
38
  const jsonReportPath = buildPlaywrightJsonReportPath(lease, task);
39
+ const playwrightCliPath = resolvePlaywrightCliPath();
36
40
  const subprocess = startManagedSubprocess(
37
- "npx",
38
- ["playwright", "test", "--config", playwrightConfigPath, "--reporter=json"],
41
+ process.execPath,
42
+ [playwrightCliPath, "test", "--config", playwrightConfigPath, "--reporter=json"],
39
43
  {
40
44
  cwd,
41
45
  env: {
@@ -113,6 +117,10 @@ export async function runPlaywrightTask(targetConfig, task, lifecycle, lease, re
113
117
  };
114
118
  }
115
119
 
120
+ export function resolvePlaywrightCliPath() {
121
+ return require.resolve("@playwright/test/cli");
122
+ }
123
+
116
124
  export function buildPlaywrightJsonReportPath(lease, task) {
117
125
  if (!lease?.leaseDir) {
118
126
  throw new Error(`Playwright task ${task?.file || ""} requires a lease-scoped directory`);
@@ -105,6 +105,9 @@ export function compareScheduledTasks(a, b) {
105
105
 
106
106
  function resolveTaskLocks(config, suite, file) {
107
107
  const locks = new Set();
108
+ if (suite.type === "dal") {
109
+ locks.add(`database:${config.name}`);
110
+ }
108
111
  const matchedSuiteRules = config.testkit.requirements?.suites || [];
109
112
  for (const rule of matchedSuiteRules) {
110
113
  if (matchesSuiteSelectors(suite.displayType, suite.name, [rule.selector])) {
@@ -61,6 +61,7 @@ export interface WaitForOptions {
61
61
  export interface RuntimeEnv {
62
62
  BASE: string;
63
63
  MACHINE_ID?: string;
64
+ rawEnv?: Record<string, string | undefined>;
64
65
  routeParams: RuntimeHeaders;
65
66
  }
66
67
 
@@ -290,6 +291,13 @@ export interface ActorRequestClient {
290
291
  }
291
292
 
292
293
  export interface HttpClient<TSetup = unknown> {
294
+ (
295
+ method: RuntimeMethod,
296
+ path: string,
297
+ setupData?: TSetup | null,
298
+ body?: unknown,
299
+ extraHeaders?: RuntimeHeaders
300
+ ): RuntimeResponse;
293
301
  as(actorName: string): ActorRequestClient;
294
302
  headers(extraHeaders?: RuntimeHeaders): RuntimeHeaders;
295
303
  multipart: MultipartRequestClient;
@@ -233,7 +233,12 @@ export function createHttpClient(config) {
233
233
  const defaultClient = createActorClient(defaultActor);
234
234
  const rawClient = createRawInvoker(null);
235
235
 
236
- return {
236
+ function client(method, path, setupDataOrBody = null, body, extraHeaders = {}) {
237
+ const requestBody = arguments.length >= 4 ? body : setupDataOrBody;
238
+ return defaultClient.request(method, path, requestBody, extraHeaders);
239
+ }
240
+
241
+ Object.assign(client, {
237
242
  rawHttp: http,
238
243
  headers(extraHeaders = {}) {
239
244
  return resolvedHeadersFor(defaultActor, extraHeaders);
@@ -299,7 +304,9 @@ export function createHttpClient(config) {
299
304
  return rawClient.multipart.patch(path, payload, extraHeaders);
300
305
  },
301
306
  },
302
- };
307
+ });
308
+
309
+ return client;
303
310
  }
304
311
 
305
312
  export function makeReq(baseUrl, sessionBundle = null, routeHeaders = {}, getHeaders = null, defaultActor = null) {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elench/next-analysis",
3
- "version": "0.1.145",
3
+ "version": "0.1.147",
4
4
  "description": "SWC-backed Next.js source analysis primitives for Erench tools",
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elench/testkit-bridge",
3
- "version": "0.1.145",
3
+ "version": "0.1.147",
4
4
  "description": "Browser bridge helpers for testkit",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -22,7 +22,7 @@
22
22
  "typecheck": "tsc -p tsconfig.json --noEmit"
23
23
  },
24
24
  "dependencies": {
25
- "@elench/testkit-protocol": "0.1.145"
25
+ "@elench/testkit-protocol": "0.1.146"
26
26
  },
27
27
  "private": false
28
28
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elench/testkit-protocol",
3
- "version": "0.1.145",
3
+ "version": "0.1.147",
4
4
  "description": "Shared browser protocol for testkit bridge and extension consumers",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elench/ts-analysis",
3
- "version": "0.1.145",
3
+ "version": "0.1.147",
4
4
  "description": "TypeScript compiler-backed source analysis primitives for Erench tools",
5
5
  "type": "module",
6
6
  "exports": {