@blzsky/variant 0.2.1 → 0.3.0-canary.1

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 (58) hide show
  1. package/README.md +222 -85
  2. package/dist/{build-3DSBMMI3.js → build-GCUFPPIB.js} +11 -10
  3. package/dist/check-AKYABBIV.js +1459 -0
  4. package/dist/{chunk-43VLELTL.js → chunk-2UVLKBLV.js} +1 -1
  5. package/dist/chunk-34VGKPEM.js +59 -0
  6. package/dist/chunk-BNACT47L.js +590 -0
  7. package/dist/chunk-C3NZIFW6.js +174 -0
  8. package/dist/{chunk-ZH2PQUNL.js → chunk-EFCMBBBT.js} +72 -110
  9. package/dist/chunk-H55GYSQ6.js +102 -0
  10. package/dist/chunk-HL65IPEQ.js +167 -0
  11. package/dist/chunk-IHXJ72XA.js +31 -0
  12. package/dist/{chunk-FZEXZBXB.js → chunk-JEJRXPCU.js} +41 -9
  13. package/dist/chunk-LUTP2TVU.js +94 -0
  14. package/dist/{chunk-7OPTWU6E.js → chunk-MKUYZIXF.js} +1 -1
  15. package/dist/chunk-MNYKLWRC.js +377 -0
  16. package/dist/{chunk-Q7RLUZSN.js → chunk-NJSTAWDM.js} +10 -3
  17. package/dist/{chunk-26DHEXGS.js → chunk-NOC53Z5Q.js} +1 -1
  18. package/dist/chunk-O6FCFZDF.js +127 -0
  19. package/dist/{chunk-6BXSZPHC.js → chunk-OON6O4S5.js} +99 -6
  20. package/dist/{chunk-FA64KWJ2.js → chunk-TPMHY4L6.js} +16 -31
  21. package/dist/chunk-TRUK3TJF.js +575 -0
  22. package/dist/chunk-WTXWEEI2.js +21 -0
  23. package/dist/{chunk-RC23JQKI.js → chunk-WULBOSG5.js} +15 -2
  24. package/dist/chunk-X3VVGTEV.js +268 -0
  25. package/dist/{chunk-TCISS2AS.js → chunk-XXMD4NC5.js} +120 -25
  26. package/dist/cli.js +89 -30
  27. package/dist/{diff-HSJBZFMC.js → diff-ST5KHT7P.js} +5 -5
  28. package/dist/{doctor-B7MHFIYJ.js → doctor-OEIY4Y5X.js} +41 -8
  29. package/dist/{env-I7JZRWDN.js → env-EQAOZCFR.js} +8 -9
  30. package/dist/git-VGHUF2SY.js +30 -0
  31. package/dist/impact-AXLZTLVT.js +266 -0
  32. package/dist/index.js +2 -2
  33. package/dist/init-QCOZWLOO.js +793 -0
  34. package/dist/{insight-JQLTGFYB.js → insight-KRWT6Z47.js} +9 -10
  35. package/dist/pr-7JXUC5BQ.js +710 -0
  36. package/dist/{run-C6VAQOVM.js → run-UOPS5FRU.js} +11 -10
  37. package/dist/stryker-host.d.ts +2 -0
  38. package/dist/stryker-host.js +135 -0
  39. package/dist/stryker-plugins.d.ts +33 -0
  40. package/dist/stryker-plugins.js +142 -0
  41. package/dist/{symbol-graph-E5GXKDFZ.js → symbol-graph-GYOI4GU4.js} +3 -3
  42. package/dist/{tsconfig-paths-E3UEDGXZ.js → tsconfig-paths-SV3PULCW.js} +3 -1
  43. package/dist/vitest.cjs +629 -0
  44. package/dist/vitest.d.cts +181 -0
  45. package/dist/vitest.d.ts +181 -0
  46. package/dist/vitest.js +358 -0
  47. package/dist/workspace-IQFLR3RY.js +65 -0
  48. package/package.json +17 -3
  49. package/dist/check-SIQJU6Z2.js +0 -44
  50. package/dist/chunk-DRMASVYN.js +0 -25
  51. package/dist/chunk-KSHWC2XN.js +0 -78
  52. package/dist/chunk-QRPI6TJE.js +0 -68
  53. package/dist/chunk-VYWPQKMS.js +0 -88
  54. package/dist/chunk-XTYVVUWW.js +0 -279
  55. package/dist/impact-PRZ6HEP5.js +0 -735
  56. package/dist/init-O2BY3FOY.js +0 -158
  57. package/dist/pr-Y724QAHE.js +0 -165
  58. package/dist/workspace-HRTFGWKN.js +0 -186
