flaglint 0.2.2 → 0.3.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 +7 -1
- package/README.md +100 -12
- package/dist/bin/flaglint.js +147 -20
- package/package.json +4 -2
package/CHANGELOG.md
CHANGED
|
@@ -5,7 +5,12 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
-
## [0.
|
|
8
|
+
## [0.3.0] - 2026-05-23
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **SARIF output**: `flaglint scan --format sarif --output flaglint.sarif` now emits SARIF 2.1.0 for GitHub Code Scanning / PR annotations.
|
|
13
|
+
- **Persistent scan metadata**: `ScanResult` now includes `scannedAt` and `scanRoot`, giving JSON/SARIF/HTML reports a stable scan timestamp and source-root context.
|
|
9
14
|
|
|
10
15
|
### Fixed
|
|
11
16
|
|
|
@@ -13,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
13
18
|
- **Typed scan warnings**: `ScanResult.warnings` is now a typed `ScanWarning` union (`read-failure` | `parse-failure`) instead of opaque strings, preserving structured data at the domain boundary.
|
|
14
19
|
- **StalenessEvaluator wired**: The `StalenessEvaluator` interface now has a call site in `scan()` — pass an `evaluator` to inject API-based staleness signals without touching core scanner logic.
|
|
15
20
|
- **ScanConfig boundary**: `scan()` now accepts `ScanConfig` (scan-relevant fields only) rather than the full `FlagLintConfig`, decoupling the scanner from CLI output concerns (`reportTitle`, `outputDir`).
|
|
21
|
+
- **Report count consistency**: Markdown and HTML stale candidate counts now exclude wildcard (`*`) usages, matching the CLI summary.
|
|
16
22
|
|
|
17
23
|
## [0.2.1] - 2026-05-23
|
|
18
24
|
|
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
<p align="center">
|
|
6
|
-
<strong>
|
|
6
|
+
<strong>Your LaunchDarkly codebase has flag debt. FlagLint tells you exactly what and where.</strong>
|
|
7
7
|
</p>
|
|
8
8
|
|
|
9
9
|
<p align="center">
|
|
@@ -24,11 +24,8 @@
|
|
|
24
24
|
|
|
25
25
|
# FlagLint
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
[](https://github.com/flaglint/flaglint/actions/workflows/ci.yml)
|
|
30
|
-
[](https://www.npmjs.com/package/flaglint)
|
|
31
|
-
[](https://opensource.org/licenses/MIT)
|
|
27
|
+
Find zombie flags. Eliminate flag debt. Generate your OpenFeature
|
|
28
|
+
migration plan.
|
|
32
29
|
|
|
33
30
|
---
|
|
34
31
|
|
|
@@ -36,6 +33,8 @@
|
|
|
36
33
|
|
|
37
34
|
LaunchDarkly flags accumulate. Teams add them, forget to clean them up, and gradually build flag debt — dead code paths controlled by flags nobody manages. When you finally want to migrate to OpenFeature, you don't even know what you have.
|
|
38
35
|
|
|
36
|
+
Like Uber's Piranha — for any JS/TS codebase.
|
|
37
|
+
|
|
39
38
|
**FlagLint fixes this.** It scans your codebase, maps every flag usage, identifies stale candidates, and generates a step-by-step OpenFeature migration plan.
|
|
40
39
|
|
|
41
40
|
---
|
|
@@ -46,6 +45,49 @@ LaunchDarkly flags accumulate. Teams add them, forget to clean them up, and grad
|
|
|
46
45
|
npx flaglint scan
|
|
47
46
|
```
|
|
48
47
|
|
|
48
|
+
Example output:
|
|
49
|
+
|
|
50
|
+
```text
|
|
51
|
+
✓ 15 flag usages found across 6 unique flags (48ms)
|
|
52
|
+
⚠ 5 potentially stale flag(s) — review recommended
|
|
53
|
+
ℹ 1 dynamic flag key(s) require manual review
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Markdown report excerpt:
|
|
57
|
+
|
|
58
|
+
```markdown
|
|
59
|
+
## Flag Inventory
|
|
60
|
+
| Flag Key | Usages | Files | Call Types | Status |
|
|
61
|
+
|----------|--------|-------|------------|--------|
|
|
62
|
+
| show-banner | 1 | 1 | variation | ✓ Active |
|
|
63
|
+
| old-checkout | 1 | 1 | variation | ⚠ Stale |
|
|
64
|
+
| temp-debug-mode | 1 | 1 | variation | ⚠ Stale |
|
|
65
|
+
|
|
66
|
+
## ⚠ Stale Flag Candidates
|
|
67
|
+
| Flag Key | Reason | Location |
|
|
68
|
+
|----------|--------|----------|
|
|
69
|
+
| old-checkout | Contains "old" in key | ld-stale.ts:1 |
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### JSON output (`--format json`)
|
|
73
|
+
|
|
74
|
+
Pipe-friendly. Every usage includes file, line, call type,
|
|
75
|
+
and structured staleness signals:
|
|
76
|
+
|
|
77
|
+
```json
|
|
78
|
+
{
|
|
79
|
+
"flagKey": "old-checkout",
|
|
80
|
+
"isDynamic": false,
|
|
81
|
+
"file": "src/components/Checkout.tsx",
|
|
82
|
+
"line": 14,
|
|
83
|
+
"callType": "variation",
|
|
84
|
+
"stalenessSignals": [
|
|
85
|
+
{ "source": "keyword", "keyword": "old" },
|
|
86
|
+
{ "source": "minFileCount", "fileCount": 1, "threshold": 1 }
|
|
87
|
+
]
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
49
91
|
---
|
|
50
92
|
|
|
51
93
|
## Installation
|
|
@@ -68,13 +110,15 @@ Scans a directory for LaunchDarkly SDK usage.
|
|
|
68
110
|
flaglint scan ./src
|
|
69
111
|
flaglint scan --format json --output report.json
|
|
70
112
|
flaglint scan --format html --output report.html
|
|
113
|
+
flaglint scan --format sarif --output flaglint.sarif
|
|
71
114
|
```
|
|
72
115
|
|
|
73
116
|
| Option | Default | Description |
|
|
74
117
|
|--------|---------|-------------|
|
|
75
|
-
| `--format` | `markdown` | Output format: `json`, `markdown`, `html` |
|
|
118
|
+
| `--format` | `markdown` | Output format: `json`, `markdown`, `html`, `sarif` |
|
|
76
119
|
| `--output` | stdout | Write report to file |
|
|
77
|
-
| `--config` | auto-detect | Path to
|
|
120
|
+
| `--config` | auto-detect | Path to a config file |
|
|
121
|
+
| `--exclude-tests` | — | Exclude test files from scan results |
|
|
78
122
|
|
|
79
123
|
Exit code `0` when no stale flags found, `1` when stale flags exist — enabling CI blocking.
|
|
80
124
|
|
|
@@ -94,13 +138,13 @@ flaglint migrate --output MIGRATION.md
|
|
|
94
138
|
|--------|---------|-------------|
|
|
95
139
|
| `--output` | `MIGRATION.md` | Write migration plan to file |
|
|
96
140
|
| `--dry-run` | — | Print plan to stdout, do not write file |
|
|
97
|
-
| `--config` | auto-detect | Path to
|
|
141
|
+
| `--config` | auto-detect | Path to a config file |
|
|
98
142
|
|
|
99
143
|
---
|
|
100
144
|
|
|
101
145
|
## Configuration
|
|
102
146
|
|
|
103
|
-
Create `.flaglintrc` in your project root:
|
|
147
|
+
Create `.flaglintrc`, `.flaglintrc.json`, or `flaglint.config.json` in your project root:
|
|
104
148
|
|
|
105
149
|
```json
|
|
106
150
|
{
|
|
@@ -122,18 +166,53 @@ Create `.flaglintrc` in your project root:
|
|
|
122
166
|
| `reportTitle` | `string` | — | Custom title for generated reports |
|
|
123
167
|
| `outputDir` | `string` | `"."` | Default output directory |
|
|
124
168
|
|
|
125
|
-
FlagLint searches for config in this order: `--config`
|
|
169
|
+
FlagLint searches for config in this order: `--config` path → `.flaglintrc` → `.flaglintrc.json` → `flaglint.config.json`.
|
|
126
170
|
|
|
127
171
|
---
|
|
128
172
|
|
|
129
173
|
## CI Integration
|
|
130
174
|
|
|
175
|
+
### Basic — block PRs on stale flags
|
|
176
|
+
|
|
131
177
|
```yaml
|
|
132
178
|
- name: Check for stale flags
|
|
133
179
|
run: npx flaglint scan --format json --output flaglint-report.json
|
|
134
180
|
# exits 1 if stale flags found, blocking the PR
|
|
135
181
|
```
|
|
136
182
|
|
|
183
|
+
### GitHub PR annotations via SARIF
|
|
184
|
+
|
|
185
|
+
Stale flags appear as warnings directly in the PR diff —
|
|
186
|
+
no dashboard, no separate tool.
|
|
187
|
+
|
|
188
|
+
```yaml
|
|
189
|
+
name: FlagLint
|
|
190
|
+
on: [pull_request]
|
|
191
|
+
|
|
192
|
+
jobs:
|
|
193
|
+
flaglint:
|
|
194
|
+
runs-on: ubuntu-latest
|
|
195
|
+
permissions:
|
|
196
|
+
security-events: write
|
|
197
|
+
contents: read
|
|
198
|
+
steps:
|
|
199
|
+
- uses: actions/checkout@v4
|
|
200
|
+
- uses: actions/setup-node@v4
|
|
201
|
+
with:
|
|
202
|
+
node-version: 20
|
|
203
|
+
- name: Scan for flag debt
|
|
204
|
+
run: npx flaglint scan --format sarif --output flaglint.sarif
|
|
205
|
+
continue-on-error: true
|
|
206
|
+
- name: Upload to GitHub Code Scanning
|
|
207
|
+
uses: github/codeql-action/upload-sarif@v3
|
|
208
|
+
with:
|
|
209
|
+
sarif_file: flaglint.sarif
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Stale flags show up as Code Scanning alerts on the exact file
|
|
213
|
+
and line where the flag is used — reviewers see them in the PR
|
|
214
|
+
without running anything locally.
|
|
215
|
+
|
|
137
216
|
---
|
|
138
217
|
|
|
139
218
|
## What FlagLint detects
|
|
@@ -142,9 +221,10 @@ FlagLint searches for config in this order: `--config` flag → `.flaglintrc`
|
|
|
142
221
|
- `ldClient.allFlags()`
|
|
143
222
|
- `useFlags()`, `useLDClient()` React hooks
|
|
144
223
|
- `<LDProvider>` and `withLDConsumer()` patterns
|
|
224
|
+
- Custom wrapper calls such as `flagPredicate("my-flag", false)` when configured with `wrappers`
|
|
145
225
|
- Dynamic flag keys (runtime-determined, flagged for manual review)
|
|
146
226
|
|
|
147
|
-
All detections include the **file path**, **line number**, **call type**, and
|
|
227
|
+
All detections include the **file path**, **line number**, **call type**, and staleness signals based on key names, file locations, and low file counts.
|
|
148
228
|
|
|
149
229
|
---
|
|
150
230
|
|
|
@@ -169,6 +249,14 @@ See [CONTRIBUTING.md](./CONTRIBUTING.md).
|
|
|
169
249
|
|
|
170
250
|
---
|
|
171
251
|
|
|
252
|
+
## Free flag debt audit
|
|
253
|
+
|
|
254
|
+
Running this on a real codebase?
|
|
255
|
+
[Book a free 30-minute audit →](https://flaglint.dev#waitlist)
|
|
256
|
+
I'll run FlagLint on your repo and walk you through the results.
|
|
257
|
+
|
|
258
|
+
---
|
|
259
|
+
|
|
172
260
|
## License
|
|
173
261
|
|
|
174
262
|
MIT — see [LICENSE](./LICENSE).
|
package/dist/bin/flaglint.js
CHANGED
|
@@ -6,7 +6,7 @@ import { Command } from "commander";
|
|
|
6
6
|
// src/commands/scan.ts
|
|
7
7
|
import { writeFile } from "fs/promises";
|
|
8
8
|
import { stat } from "fs/promises";
|
|
9
|
-
import { resolve as
|
|
9
|
+
import { resolve as resolve4 } from "path";
|
|
10
10
|
import chalk from "chalk";
|
|
11
11
|
import ora from "ora";
|
|
12
12
|
|
|
@@ -190,6 +190,7 @@ function detectUsages(ast, filePath, wrappers) {
|
|
|
190
190
|
}
|
|
191
191
|
async function scan(source, config, onProgress, evaluator) {
|
|
192
192
|
const start = Date.now();
|
|
193
|
+
const scannedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
193
194
|
for (const pattern of config.include) {
|
|
194
195
|
if (pattern.startsWith("/") || pattern.startsWith("..")) {
|
|
195
196
|
throw new Error(
|
|
@@ -270,6 +271,8 @@ async function scan(source, config, onProgress, evaluator) {
|
|
|
270
271
|
)
|
|
271
272
|
];
|
|
272
273
|
return {
|
|
274
|
+
scannedAt,
|
|
275
|
+
scanRoot: source.root ?? ".",
|
|
273
276
|
scannedFiles,
|
|
274
277
|
totalUsages: allUsages.length,
|
|
275
278
|
uniqueFlags,
|
|
@@ -282,12 +285,14 @@ async function scan(source, config, onProgress, evaluator) {
|
|
|
282
285
|
// src/scanner/local-source.ts
|
|
283
286
|
import fg from "fast-glob";
|
|
284
287
|
import { readFile } from "fs/promises";
|
|
285
|
-
import { join, relative } from "path";
|
|
288
|
+
import { join, relative, resolve } from "path";
|
|
286
289
|
var LocalFileSource = class {
|
|
287
290
|
constructor(dir) {
|
|
288
291
|
this.dir = dir;
|
|
292
|
+
this.root = resolve(dir);
|
|
289
293
|
}
|
|
290
294
|
dir;
|
|
295
|
+
root;
|
|
291
296
|
async listFiles(include, exclude) {
|
|
292
297
|
const files = await fg(include, {
|
|
293
298
|
cwd: this.dir,
|
|
@@ -302,6 +307,10 @@ var LocalFileSource = class {
|
|
|
302
307
|
}
|
|
303
308
|
};
|
|
304
309
|
|
|
310
|
+
// src/reporter/index.ts
|
|
311
|
+
import { resolve as resolve2 } from "path";
|
|
312
|
+
import { pathToFileURL } from "url";
|
|
313
|
+
|
|
305
314
|
// src/types.ts
|
|
306
315
|
var isStale = (u) => u.stalenessSignals.length > 0;
|
|
307
316
|
|
|
@@ -331,11 +340,11 @@ function sortedFlagEntries(map) {
|
|
|
331
340
|
}
|
|
332
341
|
function formatMarkdown(result, options) {
|
|
333
342
|
const { scannedFiles, totalUsages, uniqueFlags, usages, scanDurationMs } = result;
|
|
334
|
-
const staleUsages = usages.filter(isStale);
|
|
343
|
+
const staleUsages = usages.filter((u) => isStale(u) && !u.isDynamic && u.flagKey !== "*");
|
|
335
344
|
const dynamicUsages = usages.filter((u) => u.isDynamic);
|
|
336
345
|
const flagMap = buildFlagMap(usages);
|
|
337
346
|
const sorted = sortedFlagEntries(flagMap);
|
|
338
|
-
const staleFlags = sorted.filter(([, d]) => d.isStale);
|
|
347
|
+
const staleFlags = sorted.filter(([key, d]) => key !== "*" && d.isStale);
|
|
339
348
|
const lines = [];
|
|
340
349
|
lines.push("# FlagLint Scan Report");
|
|
341
350
|
if (options.title) lines.push("", options.title);
|
|
@@ -391,13 +400,128 @@ function formatMarkdown(result, options) {
|
|
|
391
400
|
return lines.join("\n");
|
|
392
401
|
}
|
|
393
402
|
function formatJSON(result) {
|
|
394
|
-
return JSON.stringify({ generatedAt:
|
|
403
|
+
return JSON.stringify({ generatedAt: result.scannedAt, ...result }, null, 2);
|
|
404
|
+
}
|
|
405
|
+
function signalRuleId(usage) {
|
|
406
|
+
const signal = usage.stalenessSignals[0];
|
|
407
|
+
if (!signal) return "flaglint.stale";
|
|
408
|
+
return `flaglint.${signal.source}`;
|
|
409
|
+
}
|
|
410
|
+
function signalMessage(usage) {
|
|
411
|
+
const reasons = usage.stalenessSignals.map((signal) => {
|
|
412
|
+
switch (signal.source) {
|
|
413
|
+
case "keyword":
|
|
414
|
+
return `keyword "${signal.keyword}"`;
|
|
415
|
+
case "path":
|
|
416
|
+
return `path pattern "${signal.pattern}"`;
|
|
417
|
+
case "minFileCount":
|
|
418
|
+
return `file count ${signal.fileCount} <= ${signal.threshold}`;
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
return reasons.length > 0 ? reasons.join(", ") : "staleness signal";
|
|
422
|
+
}
|
|
423
|
+
function sarifUri(file) {
|
|
424
|
+
return file.split(/[\\/]/).join("/");
|
|
425
|
+
}
|
|
426
|
+
function sarifRootUri(scanRoot) {
|
|
427
|
+
const uri = pathToFileURL(resolve2(scanRoot)).href;
|
|
428
|
+
return uri.endsWith("/") ? uri : `${uri}/`;
|
|
429
|
+
}
|
|
430
|
+
function formatSARIF(result) {
|
|
431
|
+
const staleUsages = result.usages.filter((u) => isStale(u) && !u.isDynamic && u.flagKey !== "*");
|
|
432
|
+
const rules = [
|
|
433
|
+
{
|
|
434
|
+
id: "flaglint.keyword",
|
|
435
|
+
name: "Stale flag keyword",
|
|
436
|
+
shortDescription: { text: "Flag key contains a stale keyword" },
|
|
437
|
+
helpUri: "https://github.com/flaglint/flaglint#what-flaglint-detects"
|
|
438
|
+
},
|
|
439
|
+
{
|
|
440
|
+
id: "flaglint.path",
|
|
441
|
+
name: "Stale flag path",
|
|
442
|
+
shortDescription: { text: "Flag usage appears in a stale path" },
|
|
443
|
+
helpUri: "https://github.com/flaglint/flaglint#what-flaglint-detects"
|
|
444
|
+
},
|
|
445
|
+
{
|
|
446
|
+
id: "flaglint.minFileCount",
|
|
447
|
+
name: "Low file count",
|
|
448
|
+
shortDescription: { text: "Flag appears in too few files" },
|
|
449
|
+
helpUri: "https://github.com/flaglint/flaglint#configuration"
|
|
450
|
+
}
|
|
451
|
+
];
|
|
452
|
+
return JSON.stringify(
|
|
453
|
+
{
|
|
454
|
+
$schema: "https://json.schemastore.org/sarif-2.1.0.json",
|
|
455
|
+
version: "2.1.0",
|
|
456
|
+
runs: [
|
|
457
|
+
{
|
|
458
|
+
tool: {
|
|
459
|
+
driver: {
|
|
460
|
+
name: "FlagLint",
|
|
461
|
+
informationUri: "https://github.com/flaglint/flaglint",
|
|
462
|
+
rules
|
|
463
|
+
}
|
|
464
|
+
},
|
|
465
|
+
invocations: [
|
|
466
|
+
{
|
|
467
|
+
executionSuccessful: true,
|
|
468
|
+
startTimeUtc: result.scannedAt,
|
|
469
|
+
properties: {
|
|
470
|
+
scannedFiles: result.scannedFiles,
|
|
471
|
+
totalUsages: result.totalUsages,
|
|
472
|
+
uniqueFlags: result.uniqueFlags.length,
|
|
473
|
+
scanDurationMs: result.scanDurationMs
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
],
|
|
477
|
+
originalUriBaseIds: {
|
|
478
|
+
"%SRCROOT%": {
|
|
479
|
+
uri: sarifRootUri(result.scanRoot)
|
|
480
|
+
}
|
|
481
|
+
},
|
|
482
|
+
results: staleUsages.map((usage) => ({
|
|
483
|
+
ruleId: signalRuleId(usage),
|
|
484
|
+
level: "warning",
|
|
485
|
+
message: {
|
|
486
|
+
text: `Potentially stale feature flag "${usage.flagKey}" detected: ${signalMessage(usage)}.`
|
|
487
|
+
},
|
|
488
|
+
locations: [
|
|
489
|
+
{
|
|
490
|
+
physicalLocation: {
|
|
491
|
+
artifactLocation: {
|
|
492
|
+
uri: sarifUri(usage.file),
|
|
493
|
+
uriBaseId: "%SRCROOT%"
|
|
494
|
+
},
|
|
495
|
+
region: {
|
|
496
|
+
startLine: Math.max(usage.line, 1),
|
|
497
|
+
startColumn: Math.max(usage.column + 1, 1)
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
],
|
|
502
|
+
partialFingerprints: {
|
|
503
|
+
"flagKey/v1": usage.flagKey
|
|
504
|
+
},
|
|
505
|
+
properties: {
|
|
506
|
+
flagKey: usage.flagKey,
|
|
507
|
+
callType: usage.callType,
|
|
508
|
+
stalenessSignals: usage.stalenessSignals
|
|
509
|
+
}
|
|
510
|
+
}))
|
|
511
|
+
}
|
|
512
|
+
]
|
|
513
|
+
},
|
|
514
|
+
null,
|
|
515
|
+
2
|
|
516
|
+
);
|
|
395
517
|
}
|
|
396
518
|
function formatHTML(result, options) {
|
|
397
519
|
const { scannedFiles, totalUsages, uniqueFlags, usages, scanDurationMs } = result;
|
|
398
|
-
const staleCount = new Set(
|
|
520
|
+
const staleCount = new Set(
|
|
521
|
+
usages.filter((u) => isStale(u) && !u.isDynamic && u.flagKey !== "*").map((u) => u.flagKey)
|
|
522
|
+
).size;
|
|
399
523
|
const dynamicCount = usages.filter((u) => u.isDynamic).length;
|
|
400
|
-
const date =
|
|
524
|
+
const date = new Date(result.scannedAt).toLocaleString();
|
|
401
525
|
const flagMap = buildFlagMap(usages);
|
|
402
526
|
const sorted = sortedFlagEntries(flagMap);
|
|
403
527
|
const rows = sorted.map(([key, data]) => {
|
|
@@ -407,7 +531,7 @@ function formatHTML(result, options) {
|
|
|
407
531
|
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>`;
|
|
408
532
|
}).join("\n ");
|
|
409
533
|
const title = options.title ? esc(options.title) : "FlagLint Scan Report";
|
|
410
|
-
const version = true ? "0.
|
|
534
|
+
const version = true ? "0.3.0" : "0.1.0";
|
|
411
535
|
return `<!DOCTYPE html>
|
|
412
536
|
<html lang="en">
|
|
413
537
|
<head>
|
|
@@ -482,14 +606,16 @@ function formatReport(result, options) {
|
|
|
482
606
|
return formatJSON(result);
|
|
483
607
|
case "html":
|
|
484
608
|
return formatHTML(result, options);
|
|
485
|
-
|
|
609
|
+
case "markdown":
|
|
486
610
|
return formatMarkdown(result, options);
|
|
611
|
+
case "sarif":
|
|
612
|
+
return formatSARIF(result);
|
|
487
613
|
}
|
|
488
614
|
}
|
|
489
615
|
|
|
490
616
|
// src/config.ts
|
|
491
617
|
import { readFile as readFile2, access } from "fs/promises";
|
|
492
|
-
import { resolve } from "path";
|
|
618
|
+
import { resolve as resolve3 } from "path";
|
|
493
619
|
import { z, ZodError } from "zod";
|
|
494
620
|
var FlagLintConfigSchema = z.object({
|
|
495
621
|
include: z.array(z.string()).default(["**/*.{ts,tsx,js,jsx}"]),
|
|
@@ -512,7 +638,7 @@ var SEARCH_PATHS = [".flaglintrc", ".flaglintrc.json", "flaglint.config.json"];
|
|
|
512
638
|
async function loadConfig(configPath) {
|
|
513
639
|
const candidates = configPath ? [configPath] : SEARCH_PATHS;
|
|
514
640
|
for (const candidate of candidates) {
|
|
515
|
-
const full =
|
|
641
|
+
const full = resolve3(candidate);
|
|
516
642
|
try {
|
|
517
643
|
await access(full);
|
|
518
644
|
} catch {
|
|
@@ -538,15 +664,16 @@ async function loadConfig(configPath) {
|
|
|
538
664
|
}
|
|
539
665
|
|
|
540
666
|
// src/commands/scan.ts
|
|
541
|
-
var VALID_FORMATS = ["json", "markdown", "html"];
|
|
667
|
+
var VALID_FORMATS = ["json", "markdown", "html", "sarif"];
|
|
542
668
|
function registerScanCommand(program2) {
|
|
543
|
-
program2.command("scan").description("Scan a directory for feature flag usages and detect stale flags").argument("[dir]", "directory to scan", process.cwd()).option("-f, --format <format>", "output format: json | markdown | html", "markdown").option("-o, --output <file>", "write report to file").option("-c, --config <path>", "path to .flaglintrc config file").option("--exclude-tests", "exclude test files (*.test.*, *.spec.*, __tests__/, tests/)").addHelpText(
|
|
669
|
+
program2.command("scan").description("Scan a directory for feature flag usages and detect stale flags").argument("[dir]", "directory to scan", process.cwd()).option("-f, --format <format>", "output format: json | markdown | html | sarif", "markdown").option("-o, --output <file>", "write report to file").option("-c, --config <path>", "path to .flaglintrc config file").option("--exclude-tests", "exclude test files (*.test.*, *.spec.*, __tests__/, tests/)").addHelpText(
|
|
544
670
|
"after",
|
|
545
671
|
`
|
|
546
672
|
Examples:
|
|
547
673
|
$ flaglint scan scan current directory
|
|
548
674
|
$ flaglint scan ./src scan specific directory
|
|
549
675
|
$ flaglint scan --format json output as JSON
|
|
676
|
+
$ flaglint scan --format sarif output as SARIF for GitHub Code Scanning
|
|
550
677
|
$ flaglint scan --output report.md save to file
|
|
551
678
|
$ flaglint scan --exclude-tests skip test and spec files`
|
|
552
679
|
).action(
|
|
@@ -561,7 +688,7 @@ Examples:
|
|
|
561
688
|
process.exit(2);
|
|
562
689
|
}
|
|
563
690
|
try {
|
|
564
|
-
const s = await stat(
|
|
691
|
+
const s = await stat(resolve4(dir));
|
|
565
692
|
if (!s.isDirectory()) {
|
|
566
693
|
process.stderr.write(chalk.red(`Error: Not a directory: ${dir}
|
|
567
694
|
`));
|
|
@@ -661,7 +788,7 @@ Examples:
|
|
|
661
788
|
}
|
|
662
789
|
const report = formatReport(result, { format, title: config.reportTitle });
|
|
663
790
|
if (options.output) {
|
|
664
|
-
const outPath =
|
|
791
|
+
const outPath = resolve4(options.output);
|
|
665
792
|
try {
|
|
666
793
|
await writeFile(outPath, report, "utf8");
|
|
667
794
|
process.stderr.write(chalk.dim(` Report written to ${options.output}
|
|
@@ -686,7 +813,7 @@ Examples:
|
|
|
686
813
|
// src/commands/migrate.ts
|
|
687
814
|
import { writeFile as writeFile2 } from "fs/promises";
|
|
688
815
|
import { stat as stat2 } from "fs/promises";
|
|
689
|
-
import { resolve as
|
|
816
|
+
import { resolve as resolve5 } from "path";
|
|
690
817
|
import chalk2 from "chalk";
|
|
691
818
|
import ora2 from "ora";
|
|
692
819
|
|
|
@@ -827,7 +954,7 @@ function analyze(result) {
|
|
|
827
954
|
function formatMigrationReport(analysis) {
|
|
828
955
|
const { readinessScore, requiredPackages, items, manualReviewCount, autoMigrateCount } = analysis;
|
|
829
956
|
const date = (/* @__PURE__ */ new Date()).toLocaleDateString();
|
|
830
|
-
const version = true ? "0.
|
|
957
|
+
const version = true ? "0.3.0" : "0.1.0";
|
|
831
958
|
let scoreLabel;
|
|
832
959
|
if (readinessScore >= 80) scoreLabel = "\u2713 Your codebase is ready for migration";
|
|
833
960
|
else if (readinessScore >= 50) scoreLabel = "\u26A0 Some manual work required before migration";
|
|
@@ -913,7 +1040,7 @@ Examples:
|
|
|
913
1040
|
).action(
|
|
914
1041
|
async (dir, options) => {
|
|
915
1042
|
try {
|
|
916
|
-
const s = await stat2(
|
|
1043
|
+
const s = await stat2(resolve5(dir));
|
|
917
1044
|
if (!s.isDirectory()) {
|
|
918
1045
|
process.stderr.write(chalk2.red(`Error: Not a directory: ${dir}
|
|
919
1046
|
`));
|
|
@@ -1003,7 +1130,7 @@ Examples:
|
|
|
1003
1130
|
process.stdout.write(report + "\n");
|
|
1004
1131
|
process.exit(0);
|
|
1005
1132
|
}
|
|
1006
|
-
const outPath =
|
|
1133
|
+
const outPath = resolve5(options.output);
|
|
1007
1134
|
try {
|
|
1008
1135
|
await writeFile2(outPath, report, "utf8");
|
|
1009
1136
|
process.stderr.write(chalk2.green(`Migration plan written to ${options.output}
|
|
@@ -1025,7 +1152,7 @@ Examples:
|
|
|
1025
1152
|
// src/cli.ts
|
|
1026
1153
|
function createCLI() {
|
|
1027
1154
|
const program2 = new Command();
|
|
1028
|
-
program2.name("flaglint").description("Find stale feature flags. Detect flag debt. Plan your OpenFeature migration.").version("0.
|
|
1155
|
+
program2.name("flaglint").description("Find stale feature flags. Detect flag debt. Plan your OpenFeature migration.").version("0.3.0", "-v, --version", "output the current version").addHelpText(
|
|
1029
1156
|
"after",
|
|
1030
1157
|
`
|
|
1031
1158
|
Examples:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flaglint",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Find stale feature flags. Detect flag debt. Plan your OpenFeature migration.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -34,13 +34,15 @@
|
|
|
34
34
|
"url": "https://github.com/flaglint/flaglint/issues"
|
|
35
35
|
},
|
|
36
36
|
"scripts": {
|
|
37
|
-
"
|
|
37
|
+
"sync:www": "tsx scripts/sync-www.ts",
|
|
38
|
+
"build": "npm run sync:www && tsup",
|
|
38
39
|
"dev": "tsup --watch",
|
|
39
40
|
"typecheck": "tsc --noEmit",
|
|
40
41
|
"typecheck:agent": "tsc --project tsconfig.agent.json",
|
|
41
42
|
"test": "vitest",
|
|
42
43
|
"test:run": "vitest run",
|
|
43
44
|
"test:coverage": "vitest run --coverage",
|
|
45
|
+
"new-branch": "tsx scripts/new-branch.ts",
|
|
44
46
|
"release:patch": "tsx scripts/release.ts patch",
|
|
45
47
|
"release:minor": "tsx scripts/release.ts minor",
|
|
46
48
|
"release:major": "tsx scripts/release.ts major"
|