@elench/testkit 0.1.118 → 0.1.119

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 (30) hide show
  1. package/lib/app/doctor.mjs +11 -113
  2. package/lib/cli/assistant/command-observer.mjs +1 -1
  3. package/lib/cli/assistant/state.mjs +2 -0
  4. package/lib/cli/commands/lint.mjs +37 -0
  5. package/lib/cli/entrypoint.mjs +1 -0
  6. package/lib/cli/operations/lint/operation.mjs +12 -0
  7. package/lib/cli/renderers/doctor/text.mjs +5 -0
  8. package/lib/cli/renderers/lint/text.mjs +20 -0
  9. package/lib/config-api/database-steps.mjs +132 -0
  10. package/lib/config-api/index.d.ts +36 -3
  11. package/lib/config-api/index.mjs +118 -12
  12. package/lib/lint/index.mjs +569 -0
  13. package/lib/runner/template-steps.mjs +8 -0
  14. package/lib/runtime/index.d.ts +43 -0
  15. package/lib/runtime/index.mjs +24 -0
  16. package/lib/runtime-src/k6/http-assertions.js +82 -0
  17. package/lib/shared/configured-steps.mjs +16 -0
  18. package/lib/ui/index.d.ts +46 -0
  19. package/lib/ui/index.mjs +11 -0
  20. package/lib/ui/sandbox.mjs +115 -0
  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/package.json +5 -5
  26. package/packages/testkit-bridge/node_modules/@elench/testkit-protocol/dist/index.d.ts +188 -0
  27. package/packages/testkit-bridge/node_modules/@elench/testkit-protocol/dist/index.d.ts.map +1 -0
  28. package/packages/testkit-bridge/node_modules/@elench/testkit-protocol/dist/index.js +293 -0
  29. package/packages/testkit-bridge/node_modules/@elench/testkit-protocol/dist/index.js.map +1 -0
  30. package/packages/testkit-bridge/node_modules/@elench/testkit-protocol/package.json +25 -0
@@ -1,16 +1,14 @@
1
- import fs from "fs";
2
1
  import path from "path";
3
- import ts from "typescript";
4
2
  import { discoverTests } from "../discovery/index.mjs";
5
3
  import { loadConfigContext } from "../config/index.mjs";
6
4
  import { runTestkitTypecheck } from "./typecheck.mjs";
7
- import { findConfigFile } from "../config/config-loader.mjs";
5
+ import { runLint } from "../lint/index.mjs";
8
6
 
