flaglint 0.1.5 → 0.2.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,39 @@ 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.2.0] - 2026-05-22
9
+
10
+ ### Breaking Changes
11
+
12
+ - **`FlagUsage.isStale: boolean` replaced with `stalenessSignals: StalenessSignal[]`**
13
+ The boolean had no provenance — you could not tell which signal (keyword, path, file-count, future LD API age) caused a flag to be marked stale. Replaced with a typed union array that records every signal that fired and why.
14
+
15
+ **Migration:** Replace `usage.isStale` checks with the exported helper:
16
+ ```typescript
17
+ import { isStale } from "flaglint";
18
+ if (isStale(usage)) { ... }
19
+ ```
20
+ JSON report consumers: the `usages[].isStale` field is gone. Use `usages[].stalenessSignals.length > 0` or the `isStale()` helper. Reports now include staleness provenance (source + keyword/pattern/count).
21
+
22
+ - **Renamed config field: `staleThreshold` → `minFileCount`**
23
+ The field was previously documented as "days before a flag is considered stale" but was actually implemented as a file-count threshold (a flag is stale if it appears in ≤ N files). The rename makes the actual behavior honest.
24
+
25
+ **Migration:** In your `flaglint.config.json` or `.flaglintrc`, rename the field:
26
+ ```json
27
+ // Before
28
+ { "staleThreshold": 5 }
29
+ // After
30
+ { "minFileCount": 5 }
31
+ ```
32
+
33
+ ### Changed
34
+
35
+ - Extracted shared stale detection logic (`STALE_KEYWORDS`, `checkStale`, `staleReason`) into `src/stale.ts` — single source of truth, eliminates duplicate keyword lists between scanner and reporter
36
+
37
+ ### Roadmap
38
+
39
+ - `v0.3`: Replace `minFileCount` with real date-based staleness detection via `git log` integration
40
+
8
41
  ## [0.1.5] - 2026-05-21
9
42
 
10
43
  ### Fixed
package/README.md CHANGED
@@ -107,7 +107,7 @@ Create `.flaglintrc` in your project root:
107
107
  "include": ["**/*.{ts,tsx,js,jsx}"],
108
108
  "exclude": ["**/node_modules/**", "**/dist/**"],
109
109
  "provider": "launchdarkly",
110
- "staleThreshold": 1,
110
+ "minFileCount": 1,
111
111
  "reportTitle": "My Project Flag Report"
112
112
  }
