@democratize-quality/qualitylens 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.
- package/dist/index.d.mts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +385 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +362 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +55 -0
package/dist/index.d.mts
ADDED
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
18
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
19
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
20
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
21
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
22
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
23
|
+
mod
|
|
24
|
+
));
|
|
25
|
+
|
|
26
|
+
// src/index.ts
|
|
27
|
+
var import_commander = require("commander");
|
|
28
|
+
var fs = __toESM(require("fs"));
|
|
29
|
+
var path = __toESM(require("path"));
|
|
30
|
+
var import_child_process = require("child_process");
|
|
31
|
+
var import_prompts = __toESM(require("prompts"));
|
|
32
|
+
var import_logger = require("@democratize-quality/qualitylens-core/utils/logger");
|
|
33
|
+
var import_config = require("@democratize-quality/qualitylens-core/core/config");
|
|
34
|
+
var import_detector = require("@democratize-quality/qualitylens-core/core/detector");
|
|
35
|
+
var import_engine = require("@democratize-quality/qualitylens-core/core/engine");
|
|
36
|
+
var import_playwright = require("@democratize-quality/qualitylens-core/sources/playwright.source");
|
|
37
|
+
var import_yaml = require("@democratize-quality/qualitylens-core/sources/yaml.source");
|
|
38
|
+
var import_routes = require("@democratize-quality/qualitylens-core/sources/routes.source");
|
|
39
|
+
var import_fuzzy = require("@democratize-quality/qualitylens-core/matchers/fuzzy.matcher");
|
|
40
|
+
var import_area = require("@democratize-quality/qualitylens-core/matchers/area.matcher");
|
|
41
|
+
var import_console = require("@democratize-quality/qualitylens-core/reporters/console.reporter");
|
|
42
|
+
var program = new import_commander.Command();
|
|
43
|
+
program.name("qualitylens").description("Test coverage map \u2014 automated + manual, by functional area").version("0.1.0");
|
|
44
|
+
program.command("scan").description("Scan the project and generate a coverage report").option("-c, --config <path>", "path to qualitylens.yaml", "./qualitylens.yaml").option("-o, --output <path>", "output directory", "./").option("-f, --format <formats>", "output formats: console,html,json,markdown (pro formats require qualitylens-pro)", "console").option("--since <branch>", "only flag routes changed since this git branch").option("--fail-under <n>", "exit code 1 if total coverage < n (CI gate)", parseFloat).action(async (opts) => {
|
|
45
|
+
const outputDir = path.resolve(opts.output);
|
|
46
|
+
import_logger.logger.configure(outputDir);
|
|
47
|
+
import_logger.logger.info("scan started", { config: opts.config, format: opts.format, output: opts.output });
|
|
48
|
+
let pro = null;
|
|
49
|
+
try {
|
|
50
|
+
const proPackage = "@democratize-quality/qualitylens-pro";
|
|
51
|
+
pro = await import(proPackage);
|
|
52
|
+
} catch {
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
const config = import_config.Config.load(opts.config);
|
|
56
|
+
import_logger.logger.info("config loaded", { projectName: config.projectName, routesType: config.routes.type });
|
|
57
|
+
const configDir = path.dirname(path.resolve(opts.config));
|
|
58
|
+
if (config.routes.generate) {
|
|
59
|
+
console.log(` [routes] running generate command: ${config.routes.generate}`);
|
|
60
|
+
import_logger.logger.info("[routes] running generate command", { command: config.routes.generate, cwd: configDir });
|
|
61
|
+
try {
|
|
62
|
+
(0, import_child_process.execSync)(config.routes.generate, {
|
|
63
|
+
stdio: "inherit",
|
|
64
|
+
cwd: configDir
|
|
65
|
+
});
|
|
66
|
+
import_logger.logger.info("[routes] generate command completed successfully");
|
|
67
|
+
} catch (err) {
|
|
68
|
+
import_logger.logger.error("[routes] generate command failed", err);
|
|
69
|
+
const exitCode = err.status ?? 1;
|
|
70
|
+
throw new Error(
|
|
71
|
+
`Generate command failed (exit ${exitCode}): ${config.routes.generate}
|
|
72
|
+
|
|
73
|
+
Common causes:
|
|
74
|
+
\u2022 Script file does not exist \u2014 check the path in routes.generate
|
|
75
|
+
\u2022 Missing dependencies \u2014 run 'npm install' in the project directory
|
|
76
|
+
\u2022 TypeScript errors in the generate script
|
|
77
|
+
|
|
78
|
+
The full error output is shown above.
|
|
79
|
+
Full details logged to: ${import_logger.logger.filePath}`
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const testsDir = config.tests?.path ? path.resolve(configDir, config.tests.path) : configDir;
|
|
84
|
+
import_logger.logger.info("test directory resolved", { testsDir });
|
|
85
|
+
const sources = [
|
|
86
|
+
new import_playwright.PlaywrightSource(testsDir),
|
|
87
|
+
new import_yaml.YamlSource(opts.config),
|
|
88
|
+
...config.ado && pro ? [pro.createAdoSource(config.ado)] : []
|
|
89
|
+
];
|
|
90
|
+
const engine = new import_engine.CoverageEngine(
|
|
91
|
+
sources,
|
|
92
|
+
new import_routes.RoutesSource(config.routes),
|
|
93
|
+
new import_fuzzy.FuzzyMatcher(),
|
|
94
|
+
new import_area.AreaMatcher(config.areas),
|
|
95
|
+
config
|
|
96
|
+
);
|
|
97
|
+
const report = await engine.run();
|
|
98
|
+
import_logger.logger.info("scan complete", {
|
|
99
|
+
totalRoutes: report.summary.totalRoutes,
|
|
100
|
+
totalCoverage: report.summary.totalCoverage,
|
|
101
|
+
orphanedTests: report.unmatchedTests.length
|
|
102
|
+
});
|
|
103
|
+
const formats = opts.format.split(",").map((f) => f.trim());
|
|
104
|
+
await new import_console.ConsoleReporter().write(report, opts.output);
|
|
105
|
+
for (const format of formats) {
|
|
106
|
+
if (format === "console") {
|
|
107
|
+
} else if (pro) {
|
|
108
|
+
const reporter = pro.getReporter(format);
|
|
109
|
+
if (reporter) {
|
|
110
|
+
await reporter.write(report, opts.output);
|
|
111
|
+
} else {
|
|
112
|
+
console.warn(`Unknown format: ${format}`);
|
|
113
|
+
}
|
|
114
|
+
} else {
|
|
115
|
+
console.warn(`
|
|
116
|
+
Format '${format}' requires qualitylens-pro. Install: npm install @democratize-quality/qualitylens-pro`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (opts.failUnder && report.summary.totalCoverage < opts.failUnder) {
|
|
120
|
+
import_logger.logger.warn("coverage below threshold", { actual: report.summary.totalCoverage, threshold: opts.failUnder });
|
|
121
|
+
console.error(`
|
|
122
|
+
\u274C Coverage ${report.summary.totalCoverage}% is below threshold ${opts.failUnder}%`);
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
import_logger.logger.close();
|
|
126
|
+
} catch (err) {
|
|
127
|
+
const message = err.message ?? String(err);
|
|
128
|
+
import_logger.logger.error("scan failed", err);
|
|
129
|
+
import_logger.logger.close();
|
|
130
|
+
console.error(`
|
|
131
|
+
\u274C qualitylens scan failed: ${message}`);
|
|
132
|
+
if (import_logger.logger.filePath) {
|
|
133
|
+
console.error(` Full details: ${import_logger.logger.filePath}`);
|
|
134
|
+
}
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
program.command("init [directory]").description("Auto-detect project and create qualitylens.yaml (defaults to current directory)").option("-y, --yes", "accept all auto-detected defaults without prompting (for CI)").option("--base-path <prefix>", "base path prefix to strip when grouping routes (e.g. /api, /api/v1)").option("--test-path <path>", "path to test directory containing spec files (e.g. ./e2e, ./tests)").action(async (directory, opts = {}) => {
|
|
139
|
+
const ci = !!opts.yes;
|
|
140
|
+
const targetDir = path.resolve(directory ?? ".");
|
|
141
|
+
const configPath = path.join(targetDir, "qualitylens.yaml");
|
|
142
|
+
if (!fs.existsSync(targetDir)) {
|
|
143
|
+
console.error(`\u274C Directory not found: ${targetDir}`);
|
|
144
|
+
process.exit(1);
|
|
145
|
+
}
|
|
146
|
+
if (fs.existsSync(configPath)) {
|
|
147
|
+
if (ci) {
|
|
148
|
+
console.log(` qualitylens.yaml already exists \u2014 overwriting (--yes)`);
|
|
149
|
+
} else {
|
|
150
|
+
const { overwrite } = await (0, import_prompts.default)({
|
|
151
|
+
type: "confirm",
|
|
152
|
+
name: "overwrite",
|
|
153
|
+
message: `qualitylens.yaml already exists in ${targetDir}. Overwrite?`,
|
|
154
|
+
initial: false
|
|
155
|
+
});
|
|
156
|
+
if (!overwrite) {
|
|
157
|
+
console.log("Aborted.");
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
console.log(`
|
|
163
|
+
Analysing ${targetDir} ...
|
|
164
|
+
`);
|
|
165
|
+
const detected = await (0, import_detector.detect)(targetDir);
|
|
166
|
+
const resolvedBasePath = opts.basePath ?? detected.basePath;
|
|
167
|
+
const frameworkLabel = {
|
|
168
|
+
"nextjs": "Next.js",
|
|
169
|
+
"express-openapi": "Express + swagger-jsdoc",
|
|
170
|
+
"express": "Express",
|
|
171
|
+
"unknown": "Unknown framework"
|
|
172
|
+
};
|
|
173
|
+
console.log(` Detected: ${frameworkLabel[detected.framework]}`);
|
|
174
|
+
if (detected.routesPath) {
|
|
175
|
+
console.log(` Routes: ${detected.routesPath}`);
|
|
176
|
+
}
|
|
177
|
+
if (resolvedBasePath) {
|
|
178
|
+
console.log(` Base path: ${resolvedBasePath}${opts.basePath ? " (from --base-path)" : " (auto-detected)"}`);
|
|
179
|
+
}
|
|
180
|
+
if (detected.discoveredRoutes.length > 0) {
|
|
181
|
+
console.log(` Found ${detected.discoveredRoutes.length} route(s) across ${detected.suggestedAreas.length} area(s)
|
|
182
|
+
`);
|
|
183
|
+
}
|
|
184
|
+
let finalBasePath;
|
|
185
|
+
let finalTestsPath;
|
|
186
|
+
let finalAreas;
|
|
187
|
+
let routeType = detected.routeType;
|
|
188
|
+
let routesPath = detected.routesPath ?? "./routes.json";
|
|
189
|
+
let projectName;
|
|
190
|
+
let selectedType;
|
|
191
|
+
let selectedPath;
|
|
192
|
+
const specFileExists = detected.routeType === "openapi" && detected.routesPath != null && fs.existsSync(path.join(targetDir, detected.routesPath.replace("./", "")));
|
|
193
|
+
if (ci) {
|
|
194
|
+
projectName = detected.projectName;
|
|
195
|
+
finalBasePath = opts.basePath ?? resolvedBasePath;
|
|
196
|
+
finalTestsPath = opts.testPath ?? detected.testsPath;
|
|
197
|
+
finalAreas = (0, import_detector.suggestAreas)(detected.discoveredRoutes, finalBasePath);
|
|
198
|
+
if (detected.framework === "unknown") {
|
|
199
|
+
selectedType = "manual";
|
|
200
|
+
selectedPath = "./routes.json";
|
|
201
|
+
}
|
|
202
|
+
} else {
|
|
203
|
+
const batch1 = [
|
|
204
|
+
{
|
|
205
|
+
type: "text",
|
|
206
|
+
name: "projectName",
|
|
207
|
+
message: "Project name",
|
|
208
|
+
initial: detected.projectName
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
type: "text",
|
|
212
|
+
name: "basePath",
|
|
213
|
+
message: "Base path prefix to strip when grouping routes (leave blank for none)",
|
|
214
|
+
initial: resolvedBasePath ?? ""
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
type: "text",
|
|
218
|
+
name: "testsPath",
|
|
219
|
+
message: "Path to test directory containing spec files (leave blank for auto-detect)",
|
|
220
|
+
initial: detected.testsPath ?? ""
|
|
221
|
+
}
|
|
222
|
+
];
|
|
223
|
+
if (detected.framework === "unknown") {
|
|
224
|
+
batch1.push(
|
|
225
|
+
{
|
|
226
|
+
type: "select",
|
|
227
|
+
name: "selectedType",
|
|
228
|
+
message: "Could not detect framework. How are your routes defined?",
|
|
229
|
+
choices: [
|
|
230
|
+
{ title: "Next.js (pages/ or app/ directory)", value: "nextjs" },
|
|
231
|
+
{ title: "OpenAPI / Swagger spec file", value: "openapi" },
|
|
232
|
+
{ title: "Express routes manifest (JSON)", value: "express" },
|
|
233
|
+
{ title: "Manual list", value: "manual" }
|
|
234
|
+
]
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
type: "text",
|
|
238
|
+
name: "selectedPath",
|
|
239
|
+
message: "Path to routes directory or file",
|
|
240
|
+
initial: "./src/app"
|
|
241
|
+
}
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
const b1 = await (0, import_prompts.default)(batch1, {
|
|
245
|
+
onCancel: () => {
|
|
246
|
+
console.log("\nAborted.");
|
|
247
|
+
process.exit(0);
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
if (!b1.projectName) {
|
|
251
|
+
console.log("Aborted.");
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
projectName = b1.projectName;
|
|
255
|
+
finalBasePath = opts.basePath ?? (b1.basePath?.trim() || null);
|
|
256
|
+
finalTestsPath = opts.testPath ?? (b1.testsPath?.trim() || null);
|
|
257
|
+
selectedType = b1.selectedType;
|
|
258
|
+
selectedPath = b1.selectedPath;
|
|
259
|
+
finalAreas = (0, import_detector.suggestAreas)(detected.discoveredRoutes, finalBasePath);
|
|
260
|
+
if (finalAreas.length > 0) {
|
|
261
|
+
console.log(`
|
|
262
|
+
Suggested areas \u2014 press Enter to accept, or type a new name:
|
|
263
|
+
`);
|
|
264
|
+
const batch2 = finalAreas.map((area, i) => {
|
|
265
|
+
const routeList = area.routes.map((r) => r.path).join(", ");
|
|
266
|
+
return {
|
|
267
|
+
type: "text",
|
|
268
|
+
name: `area_${i}`,
|
|
269
|
+
message: ` ${routeList.padEnd(45)} \u2192 area name`,
|
|
270
|
+
initial: area.suggestedName
|
|
271
|
+
};
|
|
272
|
+
});
|
|
273
|
+
const b2 = await (0, import_prompts.default)(batch2, {
|
|
274
|
+
onCancel: () => {
|
|
275
|
+
console.log("\nAborted.");
|
|
276
|
+
process.exit(0);
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
finalAreas = finalAreas.map((area, i) => ({
|
|
280
|
+
...area,
|
|
281
|
+
suggestedName: (b2[`area_${i}`] ?? area.suggestedName).trim()
|
|
282
|
+
})).filter((a) => a.suggestedName);
|
|
283
|
+
} else if (detected.framework === "express-openapi" && detected.discoveredRoutes.length === 0) {
|
|
284
|
+
console.log(` Routes cannot be discovered until openapi.json is generated.`);
|
|
285
|
+
console.log(` A placeholder area will be added \u2014 update it after running 'qualitylens scan'.
|
|
286
|
+
`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
const confirmedAreas = finalAreas.filter((a) => a.suggestedName).map((a) => ({ name: a.suggestedName, patterns: a.patterns }));
|
|
290
|
+
if (detected.framework === "unknown") {
|
|
291
|
+
if (!ci && (!selectedType || !selectedPath)) {
|
|
292
|
+
console.log("Aborted.");
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
routeType = selectedType ?? "manual";
|
|
296
|
+
routesPath = selectedPath ?? "./routes.json";
|
|
297
|
+
}
|
|
298
|
+
const content = import_config.Config.generateStarter({
|
|
299
|
+
projectName,
|
|
300
|
+
routeType,
|
|
301
|
+
routesPath,
|
|
302
|
+
generate: detected.generate,
|
|
303
|
+
omitGenerate: specFileExists,
|
|
304
|
+
basePath: finalBasePath,
|
|
305
|
+
testsPath: finalTestsPath,
|
|
306
|
+
areas: confirmedAreas
|
|
307
|
+
});
|
|
308
|
+
fs.writeFileSync(configPath, content, "utf-8");
|
|
309
|
+
console.log(`
|
|
310
|
+
\u2705 qualitylens.yaml created at ${configPath}`);
|
|
311
|
+
if (detected.framework === "express-openapi") {
|
|
312
|
+
console.log(`
|
|
313
|
+
Next steps:`);
|
|
314
|
+
console.log(` 1. Export swaggerSpec from your swagger.ts`);
|
|
315
|
+
console.log(` 2. Create src/generate-openapi.ts (see docs/CONFIGURATION.md)`);
|
|
316
|
+
console.log(` 3. Update the generate: line in qualitylens.yaml`);
|
|
317
|
+
console.log(` 4. Run 'qualitylens scan' to generate your first report`);
|
|
318
|
+
} else {
|
|
319
|
+
console.log(` Run 'qualitylens scan' to generate your first coverage report.`);
|
|
320
|
+
}
|
|
321
|
+
console.log();
|
|
322
|
+
});
|
|
323
|
+
program.command("validate").description("Validate qualitylens.yaml and test ADO connection if configured").option("-c, --config <path>", "path to qualitylens.yaml", "./qualitylens.yaml").action(async (opts) => {
|
|
324
|
+
import_logger.logger.configure(path.dirname(path.resolve(opts.config)));
|
|
325
|
+
import_logger.logger.info("validate started", { config: opts.config });
|
|
326
|
+
try {
|
|
327
|
+
const config = import_config.Config.load(opts.config);
|
|
328
|
+
console.log(`\u2705 Config valid: ${config.projectName}`);
|
|
329
|
+
console.log(` Routes: ${config.routes.type} \u2192 ${config.routes.path}`);
|
|
330
|
+
console.log(` Areas: ${config.areas.map((a) => a.name).join(", ")}`);
|
|
331
|
+
console.log(` Stale threshold: ${config.staleThresholdDays} days`);
|
|
332
|
+
import_logger.logger.info("config loaded successfully", { projectName: config.projectName });
|
|
333
|
+
if (config.ado) {
|
|
334
|
+
const pat = process.env[config.ado.patEnvVar];
|
|
335
|
+
if (!pat) {
|
|
336
|
+
const msg = `ADO PAT env var '${config.ado.patEnvVar}' is not set`;
|
|
337
|
+
import_logger.logger.error(msg);
|
|
338
|
+
console.error(`\u274C ${msg}`);
|
|
339
|
+
console.error(` Set it with: export ${config.ado.patEnvVar}=<your-pat>`);
|
|
340
|
+
import_logger.logger.close();
|
|
341
|
+
process.exit(1);
|
|
342
|
+
}
|
|
343
|
+
console.log(`\u2705 ADO PAT found in ${config.ado.patEnvVar}`);
|
|
344
|
+
process.stdout.write(` Testing ADO connection to ${config.ado.orgUrl} ...`);
|
|
345
|
+
try {
|
|
346
|
+
const { httpGet } = await import("@democratize-quality/qualitylens-core/utils/http");
|
|
347
|
+
const authHeader = "Basic " + Buffer.from(":" + pat).toString("base64");
|
|
348
|
+
await httpGet(
|
|
349
|
+
`${config.ado.orgUrl}/_apis/projects/${encodeURIComponent(config.ado.project)}?api-version=7.0`,
|
|
350
|
+
{ Authorization: authHeader }
|
|
351
|
+
);
|
|
352
|
+
console.log(" \u2705");
|
|
353
|
+
console.log(` Project '${config.ado.project}' is accessible`);
|
|
354
|
+
import_logger.logger.info("ADO connection successful", { orgUrl: config.ado.orgUrl, project: config.ado.project });
|
|
355
|
+
} catch (err) {
|
|
356
|
+
console.log(" \u274C");
|
|
357
|
+
import_logger.logger.error("ADO connection failed", err);
|
|
358
|
+
console.error(` ADO connection failed: ${err.message}`);
|
|
359
|
+
console.error(` Check: org URL, project name, and PAT permissions (Test Management: Read)`);
|
|
360
|
+
if (import_logger.logger.filePath) console.error(` Full details: ${import_logger.logger.filePath}`);
|
|
361
|
+
import_logger.logger.close();
|
|
362
|
+
process.exit(1);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
import_logger.logger.close();
|
|
366
|
+
} catch (err) {
|
|
367
|
+
import_logger.logger.error("validate failed", err);
|
|
368
|
+
import_logger.logger.close();
|
|
369
|
+
console.error(`\u274C ${err.message}`);
|
|
370
|
+
if (import_logger.logger.filePath) console.error(` Full details: ${import_logger.logger.filePath}`);
|
|
371
|
+
process.exit(1);
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
(async () => {
|
|
375
|
+
try {
|
|
376
|
+
const proPackage = "@democratize-quality/qualitylens-pro";
|
|
377
|
+
const pro = await import(proPackage);
|
|
378
|
+
if (typeof pro.registerCommands === "function") {
|
|
379
|
+
pro.registerCommands(program);
|
|
380
|
+
}
|
|
381
|
+
} catch {
|
|
382
|
+
}
|
|
383
|
+
program.parse();
|
|
384
|
+
})();
|
|
385
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * src/index.ts — CLI entry point\n * Commands: scan, init, validate\n *\n * AI INSTRUCTIONS:\n * The skeleton is here. Implement the three commands following the comments.\n * Do not change command names or flag names — these are the public API.\n */\n\nimport { Command } from 'commander'\nimport * as fs from 'fs'\nimport * as path from 'path'\nimport { execSync } from 'child_process'\nimport prompts, { PromptObject } from 'prompts'\nimport { logger } from '@democratize-quality/qualitylens-core/utils/logger'\nimport { Config } from '@democratize-quality/qualitylens-core/core/config'\nimport { detect, suggestAreas } from '@democratize-quality/qualitylens-core/core/detector'\nimport { CoverageEngine } from '@democratize-quality/qualitylens-core/core/engine'\nimport { PlaywrightSource } from '@democratize-quality/qualitylens-core/sources/playwright.source'\nimport { YamlSource } from '@democratize-quality/qualitylens-core/sources/yaml.source'\nimport { RoutesSource } from '@democratize-quality/qualitylens-core/sources/routes.source'\nimport { FuzzyMatcher } from '@democratize-quality/qualitylens-core/matchers/fuzzy.matcher'\nimport { AreaMatcher } from '@democratize-quality/qualitylens-core/matchers/area.matcher'\nimport { ConsoleReporter } from '@democratize-quality/qualitylens-core/reporters/console.reporter'\nimport type { BaseReporter } from '@democratize-quality/qualitylens-core/reporters/base.reporter'\n\ninterface ProModule {\n SUPPORTED_FORMATS: string[]\n getReporter(format: string): BaseReporter | null\n createAdoSource(config: any): InstanceType<typeof import('@democratize-quality/qualitylens-core/sources/base.source').BaseSource>\n registerCommands(program: typeof import('commander').Command.prototype): void\n}\n\nconst program = new Command()\n\nprogram\n .name('qualitylens')\n .description('Test coverage map — automated + manual, by functional area')\n .version('0.1.0')\n\n// ─── scan ──────────────────────────────────────────────────────────────────\n\nprogram\n .command('scan')\n .description('Scan the project and generate a coverage report')\n .option('-c, --config <path>', 'path to qualitylens.yaml', './qualitylens.yaml')\n .option('-o, --output <path>', 'output directory', './')\n .option('-f, --format <formats>', 'output formats: console,html,json,markdown (pro formats require qualitylens-pro)', 'console')\n .option('--since <branch>', 'only flag routes changed since this git branch')\n .option('--fail-under <n>', 'exit code 1 if total coverage < n (CI gate)', parseFloat)\n .action(async (opts) => {\n const outputDir = path.resolve(opts.output)\n logger.configure(outputDir)\n logger.info('scan started', { config: opts.config, format: opts.format, output: opts.output })\n\n // Attempt to load pro features — silently skip if not installed\n // Variable path prevents TypeScript from statically resolving the module during DTS generation,\n // breaking the build-time dependency on qualitylens-pro.\n let pro: ProModule | null = null\n try {\n const proPackage = '@democratize-quality/qualitylens-pro'\n pro = await import(proPackage) as unknown as ProModule\n } catch {\n // qualitylens-pro not installed — console-only mode\n }\n\n try {\n const config = Config.load(opts.config)\n logger.info('config loaded', { projectName: config.projectName, routesType: config.routes.type })\n\n const configDir = path.dirname(path.resolve(opts.config))\n\n if (config.routes.generate) {\n console.log(` [routes] running generate command: ${config.routes.generate}`)\n logger.info('[routes] running generate command', { command: config.routes.generate, cwd: configDir })\n try {\n execSync(config.routes.generate, {\n stdio: 'inherit',\n cwd: configDir,\n })\n logger.info('[routes] generate command completed successfully')\n } catch (err: any) {\n logger.error('[routes] generate command failed', err)\n const exitCode = err.status ?? 1\n throw new Error(\n `Generate command failed (exit ${exitCode}): ${config.routes.generate}\\n` +\n `\\n Common causes:\\n` +\n ` • Script file does not exist — check the path in routes.generate\\n` +\n ` • Missing dependencies — run 'npm install' in the project directory\\n` +\n ` • TypeScript errors in the generate script\\n` +\n `\\n The full error output is shown above.\\n` +\n ` Full details logged to: ${logger.filePath}`\n )\n }\n }\n\n const testsDir = config.tests?.path\n ? path.resolve(configDir, config.tests.path)\n : configDir\n logger.info('test directory resolved', { testsDir })\n\n const sources = [\n new PlaywrightSource(testsDir),\n new YamlSource(opts.config),\n ...(config.ado && pro ? [pro.createAdoSource(config.ado)] : []),\n ]\n\n const engine = new CoverageEngine(\n sources,\n new RoutesSource(config.routes),\n new FuzzyMatcher(),\n new AreaMatcher(config.areas),\n config\n )\n\n const report = await engine.run()\n logger.info('scan complete', {\n totalRoutes: report.summary.totalRoutes,\n totalCoverage: report.summary.totalCoverage,\n orphanedTests: report.unmatchedTests.length,\n })\n\n const formats = opts.format.split(',').map((f: string) => f.trim())\n\n // Console reporter always runs\n await new ConsoleReporter().write(report, opts.output)\n\n for (const format of formats) {\n if (format === 'console') {\n // already ran above\n } else if (pro) {\n const reporter = pro.getReporter(format)\n if (reporter) {\n await reporter.write(report, opts.output)\n } else {\n console.warn(`Unknown format: ${format}`)\n }\n } else {\n console.warn(`\\n Format '${format}' requires qualitylens-pro. Install: npm install @democratize-quality/qualitylens-pro`)\n }\n }\n\n if (opts.failUnder && report.summary.totalCoverage < opts.failUnder) {\n logger.warn('coverage below threshold', { actual: report.summary.totalCoverage, threshold: opts.failUnder })\n console.error(`\\n❌ Coverage ${report.summary.totalCoverage}% is below threshold ${opts.failUnder}%`)\n process.exit(1)\n }\n\n logger.close()\n } catch (err) {\n const message = (err as Error).message ?? String(err)\n logger.error('scan failed', err)\n logger.close()\n console.error(`\\n❌ qualitylens scan failed: ${message}`)\n if (logger.filePath) {\n console.error(` Full details: ${logger.filePath}`)\n }\n process.exit(1)\n }\n })\n\n// ─── init ──────────────────────────────────────────────────────────────────\n\nprogram\n .command('init [directory]')\n .description('Auto-detect project and create qualitylens.yaml (defaults to current directory)')\n .option('-y, --yes', 'accept all auto-detected defaults without prompting (for CI)')\n .option('--base-path <prefix>', 'base path prefix to strip when grouping routes (e.g. /api, /api/v1)')\n .option('--test-path <path>', 'path to test directory containing spec files (e.g. ./e2e, ./tests)')\n .action(async (directory?: string, opts: { yes?: boolean; basePath?: string; testPath?: string } = {}) => {\n const ci = !!opts.yes\n\n const targetDir = path.resolve(directory ?? '.')\n const configPath = path.join(targetDir, 'qualitylens.yaml')\n\n if (!fs.existsSync(targetDir)) {\n console.error(`❌ Directory not found: ${targetDir}`)\n process.exit(1)\n }\n\n if (fs.existsSync(configPath)) {\n if (ci) {\n console.log(` qualitylens.yaml already exists — overwriting (--yes)`)\n } else {\n const { overwrite } = await prompts({\n type: 'confirm',\n name: 'overwrite',\n message: `qualitylens.yaml already exists in ${targetDir}. Overwrite?`,\n initial: false,\n })\n if (!overwrite) { console.log('Aborted.'); return }\n }\n }\n\n console.log(`\\n Analysing ${targetDir} ...\\n`)\n const detected = await detect(targetDir)\n\n const resolvedBasePath = opts.basePath ?? detected.basePath\n\n const frameworkLabel: Record<string, string> = {\n 'nextjs': 'Next.js',\n 'express-openapi': 'Express + swagger-jsdoc',\n 'express': 'Express',\n 'unknown': 'Unknown framework',\n }\n console.log(` Detected: ${frameworkLabel[detected.framework]}`)\n\n if (detected.routesPath) {\n console.log(` Routes: ${detected.routesPath}`)\n }\n if (resolvedBasePath) {\n console.log(` Base path: ${resolvedBasePath}${opts.basePath ? ' (from --base-path)' : ' (auto-detected)'}`)\n }\n if (detected.discoveredRoutes.length > 0) {\n console.log(` Found ${detected.discoveredRoutes.length} route(s) across ${detected.suggestedAreas.length} area(s)\\n`)\n }\n\n type PromptAnswers = Record<string, string>\n\n let finalBasePath: string | null\n let finalTestsPath: string | null\n let finalAreas: typeof detected.suggestedAreas\n let routeType = detected.routeType\n let routesPath = detected.routesPath ?? './routes.json'\n let projectName: string\n let selectedType: string | undefined\n let selectedPath: string | undefined\n\n // Spec file exists on disk → no generate step needed\n const specFileExists = detected.routeType === 'openapi'\n && detected.routesPath != null\n && fs.existsSync(path.join(targetDir, detected.routesPath.replace('./', '')))\n\n if (ci) {\n projectName = detected.projectName\n finalBasePath = opts.basePath ?? resolvedBasePath\n finalTestsPath = opts.testPath ?? detected.testsPath\n finalAreas = suggestAreas(detected.discoveredRoutes, finalBasePath)\n if (detected.framework === 'unknown') {\n selectedType = 'manual'\n selectedPath = './routes.json'\n }\n } else {\n const batch1: PromptObject[] = [\n {\n type: 'text',\n name: 'projectName',\n message: 'Project name',\n initial: detected.projectName,\n },\n {\n type: 'text',\n name: 'basePath',\n message: 'Base path prefix to strip when grouping routes (leave blank for none)',\n initial: resolvedBasePath ?? '',\n },\n {\n type: 'text',\n name: 'testsPath',\n message: 'Path to test directory containing spec files (leave blank for auto-detect)',\n initial: detected.testsPath ?? '',\n },\n ]\n\n if (detected.framework === 'unknown') {\n batch1.push(\n {\n type: 'select',\n name: 'selectedType',\n message: 'Could not detect framework. How are your routes defined?',\n choices: [\n { title: 'Next.js (pages/ or app/ directory)', value: 'nextjs' },\n { title: 'OpenAPI / Swagger spec file', value: 'openapi' },\n { title: 'Express routes manifest (JSON)', value: 'express' },\n { title: 'Manual list', value: 'manual' },\n ],\n } as PromptObject,\n {\n type: 'text',\n name: 'selectedPath',\n message: 'Path to routes directory or file',\n initial: './src/app',\n }\n )\n }\n\n const b1: PromptAnswers = await prompts(batch1, {\n onCancel: () => { console.log('\\nAborted.'); process.exit(0) },\n })\n if (!b1.projectName) { console.log('Aborted.'); return }\n\n projectName = b1.projectName\n finalBasePath = opts.basePath ?? (b1.basePath?.trim() || null)\n finalTestsPath = opts.testPath ?? (b1.testsPath?.trim() || null)\n selectedType = b1.selectedType\n selectedPath = b1.selectedPath\n\n finalAreas = suggestAreas(detected.discoveredRoutes, finalBasePath)\n\n if (finalAreas.length > 0) {\n console.log(`\\n Suggested areas — press Enter to accept, or type a new name:\\n`)\n\n const batch2: PromptObject[] = finalAreas.map((area, i) => {\n const routeList = area.routes.map(r => r.path).join(', ')\n return {\n type: 'text',\n name: `area_${i}`,\n message: ` ${routeList.padEnd(45)} → area name`,\n initial: area.suggestedName,\n }\n })\n\n const b2: PromptAnswers = await prompts(batch2, {\n onCancel: () => { console.log('\\nAborted.'); process.exit(0) },\n })\n\n finalAreas = finalAreas.map((area, i) => ({\n ...area,\n suggestedName: (b2[`area_${i}`] ?? area.suggestedName).trim(),\n })).filter(a => a.suggestedName)\n\n } else if (detected.framework === 'express-openapi' && detected.discoveredRoutes.length === 0) {\n console.log(` Routes cannot be discovered until openapi.json is generated.`)\n console.log(` A placeholder area will be added — update it after running 'qualitylens scan'.\\n`)\n }\n }\n\n const confirmedAreas: Array<{ name: string; patterns: string[] }> = finalAreas\n .filter(a => a.suggestedName)\n .map(a => ({ name: a.suggestedName, patterns: a.patterns }))\n\n if (detected.framework === 'unknown') {\n if (!ci && (!selectedType || !selectedPath)) { console.log('Aborted.'); return }\n routeType = (selectedType ?? 'manual') as typeof routeType\n routesPath = selectedPath ?? './routes.json'\n }\n\n const content = Config.generateStarter({\n projectName,\n routeType,\n routesPath,\n generate: detected.generate,\n omitGenerate: specFileExists,\n basePath: finalBasePath,\n testsPath: finalTestsPath,\n areas: confirmedAreas,\n })\n\n fs.writeFileSync(configPath, content, 'utf-8')\n console.log(`\\n✅ qualitylens.yaml created at ${configPath}`)\n\n if (detected.framework === 'express-openapi') {\n console.log(`\\n Next steps:`)\n console.log(` 1. Export swaggerSpec from your swagger.ts`)\n console.log(` 2. Create src/generate-openapi.ts (see docs/CONFIGURATION.md)`)\n console.log(` 3. Update the generate: line in qualitylens.yaml`)\n console.log(` 4. Run 'qualitylens scan' to generate your first report`)\n } else {\n console.log(` Run 'qualitylens scan' to generate your first coverage report.`)\n }\n console.log()\n })\n\n// ─── validate ──────────────────────────────────────────────────────────────\n\nprogram\n .command('validate')\n .description('Validate qualitylens.yaml and test ADO connection if configured')\n .option('-c, --config <path>', 'path to qualitylens.yaml', './qualitylens.yaml')\n .action(async (opts) => {\n logger.configure(path.dirname(path.resolve(opts.config)))\n logger.info('validate started', { config: opts.config })\n\n try {\n const config = Config.load(opts.config)\n console.log(`✅ Config valid: ${config.projectName}`)\n console.log(` Routes: ${config.routes.type} → ${config.routes.path}`)\n console.log(` Areas: ${config.areas.map(a => a.name).join(', ')}`)\n console.log(` Stale threshold: ${config.staleThresholdDays} days`)\n logger.info('config loaded successfully', { projectName: config.projectName })\n\n if (config.ado) {\n const pat = process.env[config.ado.patEnvVar]\n if (!pat) {\n const msg = `ADO PAT env var '${config.ado.patEnvVar}' is not set`\n logger.error(msg)\n console.error(`❌ ${msg}`)\n console.error(` Set it with: export ${config.ado.patEnvVar}=<your-pat>`)\n logger.close()\n process.exit(1)\n }\n console.log(`✅ ADO PAT found in ${config.ado.patEnvVar}`)\n\n process.stdout.write(` Testing ADO connection to ${config.ado.orgUrl} ...`)\n try {\n const { httpGet } = await import('@democratize-quality/qualitylens-core/utils/http')\n const authHeader = 'Basic ' + Buffer.from(':' + pat).toString('base64')\n await httpGet(\n `${config.ado.orgUrl}/_apis/projects/${encodeURIComponent(config.ado.project)}?api-version=7.0`,\n { Authorization: authHeader }\n )\n console.log(' ✅')\n console.log(` Project '${config.ado.project}' is accessible`)\n logger.info('ADO connection successful', { orgUrl: config.ado.orgUrl, project: config.ado.project })\n } catch (err) {\n console.log(' ❌')\n logger.error('ADO connection failed', err)\n console.error(` ADO connection failed: ${(err as Error).message}`)\n console.error(` Check: org URL, project name, and PAT permissions (Test Management: Read)`)\n if (logger.filePath) console.error(` Full details: ${logger.filePath}`)\n logger.close()\n process.exit(1)\n }\n }\n\n logger.close()\n } catch (err) {\n logger.error('validate failed', err)\n logger.close()\n console.error(`❌ ${(err as Error).message}`)\n if (logger.filePath) console.error(` Full details: ${logger.filePath}`)\n process.exit(1)\n }\n })\n\n// Load pro commands before parsing so they appear in --help and tab-completion\n;(async () => {\n try {\n const proPackage = '@democratize-quality/qualitylens-pro'\n const pro = await import(proPackage) as unknown as ProModule\n if (typeof pro.registerCommands === 'function') {\n pro.registerCommands(program)\n }\n } catch {\n // qualitylens-pro not installed — pro commands unavailable\n }\n program.parse()\n})()\n"],"mappings":"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","names":["prompts"]}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import * as fs from "fs";
|
|
6
|
+
import * as path from "path";
|
|
7
|
+
import { execSync } from "child_process";
|
|
8
|
+
import prompts from "prompts";
|
|
9
|
+
import { logger } from "@democratize-quality/qualitylens-core/utils/logger";
|
|
10
|
+
import { Config } from "@democratize-quality/qualitylens-core/core/config";
|
|
11
|
+
import { detect, suggestAreas } from "@democratize-quality/qualitylens-core/core/detector";
|
|
12
|
+
import { CoverageEngine } from "@democratize-quality/qualitylens-core/core/engine";
|
|
13
|
+
import { PlaywrightSource } from "@democratize-quality/qualitylens-core/sources/playwright.source";
|
|
14
|
+
import { YamlSource } from "@democratize-quality/qualitylens-core/sources/yaml.source";
|
|
15
|
+
import { RoutesSource } from "@democratize-quality/qualitylens-core/sources/routes.source";
|
|
16
|
+
import { FuzzyMatcher } from "@democratize-quality/qualitylens-core/matchers/fuzzy.matcher";
|
|
17
|
+
import { AreaMatcher } from "@democratize-quality/qualitylens-core/matchers/area.matcher";
|
|
18
|
+
import { ConsoleReporter } from "@democratize-quality/qualitylens-core/reporters/console.reporter";
|
|
19
|
+
var program = new Command();
|
|
20
|
+
program.name("qualitylens").description("Test coverage map \u2014 automated + manual, by functional area").version("0.1.0");
|
|
21
|
+
program.command("scan").description("Scan the project and generate a coverage report").option("-c, --config <path>", "path to qualitylens.yaml", "./qualitylens.yaml").option("-o, --output <path>", "output directory", "./").option("-f, --format <formats>", "output formats: console,html,json,markdown (pro formats require qualitylens-pro)", "console").option("--since <branch>", "only flag routes changed since this git branch").option("--fail-under <n>", "exit code 1 if total coverage < n (CI gate)", parseFloat).action(async (opts) => {
|
|
22
|
+
const outputDir = path.resolve(opts.output);
|
|
23
|
+
logger.configure(outputDir);
|
|
24
|
+
logger.info("scan started", { config: opts.config, format: opts.format, output: opts.output });
|
|
25
|
+
let pro = null;
|
|
26
|
+
try {
|
|
27
|
+
const proPackage = "@democratize-quality/qualitylens-pro";
|
|
28
|
+
pro = await import(proPackage);
|
|
29
|
+
} catch {
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
const config = Config.load(opts.config);
|
|
33
|
+
logger.info("config loaded", { projectName: config.projectName, routesType: config.routes.type });
|
|
34
|
+
const configDir = path.dirname(path.resolve(opts.config));
|
|
35
|
+
if (config.routes.generate) {
|
|
36
|
+
console.log(` [routes] running generate command: ${config.routes.generate}`);
|
|
37
|
+
logger.info("[routes] running generate command", { command: config.routes.generate, cwd: configDir });
|
|
38
|
+
try {
|
|
39
|
+
execSync(config.routes.generate, {
|
|
40
|
+
stdio: "inherit",
|
|
41
|
+
cwd: configDir
|
|
42
|
+
});
|
|
43
|
+
logger.info("[routes] generate command completed successfully");
|
|
44
|
+
} catch (err) {
|
|
45
|
+
logger.error("[routes] generate command failed", err);
|
|
46
|
+
const exitCode = err.status ?? 1;
|
|
47
|
+
throw new Error(
|
|
48
|
+
`Generate command failed (exit ${exitCode}): ${config.routes.generate}
|
|
49
|
+
|
|
50
|
+
Common causes:
|
|
51
|
+
\u2022 Script file does not exist \u2014 check the path in routes.generate
|
|
52
|
+
\u2022 Missing dependencies \u2014 run 'npm install' in the project directory
|
|
53
|
+
\u2022 TypeScript errors in the generate script
|
|
54
|
+
|
|
55
|
+
The full error output is shown above.
|
|
56
|
+
Full details logged to: ${logger.filePath}`
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const testsDir = config.tests?.path ? path.resolve(configDir, config.tests.path) : configDir;
|
|
61
|
+
logger.info("test directory resolved", { testsDir });
|
|
62
|
+
const sources = [
|
|
63
|
+
new PlaywrightSource(testsDir),
|
|
64
|
+
new YamlSource(opts.config),
|
|
65
|
+
...config.ado && pro ? [pro.createAdoSource(config.ado)] : []
|
|
66
|
+
];
|
|
67
|
+
const engine = new CoverageEngine(
|
|
68
|
+
sources,
|
|
69
|
+
new RoutesSource(config.routes),
|
|
70
|
+
new FuzzyMatcher(),
|
|
71
|
+
new AreaMatcher(config.areas),
|
|
72
|
+
config
|
|
73
|
+
);
|
|
74
|
+
const report = await engine.run();
|
|
75
|
+
logger.info("scan complete", {
|
|
76
|
+
totalRoutes: report.summary.totalRoutes,
|
|
77
|
+
totalCoverage: report.summary.totalCoverage,
|
|
78
|
+
orphanedTests: report.unmatchedTests.length
|
|
79
|
+
});
|
|
80
|
+
const formats = opts.format.split(",").map((f) => f.trim());
|
|
81
|
+
await new ConsoleReporter().write(report, opts.output);
|
|
82
|
+
for (const format of formats) {
|
|
83
|
+
if (format === "console") {
|
|
84
|
+
} else if (pro) {
|
|
85
|
+
const reporter = pro.getReporter(format);
|
|
86
|
+
if (reporter) {
|
|
87
|
+
await reporter.write(report, opts.output);
|
|
88
|
+
} else {
|
|
89
|
+
console.warn(`Unknown format: ${format}`);
|
|
90
|
+
}
|
|
91
|
+
} else {
|
|
92
|
+
console.warn(`
|
|
93
|
+
Format '${format}' requires qualitylens-pro. Install: npm install @democratize-quality/qualitylens-pro`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (opts.failUnder && report.summary.totalCoverage < opts.failUnder) {
|
|
97
|
+
logger.warn("coverage below threshold", { actual: report.summary.totalCoverage, threshold: opts.failUnder });
|
|
98
|
+
console.error(`
|
|
99
|
+
\u274C Coverage ${report.summary.totalCoverage}% is below threshold ${opts.failUnder}%`);
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
logger.close();
|
|
103
|
+
} catch (err) {
|
|
104
|
+
const message = err.message ?? String(err);
|
|
105
|
+
logger.error("scan failed", err);
|
|
106
|
+
logger.close();
|
|
107
|
+
console.error(`
|
|
108
|
+
\u274C qualitylens scan failed: ${message}`);
|
|
109
|
+
if (logger.filePath) {
|
|
110
|
+
console.error(` Full details: ${logger.filePath}`);
|
|
111
|
+
}
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
program.command("init [directory]").description("Auto-detect project and create qualitylens.yaml (defaults to current directory)").option("-y, --yes", "accept all auto-detected defaults without prompting (for CI)").option("--base-path <prefix>", "base path prefix to strip when grouping routes (e.g. /api, /api/v1)").option("--test-path <path>", "path to test directory containing spec files (e.g. ./e2e, ./tests)").action(async (directory, opts = {}) => {
|
|
116
|
+
const ci = !!opts.yes;
|
|
117
|
+
const targetDir = path.resolve(directory ?? ".");
|
|
118
|
+
const configPath = path.join(targetDir, "qualitylens.yaml");
|
|
119
|
+
if (!fs.existsSync(targetDir)) {
|
|
120
|
+
console.error(`\u274C Directory not found: ${targetDir}`);
|
|
121
|
+
process.exit(1);
|
|
122
|
+
}
|
|
123
|
+
if (fs.existsSync(configPath)) {
|
|
124
|
+
if (ci) {
|
|
125
|
+
console.log(` qualitylens.yaml already exists \u2014 overwriting (--yes)`);
|
|
126
|
+
} else {
|
|
127
|
+
const { overwrite } = await prompts({
|
|
128
|
+
type: "confirm",
|
|
129
|
+
name: "overwrite",
|
|
130
|
+
message: `qualitylens.yaml already exists in ${targetDir}. Overwrite?`,
|
|
131
|
+
initial: false
|
|
132
|
+
});
|
|
133
|
+
if (!overwrite) {
|
|
134
|
+
console.log("Aborted.");
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
console.log(`
|
|
140
|
+
Analysing ${targetDir} ...
|
|
141
|
+
`);
|
|
142
|
+
const detected = await detect(targetDir);
|
|
143
|
+
const resolvedBasePath = opts.basePath ?? detected.basePath;
|
|
144
|
+
const frameworkLabel = {
|
|
145
|
+
"nextjs": "Next.js",
|
|
146
|
+
"express-openapi": "Express + swagger-jsdoc",
|
|
147
|
+
"express": "Express",
|
|
148
|
+
"unknown": "Unknown framework"
|
|
149
|
+
};
|
|
150
|
+
console.log(` Detected: ${frameworkLabel[detected.framework]}`);
|
|
151
|
+
if (detected.routesPath) {
|
|
152
|
+
console.log(` Routes: ${detected.routesPath}`);
|
|
153
|
+
}
|
|
154
|
+
if (resolvedBasePath) {
|
|
155
|
+
console.log(` Base path: ${resolvedBasePath}${opts.basePath ? " (from --base-path)" : " (auto-detected)"}`);
|
|
156
|
+
}
|
|
157
|
+
if (detected.discoveredRoutes.length > 0) {
|
|
158
|
+
console.log(` Found ${detected.discoveredRoutes.length} route(s) across ${detected.suggestedAreas.length} area(s)
|
|
159
|
+
`);
|
|
160
|
+
}
|
|
161
|
+
let finalBasePath;
|
|
162
|
+
let finalTestsPath;
|
|
163
|
+
let finalAreas;
|
|
164
|
+
let routeType = detected.routeType;
|
|
165
|
+
let routesPath = detected.routesPath ?? "./routes.json";
|
|
166
|
+
let projectName;
|
|
167
|
+
let selectedType;
|
|
168
|
+
let selectedPath;
|
|
169
|
+
const specFileExists = detected.routeType === "openapi" && detected.routesPath != null && fs.existsSync(path.join(targetDir, detected.routesPath.replace("./", "")));
|
|
170
|
+
if (ci) {
|
|
171
|
+
projectName = detected.projectName;
|
|
172
|
+
finalBasePath = opts.basePath ?? resolvedBasePath;
|
|
173
|
+
finalTestsPath = opts.testPath ?? detected.testsPath;
|
|
174
|
+
finalAreas = suggestAreas(detected.discoveredRoutes, finalBasePath);
|
|
175
|
+
if (detected.framework === "unknown") {
|
|
176
|
+
selectedType = "manual";
|
|
177
|
+
selectedPath = "./routes.json";
|
|
178
|
+
}
|
|
179
|
+
} else {
|
|
180
|
+
const batch1 = [
|
|
181
|
+
{
|
|
182
|
+
type: "text",
|
|
183
|
+
name: "projectName",
|
|
184
|
+
message: "Project name",
|
|
185
|
+
initial: detected.projectName
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
type: "text",
|
|
189
|
+
name: "basePath",
|
|
190
|
+
message: "Base path prefix to strip when grouping routes (leave blank for none)",
|
|
191
|
+
initial: resolvedBasePath ?? ""
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
type: "text",
|
|
195
|
+
name: "testsPath",
|
|
196
|
+
message: "Path to test directory containing spec files (leave blank for auto-detect)",
|
|
197
|
+
initial: detected.testsPath ?? ""
|
|
198
|
+
}
|
|
199
|
+
];
|
|
200
|
+
if (detected.framework === "unknown") {
|
|
201
|
+
batch1.push(
|
|
202
|
+
{
|
|
203
|
+
type: "select",
|
|
204
|
+
name: "selectedType",
|
|
205
|
+
message: "Could not detect framework. How are your routes defined?",
|
|
206
|
+
choices: [
|
|
207
|
+
{ title: "Next.js (pages/ or app/ directory)", value: "nextjs" },
|
|
208
|
+
{ title: "OpenAPI / Swagger spec file", value: "openapi" },
|
|
209
|
+
{ title: "Express routes manifest (JSON)", value: "express" },
|
|
210
|
+
{ title: "Manual list", value: "manual" }
|
|
211
|
+
]
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
type: "text",
|
|
215
|
+
name: "selectedPath",
|
|
216
|
+
message: "Path to routes directory or file",
|
|
217
|
+
initial: "./src/app"
|
|
218
|
+
}
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
const b1 = await prompts(batch1, {
|
|
222
|
+
onCancel: () => {
|
|
223
|
+
console.log("\nAborted.");
|
|
224
|
+
process.exit(0);
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
if (!b1.projectName) {
|
|
228
|
+
console.log("Aborted.");
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
projectName = b1.projectName;
|
|
232
|
+
finalBasePath = opts.basePath ?? (b1.basePath?.trim() || null);
|
|
233
|
+
finalTestsPath = opts.testPath ?? (b1.testsPath?.trim() || null);
|
|
234
|
+
selectedType = b1.selectedType;
|
|
235
|
+
selectedPath = b1.selectedPath;
|
|
236
|
+
finalAreas = suggestAreas(detected.discoveredRoutes, finalBasePath);
|
|
237
|
+
if (finalAreas.length > 0) {
|
|
238
|
+
console.log(`
|
|
239
|
+
Suggested areas \u2014 press Enter to accept, or type a new name:
|
|
240
|
+
`);
|
|
241
|
+
const batch2 = finalAreas.map((area, i) => {
|
|
242
|
+
const routeList = area.routes.map((r) => r.path).join(", ");
|
|
243
|
+
return {
|
|
244
|
+
type: "text",
|
|
245
|
+
name: `area_${i}`,
|
|
246
|
+
message: ` ${routeList.padEnd(45)} \u2192 area name`,
|
|
247
|
+
initial: area.suggestedName
|
|
248
|
+
};
|
|
249
|
+
});
|
|
250
|
+
const b2 = await prompts(batch2, {
|
|
251
|
+
onCancel: () => {
|
|
252
|
+
console.log("\nAborted.");
|
|
253
|
+
process.exit(0);
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
finalAreas = finalAreas.map((area, i) => ({
|
|
257
|
+
...area,
|
|
258
|
+
suggestedName: (b2[`area_${i}`] ?? area.suggestedName).trim()
|
|
259
|
+
})).filter((a) => a.suggestedName);
|
|
260
|
+
} else if (detected.framework === "express-openapi" && detected.discoveredRoutes.length === 0) {
|
|
261
|
+
console.log(` Routes cannot be discovered until openapi.json is generated.`);
|
|
262
|
+
console.log(` A placeholder area will be added \u2014 update it after running 'qualitylens scan'.
|
|
263
|
+
`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
const confirmedAreas = finalAreas.filter((a) => a.suggestedName).map((a) => ({ name: a.suggestedName, patterns: a.patterns }));
|
|
267
|
+
if (detected.framework === "unknown") {
|
|
268
|
+
if (!ci && (!selectedType || !selectedPath)) {
|
|
269
|
+
console.log("Aborted.");
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
routeType = selectedType ?? "manual";
|
|
273
|
+
routesPath = selectedPath ?? "./routes.json";
|
|
274
|
+
}
|
|
275
|
+
const content = Config.generateStarter({
|
|
276
|
+
projectName,
|
|
277
|
+
routeType,
|
|
278
|
+
routesPath,
|
|
279
|
+
generate: detected.generate,
|
|
280
|
+
omitGenerate: specFileExists,
|
|
281
|
+
basePath: finalBasePath,
|
|
282
|
+
testsPath: finalTestsPath,
|
|
283
|
+
areas: confirmedAreas
|
|
284
|
+
});
|
|
285
|
+
fs.writeFileSync(configPath, content, "utf-8");
|
|
286
|
+
console.log(`
|
|
287
|
+
\u2705 qualitylens.yaml created at ${configPath}`);
|
|
288
|
+
if (detected.framework === "express-openapi") {
|
|
289
|
+
console.log(`
|
|
290
|
+
Next steps:`);
|
|
291
|
+
console.log(` 1. Export swaggerSpec from your swagger.ts`);
|
|
292
|
+
console.log(` 2. Create src/generate-openapi.ts (see docs/CONFIGURATION.md)`);
|
|
293
|
+
console.log(` 3. Update the generate: line in qualitylens.yaml`);
|
|
294
|
+
console.log(` 4. Run 'qualitylens scan' to generate your first report`);
|
|
295
|
+
} else {
|
|
296
|
+
console.log(` Run 'qualitylens scan' to generate your first coverage report.`);
|
|
297
|
+
}
|
|
298
|
+
console.log();
|
|
299
|
+
});
|
|
300
|
+
program.command("validate").description("Validate qualitylens.yaml and test ADO connection if configured").option("-c, --config <path>", "path to qualitylens.yaml", "./qualitylens.yaml").action(async (opts) => {
|
|
301
|
+
logger.configure(path.dirname(path.resolve(opts.config)));
|
|
302
|
+
logger.info("validate started", { config: opts.config });
|
|
303
|
+
try {
|
|
304
|
+
const config = Config.load(opts.config);
|
|
305
|
+
console.log(`\u2705 Config valid: ${config.projectName}`);
|
|
306
|
+
console.log(` Routes: ${config.routes.type} \u2192 ${config.routes.path}`);
|
|
307
|
+
console.log(` Areas: ${config.areas.map((a) => a.name).join(", ")}`);
|
|
308
|
+
console.log(` Stale threshold: ${config.staleThresholdDays} days`);
|
|
309
|
+
logger.info("config loaded successfully", { projectName: config.projectName });
|
|
310
|
+
if (config.ado) {
|
|
311
|
+
const pat = process.env[config.ado.patEnvVar];
|
|
312
|
+
if (!pat) {
|
|
313
|
+
const msg = `ADO PAT env var '${config.ado.patEnvVar}' is not set`;
|
|
314
|
+
logger.error(msg);
|
|
315
|
+
console.error(`\u274C ${msg}`);
|
|
316
|
+
console.error(` Set it with: export ${config.ado.patEnvVar}=<your-pat>`);
|
|
317
|
+
logger.close();
|
|
318
|
+
process.exit(1);
|
|
319
|
+
}
|
|
320
|
+
console.log(`\u2705 ADO PAT found in ${config.ado.patEnvVar}`);
|
|
321
|
+
process.stdout.write(` Testing ADO connection to ${config.ado.orgUrl} ...`);
|
|
322
|
+
try {
|
|
323
|
+
const { httpGet } = await import("@democratize-quality/qualitylens-core/utils/http");
|
|
324
|
+
const authHeader = "Basic " + Buffer.from(":" + pat).toString("base64");
|
|
325
|
+
await httpGet(
|
|
326
|
+
`${config.ado.orgUrl}/_apis/projects/${encodeURIComponent(config.ado.project)}?api-version=7.0`,
|
|
327
|
+
{ Authorization: authHeader }
|
|
328
|
+
);
|
|
329
|
+
console.log(" \u2705");
|
|
330
|
+
console.log(` Project '${config.ado.project}' is accessible`);
|
|
331
|
+
logger.info("ADO connection successful", { orgUrl: config.ado.orgUrl, project: config.ado.project });
|
|
332
|
+
} catch (err) {
|
|
333
|
+
console.log(" \u274C");
|
|
334
|
+
logger.error("ADO connection failed", err);
|
|
335
|
+
console.error(` ADO connection failed: ${err.message}`);
|
|
336
|
+
console.error(` Check: org URL, project name, and PAT permissions (Test Management: Read)`);
|
|
337
|
+
if (logger.filePath) console.error(` Full details: ${logger.filePath}`);
|
|
338
|
+
logger.close();
|
|
339
|
+
process.exit(1);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
logger.close();
|
|
343
|
+
} catch (err) {
|
|
344
|
+
logger.error("validate failed", err);
|
|
345
|
+
logger.close();
|
|
346
|
+
console.error(`\u274C ${err.message}`);
|
|
347
|
+
if (logger.filePath) console.error(` Full details: ${logger.filePath}`);
|
|
348
|
+
process.exit(1);
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
(async () => {
|
|
352
|
+
try {
|
|
353
|
+
const proPackage = "@democratize-quality/qualitylens-pro";
|
|
354
|
+
const pro = await import(proPackage);
|
|
355
|
+
if (typeof pro.registerCommands === "function") {
|
|
356
|
+
pro.registerCommands(program);
|
|
357
|
+
}
|
|
358
|
+
} catch {
|
|
359
|
+
}
|
|
360
|
+
program.parse();
|
|
361
|
+
})();
|
|
362
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * src/index.ts — CLI entry point\n * Commands: scan, init, validate\n *\n * AI INSTRUCTIONS:\n * The skeleton is here. Implement the three commands following the comments.\n * Do not change command names or flag names — these are the public API.\n */\n\nimport { Command } from 'commander'\nimport * as fs from 'fs'\nimport * as path from 'path'\nimport { execSync } from 'child_process'\nimport prompts, { PromptObject } from 'prompts'\nimport { logger } from '@democratize-quality/qualitylens-core/utils/logger'\nimport { Config } from '@democratize-quality/qualitylens-core/core/config'\nimport { detect, suggestAreas } from '@democratize-quality/qualitylens-core/core/detector'\nimport { CoverageEngine } from '@democratize-quality/qualitylens-core/core/engine'\nimport { PlaywrightSource } from '@democratize-quality/qualitylens-core/sources/playwright.source'\nimport { YamlSource } from '@democratize-quality/qualitylens-core/sources/yaml.source'\nimport { RoutesSource } from '@democratize-quality/qualitylens-core/sources/routes.source'\nimport { FuzzyMatcher } from '@democratize-quality/qualitylens-core/matchers/fuzzy.matcher'\nimport { AreaMatcher } from '@democratize-quality/qualitylens-core/matchers/area.matcher'\nimport { ConsoleReporter } from '@democratize-quality/qualitylens-core/reporters/console.reporter'\nimport type { BaseReporter } from '@democratize-quality/qualitylens-core/reporters/base.reporter'\n\ninterface ProModule {\n SUPPORTED_FORMATS: string[]\n getReporter(format: string): BaseReporter | null\n createAdoSource(config: any): InstanceType<typeof import('@democratize-quality/qualitylens-core/sources/base.source').BaseSource>\n registerCommands(program: typeof import('commander').Command.prototype): void\n}\n\nconst program = new Command()\n\nprogram\n .name('qualitylens')\n .description('Test coverage map — automated + manual, by functional area')\n .version('0.1.0')\n\n// ─── scan ──────────────────────────────────────────────────────────────────\n\nprogram\n .command('scan')\n .description('Scan the project and generate a coverage report')\n .option('-c, --config <path>', 'path to qualitylens.yaml', './qualitylens.yaml')\n .option('-o, --output <path>', 'output directory', './')\n .option('-f, --format <formats>', 'output formats: console,html,json,markdown (pro formats require qualitylens-pro)', 'console')\n .option('--since <branch>', 'only flag routes changed since this git branch')\n .option('--fail-under <n>', 'exit code 1 if total coverage < n (CI gate)', parseFloat)\n .action(async (opts) => {\n const outputDir = path.resolve(opts.output)\n logger.configure(outputDir)\n logger.info('scan started', { config: opts.config, format: opts.format, output: opts.output })\n\n // Attempt to load pro features — silently skip if not installed\n // Variable path prevents TypeScript from statically resolving the module during DTS generation,\n // breaking the build-time dependency on qualitylens-pro.\n let pro: ProModule | null = null\n try {\n const proPackage = '@democratize-quality/qualitylens-pro'\n pro = await import(proPackage) as unknown as ProModule\n } catch {\n // qualitylens-pro not installed — console-only mode\n }\n\n try {\n const config = Config.load(opts.config)\n logger.info('config loaded', { projectName: config.projectName, routesType: config.routes.type })\n\n const configDir = path.dirname(path.resolve(opts.config))\n\n if (config.routes.generate) {\n console.log(` [routes] running generate command: ${config.routes.generate}`)\n logger.info('[routes] running generate command', { command: config.routes.generate, cwd: configDir })\n try {\n execSync(config.routes.generate, {\n stdio: 'inherit',\n cwd: configDir,\n })\n logger.info('[routes] generate command completed successfully')\n } catch (err: any) {\n logger.error('[routes] generate command failed', err)\n const exitCode = err.status ?? 1\n throw new Error(\n `Generate command failed (exit ${exitCode}): ${config.routes.generate}\\n` +\n `\\n Common causes:\\n` +\n ` • Script file does not exist — check the path in routes.generate\\n` +\n ` • Missing dependencies — run 'npm install' in the project directory\\n` +\n ` • TypeScript errors in the generate script\\n` +\n `\\n The full error output is shown above.\\n` +\n ` Full details logged to: ${logger.filePath}`\n )\n }\n }\n\n const testsDir = config.tests?.path\n ? path.resolve(configDir, config.tests.path)\n : configDir\n logger.info('test directory resolved', { testsDir })\n\n const sources = [\n new PlaywrightSource(testsDir),\n new YamlSource(opts.config),\n ...(config.ado && pro ? [pro.createAdoSource(config.ado)] : []),\n ]\n\n const engine = new CoverageEngine(\n sources,\n new RoutesSource(config.routes),\n new FuzzyMatcher(),\n new AreaMatcher(config.areas),\n config\n )\n\n const report = await engine.run()\n logger.info('scan complete', {\n totalRoutes: report.summary.totalRoutes,\n totalCoverage: report.summary.totalCoverage,\n orphanedTests: report.unmatchedTests.length,\n })\n\n const formats = opts.format.split(',').map((f: string) => f.trim())\n\n // Console reporter always runs\n await new ConsoleReporter().write(report, opts.output)\n\n for (const format of formats) {\n if (format === 'console') {\n // already ran above\n } else if (pro) {\n const reporter = pro.getReporter(format)\n if (reporter) {\n await reporter.write(report, opts.output)\n } else {\n console.warn(`Unknown format: ${format}`)\n }\n } else {\n console.warn(`\\n Format '${format}' requires qualitylens-pro. Install: npm install @democratize-quality/qualitylens-pro`)\n }\n }\n\n if (opts.failUnder && report.summary.totalCoverage < opts.failUnder) {\n logger.warn('coverage below threshold', { actual: report.summary.totalCoverage, threshold: opts.failUnder })\n console.error(`\\n❌ Coverage ${report.summary.totalCoverage}% is below threshold ${opts.failUnder}%`)\n process.exit(1)\n }\n\n logger.close()\n } catch (err) {\n const message = (err as Error).message ?? String(err)\n logger.error('scan failed', err)\n logger.close()\n console.error(`\\n❌ qualitylens scan failed: ${message}`)\n if (logger.filePath) {\n console.error(` Full details: ${logger.filePath}`)\n }\n process.exit(1)\n }\n })\n\n// ─── init ──────────────────────────────────────────────────────────────────\n\nprogram\n .command('init [directory]')\n .description('Auto-detect project and create qualitylens.yaml (defaults to current directory)')\n .option('-y, --yes', 'accept all auto-detected defaults without prompting (for CI)')\n .option('--base-path <prefix>', 'base path prefix to strip when grouping routes (e.g. /api, /api/v1)')\n .option('--test-path <path>', 'path to test directory containing spec files (e.g. ./e2e, ./tests)')\n .action(async (directory?: string, opts: { yes?: boolean; basePath?: string; testPath?: string } = {}) => {\n const ci = !!opts.yes\n\n const targetDir = path.resolve(directory ?? '.')\n const configPath = path.join(targetDir, 'qualitylens.yaml')\n\n if (!fs.existsSync(targetDir)) {\n console.error(`❌ Directory not found: ${targetDir}`)\n process.exit(1)\n }\n\n if (fs.existsSync(configPath)) {\n if (ci) {\n console.log(` qualitylens.yaml already exists — overwriting (--yes)`)\n } else {\n const { overwrite } = await prompts({\n type: 'confirm',\n name: 'overwrite',\n message: `qualitylens.yaml already exists in ${targetDir}. Overwrite?`,\n initial: false,\n })\n if (!overwrite) { console.log('Aborted.'); return }\n }\n }\n\n console.log(`\\n Analysing ${targetDir} ...\\n`)\n const detected = await detect(targetDir)\n\n const resolvedBasePath = opts.basePath ?? detected.basePath\n\n const frameworkLabel: Record<string, string> = {\n 'nextjs': 'Next.js',\n 'express-openapi': 'Express + swagger-jsdoc',\n 'express': 'Express',\n 'unknown': 'Unknown framework',\n }\n console.log(` Detected: ${frameworkLabel[detected.framework]}`)\n\n if (detected.routesPath) {\n console.log(` Routes: ${detected.routesPath}`)\n }\n if (resolvedBasePath) {\n console.log(` Base path: ${resolvedBasePath}${opts.basePath ? ' (from --base-path)' : ' (auto-detected)'}`)\n }\n if (detected.discoveredRoutes.length > 0) {\n console.log(` Found ${detected.discoveredRoutes.length} route(s) across ${detected.suggestedAreas.length} area(s)\\n`)\n }\n\n type PromptAnswers = Record<string, string>\n\n let finalBasePath: string | null\n let finalTestsPath: string | null\n let finalAreas: typeof detected.suggestedAreas\n let routeType = detected.routeType\n let routesPath = detected.routesPath ?? './routes.json'\n let projectName: string\n let selectedType: string | undefined\n let selectedPath: string | undefined\n\n // Spec file exists on disk → no generate step needed\n const specFileExists = detected.routeType === 'openapi'\n && detected.routesPath != null\n && fs.existsSync(path.join(targetDir, detected.routesPath.replace('./', '')))\n\n if (ci) {\n projectName = detected.projectName\n finalBasePath = opts.basePath ?? resolvedBasePath\n finalTestsPath = opts.testPath ?? detected.testsPath\n finalAreas = suggestAreas(detected.discoveredRoutes, finalBasePath)\n if (detected.framework === 'unknown') {\n selectedType = 'manual'\n selectedPath = './routes.json'\n }\n } else {\n const batch1: PromptObject[] = [\n {\n type: 'text',\n name: 'projectName',\n message: 'Project name',\n initial: detected.projectName,\n },\n {\n type: 'text',\n name: 'basePath',\n message: 'Base path prefix to strip when grouping routes (leave blank for none)',\n initial: resolvedBasePath ?? '',\n },\n {\n type: 'text',\n name: 'testsPath',\n message: 'Path to test directory containing spec files (leave blank for auto-detect)',\n initial: detected.testsPath ?? '',\n },\n ]\n\n if (detected.framework === 'unknown') {\n batch1.push(\n {\n type: 'select',\n name: 'selectedType',\n message: 'Could not detect framework. How are your routes defined?',\n choices: [\n { title: 'Next.js (pages/ or app/ directory)', value: 'nextjs' },\n { title: 'OpenAPI / Swagger spec file', value: 'openapi' },\n { title: 'Express routes manifest (JSON)', value: 'express' },\n { title: 'Manual list', value: 'manual' },\n ],\n } as PromptObject,\n {\n type: 'text',\n name: 'selectedPath',\n message: 'Path to routes directory or file',\n initial: './src/app',\n }\n )\n }\n\n const b1: PromptAnswers = await prompts(batch1, {\n onCancel: () => { console.log('\\nAborted.'); process.exit(0) },\n })\n if (!b1.projectName) { console.log('Aborted.'); return }\n\n projectName = b1.projectName\n finalBasePath = opts.basePath ?? (b1.basePath?.trim() || null)\n finalTestsPath = opts.testPath ?? (b1.testsPath?.trim() || null)\n selectedType = b1.selectedType\n selectedPath = b1.selectedPath\n\n finalAreas = suggestAreas(detected.discoveredRoutes, finalBasePath)\n\n if (finalAreas.length > 0) {\n console.log(`\\n Suggested areas — press Enter to accept, or type a new name:\\n`)\n\n const batch2: PromptObject[] = finalAreas.map((area, i) => {\n const routeList = area.routes.map(r => r.path).join(', ')\n return {\n type: 'text',\n name: `area_${i}`,\n message: ` ${routeList.padEnd(45)} → area name`,\n initial: area.suggestedName,\n }\n })\n\n const b2: PromptAnswers = await prompts(batch2, {\n onCancel: () => { console.log('\\nAborted.'); process.exit(0) },\n })\n\n finalAreas = finalAreas.map((area, i) => ({\n ...area,\n suggestedName: (b2[`area_${i}`] ?? area.suggestedName).trim(),\n })).filter(a => a.suggestedName)\n\n } else if (detected.framework === 'express-openapi' && detected.discoveredRoutes.length === 0) {\n console.log(` Routes cannot be discovered until openapi.json is generated.`)\n console.log(` A placeholder area will be added — update it after running 'qualitylens scan'.\\n`)\n }\n }\n\n const confirmedAreas: Array<{ name: string; patterns: string[] }> = finalAreas\n .filter(a => a.suggestedName)\n .map(a => ({ name: a.suggestedName, patterns: a.patterns }))\n\n if (detected.framework === 'unknown') {\n if (!ci && (!selectedType || !selectedPath)) { console.log('Aborted.'); return }\n routeType = (selectedType ?? 'manual') as typeof routeType\n routesPath = selectedPath ?? './routes.json'\n }\n\n const content = Config.generateStarter({\n projectName,\n routeType,\n routesPath,\n generate: detected.generate,\n omitGenerate: specFileExists,\n basePath: finalBasePath,\n testsPath: finalTestsPath,\n areas: confirmedAreas,\n })\n\n fs.writeFileSync(configPath, content, 'utf-8')\n console.log(`\\n✅ qualitylens.yaml created at ${configPath}`)\n\n if (detected.framework === 'express-openapi') {\n console.log(`\\n Next steps:`)\n console.log(` 1. Export swaggerSpec from your swagger.ts`)\n console.log(` 2. Create src/generate-openapi.ts (see docs/CONFIGURATION.md)`)\n console.log(` 3. Update the generate: line in qualitylens.yaml`)\n console.log(` 4. Run 'qualitylens scan' to generate your first report`)\n } else {\n console.log(` Run 'qualitylens scan' to generate your first coverage report.`)\n }\n console.log()\n })\n\n// ─── validate ──────────────────────────────────────────────────────────────\n\nprogram\n .command('validate')\n .description('Validate qualitylens.yaml and test ADO connection if configured')\n .option('-c, --config <path>', 'path to qualitylens.yaml', './qualitylens.yaml')\n .action(async (opts) => {\n logger.configure(path.dirname(path.resolve(opts.config)))\n logger.info('validate started', { config: opts.config })\n\n try {\n const config = Config.load(opts.config)\n console.log(`✅ Config valid: ${config.projectName}`)\n console.log(` Routes: ${config.routes.type} → ${config.routes.path}`)\n console.log(` Areas: ${config.areas.map(a => a.name).join(', ')}`)\n console.log(` Stale threshold: ${config.staleThresholdDays} days`)\n logger.info('config loaded successfully', { projectName: config.projectName })\n\n if (config.ado) {\n const pat = process.env[config.ado.patEnvVar]\n if (!pat) {\n const msg = `ADO PAT env var '${config.ado.patEnvVar}' is not set`\n logger.error(msg)\n console.error(`❌ ${msg}`)\n console.error(` Set it with: export ${config.ado.patEnvVar}=<your-pat>`)\n logger.close()\n process.exit(1)\n }\n console.log(`✅ ADO PAT found in ${config.ado.patEnvVar}`)\n\n process.stdout.write(` Testing ADO connection to ${config.ado.orgUrl} ...`)\n try {\n const { httpGet } = await import('@democratize-quality/qualitylens-core/utils/http')\n const authHeader = 'Basic ' + Buffer.from(':' + pat).toString('base64')\n await httpGet(\n `${config.ado.orgUrl}/_apis/projects/${encodeURIComponent(config.ado.project)}?api-version=7.0`,\n { Authorization: authHeader }\n )\n console.log(' ✅')\n console.log(` Project '${config.ado.project}' is accessible`)\n logger.info('ADO connection successful', { orgUrl: config.ado.orgUrl, project: config.ado.project })\n } catch (err) {\n console.log(' ❌')\n logger.error('ADO connection failed', err)\n console.error(` ADO connection failed: ${(err as Error).message}`)\n console.error(` Check: org URL, project name, and PAT permissions (Test Management: Read)`)\n if (logger.filePath) console.error(` Full details: ${logger.filePath}`)\n logger.close()\n process.exit(1)\n }\n }\n\n logger.close()\n } catch (err) {\n logger.error('validate failed', err)\n logger.close()\n console.error(`❌ ${(err as Error).message}`)\n if (logger.filePath) console.error(` Full details: ${logger.filePath}`)\n process.exit(1)\n }\n })\n\n// Load pro commands before parsing so they appear in --help and tab-completion\n;(async () => {\n try {\n const proPackage = '@democratize-quality/qualitylens-pro'\n const pro = await import(proPackage) as unknown as ProModule\n if (typeof pro.registerCommands === 'function') {\n pro.registerCommands(program)\n }\n } catch {\n // qualitylens-pro not installed — pro commands unavailable\n }\n program.parse()\n})()\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@democratize-quality/qualitylens",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Test coverage map for Playwright automated tests, by functional area",
|
|
5
|
+
"author": "Raj Uppadhyay",
|
|
6
|
+
"license": "AGPL-3.0-only",
|
|
7
|
+
"bin": {
|
|
8
|
+
"qualitylens": "./dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"main": "./dist/index.js",
|
|
11
|
+
"module": "./dist/index.mjs",
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"import": "./dist/index.mjs",
|
|
17
|
+
"require": "./dist/index.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsup",
|
|
25
|
+
"dev": "tsup --watch",
|
|
26
|
+
"lint": "tsc --noEmit"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"testing",
|
|
30
|
+
"playwright",
|
|
31
|
+
"qa",
|
|
32
|
+
"coverage",
|
|
33
|
+
"test-coverage"
|
|
34
|
+
],
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@democratize-quality/qualitylens-core": "*",
|
|
37
|
+
"chalk": "^5.3.0",
|
|
38
|
+
"commander": "^12.0.0",
|
|
39
|
+
"ora": "^8.0.1",
|
|
40
|
+
"prompts": "^2.4.2"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@types/node": "^20.0.0",
|
|
44
|
+
"@types/prompts": "^2.4.9",
|
|
45
|
+
"tsup": "^8.0.0",
|
|
46
|
+
"typescript": "^5.4.0"
|
|
47
|
+
},
|
|
48
|
+
"engines": {
|
|
49
|
+
"node": ">=20.0.0"
|
|
50
|
+
},
|
|
51
|
+
"publishConfig": {
|
|
52
|
+
"registry": "https://registry.npmjs.org",
|
|
53
|
+
"access": "public"
|
|
54
|
+
}
|
|
55
|
+
}
|