flaglint 0.8.0 → 0.9.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/bin/flaglint.js +33 -13
- package/package.json +14 -4
package/dist/bin/flaglint.js
CHANGED
|
@@ -789,7 +789,7 @@ function formatHTML(result, options) {
|
|
|
789
789
|
<div class="card"><div class="card-num orange">${manualCount}</div><div class="card-label">Manual Review (${manualPct}%)</div></div>
|
|
790
790
|
<div class="card"><div class="card-num blue">${detailBulkCount}</div><div class="card-label">Detail/Bulk Calls</div></div>` : "";
|
|
791
791
|
const title = options.title ? esc(options.title) : "FlagLint Scan Report";
|
|
792
|
-
const version = true ? "0.
|
|
792
|
+
const version = true ? "0.9.0" : "0.1.0";
|
|
793
793
|
return `<!DOCTYPE html>
|
|
794
794
|
<html lang="en">
|
|
795
795
|
<head>
|
|
@@ -1322,7 +1322,7 @@ function formatMigrationReport(analysis) {
|
|
|
1322
1322
|
unsupportedUnknownCount
|
|
1323
1323
|
} = analysis;
|
|
1324
1324
|
const date = (/* @__PURE__ */ new Date()).toLocaleDateString();
|
|
1325
|
-
const version = true ? "0.
|
|
1325
|
+
const version = true ? "0.9.0" : "0.1.0";
|
|
1326
1326
|
const lines = [];
|
|
1327
1327
|
lines.push("# OpenFeature Migration Inventory");
|
|
1328
1328
|
lines.push(`Generated by FlagLint v${version} on ${date}`);
|
|
@@ -2154,9 +2154,12 @@ function buildAuditReport(scanResult, inventoryItems) {
|
|
|
2154
2154
|
const { riskLevel, riskReasons } = scoreFlag(usages, items);
|
|
2155
2155
|
const files = [...new Set(usages.map((u) => u.file))];
|
|
2156
2156
|
const callTypes = [...new Set(usages.map((u) => u.callType))];
|
|
2157
|
+
const isAutomatableTier = riskLevel === "medium" && riskReasons.length === 1 && riskReasons[0] === "safely automatable";
|
|
2158
|
+
const displayTier = isAutomatableTier ? "automatable" : riskLevel;
|
|
2157
2159
|
flags.push({
|
|
2158
2160
|
flagKey,
|
|
2159
2161
|
riskLevel,
|
|
2162
|
+
displayTier,
|
|
2160
2163
|
riskReasons,
|
|
2161
2164
|
callTypes,
|
|
2162
2165
|
fileCount: files.length,
|
|
@@ -2176,23 +2179,28 @@ function buildAuditReport(scanResult, inventoryItems) {
|
|
|
2176
2179
|
const highRisk = flags.filter((f) => f.riskLevel === "high").length;
|
|
2177
2180
|
const mediumRisk = flags.filter((f) => f.riskLevel === "medium").length;
|
|
2178
2181
|
const lowRisk = flags.filter((f) => f.riskLevel === "low").length;
|
|
2182
|
+
const automatableFlags = flags.filter((f) => f.displayTier === "automatable").length;
|
|
2183
|
+
const staleSignals = scanResult.usages.filter((u) => u.stalenessSignals.length > 0).length;
|
|
2184
|
+
const stalenessNote = staleSignals === 0 ? "No staleness signals detected. Heuristics checked: keyword match (flag key contains old/deprecated/legacy/temp/tmp/test/demo), path pattern (test/spec/mock files, deprecated/old/legacy directories), and minFileCount threshold. Git-history-based staleness (last evaluation date) requires git metadata and is not available in a pure static scan." : void 0;
|
|
2179
2185
|
const summary = {
|
|
2180
2186
|
totalFlags: flags.length,
|
|
2181
2187
|
highRisk,
|
|
2182
2188
|
mediumRisk,
|
|
2183
2189
|
lowRisk,
|
|
2190
|
+
automatableFlags,
|
|
2184
2191
|
totalUsages: scanResult.totalUsages,
|
|
2185
2192
|
dynamicKeys: scanResult.usages.filter((u) => u.isDynamic).length,
|
|
2186
2193
|
detailEvaluations: inventoryItems.filter((i) => i.manualReviewReason === "detail-method").length,
|
|
2187
2194
|
bulkCalls: inventoryItems.filter((i) => i.manualReviewReason === "bulk-inventory-call").length,
|
|
2188
2195
|
wrapperUsages: scanResult.usages.filter((u) => u.callType === "variation").length,
|
|
2189
|
-
staleSignals
|
|
2196
|
+
staleSignals,
|
|
2190
2197
|
safelyAutomatable: inventoryItems.filter((i) => i.safelyAutomatable).length,
|
|
2191
2198
|
manualReview: inventoryItems.filter((i) => !i.safelyAutomatable).length,
|
|
2192
2199
|
scannedFiles: scanResult.scannedFiles,
|
|
2193
2200
|
scanDurationMs: scanResult.scanDurationMs,
|
|
2194
2201
|
scannedAt: scanResult.scannedAt,
|
|
2195
|
-
scanRoot: scanResult.scanRoot
|
|
2202
|
+
scanRoot: scanResult.scanRoot,
|
|
2203
|
+
...stalenessNote !== void 0 ? { stalenessNote } : {}
|
|
2196
2204
|
};
|
|
2197
2205
|
const readiness = computeReadiness(scanResult.usages, inventoryItems);
|
|
2198
2206
|
return { summary, flags, readiness };
|
|
@@ -2256,6 +2264,10 @@ function formatAuditMarkdown(report, options) {
|
|
|
2256
2264
|
`| ${summary.dynamicKeys} | ${summary.detailEvaluations} | ${summary.bulkCalls} | ${summary.staleSignals} | ${summary.safelyAutomatable} | ${summary.manualReview} |`
|
|
2257
2265
|
);
|
|
2258
2266
|
lines.push("");
|
|
2267
|
+
if (summary.stalenessNote !== void 0) {
|
|
2268
|
+
lines.push(`> **Staleness:** ${summary.stalenessNote}`);
|
|
2269
|
+
lines.push("");
|
|
2270
|
+
}
|
|
2259
2271
|
lines.push("## Migration Readiness");
|
|
2260
2272
|
lines.push("");
|
|
2261
2273
|
if (readiness.grade === "not-applicable") {
|
|
@@ -2300,16 +2312,17 @@ function formatAuditMarkdown(report, options) {
|
|
|
2300
2312
|
lines.push("");
|
|
2301
2313
|
lines.push("| Flag Key | Risk | Usages | Files | Call Types | Reasons |");
|
|
2302
2314
|
lines.push("|----------|------|--------|-------|------------|---------|");
|
|
2303
|
-
const
|
|
2315
|
+
const tierLabel = {
|
|
2304
2316
|
high: "\u{1F534} High",
|
|
2305
2317
|
medium: "\u{1F7E1} Medium",
|
|
2318
|
+
automatable: "\u{1F7E2} Automatable",
|
|
2306
2319
|
low: "\u{1F7E2} Low"
|
|
2307
2320
|
};
|
|
2308
2321
|
for (const flag of flags) {
|
|
2309
2322
|
const reasons = flag.riskReasons.join(", ") || "\u2014";
|
|
2310
2323
|
const flagKey = displayFlagKey(flag);
|
|
2311
2324
|
lines.push(
|
|
2312
|
-
`| \`${flagKey}\` | ${
|
|
2325
|
+
`| \`${flagKey}\` | ${tierLabel[flag.displayTier]} | ${flag.usageCount} | ${flag.fileCount} | ${flag.callTypes.join(", ")} | ${reasons} |`
|
|
2313
2326
|
);
|
|
2314
2327
|
}
|
|
2315
2328
|
lines.push("");
|
|
@@ -2329,12 +2342,14 @@ function formatAuditMarkdown(report, options) {
|
|
|
2329
2342
|
}
|
|
2330
2343
|
function formatAuditHtml(report, options) {
|
|
2331
2344
|
const { summary, flags, readiness } = report;
|
|
2332
|
-
const version = true ? "0.
|
|
2345
|
+
const version = true ? "0.9.0" : "0.1.0";
|
|
2333
2346
|
const date = new Date(summary.scannedAt).toLocaleString();
|
|
2334
|
-
const
|
|
2335
|
-
if (
|
|
2347
|
+
const tierBadge = (tier) => {
|
|
2348
|
+
if (tier === "high")
|
|
2336
2349
|
return '<span class="badge badge-high">High</span>';
|
|
2337
|
-
if (
|
|
2350
|
+
if (tier === "automatable")
|
|
2351
|
+
return '<span class="badge badge-automatable">Automatable</span>';
|
|
2352
|
+
if (tier === "medium")
|
|
2338
2353
|
return '<span class="badge badge-medium">Medium</span>';
|
|
2339
2354
|
return '<span class="badge badge-low">Low</span>';
|
|
2340
2355
|
};
|
|
@@ -2343,7 +2358,7 @@ function formatAuditHtml(report, options) {
|
|
|
2343
2358
|
const flagKey = displayFlagKey(f);
|
|
2344
2359
|
return `<tr>
|
|
2345
2360
|
<td><code>${esc2(flagKey)}</code></td>
|
|
2346
|
-
<td>${
|
|
2361
|
+
<td>${tierBadge(f.displayTier)}</td>
|
|
2347
2362
|
<td>${f.usageCount}</td>
|
|
2348
2363
|
<td>${f.fileCount}</td>
|
|
2349
2364
|
<td>${f.callTypes.map(esc2).join(", ")}</td>
|
|
@@ -2439,11 +2454,14 @@ function formatAuditHtml(report, options) {
|
|
|
2439
2454
|
.badge-high{background:#fee2e2;color:#991b1b}
|
|
2440
2455
|
.badge-medium{background:#fef3c7;color:#92400e}
|
|
2441
2456
|
.badge-low{background:#dcfce7;color:#166534}
|
|
2457
|
+
.badge-automatable{background:#d1fae5;color:#065f46}
|
|
2442
2458
|
@media(prefers-color-scheme:dark){
|
|
2443
2459
|
.badge-high{background:#7f1d1d;color:#fca5a5}
|
|
2444
2460
|
.badge-medium{background:#78350f;color:#fcd34d}
|
|
2445
2461
|
.badge-low{background:#14532d;color:#86efac}
|
|
2462
|
+
.badge-automatable{background:#064e3b;color:#6ee7b7}
|
|
2446
2463
|
}
|
|
2464
|
+
.staleness-note{margin-top:.75rem;padding:.75rem 1rem;background:var(--surface);border:1px solid var(--border);border-radius:6px;font-size:.8125rem;color:var(--muted)}
|
|
2447
2465
|
.estimate-block{background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:1.25rem;margin-bottom:1.5rem}
|
|
2448
2466
|
.estimate-total{font-size:1.5rem;font-weight:700;margin-bottom:.75rem}
|
|
2449
2467
|
.estimate-cost{font-size:1rem;font-weight:600;margin:.75rem 0;color:#3b82f6}
|
|
@@ -2463,11 +2481,13 @@ function formatAuditHtml(report, options) {
|
|
|
2463
2481
|
<div class="cards">
|
|
2464
2482
|
<div class="card"><div class="card-num">${summary.totalFlags}</div><div class="card-label">Total Flags</div></div>
|
|
2465
2483
|
<div class="card"><div class="card-num red">${summary.highRisk}</div><div class="card-label">High Risk</div></div>
|
|
2466
|
-
|
|
2484
|
+
${summary.automatableFlags > 0 ? `<div class="card"><div class="card-num green">${summary.automatableFlags}</div><div class="card-label">Automatable</div></div>` : ""}
|
|
2485
|
+
${summary.mediumRisk - summary.automatableFlags > 0 ? `<div class="card"><div class="card-num amber">${summary.mediumRisk - summary.automatableFlags}</div><div class="card-label">Medium Risk</div></div>` : ""}
|
|
2467
2486
|
${summary.lowRisk > 0 ? `<div class="card"><div class="card-num green">${summary.lowRisk}</div><div class="card-label">Low Risk</div></div>` : ""}
|
|
2468
2487
|
<div class="card"><div class="card-num purple">${summary.safelyAutomatable}</div><div class="card-label">Safely Automatable</div></div>
|
|
2469
2488
|
<div class="card"><div class="card-num orange">${summary.manualReview}</div><div class="card-label">Manual Review</div></div>
|
|
2470
2489
|
</div>
|
|
2490
|
+
${summary.stalenessNote !== void 0 ? `<div class="staleness-note"><strong>Staleness:</strong> ${esc2(summary.stalenessNote)}</div>` : ""}
|
|
2471
2491
|
|
|
2472
2492
|
<h2>Migration Readiness</h2>
|
|
2473
2493
|
<div class="readiness-block">
|
|
@@ -2800,7 +2820,7 @@ Examples:
|
|
|
2800
2820
|
// src/cli.ts
|
|
2801
2821
|
function createCLI() {
|
|
2802
2822
|
const program2 = new Command();
|
|
2803
|
-
program2.name("flaglint").description("
|
|
2823
|
+
program2.name("flaglint").description("Find every direct LaunchDarkly SDK call, detect flag debt, and generate safe OpenFeature migration plans. Static analysis \u2014 no API key required.").version("0.9.0", "-v, --version", "output the current version").addHelpText(
|
|
2804
2824
|
"after",
|
|
2805
2825
|
`
|
|
2806
2826
|
Examples:
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flaglint",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.9.0",
|
|
4
|
+
"description": "Find every direct LaunchDarkly SDK call, detect flag debt, and generate safe OpenFeature migration plans. Static analysis — no API key required.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"flaglint": "dist/bin/flaglint.js"
|
|
@@ -16,11 +16,20 @@
|
|
|
16
16
|
"node": ">=20"
|
|
17
17
|
},
|
|
18
18
|
"keywords": [
|
|
19
|
-
"feature-flags",
|
|
20
19
|
"launchdarkly",
|
|
21
20
|
"openfeature",
|
|
22
|
-
"
|
|
21
|
+
"feature-flags",
|
|
22
|
+
"feature-flag-migration",
|
|
23
|
+
"launchdarkly-migration",
|
|
24
|
+
"openfeature-migration",
|
|
23
25
|
"flag-debt",
|
|
26
|
+
"static-analysis",
|
|
27
|
+
"codemod",
|
|
28
|
+
"cli",
|
|
29
|
+
"typescript",
|
|
30
|
+
"nodejs",
|
|
31
|
+
"cncf",
|
|
32
|
+
"migration-tool",
|
|
24
33
|
"developer-tools"
|
|
25
34
|
],
|
|
26
35
|
"author": "flaglint",
|
|
@@ -52,6 +61,7 @@
|
|
|
52
61
|
"test:package": "tsx scripts/test-package.ts",
|
|
53
62
|
"test:smoke": "playwright test",
|
|
54
63
|
"metrics": "node scripts/metrics/collect.mjs",
|
|
64
|
+
"metrics:report": "node scripts/metrics/weekly-report.mjs",
|
|
55
65
|
"new-branch": "tsx scripts/new-branch.ts",
|
|
56
66
|
"release:patch": "tsx scripts/release.ts patch",
|
|
57
67
|
"release:minor": "tsx scripts/release.ts minor",
|