113
113
  ```
@@ -117,7 +117,7 @@ Create `.flaglintrc` in your project root:
117
117
  | `include` | `string[]` | `["**/*.{ts,tsx,js,jsx}"]` | Glob patterns to scan |
118
118
  | `exclude` | `string[]` | `["**/node_modules/**", ...]` | Glob patterns to ignore |
119
119
  | `provider` | `string` | `"launchdarkly"` | Feature flag provider |
120
- | `staleThreshold` | `number` | `1` | Days before a flag is considered stale |
120
+ | `minFileCount` | `number` | `1` | A flag is stale if it appears in ≤ N files (default: 1) |
121
121
  | `reportTitle` | `string` | — | Custom title for generated reports |
122
122
  | `outputDir` | `string` | `"."` | Default output directory |
123
123
 
@@ -11,16 +11,34 @@ import chalk from "chalk";
11
11
  import ora from "ora";
12
12
 
13
13
  // src/scanner/index.ts
14
- import { readFile } from "fs/promises";
15
- import { relative } from "path";
16
- import fg from "fast-glob";
14
+ import pLimit from "p-limit";
17
15
  import { parse } from "@typescript-eslint/typescript-estree";
18
- var LD_MEMBER_METHODS = /* @__PURE__ */ new Set(["variation", "variationDetail", "allFlags"]);
19
- var LD_CLIENT_PATTERN = /ld|client/i;
20
- var LD_HOOKS = /* @__PURE__ */ new Set(["useFlags", "useLDClient"]);
21
- var STALE_KEY_WORDS = ["old", "deprecated", "legacy", "temp", "tmp", "test", "demo"];
16
+
17
+ // src/stale.ts
18
+ var STALE_KEYWORDS = ["old", "deprecated", "legacy", "temp", "tmp", "test", "demo"];
22
19
  var STALE_FILE_RE = /\.(test|spec|mock)\.[jt]sx?$/;
23
20
  var STALE_PATH_RE = /\/deprecated\/|\/old\/|\/legacy\//;
21
+ function checkStale(flagKey, filePath) {
22
+ if (STALE_FILE_RE.test(filePath)) return { source: "path", pattern: "test/spec/mock file" };
23
+ if (STALE_PATH_RE.test(filePath)) return { source: "path", pattern: "deprecated/old/legacy path" };
24
+ const lk = flagKey.toLowerCase();
25
+ const kw = STALE_KEYWORDS.find((k) => new RegExp(`(?:^|[-_])${k}(?:[-_]|$)`).test(lk));
26
+ if (kw) return { source: "keyword", keyword: kw };
27
+ return null;
28
+ }
29
+ function staleReason(u) {
30
+ for (const s of u.stalenessSignals) {
31
+ if (s.source === "keyword") return `Contains "${s.keyword}" in key`;
32
+ if (s.source === "path") return s.pattern === "test/spec/mock file" ? "Located in test file" : "Located in deprecated path";
33
+ if (s.source === "minFileCount") return `Appears in only ${s.fileCount} file(s) (threshold: ${s.threshold})`;
34
+ }
35
+ return "Flagged as stale";
36
+ }
37
+
38
+ // src/scanner/index.ts
39
+ var LD_MEMBER_METHODS = /* @__PURE__ */ new Set(["variation", "variationDetail", "allFlags"]);
40
+ var LD_CLIENT_PATTERN = /^ld|client/i;
41
+ var LD_HOOKS = /* @__PURE__ */ new Set(["useFlags", "useLDClient"]);
24
42
  var DEFAULT_EXCLUDE = ["**/node_modules/**", "**/dist/**", "**/build/**", "**/.next/**"];
25
43
  function extractFlagKey(arg) {
26
44
  if (!arg) return { flagKey: "dynamic", isDynamic: true };
@@ -33,26 +51,28 @@ function extractFlagKey(arg) {
33
51
  }
34
52
  return { flagKey: "dynamic", isDynamic: true };
35
53
  }
36
- function checkStale(flagKey, filePath) {
37
- if (STALE_FILE_RE.test(filePath)) return true;
38
- if (STALE_PATH_RE.test(filePath)) return true;
39
- const lk = flagKey.toLowerCase();
40
- return STALE_KEY_WORDS.some((kw) => lk.includes(kw));
41
- }
42
- function walk(node, visit) {
43
- if (!node || typeof node !== "object") return;
44
- visit(node);
45
- for (const key of Object.keys(node)) {
46
- if (key === "parent") continue;
47
- const val = node[key];
48
- if (Array.isArray(val)) {
49
- for (const item of val) {
50
- if (item && typeof item === "object" && "type" in item) {
51
- walk(item, visit);
54
+ function walk(root, visit) {
55
+ if (!root || typeof root !== "object") return;
56
+ const stack = [root];
57
+ while (stack.length > 0) {
58
+ const node = stack.pop();
59
+ visit(node);
60
+ const children = [];
61
+ for (const key of Object.keys(node)) {
62
+ if (key === "parent") continue;
63
+ const val = node[key];
64
+ if (Array.isArray(val)) {
65
+ for (const item of val) {
66
+ if (item && typeof item === "object" && "type" in item) {
67
+ children.push(item);
68
+ }
52
69
  }
70
+ } else if (val && typeof val === "object" && "type" in val) {
71
+ children.push(val);
53
72
  }
54
- } else if (val && typeof val === "object" && "type" in val) {
55
- walk(val, visit);
73
+ }
74
+ for (let i = children.length - 1; i >= 0; i--) {
75
+ stack.push(children[i]);
56
76
  }
57
77
  }
58
78
  }
@@ -66,6 +86,7 @@ function detectUsages(ast, filePath) {
66
86
  if (callee.type === "MemberExpression" && !callee.computed && callee.object.type === "Identifier" && callee.property.type === "Identifier" && LD_CLIENT_PATTERN.test(callee.object.name) && LD_MEMBER_METHODS.has(callee.property.name)) {
67
87
  const method = callee.property.name;
68
88
  if (method === "allFlags") {
89
+ const sig = checkStale("*", filePath);
69
90
  usages.push({
70
91
  flagKey: "*",
71
92
  isDynamic: false,
@@ -73,10 +94,11 @@ function detectUsages(ast, filePath) {
73
94
  line: loc.line,
74
95
  column: loc.column,
75
96
  callType: "allFlags",
76
- isStale: checkStale("*", filePath)
97
+ stalenessSignals: sig ? [sig] : []
77
98
  });
78
99
  } else {
79
100
  const { flagKey, isDynamic } = extractFlagKey(call.arguments[0]);
101
+ const sig = checkStale(flagKey, filePath);
80
102
  usages.push({
81
103
  flagKey,
82
104
  isDynamic,
@@ -84,7 +106,7 @@ function detectUsages(ast, filePath) {
84
106
  line: loc.line,
85
107
  column: loc.column,
86
108
  callType: method,
87
- isStale: checkStale(flagKey, filePath)
109
+ stalenessSignals: sig ? [sig] : []
88
110
  });
89
111
  }
90
112
  return;
@@ -93,6 +115,7 @@ function detectUsages(ast, filePath) {
93
115
  const name = callee.name;
94
116
  if (name === "isFeatureEnabled") {
95
117
  const { flagKey, isDynamic } = extractFlagKey(call.arguments[0]);
118
+ const sig = checkStale(flagKey, filePath);
96
119
  usages.push({
97
120
  flagKey,
98
121
  isDynamic,
@@ -100,11 +123,12 @@ function detectUsages(ast, filePath) {
100
123
  line: loc.line,
101
124
  column: loc.column,
102
125
  callType: "isFeatureEnabled",
103
- isStale: checkStale(flagKey, filePath)
126
+ stalenessSignals: sig ? [sig] : []
104
127
  });
105
128
  return;
106
129
  }
107
130
  if (LD_HOOKS.has(name)) {
131
+ const sig = checkStale("*", filePath);
108
132
  usages.push({
109
133
  flagKey: "*",
110
134
  isDynamic: false,
@@ -112,12 +136,13 @@ function detectUsages(ast, filePath) {
112
136
  line: loc.line,
113
137
  column: loc.column,
114
138
  callType: name === "useFlags" ? "hook-useFlags" : "hook-useLDClient",
115
- isStale: checkStale("*", filePath)
139
+ stalenessSignals: sig ? [sig] : []
116
140
  });
117
141
  return;
118
142
  }
119
143
  }
120
144
  if (callee.type === "CallExpression" && callee.callee.type === "Identifier" && callee.callee.name === "withLDConsumer") {
145
+ const sig = checkStale("*", filePath);
121
146
  usages.push({
122
147
  flagKey: "*",
123
148
  isDynamic: false,
@@ -125,7 +150,7 @@ function detectUsages(ast, filePath) {
125
150
  line: loc.line,
126
151
  column: loc.column,
127
152
  callType: "hoc",
128
- isStale: checkStale("*", filePath)
153
+ stalenessSignals: sig ? [sig] : []
129
154
  });
130
155
  return;
131
156
  }
@@ -134,6 +159,7 @@ function detectUsages(ast, filePath) {
134
159
  const jsx = node;
135
160
  if (jsx.name.type === "JSXIdentifier" && jsx.name.name === "LDProvider") {
136
161
  const loc = jsx.loc?.start ?? { line: 0, column: 0 };
162
+ const sigP = checkStale("*", filePath);
137
163
  usages.push({
138
164
  flagKey: "*",
139
165
  isDynamic: false,
@@ -141,14 +167,14 @@ function detectUsages(ast, filePath) {
141
167
  line: loc.line,
142
168
  column: loc.column,
143
169
  callType: "provider",
144
- isStale: checkStale("*", filePath)
170
+ stalenessSignals: sigP ? [sigP] : []
145
171
  });
146
172
  }
147
173
  }
148
174
  });
149
175
  return usages;
150
176
  }
151
- async function scan(dir, config, onProgress) {
177
+ async function scan(source, config, onProgress) {
152
178
  const start = Date.now();
153
179
  for (const pattern of config.include) {
154
180
  if (pattern.startsWith("/") || pattern.startsWith("..")) {
@@ -157,23 +183,17 @@ async function scan(dir, config, onProgress) {
157
183
  );
158
184
  }
159
185
  }
160
- const files = await fg(config.include, {
161
- cwd: dir,
162
- absolute: true,
163
- ignore: [...DEFAULT_EXCLUDE, ...config.exclude],
164
- onlyFiles: true
165
- });
186
+ const files = await source.listFiles(config.include, config.exclude);
166
187
  const allUsages = [];
167
188
  const warnings = [];
168
189
  let scannedFiles = 0;
169
- for (const file of files) {
170
- scannedFiles++;
171
- onProgress?.(scannedFiles);
190
+ async function processFile(file) {
172
191
  let code;
173
192
  try {
174
- code = await readFile(file, "utf8");
175
- } catch {
176
- continue;
193
+ code = await source.readFile(file);
194
+ } catch (err) {
195
+ const fsCode = err.code ?? "UNKNOWN";
196
+ return { usages: [], warning: `warn: could not read ${file} (${fsCode})` };
177
197
  }
178
198
  let ast;
179
199
  try {
@@ -185,12 +205,25 @@ async function scan(dir, config, onProgress) {
185
205
  tokens: false
186
206
  });
187
207
  } catch {
188
- warnings.push(`warn: failed to parse ${relative(dir, file)}`);
189
- continue;
208
+ return { usages: [], warning: `warn: failed to parse ${file}` };
190
209
  }
191
- allUsages.push(...detectUsages(ast, file));
210
+ return { usages: detectUsages(ast, file), warning: null };
211
+ }
212
+ const limit = pLimit(50);
213
+ const results = await Promise.all(
214
+ files.map(
215
+ (file) => limit(async () => {
216
+ scannedFiles++;
217
+ onProgress?.(scannedFiles);
218
+ return processFile(file);
219
+ })
220
+ )
221
+ );
222
+ for (const r of results) {
223
+ allUsages.push(...r.usages);
224
+ if (r.warning) warnings.push(r.warning);
192
225
  }
193
- if (config.staleThreshold > 0) {
226
+ if (config.minFileCount > 0) {
194
227
  const flagFileCount = /* @__PURE__ */ new Map();
195
228
  for (const usage of allUsages) {
196
229
  if (!usage.isDynamic && usage.flagKey !== "*") {
@@ -203,8 +236,12 @@ async function scan(dir, config, onProgress) {
203
236
  for (const usage of allUsages) {
204
237
  if (!usage.isDynamic && usage.flagKey !== "*") {
205
238
  const fileCount = flagFileCount.get(usage.flagKey)?.size ?? 0;
206
- if (fileCount <= config.staleThreshold) {
207
- usage.isStale = true;
239
+ if (fileCount <= config.minFileCount) {
240
+ usage.stalenessSignals.push({
241
+ source: "minFileCount",
242
+ fileCount,
243
+ threshold: config.minFileCount
244
+ });
208
245
  }
209
246
  }
210
247
  }
@@ -224,18 +261,36 @@ async function scan(dir, config, onProgress) {
224
261
  };
225
262
  }
226
263
 
264
+ // src/scanner/local-source.ts
265
+ import fg from "fast-glob";
266
+ import { readFile } from "fs/promises";
267
+ import { join, relative } from "path";
268
+ var LocalFileSource = class {
269
+ constructor(dir) {
270
+ this.dir = dir;
271
+ }
272
+ dir;
273
+ async listFiles(include, exclude) {
274
+ const files = await fg(include, {
275
+ cwd: this.dir,
276
+ absolute: true,
277
+ ignore: [...DEFAULT_EXCLUDE, ...exclude],
278
+ onlyFiles: true
279
+ });
280
+ return files.map((f) => relative(this.dir, f));
281
+ }
282
+ async readFile(path) {
283
+ return readFile(join(this.dir, path), "utf8");
284
+ }
285
+ };
286
+
287
+ // src/types.ts
288
+ var isStale = (u) => u.stalenessSignals.length > 0;
289
+
227
290
  // src/reporter/index.ts
228
291
  function esc(s) {
229
292
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
230
293
  }
231
- function staleReason(u) {
232
- if (/\.(test|spec|mock)\.[jt]sx?$/.test(u.file)) return "Located in test file";
233
- if (/\/deprecated\/|\/old\/|\/legacy\//.test(u.file)) return "Located in deprecated path";
234
- const kw = ["old", "deprecated", "legacy", "temp", "tmp", "test", "demo"].find(
235
- (k) => u.flagKey.toLowerCase().includes(k)
236
- );
237
- return kw ? `Contains "${kw}" in key` : "Flagged as stale";
238
- }
239
294
  function buildFlagMap(usages) {
240
295
  const map = /* @__PURE__ */ new Map();
241
296
  for (const u of usages) {
@@ -246,7 +301,7 @@ function buildFlagMap(usages) {
246
301
  entry.usages.push(u);
247
302
  entry.files.add(u.file);
248
303
  entry.callTypes.add(u.callType);
249
- if (u.isStale) entry.isStale = true;
304
+ if (isStale(u)) entry.isStale = true;
250
305
  }
251
306
  return map;
252
307
  }
@@ -258,7 +313,7 @@ function sortedFlagEntries(map) {
258
313
  }
259
314
  function formatMarkdown(result, options) {
260
315
  const { scannedFiles, totalUsages, uniqueFlags, usages, scanDurationMs } = result;
261
- const staleUsages = usages.filter((u) => u.isStale);
316
+ const staleUsages = usages.filter(isStale);
262
317
  const dynamicUsages = usages.filter((u) => u.isDynamic);
263
318
  const flagMap = buildFlagMap(usages);
264
319
  const sorted = sortedFlagEntries(flagMap);
@@ -300,7 +355,7 @@ function formatMarkdown(result, options) {
300
355
  lines.push("| Flag Key | Reason | Location |");
301
356
  lines.push("|----------|--------|----------|");
302
357
  for (const [key, data] of staleFlags) {
303
- const first = data.usages.find((u) => u.isStale) ?? data.usages[0];
358
+ const first = data.usages.find(isStale) ?? data.usages[0];
304
359
  lines.push(`| ${key} | ${staleReason(first)} | ${first.file}:${first.line} |`);
305
360
  }
306
361
  lines.push("");
@@ -322,7 +377,7 @@ function formatJSON(result) {
322
377
  }
323
378
  function formatHTML(result, options) {
324
379
  const { scannedFiles, totalUsages, uniqueFlags, usages, scanDurationMs } = result;
325
- const staleCount = new Set(usages.filter((u) => u.isStale).map((u) => u.flagKey)).size;
380
+ const staleCount = new Set(usages.filter(isStale).map((u) => u.flagKey)).size;
326
381
  const dynamicCount = usages.filter((u) => u.isDynamic).length;
327
382
  const date = (/* @__PURE__ */ new Date()).toLocaleString();
328
383
  const flagMap = buildFlagMap(usages);
@@ -334,7 +389,7 @@ function formatHTML(result, options) {
334
389
  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>`;
