flaglint 0.2.1 → 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 CHANGED
@@ -5,6 +5,21 @@ 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.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.
14
+
15
+ ### Fixed
16
+
17
+ - **Config mutation**: `--exclude-tests` no longer mutates the loaded config object in both `scan` and `migrate` commands — uses spread instead of `push()` so the original config is never modified.
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.
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.
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.
22
+
8
23
  ## [0.2.1] - 2026-05-23
9
24
 
10
25
  ### Fixed
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  </p>
4
4
 
5
5
  <p align="center">
6
- <strong>Find stale feature flags. Detect flag debt. Plan your OpenFeature migration.</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
- **Find stale feature flags. Detect flag debt. Plan your OpenFeature migration.**
28
-
29
- [![CI](https://github.com/flaglint/flaglint/actions/workflows/ci.yml/badge.svg)](https://github.com/flaglint/flaglint/actions/workflows/ci.yml)
30
- [![npm version](https://img.shields.io/npm/v/flaglint.svg)](https://www.npmjs.com/package/flaglint)
31
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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 `.flaglintrc` |
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 `.flaglintrc` |
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` flag → `.flaglintrc` → `.flaglintrc.json` → `flaglint.config.json`.
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 a **stale heuristic** based on key names and file locations.
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).
@@ -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 resolve2 } from "path";
9
+ import { resolve as resolve4 } from "path";
10
10
  import chalk from "chalk";
11
11
  import ora from "ora";
12
12
 
@@ -188,8 +188,9 @@ function detectUsages(ast, filePath, wrappers) {
188
188
  });
189
189
  return usages;
190
190
  }
191
- async function scan(source, config, onProgress) {
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(
@@ -207,7 +208,7 @@ async function scan(source, config, onProgress) {
207
208
  code = await source.readFile(file);
208
209
  } catch (err) {
209
210
  const fsCode = err.code ?? "UNKNOWN";
210
- return { usages: [], warning: `warn: could not read ${file} (${fsCode})` };
211
+ return { usages: [], warning: { kind: "read-failure", file, fsCode } };
211
212
  }
212
213
  let ast;
213
214
  try {
@@ -220,7 +221,7 @@ async function scan(source, config, onProgress) {
220
221
  filePath: file
221
222
  });
222
223
  } catch {
223
- return { usages: [], warning: `warn: failed to parse ${file}` };
224
+ return { usages: [], warning: { kind: "parse-failure", file } };
224
225
  }
225
226
  return { usages: detectUsages(ast, file, config.wrappers), warning: null };
226
227
  }
@@ -261,12 +262,17 @@ async function scan(source, config, onProgress) {
261
262
  }
262
263
  }
263
264
  }
265
+ if (evaluator) {
266
+ await evaluator.evaluate(allUsages, config);
267
+ }
264
268
  const uniqueFlags = [
265
269
  ...new Set(
266
270
  allUsages.filter((u) => !u.isDynamic && u.flagKey !== "*").map((u) => u.flagKey)
267
271
  )
268
272
  ];
269
273
  return {
274
+ scannedAt,
275
+ scanRoot: source.root ?? ".",
270
276
  scannedFiles,
271
277
  totalUsages: allUsages.length,
272
278
  uniqueFlags,
@@ -279,12 +285,14 @@ async function scan(source, config, onProgress) {
279
285
  // src/scanner/local-source.ts
280
286
  import fg from "fast-glob";
281
287
  import { readFile } from "fs/promises";
282
- import { join, relative } from "path";
288
+ import { join, relative, resolve } from "path";
283
289
  var LocalFileSource = class {
284
290
  constructor(dir) {
285
291
  this.dir = dir;
292
+ this.root = resolve(dir);
286
293
  }
287
294
  dir;
295
+ root;
288
296
  async listFiles(include, exclude) {
289
297
  const files = await fg(include, {
290
298
  cwd: this.dir,
@@ -299,6 +307,10 @@ var LocalFileSource = class {
299
307
  }
300
308
  };
301
309
 
310
+ // src/reporter/index.ts
311
+ import { resolve as resolve2 } from "path";
312
+ import { pathToFileURL } from "url";
313
+
302
314
  // src/types.ts
303
315
  var isStale = (u) => u.stalenessSignals.length > 0;
304
316
 
@@ -328,11 +340,11 @@ function sortedFlagEntries(map) {
328
340
  }
329
341
  function formatMarkdown(result, options) {
330
342
  const { scannedFiles, totalUsages, uniqueFlags, usages, scanDurationMs } = result;
331
- const staleUsages = usages.filter(isStale);
343
+ const staleUsages = usages.filter((u) => isStale(u) && !u.isDynamic && u.flagKey !== "*");
332
344
  const dynamicUsages = usages.filter((u) => u.isDynamic);
333
345
  const flagMap = buildFlagMap(usages);
334
346
  const sorted = sortedFlagEntries(flagMap);
335
- const staleFlags = sorted.filter(([, d]) => d.isStale);
347
+ const staleFlags = sorted.filter(([key, d]) => key !== "*" && d.isStale);
336
348
  const lines = [];
337
349
  lines.push("# FlagLint Scan Report");
338
350
  if (options.title) lines.push("", options.title);
@@ -388,13 +400,128 @@ function formatMarkdown(result, options) {
388
400
  return lines.join("\n");
389
401
  }
390
402
  function formatJSON(result) {
391
- return JSON.stringify({ generatedAt: (/* @__PURE__ */ new Date()).toISOString(), ...result }, null, 2);
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
+ );
392
517
  }
393
518
  function formatHTML(result, options) {
394
519
  const { scannedFiles, totalUsages, uniqueFlags, usages, scanDurationMs } = result;
395
- const staleCount = new Set(usages.filter(isStale).map((u) => u.flagKey)).size;
520
+ const staleCount = new Set(
521
+ usages.filter((u) => isStale(u) && !u.isDynamic && u.flagKey !== "*").map((u) => u.flagKey)
522
+ ).size;
396
523
  const dynamicCount = usages.filter((u) => u.isDynamic).length;
397
- const date = (/* @__PURE__ */ new Date()).toLocaleString();
524
+ const date = new Date(result.scannedAt).toLocaleString();
398
525
  const flagMap = buildFlagMap(usages);
399
526
  const sorted = sortedFlagEntries(flagMap);
400
527
  const rows = sorted.map(([key, data]) => {
@@ -404,7 +531,7 @@ function formatHTML(result, options) {
404
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>`;
405
532
  }).join("\n ");
406
533
  const title = options.title ? esc(options.title) : "FlagLint Scan Report";
407
- const version = true ? "0.2.1" : "0.1.0";
534
+ const version = true ? "0.3.0" : "0.1.0";
408
535
  return `<!DOCTYPE html>
409
536
  <html lang="en">
410
537
  <head>
@@ -479,14 +606,16 @@ function formatReport(result, options) {
479
606
  return formatJSON(result);
480
607
  case "html":
481
608
  return formatHTML(result, options);
482
- default:
609
+ case "markdown":
483
610
  return formatMarkdown(result, options);
611
+ case "sarif":
612
+ return formatSARIF(result);
484
613
  }
485
614
  }
486
615
 
487
616
  // src/config.ts
488
617
  import { readFile as readFile2, access } from "fs/promises";
489
- import { resolve } from "path";
618
+ import { resolve as resolve3 } from "path";
490
619
  import { z, ZodError } from "zod";
491
620
  var FlagLintConfigSchema = z.object({
492
621
  include: z.array(z.string()).default(["**/*.{ts,tsx,js,jsx}"]),
@@ -509,7 +638,7 @@ var SEARCH_PATHS = [".flaglintrc", ".flaglintrc.json", "flaglint.config.json"];
509
638
  async function loadConfig(configPath) {
510
639
  const candidates = configPath ? [configPath] : SEARCH_PATHS;
511
640
  for (const candidate of candidates) {
512
- const full = resolve(candidate);
641
+ const full = resolve3(candidate);
513
642
  try {
514
643
  await access(full);
515
644
  } catch {
@@ -535,15 +664,16 @@ async function loadConfig(configPath) {
535
664
  }
536
665
 
537
666
  // src/commands/scan.ts
538
- var VALID_FORMATS = ["json", "markdown", "html"];
667
+ var VALID_FORMATS = ["json", "markdown", "html", "sarif"];
539
668
  function registerScanCommand(program2) {
540
- 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(
541
670
  "after",
542
671
  `
543
672
  Examples:
544
673
  $ flaglint scan scan current directory
545
674
  $ flaglint scan ./src scan specific directory
546
675
  $ flaglint scan --format json output as JSON
676
+ $ flaglint scan --format sarif output as SARIF for GitHub Code Scanning
547
677
  $ flaglint scan --output report.md save to file
548
678
  $ flaglint scan --exclude-tests skip test and spec files`
549
679
  ).action(
@@ -558,7 +688,7 @@ Examples:
558
688
  process.exit(2);
559
689
  }
560
690
  try {
561
- const s = await stat(resolve2(dir));
691
+ const s = await stat(resolve4(dir));
562
692
  if (!s.isDirectory()) {
563
693
  process.stderr.write(chalk.red(`Error: Not a directory: ${dir}
564
694
  `));
@@ -585,16 +715,15 @@ Examples:
585
715
  process.stderr.write(chalk.red(String(err instanceof Error ? err.message : err)) + "\n");
586
716
  process.exit(1);
587
717
  }
588
- if (options.excludeTests) {
589
- config.exclude.push(
590
- "**/*.test.ts",
591
- "**/*.test.tsx",
592
- "**/*.spec.ts",
593
- "**/*.spec.tsx",
594
- "**/__tests__/**",
595
- "**/tests/**"
596
- );
597
- }
718
+ const TEST_PATTERNS = [
719
+ "**/*.test.ts",
720
+ "**/*.test.tsx",
721
+ "**/*.spec.ts",
722
+ "**/*.spec.tsx",
723
+ "**/__tests__/**",
724
+ "**/tests/**"
725
+ ];
726
+ const scanConfig = options.excludeTests ? { ...config, exclude: [...config.exclude, ...TEST_PATTERNS] } : config;
598
727
  const format = options.format;
599
728
  const spinner = ora(`Scanning ${dir}...`).start();
600
729
  process.once("SIGINT", () => {
@@ -604,7 +733,7 @@ Examples:
604
733
  let lastSpinnerUpdate = 0;
605
734
  let result;
606
735
  try {
607
- result = await scan(new LocalFileSource(dir), config, (filesScanned) => {
736
+ result = await scan(new LocalFileSource(dir), scanConfig, (filesScanned) => {
608
737
  if (filesScanned - lastSpinnerUpdate >= 50) {
609
738
  spinner.text = `Scanning... (${filesScanned} files)`;
610
739
  lastSpinnerUpdate = filesScanned;
@@ -617,7 +746,8 @@ Examples:
617
746
  process.exit(1);
618
747
  }
619
748
  for (const w of result.warnings) {
620
- process.stderr.write(chalk.yellow(w + "\n"));
749
+ const msg = w.kind === "read-failure" ? `warn: could not read ${w.file} (${w.fsCode})` : `warn: failed to parse ${w.file}`;
750
+ process.stderr.write(chalk.yellow(msg + "\n"));
621
751
  }
622
752
  if (result.scannedFiles === 0) {
623
753
  process.stderr.write(
@@ -658,7 +788,7 @@ Examples:
658
788
  }
659
789
  const report = formatReport(result, { format, title: config.reportTitle });
660
790
  if (options.output) {
661
- const outPath = resolve2(options.output);
791
+ const outPath = resolve4(options.output);
662
792
  try {
663
793
  await writeFile(outPath, report, "utf8");
664
794
  process.stderr.write(chalk.dim(` Report written to ${options.output}
@@ -683,7 +813,7 @@ Examples:
683
813
  // src/commands/migrate.ts
684
814
  import { writeFile as writeFile2 } from "fs/promises";
685
815
  import { stat as stat2 } from "fs/promises";
686
- import { resolve as resolve3 } from "path";
816
+ import { resolve as resolve5 } from "path";
687
817
  import chalk2 from "chalk";
688
818
  import ora2 from "ora";
689
819
 
@@ -824,7 +954,7 @@ function analyze(result) {
824
954
  function formatMigrationReport(analysis) {
825
955
  const { readinessScore, requiredPackages, items, manualReviewCount, autoMigrateCount } = analysis;
826
956
  const date = (/* @__PURE__ */ new Date()).toLocaleDateString();
827
- const version = true ? "0.2.1" : "0.1.0";
957
+ const version = true ? "0.3.0" : "0.1.0";
828
958
  let scoreLabel;
829
959
  if (readinessScore >= 80) scoreLabel = "\u2713 Your codebase is ready for migration";
830
960
  else if (readinessScore >= 50) scoreLabel = "\u26A0 Some manual work required before migration";
@@ -910,7 +1040,7 @@ Examples:
910
1040
  ).action(
911
1041
  async (dir, options) => {
912
1042
  try {
913
- const s = await stat2(resolve3(dir));
1043
+ const s = await stat2(resolve5(dir));
914
1044
  if (!s.isDirectory()) {
915
1045
  process.stderr.write(chalk2.red(`Error: Not a directory: ${dir}
916
1046
  `));
@@ -937,16 +1067,15 @@ Examples:
937
1067
  process.stderr.write(chalk2.red(String(err instanceof Error ? err.message : err)) + "\n");
938
1068
  process.exit(1);
939
1069
  }
940
- if (options.excludeTests) {
941
- config.exclude.push(
942
- "**/*.test.ts",
943
- "**/*.test.tsx",
944
- "**/*.spec.ts",
945
- "**/*.spec.tsx",
946
- "**/__tests__/**",
947
- "**/tests/**"
948
- );
949
- }
1070
+ const TEST_PATTERNS = [
1071
+ "**/*.test.ts",
1072
+ "**/*.test.tsx",
1073
+ "**/*.spec.ts",
1074
+ "**/*.spec.tsx",
1075
+ "**/__tests__/**",
1076
+ "**/tests/**"
1077
+ ];
1078
+ const scanConfig = options.excludeTests ? { ...config, exclude: [...config.exclude, ...TEST_PATTERNS] } : config;
950
1079
  const spinner = ora2(`Scanning ${dir}...`).start();
951
1080
  process.once("SIGINT", () => {
952
1081
  spinner.stop();
@@ -954,7 +1083,7 @@ Examples:
954
1083
  });
955
1084
  let scanResult;
956
1085
  try {
957
- scanResult = await scan(new LocalFileSource(dir), config, (filesScanned) => {
1086
+ scanResult = await scan(new LocalFileSource(dir), scanConfig, (filesScanned) => {
958
1087
  spinner.text = `Scanning files... ${filesScanned}`;
959
1088
  });
960
1089
  spinner.text = "Analyzing migration readiness...";
@@ -983,7 +1112,8 @@ Examples:
983
1112
  const analysis = analyze(scanResult);
984
1113
  spinner.stop();
985
1114
  for (const w of scanResult.warnings) {
986
- process.stderr.write(chalk2.yellow(w + "\n"));
1115
+ const msg = w.kind === "read-failure" ? `warn: could not read ${w.file} (${w.fsCode})` : `warn: failed to parse ${w.file}`;
1116
+ process.stderr.write(chalk2.yellow(msg + "\n"));
987
1117
  }
988
1118
  const { readinessScore } = analysis;
989
1119
  const scoreColor = readinessScore >= 80 ? chalk2.green : readinessScore >= 50 ? chalk2.yellow : chalk2.red;
@@ -1000,7 +1130,7 @@ Examples:
1000
1130
  process.stdout.write(report + "\n");
1001
1131
  process.exit(0);
1002
1132
  }
1003
- const outPath = resolve3(options.output);
1133
+ const outPath = resolve5(options.output);
1004
1134
  try {
1005
1135
  await writeFile2(outPath, report, "utf8");
1006
1136
  process.stderr.write(chalk2.green(`Migration plan written to ${options.output}
@@ -1022,7 +1152,7 @@ Examples:
1022
1152
  // src/cli.ts
1023
1153
  function createCLI() {
1024
1154
  const program2 = new Command();
1025
- program2.name("flaglint").description("Find stale feature flags. Detect flag debt. Plan your OpenFeature migration.").version("0.2.1", "-v, --version", "output the current version").addHelpText(
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(
1026
1156
  "after",
1027
1157
  `
1028
1158
  Examples:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flaglint",
3
- "version": "0.2.1",
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
- "build": "tsup",
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"