@blzsky/variant 0.2.1 → 0.3.0-canary.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 (47) hide show
  1. package/README.md +110 -56
  2. package/dist/{build-3DSBMMI3.js → build-GKWNLU4Q.js} +9 -8
  3. package/dist/{check-SIQJU6Z2.js → check-KO5VY67Z.js} +6 -6
  4. package/dist/{chunk-FA64KWJ2.js → chunk-7NC7AO75.js} +15 -28
  5. package/dist/{chunk-VYWPQKMS.js → chunk-ACTY42RE.js} +20 -0
  6. package/dist/{chunk-ZH2PQUNL.js → chunk-CN25SRKD.js} +6 -6
  7. package/dist/{chunk-43VLELTL.js → chunk-CWF6QXEW.js} +1 -1
  8. package/dist/{chunk-7OPTWU6E.js → chunk-DMDAJPFJ.js} +1 -1
  9. package/dist/{chunk-RC23JQKI.js → chunk-DSQDMFQ6.js} +9 -1
  10. package/dist/chunk-H5C4SE73.js +184 -0
  11. package/dist/chunk-IFFZ46YS.js +575 -0
  12. package/dist/chunk-IHXJ72XA.js +31 -0
  13. package/dist/chunk-KCEGA56S.js +670 -0
  14. package/dist/{chunk-FZEXZBXB.js → chunk-MAK3HG46.js} +41 -9
  15. package/dist/chunk-MT4BOAAV.js +167 -0
  16. package/dist/{chunk-TCISS2AS.js → chunk-NFFXYYM6.js} +120 -25
  17. package/dist/{chunk-26DHEXGS.js → chunk-RJBFCURK.js} +1 -1
  18. package/dist/{chunk-6BXSZPHC.js → chunk-RSIOYQOQ.js} +67 -8
  19. package/dist/chunk-SGP5JX6I.js +377 -0
  20. package/dist/chunk-X3VVGTEV.js +268 -0
  21. package/dist/{chunk-Q7RLUZSN.js → chunk-YII4TAKS.js} +10 -3
  22. package/dist/cli.js +62 -27
  23. package/dist/{diff-HSJBZFMC.js → diff-A3TQWWUJ.js} +5 -5
  24. package/dist/{doctor-B7MHFIYJ.js → doctor-6ONS6LKY.js} +4 -4
  25. package/dist/{env-I7JZRWDN.js → env-XF2US3CB.js} +6 -6
  26. package/dist/git-W6N3WMYQ.js +24 -0
  27. package/dist/impact-DPZIAJR4.js +263 -0
  28. package/dist/index.js +2 -2
  29. package/dist/init-VNK5E4H3.js +777 -0
  30. package/dist/{insight-JQLTGFYB.js → insight-LNYVMCAE.js} +7 -7
  31. package/dist/pr-6CYMM2XU.js +707 -0
  32. package/dist/{run-C6VAQOVM.js → run-44PKMOV5.js} +9 -8
  33. package/dist/{symbol-graph-E5GXKDFZ.js → symbol-graph-OB4AJCLW.js} +3 -3
  34. package/dist/{tsconfig-paths-E3UEDGXZ.js → tsconfig-paths-SV3PULCW.js} +3 -1
  35. package/dist/vitest.cjs +629 -0
  36. package/dist/vitest.d.cts +181 -0
  37. package/dist/vitest.d.ts +181 -0
  38. package/dist/vitest.js +356 -0
  39. package/dist/workspace-PIQXG4ZD.js +65 -0
  40. package/package.json +13 -2
  41. package/dist/chunk-DRMASVYN.js +0 -25
  42. package/dist/chunk-KSHWC2XN.js +0 -78
  43. package/dist/chunk-XTYVVUWW.js +0 -279
  44. package/dist/impact-PRZ6HEP5.js +0 -735
  45. package/dist/init-O2BY3FOY.js +0 -158
  46. package/dist/pr-Y724QAHE.js +0 -165
  47. package/dist/workspace-HRTFGWKN.js +0 -186