335
390
  }).join("\n ");
336
391
  const title = options.title ? esc(options.title) : "FlagLint Scan Report";
337
- const version = true ? "0.1.5" : "0.1.0";
392
+ const version = true ? "0.2.0" : "0.1.0";
338
393
  return `<!DOCTYPE html>
339
394
  <html lang="en">
340
395
  <head>
@@ -415,7 +470,7 @@ function formatReport(result, options) {
415
470
  }
416
471
 
417
472
  // src/config.ts
418
- import { existsSync, readFileSync } from "fs";
473
+ import { readFile as readFile2, access } from "fs/promises";
419
474
  import { resolve } from "path";
420
475
  import { z, ZodError } from "zod";
421
476
  var FlagLintConfigSchema = z.object({
@@ -429,19 +484,24 @@ var FlagLintConfigSchema = z.object({
429
484
  "**/*.d.ts"
430
485
  ]),
431
486
  provider: z.enum(["launchdarkly", "unleash", "growthbook", "custom"]).default("launchdarkly"),
432
- staleThreshold: z.number().int().min(0).default(1),
487
+ // TODO v0.3: replace minFileCount with real date-based staleness via git log
488
+ minFileCount: z.number().int().min(0).default(1),
433
489
  reportTitle: z.string().optional(),
434
490
  outputDir: z.string().default(".")
435
491
  });
436
492
  var SEARCH_PATHS = [".flaglintrc", ".flaglintrc.json", "flaglint.config.json"];
437
- function loadConfig(configPath) {
493
+ async function loadConfig(configPath) {
438
494
  const candidates = configPath ? [configPath] : SEARCH_PATHS;
439
495
  for (const candidate of candidates) {
440
496
  const full = resolve(candidate);
441
- if (!existsSync(full)) continue;
497
+ try {
498
+ await access(full);
499
+ } catch {
500
+ continue;
501
+ }
442
502
  let raw;
443
503
  try {
444
- raw = JSON.parse(readFileSync(full, "utf8"));
504
+ raw = JSON.parse(await readFile2(full, "utf8"));
445
505
  } catch (err) {
446
506
  throw new Error(`Error reading ${candidate}: ${String(err)}`);
447
507
  }
@@ -504,7 +564,7 @@ Examples:
504
564
  }
505
565
  let config;
506
566
  try {
507
- config = loadConfig(options.config);
567
+ config = await loadConfig(options.config);
508
568
  } catch (err) {
509
569
  process.stderr.write(chalk.red(String(err instanceof Error ? err.message : err)) + "\n");
510
570
  process.exit(1);
@@ -528,7 +588,7 @@ Examples:
528
588
  let lastSpinnerUpdate = 0;
529
589
  let result;
530
590
  try {
531
- result = await scan(dir, config, (filesScanned) => {
591
+ result = await scan(new LocalFileSource(dir), config, (filesScanned) => {
532
592
  if (filesScanned - lastSpinnerUpdate >= 50) {
533
593
  spinner.text = `Scanning... (${filesScanned} files)`;
534
594
  lastSpinnerUpdate = filesScanned;
@@ -558,7 +618,9 @@ Examples:
558
618
  );
559
619
  process.exit(0);
560
620
  }
561
- const staleCount = new Set(result.usages.filter((u) => u.isStale).map((u) => u.flagKey)).size;
621
+ const staleCount = new Set(
622
+ result.usages.filter((u) => isStale(u) && !u.isDynamic && u.flagKey !== "*").map((u) => u.flagKey)
623
+ ).size;
562
624
  const dynamicCount = new Set(result.usages.filter((u) => u.isDynamic).map((u) => u.flagKey)).size;
563
625
  process.stderr.write(
564
626
  chalk.green(
@@ -616,11 +678,13 @@ function keyLiteral(usage) {
616
678
  function buildItem(usage) {
617
679
  const k = keyLiteral(usage);
618
680
  if (usage.isDynamic) {
681
+ const isDetail = usage.callType === "variationDetail";
682
+ const methodName = isDetail ? "variationDetail" : "variation";
619
683
  return {
620
684
  usage,
621
- openFeatureEquivalent: "client.getBooleanValue()",
622
- codeChangeBefore: `ldClient.variation(flagKey, context, false)`,
623
- codeChangeAfter: `await client.getBooleanValue(flagKey, false) // server SDK is async`,
685
+ openFeatureEquivalent: isDetail ? "client.getBooleanDetails()" : "client.getBooleanValue()",
686
+ codeChangeBefore: `ldClient.${methodName}(flagKey, context, false)`,
687
+ codeChangeAfter: isDetail ? `await client.getBooleanDetails(flagKey, false) // server SDK is async` : `await client.getBooleanValue(flagKey, false) // server SDK is async`,
624
688
  requiresManualReview: true,
