@mutineerjs/mutineer 0.1.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 (200) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +218 -0
  3. package/dist/admin/assets/index-B7nXq-e7.js +32 -0
  4. package/dist/admin/assets/index-B7nXq-e7.js.map +1 -0
  5. package/dist/admin/assets/index-BDQLkBUE.js +32 -0
  6. package/dist/admin/assets/index-BDQLkBUE.js.map +1 -0
  7. package/dist/admin/assets/index-DVkP-Tc7.css +1 -0
  8. package/dist/admin/index.html +13 -0
  9. package/dist/admin/server/admin.d.ts +6 -0
  10. package/dist/admin/server/admin.js +234 -0
  11. package/dist/bin/mutate-vitest.d.ts +2 -0
  12. package/dist/bin/mutate-vitest.js +90 -0
  13. package/dist/bin/mutineer.d.ts +2 -0
  14. package/dist/bin/mutineer.js +46 -0
  15. package/dist/core/__tests__/module.spec.d.ts +1 -0
  16. package/dist/core/__tests__/module.spec.js +6 -0
  17. package/dist/core/module.d.ts +11 -0
  18. package/dist/core/module.js +14 -0
  19. package/dist/core/sfc.d.ts +12 -0
  20. package/dist/core/sfc.js +54 -0
  21. package/dist/core/types.d.ts +6 -0
  22. package/dist/core/types.js +1 -0
  23. package/dist/core/variant-utils.d.ts +30 -0
  24. package/dist/core/variant-utils.js +54 -0
  25. package/dist/index.d.ts +4 -0
  26. package/dist/index.js +3 -0
  27. package/dist/mutators/__tests__/registry.spec.d.ts +1 -0
  28. package/dist/mutators/__tests__/registry.spec.js +43 -0
  29. package/dist/mutators/__tests__/utils.spec.d.ts +1 -0
  30. package/dist/mutators/__tests__/utils.spec.js +15 -0
  31. package/dist/mutators/registry.d.ts +37 -0
  32. package/dist/mutators/registry.js +101 -0
  33. package/dist/mutators/types.d.ts +39 -0
  34. package/dist/mutators/types.js +7 -0
  35. package/dist/mutators/utils.d.ts +37 -0
  36. package/dist/mutators/utils.js +151 -0
  37. package/dist/plugin/viteMutate.d.ts +15 -0
  38. package/dist/plugin/viteMutate.js +52 -0
  39. package/dist/plugin/vitest.setup.d.ts +47 -0
  40. package/dist/plugin/vitest.setup.js +118 -0
  41. package/dist/plugin/withVitest.d.ts +13 -0
  42. package/dist/plugin/withVitest.js +30 -0
  43. package/dist/runner/__tests__/discover.spec.d.ts +1 -0
  44. package/dist/runner/__tests__/discover.spec.js +59 -0
  45. package/dist/runner/__tests__/orchestrator.spec.d.ts +1 -0
  46. package/dist/runner/__tests__/orchestrator.spec.js +55 -0
  47. package/dist/runner/adapters/__tests__/jest.spec.d.ts +1 -0
  48. package/dist/runner/adapters/__tests__/jest.spec.js +88 -0
  49. package/dist/runner/adapters/__tests__/vitest-worker-runtime.spec.d.ts +1 -0
  50. package/dist/runner/adapters/__tests__/vitest-worker-runtime.spec.js +59 -0
  51. package/dist/runner/adapters/__tests__/vitest.spec.d.ts +1 -0
  52. package/dist/runner/adapters/__tests__/vitest.spec.js +118 -0
  53. package/dist/runner/adapters/index.d.ts +10 -0
  54. package/dist/runner/adapters/index.js +9 -0
  55. package/dist/runner/adapters/jest/__tests__/index.spec.d.ts +1 -0
  56. package/dist/runner/adapters/jest/__tests__/index.spec.js +88 -0
  57. package/dist/runner/adapters/jest/index.d.ts +24 -0
  58. package/dist/runner/adapters/jest/index.js +216 -0
  59. package/dist/runner/adapters/jest/worker-runtime.d.ts +37 -0
  60. package/dist/runner/adapters/jest/worker-runtime.js +171 -0
  61. package/dist/runner/adapters/jest-worker-runtime.d.ts +37 -0
  62. package/dist/runner/adapters/jest-worker-runtime.js +171 -0
  63. package/dist/runner/adapters/jest.d.ts +24 -0
  64. package/dist/runner/adapters/jest.js +216 -0
  65. package/dist/runner/adapters/types.d.ts +89 -0
  66. package/dist/runner/adapters/types.js +8 -0
  67. package/dist/runner/adapters/vitest/__tests__/index.spec.d.ts +1 -0
  68. package/dist/runner/adapters/vitest/__tests__/index.spec.js +118 -0
  69. package/dist/runner/adapters/vitest/__tests__/worker-runtime.spec.d.ts +1 -0
  70. package/dist/runner/adapters/vitest/__tests__/worker-runtime.spec.js +59 -0
  71. package/dist/runner/adapters/vitest/index.d.ts +33 -0
  72. package/dist/runner/adapters/vitest/index.js +267 -0
  73. package/dist/runner/adapters/vitest/worker-runtime.d.ts +25 -0
  74. package/dist/runner/adapters/vitest/worker-runtime.js +118 -0
  75. package/dist/runner/adapters/vitest-worker-runtime.d.ts +25 -0
  76. package/dist/runner/adapters/vitest-worker-runtime.js +118 -0
  77. package/dist/runner/adapters/vitest.d.ts +33 -0
  78. package/dist/runner/adapters/vitest.js +267 -0
  79. package/dist/runner/args.d.ts +50 -0
  80. package/dist/runner/args.js +123 -0
  81. package/dist/runner/cache.d.ts +38 -0
  82. package/dist/runner/cache.js +118 -0
  83. package/dist/runner/changed.d.ts +22 -0
  84. package/dist/runner/changed.js +210 -0
  85. package/dist/runner/cleanup.d.ts +4 -0
  86. package/dist/runner/cleanup.js +21 -0
  87. package/dist/runner/config.d.ts +13 -0
  88. package/dist/runner/config.js +94 -0
  89. package/dist/runner/discover.d.ts +7 -0
  90. package/dist/runner/discover.js +258 -0
  91. package/dist/runner/jest/__tests__/adapter.spec.d.ts +1 -0
  92. package/dist/runner/jest/__tests__/adapter.spec.js +110 -0
  93. package/dist/runner/jest/adapter.d.ts +24 -0
  94. package/dist/runner/jest/adapter.js +191 -0
  95. package/dist/runner/jest/index.d.ts +8 -0
  96. package/dist/runner/jest/index.js +7 -0
  97. package/dist/runner/jest/pool.d.ts +47 -0
  98. package/dist/runner/jest/pool.js +307 -0
  99. package/dist/runner/jest/resolver.cjs +61 -0
  100. package/dist/runner/jest/resolver.d.cts +11 -0
  101. package/dist/runner/jest/worker-runtime.d.ts +30 -0
  102. package/dist/runner/jest/worker-runtime.js +98 -0
  103. package/dist/runner/jest/worker.d.mts +1 -0
  104. package/dist/runner/jest/worker.mjs +55 -0
  105. package/dist/runner/orchestrator.d.ts +13 -0
  106. package/dist/runner/orchestrator.js +387 -0
  107. package/dist/runner/pool/__tests__/index.spec.d.ts +1 -0
  108. package/dist/runner/pool/__tests__/index.spec.js +83 -0
  109. package/dist/runner/pool/__tests__/pool-plugin.spec.d.ts +1 -0
  110. package/dist/runner/pool/__tests__/pool-plugin.spec.js +59 -0
  111. package/dist/runner/pool/__tests__/pool-redirect-loader.spec.d.ts +1 -0
  112. package/dist/runner/pool/__tests__/pool-redirect-loader.spec.js +78 -0
  113. package/dist/runner/pool/index.d.ts +8 -0
  114. package/dist/runner/pool/index.js +9 -0
  115. package/dist/runner/pool/jest/pool.d.ts +52 -0
  116. package/dist/runner/pool/jest/pool.js +309 -0
  117. package/dist/runner/pool/jest/worker.d.mts +1 -0
  118. package/dist/runner/pool/jest/worker.mjs +60 -0
  119. package/dist/runner/pool/jest-pool.d.ts +52 -0
  120. package/dist/runner/pool/jest-pool.js +309 -0
  121. package/dist/runner/pool/jest-worker.d.mts +1 -0
  122. package/dist/runner/pool/jest-worker.mjs +60 -0
  123. package/dist/runner/pool/plugin.d.ts +18 -0
  124. package/dist/runner/pool/plugin.js +60 -0
  125. package/dist/runner/pool/pool-plugin.d.ts +18 -0
  126. package/dist/runner/pool/pool-plugin.js +60 -0
  127. package/dist/runner/pool/pool-redirect-loader.d.ts +19 -0
  128. package/dist/runner/pool/pool-redirect-loader.js +116 -0
  129. package/dist/runner/pool/pool-redirect-loader.mjs +146 -0
  130. package/dist/runner/pool/redirect-loader.d.ts +19 -0
  131. package/dist/runner/pool/redirect-loader.js +116 -0
  132. package/dist/runner/pool/vitest/pool.d.ts +70 -0
  133. package/dist/runner/pool/vitest/pool.js +376 -0
  134. package/dist/runner/pool/vitest/worker.d.mts +15 -0
  135. package/dist/runner/pool/vitest/worker.mjs +96 -0
  136. package/dist/runner/pool/vitest-worker.d.mts +15 -0
  137. package/dist/runner/pool/vitest-worker.mjs +96 -0
  138. package/dist/runner/shared/index.d.ts +9 -0
  139. package/dist/runner/shared/index.js +8 -0
  140. package/dist/runner/shared/mutant-paths.d.ts +15 -0
  141. package/dist/runner/shared/mutant-paths.js +30 -0
  142. package/dist/runner/shared/redirect-state.d.ts +45 -0
  143. package/dist/runner/shared/redirect-state.js +50 -0
  144. package/dist/runner/shared-module-redirect.d.ts +56 -0
  145. package/dist/runner/shared-module-redirect.js +84 -0
  146. package/dist/runner/types.d.ts +88 -0
  147. package/dist/runner/types.js +8 -0
  148. package/dist/runner/variants.d.ts +21 -0
  149. package/dist/runner/variants.js +66 -0
  150. package/dist/runner/vitest/__tests__/adapter.spec.d.ts +1 -0
  151. package/dist/runner/vitest/__tests__/adapter.spec.js +131 -0
  152. package/dist/runner/vitest/__tests__/plugin.spec.d.ts +1 -0
  153. package/dist/runner/vitest/__tests__/plugin.spec.js +65 -0
  154. package/dist/runner/vitest/__tests__/pool.spec.d.ts +1 -0
  155. package/dist/runner/vitest/__tests__/pool.spec.js +106 -0
  156. package/dist/runner/vitest/__tests__/redirect-loader.spec.d.ts +1 -0
  157. package/dist/runner/vitest/__tests__/redirect-loader.spec.js +87 -0
  158. package/dist/runner/vitest/__tests__/worker-runtime.spec.d.ts +1 -0
  159. package/dist/runner/vitest/__tests__/worker-runtime.spec.js +75 -0
  160. package/dist/runner/vitest/adapter.d.ts +33 -0
  161. package/dist/runner/vitest/adapter.js +277 -0
  162. package/dist/runner/vitest/index.d.ts +11 -0
  163. package/dist/runner/vitest/index.js +10 -0
  164. package/dist/runner/vitest/plugin.d.ts +12 -0
  165. package/dist/runner/vitest/plugin.js +49 -0
  166. package/dist/runner/vitest/pool.d.ts +65 -0
  167. package/dist/runner/vitest/pool.js +376 -0
  168. package/dist/runner/vitest/redirect-loader.d.ts +30 -0
  169. package/dist/runner/vitest/redirect-loader.js +123 -0
  170. package/dist/runner/vitest/worker-runtime.d.ts +16 -0
  171. package/dist/runner/vitest/worker-runtime.js +105 -0
  172. package/dist/runner/vitest/worker.d.mts +15 -0
  173. package/dist/runner/vitest/worker.mjs +92 -0
  174. package/dist/types/api.d.ts +20 -0
  175. package/dist/types/api.js +1 -0
  176. package/dist/types/config.d.ts +48 -0
  177. package/dist/types/config.js +1 -0
  178. package/dist/types/index.d.ts +13 -0
  179. package/dist/types/index.js +11 -0
  180. package/dist/types/mutant.d.ts +44 -0
  181. package/dist/types/mutant.js +7 -0
  182. package/dist/utils/PoolSpinner.d.ts +5 -0
  183. package/dist/utils/PoolSpinner.js +6 -0
  184. package/dist/utils/ProgressBar.d.ts +11 -0
  185. package/dist/utils/ProgressBar.js +9 -0
  186. package/dist/utils/__tests__/coverage.spec.d.ts +1 -0
  187. package/dist/utils/__tests__/coverage.spec.js +91 -0
  188. package/dist/utils/__tests__/progress.spec.d.ts +1 -0
  189. package/dist/utils/__tests__/progress.spec.js +50 -0
  190. package/dist/utils/__tests__/summary.spec.d.ts +1 -0
  191. package/dist/utils/__tests__/summary.spec.js +54 -0
  192. package/dist/utils/coverage.d.ts +57 -0
  193. package/dist/utils/coverage.js +204 -0
  194. package/dist/utils/logger.d.ts +8 -0
  195. package/dist/utils/logger.js +18 -0
  196. package/dist/utils/progress.d.ts +25 -0
  197. package/dist/utils/progress.js +90 -0
  198. package/dist/utils/summary.d.ts +12 -0
  199. package/dist/utils/summary.js +107 -0
  200. package/package.json +59 -0