@@ -1,13 +1,13 @@
1
1
  import {
2
2
  resolveCommit
3
- } from "./chunk-6BXSZPHC.js";
3
+ } from "./chunk-OON6O4S5.js";
4
4
  import {
5
5
  collectExportedSurface,
6
6
  significantText
7
- } from "./chunk-RC23JQKI.js";
7
+ } from "./chunk-WULBOSG5.js";
8
8
  import {
9
9
  GitRefError
10
- } from "./chunk-VYWPQKMS.js";
10
+ } from "./chunk-C3NZIFW6.js";
11
11
 
12
12
  // src/core/vcs/base-ref.ts
13
13
  async function assertBaseRef(cwd, ref) {
@@ -25,18 +25,22 @@ function createClassifier() {
25
25
  const tsm2 = await import("ts-morph");
26
26
  return {
27
27
  tsm: tsm2,
28
- project: new tsm2.Project({ useInMemoryFileSystem: true })
28
+ project: new tsm2.Project({
29
+ useInMemoryFileSystem: true,
30
+ compilerOptions: { allowJs: true }
31
+ })
29
32
  };
30
33
  })();
31
34
  const { tsm, project } = await host;
32
35
  const id = nextId++;
36
+ const ext = sourceExtension(input.filePath);
33
37
  const beforeFile = project.createSourceFile(
34
- `__before_${id}.ts`,
38
+ `__before_${id}${ext}`,
35
39
  input.before,
36
40
  { overwrite: true }
37
41
  );
38
42
  const afterFile = project.createSourceFile(
39
- `__after_${id}.ts`,
43
+ `__after_${id}${ext}`,
40
44
  input.after,
41
45
  { overwrite: true }
42
46
  );
@@ -76,15 +80,23 @@ function compareSurfaces(input, beforeFile, afterFile, tsm) {
76
80
  if (!afterApi.symbols.has(name)) removed.push(name);
77
81
  }
78
82
  const surfaceChanged = added.length > 0 || removed.length > 0 || changed.some((c) => c.kind !== "body");
83
+ const parseNotes = [
84
+ ...hasParseErrors(beforeFile) ? ["base"] : [],
85
+ ...hasParseErrors(afterFile) ? ["working-tree"] : []
86
+ ].map(
87
+ (side) => `the ${side} version does not parse; classified as breaking without trusting the comparison`
88
+ );
79
89
  let classification;