625
689
  reviewReason: "Flag key determined at runtime; OpenFeature server SDK methods are async \u2014 add await and make the enclosing function async"
626
690
  };
@@ -744,7 +808,7 @@ function analyze(result) {
744
808
  function formatMigrationReport(analysis) {
745
809
  const { readinessScore, requiredPackages, items, manualReviewCount, autoMigrateCount } = analysis;
746
810
  const date = (/* @__PURE__ */ new Date()).toLocaleDateString();
747
- const version = true ? "0.1.5" : "0.1.0";
811
+ const version = true ? "0.2.0" : "0.1.0";
748
812
  let scoreLabel;
749
813
  if (readinessScore >= 80) scoreLabel = "\u2713 Your codebase is ready for migration";
750
814
  else if (readinessScore >= 50) scoreLabel = "\u26A0 Some manual work required before migration";
@@ -852,7 +916,7 @@ Examples:
852
916
  }
853
917
  let config;
854
918
  try {
855
- config = loadConfig(options.config);
919
+ config = await loadConfig(options.config);
856
920
  } catch (err) {
857
921
  process.stderr.write(chalk2.red(String(err instanceof Error ? err.message : err)) + "\n");
858
922
  process.exit(1);
@@ -874,7 +938,7 @@ Examples:
874
938
  });
