@wix/pathgrade 0.36.0 → 1.0.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 (117) hide show
  1. package/LICENSE +24 -0
  2. package/README.md +508 -0
  3. package/dist/adapter-kit/index.d.ts +17 -0
  4. package/dist/adapter-kit/index.js +14 -0
  5. package/dist/adapters/jest/index.d.ts +3 -0
  6. package/dist/adapters/jest/index.js +3 -0
  7. package/dist/adapters/jest/invocation-adapter.d.ts +15 -0
  8. package/dist/adapters/jest/invocation-adapter.js +115 -0
  9. package/dist/adapters/jest/lifecycle.d.ts +21 -0
  10. package/dist/adapters/jest/lifecycle.js +62 -0
  11. package/dist/adapters/jest/metadata.d.ts +6 -0
  12. package/dist/adapters/jest/metadata.js +35 -0
  13. package/dist/adapters/jest/reporter.cjs +10 -0
  14. package/dist/adapters/jest/reporter.d.ts +5 -0
  15. package/dist/adapters/jest/reporter.js +67 -0
  16. package/dist/adapters/jest/results.d.ts +31 -0
  17. package/dist/adapters/jest/results.js +144 -0
  18. package/dist/adapters/jest/runner-adapter.d.ts +9 -0
  19. package/dist/adapters/jest/runner-adapter.js +49 -0
  20. package/dist/adapters/jest/setup.d.ts +1 -0
  21. package/dist/adapters/jest/setup.js +10 -0
  22. package/dist/adapters/node-test/index.d.ts +8 -0
  23. package/dist/adapters/node-test/index.js +106 -0
  24. package/dist/adapters/node-test/invocation-adapter.d.ts +5 -0
  25. package/dist/adapters/node-test/invocation-adapter.js +47 -0
  26. package/dist/adapters/node-test/runner-adapter.d.ts +2 -0
  27. package/dist/adapters/node-test/runner-adapter.js +116 -0
  28. package/dist/adapters/vitest/index.d.ts +9 -0
  29. package/dist/adapters/vitest/index.js +82 -0
  30. package/dist/adapters/vitest/lifecycle.d.ts +24 -0
  31. package/dist/adapters/vitest/lifecycle.js +41 -0
  32. package/dist/adapters/vitest/reporter.d.ts +12 -0
  33. package/dist/adapters/vitest/reporter.js +75 -0
  34. package/dist/adapters/vitest/setup.d.ts +1 -0
  35. package/dist/adapters/vitest/setup.js +3 -0
  36. package/dist/agents/codex-app-server/agent.js +5 -1
  37. package/dist/agents/codex-app-server/protocol/index.js +3 -3
  38. package/dist/agents/codex-app-server/turn-completion.d.ts +1 -0
  39. package/dist/agents/codex-app-server/turn-completion.js +9 -0
  40. package/dist/commands/affected.js +22 -15
  41. package/dist/commands/run-args.d.ts +4 -3
  42. package/dist/commands/run-args.js +12 -6
  43. package/dist/commands/run-changed.d.ts +4 -4
  44. package/dist/commands/run-changed.js +45 -58
  45. package/dist/config/pathgrade.d.ts +48 -0
  46. package/dist/config/pathgrade.js +204 -0
  47. package/dist/pathgrade.d.ts +2 -2
  48. package/dist/pathgrade.js +35 -34
  49. package/dist/plugin/index.d.ts +2 -9
  50. package/dist/plugin/index.js +1 -81
  51. package/dist/plugin/lifecycle.d.ts +1 -45
  52. package/dist/plugin/lifecycle.js +1 -146
  53. package/dist/plugin/reporter.d.ts +1 -37
  54. package/dist/plugin/reporter.js +1 -280
  55. package/dist/plugin/setup.d.ts +1 -1
  56. package/dist/plugin/setup.js +1 -3
  57. package/dist/providers/copy-filter.d.ts +5 -0
  58. package/dist/providers/copy-filter.js +9 -0
  59. package/dist/providers/sandbox.js +2 -2
  60. package/dist/providers/workspace.d.ts +6 -0
  61. package/dist/providers/workspace.js +34 -14
  62. package/dist/reporters/diagnostics.d.ts +2 -40
  63. package/dist/reporters/diagnostics.js +1 -80
  64. package/dist/reporters/report-summary.d.ts +6 -0
  65. package/dist/reporters/report-summary.js +29 -0
  66. package/dist/reporting/artifacts.d.ts +2 -0
  67. package/dist/reporting/artifacts.js +20 -0
  68. package/dist/reporting/core.d.ts +2 -0
  69. package/dist/reporting/core.js +173 -0
  70. package/dist/reporting/types.d.ts +57 -0
  71. package/dist/reporting/types.js +1 -0
  72. package/dist/reporting/vitest-edge.d.ts +1 -0
  73. package/dist/reporting/vitest-edge.js +1 -0
  74. package/dist/runners/adapter-loader.d.ts +17 -0
  75. package/dist/runners/adapter-loader.js +67 -0
  76. package/dist/runners/adapter.d.ts +65 -0
  77. package/dist/runners/adapter.js +1 -0
  78. package/dist/runners/invocation.d.ts +10 -0
  79. package/dist/runners/invocation.js +1 -0
  80. package/dist/runners/lifecycle-hooks.d.ts +2 -0
  81. package/dist/runners/lifecycle-hooks.js +18 -0
  82. package/dist/runners/model-builders.d.ts +4 -0
  83. package/dist/runners/model-builders.js +67 -0
  84. package/dist/runners/model-validation.d.ts +12 -0
  85. package/dist/runners/model-validation.js +213 -0
  86. package/dist/runners/model.d.ts +117 -0
  87. package/dist/runners/model.js +1 -0
  88. package/dist/runners/orchestrator.d.ts +30 -0
  89. package/dist/runners/orchestrator.js +74 -0
  90. package/dist/runners/report-projection.d.ts +6 -0
  91. package/dist/runners/report-projection.js +62 -0
  92. package/dist/runners/selection.d.ts +6 -0
  93. package/dist/runners/selection.js +10 -0
  94. package/dist/runners/vitest-adapter.d.ts +8 -0
  95. package/dist/runners/vitest-adapter.js +108 -0
  96. package/dist/runners/vitest-invocation.d.ts +10 -0
  97. package/dist/runners/vitest-invocation.js +44 -0
  98. package/dist/runners/vitest-lifecycle.d.ts +43 -0
  99. package/dist/runners/vitest-lifecycle.js +150 -0
  100. package/dist/sdk/agent.js +15 -16
  101. package/dist/sdk/case-context.d.ts +20 -0
  102. package/dist/sdk/case-context.js +31 -0
  103. package/dist/sdk/diagnostics.d.ts +40 -0
  104. package/dist/sdk/diagnostics.js +80 -0
  105. package/dist/sdk/eval-runtime.d.ts +4 -0
  106. package/dist/sdk/eval-runtime.js +11 -1
  107. package/dist/sdk/evaluate.js +2 -1
  108. package/dist/sdk/index.d.ts +2 -0
  109. package/dist/sdk/index.js +1 -0
  110. package/dist/sdk/lifecycle.d.ts +40 -0
  111. package/dist/sdk/lifecycle.js +170 -0
  112. package/dist/sdk/result-capture.d.ts +18 -0
  113. package/dist/sdk/result-capture.js +49 -0
  114. package/dist/sdk/types.d.ts +1 -1
  115. package/dist/types.d.ts +2 -2
  116. package/dist/utils/llm-providers/anthropic.js +9 -2
  117. package/package.json +73 -36