@@ -0,0 +1,777 @@
1
+ import {
2
+ discoverRepository,
3
+ pullRequestWorkflow,
4
+ withFullHistory,
5
+ workflowStyle
6
+ } from "./chunk-SGP5JX6I.js";
7
+ import {
8
+ CLEAR_LINE,
9
+ HIDE_CURSOR,
10
+ SHOW_CURSOR
11
+ } from "./chunk-IHXJ72XA.js";
12
+ import {
13
+ detectProject
14
+ } from "./chunk-V5CTGXMM.js";
15
+ import {
16
+ findVariantConfigPath
17
+ } from "./chunk-RJBFCURK.js";
18
+ import {
19
+ plural
20
+ } from "./chunk-CXNXZLDG.js";
21
+ import "./chunk-3E5LBU3O.js";
22
+ import {
23
+ getColors
24
+ } from "./chunk-YYGCSKIQ.js";
25
+ import {
26
+ lines
27
+ } from "./chunk-6J4XDG44.js";
28
+ import {
29
+ getPrinter
30
+ } from "./chunk-EISTWXDW.js";
31
+ import "./chunk-RSIOYQOQ.js";
32
+ import "./chunk-ACTY42RE.js";
33
+
34
+ // src/cli/commands/init.ts
35
+ import { writeFile as writeFile2 } from "fs/promises";
36
+ import path4 from "path";
37
+ import { createInterface as createInterface2 } from "readline/promises";
38
+
39
+ // src/core/scaffold/config-template.ts
40
+ import { readFile } from "fs/promises";
41
+ import path from "path";
42
+ var DEFAULT_TASK_INPUTS = {
43
+ build: ["src/**/*", "package.json"],
44
+ lint: ["src/**/*", "*.config.*"],
45
+ test: ["src/**/*", "**/__tests__/**/*"],
46
+ typecheck: ["src/**/*", "tsconfig*.json"],
47
+ format: ["src/**/*"]
48
+ };
49
+ var TASK_ORDER = ["build", "lint", "test", "typecheck", "format"];
50
+ var FALLBACK_TASKS = ["build", "lint", "test"];
51
+ function inputsFor(task) {
52
+ return DEFAULT_TASK_INPUTS[task] ?? DEFAULT_TASK_INPUTS["build"] ?? [];
53
+ }
54
+ function runPrefix(pm) {
55
+ return pm === "pnpm" || pm === "yarn" ? pm : "npm run";
56
+ }
57
+ function binaryRunner(pm) {
58
+ if (pm === "pnpm") return "pnpm";
59
+ if (pm === "yarn") return "yarn";
60
+ return "npx";
61
+ }
62
+ function defaultCommandFor(pm, task, framework) {
63
+ if (task === "build" && framework === "next") {
64
+ return `${binaryRunner(pm)} next build`;
65
+ }
66
+ if (task === "build" && framework === "vite") {
67
+ return `${binaryRunner(pm)} vite build`;
68
+ }
69
+ return `${runPrefix(pm)} ${task}`;
70
+ }
71
+ async function detectScriptTasks(cwd) {
72
+ try {
73
+ const manifest = JSON.parse(
74
+ await readFile(path.join(cwd, "package.json"), "utf8")
75
+ );
76
+ const present = new Set(Object.keys(manifest.scripts ?? {}));
77
+ const ordered = TASK_ORDER.filter((task) => present.has(task));
78
+ return ordered.length > 0 ? ordered : [...FALLBACK_TASKS];
79
+ } catch {
80
+ return [...FALLBACK_TASKS];
81
+ }
82
+ }
83
+ function renderConfigTemplate(tasks) {
84
+ const taskLines = tasks.map(
85
+ ({ name, command, inputs }) => ` ${name}: {
86
+ command: "${command}",
87
+ inputs: [${inputs.map((i) => `"${i}"`).join(", ")}],
88
+ }`
89
+ ).join(",\n");
90
+ return `import { defineConfig } from "@blzsky/variant";
91
+
92
+ export default defineConfig({
93
+ strategy: "adaptive",
94
+ tasks: {
95
+ ${taskLines},
96
+ },
97
+ });
98
+ `;
99
+ }
100
+
101
+ // src/core/setup/apply.ts
102
+ import { mkdir, writeFile } from "fs/promises";
103
+ import path2 from "path";
104
+ async function runCommand(command, cwd) {
105
+ const [file, ...args] = command;
106
+ if (file === void 0) return;
107
+ const { execa } = await import("execa");
108
+ await execa(file, args, { cwd, stdio: "inherit" });
109
+ }
110
+ async function applySetup(cwd, actions, deps = { run: runCommand }) {
111
+ const result = { written: [], installed: false };
112
+ for (const action of actions) {
113
+ if (action.kind !== "write") continue;
114
+ const target = path2.join(cwd, action.file);
115
+ await mkdir(path2.dirname(target), { recursive: true });
116
+ await writeFile(target, action.after);
117
+ result.written.push(action.file);
118
+ }
119
+ for (const action of actions) {
120
+ if (action.kind !== "install") continue;
121
+ try {
122
+ await deps.run(action.command, cwd);
123
+ result.installed = true;
124
+ } catch (error) {
125
+ result.installError = (error instanceof Error ? error.message : String(error)).split(
126
+ "\n"
127
+ )[0] ?? "the install command failed";
128
+ }
129
+ }
130
+ return result;
131
+ }
132
+
133
+ // src/core/setup/plan.ts
134
+ import { readFile as readFile2 } from "fs/promises";
135
+ import path3 from "path";
136
+
137
+ // src/core/setup/vitest-config.ts
138
+ var ADAPTER_MODULE = "@blzsky/variant/vitest";
139
+ var WRAPPERS = /* @__PURE__ */ new Set(["defineConfig", "defineProject", "mergeConfig"]);
140
+ function indentationOf(source) {
141
+ const indented = /^([ \t]+)\S/m.exec(source);
142
+ if (indented === null) return " ";
143
+ return indented[1]?.startsWith(" ") ? " " : indented[1] ?? " ";
144
+ }
145
+ function configObject(node, tsm, hops = 0) {
146
+ if (node === void 0 || hops > 4) return void 0;
147
+ const { Node: N } = tsm;
148
+ if (N.isObjectLiteralExpression(node)) return node;
149
+ if (N.isParenthesizedExpression(node) || N.isAsExpression(node)) {
150
+ return configObject(node.getExpression(), tsm, hops + 1);
151
+ }
152
+ if (N.isSatisfiesExpression(node)) {
153
+ return configObject(node.getExpression(), tsm, hops + 1);
154
+ }
155
+ if (N.isCallExpression(node)) {
156
+ if (!WRAPPERS.has(node.getExpression().getText())) return void 0;
157
+ return configObject(node.getArguments().at(-1), tsm, hops + 1);
158
+ }
159
+ if (N.isArrowFunction(node)) {
160
+ const body = node.getBody();
161
+ return N.isBlock(body) ? void 0 : configObject(body, tsm, hops + 1);
162
+ }
163
+ if (N.isIdentifier(node)) {
164
+ const declaration = node.getSourceFile().getVariableDeclaration(node.getText());
165
+ return configObject(declaration?.getInitializer(), tsm, hops + 1);
166
+ }
167
+ return void 0;
168
+ }
169
+ function objectProperty(object, name, tsm) {
170
+ const property = object.getProperty(name);
171
+ if (property === void 0) return void 0;
172
+ return tsm.Node.isPropertyAssignment(property) ? property.getInitializer() : "unreadable";
173
+ }
174
+ function addImport(file, name, isDefault) {
175
+ file.addImportDeclaration({
176
+ moduleSpecifier: ADAPTER_MODULE,
177
+ ...isDefault ? { defaultImport: name } : { namedImports: [name] }
178
+ });
179
+ }
180
+ var WITH_DEFAULTS = "variantReporters";
181
+ function newConfig() {
182
+ return `import { ${WITH_DEFAULTS} } from "${ADAPTER_MODULE}";
183
+
184
+ export default {
185
+ test: {
186
+ reporters: ${WITH_DEFAULTS}(),
187
+ },
188
+ };
189
+ `;
190
+ }
191
+ async function withAdapter(source, fileName) {
192
+ if (source.includes(ADAPTER_MODULE)) return { kind: "present" };
193
+ if (/\.cjs$/.test(fileName) || /\bmodule\.exports\b/.test(source)) {
194
+ return { kind: "manual", reason: "it is a CommonJS config" };
195
+ }
196
+ const tsm = await import("ts-morph");
197
+ const project = new tsm.Project({
198
+ useInMemoryFileSystem: true,
199
+ manipulationSettings: {
200
+ indentationText: indentationOf(source),
201
+ quoteKind: /from\s+'/.test(source) ? tsm.QuoteKind.Single : tsm.QuoteKind.Double,
202
+ useTrailingCommas: true
203
+ }
204
+ });
205
+ const file = project.createSourceFile(
206
+ `config${extensionOf(fileName)}`,
207
+ source
208
+ );
209
+ const exported = file.getExportAssignment((assignment) => !assignment.isExportEquals())?.getExpression();
210
+ const config = configObject(exported, tsm);
211
+ if (config === void 0) {
212
+ return {
213
+ kind: "manual",
214
+ reason: "its default export is not a plain config object"
215
+ };
216
+ }
217
+ const clash = file.getLocals().some((local) => local.getName() === "variant");
218
+ const name = clash ? "variantReporter" : "variant";
219
+ const test = objectProperty(config, "test", tsm);
220
+ if (test === "unreadable") {
221
+ return {
222
+ kind: "manual",
223
+ reason: "its `test` option is not written inline"
224
+ };
225
+ }
226
+ if (test === void 0) {
227
+ config.addPropertyAssignment({
228
+ name: "test",
229
+ initializer: (writer) => writer.block(() => writer.write(`reporters: ${WITH_DEFAULTS}(),`))
230
+ });
231
+ addImport(file, WITH_DEFAULTS, false);
232
+ return { kind: "edited", text: file.getFullText() };
233
+ }
234
+ if (!tsm.Node.isObjectLiteralExpression(test)) {
235
+ return {
236
+ kind: "manual",
237
+ reason: "its `test` option is not an object literal"
238
+ };
239
+ }
240
+ const existing = objectProperty(test, "reporters", tsm);
241
+ if (existing === "unreadable") {
242
+ return {
243
+ kind: "manual",
244
+ reason: "its `reporters` are not written inline"
245
+ };
246
+ }
247
+ if (existing === void 0) {
248
+ test.insertPropertyAssignment(0, {
249
+ name: "reporters",
250
+ initializer: `${WITH_DEFAULTS}()`
251
+ });
252
+ addImport(file, WITH_DEFAULTS, false);
253
+ return { kind: "edited", text: file.getFullText() };
254
+ }
255
+ if (!tsm.Node.isArrayLiteralExpression(existing)) {
256
+ return {
257
+ kind: "manual",
258
+ reason: "its `reporters` option is not an array literal"
259
+ };
260
+ }
261
+ existing.addElement(`${name}()`);
262
+ addImport(file, name, !clash);
263
+ return { kind: "edited", text: file.getFullText() };
264
+ }
265
+ function extensionOf(fileName) {
266
+ return /\.[cm]?[jt]s$/.exec(fileName)?.[0] ?? ".ts";
267
+ }
268
+ function manualInstructions(file) {
269
+ return [
270
+ `Add the adapter to ${file} yourself:`,
271
+ ` import { ${WITH_DEFAULTS} } from "${ADAPTER_MODULE}";`,
272
+ ` test: { reporters: ${WITH_DEFAULTS}() }`,
273
+ "If the config already lists reporters, import the default export as `variant` and add `variant()` to that list instead."
274
+ ];
275
+ }
276
+
277
+ // src/core/setup/plan.ts
278
+ var PACKAGE_NAME = "@blzsky/variant";
279
+ var NEW_CONFIG = "vitest.config.mts";
280
+ var PULL_REQUEST_WORKFLOW = ".github/workflows/variant.yml";
281
+ var ADD_DEV = {
282
+ npm: ["npm", "install", "--save-dev"],
283
+ pnpm: ["pnpm", "add", "--save-dev"],
284
+ yarn: ["yarn", "add", "--dev"],
285
+ bun: ["bun", "add", "--dev"]
286
+ };
287
+ var WORKSPACE_ROOT_FLAG = {
288
+ pnpm: "--workspace-root",
289
+ yarn: "--ignore-workspace-root-check"
290
+ };
291
+ function installCommand(facts, packageSpec = PACKAGE_NAME) {
292
+ const packageManager = facts.packageManager ?? "npm";
293
+ const rootFlag = facts.workspacePackages > 0 ? WORKSPACE_ROOT_FLAG[packageManager] : void 0;
294
+ return [
295
+ ...ADD_DEV[packageManager],
296
+ ...rootFlag === void 0 ? [] : [rootFlag],
297
+ packageSpec
298
+ ];
299
+ }
300
+ function withIgnoreEntry(gitignore) {
301
+ if (gitignore === null || gitignore === "") return ".variant/\n";
302
+ return `${gitignore}${gitignore.endsWith("\n") ? "" : "\n"}.variant/
303
+ `;
304
+ }
305
+ async function readText(file) {
306
+ try {
307
+ return await readFile2(file, "utf8");
308
+ } catch {
309
+ return null;
310
+ }
311
+ }
312
+ async function planSetup(cwd, facts, options = {}) {
313
+ const actions = [];
314
+ if (!facts.hasManifest) {
315
+ return [
316
+ {
317
+ kind: "note",
318
+ lines: [
319
+ "No package.json here. Run `variant init` in the directory that has one."
320
+ ]
321
+ }
322
+ ];
323
+ }
324
+ const observed = facts.vitestConfigs.length > 0 || facts.vitestWithoutConfig.length > 0;
325
+ if (!facts.installed && options.install !== false) {
326
+ actions.push({
327
+ kind: "install",
328
+ command: installCommand(facts, options.packageSpec),
329
+ reason: observed ? "the adapter is imported from your test config" : "the pull-request workflow runs it"
330
+ });
331
+ } else if (!facts.installed) {
332
+ actions.push({
333
+ kind: "note",
334
+ lines: [
335
+ `${PACKAGE_NAME} is not installed. The adapter needs it: ${installCommand(facts, options.packageSpec).join(" ")}`
336
+ ]
337
+ });
338
+ }
339
+ for (const file of facts.vitestConfigs) {
340
+ const before = await readText(path3.join(cwd, file));
341
+ if (before === null) continue;
342
+ const edit = await withAdapter(before, file);
343
+ if (edit.kind === "edited") {
344
+ actions.push({
345
+ kind: "write",
346
+ file,
347
+ before,
348
+ after: edit.text,
349
+ reason: "predict and check on every CI test run"
350
+ });
351
+ } else if (edit.kind === "manual") {
352
+ actions.push({
353
+ kind: "note",
354
+ lines: [
355
+ `${file} was left alone: ${edit.reason}.`,
356
+ ...manualInstructions(file)
357
+ ]
358
+ });
359
+ }
360
+ }
361
+ for (const dir of facts.vitestWithoutConfig) {
362
+ const file = dir === "" ? NEW_CONFIG : `${dir}/${NEW_CONFIG}`;
363
+ actions.push({
364
+ kind: "write",
365
+ file,
366
+ before: null,
367
+ after: newConfig(),
368
+ reason: "Vitest runs here without a config, and the adapter needs one to be in; this sets nothing else"
369
+ });
370
+ }
371
+ for (const file of facts.viteConfigs) {
372
+ actions.push({
373
+ kind: "note",
374
+ gap: true,
375
+ lines: [
376
+ `${file} was left alone: Vitest reads its settings from it, and a separate Vitest config would replace it.`,
377
+ ...manualInstructions(file)
378
+ ]
379
+ });
380
+ }
381
+ for (const file of facts.reporterFlags) {
382
+ actions.push({
383
+ kind: "note",
384
+ lines: [
385
+ `A script in ${file} passes --reporter to Vitest. That replaces the reporters in the config, the adapter with them: remove the flag, or the adapter will not load.`
386
+ ]
387
+ });
388
+ }
389
+ if (observed && facts.taskRunner === "turborepo") {
390
+ actions.push({
391
+ kind: "note",
392
+ lines: [
393
+ "Turborepo passes a task only the environment variables it is told about.",
394
+ 'To switch the adapter with VARIANT_SHADOW, or point it with VARIANT_BASE, add "VARIANT_*" to `globalPassThroughEnv` in turbo.json.'
395
+ ]
396
+ });
397
+ }
398
+ if (!observed && facts.viteConfigs.length === 0 && facts.jestConfigs.length === 0) {
399
+ actions.push({
400
+ kind: "note",
401
+ gap: true,
402
+ lines: [
403
+ "No package here runs Vitest, so no adapter was added.",
404
+ ...manualInstructions("your Vitest config")
405
+ ]
406
+ });
407
+ }
408
+ if (facts.jestConfigs.length > 0) {
409
+ actions.push({
410
+ kind: "note",
411
+ gap: true,
412
+ lines: [
413
+ `Jest found (${facts.jestConfigs.join(", ")}). There is no Jest adapter yet, so Jest runs are not checked.`
414
+ ]
415
+ });
416
+ }
417
+ if (!facts.ignoresVariantDir) {
418
+ const before = await readText(path3.join(cwd, ".gitignore"));
419
+ actions.push({
420
+ kind: "write",
421
+ file: ".gitignore",
422
+ before,
423
+ after: withIgnoreEntry(before),
424
+ reason: "variant keeps its index and history in .variant/"
425
+ });
426
+ }
427
+ for (const workflow of facts.workflows) {
428
+ if (!observed || !workflow.runsTests || !workflow.shallow) continue;
429
+ const after = withFullHistory(workflow.content);
430
+ if (after === null) continue;
431
+ actions.push({
432
+ kind: "write",
433
+ file: workflow.file,
434
+ before: workflow.content,
435
+ after,
436
+ reason: "the adapter compares against an earlier commit, and the default checkout fetches only one"
437
+ });
438
+ }
439
+ if (!facts.workflows.some((w) => w.file === PULL_REQUEST_WORKFLOW)) {
440
+ actions.push({
441
+ kind: "write",
442
+ file: PULL_REQUEST_WORKFLOW,
443
+ before: null,
444
+ after: pullRequestWorkflow(
445
+ facts.packageManager ?? "npm",
446
+ workflowStyle(
447
+ facts.workflows.find((workflow) => workflow.runsTests)?.content ?? ""
448
+ )
449
+ ),
450
+ reason: "one comment on each pull request saying what it affects"
451
+ });
452
+ }
453
+ return actions;
454
+ }
455
+
456
+ // src/core/setup/diff.ts
457
+ var CONTEXT = 2;
458
+ function diffLines(before, after) {
459
+ const a = before.split("\n");
460
+ const b = after.split("\n");
461
+ const lengths = Array.from(
462
+ { length: a.length + 1 },
463
+ () => new Array(b.length + 1).fill(0)
464
+ );
465
+ for (let i2 = a.length - 1; i2 >= 0; i2--) {
466
+ for (let j2 = b.length - 1; j2 >= 0; j2--) {
467
+ const row = lengths[i2];
468
+ if (row === void 0) continue;
469
+ row[j2] = a[i2] === b[j2] ? (lengths[i2 + 1]?.[j2 + 1] ?? 0) + 1 : Math.max(lengths[i2 + 1]?.[j2] ?? 0, row[j2 + 1] ?? 0);
470
+ }
471
+ }
472
+ const out = [];
473
+ let i = 0;
474
+ let j = 0;
475
+ while (i < a.length && j < b.length) {
476
+ if (a[i] === b[j]) {
477
+ out.push({ kind: "same", text: a[i] ?? "" });
478
+ i++;
479
+ j++;
480
+ } else if ((lengths[i + 1]?.[j] ?? 0) >= (lengths[i]?.[j + 1] ?? 0)) {
481
+ out.push({ kind: "removed", text: a[i] ?? "" });
482
+ i++;
483
+ } else {
484
+ out.push({ kind: "added", text: b[j] ?? "" });
485
+ j++;
486
+ }
487
+ }
488
+ for (; i < a.length; i++) out.push({ kind: "removed", text: a[i] ?? "" });
489
+ for (; j < b.length; j++) out.push({ kind: "added", text: b[j] ?? "" });
490
+ return out;
491
+ }
492
+ function renderDiff(before, after) {
493
+ const lines2 = diffLines(before, after);
494
+ const keep = new Array(lines2.length).fill(false);
495
+ lines2.forEach((line, index) => {
496
+ if (line.kind === "same") return;
497
+ for (let k = Math.max(0, index - CONTEXT); k <= Math.min(lines2.length - 1, index + CONTEXT); k++) {
498
+ keep[k] = true;
499
+ }
500
+ });
501
+ const out = [];
502
+ let skipped = false;
503
+ lines2.forEach((line, index) => {
504
+ if (!keep[index]) {
505
+ skipped = true;
506
+ return;
507
+ }
508
+ if (skipped && out.length > 0) out.push(" \u2026");
509
+ skipped = false;
510
+ const margin = line.kind === "added" ? "+" : line.kind === "removed" ? "-" : " ";
511
+ out.push(` ${margin} ${line.text}`);
512
+ });
513
+ return out;
514
+ }
515
+
516
+ // src/cli/render/init.ts
517
+ function renderFacts(facts, printer = getPrinter()) {
518
+ const { green, dim } = getColors();
519
+ const found = (text) => `${green("\u2713")} ${text}`;
520
+ const absent = (text) => `${dim("\xB7")} ${dim(text)}`;
521
+ lines(
522
+ printer,
523
+ "",
524
+ facts.packageManager === null ? absent("no lockfile; assuming npm") : found(`${facts.packageManager} detected`),
525
+ facts.workspacePackages > 0 ? found(
526
+ `${plural(facts.workspacePackages, "workspace package")} discovered`
527
+ ) : absent("not a workspace"),
528
+ ...facts.taskRunner === null ? [] : [
529
+ found(
530
+ `${facts.taskRunner === "turborepo" ? "Turborepo" : "Nx"} detected`
531
+ )
532
+ ],
533
+ facts.vitestConfigs.length > 0 ? found(`Vitest detected (${facts.vitestConfigs.join(", ")})`) : facts.vitestWithoutConfig.length + facts.viteConfigs.length > 0 ? found(
534
+ `Vitest detected, with no Vitest config (${[...facts.vitestWithoutConfig.map((dir) => dir || "the root"), ...facts.viteConfigs].join(", ")})`
535
+ ) : absent("no Vitest found at the root or in a workspace package"),
536
+ ...facts.jestConfigs.length > 0 ? [found("Jest detected")] : [],
537
+ ...facts.playwrightConfigs.length > 0 ? [found("Playwright detected")] : [],
538
+ facts.workflows.length > 0 ? found(
539
+ `GitHub Actions detected (${plural(facts.workflows.length, "workflow")})`
540
+ ) : absent("no GitHub Actions workflows"),
541
+ facts.defaultBranch === null ? absent("no default branch found") : found(`default branch: ${facts.defaultBranch}`)
542
+ );
543
+ }
544
+ function renderPlan(actions, printer = getPrinter()) {
545
+ const { bold, green, red, yellow } = getColors();
546
+ const changes = actions.filter((action) => action.kind !== "note");
547
+ if (changes.length === 0) {
548
+ lines(printer, "", "Nothing to change: this repository is already set up.");
549
+ }
550
+ for (const action of actions) {
551
+ if (action.kind === "install") {
552
+ lines(
553
+ printer,
554
+ "",
555
+ `${bold("Run")} ${action.command.join(" ")}`,
556
+ ` ${action.reason}`
557
+ );
558
+ } else if (action.kind === "write") {
559
+ lines(
560
+ printer,
561
+ "",
562
+ `${bold(action.before === null ? "Create" : "Edit")} ${action.file}`,
563
+ ` ${action.reason}`
564
+ );
565
+ if (action.before === null) {
566
+ for (const line of action.after.trimEnd().split("\n")) {
567
+ lines(printer, green(` + ${line}`));
568
+ }
569
+ } else {
570
+ for (const line of renderDiff(action.before, action.after)) {
571
+ lines(
572
+ printer,
573
+ line.startsWith(" +") ? green(line) : line.startsWith(" -") ? red(line) : line
574
+ );
575
+ }
576
+ }
577
+ } else {
578
+ lines(printer, "", `${yellow("Note:")} ${action.lines[0] ?? ""}`);
579
+ for (const line of action.lines.slice(1)) lines(printer, ` ${line}`);
580
+ }
581
+ }
582
+ }
583
+ function renderApplied(result, actions, printer = getPrinter()) {
584
+ const install = actions.find((action) => action.kind === "install");
585
+ lines(printer, "");
586
+ for (const file of result.written) lines(printer, `Wrote ${file}`);
587
+ if (result.installed) lines(printer, "Installed @blzsky/variant");
588
+ if (result.installError !== void 0 && install?.kind === "install") {
589
+ lines(
590
+ printer,
591
+ `The install did not finish (${result.installError}). Run it yourself: ${install.command.join(" ")}`
592
+ );
593
+ }
594
+ const unchecked = actions.some(
595
+ (action) => action.kind === "note" && action.gap === true
596
+ );
597
+ lines(
598
+ printer,
599
+ "",
600
+ unchecked ? "The pull-request report is set up. Test runs are not checked yet: see the note above." : "variant is configured. Your test command and your pull requests are all you need from here."
601
+ );
602
+ }
603
+
604
+ // src/cli/visuals/prompts.ts
605
+ import { createInterface, emitKeypressEvents } from "readline";
606
+ function readLine(input) {
607
+ return new Promise((resolve) => {
608
+ const rl = createInterface({ input });
609
+ rl.once("line", (line) => {
610
+ resolve(line);
611
+ rl.close();
612
+ });
613
+ rl.once("close", () => {
614
+ resolve("");
615
+ });
616
+ });
617
+ }
618
+ function readConfirmKey(input, output, defaultValue) {
619
+ return new Promise((resolve) => {
620
+ emitKeypressEvents(input);
621
+ input.setRawMode?.(true);
622
+ const settle = (response) => {
623
+ input.setRawMode?.(false);
624
+ input.removeListener("keypress", onKeypress);
625
+ input.pause();
626
+ resolve(response);
627
+ };
628
+ const onKeypress = (_chunk, key) => {
629
+ if (key?.ctrl === true && key.name === "c") {
630
+ input.setRawMode?.(false);
631
+ output.write(`${SHOW_CURSOR}
632
+ `);
633
+ process.exit(130);
634
+ }
635
+ if (key?.name === "y") {
636
+ settle(true);
637
+ } else if (key?.name === "n") {
638
+ settle(false);
639
+ } else if (key?.name === "return" || key?.name === "enter") {
640
+ settle(defaultValue);
641
+ }
642
+ };
643
+ input.on("keypress", onKeypress);
644
+ });
645
+ }
646
+ async function confirm(message, options = {}) {
647
+ const input = options.input ?? process.stdin;
648
+ const output = options.output ?? process.stderr;
649
+ const defaultValue = options.defaultValue ?? false;
650
+ const colors = getColors();
651
+ const hint = defaultValue ? "yes" : "no";
652
+ const prompt = `${colors.yellow("?")} ${colors.bold(message)} ${colors.dim("[y/n]")} ${colors.dim("\u203A")} ${colors.cyan(hint)}`;
653
+ if (input.isTTY !== true || input.setRawMode === void 0) {
654
+ output.write(`${prompt} `);
655
+ const answer = (await readLine(input)).trim().toLowerCase();
656
+ if (answer === "") return defaultValue;
657
+ return answer === "y" || answer === "yes";
658
+ }
659
+ output.write(`${prompt}${HIDE_CURSOR}`);
660
+ const response = await readConfirmKey(input, output, defaultValue);
661
+ const report = `${colors.green("\u2714")} ${colors.bold(message)} ${colors.dim("\xB7")} ${colors.cyan(response ? "yes" : "no")}`;
662
+ output.write(`\r${CLEAR_LINE}${report}${SHOW_CURSOR}
663
+ `);
664
+ return response;
665
+ }
666
+
667
+ // src/cli/commands/init.ts
668
+ var CONFIG_FILENAME = "variant.config.ts";
669
+ function splitList(answer) {
670
+ return answer.split(",").map((entry) => entry.trim()).filter(Boolean);
671
+ }
672
+ async function registerInitAction(opts = {}) {
673
+ if (opts.tasks === true) return registerInitTasksAction();
674
+ const cwd = process.cwd();
675
+ const printer = getPrinter();
676
+ const facts = await discoverRepository(cwd);
677
+ renderFacts(facts);
678
+ const packageSpec = opts.package ?? (opts.version === void 0 ? void 0 : `${PACKAGE_NAME}@${opts.version}`);
679
+ const actions = await planSetup(cwd, facts, {
680
+ ...packageSpec !== void 0 && { packageSpec },
681
+ ...opts.install === false && { install: false }
682
+ });
683
+ renderPlan(actions);
684
+ if (!actions.some((action) => action.kind !== "note")) return;
685
+ if (opts.dryRun === true) {
686
+ lines(printer, "", "Dry run: nothing was changed.");
687
+ return;
688
+ }
689
+ if (opts.yes !== true) {
690
+ if (process.stdin.isTTY !== true) {
691
+ lines(
692
+ printer,
693
+ "",
694
+ "Nothing was changed. Run `variant init --yes` to apply these changes."
695
+ );
696
+ return;
697
+ }
698
+ lines(printer, "");
699
+ if (!await confirm("Apply these changes?")) {
700
+ lines(printer, "Nothing was changed.");
701
+ return;
702
+ }
703
+ }
704
+ renderApplied(await applySetup(cwd, actions), actions);
705
+ }
706
+ async function registerInitTasksAction() {
707
+ const cwd = process.cwd();
708
+ const printer = getPrinter();
709
+ const existing = findVariantConfigPath(cwd);
710
+ if (existing) {
711
+ lines(
712
+ printer,
713
+ `${path4.basename(existing)} already exists \u2014 nothing written.`
714
+ );
715
+ return;
716
+ }
717
+ const [{ pm, framework }, detectedScripts] = await Promise.all([
718
+ detectProject(cwd),
719
+ detectScriptTasks(cwd)
720
+ ]);
721
+ const pmName = pm.name;
722
+ const frameworkName = framework?.name ?? null;
723
+ const destination = path4.join(cwd, CONFIG_FILENAME);
724
+ if (!process.stdin.isTTY) {
725
+ const build = {
726
+ name: "build",
727
+ command: defaultCommandFor(pmName, "build", frameworkName),
728
+ inputs: inputsFor("build")
729
+ };
730
+ await writeFile2(destination, renderConfigTemplate([build]));
731
+ lines(
732
+ printer,
733
+ `Created ${CONFIG_FILENAME} (non-interactive, detected ${pmName}${frameworkName ? `/${frameworkName}` : ""})`
734
+ );
735
+ return;
736
+ }
737
+ const rl = createInterface2({ input: process.stdin, output: process.stdout });
738
+ try {
739
+ lines(
740
+ printer,
741
+ "",
742
+ `Detected: ${pmName}${frameworkName ? ` \xB7 ${frameworkName}` : ""} \xB7 scripts: ${detectedScripts.join(", ")}`,
743
+ ""
744
+ );
745
+ const answeredTasks = await rl.question(
746
+ `Tasks to configure [${detectedScripts.join(",")}]: `
747
+ );
748
+ const taskNames = answeredTasks.trim() ? splitList(answeredTasks) : detectedScripts;
749
+ const tasks = [];
750
+ for (const name of taskNames) {
751
+ const suggestedCommand = defaultCommandFor(pmName, name, frameworkName);
752
+ const answeredCommand = await rl.question(
753
+ ` Command for "${name}" [${suggestedCommand}]: `
754
+ );
755
+ const suggestedInputs = inputsFor(name);
756
+ const answeredInputs = await rl.question(
757
+ ` Inputs for "${name}" [${suggestedInputs.join(", ")}]: `
758
+ );
759
+ tasks.push({
760
+ name,
761
+ command: answeredCommand.trim() || suggestedCommand,
762
+ inputs: answeredInputs.trim() ? splitList(answeredInputs) : suggestedInputs
763
+ });
764
+ }
765
+ await writeFile2(destination, renderConfigTemplate(tasks));
766
+ lines(
767
+ printer,
768
+ "",
769
+ `Created ${CONFIG_FILENAME} \u2014 run \`variant doctor\` to verify.`
770
+ );
771
+ } finally {
772
+ rl.close();
773
+ }
774
+ }
775
+ export {
776
+ registerInitAction
777
+ };