infernoflow 0.37.1 → 0.37.4

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 (88) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/dist/bin/infernoflow.mjs +29 -277
  3. package/dist/lib/adopters/angular.mjs +1 -128
  4. package/dist/lib/adopters/css.mjs +1 -111
  5. package/dist/lib/adopters/react.mjs +1 -104
  6. package/dist/lib/ai/ideDetection.mjs +1 -31
  7. package/dist/lib/ai/localProvider.mjs +1 -88
  8. package/dist/lib/ai/providerRouter.mjs +2 -295
  9. package/dist/lib/commands/adopt.mjs +20 -869
  10. package/dist/lib/commands/adoptWizard.mjs +9 -320
  11. package/dist/lib/commands/agent.mjs +5 -191
  12. package/dist/lib/commands/ai.mjs +2 -407
  13. package/dist/lib/commands/ask.mjs +4 -299
  14. package/dist/lib/commands/audit.mjs +13 -300
  15. package/dist/lib/commands/changelog.mjs +26 -594
  16. package/dist/lib/commands/check.mjs +3 -184
  17. package/dist/lib/commands/ci.mjs +3 -208
  18. package/dist/lib/commands/claudeMd.mjs +30 -135
  19. package/dist/lib/commands/cloud.mjs +10 -773
  20. package/dist/lib/commands/context.mjs +34 -346
  21. package/dist/lib/commands/coverage.mjs +2 -282
  22. package/dist/lib/commands/dashboard.mjs +123 -635
  23. package/dist/lib/commands/demo.mjs +8 -465
  24. package/dist/lib/commands/diff.mjs +5 -274
  25. package/dist/lib/commands/docGate.mjs +2 -81
  26. package/dist/lib/commands/doctor.mjs +3 -321
  27. package/dist/lib/commands/explain.mjs +8 -438
  28. package/dist/lib/commands/export.mjs +10 -239
  29. package/dist/lib/commands/feedback.mjs +12 -216
  30. package/dist/lib/commands/generateSkills.mjs +38 -163
  31. package/dist/lib/commands/graph.mjs +11 -378
  32. package/dist/lib/commands/health.mjs +2 -309
  33. package/dist/lib/commands/impact.mjs +2 -325
  34. package/dist/lib/commands/implement.mjs +7 -103
  35. package/dist/lib/commands/init.mjs +45 -631
  36. package/dist/lib/commands/installCursorHooks.mjs +1 -36
  37. package/dist/lib/commands/installVsCodeCopilotHooks.mjs +1 -37
  38. package/dist/lib/commands/link.mjs +2 -342
  39. package/dist/lib/commands/log.mjs +18 -248
  40. package/dist/lib/commands/monorepo.mjs +4 -428
  41. package/dist/lib/commands/notify.mjs +4 -258
  42. package/dist/lib/commands/onboard.mjs +4 -296
  43. package/dist/lib/commands/prComment.mjs +2 -361
  44. package/dist/lib/commands/prImpact.mjs +2 -157
  45. package/dist/lib/commands/publish.mjs +15 -316
  46. package/dist/lib/commands/recap.mjs +6 -380
  47. package/dist/lib/commands/report.mjs +28 -272
  48. package/dist/lib/commands/review.mjs +9 -223
  49. package/dist/lib/commands/run.mjs +8 -336
  50. package/dist/lib/commands/scaffold.mjs +54 -419
  51. package/dist/lib/commands/scan.mjs +11 -1118
  52. package/dist/lib/commands/scout.mjs +2 -291
  53. package/dist/lib/commands/setup.mjs +5 -310
  54. package/dist/lib/commands/share.mjs +13 -196
  55. package/dist/lib/commands/snapshot.mjs +3 -383
  56. package/dist/lib/commands/stability.mjs +2 -293
  57. package/dist/lib/commands/stats.mjs +5 -402
  58. package/dist/lib/commands/status.mjs +4 -172
  59. package/dist/lib/commands/suggest.mjs +21 -563
  60. package/dist/lib/commands/switch.mjs +13 -520
  61. package/dist/lib/commands/syncAuto.mjs +1 -96
  62. package/dist/lib/commands/synthesize.mjs +10 -228
  63. package/dist/lib/commands/teamSync.mjs +2 -388
  64. package/dist/lib/commands/test.mjs +6 -363
  65. package/dist/lib/commands/theme.mjs +18 -195
  66. package/dist/lib/commands/uninstall.mjs +13 -406
  67. package/dist/lib/commands/upgrade.mjs +20 -153
  68. package/dist/lib/commands/version.mjs +2 -282
  69. package/dist/lib/commands/vibe.mjs +7 -357
  70. package/dist/lib/commands/watch.mjs +4 -203
  71. package/dist/lib/commands/why.mjs +4 -358
  72. package/dist/lib/cursorHooksInstall.mjs +1 -60
  73. package/dist/lib/draftToolingInstall.mjs +7 -68
  74. package/dist/lib/git/detect-drift.mjs +4 -208
  75. package/dist/lib/learning/adapt.mjs +6 -101
  76. package/dist/lib/learning/observe.mjs +1 -119
  77. package/dist/lib/learning/patternDetector.mjs +1 -298
  78. package/dist/lib/learning/profile.mjs +2 -279
  79. package/dist/lib/learning/skillSynthesizer.mjs +24 -145
  80. package/dist/lib/telemetry.mjs +19 -269
  81. package/dist/lib/templates/index.mjs +1 -131
  82. package/dist/lib/theme/scanner.mjs +4 -343
  83. package/dist/lib/ui/errors.mjs +1 -142
  84. package/dist/lib/ui/output.mjs +6 -95
  85. package/dist/lib/ui/prompts.mjs +6 -147
  86. package/dist/lib/vsCodeCopilotHooksInstall.mjs +1 -42
  87. package/package.json +2 -4
  88. package/scripts/postinstall.js +2 -2