875
939
  let scanResult;
876
940
  try {
877
- scanResult = await scan(dir, config, (filesScanned) => {
941
+ scanResult = await scan(new LocalFileSource(dir), config, (filesScanned) => {
878
942
  spinner.text = `Scanning files... ${filesScanned}`;
879
943
  });
880
944
  spinner.text = "Analyzing migration readiness...";
@@ -942,7 +1006,7 @@ Examples:
942
1006
  // src/cli.ts
943
1007
  function createCLI() {
944
1008
  const program2 = new Command();
945
- program2.name("flaglint").description("Find stale feature flags. Detect flag debt. Plan your OpenFeature migration.").version("0.1.5", "-v, --version", "output the current version").addHelpText(
1009
+ program2.name("flaglint").description("Find stale feature flags. Detect flag debt. Plan your OpenFeature migration.").version("0.2.0", "-v, --version", "output the current version").addHelpText(
946
1010
  "after",
947
1011
  `
948
1012
  Examples:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flaglint",
3
- "version": "0.1.5",
3
+ "version": "0.2.0",
4
4
  "description": "Find stale feature flags. Detect flag debt. Plan your OpenFeature migration.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -56,6 +56,7 @@
56
56
  "commander": "^12.1.0",
57
57
  "fast-glob": "^3.3.2",
58
58
  "ora": "^8.1.0",
59
+ "p-limit": "^7.3.0",
59
60
  "zod": "^3.23.8"
60
61
  },
61
62
  "devDependencies": {