80
- if (surfaceChanged) {
90
+ if (surfaceChanged || parseNotes.length > 0) {
81
91
  classification = "breaking";
82
92
  } else if (changed.length > 0 || significantText(input.before, tsm) !== significantText(input.after, tsm)) {
83
93
  classification = "internal";
84
94
  } else {
85
95
  classification = "non-impacting";
86
96
  }
87
- const confidenceNotes = [.../* @__PURE__ */ new Set([...beforeApi.notes, ...afterApi.notes])];
97
+ const confidenceNotes = [
98
+ .../* @__PURE__ */ new Set([...parseNotes, ...beforeApi.notes, ...afterApi.notes])
99
+ ];
88
100
  const confidence = Math.max(
89
101
  0.3,
90
102
  Math.round((1 - confidenceNotes.length * 0.15) * 100) / 100
@@ -94,9 +106,29 @@ function compareSurfaces(input, beforeFile, afterFile, tsm) {
94
106
  classification,
95
107
  exportedSymbols: { added, removed, changed },
96
108
  confidence,
97
- confidenceNotes
109
+ confidenceNotes,
110
+ syntaxErrors: parseNotes.length > 0
98
111
  };
99
112
  }
113
+ var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([
114
+ ".ts",
115
+ ".tsx",
116
+ ".mts",
117
+ ".cts",
118
+ ".js",
119
+ ".jsx",
120
+ ".mjs",
121
+ ".cjs"
122
+ ]);
123
+ function sourceExtension(filePath) {
124
+ const dot = filePath.lastIndexOf(".");
125
+ const ext = dot === -1 ? "" : filePath.slice(dot);
126
+ return SOURCE_EXTENSIONS.has(ext) ? ext : ".ts";
127
+ }
128
+ function hasParseErrors(file) {
129
+ const { parseDiagnostics } = file.compilerNode;
130
+ return (parseDiagnostics?.length ?? 0) > 0;
131
+ }
100
132
 
101
133
  export {
102
134
  createClassifier,
@@ -0,0 +1,94 @@
1
+ import {
2
+ deriveVerdict,
3
+ traceTestImpact
4
+ } from "./chunk-BNACT47L.js";
5
+ import {
6
+ assertBaseRef
7
+ } from "./chunk-JEJRXPCU.js";
8
+ import {
9
+ loadPackageGraph
10
+ } from "./chunk-3E5LBU3O.js";
11
+ import {
12
+ appendImpactPrediction,
13
+ defaultHistoryDir
14
+ } from "./chunk-34VGKPEM.js";
15
+ import {
16
+ changeBaseDeps,
17
+ resolveChangeBase
18
+ } from "./chunk-O6FCFZDF.js";
19
+ import {
20
+ readHeadSha
21
+ } from "./chunk-OON6O4S5.js";
22
+ import {
23
+ GitRefError,
24
+ NoTestFilesError
25
+ } from "./chunk-C3NZIFW6.js";
26
+
27
+ // src/core/impact/predict.ts
28
+ var DEFAULT_IMPACT_BASE_REF = "HEAD~1";
29
+ async function analyzeImpact(cwd, options = {}) {
30
+ const base = options.changedFiles === void 0 ? await resolveChangeBase(
31
+ options.base,
32
+ options.changeBaseDeps ?? changeBaseDeps(cwd)
33
+ ) : {
34
+ ref: options.base ?? DEFAULT_IMPACT_BASE_REF,
35
+ source: options.base === void 0 ? "previous-commit" : "flag",
36
+ label: options.base ?? "previous commit"
37
+ };
38
+ const baseRef = base.ref;
39
+ if (options.changedFiles === void 0) await assertBaseRef(cwd, baseRef);
40
+ const packageGraph = await loadPackageGraph(cwd).catch(() => void 0);
41
+ const result = await traceTestImpact(cwd, {
42
+ baseRef,
43
+ ...packageGraph !== void 0 && { packageGraph },
44
+ ...options.changedFiles !== void 0 && {
45
+ changedFiles: options.changedFiles
46
+ },
47
+ ...options.readBefore !== void 0 && { readBefore: options.readBefore }
48
+ });
49
+ if (result === null) {
50
+ throw new GitRefError(
51
+ `Could not list the files changed since "${baseRef}"`
52
+ );
53
+ }
54
+ return { base, result, packageGraph };
55
+ }
56
+ async function predictImpact(cwd, options = {}) {
57
+ const { base, result, packageGraph } = await analyzeImpact(cwd, options);
58
+ const baseRef = base.ref;
59
+ if (result.tests.totalTests === 0) throw new NoTestFilesError();
60
+ const verdict = deriveVerdict(
61
+ result.radius.changed.map((change) => change.classification),
62
+ { forceBuild: result.tests.selectAll }
63
+ );
64
+ const headSha = options.headSha !== void 0 ? options.headSha : base.headSha ?? await readHeadSha(cwd);
65
+ const historyLogged = await appendImpactPrediction(defaultHistoryDir(cwd), {
66
+ at: (/* @__PURE__ */ new Date()).toISOString(),
67
+ baseRef,
68
+ headSha,
69
+ changedFiles: result.radius.changed.map((change) => change.filePath),
70
+ affectedFiles: result.radius.affectedFiles.length,
71
+ affectedPackages: result.radius.affectedPackages,
72
+ affectedTests: result.tests.affectedTests,
73
+ totalTests: result.tests.totalTests,
74
+ selectAll: result.tests.selectAll,
75
+ verdict,
76
+ confidence: result.tests.confidence,
77
+ notes: [...result.radius.notes, ...result.tests.notes]
78
+ });
79
+ return {
80
+ baseRef,
81
+ baseSource: base.source,
82
+ baseLabel: base.label,
83
+ headSha,
84
+ result,
85
+ verdict,
86
+ packagesFound: packageGraph?.packages.length ?? 0,
87
+ historyLogged
88
+ };
89
+ }
90
+
91
+ export {
92
+ analyzeImpact,
93
+ predictImpact
94
+ };
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-3E5LBU3O.js";
4
4
  import {
5
5
  listChangedFiles
6
- } from "./chunk-6BXSZPHC.js";
6
+ } from "./chunk-OON6O4S5.js";
7
7
 
8
8
  // src/core/cache/git-diff.ts
9
9
  import path from "path";
@@ -0,0 +1,377 @@
1
+ import {
2
+ loadPackageGraph
3
+ } from "./chunk-3E5LBU3O.js";
4
+ import {
5
+ defaultBranchRef
6
+ } from "./chunk-OON6O4S5.js";
7
+
8
+ // src/core/setup/discover.ts
9
+ import { access, readFile } from "fs/promises";
10
+ import path from "path";
11
+
12
+ // src/core/setup/workflow.ts
13
+ var TEST_COMMAND = /\b(?:vitest|jest)\b|\b(?:npm|pnpm|yarn|bun)(?:\s+run)?\s+test\b|\bturbo(?:\s+run)?\s+test\b|\bnx\s+(?:run-many|affected|test)\b/;
14
+ function runsTests(workflow) {
15
+ return workflow.split("\n").some((line) => !line.trim().startsWith("#") && TEST_COMMAND.test(line));
16
+ }
17
+ var CHECKOUT = /^(\s*)(-\s+)?uses:\s*actions\/checkout@\S+\s*$/;
18
+ function indentOf(line) {
19
+ return line.length - line.trimStart().length;
20
+ }
21
+ function withFullHistory(workflow) {
22
+ const lines = workflow.split("\n");
23
+ const out = [];
24
+ let changed = false;
25
+ for (let i = 0; i < lines.length; i++) {
26
+ const line = lines[i] ?? "";
27
+ out.push(line);
28
+ const match = CHECKOUT.exec(line);
29
+ if (match === null) continue;
30
+ const keyIndent = (match[1]?.length ?? 0) + (match[2]?.length ?? 0);
31
+ const pad = " ".repeat(keyIndent);
32
+ let end = i + 1;
33
+ while (end < lines.length) {
34
+ const next = lines[end] ?? "";
35
+ if (next.trim() !== "" && indentOf(next) < keyIndent) break;
36
+ if (next.trim().startsWith("- ") && indentOf(next) < keyIndent) break;
37
+ end++;
38
+ }
39
+ const step = lines.slice(i + 1, end);
40
+ const withAt = step.findIndex(
41
+ (l) => indentOf(l) === keyIndent && /^with:\s*$/.test(l.trim())
42
+ );
43
+ if (withAt === -1) {
44
+ out.push(`${pad}with:`, `${pad} fetch-depth: 0`);
45
+ changed = true;
46
+ continue;
47
+ }
48
+ const depthAt = step.findIndex(
49
+ (l, index) => index > withAt && indentOf(l) > keyIndent && /^fetch-depth:/.test(l.trim())
50
+ );
51
+ for (let j = 0; j < step.length; j++) {
52
+ const stepLine = step[j] ?? "";
53
+ if (j === depthAt) {
54
+ const fixed = stepLine.replace(/fetch-depth:.*$/, "fetch-depth: 0");
55
+ if (fixed !== stepLine) changed = true;
56
+ out.push(fixed);
57
+ continue;
58
+ }
59
+ out.push(stepLine);
60
+ if (j === withAt && depthAt === -1) {
61
+ out.push(`${pad} fetch-depth: 0`);
62
+ changed = true;
63
+ }
64
+ }
65
+ i = end - 1;
66
+ }
67
+ return changed ? out.join("\n") : null;
68
+ }
69
+ var ACTIONS = [
70
+ "actions/checkout",
71
+ "actions/setup-node",
72
+ "actions/github-script",
73
+ "pnpm/action-setup",
74
+ "oven-sh/setup-bun"
75
+ ];
76
+ function workflowStyle(workflow) {
77
+ const actions = {};
78
+ let node;
79
+ let bun;
80
+ for (const raw of workflow.split("\n")) {
81
+ const line = raw.trim();
82
+ if (line.startsWith("#")) continue;
83
+ const uses = /^(?:-\s+)?uses:\s*(\S+)@(\S+)/.exec(line);
84
+ if (uses !== null) {
85
+ const [, action, version] = uses;
86
+ if (action !== void 0 && ACTIONS.includes(action)) {
87
+ actions[action] ??= `${action}@${version}`;
88
+ }
89
+ continue;
90
+ }
91
+ if (line.includes("${{")) continue;
92
+ if (/^node-version(?:-file)?:\s*\S/.test(line)) node ??= line;
93
+ if (/^bun-version(?:-file)?:\s*\S/.test(line)) bun ??= line;
94
+ }
95
+ return {
96
+ actions,
97
+ ...node !== void 0 && { node },
98
+ ...bun !== void 0 && { bun }
99
+ };
100
+ }
101
+ var DEFAULT_ACTIONS = {
102
+ "actions/checkout": "actions/checkout@v7",
103
+ "actions/setup-node": "actions/setup-node@v7",
104
+ "actions/github-script": "actions/github-script@v9",
105
+ "pnpm/action-setup": "pnpm/action-setup@v4",
106
+ "oven-sh/setup-bun": "oven-sh/setup-bun@v2"
107
+ };
108
+ var INSTALL = {
109
+ npm: "npm ci",
110
+ pnpm: "pnpm install --frozen-lockfile",
111
+ yarn: "yarn install --frozen-lockfile",
112
+ bun: "bun install --frozen-lockfile"
113
+ };
114
+ var EXEC = {
115
+ npm: "npx --no -- variant",
116
+ pnpm: "pnpm exec variant",
117
+ yarn: "yarn variant",
118
+ bun: "bun run variant"
119
+ };
120
+ var NODE_CACHE = {
121
+ npm: "npm",
122
+ pnpm: "pnpm",
123
+ yarn: "yarn"
124
+ };
125
+ function pullRequestWorkflow(packageManager, style = { actions: {} }) {
126
+ const cache = NODE_CACHE[packageManager];
127
+ const action = (name) => style.actions[name] ?? DEFAULT_ACTIONS[name] ?? name;
128
+ const setup = packageManager === "pnpm" ? [` - uses: ${action("pnpm/action-setup")}`, ""] : packageManager === "bun" ? [
129
+ ` - uses: ${action("oven-sh/setup-bun")}`,
130
+ ...style.bun === void 0 ? [] : [" with:", ` ${style.bun}`],
131
+ ""
132
+ ] : [];
133
+ return [
134
+ "# Written by `variant init`. It keeps one comment on each pull request",
135
+ "# saying what the change affects. It is yours to edit.",
136
+ "",
137
+ "name: variant",
138
+ "",
139
+ "on:",
140
+ " pull_request:",
141
+ "",
142
+ "permissions:",
143
+ " contents: read",
144
+ " pull-requests: write",
145
+ "",
146
+ "# A newer push makes the report for an older one pointless.",
147
+ "concurrency:",
148
+ // Split so the linter does not take a workflow expression for a
149
+ // template placeholder someone forgot the backticks on.
150
+ ` group: variant-$${"{{ github.event.pull_request.number }}"}`,
151
+ " cancel-in-progress: true",
152
+ "",
153
+ "jobs:",
154
+ " report:",
155
+ " runs-on: ubuntu-latest",
156
+ " timeout-minutes: 10",
157
+ " steps:",
158
+ ` - uses: ${action("actions/checkout")}`,
159
+ " with:",
160
+ " # variant compares against the commit the branch started from.",
161
+ " fetch-depth: 0",
162
+ "",
163
+ ...setup,
164
+ ` - uses: ${action("actions/setup-node")}`,
165
+ " with:",
166
+ ` ${style.node ?? "node-version: 22"}`,
167
+ ...cache === void 0 ? [] : [` cache: ${cache}`],
168
+ "",
169
+ ` - run: ${INSTALL[packageManager]}`,
170
+ "",
171
+ " - name: Describe the pull request",
172
+ ` run: ${EXEC[packageManager]} pr report --markdown --output pr-report.md`,
173
+ "",
174
+ " - name: Post or update the comment",
175
+ " # Pull requests from forks get a read-only token and cannot comment.",
176
+ " if: github.event.pull_request.head.repo.full_name == github.repository",
177
+ ` uses: ${action("actions/github-script")}`,
178
+ " with:",
179
+ " script: |",
180
+ ' const fs = require("node:fs");',
181
+ ' const body = fs.readFileSync("pr-report.md", "utf8");',
182
+ " // The first line of every report; it marks the comment to update.",
183
+ ' const marker = "## Variant PR Report";',
184
+ " const { owner, repo } = context.repo;",
185
+ " const issue_number = context.issue.number;",
186
+ " const comments = await github.paginate(github.rest.issues.listComments, {",
187
+ " owner,",
188
+ " repo,",
189
+ " issue_number,",
190
+ " });",
191
+ " const existing = comments.find(",
192
+ " (comment) =>",
193
+ ' comment.user?.login === "github-actions[bot]" &&',
194
+ " comment.body?.startsWith(marker),",
195
+ " );",
196
+ " if (existing) {",
197
+ " await github.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body });",
198
+ " } else {",
199
+ " await github.rest.issues.createComment({ owner, repo, issue_number, body });",
200
+ " }",
201
+ ""
202
+ ].join("\n");
203
+ }
204
+
205
+ // src/core/setup/discover.ts
206
+ var LOCKFILES = [
207
+ ["bun.lock", "bun"],
208
+ ["bun.lockb", "bun"],
209
+ ["pnpm-lock.yaml", "pnpm"],
210
+ ["yarn.lock", "yarn"],
211
+ ["package-lock.json", "npm"],
212
+ ["npm-shrinkwrap.json", "npm"]
213
+ ];
214
+ var IGNORE = ["**/node_modules/**", "**/dist/**", ".git/**", ".variant/**"];
215
+ async function readText(file) {
216
+ try {
217
+ return await readFile(file, "utf8");
218
+ } catch {
219
+ return null;
220
+ }
221
+ }
222
+ async function readManifest(cwd) {
223
+ const raw = await readText(path.join(cwd, "package.json"));
224
+ if (raw === null) return null;
225
+ try {
226
+ const parsed = JSON.parse(raw);
227
+ return typeof parsed === "object" && parsed !== null ? parsed : null;
228
+ } catch {
229
+ return null;
230
+ }
231
+ }
232
+ var MANAGERS = /* @__PURE__ */ new Set(["npm", "pnpm", "yarn", "bun"]);
233
+ async function managerIn(dir) {
234
+ for (const [lockfile, name2] of LOCKFILES) {
235
+ if (await readText(path.join(dir, lockfile)) !== null) return name2;
236
+ }
237
+ const declared = (await readManifest(dir))?.packageManager;
238
+ const name = typeof declared === "string" ? declared.split("@")[0] : void 0;
239
+ return name !== void 0 && MANAGERS.has(name) ? name : null;
240
+ }
241
+ async function detectPackageManager(cwd) {
242
+ let dir = path.resolve(cwd);
243
+ for (; ; ) {
244
+ const found = await managerIn(dir);
245
+ if (found !== null) return found;
246
+ const parent = path.dirname(dir);
247
+ const atRoot = await access(path.join(dir, ".git")).then(
248
+ () => true,
249
+ () => false
250
+ );
251
+ if (parent === dir || atRoot) return null;
252
+ dir = parent;
253
+ }
254
+ }
255
+ async function packageManagerOf(cwd) {
256
+ return detectPackageManager(cwd);
257
+ }
258
+ var CONFIG_FLAG = /(?:--config|-c)(?:=|\s+)(\S+)/;
259
+ function vitestRuns(manifest) {
260
+ const configs = [];
261
+ let plain = false;
262
+ for (const script of Object.values(manifest?.scripts ?? {})) {
263
+ if (typeof script !== "string") continue;
264
+ for (const command of script.split(/&&|\|\||;/)) {
265
+ if (!/\bvitest\b/.test(command)) continue;
266
+ const config = CONFIG_FLAG.exec(command)?.[1];
267
+ if (config === void 0) plain = true;
268
+ else configs.push(config.replace(/^["']|["']$/g, ""));
269
+ }
270
+ }
271
+ return { plain, configs };
272
+ }
273
+ function adapterTargets(configs, packageDirs) {
274
+ const root = configs.filter((file) => !file.includes("/"));
275
+ if (root.length > 0) return root;
276
+ const dirs = new Set(packageDirs);
277
+ return configs.filter((file) => dirs.has(path.posix.dirname(file)));
278
+ }
279
+ async function discoverRepository(cwd) {
280
+ const fg = (await import("fast-glob")).default;
281
+ const [manifest, packageGraph, found, gitignore, branch] = await Promise.all([
282
+ readManifest(cwd),
283
+ loadPackageGraph(cwd).catch(() => null),
284
+ fg(
285
+ [
286
+ "**/vitest.config.{ts,mts,cts,js,mjs,cjs}",
287
+ "**/vite.config.{ts,mts,cts,js,mjs,cjs}",
288
+ "**/jest.config.{ts,mts,cts,js,mjs,cjs,json}",
289
+ "**/playwright.config.{ts,mts,cts,js,mjs,cjs}",
290
+ ".github/workflows/*.{yml,yaml}",
291
+ "turbo.json",
292
+ "nx.json"
293
+ ],
294
+ { cwd, onlyFiles: true, dot: true, ignore: IGNORE }
295
+ ),
296
+ readText(path.join(cwd, ".gitignore")),
297
+ defaultBranchRef(cwd)
298
+ ]);
299
+ const files = found.map((file) => file.replace(/\\/g, "/")).sort();
300
+ const named = (prefix) => files.filter((file) => path.posix.basename(file).startsWith(prefix));
301
+ const packageDirs = (packageGraph?.packages ?? []).map(
302
+ (pkg) => path.relative(cwd, pkg.dir).replace(/\\/g, "/")
303
+ );
304
+ const workflows = await Promise.all(
305
+ files.filter((file) => file.startsWith(".github/workflows/")).map(async (file) => {
306
+ const content = await readText(path.join(cwd, file)) ?? "";
307
+ return {
308
+ file,
309
+ content,
310
+ runsTests: runsTests(content),
311
+ shallow: withFullHistory(content) !== null
312
+ };
313
+ })
314
+ );
315
+ const vitestConfigs = adapterTargets(named("vitest.config."), packageDirs);
316
+ const hasRootConfig = vitestConfigs.some((file) => !file.includes("/"));
317
+ const configDirs = (prefix) => new Map(
318
+ named(prefix).map((file) => {
319
+ const dir = path.posix.dirname(file);
320
+ return [dir === "." ? "" : dir, file];
321
+ })
322
+ );
323
+ const vitestDirs = configDirs("vitest.config.");
324
+ const viteDirs = configDirs("vite.config.");
325
+ const members = (packageGraph?.packages ?? []).map((pkg) => ({
326
+ dir: path.relative(cwd, pkg.dir).replace(/\\/g, "/"),
327
+ manifest: pkg.manifest
328
+ })).filter((member) => member.dir !== "");
329
+ const usesVitest = (candidate) => "vitest" in (candidate?.devDependencies ?? {}) || "vitest" in (candidate?.dependencies ?? {});
330
+ const everyone = [{ dir: "", manifest }, ...members];
331
+ const runs = new Map(
332
+ everyone.map((member) => [member.dir, vitestRuns(member.manifest)])
333
+ );
334
+ const scripted = everyone.some((member) => {
335
+ const run = runs.get(member.dir);
336
+ return run !== void 0 && (run.plain || run.configs.length > 0);
337
+ });
338
+ const users = members.filter((member) => usesVitest(member.manifest));
339
+ const runners = scripted ? everyone.filter((member) => runs.get(member.dir)?.plain === true) : users.length > 0 ? users : usesVitest(manifest) ? [{ dir: "", manifest }] : [];
340
+ const unconfigured = hasRootConfig ? [] : runners.filter((runner) => !vitestDirs.has(runner.dir));
341
+ const scriptConfigs = everyone.flatMap(
342
+ (member) => (runs.get(member.dir)?.configs ?? []).map(
343
+ (config) => path.posix.normalize(path.posix.join(member.dir, config))
344
+ )
345
+ );
346
+ const allConfigs = [.../* @__PURE__ */ new Set([...vitestConfigs, ...scriptConfigs])].sort();
347
+ return {
348
+ packageManager: await detectPackageManager(cwd),
349
+ workspacePackages: packageDirs.filter((dir) => dir !== "").length,
350
+ taskRunner: files.includes("turbo.json") ? "turborepo" : files.includes("nx.json") ? "nx" : null,
351
+ vitestConfigs: allConfigs,
352
+ vitestWithoutConfig: unconfigured.filter((runner) => !viteDirs.has(runner.dir)).map((runner) => runner.dir).sort(),
353
+ viteConfigs: unconfigured.flatMap((runner) => viteDirs.get(runner.dir) ?? []).sort(),
354
+ reporterFlags: [{ dir: "", manifest }, ...members].filter(
355
+ (member) => Object.values(member.manifest?.scripts ?? {}).some(
356
+ (script) => typeof script === "string" && /\bvitest\b.*--reporter\b/.test(script)
357
+ )
358
+ ).map(
359
+ (member) => member.dir === "" ? "package.json" : `${member.dir}/package.json`
360
+ ).sort(),
361
+ jestConfigs: named("jest.config."),
362
+ playwrightConfigs: named("playwright.config."),
363
+ workflows,
364
+ defaultBranch: branch?.replace(/^origin\//, "") ?? null,
365
+ installed: "@blzsky/variant" in (manifest?.devDependencies ?? {}) || "@blzsky/variant" in (manifest?.dependencies ?? {}),
366
+ ignoresVariantDir: (gitignore ?? "").split("\n").some((line) => /^\/?\.variant\/?$/.test(line.trim())),
367
+ hasManifest: manifest !== null
368
+ };
369
+ }
370
+
371
+ export {
372
+ withFullHistory,
373
+ workflowStyle,
374
+ pullRequestWorkflow,
375
+ packageManagerOf,
376
+ discoverRepository
377
+ };
@@ -1,16 +1,23 @@
1
1
  import {
2
2
  classifyChange
3
- } from "./chunk-FZEXZBXB.js";
3
+ } from "./chunk-JEJRXPCU.js";
4
4
  import {
5
5
  readFileAtRef
6
- } from "./chunk-6BXSZPHC.js";
6
+ } from "./chunk-OON6O4S5.js";
7
+ import {
8
+ CliUsageError
9
+ } from "./chunk-C3NZIFW6.js";
7
10
 
8
11
  // src/core/semantic/file-change.ts
9
12
  import { readFile } from "fs/promises";
10
13
  import path from "path";
11
14
  var DEFAULT_DIFF_BASE_REF = "HEAD~1";
12
15
  function toWorkspaceRelative(cwd, filePath) {
13
- return path.relative(cwd, path.resolve(cwd, filePath)).replace(/\\/g, "/");
16
+ const relative = path.relative(cwd, path.resolve(cwd, filePath));
17
+ if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
18
+ throw new CliUsageError(`${filePath} is outside the repository at ${cwd}`);
19
+ }
20
+ return relative.replace(/\\/g, "/");
14
21
  }
15
22
  async function classifyFileAgainstRef(cwd, relPath, baseRef) {
16
23
  const [before, after] = await Promise.all([
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  ConfigError
3
- } from "./chunk-VYWPQKMS.js";
3
+ } from "./chunk-C3NZIFW6.js";
4
4
 
5
5
  // src/core/config/loader.ts
6
6
  import { existsSync } from "fs";