@@ -0,0 +1,57 @@
1
+ export interface CoverageData {
2
+ /** Set of covered lines per file (absolute path -> Set of line numbers) */
3
+ coveredLines: Map<string, Set<number>>;
4
+ }
5
+ /** Per-test coverage map: test file -> file path -> covered lines */
6
+ export type PerTestCoverageMap = Map<string, Map<string, Set<number>>>;
7
+ /**
8
+ * Load and parse Istanbul-format coverage JSON file.
9
+ * Supports both coverage-final.json (from Istanbul) and Vitest's coverage output.
10
+ *
11
+ * @param coverageFile - Path to the coverage JSON file
12
+ * @param cwd - Current working directory for resolving relative paths
13
+ * @returns Parsed coverage data with covered lines per file
14
+ */
15
+ export declare function loadCoverageData(coverageFile: string, cwd: string): Promise<CoverageData>;
16
+ /**
17
+ * Check if a specific line in a file is covered by tests.
18
+ *
19
+ * @param coverage - Coverage data from loadCoverageData
20
+ * @param filePath - Absolute path to the file
21
+ * @param line - Line number to check (1-indexed)
22
+ * @returns true if the line is covered, false otherwise
23
+ */
24
+ export declare function isLineCovered(coverage: CoverageData, filePath: string, line: number): boolean;
25
+ /**
26
+ * Get coverage statistics for a file.
27
+ *
28
+ * @param coverage - Coverage data from loadCoverageData
29
+ * @param filePath - Absolute path to the file
30
+ * @returns Object with covered line count and set of covered lines, or null if file not in coverage
31
+ */
32
+ export declare function getFileCoverageStats(coverage: CoverageData, filePath: string): {
33
+ count: number;
34
+ lines: Set<number>;
35
+ } | null;
36
+ /**
37
+ * Best-effort loader for per-test coverage data.
38
+ * Expects a JSON file with a shape like:
39
+ * {
40
+ * "tests": {
41
+ * "/abs/path/to/test.spec.ts": {
42
+ * "files": {
43
+ * "/abs/path/to/src/file.ts": { "lines": [1,2,3] }
44
+ * }
45
+ * }
46
+ * }
47
+ * }
48
+ * or a simplified shape:
49
+ * {
50
+ * "/abs/path/to/test.spec.ts": {
51
+ * "/abs/path/to/src/file.ts": [1,2,3]
52
+ * }
53
+ * }
54
+ *
55
+ * Returns a map: testFile -> (filePath -> covered lines).
56
+ */
57
+ export declare function loadPerTestCoverageData(reportsDir: string, cwd: string): Promise<PerTestCoverageMap | null>;
@@ -0,0 +1,204 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ /**
4
+ * Load and parse Istanbul-format coverage JSON file.
5
+ * Supports both coverage-final.json (from Istanbul) and Vitest's coverage output.
6
+ *
7
+ * @param coverageFile - Path to the coverage JSON file
8
+ * @param cwd - Current working directory for resolving relative paths
9
+ * @returns Parsed coverage data with covered lines per file
10
+ */
11
+ export async function loadCoverageData(coverageFile, cwd) {
12
+ const absPath = path.isAbsolute(coverageFile)
13
+ ? coverageFile
14
+ : path.join(cwd, coverageFile);
15
+ let raw;
16
+ try {
17
+ raw = await fs.readFile(absPath, 'utf8');
18
+ }
19
+ catch (err) {
20
+ const msg = err instanceof Error ? err.message : String(err);
21
+ throw new Error(`Failed to read coverage file "${absPath}": ${msg}`);
22
+ }
23
+ let data;
24
+ try {
25
+ data = JSON.parse(raw);
26
+ }
27
+ catch (err) {
28
+ const msg = err instanceof Error ? err.message : String(err);
29
+ throw new Error(`Failed to parse coverage file "${absPath}" as JSON: ${msg}`);
30
+ }
31
+ const coveredLines = new Map();
32
+ for (const [filePath, fileCoverage] of Object.entries(data)) {
33
+ if (!fileCoverage.statementMap || !fileCoverage.s) {
34
+ continue;
35
+ }
36
+ const lines = new Set();
37
+ // Extract lines that have been executed (hit count > 0)
38
+ for (const [stmtId, hitCount] of Object.entries(fileCoverage.s)) {
39
+ if (hitCount > 0) {
40
+ const stmt = fileCoverage.statementMap[stmtId];
41
+ if (stmt) {
42
+ // Add all lines covered by this statement
43
+ for (let line = stmt.start.line; line <= stmt.end.line; line++) {
44
+ lines.add(line);
45
+ }
46
+ }
47
+ }
48
+ }
49
+ if (lines.size > 0) {
50
+ // Normalize the file path to absolute
51
+ const absFilePath = path.isAbsolute(filePath)
52
+ ? filePath
53
+ : path.join(cwd, filePath);
54
+ coveredLines.set(absFilePath, lines);
55
+ }
56
+ }
57
+ return { coveredLines };
58
+ }
59
+ /**
60
+ * Check if a specific line in a file is covered by tests.
61
+ *
62
+ * @param coverage - Coverage data from loadCoverageData
63
+ * @param filePath - Absolute path to the file
64
+ * @param line - Line number to check (1-indexed)
65
+ * @returns true if the line is covered, false otherwise
66
+ */
67
+ export function isLineCovered(coverage, filePath, line) {
68
+ const fileLines = coverage.coveredLines.get(filePath);
69
+ if (!fileLines) {
70
+ return false;
71
+ }
72
+ return fileLines.has(line);
73
+ }
74
+ /**
75
+ * Get coverage statistics for a file.
76
+ *
77
+ * @param coverage - Coverage data from loadCoverageData
78
+ * @param filePath - Absolute path to the file
79
+ * @returns Object with covered line count and set of covered lines, or null if file not in coverage
80
+ */
81
+ export function getFileCoverageStats(coverage, filePath) {
82
+ const fileLines = coverage.coveredLines.get(filePath);
83
+ if (!fileLines) {
84
+ return null;
85
+ }
86
+ return { count: fileLines.size, lines: fileLines };
87
+ }
88
+ /**
89
+ * Best-effort loader for per-test coverage data.
90
+ * Expects a JSON file with a shape like:
91
+ * {
92
+ * "tests": {
93
+ * "/abs/path/to/test.spec.ts": {
94
+ * "files": {
95
+ * "/abs/path/to/src/file.ts": { "lines": [1,2,3] }
96
+ * }
97
+ * }
98
+ * }
99
+ * }
100
+ * or a simplified shape:
101
+ * {
102
+ * "/abs/path/to/test.spec.ts": {
103
+ * "/abs/path/to/src/file.ts": [1,2,3]
104
+ * }
105
+ * }
106
+ *
107
+ * Returns a map: testFile -> (filePath -> covered lines).
108
+ */
109
+ export async function loadPerTestCoverageData(reportsDir, cwd) {
110
+ const base = path.isAbsolute(reportsDir)
111
+ ? reportsDir
112
+ : path.join(cwd, reportsDir);
113
+ const candidates = [
114
+ path.join(base, 'per-test-coverage.json'),
115
+ path.join(base, 'coverage-per-test.json'),
116
+ path.join(base, 'coverage-final.json'), // fallback if someone writes per-test into the main file
117
+ ];
118
+ const map = new Map();
119
+ let loaded = false;
120
+ for (const file of candidates) {
121
+ try {
122
+ const raw = await fs.readFile(file, 'utf8');
123
+ const data = JSON.parse(raw);
124
+ ingestPerTestJson(data, map);
125
+ loaded = map.size > 0;
126
+ if (loaded)
127
+ break;
128
+ }
129
+ catch {
130
+ // ignore missing/unreadable candidates
131
+ }
132
+ }
133
+ if (!loaded) {
134
+ // Try scanning coverage/tmp for per-test artifacts
135
+ try {
136
+ const tmpDir = path.join(base, 'tmp');
137
+ const entries = await fs.readdir(tmpDir, { withFileTypes: true });
138
+ for (const entry of entries) {
139
+ if (!entry.isFile() || !entry.name.endsWith('.json'))
140
+ continue;
141
+ const p = path.join(tmpDir, entry.name);
142
+ try {
143
+ const raw = await fs.readFile(p, 'utf8');
144
+ const data = JSON.parse(raw);
145
+ ingestPerTestJson(data, map);
146
+ }
147
+ catch {
148
+ continue;
149
+ }
150
+ }
151
+ loaded = map.size > 0;
152
+ }
153
+ catch {
154
+ // ignore
155
+ }
156
+ }
157
+ return loaded ? map : null;
158
+ }
159
+ function ingestPerTestJson(json, out) {
160
+ if (!json || typeof json !== 'object')
161
+ return;
162
+ // If wrapped under "tests"
163
+ if ('tests' in json && json.tests && typeof json.tests === 'object') {
164
+ ingestTestsObject(json.tests, out);
165
+ return;
166
+ }
167
+ // Otherwise treat top-level as tests map
168
+ ingestTestsObject(json, out);
169
+ }
170
+ function ingestTestsObject(testsObj, out) {
171
+ if (!testsObj || typeof testsObj !== 'object')
172
+ return;
173
+ for (const [testPath, value] of Object.entries(testsObj)) {
174
+ if (!value || typeof value !== 'object')
175
+ continue;
176
+ // Format A: { files: { "/file": { lines: [] } } }
177
+ if ('files' in value && value.files && typeof value.files === 'object') {
178
+ const files = value.files;
179
+ const fileMap = ensureTestEntry(out, testPath);
180
+ for (const [filePath, detail] of Object.entries(files)) {
181
+ if (detail &&
182
+ typeof detail === 'object' &&
183
+ 'lines' in detail &&
184
+ Array.isArray(detail.lines)) {
185
+ fileMap.set(filePath, new Set(detail.lines));
186
+ }
187
+ }
188
+ continue;
189
+ }
190
+ // Format B: { "/file": [lines] }
191
+ const fileMap = ensureTestEntry(out, testPath);
192
+ for (const [filePath, lines] of Object.entries(value)) {
193
+ if (Array.isArray(lines)) {
194
+ fileMap.set(filePath, new Set(lines.filter((n) => Number.isFinite(n))));
195
+ }
196
+ }
197
+ }
198
+ }
199
+ function ensureTestEntry(map, testPath) {
200
+ if (!map.has(testPath)) {
201
+ map.set(testPath, new Map());
202
+ }
203
+ return map.get(testPath);
204
+ }
@@ -0,0 +1,8 @@
1
+ export declare const DEBUG: boolean;
2
+ export interface Logger {
3
+ debug(msg: string, ...args: unknown[]): void;
4
+ info(msg: string, ...args: unknown[]): void;
5
+ warn(msg: string, ...args: unknown[]): void;
6
+ error(msg: string, ...args: unknown[]): void;
7
+ }
8
+ export declare function createLogger(tag: string): Logger;
@@ -0,0 +1,18 @@
1
+ export const DEBUG = process.env.MUTINEER_DEBUG === '1';
2
+ export function createLogger(tag) {
3
+ return {
4
+ debug(msg, ...args) {
5
+ if (DEBUG)
6
+ console.error(`[${tag}] ${msg}`, ...args);
7
+ },
8
+ info(msg, ...args) {
9
+ console.log(msg, ...args);
10
+ },
11
+ warn(msg, ...args) {
12
+ console.warn(msg, ...args);
13
+ },
14
+ error(msg, ...args) {
15
+ console.error(msg, ...args);
16
+ },
17
+ };
18
+ }
@@ -0,0 +1,25 @@
1
+ import type { MutantStatus } from '../types/mutant.js';
2
+ export type ProgressMode = 'bar' | 'list';
3
+ export interface ProgressOptions {
4
+ readonly mode?: ProgressMode;
5
+ readonly stream?: 'stdout' | 'stderr';
6
+ }
7
+ export declare class Progress {
8
+ private total;
9
+ private done;
10
+ private killed;
11
+ private escaped;
12
+ private skipped;
13
+ private errors;
14
+ private timeouts;
15
+ private readonly mode;
16
+ private readonly useTTY;
17
+ private readonly stream;
18
+ private started;
19
+ private finished;
20
+ constructor(total: number, opts?: ProgressOptions);
21
+ start(): void;
22
+ update(status: MutantStatus): void;
23
+ finish(): void;
24
+ private writeBar;
25
+ }
@@ -0,0 +1,90 @@
1
+ import chalk from 'chalk';
2
+ export class Progress {
3
+ constructor(total, opts = {}) {
4
+ this.done = 0;
5
+ this.killed = 0;
6
+ this.escaped = 0;
7
+ this.skipped = 0;
8
+ this.errors = 0;
9
+ this.timeouts = 0;
10
+ this.started = false;
11
+ this.finished = false;
12
+ this.total = Math.max(0, total);
13
+ this.mode = opts.mode ?? 'bar';
14
+ this.stream = opts.stream === 'stdout' ? process.stdout : process.stderr;
15
+ this.useTTY = Boolean(this.stream.isTTY) && this.mode === 'bar';
16
+ }
17
+ start() {
18
+ if (this.started || this.finished)
19
+ return;
20
+ this.started = true;
21
+ if (this.useTTY) {
22
+ this.stream.write('\x1b[?25l'); // hide cursor
23
+ this.writeBar();
24
+ }
25
+ else {
26
+ console.log(`mutineer: running ${this.total} mutants`);
27
+ }
28
+ }
29
+ update(status) {
30
+ if (!this.started || this.finished)
31
+ return;
32
+ this.done++;
33
+ if (status === 'killed')
34
+ this.killed++;
35
+ else if (status === 'escaped')
36
+ this.escaped++;
37
+ else if (status === 'error')
38
+ this.errors++;
39
+ else if (status === 'timeout')
40
+ this.timeouts++;
41
+ else
42
+ this.skipped++;
43
+ if (this.useTTY) {
44
+ this.writeBar();
45
+ }
46
+ else {
47
+ console.log(`mutant ${this.done}/${this.total} ${status}`);
48
+ }
49
+ }
50
+ finish() {
51
+ if (!this.started || this.finished)
52
+ return;
53
+ this.finished = true;
54
+ if (this.useTTY) {
55
+ this.stream.write('\r\x1b[2K'); // clear the bar line
56
+ this.stream.write('\x1b[?25h'); // show cursor
57
+ }
58
+ console.log(`mutineer: killed=${this.killed} escaped=${this.escaped} ` +
59
+ `errors=${this.errors} timeouts=${this.timeouts} skipped=${this.skipped}`);
60
+ }
61
+ writeBar() {
62
+ const cols = this.stream.columns || 80;
63
+ const ratio = this.total === 0 ? 1 : Math.min(this.done / this.total, 1);
64
+ const pct = Math.round(ratio * 100);
65
+ // Calculate visible widths to ensure the line fits in one terminal row
66
+ const prefix = `mutants ${this.done}/${this.total} [`;
67
+ const suffix = `] ${pct}% `;
68
+ const stats = `killed=${this.killed} ` +
69
+ `escaped=${this.escaped} ` +
70
+ `errors=${this.errors} ` +
71
+ `timeouts=${this.timeouts} ` +
72
+ `skipped=${this.skipped}`;
73
+ const barWidth = Math.max(10, cols - prefix.length - suffix.length - stats.length - 1);
74
+ const filled = Math.round(ratio * barWidth);
75
+ const bar = '\u2588'.repeat(filled) + '\u2591'.repeat(barWidth - filled);
76
+ const line = prefix +
77
+ bar +
78
+ suffix +
79
+ chalk.green(`killed=${this.killed}`) +
80
+ ' ' +
81
+ chalk.red(`escaped=${this.escaped}`) +
82
+ ' ' +
83
+ chalk.yellow(`errors=${this.errors}`) +
84
+ ' ' +
85
+ chalk.yellow(`timeouts=${this.timeouts}`) +
86
+ ' ' +
87
+ chalk.dim(`skipped=${this.skipped}`);
88
+ this.stream.write('\r\x1b[2K' + line);
89
+ }
90
+ }
@@ -0,0 +1,12 @@
1
+ import type { MutantCacheEntry } from '../types/mutant.js';
2
+ export interface Summary {
3
+ readonly total: number;
4
+ readonly killed: number;
5
+ readonly escaped: number;
6
+ readonly skipped: number;
7
+ readonly evaluated: number;
8
+ readonly killRate: number;
9
+ }
10
+ export declare function computeSummary(cache: Readonly<Record<string, MutantCacheEntry>>): Summary;
11
+ export declare function printSummary(summary: Summary, cache?: Readonly<Record<string, MutantCacheEntry>>, durationMs?: number): void;
12
+ export declare function summarise(cache: Readonly<Record<string, MutantCacheEntry>>): Summary;
@@ -0,0 +1,107 @@
1
+ import path from 'node:path';
2
+ import chalk from 'chalk';
3
+ const SEPARATOR = '\u2500'.repeat(45);
4
+ export function computeSummary(cache) {
5
+ const allEntries = Object.values(cache);
6
+ let killed = 0;
7
+ let escaped = 0;
8
+ let skipped = 0;
9
+ for (const entry of allEntries) {
10
+ if (entry.status === 'killed')
11
+ killed++;
12
+ else if (entry.status === 'escaped')
13
+ escaped++;
14
+ else
15
+ skipped++;
16
+ }
17
+ const evaluated = killed + escaped;
18
+ const total = allEntries.length;
19
+ const killRate = evaluated === 0 ? 0 : (killed / evaluated) * 100;
20
+ return { total, killed, escaped, skipped, evaluated, killRate };
21
+ }
22
+ function formatDuration(ms) {
23
+ if (ms < 1000)
24
+ return `${ms}ms`;
25
+ const seconds = ms / 1000;
26
+ if (seconds < 60)
27
+ return `${seconds.toFixed(2)}s`;
28
+ const minutes = Math.floor(seconds / 60);
29
+ const remainingSeconds = seconds % 60;
30
+ return `${minutes}m ${remainingSeconds.toFixed(1)}s`;
31
+ }
32
+ export function printSummary(summary, cache, durationMs) {
33
+ console.log('\n' + chalk.dim(SEPARATOR));
34
+ console.log(chalk.bold(' Mutineer Test Suite Summary'));
35
+ console.log(chalk.dim(SEPARATOR));
36
+ if (summary.total === 0) {
37
+ console.log('\nNo mutants found');
38
+ console.log('\n' + chalk.dim(SEPARATOR) + '\n');
39
+ return;
40
+ }
41
+ const cwd = process.cwd();
42
+ const allEntries = cache ? Object.values(cache) : [];
43
+ const relativePaths = allEntries.map((e) => path.relative(cwd, e.file));
44
+ const maxPathLen = Math.min(Math.max(...relativePaths.map((p) => p.length), 0), 40) || 25;
45
+ const maxMutatorLen = Math.min(Math.max(...allEntries.map((e) => e.mutator.length), 0), 20) || 10;
46
+ function formatRow(entry) {
47
+ const relativePath = path.relative(cwd, entry.file);
48
+ const location = `${relativePath}@${entry.line},${entry.col}`;
49
+ if (entry.status === 'killed') {
50
+ return `${chalk.green('\u2713')} ${location.padEnd(maxPathLen)} ${chalk.dim(entry.mutator.padEnd(maxMutatorLen))}`;
51
+ }
52
+ if (entry.status === 'escaped') {
53
+ return `${chalk.red('\u2A2F')} ${location.padEnd(maxPathLen)} ${chalk.dim(entry.mutator.padEnd(maxMutatorLen))}`;
54
+ }
55
+ return `${chalk.dim('\u2022')} ${chalk.dim(location.padEnd(maxPathLen))} ${chalk.dim(entry.mutator.padEnd(maxMutatorLen))}`;
56
+ }
57
+ const entriesByStatus = {
58
+ killed: [],
59
+ escaped: [],
60
+ skipped: [],
61
+ };
62
+ for (const entry of allEntries) {
63
+ if (entry.status === 'killed')
64
+ entriesByStatus.killed.push(entry);
65
+ else if (entry.status === 'escaped')
66
+ entriesByStatus.escaped.push(entry);
67
+ else
68
+ entriesByStatus.skipped.push(entry);
69
+ }
70
+ if (entriesByStatus.killed.length) {
71
+ console.log('\n' + chalk.green.bold('Killed Mutants:'));
72
+ for (const entry of entriesByStatus.killed)
73
+ console.log(' ' + formatRow(entry));
74
+ }
75
+ if (entriesByStatus.escaped.length) {
76
+ console.log('\n' + chalk.red.bold('Escaped Mutants:'));
77
+ for (const entry of entriesByStatus.escaped)
78
+ console.log(' ' + formatRow(entry));
79
+ }
80
+ if (entriesByStatus.skipped.length) {
81
+ console.log('\n' + chalk.dim('Skipped Mutants:'));
82
+ for (const entry of entriesByStatus.skipped)
83
+ console.log(' ' + formatRow(entry));
84
+ }
85
+ console.log('\n' + chalk.dim(SEPARATOR));
86
+ console.log(`Total: ${summary.total} \u2014 ${chalk.green(`Killed: ${summary.killed}`)}, ${chalk.red(`Escaped: ${summary.escaped}`)}, ${chalk.dim(`Skipped: ${summary.skipped}`)}`);
87
+ if (summary.evaluated === 0) {
88
+ console.log(`Kill rate: ${chalk.dim('0.00% (no mutants executed)')}`);
89
+ }
90
+ else {
91
+ const rateColor = summary.killRate >= 80
92
+ ? chalk.green
93
+ : summary.killRate >= 50
94
+ ? chalk.yellow
95
+ : chalk.red;
96
+ console.log(`Kill rate: ${rateColor(summary.killRate.toFixed(2) + '%')} (${summary.killed}/${summary.evaluated})`);
97
+ }
98
+ if (durationMs !== undefined) {
99
+ console.log(`Duration: ${chalk.cyan(formatDuration(durationMs))}`);
100
+ }
101
+ console.log(chalk.dim(SEPARATOR) + '\n');
102
+ }
103
+ export function summarise(cache) {
104
+ const s = computeSummary(cache);
105
+ printSummary(s, cache);
106
+ return s;
107
+ }
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@mutineerjs/mutineer",
3
+ "version": "v0.1.0",
4
+ "type": "module",
5
+ "private": false,
6
+ "bin": {
7
+ "mutineer": "./dist/bin/mutineer.js"
8
+ },
9
+ "main": "dist/index.js",
10
+ "types": "dist/index.d.ts",
11
+ "files": [
12
+ "dist"
13
+ ],
14
+ "exports": {
15
+ ".": {
16
+ "types": "./dist/index.d.ts",
17
+ "import": "./dist/index.js"
18
+ }
19
+ },
20
+ "scripts": {
21
+ "build": "tsc -p tsconfig.json",
22
+ "dev": "tsc -w",
23
+ "test": "vitest --run",
24
+ "test:watch": "vitest",
25
+ "test:coverage": "vitest --run --coverage",
26
+ "lint": "eslint --config ./eslint.config.cjs --ext .ts,.js src/",
27
+ "mutate": "MUTINEER_DEBUG=0 tsx src/bin/mutineer.ts --config mutineer.config.ts"
28
+ },
29
+ "dependencies": {
30
+ "chalk": "^5.6.2",
31
+ "ink": "^5.2.1",
32
+ "ink-spinner": "^5.0.0",
33
+ "magic-string": "^0.30.9",
34
+ "react": "^18.3.1",
35
+ "tsx": "^4.20.6"
36
+ },
37
+ "peerDependencies": {
38
+ "@vue/compiler-sfc": ">=3.4.0",
39
+ "esbuild": "^0.25.10"
40
+ },
41
+ "devDependencies": {
42
+ "@babel/parser": "^7.28.4",
43
+ "@babel/traverse": "^7.28.4",
44
+ "@babel/types": "^7.28.4",
45
+ "@types/babel__traverse": "^7.28.0",
46
+ "@types/node": "^24.7.0",
47
+ "@types/react": "^19.2.14",
48
+ "@typescript-eslint/eslint-plugin": "^8.47.0",
49
+ "@typescript-eslint/parser": "^8.47.0",
50
+ "@vitejs/plugin-vue": "^5.1.4",
51
+ "@vitest/coverage-v8": "^4.0.15",
52
+ "eslint": "^9.39.1",
53
+ "fast-glob": "^3.3.3",
54
+ "jsdom": "^27.0.0",
55
+ "typescript": "^5.5.4",
56
+ "vite": "^6.3.6",
57
+ "vue": "^3.5.12"
58
+ }
59
+ }