@@ -1,282 +1,2 @@
1
- /**
2
- * infernoflow coverage
3
- *
4
- * Maps test files to capabilities via fuzzy name matching.
5
- * Shows which capabilities have test coverage and which don't.
6
- *
7
- * Usage:
8
- * infernoflow coverage Print coverage table
9
- * infernoflow coverage --json Machine-readable output
10
- * infernoflow coverage --dir src/ Extra dirs to scan (default: project root)
11
- * infernoflow coverage --fail-below 50 Exit 1 if coverage < N%
12
- */
13
-
14
- import * as fs from "node:fs";
15
- import * as path from "node:path";
16
- import { bold, cyan, gray, green, yellow, red } from "../ui/output.mjs";
17
-
18
- // ─── test pattern extractors ─────────────────────────────────────────────────
19
-
20
- const TEST_PATTERNS = [
21
- // Jest / Vitest — it("...", …) test("...", …) describe("...", …)
22
- { regex: /(?:it|test|describe)\s*\(\s*["'`]([^"'`]+)["'`]/g, lang: "js" },
23
- // Pytest — def test_something
24
- { regex: /def\s+(test_[\w_]+)\s*\(/g, lang: "py" },
25
- // RSpec — describe/it "..."
26
- { regex: /(?:describe|it)\s+["']([^"']+)["']/g, lang: "rb" },
27
- // Go — func TestXxx(
28
- { regex: /func\s+(Test\w+)\s*\(/g, lang: "go" },
29
- // Rust — #[test] fn xxx
30
- { regex: /#\[test\]\s*\n\s*(?:async\s+)?fn\s+(\w+)/g, lang: "rs" },
31
- ];
32
-
33
- const TEST_FILE_GLOBS = [
34
- /\.(test|spec)\.[jt]sx?$/, // foo.test.ts
35
- /__tests__/, // __tests__/foo.js
36
- /\.test\.py$/, // test_foo.py
37
- /^test_.*\.py$/, // test_foo.py (basename)
38
- /_spec\.rb$/, // foo_spec.rb
39
- /\/spec\//, // spec/ directory
40
- /_test\.go$/, // foo_test.go
41
- /_test\.rs$/, // foo_test.rs
42
- ];
43
-
44
- const SKIP_DIRS = new Set([
45
- "node_modules", ".git", "dist", "build", "out", ".next",
46
- "coverage", ".nyc_output", "__pycache__", ".pytest_cache",
47
- "vendor", "tmp", ".turbo",
48
- ]);
49
-
50
- // ─── file walker ─────────────────────────────────────────────────────────────
51
-
52
- function* walkFiles(dir) {
53
- let entries;
54
- try { entries = fs.readdirSync(dir, { withFileTypes: true }); }
55
- catch { return; }
56
-
57
- for (const e of entries) {
58
- if (e.isDirectory()) {
59
- if (!SKIP_DIRS.has(e.name)) yield* walkFiles(path.join(dir, e.name));
60
- } else if (e.isFile()) {
61
- yield path.join(dir, e.name);
62
- }
63
- }
64
- }
65
-
66
- function isTestFile(filePath) {
67
- const basename = path.basename(filePath);
68
- return TEST_FILE_GLOBS.some(re => re.test(filePath) || re.test(basename));
69
- }
70
-
71
- // ─── test name extractor ─────────────────────────────────────────────────────
72
-
73
- function extractTestNames(filePath) {
74
- let src;
75
- try { src = fs.readFileSync(filePath, "utf8"); }
76
- catch { return []; }
77
-
78
- const names = new Set();
79
- for (const { regex } of TEST_PATTERNS) {
80
- const r = new RegExp(regex.source, regex.flags);
81
- let m;
82
- while ((m = r.exec(src)) !== null) {
83
- names.add(m[1].trim());
84
- }
85
- }
86
- return [...names];
87
- }
88
-
89
- // ─── fuzzy matcher ───────────────────────────────────────────────────────────
90
-
91
- /**
92
- * Tokenise a string: split on spaces, hyphens, underscores, camelCase.
93
- * Returns an array of lowercase tokens.
94
- */
95
- function tokenise(str) {
96
- return str
97
- .replace(/([a-z])([A-Z])/g, "$1 $2") // camelCase split
98
- .toLowerCase()
99
- .split(/[\s_\-/]+/)
100
- .filter(Boolean);
101
- }
102
-
103
- /**
104
- * Jaccard-like overlap score between two token sets.
105
- * Returns a value in [0, 1].
106
- */
107
- function overlapScore(a, b) {
108
- const setA = new Set(a);
109
- const setB = new Set(b);
110
- let common = 0;
111
- for (const t of setA) if (setB.has(t)) common++;
112
- const union = setA.size + setB.size - common;
113
- return union === 0 ? 0 : common / union;
114
- }
115
-
116
- /**
117
- * Best match score between a test name and a capability (id + name).
118
- */
119
- function matchScore(testName, cap) {
120
- const testTokens = tokenise(testName);
121
- const idTokens = tokenise(cap.id || "");
122
- const nameTokens = tokenise(cap.name || "");
123
- return Math.max(
124
- overlapScore(testTokens, idTokens),
125
- overlapScore(testTokens, nameTokens),
126
- );
127
- }
128
-
129
- // ─── main scanner ─────────────────────────────────────────────────────────────
130
-
131
- function scanTestFiles(dirs) {
132
- const testFiles = [];
133
- for (const dir of dirs) {
134
- for (const f of walkFiles(dir)) {
135
- if (isTestFile(f)) testFiles.push(f);
136
- }
137
- }
138
- return testFiles;
139
- }
140
-
141
- function buildTestIndex(testFiles) {
142
- // Returns: Map<testName, filePath>
143
- const index = new Map();
144
- for (const f of testFiles) {
145
- for (const name of extractTestNames(f)) {
146
- if (!index.has(name)) index.set(name, f);
147
- }
148
- }
149
- return index;
150
- }
151
-
152
- /** Returns: Map<capId, { matched: [{testName, file, score}], score: number }> */
153
- function mapTestsToCaps(capabilities, testIndex, threshold = 0.25) {
154
- const result = new Map();
155
-
156
- for (const cap of capabilities) {
157
- const hits = [];
158
- for (const [testName, file] of testIndex) {
159
- const score = matchScore(testName, cap);
160
- if (score >= threshold) {
161
- hits.push({ testName, file: path.relative(process.cwd(), file), score });
162
- }
163
- }
164
- hits.sort((a, b) => b.score - a.score);
165
- result.set(cap.id, { cap, hits });
166
- }
167
- return result;
168
- }
169
-
170
- // ─── reporters ────────────────────────────────────────────────────────────────
171
-
172
- function bar(pct, width = 20) {
173
- const filled = Math.round((pct / 100) * width);
174
- const colour = pct >= 75 ? green : pct >= 40 ? yellow : red;
175
- return colour("█".repeat(filled)) + gray("░".repeat(width - filled));
176
- }
177
-
178
- function printTable(coverageMap) {
179
- const covered = [...coverageMap.values()].filter(v => v.hits.length > 0).length;
180
- const total = coverageMap.size;
181
- const pct = total === 0 ? 0 : Math.round((covered / total) * 100);
182
-
183
- console.log();
184
- console.log(bold(" Capability Coverage"));
185
- console.log(gray(" ─────────────────────────────────────────────────────────────"));
186
- console.log(
187
- gray(" ") +
188
- bold(cyan("Capability".padEnd(32))) +
189
- bold(cyan("Tests".padEnd(8))) +
190
- bold(cyan("Top match"))
191
- );
192
- console.log(gray(" ─────────────────────────────────────────────────────────────"));
193
-
194
- for (const [, { cap, hits }] of coverageMap) {
195
- const status = hits.length > 0 ? green("✔") : red("✗");
196
- const topName = hits[0] ? gray(` ${hits[0].testName.slice(0, 42)}`) : "";
197
- const count = hits.length === 0 ? red("0") : green(String(hits.length));
198
- console.log(
199
- ` ${status} ${cap.id.padEnd(30)} ${count.padEnd(6)} ${topName}`
200
- );
201
- }
202
-
203
- console.log(gray(" ─────────────────────────────────────────────────────────────"));
204
- console.log();
205
- console.log(` ${bar(pct)} ${bold(pct + "%")} (${covered}/${total} capabilities covered)`);
206
- console.log();
207
-
208
- if (total > 0 && covered < total) {
209
- const uncovered = [...coverageMap.values()]
210
- .filter(v => v.hits.length === 0)
211
- .map(v => v.cap.id);
212
- console.log(yellow(` ⚠ Uncovered: ${uncovered.join(", ")}`));
213
- console.log();
214
- }
215
- }
216
-
217
- // ─── entry point ─────────────────────────────────────────────────────────────
218
-
219
- export async function coverageCommand(rawArgs) {
220
- const args = rawArgs || [];
221
- const jsonMode = args.includes("--json");
222
- const dirIdx = args.indexOf("--dir");
223
- const extraDirs = dirIdx !== -1 ? [args[dirIdx + 1]] : [];
224
- const failIdx = args.indexOf("--fail-below");
225
- const failBelow = failIdx !== -1 ? Number(args[failIdx + 1]) : null;
226
- const threshold = (() => {
227
- const i = args.indexOf("--threshold");
228
- return i !== -1 ? Number(args[i + 1]) : 0.25;
229
- })();
230
-
231
- const cwd = process.cwd();
232
- const infernoDir = path.join(cwd, "inferno");
233
-
234
- // Load capabilities
235
- const capsPath = path.join(infernoDir, "capabilities.json");
236
- if (!fs.existsSync(capsPath)) {
237
- console.error(red("✗ inferno/capabilities.json not found — run `infernoflow init` first."));
238
- process.exit(1);
239
- }
240
- let capabilities;
241
- try { capabilities = JSON.parse(fs.readFileSync(capsPath, "utf8")); }
242
- catch (e) { console.error(red("✗ Failed to parse capabilities.json: " + e.message)); process.exit(1); }
243
-
244
- if (!Array.isArray(capabilities) || capabilities.length === 0) {
245
- console.log(yellow("No capabilities found."));
246
- process.exit(0);
247
- }
248
-
249
- // Scan test files
250
- const scanDirs = [cwd, ...extraDirs];
251
- if (!jsonMode) process.stdout.write(gray(" Scanning test files…"));
252
- const testFiles = scanTestFiles(scanDirs);
253
- if (!jsonMode) process.stdout.write(`\r Found ${testFiles.length} test file(s). \n`);
254
-
255
- const testIndex = buildTestIndex(testFiles);
256
- const coverageMap = mapTestsToCaps(capabilities, testIndex, threshold);
257
-
258
- const covered = [...coverageMap.values()].filter(v => v.hits.length > 0).length;
259
- const total = coverageMap.size;
260
- const pct = total === 0 ? 0 : Math.round((covered / total) * 100);
261
-
262
- if (jsonMode) {
263
- const out = {
264
- summary: { covered, total, pct, testFiles: testFiles.length },
265
- capabilities: [...coverageMap.entries()].map(([id, { cap, hits }]) => ({
266
- id,
267
- name: cap.name,
268
- covered: hits.length > 0,
269
- testCount: hits.length,
270
- topTests: hits.slice(0, 3).map(h => ({ name: h.testName, file: h.file, score: +h.score.toFixed(3) })),
271
- })),
272
- };
273
- console.log(JSON.stringify(out, null, 2));
274
- } else {
275
- printTable(coverageMap);
276
- }
277
-
278
- if (failBelow !== null && pct < failBelow) {
279
- if (!jsonMode) console.error(red(`✗ Coverage ${pct}% is below threshold ${failBelow}%`));
280
- process.exit(1);
281
- }
282
- }
1
+ import*as h from"node:fs";import*as u from"node:path";import{bold as p,cyan as T,gray as f,green as $,yellow as S,red as d}from"../ui/output.mjs";const A=[{regex:/(?:it|test|describe)\s*\(\s*["'`]([^"'`]+)["'`]/g,lang:"js"},{regex:/def\s+(test_[\w_]+)\s*\(/g,lang:"py"},{regex:/(?:describe|it)\s+["']([^"']+)["']/g,lang:"rb"},{regex:/func\s+(Test\w+)\s*\(/g,lang:"go"},{regex:/#\[test\]\s*\n\s*(?:async\s+)?fn\s+(\w+)/g,lang:"rs"}],D=[/\.(test|spec)\.[jt]sx?$/,/__tests__/,/\.test\.py$/,/^test_.*\.py$/,/_spec\.rb$/,/\/spec\//,/_test\.go$/,/_test\.rs$/],M=new Set(["node_modules",".git","dist","build","out",".next","coverage",".nyc_output","__pycache__",".pytest_cache","vendor","tmp",".turbo"]);function*N(s){let t;try{t=h.readdirSync(s,{withFileTypes:!0})}catch{return}for(const e of t)e.isDirectory()?M.has(e.name)||(yield*N(u.join(s,e.name))):e.isFile()&&(yield u.join(s,e.name))}function B(s){const t=u.basename(s);return D.some(e=>e.test(s)||e.test(t))}function L(s){let t;try{t=h.readFileSync(s,"utf8")}catch{return[]}const e=new Set;for(const{regex:o}of A){const c=new RegExp(o.source,o.flags);let n;for(;(n=c.exec(t))!==null;)e.add(n[1].trim())}return[...e]}function v(s){return s.replace(/([a-z])([A-Z])/g,"$1 $2").toLowerCase().split(/[\s_\-/]+/).filter(Boolean)}function E(s,t){const e=new Set(s),o=new Set(t);let c=0;for(const r of e)o.has(r)&&c++;const n=e.size+o.size-c;return n===0?0:c/n}function R(s,t){const e=v(s),o=v(t.id||""),c=v(t.name||"");return Math.max(E(e,o),E(e,c))}function J(s){const t=[];for(const e of s)for(const o of N(e))B(o)&&t.push(o);return t}function P(s){const t=new Map;for(const e of s)for(const o of L(e))t.has(o)||t.set(o,e);return t}function G(s,t,e=.25){const o=new Map;for(const c of s){const n=[];for(const[r,l]of t){const a=R(r,c);a>=e&&n.push({testName:r,file:u.relative(process.cwd(),l),score:a})}n.sort((r,l)=>l.score-r.score),o.set(c.id,{cap:c,hits:n})}return o}function K(s,t=20){const e=Math.round(s/100*t);return(s>=75?$:s>=40?S:d)("\u2588".repeat(e))+f("\u2591".repeat(t-e))}function U(s){const t=[...s.values()].filter(c=>c.hits.length>0).length,e=s.size,o=e===0?0:Math.round(t/e*100);console.log(),console.log(p(" Capability Coverage")),console.log(f(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500")),console.log(f(" ")+p(T("Capability".padEnd(32)))+p(T("Tests".padEnd(8)))+p(T("Top match"))),console.log(f(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));for(const[,{cap:c,hits:n}]of s){const r=n.length>0?$("\u2714"):d("\u2717"),l=n[0]?f(` ${n[0].testName.slice(0,42)}`):"",a=n.length===0?d("0"):$(String(n.length));console.log(` ${r} ${c.id.padEnd(30)} ${a.padEnd(6)} ${l}`)}if(console.log(f(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500")),console.log(),console.log(` ${K(o)} ${p(o+"%")} (${t}/${e} capabilities covered)`),console.log(),e>0&&t<e){const c=[...s.values()].filter(n=>n.hits.length===0).map(n=>n.cap.id);console.log(S(` \u26A0 Uncovered: ${c.join(", ")}`)),console.log()}}async function q(s){const t=s||[],e=t.includes("--json"),o=t.indexOf("--dir"),c=o!==-1?[t[o+1]]:[],n=t.indexOf("--fail-below"),r=n!==-1?Number(t[n+1]):null,l=(()=>{const i=t.indexOf("--threshold");return i!==-1?Number(t[i+1]):.25})(),a=process.cwd(),C=u.join(a,"inferno"),F=u.join(C,"capabilities.json");h.existsSync(F)||(console.error(d("\u2717 inferno/capabilities.json not found \u2014 run `infernoflow init` first.")),process.exit(1));let g;try{g=JSON.parse(h.readFileSync(F,"utf8"))}catch(i){console.error(d("\u2717 Failed to parse capabilities.json: "+i.message)),process.exit(1)}(!Array.isArray(g)||g.length===0)&&(console.log(S("No capabilities found.")),process.exit(0));const I=[a,...c];e||process.stdout.write(f(" Scanning test files\u2026"));const x=J(I);e||process.stdout.write(`\r Found ${x.length} test file(s).
2
+ `);const O=P(x),m=G(g,O,l),j=[...m.values()].filter(i=>i.hits.length>0).length,y=m.size,b=y===0?0:Math.round(j/y*100);if(e){const i={summary:{covered:j,total:y,pct:b,testFiles:x.length},capabilities:[...m.entries()].map(([k,{cap:z,hits:w}])=>({id:k,name:z.name,covered:w.length>0,testCount:w.length,topTests:w.slice(0,3).map(_=>({name:_.testName,file:_.file,score:+_.score.toFixed(3)}))}))};console.log(JSON.stringify(i,null,2))}else U(m);r!==null&&b<r&&(e||console.error(d(`\u2717 Coverage ${b}% is below threshold ${r}%`)),process.exit(1))}export{q as coverageCommand};