flaglint 0.4.0 → 0.5.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/CHANGELOG.md +92 -1
- package/README.md +163 -34
- package/dist/apply-ZYLA2N7Y.js +13 -0
- package/dist/bin/flaglint.js +295 -234
- package/dist/chunk-MJLXM6GZ.js +263 -0
- package/package.json +7 -2
package/dist/bin/flaglint.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
ApplyError,
|
|
4
|
+
applyMigration
|
|
5
|
+
} from "../chunk-MJLXM6GZ.js";
|
|
2
6
|
|
|
3
7
|
// src/cli.ts
|
|
4
8
|
import { Command } from "commander";
|
|
@@ -53,6 +57,13 @@ var LD_FLAG_KEY_METHODS = /* @__PURE__ */ new Set([
|
|
|
53
57
|
"jsonVariationDetail"
|
|
54
58
|
]);
|
|
55
59
|
var LD_ALL_FLAGS_METHODS = /* @__PURE__ */ new Set(["allFlags", "allFlagsState"]);
|
|
60
|
+
var LD_DETAIL_METHODS = /* @__PURE__ */ new Set([
|
|
61
|
+
"variationDetail",
|
|
62
|
+
"boolVariationDetail",
|
|
63
|
+
"stringVariationDetail",
|
|
64
|
+
"numberVariationDetail",
|
|
65
|
+
"jsonVariationDetail"
|
|
66
|
+
]);
|
|
56
67
|
var LD_HOOKS = /* @__PURE__ */ new Set(["useFlags", "useLDClient"]);
|
|
57
68
|
var DEFAULT_EXCLUDE = ["**/node_modules/**", "**/dist/**", "**/build/**", "**/.next/**"];
|
|
58
69
|
function extractFlagKey(arg) {
|
|
@@ -120,7 +131,7 @@ function buildMigrationInventoryItem(code, filePath, loc, call, methodName, args
|
|
|
120
131
|
}
|
|
121
132
|
const fallback = args[2];
|
|
122
133
|
const valueType = inferValueType(methodName, fallback);
|
|
123
|
-
const manualReviewReason = isDynamic ? "dynamic-key" : valueType === "unknown" ? "unknown-fallback" : void 0;
|
|
134
|
+
const manualReviewReason = isDynamic ? "dynamic-key" : LD_DETAIL_METHODS.has(methodName) ? "detail-method" : valueType === "unknown" ? "unknown-fallback" : void 0;
|
|
124
135
|
return {
|
|
125
136
|
file: filePath,
|
|
126
137
|
line: loc.line,
|
|
@@ -528,7 +539,7 @@ function formatMarkdown(result, options) {
|
|
|
528
539
|
}
|
|
529
540
|
if (staleFlags.length > 0) {
|
|
530
541
|
lines.push("## \u26A0 Stale Flag Candidates");
|
|
531
|
-
lines.push("Flags
|
|
542
|
+
lines.push("Flags with review signals:");
|
|
532
543
|
lines.push("| Flag Key | Reason | Location |");
|
|
533
544
|
lines.push("|----------|--------|----------|");
|
|
534
545
|
for (const [key, data] of staleFlags) {
|
|
@@ -665,6 +676,16 @@ function formatSARIF(result) {
|
|
|
665
676
|
2
|
|
666
677
|
);
|
|
667
678
|
}
|
|
679
|
+
function usagesByDirectory(usages) {
|
|
680
|
+
const map = /* @__PURE__ */ new Map();
|
|
681
|
+
for (const u of usages) {
|
|
682
|
+
const parts = u.file.replace(/\\/g, "/").split("/");
|
|
683
|
+
const dir = parts.length > 1 ? parts.slice(0, Math.min(2, parts.length - 1)).join("/") : ".";
|
|
684
|
+
if (!map.has(dir)) map.set(dir, []);
|
|
685
|
+
map.get(dir).push(u);
|
|
686
|
+
}
|
|
687
|
+
return new Map([...map.entries()].sort(([a], [b]) => a.localeCompare(b)));
|
|
688
|
+
}
|
|
668
689
|
function formatHTML(result, options) {
|
|
669
690
|
const { scannedFiles, totalUsages, uniqueFlags, usages, scanDurationMs } = result;
|
|
670
691
|
const staleCount = new Set(
|
|
@@ -672,6 +693,34 @@ function formatHTML(result, options) {
|
|
|
672
693
|
).size;
|
|
673
694
|
const dynamicCount = usages.filter((u) => u.isDynamic).length;
|
|
674
695
|
const date = new Date(result.scannedAt).toLocaleString();
|
|
696
|
+
const inv = result.migrationInventory ?? [];
|
|
697
|
+
const automatableCount = inv.filter((i) => i.safelyAutomatable).length;
|
|
698
|
+
const manualCount = inv.filter((i) => !i.safelyAutomatable).length;
|
|
699
|
+
const detailBulkCount = inv.filter(
|
|
700
|
+
(i) => i.manualReviewReason === "detail-method" || i.manualReviewReason === "bulk-inventory-call"
|
|
701
|
+
).length;
|
|
702
|
+
const affectedFiles = new Set(usages.map((u) => u.file)).size;
|
|
703
|
+
const automatablePct = inv.length > 0 ? Math.round(automatableCount / inv.length * 100) : 0;
|
|
704
|
+
const manualPct = inv.length > 0 ? Math.round(manualCount / inv.length * 100) : 0;
|
|
705
|
+
const markdownSummary = [
|
|
706
|
+
"## FlagLint Audit Summary",
|
|
707
|
+
"",
|
|
708
|
+
`- **Total call-sites:** ${totalUsages}`,
|
|
709
|
+
`- **Unique flags:** ${uniqueFlags.length}`,
|
|
710
|
+
`- **Files affected:** ${affectedFiles}`,
|
|
711
|
+
...inv.length > 0 ? [
|
|
712
|
+
`- **Safely automatable:** ${automatableCount} (${automatablePct}%)`,
|
|
713
|
+
`- **Manual review required:** ${manualCount} (${manualPct}%)`,
|
|
714
|
+
`- **Dynamic keys:** ${dynamicCount}`,
|
|
715
|
+
`- **Detail/bulk calls:** ${detailBulkCount}`
|
|
716
|
+
] : [`- **Dynamic keys:** ${dynamicCount}`, `- **Stale candidates:** ${staleCount}`],
|
|
717
|
+
"",
|
|
718
|
+
"### Recommended next steps",
|
|
719
|
+
"1. Configure OpenFeature provider (one-time manual step)",
|
|
720
|
+
"2. Review migration plan: `flaglint migrate --dry-run`",
|
|
721
|
+
"3. Apply automatable transformations: `flaglint migrate --apply`",
|
|
722
|
+
"4. Add CI enforcement: `flaglint validate --no-direct-launchdarkly`"
|
|
723
|
+
].join("\\n");
|
|
675
724
|
const flagMap = buildFlagMap(usages);
|
|
676
725
|
const sorted = sortedFlagEntries(flagMap);
|
|
677
726
|
const rows = sorted.map(([key, data]) => {
|
|
@@ -680,8 +729,18 @@ function formatHTML(result, options) {
|
|
|
680
729
|
const fileList = [...data.files].map((f) => esc(f)).join("<br>");
|
|
681
730
|
return `<tr class="${cls}"><td><code>${esc(key)}</code></td><td>${data.usages.length}</td><td>${fileList}</td><td>${[...data.callTypes].map(esc).join(", ")}</td><td>${status}</td></tr>`;
|
|
682
731
|
}).join("\n ");
|
|
732
|
+
const byDir = usagesByDirectory(usages);
|
|
733
|
+
const dirRows = [...byDir.entries()].map(([dir, dirUsages]) => {
|
|
734
|
+
const flagKeys = new Set(dirUsages.map((u) => u.flagKey)).size;
|
|
735
|
+
const callTypes = new Set(dirUsages.map((u) => u.callType));
|
|
736
|
+
return `<tr><td><code>${esc(dir)}</code></td><td>${dirUsages.length}</td><td>${flagKeys}</td><td>${[...callTypes].map(esc).join(", ")}</td></tr>`;
|
|
737
|
+
}).join("\n ");
|
|
738
|
+
const auditCards = inv.length > 0 ? `
|
|
739
|
+
<div class="card"><div class="card-num green">${automatableCount}</div><div class="card-label">Auto-Migratable (${automatablePct}%)</div></div>
|
|
740
|
+
<div class="card"><div class="card-num orange">${manualCount}</div><div class="card-label">Manual Review (${manualPct}%)</div></div>
|
|
741
|
+
<div class="card"><div class="card-num blue">${detailBulkCount}</div><div class="card-label">Detail/Bulk Calls</div></div>` : "";
|
|
683
742
|
const title = options.title ? esc(options.title) : "FlagLint Scan Report";
|
|
684
|
-
const version = true ? "0.
|
|
743
|
+
const version = true ? "0.5.0" : "0.1.0";
|
|
685
744
|
return `<!DOCTYPE html>
|
|
686
745
|
<html lang="en">
|
|
687
746
|
<head>
|
|
@@ -695,12 +754,15 @@ function formatHTML(result, options) {
|
|
|
695
754
|
body{background:var(--bg);color:var(--text);font-family:system-ui,-apple-system,sans-serif;padding:2rem;max-width:1200px;margin:0 auto;line-height:1.5}
|
|
696
755
|
h1{font-size:1.75rem;margin-bottom:.25rem}
|
|
697
756
|
h2{font-size:1.125rem;margin:2rem 0 .75rem;padding-bottom:.5rem;border-bottom:1px solid var(--border)}
|
|
757
|
+
h3{font-size:.9375rem;margin:1.5rem 0 .5rem;color:var(--muted)}
|
|
698
758
|
.subtitle{color:var(--muted);margin-bottom:1.5rem;font-size:.875rem}
|
|
699
759
|
.cards{display:flex;gap:1rem;flex-wrap:wrap;margin-bottom:2rem}
|
|
700
760
|
.card{flex:1;min-width:140px;background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:1rem;box-shadow:var(--card-shadow)}
|
|
701
761
|
.card-num{font-size:1.875rem;font-weight:700;line-height:1}
|
|
702
762
|
.card-num.yellow{color:#d97706}
|
|
703
763
|
.card-num.blue{color:#3b82f6}
|
|
764
|
+
.card-num.green{color:#16a34a}
|
|
765
|
+
.card-num.orange{color:#ea580c}
|
|
704
766
|
.card-label{color:var(--muted);font-size:.75rem;margin-top:.375rem;text-transform:uppercase;letter-spacing:.05em}
|
|
705
767
|
.filter-wrap{margin-bottom:.75rem}
|
|
706
768
|
#filter{width:100%;padding:.5rem .75rem;border:1px solid var(--border);border-radius:6px;background:var(--surface);color:var(--text);font-size:.875rem;outline:none}
|
|
@@ -711,19 +773,25 @@ function formatHTML(result, options) {
|
|
|
711
773
|
tr.stale td{background:var(--stale-bg)}
|
|
712
774
|
tr.dynamic td{background:var(--dyn-bg)}
|
|
713
775
|
code{font-family:ui-monospace,monospace;font-size:.8em;background:var(--surface);padding:.1em .3em;border-radius:3px}
|
|
776
|
+
.steps{margin:.75rem 0 1rem 1.25rem;line-height:2}
|
|
777
|
+
.steps li{margin-bottom:.25rem}
|
|
778
|
+
.btn{display:inline-flex;align-items:center;gap:.4rem;background:#6366f1;color:#fff;border:none;border-radius:6px;padding:.5rem 1rem;font-size:.8125rem;cursor:pointer;margin-top:.75rem}
|
|
779
|
+
.btn:hover{background:#4f46e5}
|
|
780
|
+
.btn.copied{background:#16a34a}
|
|
714
781
|
footer{margin-top:3rem;padding-top:1rem;border-top:1px solid var(--border);color:var(--muted);font-size:.75rem;text-align:center}
|
|
715
782
|
</style>
|
|
716
783
|
</head>
|
|
717
784
|
<body>
|
|
718
785
|
<h1>${title}</h1>
|
|
719
|
-
<p class="subtitle">Scanned ${scannedFiles} files in ${scanDurationMs}ms</p>
|
|
786
|
+
<p class="subtitle">Scanned ${scannedFiles} files in ${scanDurationMs}ms \xB7 ${esc(date)}</p>
|
|
720
787
|
|
|
788
|
+
<h2>Executive Summary</h2>
|
|
721
789
|
<div class="cards">
|
|
722
|
-
<div class="card"><div class="card-num">${
|
|
790
|
+
<div class="card"><div class="card-num">${totalUsages}</div><div class="card-label">Total Call-Sites</div></div>
|
|
723
791
|
<div class="card"><div class="card-num">${uniqueFlags.length}</div><div class="card-label">Unique Flags</div></div>
|
|
724
|
-
<div class="card"><div class="card-num">${
|
|
792
|
+
<div class="card"><div class="card-num">${affectedFiles}</div><div class="card-label">Files Affected</div></div>
|
|
725
793
|
<div class="card"><div class="card-num yellow">${staleCount}</div><div class="card-label">Stale Candidates</div></div>
|
|
726
|
-
<div class="card"><div class="card-num blue">${dynamicCount}</div><div class="card-label">Dynamic Keys</div></div
|
|
794
|
+
<div class="card"><div class="card-num blue">${dynamicCount}</div><div class="card-label">Dynamic Keys</div></div>${auditCards}
|
|
727
795
|
</div>
|
|
728
796
|
|
|
729
797
|
<h2>Flag Inventory</h2>
|
|
@@ -737,15 +805,41 @@ function formatHTML(result, options) {
|
|
|
737
805
|
</tbody>
|
|
738
806
|
</table>
|
|
739
807
|
|
|
740
|
-
<
|
|
808
|
+
<h2>Findings by Directory</h2>
|
|
809
|
+
<table id="dir-table">
|
|
810
|
+
<thead><tr><th>Directory</th><th>Call-Sites</th><th>Unique Flags</th><th>Call Types</th></tr></thead>
|
|
811
|
+
<tbody>
|
|
812
|
+
${dirRows}
|
|
813
|
+
</tbody>
|
|
814
|
+
</table>
|
|
815
|
+
|
|
816
|
+
<h2>Recommended Next Steps</h2>
|
|
817
|
+
<ol class="steps">
|
|
818
|
+
<li>Configure the OpenFeature provider once at application startup (manual \u2014 see <code>flaglint migrate --dry-run</code> for guidance)</li>
|
|
819
|
+
<li>Review the migration plan: <code>flaglint migrate --dry-run</code></li>
|
|
820
|
+
<li>Apply automatable transformations: <code>flaglint migrate --apply</code></li>
|
|
821
|
+
<li>Add CI policy enforcement: <code>flaglint validate --no-direct-launchdarkly</code></li>
|
|
822
|
+
</ol>
|
|
823
|
+
<button class="btn" id="copy-btn" onclick="copyMarkdown()">\u{1F4CB} Copy Markdown Summary</button>
|
|
824
|
+
|
|
825
|
+
<footer>Generated by FlagLint ${esc(version)}</footer>
|
|
741
826
|
|
|
742
827
|
<script>
|
|
743
828
|
const input = document.getElementById('filter');
|
|
744
|
-
const
|
|
829
|
+
const tableRows = document.querySelectorAll('#flags-table tbody tr');
|
|
745
830
|
input.addEventListener('input', () => {
|
|
746
831
|
const q = input.value.toLowerCase();
|
|
747
|
-
|
|
832
|
+
tableRows.forEach(r => { r.style.display = r.textContent.toLowerCase().includes(q) ? '' : 'none'; });
|
|
748
833
|
});
|
|
834
|
+
function copyMarkdown() {
|
|
835
|
+
const md = ${JSON.stringify(markdownSummary)}.replace(/\\\\n/g, '\\n');
|
|
836
|
+
navigator.clipboard.writeText(md).then(() => {
|
|
837
|
+
const btn = document.getElementById('copy-btn');
|
|
838
|
+
btn.textContent = '\u2713 Copied!';
|
|
839
|
+
btn.className = 'btn copied';
|
|
840
|
+
setTimeout(() => { btn.textContent = '\u{1F4CB} Copy Markdown Summary'; btn.className = 'btn'; }, 2000);
|
|
841
|
+
});
|
|
842
|
+
}
|
|
749
843
|
</script>
|
|
750
844
|
</body>
|
|
751
845
|
</html>`;
|
|
@@ -779,8 +873,11 @@ var FlagLintConfigSchema = z.object({
|
|
|
779
873
|
]),
|
|
780
874
|
provider: z.enum(["launchdarkly", "unleash", "growthbook", "custom"]).default("launchdarkly"),
|
|
781
875
|
// TODO v0.3: replace minFileCount with real date-based staleness via git log
|
|
782
|
-
minFileCount: z.number().int().min(0).default(
|
|
876
|
+
minFileCount: z.number().int().min(0).default(0),
|
|
783
877
|
wrappers: z.array(z.string()).default([]),
|
|
878
|
+
openFeatureClientBindings: z.array(
|
|
879
|
+
z.object({ importName: z.string(), modulePatterns: z.array(z.string()).default([]) })
|
|
880
|
+
).default([]),
|
|
784
881
|
reportTitle: z.string().optional(),
|
|
785
882
|
outputDir: z.string().default(".")
|
|
786
883
|
});
|
|
@@ -955,7 +1052,7 @@ Examples:
|
|
|
955
1052
|
} else {
|
|
956
1053
|
process.stdout.write(report + "\n");
|
|
957
1054
|
}
|
|
958
|
-
process.
|
|
1055
|
+
process.exitCode = staleCount > 0 ? 1 : 0;
|
|
959
1056
|
}
|
|
960
1057
|
);
|
|
961
1058
|
}
|
|
@@ -1011,6 +1108,8 @@ function reasonLabel(reason) {
|
|
|
1011
1108
|
return "dynamic key";
|
|
1012
1109
|
case "unknown-fallback":
|
|
1013
1110
|
return "unsupported or unknown fallback";
|
|
1111
|
+
case "detail-method":
|
|
1112
|
+
return "detail method";
|
|
1014
1113
|
case "bulk-inventory-call":
|
|
1015
1114
|
return "bulk inventory call";
|
|
1016
1115
|
default:
|
|
@@ -1094,7 +1193,7 @@ function formatMigrationReport(analysis) {
|
|
|
1094
1193
|
unsupportedUnknownCount
|
|
1095
1194
|
} = analysis;
|
|
1096
1195
|
const date = (/* @__PURE__ */ new Date()).toLocaleDateString();
|
|
1097
|
-
const version = true ? "0.
|
|
1196
|
+
const version = true ? "0.5.0" : "0.1.0";
|
|
1098
1197
|
const lines = [];
|
|
1099
1198
|
lines.push("# OpenFeature Migration Inventory");
|
|
1100
1199
|
lines.push(`Generated by FlagLint v${version} on ${date}`);
|
|
@@ -1179,10 +1278,13 @@ function methodForType(valueType) {
|
|
|
1179
1278
|
function manualReason(item) {
|
|
1180
1279
|
if (item.manualReviewReason === "dynamic-key") return "dynamic key requires manual review";
|
|
1181
1280
|
if (item.manualReviewReason === "unknown-fallback") return "unknown fallback type requires manual review";
|
|
1281
|
+
if (item.manualReviewReason === "detail-method") {
|
|
1282
|
+
return "detail methods skipped: OpenFeature detail APIs exist, but LaunchDarkly/OpenFeature detail result parity requires manual review";
|
|
1283
|
+
}
|
|
1182
1284
|
if (item.manualReviewReason === "bulk-inventory-call") return "bulk inventory call has no single-flag codemod";
|
|
1183
1285
|
return "manual review required";
|
|
1184
1286
|
}
|
|
1185
|
-
function buildReplacement(item) {
|
|
1287
|
+
function buildReplacement(item, clientBindingName) {
|
|
1186
1288
|
if (DETAIL_METHODS.has(item.launchDarklyMethod)) {
|
|
1187
1289
|
return {
|
|
1188
1290
|
item,
|
|
@@ -1200,11 +1302,13 @@ function buildReplacement(item) {
|
|
|
1200
1302
|
}
|
|
1201
1303
|
const method = methodForType(item.valueType);
|
|
1202
1304
|
if (!method) return { item, reason: "unsupported or unknown value type" };
|
|
1203
|
-
const
|
|
1305
|
+
const placeholder = "openFeatureClient";
|
|
1306
|
+
const target = clientBindingName ?? placeholder;
|
|
1307
|
+
const call = `${target}.${method}(${item.flagKeyExpression}, ${item.fallbackExpression}, ${item.evaluationContextExpression})`;
|
|
1204
1308
|
return {
|
|
1205
1309
|
item,
|
|
1206
1310
|
replacement: call,
|
|
1207
|
-
requiresProviderSetup:
|
|
1311
|
+
requiresProviderSetup: clientBindingName == null
|
|
1208
1312
|
};
|
|
1209
1313
|
}
|
|
1210
1314
|
function applyReplacements(code, replacements) {
|
|
@@ -1268,7 +1372,7 @@ function formatProviderSetupSection() {
|
|
|
1268
1372
|
lines.push('import { OpenFeature } from "@openfeature/server-sdk";');
|
|
1269
1373
|
lines.push('import { LaunchDarklyProvider } from "@launchdarkly/openfeature-node-server";');
|
|
1270
1374
|
lines.push("");
|
|
1271
|
-
lines.push(
|
|
1375
|
+
lines.push("const ldProvider = new LaunchDarklyProvider(process.env.LD_SDK_KEY!);");
|
|
1272
1376
|
lines.push("await OpenFeature.setProviderAndWait(ldProvider);");
|
|
1273
1377
|
lines.push("");
|
|
1274
1378
|
lines.push("// Share this client across your application.");
|
|
@@ -1278,26 +1382,39 @@ function formatProviderSetupSection() {
|
|
|
1278
1382
|
lines.push("");
|
|
1279
1383
|
lines.push("### 3. Evaluation context \u2014 targeting key");
|
|
1280
1384
|
lines.push("");
|
|
1281
|
-
lines.push("LaunchDarkly requires a
|
|
1282
|
-
lines.push("
|
|
1385
|
+
lines.push("LaunchDarkly requires a targeting key in every evaluation context. The");
|
|
1386
|
+
lines.push("provider accepts either OpenFeature `targetingKey` or an existing");
|
|
1387
|
+
lines.push("LaunchDarkly `key`.");
|
|
1388
|
+
lines.push("Keep your existing LaunchDarkly `key` contexts, or use `targetingKey`");
|
|
1389
|
+
lines.push("for new OpenFeature-native contexts:");
|
|
1283
1390
|
lines.push("");
|
|
1284
1391
|
lines.push("```typescript");
|
|
1285
|
-
lines.push("{ targetingKey: user.key
|
|
1392
|
+
lines.push("{ targetingKey: user.id } // or { key: user.id }");
|
|
1286
1393
|
lines.push("```");
|
|
1287
1394
|
lines.push("");
|
|
1288
1395
|
return lines;
|
|
1289
1396
|
}
|
|
1290
|
-
async function formatDryRunDiff(analysis, source) {
|
|
1397
|
+
async function formatDryRunDiff(analysis, source, allowedBindings = []) {
|
|
1291
1398
|
const replacementsByFile = /* @__PURE__ */ new Map();
|
|
1292
1399
|
const skipped = [];
|
|
1400
|
+
const itemsByFile = /* @__PURE__ */ new Map();
|
|
1293
1401
|
for (const item of analysis.inventoryItems) {
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1402
|
+
if (!itemsByFile.has(item.file)) itemsByFile.set(item.file, []);
|
|
1403
|
+
itemsByFile.get(item.file).push(item);
|
|
1404
|
+
}
|
|
1405
|
+
for (const [file, items] of [...itemsByFile.entries()].sort(([a], [b]) => a.localeCompare(b))) {
|
|
1406
|
+
const before = await source.readFile(file);
|
|
1407
|
+
const { getOpenFeatureClientBindingName } = await import("../apply-ZYLA2N7Y.js");
|
|
1408
|
+
const clientBindingName = getOpenFeatureClientBindingName(before, allowedBindings);
|
|
1409
|
+
for (const item of items) {
|
|
1410
|
+
const result = buildReplacement(item, clientBindingName);
|
|
1411
|
+
if ("reason" in result) {
|
|
1412
|
+
skipped.push(result);
|
|
1413
|
+
continue;
|
|
1414
|
+
}
|
|
1415
|
+
if (!replacementsByFile.has(item.file)) replacementsByFile.set(item.file, []);
|
|
1416
|
+
replacementsByFile.get(item.file).push(result);
|
|
1298
1417
|
}
|
|
1299
|
-
if (!replacementsByFile.has(item.file)) replacementsByFile.set(item.file, []);
|
|
1300
|
-
replacementsByFile.get(item.file).push(result);
|
|
1301
1418
|
}
|
|
1302
1419
|
const lines = [];
|
|
1303
1420
|
lines.push("# FlagLint migrate --dry-run");
|
|
@@ -1335,205 +1452,6 @@ async function formatDryRunDiff(analysis, source) {
|
|
|
1335
1452
|
return lines.join("\n");
|
|
1336
1453
|
}
|
|
1337
1454
|
|
|
1338
|
-
// src/migrator/apply.ts
|
|
1339
|
-
import { execFile } from "child_process";
|
|
1340
|
-
import { promisify } from "util";
|
|
1341
|
-
import { parse as parse2 } from "@typescript-eslint/typescript-estree";
|
|
1342
|
-
var execFileAsync = promisify(execFile);
|
|
1343
|
-
var ApplyError = class extends Error {
|
|
1344
|
-
constructor(kind, message) {
|
|
1345
|
-
super(message);
|
|
1346
|
-
this.kind = kind;
|
|
1347
|
-
this.name = "ApplyError";
|
|
1348
|
-
}
|
|
1349
|
-
kind;
|
|
1350
|
-
};
|
|
1351
|
-
var OF_SERVER_SDK = "@openfeature/server-sdk";
|
|
1352
|
-
function tryParse(code) {
|
|
1353
|
-
for (const jsx of [false, true]) {
|
|
1354
|
-
try {
|
|
1355
|
-
return parse2(code, { jsx, comment: false });
|
|
1356
|
-
} catch {
|
|
1357
|
-
}
|
|
1358
|
-
}
|
|
1359
|
-
return null;
|
|
1360
|
-
}
|
|
1361
|
-
function walkNodes(root, visit) {
|
|
1362
|
-
const stack = [root];
|
|
1363
|
-
while (stack.length > 0) {
|
|
1364
|
-
const node = stack.pop();
|
|
1365
|
-
visit(node);
|
|
1366
|
-
for (const val of Object.values(node)) {
|
|
1367
|
-
if (Array.isArray(val)) {
|
|
1368
|
-
for (const item of val) {
|
|
1369
|
-
if (item !== null && typeof item === "object" && "type" in item) {
|
|
1370
|
-
stack.push(item);
|
|
1371
|
-
}
|
|
1372
|
-
}
|
|
1373
|
-
} else if (val !== null && typeof val === "object" && "type" in val) {
|
|
1374
|
-
stack.push(val);
|
|
1375
|
-
}
|
|
1376
|
-
}
|
|
1377
|
-
}
|
|
1378
|
-
}
|
|
1379
|
-
function hasOpenFeatureClientBinding(code) {
|
|
1380
|
-
const ast = tryParse(code);
|
|
1381
|
-
if (!ast) return false;
|
|
1382
|
-
const ofApiNames = /* @__PURE__ */ new Set();
|
|
1383
|
-
for (const stmt of ast.body) {
|
|
1384
|
-
if (stmt.type === "ImportDeclaration" && stmt.source.value === OF_SERVER_SDK) {
|
|
1385
|
-
for (const spec of stmt.specifiers) {
|
|
1386
|
-
if (spec.type === "ImportSpecifier") {
|
|
1387
|
-
const importedName = spec.imported.type === "Identifier" ? spec.imported.name : spec.imported.value;
|
|
1388
|
-
if (importedName === "OpenFeature") {
|
|
1389
|
-
ofApiNames.add(spec.local.name);
|
|
1390
|
-
}
|
|
1391
|
-
}
|
|
1392
|
-
}
|
|
1393
|
-
continue;
|
|
1394
|
-
}
|
|
1395
|
-
if (stmt.type === "VariableDeclaration") {
|
|
1396
|
-
for (const decl of stmt.declarations) {
|
|
1397
|
-
const init = decl.init;
|
|
1398
|
-
if (init?.type === "CallExpression" && init.callee.type === "Identifier" && init.callee.name === "require" && init.arguments.length === 1 && init.arguments[0]?.type === "Literal" && init.arguments[0].value === OF_SERVER_SDK && decl.id.type === "ObjectPattern") {
|
|
1399
|
-
for (const prop of decl.id.properties) {
|
|
1400
|
-
if (prop.type === "Property" && prop.key.type === "Identifier" && prop.key.name === "OpenFeature" && prop.value.type === "Identifier") {
|
|
1401
|
-
ofApiNames.add(prop.value.name);
|
|
1402
|
-
}
|
|
1403
|
-
}
|
|
1404
|
-
}
|
|
1405
|
-
}
|
|
1406
|
-
}
|
|
1407
|
-
}
|
|
1408
|
-
if (ofApiNames.size === 0) return false;
|
|
1409
|
-
let proven = false;
|
|
1410
|
-
walkNodes(ast, (node) => {
|
|
1411
|
-
if (proven) return;
|
|
1412
|
-
if (node.type !== "VariableDeclarator") return;
|
|
1413
|
-
const decl = node;
|
|
1414
|
-
if (decl.id.type !== "Identifier") return;
|
|
1415
|
-
if (decl.id.name !== "openFeatureClient") return;
|
|
1416
|
-
if (decl.init?.type !== "CallExpression") return;
|
|
1417
|
-
const call = decl.init;
|
|
1418
|
-
if (call.callee.type !== "MemberExpression") return;
|
|
1419
|
-
const member = call.callee;
|
|
1420
|
-
if (member.computed) return;
|
|
1421
|
-
if (member.object.type !== "Identifier") return;
|
|
1422
|
-
if (!ofApiNames.has(member.object.name)) return;
|
|
1423
|
-
if (member.property.type !== "Identifier") return;
|
|
1424
|
-
if (member.property.name !== "getClient") return;
|
|
1425
|
-
proven = true;
|
|
1426
|
-
});
|
|
1427
|
-
return proven;
|
|
1428
|
-
}
|
|
1429
|
-
var DETAIL_METHODS2 = /* @__PURE__ */ new Set([
|
|
1430
|
-
"variationDetail",
|
|
1431
|
-
"boolVariationDetail",
|
|
1432
|
-
"stringVariationDetail",
|
|
1433
|
-
"numberVariationDetail",
|
|
1434
|
-
"jsonVariationDetail"
|
|
1435
|
-
]);
|
|
1436
|
-
function methodForType2(valueType) {
|
|
1437
|
-
switch (valueType) {
|
|
1438
|
-
case "boolean":
|
|
1439
|
-
return "getBooleanValue";
|
|
1440
|
-
case "string":
|
|
1441
|
-
return "getStringValue";
|
|
1442
|
-
case "number":
|
|
1443
|
-
return "getNumberValue";
|
|
1444
|
-
case "object":
|
|
1445
|
-
return "getObjectValue";
|
|
1446
|
-
case "unknown":
|
|
1447
|
-
return null;
|
|
1448
|
-
}
|
|
1449
|
-
}
|
|
1450
|
-
function buildReplacement2(item, code) {
|
|
1451
|
-
if (DETAIL_METHODS2.has(item.launchDarklyMethod)) {
|
|
1452
|
-
return {
|
|
1453
|
-
item,
|
|
1454
|
-
reason: "detail methods skipped: OpenFeature detail APIs exist, but LaunchDarkly/OpenFeature detail result parity requires manual review"
|
|
1455
|
-
};
|
|
1456
|
-
}
|
|
1457
|
-
if (!item.safelyAutomatable) {
|
|
1458
|
-
const reason = item.manualReviewReason === "dynamic-key" ? "dynamic key requires manual review" : item.manualReviewReason === "unknown-fallback" ? "unknown fallback type requires manual review" : item.manualReviewReason === "bulk-inventory-call" ? "bulk inventory call has no single-flag codemod" : "manual review required";
|
|
1459
|
-
return { item, reason };
|
|
1460
|
-
}
|
|
1461
|
-
if (item.rangeStart == null || item.rangeEnd == null || !item.callExpression) {
|
|
1462
|
-
return { item, reason: "missing source range for apply" };
|
|
1463
|
-
}
|
|
1464
|
-
const currentText = code.slice(item.rangeStart, item.rangeEnd);
|
|
1465
|
-
if (currentText !== item.callExpression) {
|
|
1466
|
-
return {
|
|
1467
|
-
item,
|
|
1468
|
-
reason: "range content does not match original call \u2014 already transformed or stale analysis; skipping"
|
|
1469
|
-
};
|
|
1470
|
-
}
|
|
1471
|
-
if (!item.flagKeyExpression || !item.fallbackExpression || !item.evaluationContextExpression) {
|
|
1472
|
-
return { item, reason: "missing flag key, fallback, or evaluation context evidence" };
|
|
1473
|
-
}
|
|
1474
|
-
const method = methodForType2(item.valueType);
|
|
1475
|
-
if (!method) return { item, reason: "unsupported or unknown value type" };
|
|
1476
|
-
const call = `openFeatureClient.${method}(${item.flagKeyExpression}, ${item.fallbackExpression}, ${item.evaluationContextExpression})`;
|
|
1477
|
-
return { item, replacement: call };
|
|
1478
|
-
}
|
|
1479
|
-
function applyReplacements2(code, replacements) {
|
|
1480
|
-
let next = code;
|
|
1481
|
-
for (const r of [...replacements].sort((a, b) => b.item.rangeStart - a.item.rangeStart)) {
|
|
1482
|
-
next = next.slice(0, r.item.rangeStart) + r.replacement + next.slice(r.item.rangeEnd);
|
|
1483
|
-
}
|
|
1484
|
-
return next;
|
|
1485
|
-
}
|
|
1486
|
-
async function defaultIsWorkingTreeDirty() {
|
|
1487
|
-
try {
|
|
1488
|
-
const { stdout } = await execFileAsync("git", ["status", "--porcelain"]);
|
|
1489
|
-
return stdout.trim().length > 0;
|
|
1490
|
-
} catch {
|
|
1491
|
-
return false;
|
|
1492
|
-
}
|
|
1493
|
-
}
|
|
1494
|
-
async function applyMigration(analysis, source, options = {}) {
|
|
1495
|
-
if (!options.allowDirty) {
|
|
1496
|
-
const checkDirty = options.isWorkingTreeDirty ?? defaultIsWorkingTreeDirty;
|
|
1497
|
-
if (await checkDirty()) {
|
|
1498
|
-
throw new ApplyError(
|
|
1499
|
-
"dirty-tree",
|
|
1500
|
-
"Working tree has uncommitted changes.\nCommit or stash your changes first, or pass --allow-dirty to override.\nReview `flaglint migrate --dry-run` for provider setup guidance before applying."
|
|
1501
|
-
);
|
|
1502
|
-
}
|
|
1503
|
-
}
|
|
1504
|
-
const itemsByFile = /* @__PURE__ */ new Map();
|
|
1505
|
-
for (const item of analysis.inventoryItems) {
|
|
1506
|
-
if (!itemsByFile.has(item.file)) itemsByFile.set(item.file, []);
|
|
1507
|
-
itemsByFile.get(item.file).push(item);
|
|
1508
|
-
}
|
|
1509
|
-
const transformedFiles = [];
|
|
1510
|
-
const skippedFiles = [];
|
|
1511
|
-
let transformed = 0;
|
|
1512
|
-
for (const [file, items] of [...itemsByFile.entries()].sort(([a], [b]) => a.localeCompare(b))) {
|
|
1513
|
-
const code = await source.readFile(file);
|
|
1514
|
-
if (!hasOpenFeatureClientBinding(code)) {
|
|
1515
|
-
skippedFiles.push({
|
|
1516
|
-
file,
|
|
1517
|
-
reason: "skipped \u2014 OpenFeature client setup required. Review `flaglint migrate --dry-run` provider guidance first."
|
|
1518
|
-
});
|
|
1519
|
-
continue;
|
|
1520
|
-
}
|
|
1521
|
-
const replacements = [];
|
|
1522
|
-
for (const item of items) {
|
|
1523
|
-
const result = buildReplacement2(item, code);
|
|
1524
|
-
if ("reason" in result) continue;
|
|
1525
|
-
replacements.push(result);
|
|
1526
|
-
}
|
|
1527
|
-
if (replacements.length === 0) continue;
|
|
1528
|
-
const newCode = applyReplacements2(code, replacements);
|
|
1529
|
-
if (newCode === code) continue;
|
|
1530
|
-
await source.writeFile(file, newCode);
|
|
1531
|
-
transformedFiles.push(file);
|
|
1532
|
-
transformed += replacements.length;
|
|
1533
|
-
}
|
|
1534
|
-
return { transformed, skipped: skippedFiles.length, transformedFiles, skippedFiles };
|
|
1535
|
-
}
|
|
1536
|
-
|
|
1537
1455
|
// src/commands/migrate.ts
|
|
1538
1456
|
function registerMigrateCommand(program2) {
|
|
1539
1457
|
program2.command("migrate").description("Analyze migration readiness and generate an OpenFeature migration plan").argument("[dir]", "directory to analyze", process.cwd()).option("-o, --output <file>", "write migration plan to file", "MIGRATION.md").option("-c, --config <path>", "path to .flaglintrc config file").option("--dry-run", "print reviewable diffs to stdout without writing files").option("--apply", "apply safe transformations to source files in-place").option("--allow-dirty", "allow --apply on a dirty git working tree").option("--exclude-tests", "exclude test files (*.test.*, *.spec.*, __tests__/, tests/)").addHelpText(
|
|
@@ -1640,14 +1558,14 @@ Examples:
|
|
|
1640
1558
|
)
|
|
1641
1559
|
);
|
|
1642
1560
|
if (options.dryRun) {
|
|
1643
|
-
const report2 = await formatDryRunDiff(analysis, source);
|
|
1561
|
+
const report2 = await formatDryRunDiff(analysis, source, config.openFeatureClientBindings);
|
|
1644
1562
|
process.stdout.write(report2 + "\n");
|
|
1645
1563
|
process.exit(0);
|
|
1646
1564
|
}
|
|
1647
1565
|
if (options.apply) {
|
|
1648
1566
|
let result;
|
|
1649
1567
|
try {
|
|
1650
|
-
result = await applyMigration(analysis, source, { allowDirty: options.allowDirty });
|
|
1568
|
+
result = await applyMigration(analysis, source, { allowDirty: options.allowDirty, allowedOpenFeatureClientBindings: config.openFeatureClientBindings });
|
|
1651
1569
|
} catch (err) {
|
|
1652
1570
|
if (err instanceof ApplyError && err.kind === "dirty-tree") {
|
|
1653
1571
|
process.stderr.write(chalk2.red(`
|
|
@@ -1710,12 +1628,15 @@ Error: ${err.message}
|
|
|
1710
1628
|
}
|
|
1711
1629
|
|
|
1712
1630
|
// src/commands/validate.ts
|
|
1631
|
+
import { writeFile as writeFile4 } from "fs/promises";
|
|
1713
1632
|
import { stat as stat3 } from "fs/promises";
|
|
1714
|
-
import { resolve as
|
|
1633
|
+
import { resolve as resolve7 } from "path";
|
|
1715
1634
|
import chalk3 from "chalk";
|
|
1716
1635
|
import ora3 from "ora";
|
|
1717
1636
|
|
|
1718
1637
|
// src/validator/index.ts
|
|
1638
|
+
import { resolve as resolve6 } from "path";
|
|
1639
|
+
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
1719
1640
|
function matchesBootstrapPattern(file, patterns) {
|
|
1720
1641
|
const clean = (s) => s.replace(/^\.\//, "");
|
|
1721
1642
|
const cleanFile = clean(file);
|
|
@@ -1786,8 +1707,106 @@ function formatValidationReport(result, options = {}) {
|
|
|
1786
1707
|
lines.push("Run `flaglint migrate --dry-run` to review the migration plan.");
|
|
1787
1708
|
return lines.join("\n") + "\n";
|
|
1788
1709
|
}
|
|
1710
|
+
var SARIF_RULE_NO_DIRECT_LD = {
|
|
1711
|
+
id: "flaglint.direct-launchdarkly",
|
|
1712
|
+
name: "DirectLaunchDarklySDKUsage",
|
|
1713
|
+
shortDescription: {
|
|
1714
|
+
text: "Direct LaunchDarkly SDK evaluation call detected"
|
|
1715
|
+
},
|
|
1716
|
+
fullDescription: {
|
|
1717
|
+
text: "A direct LaunchDarkly Node.js server SDK evaluation call was found. Migrate this call to OpenFeature so the codebase is provider-independent."
|
|
1718
|
+
},
|
|
1719
|
+
helpUri: "https://github.com/flaglint/flaglint#flaglint-validate-dir",
|
|
1720
|
+
properties: {
|
|
1721
|
+
tags: ["openfeature", "migration", "launchdarkly"]
|
|
1722
|
+
}
|
|
1723
|
+
};
|
|
1724
|
+
function sarifViolationUri(file) {
|
|
1725
|
+
return file.split(/[\\/]/).join("/");
|
|
1726
|
+
}
|
|
1727
|
+
function sarifScanRootUri(scanRoot) {
|
|
1728
|
+
const uri = pathToFileURL2(resolve6(scanRoot)).href;
|
|
1729
|
+
return uri.endsWith("/") ? uri : `${uri}/`;
|
|
1730
|
+
}
|
|
1731
|
+
function violationSarifMessage(v) {
|
|
1732
|
+
if (v.isDynamic) {
|
|
1733
|
+
return `Direct LaunchDarkly SDK call ${v.callType}() with a dynamic flag key at ${v.file}:${v.line}. Migrate to OpenFeature using flaglint migrate --dry-run.`;
|
|
1734
|
+
}
|
|
1735
|
+
if (v.flagKey === "*") {
|
|
1736
|
+
return `Direct LaunchDarkly bulk inventory call ${v.callType}() at ${v.file}:${v.line}. Migrate to OpenFeature using flaglint migrate --dry-run.`;
|
|
1737
|
+
}
|
|
1738
|
+
return `Direct LaunchDarkly SDK call ${v.callType}("${v.flagKey}") at ${v.file}:${v.line}. Migrate to OpenFeature using flaglint migrate --dry-run.`;
|
|
1739
|
+
}
|
|
1740
|
+
function formatValidationSarif(result, scanRoot, scannedAt) {
|
|
1741
|
+
return JSON.stringify(
|
|
1742
|
+
{
|
|
1743
|
+
$schema: "https://json.schemastore.org/sarif-2.1.0.json",
|
|
1744
|
+
version: "2.1.0",
|
|
1745
|
+
runs: [
|
|
1746
|
+
{
|
|
1747
|
+
tool: {
|
|
1748
|
+
driver: {
|
|
1749
|
+
name: "FlagLint",
|
|
1750
|
+
informationUri: "https://github.com/flaglint/flaglint",
|
|
1751
|
+
rules: [SARIF_RULE_NO_DIRECT_LD]
|
|
1752
|
+
}
|
|
1753
|
+
},
|
|
1754
|
+
invocations: [
|
|
1755
|
+
{
|
|
1756
|
+
executionSuccessful: true,
|
|
1757
|
+
startTimeUtc: scannedAt,
|
|
1758
|
+
properties: {
|
|
1759
|
+
scannedFiles: result.scannedFiles,
|
|
1760
|
+
totalUsages: result.totalUsages,
|
|
1761
|
+
violations: result.violations.length,
|
|
1762
|
+
passed: result.passed
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
],
|
|
1766
|
+
originalUriBaseIds: {
|
|
1767
|
+
"%SRCROOT%": {
|
|
1768
|
+
uri: sarifScanRootUri(scanRoot)
|
|
1769
|
+
}
|
|
1770
|
+
},
|
|
1771
|
+
results: result.violations.map((v) => ({
|
|
1772
|
+
ruleId: SARIF_RULE_NO_DIRECT_LD.id,
|
|
1773
|
+
level: "error",
|
|
1774
|
+
message: {
|
|
1775
|
+
text: violationSarifMessage(v)
|
|
1776
|
+
},
|
|
1777
|
+
locations: [
|
|
1778
|
+
{
|
|
1779
|
+
physicalLocation: {
|
|
1780
|
+
artifactLocation: {
|
|
1781
|
+
uri: sarifViolationUri(v.file),
|
|
1782
|
+
uriBaseId: "%SRCROOT%"
|
|
1783
|
+
},
|
|
1784
|
+
region: {
|
|
1785
|
+
startLine: Math.max(v.line, 1),
|
|
1786
|
+
startColumn: Math.max(v.column + 1, 1)
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
],
|
|
1791
|
+
partialFingerprints: {
|
|
1792
|
+
"flagKey/v1": v.flagKey
|
|
1793
|
+
},
|
|
1794
|
+
properties: {
|
|
1795
|
+
flagKey: v.flagKey,
|
|
1796
|
+
callType: v.callType,
|
|
1797
|
+
isDynamic: v.isDynamic
|
|
1798
|
+
}
|
|
1799
|
+
}))
|
|
1800
|
+
}
|
|
1801
|
+
]
|
|
1802
|
+
},
|
|
1803
|
+
null,
|
|
1804
|
+
2
|
|
1805
|
+
);
|
|
1806
|
+
}
|
|
1789
1807
|
|
|
1790
1808
|
// src/commands/validate.ts
|
|
1809
|
+
var VALID_VALIDATE_FORMATS = ["text", "sarif"];
|
|
1791
1810
|
function registerValidateCommand(program2) {
|
|
1792
1811
|
program2.command("validate").description("Validate that your codebase complies with feature flag usage policies").argument("[dir]", "directory to validate", process.cwd()).option(
|
|
1793
1812
|
"--no-direct-launchdarkly",
|
|
@@ -1797,7 +1816,11 @@ function registerValidateCommand(program2) {
|
|
|
1797
1816
|
"glob pattern for files allowed to use LaunchDarkly SDK directly (repeatable)",
|
|
1798
1817
|
(val, prev) => [...prev, val],
|
|
1799
1818
|
[]
|
|
1800
|
-
).option(
|
|
1819
|
+
).option(
|
|
1820
|
+
"-f, --format <format>",
|
|
1821
|
+
"output format: text | sarif",
|
|
1822
|
+
"text"
|
|
1823
|
+
).option("-o, --output <file>", "write report to file instead of stdout").option("-c, --config <path>", "path to .flaglintrc config file").addHelpText(
|
|
1801
1824
|
"after",
|
|
1802
1825
|
`
|
|
1803
1826
|
Examples:
|
|
@@ -1807,13 +1830,25 @@ Examples:
|
|
|
1807
1830
|
--bootstrap-exclude src/provider/setup.ts allow bootstrap file
|
|
1808
1831
|
$ flaglint validate --no-direct-launchdarkly \\
|
|
1809
1832
|
--bootstrap-exclude "src/provider/**" allow all provider files
|
|
1833
|
+
$ flaglint validate --no-direct-launchdarkly \\
|
|
1834
|
+
--format sarif --output flaglint.sarif emit SARIF for GitHub Code Scanning
|
|
1810
1835
|
$ flaglint validate --no-direct-launchdarkly \\
|
|
1811
1836
|
--bootstrap-exclude "src/provider/*.ts" \\
|
|
1812
1837
|
--bootstrap-exclude "src/bootstrap/**" multiple exclusion patterns`
|
|
1813
1838
|
).action(
|
|
1814
1839
|
async (dir, options) => {
|
|
1840
|
+
if (!VALID_VALIDATE_FORMATS.includes(options.format)) {
|
|
1841
|
+
process.stderr.write(
|
|
1842
|
+
chalk3.red(
|
|
1843
|
+
`Error: Invalid format '${options.format}'. Must be one of: ${VALID_VALIDATE_FORMATS.join(", ")}
|
|
1844
|
+
`
|
|
1845
|
+
)
|
|
1846
|
+
);
|
|
1847
|
+
process.exit(2);
|
|
1848
|
+
}
|
|
1849
|
+
const format = options.format;
|
|
1815
1850
|
try {
|
|
1816
|
-
const s = await stat3(
|
|
1851
|
+
const s = await stat3(resolve7(dir));
|
|
1817
1852
|
if (!s.isDirectory()) {
|
|
1818
1853
|
process.stderr.write(chalk3.red(`Error: Not a directory: ${dir}
|
|
1819
1854
|
`));
|
|
@@ -1868,8 +1903,34 @@ Examples:
|
|
|
1868
1903
|
bootstrapExclude
|
|
1869
1904
|
};
|
|
1870
1905
|
const validationResult = validateScanResult(scanResult, validateOptions);
|
|
1871
|
-
|
|
1872
|
-
|
|
1906
|
+
let report;
|
|
1907
|
+
if (format === "sarif") {
|
|
1908
|
+
report = formatValidationSarif(
|
|
1909
|
+
validationResult,
|
|
1910
|
+
scanResult.scanRoot,
|
|
1911
|
+
scanResult.scannedAt
|
|
1912
|
+
);
|
|
1913
|
+
} else {
|
|
1914
|
+
report = formatValidationReport(validationResult, validateOptions);
|
|
1915
|
+
}
|
|
1916
|
+
if (options.output) {
|
|
1917
|
+
const outPath = resolve7(options.output);
|
|
1918
|
+
try {
|
|
1919
|
+
await writeFile4(outPath, report, "utf8");
|
|
1920
|
+
process.stderr.write(chalk3.dim(` Report written to ${options.output}
|
|
1921
|
+
`));
|
|
1922
|
+
} catch (err) {
|
|
1923
|
+
process.stderr.write(
|
|
1924
|
+
chalk3.red(
|
|
1925
|
+
`Error: Failed to write report to ${options.output}: ${err instanceof Error ? err.message : String(err)}
|
|
1926
|
+
`
|
|
1927
|
+
)
|
|
1928
|
+
);
|
|
1929
|
+
process.exit(1);
|
|
1930
|
+
}
|
|
1931
|
+
} else {
|
|
1932
|
+
process.stdout.write(report);
|
|
1933
|
+
}
|
|
1873
1934
|
if (!validationResult.passed) {
|
|
1874
1935
|
process.exit(1);
|
|
1875
1936
|
}
|
|
@@ -1881,7 +1942,7 @@ Examples:
|
|
|
1881
1942
|
// src/cli.ts
|
|
1882
1943
|
function createCLI() {
|
|
1883
1944
|
const program2 = new Command();
|
|
1884
|
-
program2.name("flaglint").description("LaunchDarkly Node.js server SDK -> OpenFeature migration.").version("0.
|
|
1945
|
+
program2.name("flaglint").description("LaunchDarkly Node.js server SDK -> OpenFeature migration.").version("0.5.0", "-v, --version", "output the current version").addHelpText(
|
|
1885
1946
|
"after",
|
|
1886
1947
|
`
|
|
1887
1948
|
Examples:
|