@@ -2,11 +2,12 @@
2
2
  * Parse `pathgrade run` arguments.
3
3
  *
4
4
  * Recognized pathgrade-specific flags are consumed here; everything else
5
- * passes through to vitest. After a literal `--` separator, all remaining
6
- * args are forwarded verbatim (vitest uses `--` the same way).
5
+ * passes through to the selected runner. After a literal `--` separator,
6
+ * all remaining args are forwarded verbatim.
7
7
  */
8
8
  export interface PathgradeRunArgs {
9
- vitestArgs: string[];
9
+ runnerArgs: string[];
10
+ adapterName?: string;
10
11
  forceDiagnostics: boolean;
11
12
  forceVerbose: boolean;
12
13
  changed: boolean;
@@ -2,21 +2,22 @@
2
2
  * Parse `pathgrade run` arguments.
3
3
  *
4
4
  * Recognized pathgrade-specific flags are consumed here; everything else
5
- * passes through to vitest. After a literal `--` separator, all remaining
6
- * args are forwarded verbatim (vitest uses `--` the same way).
5
+ * passes through to the selected runner. After a literal `--` separator,
6
+ * all remaining args are forwarded verbatim.
7
7
  */
8
8
  export function parsePathgradeRunArgs(args) {
9
- const vitestArgs = [];
9
+ const runnerArgs = [];
10
10
  let forceDiagnostics = false;
11
11
  let forceVerbose = false;
12
12
  let changed = false;
13
13
  let quiet = false;
14
14
  let since;
15
15
  let changedFilesPath;
16
+ let adapterName;
16
17
  let passthrough = false;
17
18
  for (const arg of args) {
18
19
  if (passthrough) {
19
- vitestArgs.push(arg);
20
+ runnerArgs.push(arg);
20
21
  continue;
21
22
  }
22
23
  if (arg === '--') {
@@ -39,6 +40,10 @@ export function parsePathgradeRunArgs(args) {
39
40
  quiet = true;
40
41
  continue;
41
42
  }
43
+ if (arg.startsWith('--adapter=')) {
44
+ adapterName = arg.slice('--adapter='.length);
45
+ continue;
46
+ }
42
47
  if (arg.startsWith('--since=')) {
43
48
  since = arg.slice('--since='.length);
44
49
  continue;
@@ -47,7 +52,7 @@ export function parsePathgradeRunArgs(args) {
47
52
  changedFilesPath = arg.slice('--changed-files='.length);
48
53
  continue;
49
54
  }
50
- vitestArgs.push(arg);
55
+ runnerArgs.push(arg);
51
56
  }
52
57
  const warnings = [];
53
58
  if (!changed && since !== undefined) {
@@ -57,11 +62,12 @@ export function parsePathgradeRunArgs(args) {
57
62
  warnings.push('--changed-files has no effect without --changed; the flag is being ignored.');
58
63
  }
59
64
  const base = {
60
- vitestArgs,
65
+ runnerArgs,
61
66
  forceDiagnostics,
62
67
  forceVerbose,
63
68
  changed,
64
69
  quiet,
70
+ adapterName,
65
71
  since,
66
72
  changedFilesPath,
67
73
  };
@@ -12,15 +12,15 @@
12
12
  * Git-resolution failures exit non-zero. The selection pipeline is the
13
13
  * only producer of the file list here.
14
14
  */
15
+ import type { RunnerInvocationAdapter } from '../runners/invocation.js';
16
+ import type { SpawnVitest } from '../runners/vitest-invocation.js';
15
17
  import type { PathgradeRunArgs } from './run-args.js';
16
- export interface SpawnVitestRequest {
17
- argv: string[];
18
- }
19
- export type SpawnVitest = (req: SpawnVitestRequest) => Promise<number> | number;
18
+ export type { SpawnVitest };
20
19
  export interface RunChangedOptions {
21
20
  cwd: string;
22
21
  parsed: PathgradeRunArgs;
23
22
  /** Override for tests; default spawns `npx vitest` inheriting stdio. */
24
23
  spawnVitest?: SpawnVitest;
24
+ runnerInvocation?: RunnerInvocationAdapter;
25
25
  }
26
26
  export declare function runChanged(opts: RunChangedOptions): Promise<number>;
@@ -13,15 +13,42 @@
13
13
  * only producer of the file list here.
14
14
  */
15
15
  import * as fs from 'fs';
16
- import picomatch from 'picomatch';
17
16
  import { selectAffected } from '../affected/select.js';
18
17
  import { resolveBaseRef, computeChangedFiles } from '../affected/git.js';
19
- import { loadAffectedConfig } from '../affected/config.js';
20
18
  import { writeSidecar } from '../affected/sidecar.js';
21
- import { discoverEvalFiles } from './affected.js';
19
+ import { discoverPathgradeEvalFiles } from '../evals/discovery.js';
20
+ import { resolvePathgradeConfig } from '../config/pathgrade.js';
21
+ import { loadRunnerInvocationAdapter } from '../runners/adapter-loader.js';
22
22
  export async function runChanged(opts) {
23
23
  const { cwd, parsed } = opts;
24
- const spawnVitest = opts.spawnVitest ?? defaultSpawnVitest;
24
+ const runnerEnv = {
25
+ ...process.env,
26
+ ...(parsed.forceDiagnostics ? { PATHGRADE_DIAGNOSTICS: '1' } : {}),
27
+ ...(parsed.forceVerbose ? { PATHGRADE_VERBOSE: '1' } : {}),
28
+ };
29
+ const configPath = findVitestConfigArg(parsed.runnerArgs);
30
+ let config;
31
+ let runnerInvocation;
32
+ try {
33
+ config = await resolvePathgradeConfig({
34
+ cwd,
35
+ legacyVitestConfigPath: configPath,
36
+ warn: w => {
37
+ if (!parsed.quiet)
38
+ process.stderr.write(`${w}\n`);
39
+ },
40
+ });
41
+ runnerInvocation = opts.runnerInvocation ?? await loadRunnerInvocationAdapter({
42
+ adapterName: parsed.adapterName ?? config.runner.adapter,
43
+ cwd,
44
+ config,
45
+ spawnVitest: opts.spawnVitest,
46
+ });
47
+ }
48
+ catch (err) {
49
+ process.stderr.write(`${errMsg(err)}\n`);
50
+ return 1;
51
+ }
25
52
  // 1. Change-set
26
53
  let baseRef;
27
54
  let changedFiles;
@@ -53,22 +80,11 @@ export async function runChanged(opts) {
53
80
  baseRefLine = `pathgrade: base = ${baseRef} (merge-base with HEAD)`;
54
81
  }
55
82
  // 2. Selection
56
- const configPath = findVitestConfigArg(parsed.vitestArgs);
57
- let config;
58
- try {
59
- config = await loadAffectedConfig(cwd, {
60
- configPath,
61
- onWarning: w => {
62
- if (!parsed.quiet)
63
- process.stderr.write(`${w}\n`);
64
- },
65
- });
66
- }
67
- catch (err) {
68
- process.stderr.write(`${errMsg(err)}\n`);
69
- return 1;
70
- }
71
- const evalFiles = filterEvalFilesForConfig(discoverEvalFiles(cwd), config);
83
+ const evalFiles = discoverPathgradeEvalFiles({
84
+ cwd,
85
+ include: config.evals.include,
86
+ exclude: config.evals.exclude,
87
+ });
72
88
  let result;
73
89
  try {
74
90
  result = selectAffected({
@@ -76,7 +92,7 @@ export async function runChanged(opts) {
76
92
  changedFiles,
77
93
  repoRoot: cwd,
78
94
  baseRef,
79
- global: config.global,
95
+ global: config.affected.global,
80
96
  });
81
97
  }
82
98
  catch (err) {
@@ -103,16 +119,16 @@ export async function runChanged(opts) {
103
119
  return 0;
104
120
  }
105
121
  const selectedFiles = result.selected.map(s => s.file);
106
- if (hasPassWithNoTests(parsed.vitestArgs)) {
107
- process.stderr.write('pathgrade run: --passWithNoTests cannot be used with pathgrade run --changed. ' +
108
- 'The command already exits 0 when no evals are selected; if selected evals resolve to no Vitest files, CI must fail.\n');
109
- return 1;
110
- }
111
- const argv = ['run', ...selectedFiles, ...parsed.vitestArgs];
122
+ const runnerArgs = [...config.runner.args, ...parsed.runnerArgs];
112
123
  if (!parsed.quiet) {
113
- process.stderr.write(`→ vitest run ${selectedFiles.join(' ')}\n`);
124
+ process.stderr.write(`→ ${runnerInvocation.name} run ${selectedFiles.join(' ')}\n`);
114
125
  }
115
- return await spawnVitest({ argv });
126
+ return await runnerInvocation.run({
127
+ cwd,
128
+ runnerArgs,
129
+ selectedFiles,
130
+ env: runnerEnv,
131
+ });
116
132
  }
117
133
  function printRunStartSummary(input) {
118
134
  const { baseRefLine, totalEvals, changedCount, result } = input;
@@ -141,15 +157,6 @@ function readChangedFilesList(filePath) {
141
157
  function errMsg(err) {
142
158
  return err instanceof Error ? err.message : String(err);
143
159
  }
144
- function hasPassWithNoTests(args) {
145
- return args.some(arg => {
146
- if (arg === '--passWithNoTests')
147
- return true;
148
- if (!arg.startsWith('--passWithNoTests='))
149
- return false;
150
- return arg.slice('--passWithNoTests='.length).toLowerCase() !== 'false';
151
- });
152
- }
153
160
  function findVitestConfigArg(args) {
154
161
  for (let i = 0; i < args.length; i++) {
155
162
  const arg = args[i];
@@ -162,23 +169,3 @@ function findVitestConfigArg(args) {
162
169
  }
163
170
  return undefined;
164
171
  }
165
- function filterEvalFilesForConfig(evalFiles, config) {
166
- if (!config.include && !config.exclude)
167
- return evalFiles;
168
- const includeMatchers = config.include?.map(g => picomatch(g, { dot: true }));
169
- const excludeMatchers = config.exclude?.map(g => picomatch(g, { dot: true })) ?? [];
170
- return evalFiles.filter(file => {
171
- const included = includeMatchers ? includeMatchers.some(m => m(file)) : true;
172
- return included && !excludeMatchers.some(m => m(file));
173
- });
174
- }
175
- async function defaultSpawnVitest(req) {
176
- const { spawn } = await import('child_process');
177
- return await new Promise(resolve => {
178
- const child = spawn('npx', ['vitest', ...req.argv], {
179
- stdio: 'inherit',
180
- shell: true,
181
- });
182
- child.on('close', code => resolve(code ?? 0));
183
- });
184
- }
@@ -0,0 +1,48 @@
1
+ export interface PathgradeConfig {
2
+ runner?: {
3
+ adapter?: string;
4
+ args?: string[];
5
+ };
6
+ evals?: {
7
+ include?: string[];
8
+ exclude?: string[];
9
+ };
10
+ affected?: {
11
+ global?: string[];
12
+ };
13
+ reporter?: 'cli' | 'browser' | 'json';
14
+ diagnostics?: boolean;
15
+ verbose?: boolean;
16
+ ci?: {
17
+ threshold?: number;
18
+ };
19
+ }
20
+ export interface ResolvedPathgradeConfig {
21
+ runner: {
22
+ adapter: string;
23
+ args: string[];
24
+ };
25
+ evals: {
26
+ include: string[];
27
+ exclude: string[];
28
+ };
29
+ affected: {
30
+ global: string[];
31
+ };
32
+ reporter?: 'cli' | 'browser' | 'json';
33
+ diagnostics: boolean;
34
+ verbose: boolean;
35
+ ci: {
36
+ threshold?: number;
37
+ };
38
+ }
39
+ export declare const DEFAULT_EVAL_INCLUDE: string[];
40
+ export declare const DEFAULT_EVAL_EXCLUDE: string[];
41
+ export declare function defaultPathgradeConfig(): ResolvedPathgradeConfig;
42
+ export declare function resolvePathgradeConfig(input: {
43
+ cwd: string;
44
+ cli?: PathgradeConfig;
45
+ configPath?: string;
46
+ legacyVitestConfigPath?: string;
47
+ warn?: (message: string) => void;
48
+ }): Promise<ResolvedPathgradeConfig>;
@@ -0,0 +1,204 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import { createJiti } from 'jiti';
4
+ export const DEFAULT_EVAL_INCLUDE = ['**/*.eval.ts'];
5
+ export const DEFAULT_EVAL_EXCLUDE = [
6
+ '**/node_modules/**',
7
+ '**/.git/**',
8
+ '.worktrees/**',
9
+ 'worktrees/**',
10
+ '**/fixtures/**',
11
+ ];
12
+ const PATHGRADE_CONFIG_CANDIDATES = [
13
+ 'pathgrade.config.ts',
14
+ 'pathgrade.config.mts',
15
+ 'pathgrade.config.js',
16
+ 'pathgrade.config.mjs',
17
+ ];
18
+ export function defaultPathgradeConfig() {
19
+ return {
20
+ runner: {
21
+ adapter: 'vitest',
22
+ args: [],
23
+ },
24
+ evals: {
25
+ include: [...DEFAULT_EVAL_INCLUDE],
26
+ exclude: [...DEFAULT_EVAL_EXCLUDE],
27
+ },
28
+ affected: {
29
+ global: [],
30
+ },
31
+ diagnostics: false,
32
+ verbose: false,
33
+ ci: {},
34
+ };
35
+ }
36
+ export async function resolvePathgradeConfig(input) {
37
+ const fileConfig = await loadPathgradeConfigFile(input.cwd, input.configPath);
38
+ const legacyConfig = await loadLegacyVitestConfig(input.cwd, input.legacyVitestConfigPath, input.warn);
39
+ return mergePathgradeConfig(mergePathgradeConfig(mergePathgradeConfig(defaultPathgradeConfig(), legacyConfig), fileConfig), input.cli);
40
+ }
41
+ function mergePathgradeConfig(base, override) {
42
+ if (!override)
43
+ return base;
44
+ return {
45
+ runner: {
46
+ adapter: override.runner?.adapter ?? base.runner.adapter,
47
+ args: override.runner?.args ?? base.runner.args,
48
+ },
49
+ evals: {
50
+ include: override.evals?.include ?? base.evals.include,
51
+ exclude: override.evals?.exclude ?? base.evals.exclude,
52
+ },
53
+ affected: {
54
+ global: override.affected?.global ?? base.affected.global,
55
+ },
56
+ reporter: override.reporter ?? base.reporter,
57
+ diagnostics: override.diagnostics ?? base.diagnostics,
58
+ verbose: override.verbose ?? base.verbose,
59
+ ci: {
60
+ threshold: override.ci?.threshold ?? base.ci.threshold,
61
+ },
62
+ };
63
+ }
64
+ async function loadPathgradeConfigFile(cwd, configPath) {
65
+ const resolved = configPath
66
+ ? path.resolve(cwd, configPath)
67
+ : PATHGRADE_CONFIG_CANDIDATES
68
+ .map(candidate => path.join(cwd, candidate))
69
+ .find(candidate => fs.existsSync(candidate));
70
+ if (!resolved)
71
+ return undefined;
72
+ try {
73
+ const jiti = createJiti(cwd, { interopDefault: true });
74
+ const loaded = await jiti.import(resolved, { default: true });
75
+ return validatePathgradeConfig(loaded, path.relative(cwd, resolved));
76
+ }
77
+ catch (err) {
78
+ if (err instanceof InvalidPathgradeConfigError) {
79
+ throw err;
80
+ }
81
+ throw new Error(`pathgrade: failed to load ${path.relative(cwd, resolved)}: ${errMsg(err)}`);
82
+ }
83
+ }
84
+ function isObject(value) {
85
+ return value !== null && typeof value === 'object';
86
+ }
87
+ class InvalidPathgradeConfigError extends Error {
88
+ }
89
+ function validatePathgradeConfig(value, label) {
90
+ if (!isObject(value)) {
91
+ throw invalidConfig(label, 'default export must be an object');
92
+ }
93
+ validateOptionalObject(value.runner, label, 'runner');
94
+ const runner = asOptionalObject(value.runner);
95
+ validateOptionalString(runner?.adapter, label, 'runner.adapter');
96
+ validateOptionalStringArray(runner?.args, label, 'runner.args');
97
+ validateOptionalObject(value.evals, label, 'evals');
98
+ const evals = asOptionalObject(value.evals);
99
+ validateOptionalStringArray(evals?.include, label, 'evals.include');
100
+ validateOptionalStringArray(evals?.exclude, label, 'evals.exclude');
101
+ validateOptionalObject(value.affected, label, 'affected');
102
+ const affected = asOptionalObject(value.affected);
103
+ validateOptionalStringArray(affected?.global, label, 'affected.global');
104
+ if (value.reporter !== undefined && !['cli', 'browser', 'json'].includes(String(value.reporter))) {
105
+ throw invalidConfig(label, 'reporter must be one of cli, browser, or json');
106
+ }
107
+ validateOptionalBoolean(value.diagnostics, label, 'diagnostics');
108
+ validateOptionalBoolean(value.verbose, label, 'verbose');
109
+ validateOptionalObject(value.ci, label, 'ci');
110
+ const ci = asOptionalObject(value.ci);
111
+ validateOptionalNumber(ci?.threshold, label, 'ci.threshold');
112
+ return value;
113
+ }
114
+ function asOptionalObject(value) {
115
+ return isObject(value) ? value : undefined;
116
+ }
117
+ function validateOptionalObject(value, label, field) {
118
+ if (value !== undefined && !isObject(value)) {
119
+ throw invalidConfig(label, `${field} must be an object`);
120
+ }
121
+ }
122
+ function validateOptionalString(value, label, field) {
123
+ if (value !== undefined && typeof value !== 'string') {
124
+ throw invalidConfig(label, `${field} must be a string`);
125
+ }
126
+ }
127
+ function validateOptionalStringArray(value, label, field) {
128
+ if (value !== undefined && (!Array.isArray(value) || value.some(item => typeof item !== 'string'))) {
129
+ throw invalidConfig(label, `${field} must be an array of strings`);
130
+ }
131
+ }
132
+ function validateOptionalBoolean(value, label, field) {
133
+ if (value !== undefined && typeof value !== 'boolean') {
134
+ throw invalidConfig(label, `${field} must be a boolean`);
135
+ }
136
+ }
137
+ function validateOptionalNumber(value, label, field) {
138
+ if (value !== undefined && typeof value !== 'number') {
139
+ throw invalidConfig(label, `${field} must be a number`);
140
+ }
141
+ }
142
+ function invalidConfig(label, reason) {
143
+ return new InvalidPathgradeConfigError(`pathgrade: invalid ${label}: ${reason}`);
144
+ }
145
+ function errMsg(err) {
146
+ return err instanceof Error ? err.message : String(err);
147
+ }
148
+ const VITEST_CONFIG_CANDIDATES = [
149
+ 'vitest.config.ts',
150
+ 'vitest.config.mts',
151
+ 'vitest.config.js',
152
+ 'vitest.config.mjs',
153
+ ];
154
+ async function loadLegacyVitestConfig(cwd, configPath, warn = () => { }) {
155
+ const resolved = configPath
156
+ ? path.resolve(cwd, configPath)
157
+ : VITEST_CONFIG_CANDIDATES
158
+ .map(candidate => path.join(cwd, candidate))
159
+ .find(candidate => fs.existsSync(candidate));
160
+ if (!resolved)
161
+ return undefined;
162
+ let loaded;
163
+ try {
164
+ const jiti = createJiti(cwd, { interopDefault: true });
165
+ loaded = await jiti.import(resolved, { default: true });
166
+ }
167
+ catch (err) {
168
+ const message = `pathgrade: failed to load ${path.relative(cwd, resolved)}: ${errMsg(err)}`;
169
+ if (configPath)
170
+ throw new Error(message);
171
+ warn(message);
172
+ return undefined;
173
+ }
174
+ const plugins = findPluginsList(loaded);
175
+ const pathgradePlugin = plugins?.find(plugin => isPathgradePlugin(plugin));
176
+ const opts = isObject(pathgradePlugin)
177
+ ? pathgradePlugin.__pathgradeOptions
178
+ : undefined;
179
+ if (!isObject(opts))
180
+ return undefined;
181
+ return {
182
+ evals: {
183
+ ...(Array.isArray(opts.include) ? { include: opts.include } : {}),
184
+ ...(Array.isArray(opts.exclude) ? { exclude: opts.exclude } : {}),
185
+ },
186
+ affected: {
187
+ global: isObject(opts.affected) && Array.isArray(opts.affected.global)
188
+ ? opts.affected.global
189
+ : [],
190
+ },
191
+ };
192
+ }
193
+ function findPluginsList(config) {
194
+ if (!isObject(config))
195
+ return null;
196
+ if (Array.isArray(config.plugins))
197
+ return config.plugins;
198
+ if (isObject(config.test) && Array.isArray(config.test.plugins))
199
+ return config.test.plugins;
200
+ return null;
201
+ }
202
+ function isPathgradePlugin(plugin) {
203
+ return isObject(plugin) && (plugin.name === 'pathgrade' || '__pathgradeOptions' in plugin);
204
+ }
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * pathgrade CLI — thin wrapper around vitest
3
+ * pathgrade CLI — runner-neutral wrapper around Pathgrade adapters
4
4
  *
5
5
  * Usage:
6
- * pathgrade run [-- vitest-args] Run evals via vitest (loads .env, validates API keys)
6
+ * pathgrade run [-- runner-args] Run evals via the selected adapter
7
7
  * pathgrade init [--force] Generate eval scaffolding
8
8
  * pathgrade preview [browser] View results (CLI default, or browser)
9
9
  */
package/dist/pathgrade.js CHANGED
@@ -1,15 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * pathgrade CLI — thin wrapper around vitest
3
+ * pathgrade CLI — runner-neutral wrapper around Pathgrade adapters
4
4
  *
5
5
  * Usage:
6
- * pathgrade run [-- vitest-args] Run evals via vitest (loads .env, validates API keys)
6
+ * pathgrade run [-- runner-args] Run evals via the selected adapter
7
7
  * pathgrade init [--force] Generate eval scaffolding
8
8
  * pathgrade preview [browser] View results (CLI default, or browser)
9
9
  */
10
10
  import * as fs from 'fs';
11
11
  import * as path from 'path';
12
- import { spawn } from 'child_process';
13
12
  import { runInit } from './commands/init.js';
14
13
  import { runAnalyze } from './commands/analyze.js';
15
14
  import { runValidate, runValidateAffected } from './commands/validate.js';
@@ -20,6 +19,8 @@ import { runReport } from './commands/report.js';
20
19
  import { runAffected } from './commands/affected.js';
21
20
  import { runChanged } from './commands/run-changed.js';
22
21
  import { clearSidecar } from './affected/sidecar.js';
22
+ import { resolvePathgradeConfig } from './config/pathgrade.js';
23
+ import { loadRunnerInvocationAdapter } from './runners/adapter-loader.js';
23
24
  import { fmt } from './utils/cli.js';
24
25
  import { shutdown } from './utils/shutdown.js';
25
26
  function loadDotenv() {
@@ -52,7 +53,7 @@ function validateApiKeys() {
52
53
  const hasClaude = !!process.env.HOME; // Claude CLI uses OS keychain, just check it exists
53
54
  if (!hasAnthropic && !hasOpenAI) {
54
55
  console.log(`\n ${fmt.dim('warning:')} No API keys found. Set ANTHROPIC_API_KEY or OPENAI_API_KEY in .env or environment.\n` +
55
- ` ${fmt.dim(' Claude CLI auth (keychain) and Codex cached login (~/.codex/auth.json) may still work if installed.')}\n`);
56
+ ` ${fmt.dim(' Claude CLI auth (keychain) and Codex exec cached login (~/.codex/auth.json) may still work if installed.')}\n`);
56
57
  }
57
58
  }
58
59
  async function main() {
@@ -144,7 +145,7 @@ async function main() {
144
145
  return;
145
146
  }
146
147
  if (command === 'run' || !command || command.startsWith('-')) {
147
- // pathgrade run [--changed [--since=…|--changed-files=…]] [--] [vitest-args]
148
+ // pathgrade run [--changed [--since=…|--changed-files=…]] [--] [runner-args]
148
149
  loadDotenv();
149
150
  validateApiKeys();
150
151
  const parsed = parsePathgradeRunArgs(command === 'run' ? args.slice(1) : args);
@@ -155,18 +156,6 @@ async function main() {
155
156
  const exitCode = await runChanged({
156
157
  cwd: process.cwd(),
157
158
  parsed,
158
- spawnVitest: ({ argv }) => new Promise(resolve => {
159
- const child = spawn('npx', ['vitest', ...argv], {
160
- stdio: 'inherit',
161
- env: {
162
- ...process.env,
163
- ...(parsed.forceDiagnostics ? { PATHGRADE_DIAGNOSTICS: '1' } : {}),
164
- ...(parsed.forceVerbose ? { PATHGRADE_VERBOSE: '1' } : {}),
165
- },
166
- shell: true,
167
- });
168
- child.on('close', code => resolve(code ?? 0));
169
- }),
170
159
  });
171
160
  process.exitCode = exitCode;
172
161
  return;
@@ -175,18 +164,28 @@ async function main() {
175
164
  // previous `--changed` run so it doesn't leak into this full-suite
176
165
  // run (the reporter would otherwise merge old metadata).
177
166
  await clearSidecar(process.cwd());
178
- const child = spawn('npx', ['vitest', 'run', ...parsed.vitestArgs], {
179
- stdio: 'inherit',
180
- env: {
181
- ...process.env,
182
- ...(parsed.forceDiagnostics ? { PATHGRADE_DIAGNOSTICS: '1' } : {}),
183
- ...(parsed.forceVerbose ? { PATHGRADE_VERBOSE: '1' } : {}),
184
- },
185
- shell: true,
186
- });
187
- child.on('close', (code) => {
188
- process.exitCode = code ?? 0;
189
- });
167
+ const env = {
168
+ ...process.env,
169
+ ...(parsed.forceDiagnostics ? { PATHGRADE_DIAGNOSTICS: '1' } : {}),
170
+ ...(parsed.forceVerbose ? { PATHGRADE_VERBOSE: '1' } : {}),
171
+ };
172
+ try {
173
+ const config = await resolvePathgradeConfig({ cwd: process.cwd() });
174
+ const runner = await loadRunnerInvocationAdapter({
175
+ adapterName: parsed.adapterName ?? config.runner.adapter,
176
+ cwd: process.cwd(),
177
+ config,
178
+ });
179
+ process.exitCode = await runner.run({
180
+ cwd: process.cwd(),
181
+ runnerArgs: [...config.runner.args, ...parsed.runnerArgs],
182
+ env,
183
+ });
184
+ }
185
+ catch (err) {
186
+ console.error(err instanceof Error ? err.message : String(err));
187
+ process.exitCode = 1;
188
+ }
190
189
  return;
191
190
  }
192
191
  console.error(`Unknown command: ${command}`);
@@ -195,13 +194,15 @@ async function main() {
195
194
  }
196
195
  function printHelp() {
197
196
  console.log(`
198
- pathgrade - Evaluate AI agent skills with vitest
197
+ pathgrade - Evaluate AI agent skills with a runner adapter
199
198
 
200
199
  Usage:
201
- pathgrade run [-- vitest-args] Run evals (loads .env, delegates to vitest)
200
+ pathgrade run [-- runner-args] Run evals (loads .env, delegates to the selected adapter)
202
201
  [--changed] Run only evals affected by the current PR/change-set
203
202
  [--since=<ref>] Override base ref (implies git mode)
204
203
  [--changed-files=<path>] Use an explicit newline-delimited file list
204
+ [--adapter=<name|path>] Select built-in or third-party runner adapter
205
+ [--diagnostics] Print full diagnostics for passing evals too
205
206
  [--quiet] Suppress the run-start summary
206
207
  [--verbose|-v] Stream live per-turn events to stderr during the run
207
208
  pathgrade init [--force] Generate eval scaffolding
@@ -223,9 +224,9 @@ function printHelp() {
223
224
  [--json] Emit structured JSON (snake_case) to stdout
224
225
 
225
226
  Environment:
226
- PATHGRADE_AGENT=codex Override agent for all trials
227
- ANTHROPIC_API_KEY API key for Claude
228
- OPENAI_API_KEY API key for Codex (optional if Codex CLI is already logged in)
227
+ PATHGRADE_AGENT=codex Fallback agent when createAgent({ agent }) is omitted
228
+ ANTHROPIC_API_KEY API key for Claude and Anthropic-backed judges
229
+ OPENAI_API_KEY API key for Codex and OpenAI-backed judges
229
230
 
230
231
  Examples:
231
232
  pathgrade run # run all *.eval.ts files
@@ -1,9 +1,2 @@
1
- import type { PathgradePluginOptions } from '../sdk/types.js';
2
- export type { PathgradePluginOptions };
3
- /**
4
- * Vitest plugin for pathgrade evaluations.
5
- *
6
- * Configures test file patterns, timeout, auto-dispose lifecycle hooks,
7
- * and the pathgrade reporter for aggregate statistics.
8
- */
9
- export declare function pathgrade(opts?: PathgradePluginOptions): any;
1
+ export { pathgrade } from '../adapters/vitest/index.js';
2
+ export type { PathgradePluginOptions } from '../adapters/vitest/index.js';