headlamp 0.1.8 → 0.1.10

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.
package/README.md CHANGED
@@ -52,11 +52,60 @@ npx headlamp --coverage
52
52
  npx headlamp --coverage src/services/user.ts src/components/UserCard.tsx
53
53
  ```
54
54
 
55
+ ## Output flags
56
+
57
+ - `--onlyFailures[=true|false]`:
58
+ - When enabled, the CLI prints only failing tests during execution across all views, while still printing the final test summary (files/tests/time) at the end.
59
+ - Supported forms: `--onlyFailures`, `--onlyFailures=true`, `--onlyFailures=false`.
60
+ - Works with other selection flags (e.g., `-t`, `--changed`).
61
+
62
+ Examples:
63
+
64
+ ```bash
65
+ # Show only failures during the run, but still print the final summary
66
+ npx headlamp --onlyFailures
67
+
68
+ # Combine with changed-file selection
69
+ npx headlamp --changed --onlyFailures
70
+ ```
71
+
72
+ ## Changed-file selection
73
+
74
+ - `--changed[=mode]` selects tests by files changed in your working tree or branch.
75
+ - Modes:
76
+ - `all` (default when `--changed` is passed without a value): includes staged + unstaged + untracked files.
77
+ - `staged`: only staged changes.
78
+ - `unstaged`: only unstaged + untracked files.
79
+ - `branch`: union of
80
+ - files changed on the current branch relative to the default branch (via merge-base), and
81
+ - your current uncommitted changes (staged, unstaged tracked, and untracked files).
82
+ - Default branch is resolved via `origin/HEAD` when available, falling back to `origin/main` or `origin/master`.
83
+ - Effects:
84
+ - Uses changed production files as seeds to discover related tests by import-graph.
85
+ - Coverage tables prioritize and annotate files related to selection/changed files.
86
+
87
+ Examples:
88
+
89
+ ```bash
90
+ # Staged changes only
91
+ npx headlamp --changed=staged
92
+
93
+ # All working tree changes
94
+ npx headlamp --changed
95
+
96
+ # Diff current branch against default branch (merge-base)
97
+ npx headlamp --changed=branch
98
+
99
+ # Combine with coverage
100
+ npx headlamp --coverage --changed=branch
101
+ ```
102
+
55
103
  ## Coverage flags
56
104
 
57
105
  - `--coverage`: enables coverage collection and prints merged coverage output after test execution. Uses your project's Jest/Vitest setup and reads coverage JSON from Jest.
58
106
  - Prints a compact per-file table with hotspots and optionally detailed per-file breakdowns.
59
107
  - Honors file selection and include/exclude globs when rendering coverage tables.
108
+ - When `--changed` is specified, coverage views factor in those changed files as selection seeds, influencing relevancy ordering and the “changed-related” highlighting.
60
109
  - `--coverage.abortOnFailure`: if tests fail, exit immediately with the test exit code and skip coverage printing. Useful in CI when failures should short-circuit.
61
110
  - `--coverage.ui=jest|both`:
62
111
  - `jest`: write Istanbul text report to `coverage/merged/coverage.txt` only.
package/dist/cli.cjs CHANGED
@@ -248,6 +248,7 @@ var init_args = __esm({
248
248
  coverage: (coverageValue) => ({ type: "coverage", coverageValue }),
249
249
  coverageUi: (value) => ({ type: "coverageUi", value }),
250
250
  coverageAbortOnFailure: (value) => ({ type: "coverageAbortOnFailure", value }),
251
+ onlyFailures: (value) => ({ type: "onlyFailures", value }),
251
252
  jestArg: (value) => ({ type: "jestArg", value }),
252
253
  jestArgs: (values) => ({ type: "jestArgs", values }),
253
254
  vitestArg: (value) => ({ type: "vitestArg", value }),
@@ -401,6 +402,18 @@ var init_args = __esm({
401
402
  "--coverage.pageFit",
402
403
  (_flag, lookahead) => step([ActionBuilders.coveragePageFit(isTruthy(String(lookahead)))], true)
403
404
  ),
405
+ // --onlyFailures flag (boolean)
406
+ rule.eq("--onlyFailures", () => step([ActionBuilders.onlyFailures(true)])),
407
+ rule.startsWith(
408
+ "--onlyFailures=",
409
+ (value) => step([
410
+ ActionBuilders.onlyFailures(isTruthy((value.split("=")[1] ?? "").trim().toLowerCase()))
411
+ ])
412
+ ),
413
+ rule.withLookahead(
414
+ "--onlyFailures",
415
+ (_flag, lookahead) => step([ActionBuilders.onlyFailures(isTruthy(String(lookahead)))], true)
416
+ ),
404
417
  rule.withLookahead(
405
418
  "--testPathPattern",
406
419
  (flag, lookahead) => step([ActionBuilders.jestArgs([flag, lookahead])], true)
@@ -465,12 +478,12 @@ var init_args = __esm({
465
478
  rule.eq("--changed", () => step([ActionBuilders.changed("all")])),
466
479
  rule.startsWith("--changed=", (value) => {
467
480
  const raw = (value.split("=")[1] ?? "").trim().toLowerCase();
468
- const mode = raw === "staged" ? "staged" : raw === "unstaged" ? "unstaged" : "all";
481
+ const mode = raw === "staged" ? "staged" : raw === "unstaged" ? "unstaged" : raw === "branch" ? "branch" : "all";
469
482
  return step([ActionBuilders.changed(mode)]);
470
483
  }),
471
484
  rule.withLookahead("--changed", (_flag, lookahead) => {
472
485
  const raw = String(lookahead).trim().toLowerCase();
473
- const mode = raw === "staged" ? "staged" : raw === "unstaged" ? "unstaged" : "all";
486
+ const mode = raw === "staged" ? "staged" : raw === "unstaged" ? "unstaged" : raw === "branch" ? "branch" : "all";
474
487
  return step([ActionBuilders.changed(mode)], true);
475
488
  }),
476
489
  rule.withLookahead(
@@ -547,6 +560,8 @@ var init_args = __esm({
547
560
  return { vitest: [], jest: [], coverage: false, coverageUi: action.value };
548
561
  case "coverageAbortOnFailure":
549
562
  return { vitest: [], jest: [], coverage: false, coverageAbortOnFailure: action.value };
563
+ case "onlyFailures":
564
+ return { vitest: [], jest: [], coverage: false, onlyFailures: action.value };
550
565
  case "jestArgs":
551
566
  return { vitest: [], jest: action.values, coverage: false };
552
567
  case "selectionHint":
@@ -618,6 +633,7 @@ var init_args = __esm({
618
633
  ...next,
619
634
  ...right.changed !== void 0 || left.changed !== void 0 ? { changed: right.changed ?? left.changed } : {},
620
635
  ...right.coverageAbortOnFailure !== void 0 || left.coverageAbortOnFailure !== void 0 ? { coverageAbortOnFailure: right.coverageAbortOnFailure ?? left.coverageAbortOnFailure } : {},
636
+ ...right.onlyFailures !== void 0 || left.onlyFailures !== void 0 ? { onlyFailures: right.onlyFailures ?? left.onlyFailures } : {},
621
637
  ...right.coverageDetail !== void 0 || left.coverageDetail !== void 0 ? { coverageDetail: right.coverageDetail ?? left.coverageDetail } : {},
622
638
  ...right.coverageShowCode !== void 0 || left.coverageShowCode !== void 0 ? { coverageShowCode: right.coverageShowCode ?? left.coverageShowCode } : {},
623
639
  ...right.coverageMode !== void 0 || left.coverageMode !== void 0 ? { coverageMode: right.coverageMode ?? left.coverageMode } : {},
@@ -641,6 +657,7 @@ var init_args = __esm({
641
657
  let collectCoverage = false;
642
658
  let coverageUi = "both";
643
659
  let coverageAbortOnFailure = false;
660
+ let onlyFailures = false;
644
661
  let coverageShowCode = Boolean(process.stdout.isTTY);
645
662
  let coverageMode = "auto";
646
663
  const coverageMaxFilesLocalInit = void 0;
@@ -658,6 +675,7 @@ var init_args = __esm({
658
675
  collectCoverage ||= contrib.coverage;
659
676
  coverageUi = contrib.coverageUi ?? coverageUi;
660
677
  coverageAbortOnFailure = contrib.coverageAbortOnFailure ?? coverageAbortOnFailure;
678
+ onlyFailures = contrib.onlyFailures ?? onlyFailures;
661
679
  coverageShowCode = contrib.coverageShowCode ?? coverageShowCode;
662
680
  const coverageDetailComputed = contrib.coverageDetail ?? (contrib.selection ? "auto" : void 0);
663
681
  coverageMode = contrib.coverageMode ?? (contrib.selection ? "compact" : "auto");
@@ -691,6 +709,7 @@ var init_args = __esm({
691
709
  collectCoverage,
692
710
  coverageUi,
693
711
  coverageAbortOnFailure,
712
+ onlyFailures,
694
713
  selectionSpecified: Boolean(contrib.selection),
695
714
  selectionPaths: [...contrib.selectionPaths ?? []],
696
715
  includeGlobs,
@@ -4694,6 +4713,7 @@ var formatJestOutputVitest = (raw, opts) => {
4694
4713
  const projectHint = new RegExp(
4695
4714
  `(${cwd.replace(/[.*+?^${}()|[\\]\\\\]/g, "\\$&")})|(/gigworx-node/)`
4696
4715
  );
4716
+ const onlyFailures = Boolean(opts?.onlyFailures);
4697
4717
  const lines = raw.split(/\r?\n/);
4698
4718
  const out = [];
4699
4719
  const seenFailures = /* @__PURE__ */ new Set();
@@ -4781,8 +4801,10 @@ var formatJestOutputVitest = (raw, opts) => {
4781
4801
  continue;
4782
4802
  }
4783
4803
  seenFiles.add(rel);
4784
- const pill = badge === "PASS" ? colorTokens.passPill("PASS") : colorTokens.failPill("FAIL");
4785
- out.push(`${pill} ${ansi.white(rel)}`);
4804
+ if (!(onlyFailures && badge === "PASS")) {
4805
+ const pill = badge === "PASS" ? colorTokens.passPill("PASS") : colorTokens.failPill("FAIL");
4806
+ out.push(`${pill} ${ansi.white(rel)}`);
4807
+ }
4786
4808
  lineIndex += 1;
4787
4809
  continue;
4788
4810
  }
@@ -4895,14 +4917,21 @@ function renderVitestFromJestJSON(data, opts) {
4895
4917
  `(${cwd.replace(/[.*+?^${}()|[\\]\\\\]/g, "\\$&")})|(/gigworx-node/)`
4896
4918
  );
4897
4919
  const ctx = { projectHint, editorCmd: opts?.editorCmd, showStacks: true };
4920
+ const onlyFailures = Boolean(opts?.onlyFailures);
4898
4921
  const out = [];
4899
- out.push(renderRunLine(cwd));
4900
- out.push("");
4922
+ if (!onlyFailures) {
4923
+ out.push(renderRunLine(cwd));
4924
+ out.push("");
4925
+ }
4901
4926
  for (const file of data.testResults) {
4902
4927
  const rel = file.testFilePath.replace(/\\/g, "/").replace(`${cwd}/`, "");
4903
4928
  const failed = file.testResults.filter((assertion) => assertion.status === "failed");
4904
- out.push(...buildPerFileOverview(rel, file.testResults));
4905
- out.push(buildFileBadgeLine(rel, failed.length));
4929
+ if (!onlyFailures) {
4930
+ out.push(...buildPerFileOverview(rel, file.testResults));
4931
+ }
4932
+ if (!(onlyFailures && failed.length === 0)) {
4933
+ out.push(buildFileBadgeLine(rel, failed.length));
4934
+ }
4906
4935
  if (file.failureMessage && failed.length === 0) {
4907
4936
  const lines = file.failureMessage.split(/\r?\n/);
4908
4937
  const details = linesFromDetails(file.failureDetails);
@@ -5290,6 +5319,7 @@ var program = async () => {
5290
5319
  collectCoverage,
5291
5320
  coverageUi,
5292
5321
  coverageAbortOnFailure,
5322
+ onlyFailures,
5293
5323
  selectionSpecified,
5294
5324
  selectionPaths,
5295
5325
  includeGlobs,
@@ -5317,6 +5347,52 @@ var program = async () => {
5317
5347
  return [];
5318
5348
  }
5319
5349
  };
5350
+ if (mode === "branch") {
5351
+ const resolveDefaultBranch = async () => {
5352
+ const candidates = [];
5353
+ try {
5354
+ const sym = await collect("git", ["symbolic-ref", "refs/remotes/origin/HEAD"]);
5355
+ const headRef = sym.find((ln) => ln.includes("refs/remotes/origin/"));
5356
+ if (headRef) {
5357
+ const m = /refs\/remotes\/(.+)/.exec(headRef);
5358
+ if (m && m[1]) {
5359
+ candidates.push(m[1]);
5360
+ }
5361
+ }
5362
+ } catch {
5363
+ }
5364
+ candidates.push("origin/main", "origin/master");
5365
+ for (const cand of candidates) {
5366
+ const exists = await collect("git", ["rev-parse", "--verify", cand]);
5367
+ if (exists.length > 0) {
5368
+ return cand;
5369
+ }
5370
+ }
5371
+ return void 0;
5372
+ };
5373
+ const defaultBranch = await resolveDefaultBranch();
5374
+ const mergeBase = defaultBranch ? (await collect("git", ["merge-base", "HEAD", defaultBranch]))[0] : void 0;
5375
+ const diffBase = mergeBase ?? "HEAD^";
5376
+ const branchDiff = await collect("git", [
5377
+ "diff",
5378
+ "--name-only",
5379
+ "--diff-filter=ACMRTUXB",
5380
+ diffBase,
5381
+ "HEAD"
5382
+ ]);
5383
+ const stagedNow = await collect("git", [
5384
+ "diff",
5385
+ "--name-only",
5386
+ "--diff-filter=ACMRTUXB",
5387
+ "--cached"
5388
+ ]);
5389
+ const unstagedNow = await collect("git", ["diff", "--name-only", "--diff-filter=ACMRTUXB"]);
5390
+ const untrackedNow = await collect("git", ["ls-files", "--others", "--exclude-standard"]);
5391
+ const rels2 = Array.from(
5392
+ /* @__PURE__ */ new Set([...branchDiff, ...stagedNow, ...unstagedNow, ...untrackedNow])
5393
+ );
5394
+ return rels2.map((rel) => path10.resolve(cwd, rel).replace(/\\/g, "/")).filter((abs) => !abs.includes("/node_modules/") && !abs.includes("/coverage/"));
5395
+ }
5320
5396
  const staged = mode === "staged" || mode === "all" ? await collect("git", ["diff", "--name-only", "--diff-filter=ACMRTUXB", "--cached"]) : [];
5321
5397
  const unstagedTracked = mode === "unstaged" || mode === "all" ? await collect("git", ["diff", "--name-only", "--diff-filter=ACMRTUXB"]) : [];
5322
5398
  const untracked = mode === "unstaged" || mode === "all" ? await collect("git", ["ls-files", "--others", "--exclude-standard"]) : [];
@@ -5628,6 +5704,23 @@ var program = async () => {
5628
5704
  }
5629
5705
  if (effectiveJestFiles.length === 0) {
5630
5706
  const repoRoot = repoRootForRefinement;
5707
+ if (jestFiles.length === 0) {
5708
+ try {
5709
+ const allAcross = [];
5710
+ for (const cfg of projectConfigs) {
5711
+ const cfgCwd = path10.dirname(cfg);
5712
+ const listed = await discoverJestResilient([...jestDiscoveryArgs, "--config", cfg], {
5713
+ cwd: cfgCwd
5714
+ });
5715
+ allAcross.push(...listed);
5716
+ }
5717
+ const uniqAll = Array.from(new Set(allAcross.map((p) => p.replace(/\\/g, "/"))));
5718
+ if (uniqAll.length > 0) {
5719
+ jestFiles = uniqAll;
5720
+ }
5721
+ } catch {
5722
+ }
5723
+ }
5631
5724
  const seeds = prodSelections.map(
5632
5725
  (abs) => path10.relative(repoRoot, abs).replace(/\\/g, "/").replace(/\.(m?[tj]sx?)$/i, "")
5633
5726
  ).flatMap((rel) => {
@@ -5906,12 +5999,14 @@ var program = async () => {
5906
5999
  };
5907
6000
  pretty = renderVitestFromJestJSON(reordered, {
5908
6001
  cwd: repoRootForDiscovery,
5909
- ...editorCmd !== void 0 ? { editorCmd } : {}
6002
+ ...editorCmd !== void 0 ? { editorCmd } : {},
6003
+ onlyFailures
5910
6004
  });
5911
6005
  } catch {
5912
6006
  pretty = renderVitestFromJestJSON(bridge, {
5913
6007
  cwd: repoRootForDiscovery,
5914
- ...editorCmd !== void 0 ? { editorCmd } : {}
6008
+ ...editorCmd !== void 0 ? { editorCmd } : {},
6009
+ onlyFailures
5915
6010
  });
5916
6011
  }
5917
6012
  if (debug) {
@@ -5928,7 +6023,8 @@ ${preview}${pretty.includes("\n") ? "\n\u2026" : ""}`);
5928
6023
  }
5929
6024
  const renderOpts = {
5930
6025
  cwd: repoRootForDiscovery,
5931
- ...editorCmd !== void 0 ? { editorCmd } : {}
6026
+ ...editorCmd !== void 0 ? { editorCmd } : {},
6027
+ onlyFailures
5932
6028
  };
5933
6029
  pretty = formatJestOutputVitest(output, renderOpts);
5934
6030
  if (debug) {
@@ -5994,7 +6090,8 @@ ${preview}${pretty.includes("\n") ? "\n\u2026" : ""}`);
5994
6090
  }
5995
6091
  const text = renderVitestFromJestJSON(unified, {
5996
6092
  cwd: repoRootForDiscovery,
5997
- ...editorCmd !== void 0 ? { editorCmd } : {}
6093
+ ...editorCmd !== void 0 ? { editorCmd } : {},
6094
+ onlyFailures
5998
6095
  });
5999
6096
  if (text.trim().length > 0) {
6000
6097
  process.stdout.write(text.endsWith("\n") ? text : `${text}