9
7
  export async function runDoctor(options = {}) {
10
8
  const checks = [];
11
9
  const productDir = options.dir ? path.resolve(process.cwd(), options.dir) : process.cwd();
12
10
 
13
- await loadConfigContext({ dir: productDir });
11
+ const context = await loadConfigContext({ dir: productDir });
14
12
  checks.push({
15
13
  code: "config-load",
16
14
  level: "pass",
@@ -29,26 +27,18 @@ export async function runDoctor(options = {}) {
29
27
  details: discoveryErrors,
30
28
  });
31
29
 
32
- const playwrightViolations = findPlaywrightRuntimeImportViolations(productDir);
33
- checks.push({
34
- code: "ui-runtime-imports",
35
- level: playwrightViolations.length === 0 ? "pass" : "fail",
36
- message:
37
- playwrightViolations.length === 0
38
- ? "No runtime @playwright/test imports found in testkit UI suites"
39
- : `Found ${playwrightViolations.length} UI runtime import violation(s); import from @elench/testkit/ui instead`,
40
- details: playwrightViolations,
30
+ const lint = await runLint({
31
+ dir: productDir,
32
+ ...(context.config?.lint || {}),
41
33
  });
42
-
43
- const configImportViolations = findConfigImportViolations(productDir);
44
34
  checks.push({
45
- code: "config-import-hygiene",
46
- level: configImportViolations.length === 0 ? "pass" : "fail",
35
+ code: "lint",
36
+ level: lint.ok ? "pass" : "fail",
47
37
  message:
48
- configImportViolations.length === 0
49
- ? "Repo config does not import __testkit__ helper modules"
50
- : `Found ${configImportViolations.length} repo config import violation(s)`,
51
- details: configImportViolations,
38
+ lint.ok
39
+ ? `No Testkit lint violations across ${lint.summary.testkitFiles} suite file(s)`
40
+ : `Found ${lint.summary.violations} Testkit lint violation(s)`,
41
+ details: lint.violations,
52
42
  });
53
43
 
54
44
  const hasBrowserOrNextWork = discovery.files.some((entry) => entry.type === "ui");
@@ -88,95 +78,3 @@ export async function runDoctor(options = {}) {
88
78
  checks,
89
79
  };
90
80
  }
91
-
92
- function findPlaywrightRuntimeImportViolations(productDir) {
93
- const violations = [];
94
- for (const absolutePath of collectFiles(productDir)) {
95
- const sourceText = fs.readFileSync(absolutePath, "utf8");
96
- const sourceFile = ts.createSourceFile(absolutePath, sourceText, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
97
-
98
- for (const statement of sourceFile.statements) {
99
- if (!ts.isImportDeclaration(statement)) continue;
100
- if (!ts.isStringLiteral(statement.moduleSpecifier)) continue;
101
- if (statement.moduleSpecifier.text !== "@playwright/test") continue;
102
- const clause = statement.importClause;
103
- if (clause?.isTypeOnly) continue;
104
- if (!clause) {
105
- violations.push(relativeViolation(productDir, absolutePath, sourceFile, statement));
106
- continue;
107
- }
108
- if (clause.name) {
109
- violations.push(relativeViolation(productDir, absolutePath, sourceFile, statement));
110
- continue;
111
- }
112
- if (!clause.namedBindings) {
113
- violations.push(relativeViolation(productDir, absolutePath, sourceFile, statement));
114
- continue;
115
- }
116
- if (ts.isNamespaceImport(clause.namedBindings)) {
117
- violations.push(relativeViolation(productDir, absolutePath, sourceFile, statement));
118
- continue;
119
- }
120
- if (clause.namedBindings.elements.some((entry) => !entry.isTypeOnly)) {
121
- violations.push(relativeViolation(productDir, absolutePath, sourceFile, statement));
122
- }
123
- }
124
- }
125
- return violations;
126
- }
127
-
128
- function findConfigImportViolations(productDir) {
129
- const configFile = findConfigFile(productDir);
130
- if (!configFile || !fs.existsSync(configFile)) return [];
131
-
132
- const sourceText = fs.readFileSync(configFile, "utf8");
133
- const sourceFile = ts.createSourceFile(configFile, sourceText, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
134
- const violations = [];
135
-
136
- for (const statement of sourceFile.statements) {
137
- if (!ts.isImportDeclaration(statement)) continue;
138
- if (!ts.isStringLiteral(statement.moduleSpecifier)) continue;
139
- const specifier = statement.moduleSpecifier.text;
140
- if (!isRepoLocalConfigImportViolation(specifier)) continue;
141
- const position = sourceFile.getLineAndCharacterOfPosition(statement.getStart(sourceFile));
142
- violations.push({
143
- file: path.relative(productDir, configFile).split(path.sep).join("/"),
144
- line: position.line + 1,
145
- specifier,
146
- snippet: statement.getText(sourceFile),
147
- });
148
- }
149
-
150
- return violations;
151
- }
152
-
153
- function collectFiles(rootDir, out = []) {
154
- if (!fs.existsSync(rootDir)) return out;
155
- for (const entry of fs.readdirSync(rootDir, { withFileTypes: true })) {
156
- const absolutePath = path.join(rootDir, entry.name);
157
- if (entry.isDirectory()) {
158
- if (entry.name === "node_modules" || entry.name === ".git" || entry.name === ".testkit") continue;
159
- collectFiles(absolutePath, out);
160
- continue;
161
- }
162
- if (entry.isFile() && (entry.name.endsWith(".ui.testkit.ts") || entry.name.endsWith(".ui.testkit.ts"))) {
163
- out.push(absolutePath);
164
- }
165
- }
166
- return out.sort((left, right) => left.localeCompare(right));
167
- }
168
-
169
- function isRepoLocalConfigImportViolation(specifier) {
170
- if (typeof specifier !== "string") return false;
171
- if (!specifier.startsWith(".") && !specifier.startsWith("/")) return false;
172
- return specifier.includes("__testkit__");
173
- }
174
-
175
- function relativeViolation(productDir, absolutePath, sourceFile, statement) {
176
- const position = sourceFile.getLineAndCharacterOfPosition(statement.getStart(sourceFile));
177
- return {
178
- file: path.relative(productDir, absolutePath).split(path.sep).join("/"),
179
- line: position.line + 1,
180
- snippet: statement.getText(sourceFile),
181
- };
182
- }
@@ -3,7 +3,7 @@ import path from "path";
3
3
  import { isAssistantRunCommand } from "./command-classifier.mjs";
4
4
 
5
5
  const POLL_INTERVAL_MS = 150;
6
- const OBSERVED_KINDS = new Set(["run", "discover", "status", "doctor", "typecheck"]);
6
+ const OBSERVED_KINDS = new Set(["run", "discover", "status", "doctor", "lint", "typecheck"]);
7
7
 
8
8
  export function createAssistantCommandObserver({
9
9
  productDir,
@@ -10,6 +10,7 @@ import { createRunState } from "../state/run/state.mjs";
10
10
  import { buildContextSelection } from "../../results/context.mjs";
11
11
  import { renderDiscoverResult } from "../renderers/discover/text.mjs";
12
12
  import { renderDoctorResult } from "../renderers/doctor/text.mjs";
13
+ import { renderLintResult } from "../renderers/lint/text.mjs";
13
14
  import { renderStatusResult } from "../renderers/status/text.mjs";
14
15
  import { renderTypecheckResult } from "../renderers/typecheck/text.mjs";
15
16
  import { isProviderInstalled } from "./providers/index.mjs";
@@ -1092,6 +1093,7 @@ function renderObservedCommandResult(command) {
1092
1093
  return normalizeRenderedLines((result.results || []).flatMap((entry) => renderStatusResult(entry)));
1093
1094
  }
1094
1095
  if (command.kind === "doctor") return normalizeRenderedLines(renderDoctorResult(result));
1096
+ if (command.kind === "lint") return normalizeRenderedLines(renderLintResult(result));
1095
1097
  if (command.kind === "typecheck") return normalizeRenderedLines(renderTypecheckResult(result));
1096
1098
  return [];
1097
1099
  }
@@ -0,0 +1,37 @@
1
+ import { Command, Flags } from "@oclif/core";
2
+ import { executeLintOperation } from "../operations/lint/operation.mjs";
3
+ import { renderLintResult } from "../renderers/lint/text.mjs";
4
+ import { withAssistantCommandResult } from "../assistant/command-results.mjs";
5
+
6
+ export default class LintCommand extends Command {
7
+ static summary = "Run built-in Testkit repository hygiene checks";
8
+
9
+ static enableJsonFlag = true;
10
+
11
+ static flags = {
12
+ dir: Flags.string({
13
+ description: "Product directory",
14
+ }),
15
+ };
16
+
17
+ async run() {
18
+ return withAssistantCommandResult("lint", async () => {
19
+ const { flags } = await this.parse(LintCommand);
20
+ const result = await executeLintOperation(flags);
21
+
22
+ if (!this.jsonEnabled()) {
23
+ for (const line of renderLintResult(result)) {
24
+ this.log(line);
25
+ }
26
+ }
27
+
28
+ if (!result.ok) {
29
+ const error = new Error("testkit lint failed");
30
+ error.result = result;
31
+ throw error;
32
+ }
33
+
34
+ return result;
35
+ });
36
+ }
37
+ }
@@ -13,6 +13,7 @@ export function normalizeCliArgs(argv) {
13
13
  "discover",
14
14
  "typecheck",
15
15
  "doctor",
16
+ "lint",
16
17
  "browser",
17
18
  "db",
18
19
  ]);
@@ -0,0 +1,12 @@
1
+ import { runLint } from "../../../lint/index.mjs";
2
+ import { loadTestkitConfig } from "../../../config/config-loader.mjs";
3
+ import { resolveProductDir } from "../../../config/paths.mjs";
4
+
5
+ export async function executeLintOperation(flags = {}) {
6
+ const productDir = resolveProductDir(process.cwd(), flags.dir);
7
+ const { config } = await loadTestkitConfig(productDir);
8
+ return runLint({
9
+ dir: productDir,
10
+ ...(config.lint || {}),
11
+ });
12
+ }
@@ -2,6 +2,11 @@ export function renderDoctorResult(result) {
2
2
  const lines = [`testkit doctor ${result.ok ? "passed" : "failed"} for ${result.productDir}`];
3
3
  for (const check of result.checks || []) {
4
4
  lines.push(`${check.level.toUpperCase()} ${check.code} ${check.message}`);
5
+ for (const detail of (check.details || []).slice(0, 5)) {
6
+ if (detail?.ruleId) {
7
+ lines.push(` ${detail.ruleId} ${detail.file}:${detail.line} ${detail.message}`);
8
+ }
9
+ }
5
10
  }
6
11
  return lines;
7
12
  }
@@ -0,0 +1,20 @@
1
+ export function renderLintResult(result) {
2
+ const lines = [
3
+ `testkit lint ${result.ok ? "passed" : "failed"} for ${result.productDir}`,
4
+ `Checked ${result.summary.files} source file(s), ${result.summary.testkitFiles} testkit suite file(s).`,
5
+ ];
6
+
7
+ if (result.violations.length === 0) {
8
+ return lines;
9
+ }
10
+
11
+ lines.push("");
12
+ for (const violation of result.violations) {
13
+ const location = violation.line ? `${violation.file}:${violation.line}` : violation.file;
14
+ lines.push(`${location} ${violation.ruleId}: ${violation.message}`);
15
+ if (violation.snippet) {
16
+ lines.push(` ${violation.snippet}`);
17
+ }
18
+ }
19
+ return lines;
20
+ }
@@ -0,0 +1,132 @@
1
+ import { execa } from "execa";
2
+
3
+ export async function verifySeed(context = {}) {
4
+ const args = context.args || {};
5
+ const table = requireIdentifier(args.table, "verifySeed.args.table");
6
+ const where = normalizeOptionalSql(args.where);
7
+ const minRows = normalizeNonNegativeInteger(args.minRows ?? 1, "verifySeed.args.minRows");
8
+ const databaseUrl = requireDatabaseUrl(context);
9
+ const query = `select count(*)::int from ${table}${where ? ` where ${where}` : ""}`;
10
+ const count = Number(await runScalar(databaseUrl, query, context));
11
+
12
+ if (!Number.isInteger(count) || count < minRows) {
13
+ throw new Error(`Expected at least ${minRows} row(s) in ${table}, found ${count || 0}`);
14
+ }
15
+ }
16
+
17
+ export async function materializePostgresBinding(context = {}) {
18
+ const args = context.args || {};
19
+ const table = requireIdentifier(args.table, "materializePostgresBinding.args.table");
20
+ const keyColumn = requireIdentifier(
21
+ args.keyColumn || "slug",
22
+ "materializePostgresBinding.args.keyColumn"
23
+ );
24
+ const key = normalizeRequiredValue(args.key, "materializePostgresBinding.args.key");
25
+ const values = normalizeObject(args.values, "materializePostgresBinding.args.values");
26
+ const databaseUrl = requireDatabaseUrl(context);
27
+
28
+ const columns = Object.keys(values);
29
+ if (columns.length === 0) {
30
+ throw new Error("materializePostgresBinding.args.values must contain at least one column");
31
+ }
32
+
33
+ const assignments = columns.map((column) => {
34
+ const identifier = requireIdentifier(column, `materializePostgresBinding.args.values.${column}`);
35
+ return `${identifier} = ${toSqlLiteral(values[column])}`;
36
+ });
37
+ const query =
38
+ `with updated as (update ${table} set ${assignments.join(", ")} ` +
39
+ `where ${keyColumn} = ${toSqlLiteral(key)} returning 1) select count(*)::int from updated`;
40
+ const result = await runPsql(databaseUrl, query, context);
41
+ const updated = Number(String(result.stdout || "").trim() || "0");
42
+ if (updated !== 1) {
43
+ throw new Error(`Expected to materialize 1 ${table} row for ${keyColumn}=${key}, updated ${updated || 0}`);
44
+ }
45
+ }
46
+
47
+ function requireDatabaseUrl(context) {
48
+ const databaseUrl = String(context.databaseUrl || context.env?.DATABASE_URL || "").trim();
49
+ if (!databaseUrl) {
50
+ throw new Error("Database template step requires DATABASE_URL");
51
+ }
52
+ return databaseUrl;
53
+ }
54
+
55
+ async function runScalar(databaseUrl, query, context) {
56
+ const result = await runPsql(databaseUrl, query, context);
57
+ return String(result.stdout || "").trim();
58
+ }
59
+
60
+ async function runPsql(databaseUrl, query, context) {
61
+ const result = await execa(
62
+ "psql",
63
+ [
64
+ databaseUrl,
65
+ "-v",
66
+ "ON_ERROR_STOP=1",
67
+ "-X",
68
+ "-q",
69
+ "-t",
70
+ "-A",
71
+ "-c",
72
+ query,
73
+ ],
74
+ {
75
+ cwd: context.cwd || context.productDir || process.cwd(),
76
+ env: context.env || process.env,
77
+ stdout: "pipe",
78
+ stderr: "pipe",
79
+ reject: false,
80
+ }
81
+ );
82
+ if (result.exitCode !== 0) {
83
+ throw new Error(result.stderr || result.shortMessage || `psql failed with exit code ${result.exitCode}`);
84
+ }
85
+ return result;
86
+ }
87
+
88
+ function requireIdentifier(value, label) {
89
+ const normalized = String(value || "").trim();
90
+ if (!/^[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)?$/.test(normalized)) {
91
+ throw new Error(`${label} must be a SQL identifier or schema-qualified identifier`);
92
+ }
93
+ return normalized;
94
+ }
95
+
96
+ function normalizeOptionalSql(value) {
97
+ if (value == null || value === "") return "";
98
+ const normalized = String(value).trim();
99
+ if (!normalized || /;|--|\/\*/.test(normalized)) {
100
+ throw new Error("verifySeed.args.where must be a single SQL predicate without comments or semicolons");
101
+ }
102
+ return normalized;
103
+ }
104
+
105
+ function normalizeNonNegativeInteger(value, label) {
106
+ const normalized = Number(value);
107
+ if (!Number.isInteger(normalized) || normalized < 0) {
108
+ throw new Error(`${label} must be a non-negative integer`);
109
+ }
110
+ return normalized;
111
+ }
112
+
113
+ function normalizeObject(value, label) {
114
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
115
+ throw new Error(`${label} must be an object`);
116
+ }
117
+ return value;
118
+ }
119
+
120
+ function normalizeRequiredValue(value, label) {
121
+ if (value == null || String(value).length === 0) {
122
+ throw new Error(`${label} is required`);
123
+ }
124
+ return value;
125
+ }
126
+
127
+ function toSqlLiteral(value) {
128
+ if (value === null) return "null";
129
+ if (typeof value === "number" && Number.isFinite(value)) return String(value);
130
+ if (typeof value === "boolean") return value ? "true" : "false";
131
+ return `'${String(value).replaceAll("'", "''")}'`;
132
+ }
@@ -9,9 +9,9 @@ export interface DatabaseTemplateConfig {
9
9
 
10
10
  export interface DatabaseTemplateOptions {
11
11
  inputs?: string[];
12
- migrate?: TemplateLifecycleStepConfig | TemplateLifecycleStepConfig[];
13
- seed?: TemplateLifecycleStepConfig | TemplateLifecycleStepConfig[];
14
- verify?: TemplateLifecycleStepConfig | TemplateLifecycleStepConfig[];
12
+ migrate?: TemplateLifecycleStepConfig | TemplateLifecycleStepConfig[] | string | string[];
13
+ seed?: TemplateLifecycleStepConfig | TemplateLifecycleStepConfig[] | string | string[];
14
+ verify?: TemplateLifecycleStepConfig | TemplateLifecycleStepConfig[] | string | string[];
15
15
  }
16
16
 
17
17
  export interface DatabaseSourceSchemaOptions {
@@ -26,6 +26,7 @@ export interface DatabaseSourceSchemaConfig extends DatabaseSourceSchemaOptions
26
26
  }
27
27
 
28
28
  export interface TemplateStepBaseConfig {
29
+ args?: unknown;
29
30
  cwd?: string;
30
31
  inputs?: string[];
31
32
  }
@@ -50,6 +51,8 @@ export type TemplateLifecycleStepConfig =
50
51
  | TemplateSqlFileStepConfig
51
52
  | TemplateModuleStepConfig;
52
53
 
54
+ export interface TemplateLifecycleStepOptions extends TemplateStepBaseConfig {}
55
+
53
56
  export interface TscBuildConfig {
54
57
  kind: "tsc";
55
58
  cwd?: string;
@@ -379,6 +382,8 @@ export interface NodeAppOptions extends Omit<ServiceConfig, "local" | "runtime"
379
382
  }
380
383
 
381
384
  export interface NextAppOptions extends Omit<ServiceConfig, "local" | "runtime" | "env"> {
385
+ api?: string | { baseUrl: string };
386
+ auth?: "disabled-clerk" | { kind: "disabled-clerk" };
382
387
  baseUrl?: string;
383
388
  build?: BuildConfig | null;
384
389
  buildInputs?: string[];
@@ -413,6 +418,14 @@ export interface TestkitConfig {
413
418
  };
414
419
  }
415
420
 
421
+ export interface NodeNextPresetOptions {
422
+ fileTimeoutSeconds?: number;
423
+ install?: "require-host" | "download";
424
+ node?: string;
425
+ npm?: string;
426
+ workers?: number;
427
+ }
428
+
416
429
  export declare function defineConfig<T extends TestkitConfig>(config: T): T;
417
430
  export declare function defineFile<T extends TestkitFileMetadata>(metadata: T): T;
418
431
  export declare const app: {
@@ -423,19 +436,39 @@ export declare const database: {
423
436
  schema: {
424
437
  fromEnv(envName: string, options?: DatabaseSourceSchemaOptions): DatabaseSourceSchemaConfig;
425
438
  };
439
+ steps: {
440
+ materializePostgresBinding(options?: unknown): TemplateModuleStepConfig;
441
+ verifySeed(options?: unknown): TemplateModuleStepConfig;
442
+ };
426
443
  postgres(
427
444
  options?: Omit<LocalDatabaseConfig, "provider" | "template"> & {
445
+ inputs?: string[];
446
+ migrate?: TemplateLifecycleStepConfig | TemplateLifecycleStepConfig[] | string | string[];
447
+ seed?: TemplateLifecycleStepConfig | TemplateLifecycleStepConfig[] | string | string[];
428
448
  template?: DatabaseTemplateOptions;
449
+ verify?: TemplateLifecycleStepConfig | TemplateLifecycleStepConfig[] | string | string[];
429
450
  }
430
451
  ): LocalDatabaseConfig;
431
452
  fixture(
432
453
  options?: Omit<LocalDatabaseConfig, "provider" | "template"> & {
454
+ inputs?: string[];
455
+ migrate?: TemplateLifecycleStepConfig | TemplateLifecycleStepConfig[] | string | string[];
456
+ seed?: TemplateLifecycleStepConfig | TemplateLifecycleStepConfig[] | string | string[];
433
457
  template?: DatabaseTemplateOptions;
458
+ verify?: TemplateLifecycleStepConfig | TemplateLifecycleStepConfig[] | string | string[];
434
459
  discovery?: DiscoveryConfig;
435
460
  envFiles?: string[];
436
461
  }
437
462
  ): ServiceConfig;
438
463
  };
464
+ export declare const step: {
465
+ command(run: string, options?: TemplateLifecycleStepOptions): TemplateCommandStepConfig;
466
+ module(target: string, options?: TemplateLifecycleStepOptions): TemplateModuleStepConfig;
467
+ sqlFile(path: string, options?: TemplateLifecycleStepOptions): TemplateSqlFileStepConfig;
468
+ };
469
+ export declare const presets: {
470
+ nodeNext(options?: NodeNextPresetOptions): Pick<TestkitConfig, "execution" | "toolchains">;
471
+ };
439
472
  export declare const toolchain: {
440
473
  node(options?: NodeToolchainConfig): NodeToolchainConfig;
441
474
  };