flagshark 1.2.0 โ†’ 1.3.1

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.
Files changed (3) hide show
  1. package/README.md +111 -106
  2. package/dist/cli.js +90 -4
  3. package/package.json +4 -3
package/README.md CHANGED
@@ -1,60 +1,82 @@
1
- # flagshark
1
+ # ๐Ÿฆˆ flagshark
2
2
 
3
- Find stale feature flags in your codebase. CLI tool + GitHub Action.
3
+ **Find stale feature flags in your codebase.** Polyglot CLI + GitHub Action. 13 languages, 13 providers, zero config.
4
4
 
5
5
  ```bash
6
6
  npx flagshark scan
7
7
  ```
8
8
 
9
9
  ```
10
- ๐Ÿฆˆ FlagShark v1.1.1
10
+ ๐Ÿฆˆ FlagShark v1.3.0 โ€” scanned 156 files in 2.3s
11
+ (47 excluded via .flagsharkignore + test-files preset)
11
12
 
12
- Scanned 156 files across 4 languages
13
- Detected providers: LaunchDarkly (JS SDK), Unleash (Go SDK)
14
- Found 23 feature flags, 7 stale
13
+ Detected providers: LaunchDarkly (Node SDK), Unleash, PostHog
14
+ Found 23 feature flags ยท 7 stale ยท health 70/100 โš ๏ธ
15
15
 
16
- Stale flags:
17
16
  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
18
17
  โ”‚ Flag โ”‚ File โ”‚ Added โ”‚ Signal โ”‚
19
18
  โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
20
- โ”‚ CHECKOUT_V2 โ”‚ src/checkout.ts:47 โ”‚ 14 months ago โ”‚ Age > 6 months โ”‚
21
- โ”‚ NEW_NAV โ”‚ src/layout.tsx:12 โ”‚ 8 months ago โ”‚ Age > 6 months, Single file โ”‚
22
- โ”‚ BETA_SEARCH โ”‚ src/search.ts:91 โ”‚ 11 months ago โ”‚ Single file reference โ”‚
19
+ โ”‚ CHECKOUT_V2 โ”‚ src/checkout.ts:47 โ”‚ 14 months ago โ”‚ age โ”‚
20
+ โ”‚ NEW_NAV โ”‚ src/layout.tsx:12 โ”‚ 8 months ago โ”‚ age, low-usage โ”‚
21
+ โ”‚ BETA_SEARCH โ”‚ src/search.ts:91 โ”‚ 11 months ago โ”‚ low-usage โ”‚
23
22
  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
24
23
 
25
- Flag Health Score: 70/100 (7/23 flags are stale)
24
+ Exit code: 1 (stale flags found)
26
25
  ```
27
26
 
27
+ ## Why FlagShark
28
+
29
+ - **Zero install, zero config.** `npx flagshark scan` works on any repo today.
30
+ - **Polyglot.** TypeScript, JavaScript, Go, Python, Java, Kotlin, Swift, Ruby, C#, PHP, Rust, C/C++, Objective-C.
31
+ - **Provider-aware.** Auto-detects 13 flag SDKs โ€” no custom rules to maintain.
32
+ - **AST-based detection** for TS/JS/Go/Python via [tree-sitter](https://tree-sitter.github.io/). Flag names inside strings, comments, and unrelated calls aren't false positives.
33
+ - **Two staleness signals** โ€” `git blame` age + single-file usage. Both run automatically.
34
+ - **MIT licensed.** No account, no token, no telemetry.
35
+
28
36
  ## Install
29
37
 
30
38
  ```bash
31
- # Run without installing
39
+ # Recommended โ€” run without installing
32
40
  npx flagshark scan
33
41
 
34
- # Or install globally
42
+ # Or globally
35
43
  npm install -g flagshark
36
44
  ```
37
45
 
38
- Building a tool on top of the engine? Use [`@flagshark/core`](https://www.npmjs.com/package/@flagshark/core) directly.
46
+ ## CLI
47
+
48
+ ```bash
49
+ flagshark scan [options]
50
+
51
+ Scan options:
52
+ --diff <ref> Only scan files changed since this git ref (e.g. main, HEAD~1)
53
+ --threshold <months> Staleness age threshold (default: 6, or config.threshold)
54
+ --verbose Show all stale flags + effective exclude rules
55
+
56
+ Output:
57
+ --json Emit JSON to stdout (stable schema for tooling)
58
+
59
+ Configuration:
60
+ --config <path> Use this config file (overrides .flagshark.yml discovery)
61
+ --no-config Skip .flagshark.yml discovery
62
+ --no-ignore-file Skip .flagsharkignore discovery
63
+ --show-excluded List excluded files in the output
64
+ ```
39
65
 
40
- ## CLI Usage
66
+ Example invocations:
41
67
 
42
68
  ```bash
43
- # Scan current directory
69
+ # Scan current directory, default 6-month threshold
44
70
  flagshark scan
45
71
 
46
- # JSON output (for piping to other tools)
47
- flagshark scan --json
48
-
49
- # Only scan files changed since a git ref
50
- flagshark scan --diff HEAD~1
72
+ # Scan only files changed since main
51
73
  flagshark scan --diff main
52
74
 
53
- # Custom staleness threshold (default: 6 months)
54
- flagshark scan --threshold 3
75
+ # Stricter threshold + JSON for piping
76
+ flagshark scan --threshold 3 --json | jq '.staleFlags'
55
77
 
56
- # Show all stale flags (default shows top 10)
57
- flagshark scan --verbose
78
+ # Use a custom config file
79
+ flagshark scan --config ./tooling/flagshark.yml
58
80
  ```
59
81
 
60
82
  ### Exit codes
@@ -63,11 +85,64 @@ flagshark scan --verbose
63
85
  |------|---------|
64
86
  | 0 | No stale flags found |
65
87
  | 1 | Stale flags detected |
66
- | 2 | Runtime error |
88
+ | 2 | Runtime or configuration error |
67
89
 
68
- ## GitHub Action
90
+ ## Configuration
91
+
92
+ FlagShark is zero-config by default. When you want more control, two files compose:
93
+
94
+ ### `.flagsharkignore` โ€” skip files entirely
95
+
96
+ Drop a `.flagsharkignore` at your repo root. Same syntax as `.gitignore`:
97
+
98
+ ```gitignore
99
+ examples/
100
+ **/*.test.ts
101
+ **/*_test.go
102
+ **/test_*.py
103
+ !examples/important-flag-test.ts # Re-include with `!`
104
+ ```
69
105
 
70
- Add to your workflow:
106
+ ### `.flagshark.yml` โ€” full config
107
+
108
+ ```yaml
109
+ threshold: 6
110
+
111
+ excludes:
112
+ paths:
113
+ - 'examples/**'
114
+ files:
115
+ - '**/*.test.ts'
116
+ presets:
117
+ - test-files # Curated bundles โ€” see table below
118
+ - snapshots
119
+
120
+ suppress:
121
+ flags:
122
+ - 'INTERNAL_DEBUG_*' # Don't report these flag names
123
+ - 'PERMANENT_KILLSWITCH'
124
+
125
+ paths: # Per-path threshold overrides
126
+ - match: 'src/critical/**'
127
+ threshold: 3
128
+ - match: 'src/experimental/**'
129
+ threshold: 12
130
+ ```
131
+
132
+ The unconditional baseline โ€” `node_modules`, `.git`, `dist`, `build`, `coverage`, `__pycache__`, `vendor`, `.next`, `.turbo` โ€” is always skipped.
133
+
134
+ ### Built-in presets
135
+
136
+ | Preset | Covers |
137
+ |---|---|
138
+ | `test-files` | `*.test.*`, `*.spec.*`, `*_test.go`, `test_*.py`, `*Test.java`, `*_spec.rb`, `__tests__/**`, etc. |
139
+ | `snapshots` | `*.snap`, `__snapshots__/**` |
140
+ | `examples` | `examples/**`, `demo/**` |
141
+ | `stories` | `*.stories.{ts,tsx,js,jsx}` (Storybook) |
142
+ | `fixtures` | `__fixtures__/**`, `fixtures/**` |
143
+ | `generated` | `*.generated.{ts,js}`, `*.gen.go`, `generated/**` |
144
+
145
+ ## GitHub Action
71
146
 
72
147
  ```yaml
73
148
  name: FlagShark
@@ -83,95 +158,25 @@ jobs:
83
158
  steps:
84
159
  - uses: actions/checkout@v4
85
160
  with:
86
- fetch-depth: 0 # Required for git blame (staleness) and changed-file scanning
161
+ fetch-depth: 0
87
162
  - uses: FlagShark/flagshark@v1
88
163
  env:
89
164
  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
90
165
  ```
91
166
 
92
- ### Action Inputs
167
+ See the [main repo README](https://github.com/FlagShark/flagshark) for action inputs and full docs.
93
168
 
94
- | Input | Default | Description |
95
- |-------|---------|-------------|
96
- | `scan` | `changed` | `changed` (PR files only) or `full` (entire repo) |
97
- | `threshold` | `6` | Staleness threshold in months |
98
- | `fail-threshold` | `0` | Health score below which the check fails (0 = never fail) |
169
+ ## Library usage
99
170
 
100
- ### Scan Modes
171
+ Building a tool on top of the engine? Use [`@flagshark/core`](https://www.npmjs.com/package/@flagshark/core) directly:
101
172
 
102
- **`scan: changed`** (default) scans only files modified in the PR. Fast, focused on what you're changing.
173
+ ```ts
174
+ import { scanRepo } from '@flagshark/core'
103
175
 
104
- **`scan: full`** scans the entire repository. Shows your full flag health score and finds stale flags everywhere, not just in changed files:
105
-
106
- ```yaml
107
- - uses: FlagShark/flagshark@v1
108
- with:
109
- scan: full
110
- env:
111
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
176
+ const result = await scanRepo({ cwd: process.cwd(), threshold: 6 })
177
+ console.log(`${result.staleFlags.length} stale of ${result.totalFlags}`)
112
178
  ```
113
179
 
114
- ### What the Action does
115
-
116
- On every PR, FlagShark comments with a table of stale flags:
117
-
118
- > ### ๐Ÿฆˆ FlagShark found 3 stale flags
119
- >
120
- > | Flag | File | Added | Signal |
121
- > |------|------|-------|--------|
122
- > | `CHECKOUT_V2` | src/checkout.ts:47 | 14 months ago | Age > 6 months |
123
- > | `NEW_NAV` | src/layout.tsx:12 | 8 months ago | Single file |
124
- >
125
- > **Flag Health:** 70/100
126
-
127
- It also sets a GitHub status check that can optionally block merge if health drops below a threshold.
128
-
129
- ## Supported Languages
130
-
131
- FlagShark detects feature flags across 13 languages:
132
-
133
- | Language | Extensions |
134
- |----------|-----------|
135
- | TypeScript/JavaScript | .ts, .tsx, .js, .jsx, .mjs, .cjs |
136
- | Go | .go |
137
- | Python | .py |
138
- | Java | .java |
139
- | Kotlin | .kt |
140
- | Swift | .swift |
141
- | Ruby | .rb |
142
- | C# | .cs |
143
- | PHP | .php |
144
- | Rust | .rs |
145
- | C/C++ | .c, .cpp, .h, .hpp |
146
- | Objective-C | .m |
147
-
148
- ## Supported Providers
149
-
150
- Auto-detected from imports (no configuration needed):
151
-
152
- - LaunchDarkly
153
- - Unleash
154
- - Flipt
155
- - Split.io
156
- - PostHog
157
- - Flagsmith
158
- - ConfigCat
159
- - Statsig
160
- - GrowthBook
161
- - DevCycle
162
- - Eppo
163
- - Optimizely
164
- - Custom flag implementations
165
-
166
- ## How Staleness Works
167
-
168
- A flag is marked stale if **any** of these signals fires:
169
-
170
- 1. **Age:** `git blame` shows the flag reference was added more than 6 months ago (configurable with `--threshold`)
171
- 2. **Single file:** The flag name appears in only one file across the entire repo, suggesting a completed rollout
172
-
173
- FlagShark only checks files that actually import a flag SDK. A function called `isEnabled()` in a file that doesn't import LaunchDarkly/Unleash/etc. won't be flagged. This prevents false positives.
174
-
175
180
  ## License
176
181
 
177
182
  MIT
package/dist/cli.js CHANGED
@@ -1,7 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import { scanRepo } from "@flagshark/core";
4
+ import { readFileSync, existsSync } from "node:fs";
5
+ import { parse as parseYaml } from "yaml";
6
+ import { scanRepo, FlagsharkConfigSchema } from "@flagshark/core";
5
7
 
6
8
  // src/formatter.ts
7
9
  var VERSION = "1.2.0";
@@ -49,6 +51,9 @@ function formatText(result, options) {
49
51
  lines.push("");
50
52
  const langCount = Object.keys(result.languageBreakdown).length;
51
53
  lines.push(`Scanned ${result.filesScanned} files across ${langCount} language${langCount === 1 ? "" : "s"}`);
54
+ if (result.excludedCount && result.excludedCount > 0) {
55
+ lines.push(`(${result.excludedCount} excluded via .flagsharkignore + excludes)`);
56
+ }
52
57
  if (result.totalFlags === 0) {
53
58
  lines.push("No feature flags detected.");
54
59
  lines.push("");
@@ -85,6 +90,13 @@ function formatText(result, options) {
85
90
  lines.push("Automate cleanup \u2192 https://flagshark.com");
86
91
  lines.push("Open source CLI \u2192 https://github.com/FlagShark/flagshark");
87
92
  }
93
+ if (result.excludedPaths && result.excludedPaths.length > 0) {
94
+ lines.push("");
95
+ lines.push(`Excluded files (${result.excludedPaths.length}):`);
96
+ for (const p of result.excludedPaths) {
97
+ lines.push(` ${p}`);
98
+ }
99
+ }
88
100
  return lines.join("\n");
89
101
  }
90
102
  function formatJson(result) {
@@ -110,6 +122,7 @@ function formatJson(result) {
110
122
  detectedProviders: result.detectedProviders,
111
123
  languages,
112
124
  flags,
125
+ excludedPaths: result.excludedPaths,
113
126
  scanDuration: result.scanDuration,
114
127
  links: {
115
128
  dashboard: "https://flagshark.com",
@@ -121,7 +134,7 @@ function formatJson(result) {
121
134
  }
122
135
 
123
136
  // src/cli.ts
124
- var VERSION2 = "1.2.0";
137
+ var VERSION2 = "1.3.1";
125
138
  var HELP_TEXT = `
126
139
  flagshark scan [options]
127
140
 
@@ -132,19 +145,30 @@ Options:
132
145
  --verbose Show all stale flags (not just top 10)
133
146
  --help Show help
134
147
  --version Show version
148
+
149
+ Configuration:
150
+ --config <path> Use this config file (overrides .flagshark.yml discovery)
151
+ --no-config Skip config file discovery
152
+ --no-ignore-file Skip .flagsharkignore discovery
153
+ --show-excluded Show excluded files in text output
135
154
  `.trim();
136
155
  function parseArgs(argv) {
137
156
  const args = {
138
157
  json: false,
139
158
  diff: null,
140
- threshold: 6,
159
+ threshold: void 0,
141
160
  verbose: false,
142
161
  help: false,
143
162
  version: false
144
163
  };
145
164
  let i = 2;
146
165
  while (i < argv.length) {
147
- const arg = argv[i];
166
+ let arg = argv[i];
167
+ if (arg.startsWith("--") && arg.includes("=")) {
168
+ const eqIdx = arg.indexOf("=");
169
+ argv.splice(i, 1, arg.slice(0, eqIdx), arg.slice(eqIdx + 1));
170
+ arg = argv[i];
171
+ }
148
172
  switch (arg) {
149
173
  case "--json":
150
174
  args.json = true;
@@ -174,6 +198,32 @@ function parseArgs(argv) {
174
198
  case "-v":
175
199
  args.version = true;
176
200
  break;
201
+ case "--engine": {
202
+ const value = argv[++i];
203
+ if (value !== "regex" && value !== "tree-sitter") {
204
+ process.stderr.write(`Error: --engine must be 'regex' or 'tree-sitter', got '${value}'
205
+ `);
206
+ process.exit(2);
207
+ }
208
+ args.engine = value;
209
+ break;
210
+ }
211
+ case "--config":
212
+ i++;
213
+ args.configPath = argv[i];
214
+ if (!args.configPath) {
215
+ throw new Error("--config requires a file path argument");
216
+ }
217
+ break;
218
+ case "--no-config":
219
+ args.noConfig = true;
220
+ break;
221
+ case "--no-ignore-file":
222
+ args.noIgnoreFile = true;
223
+ break;
224
+ case "--show-excluded":
225
+ args.showExcluded = true;
226
+ break;
177
227
  case "scan":
178
228
  break;
179
229
  default:
@@ -212,12 +262,48 @@ async function main() {
212
262
  } else {
213
263
  logger.info("Scanning current directory...");
214
264
  }
265
+ let configOverride;
266
+ if (args.configPath) {
267
+ if (!existsSync(args.configPath)) {
268
+ process.stderr.write(`Error: config file not found: ${args.configPath}
269
+ `);
270
+ process.exit(2);
271
+ }
272
+ const raw = readFileSync(args.configPath, "utf-8");
273
+ const parsed = parseYaml(raw);
274
+ const configResult = FlagsharkConfigSchema.safeParse(parsed);
275
+ if (!configResult.success) {
276
+ process.stderr.write(`Error: invalid config at ${args.configPath}: ${configResult.error.message}
277
+ `);
278
+ process.exit(2);
279
+ }
280
+ configOverride = configResult.data;
281
+ }
215
282
  const result = await scanRepo({
216
283
  cwd: process.cwd(),
217
284
  threshold: args.threshold,
218
285
  diff: args.diff ?? void 0,
286
+ engine: args.engine,
287
+ config: configOverride,
288
+ noConfig: args.noConfig,
289
+ noIgnoreFile: args.noIgnoreFile,
290
+ collectExcludedPaths: args.showExcluded,
219
291
  logger
220
292
  });
293
+ if (args.verbose && result.effectiveExcludes) {
294
+ const r = result.effectiveExcludes;
295
+ const allRules = [
296
+ ...r.paths.map((p) => `excludes.paths: ${p}`),
297
+ ...r.files.map((p) => `excludes.files: ${p}`),
298
+ ...r.presets.flatMap((name, i) => [`excludes.presets[${i}]: ${name}`]),
299
+ ...r.ignoreFile.map((p) => `.flagsharkignore: ${p}`)
300
+ ];
301
+ if (allRules.length > 0) {
302
+ process.stderr.write("Effective excludes:\n");
303
+ for (const rule of allRules) process.stderr.write(` ${rule}
304
+ `);
305
+ }
306
+ }
221
307
  const output = args.json ? formatJson(result) + "\n" : formatText(result, { json: false, verbose: args.verbose, maxDisplay: 10 }) + "\n";
222
308
  const exitCode = result.staleFlags.length > 0 ? 1 : 0;
223
309
  if (process.stdout.write(output)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flagshark",
3
- "version": "1.2.0",
3
+ "version": "1.3.1",
4
4
  "type": "module",
5
5
  "description": "Find stale feature flags in your codebase",
6
6
  "license": "MIT",
@@ -17,12 +17,13 @@
17
17
  "main": "./dist/cli.js",
18
18
  "files": ["dist/", "bin/"],
19
19
  "scripts": {
20
- "build": "esbuild src/cli.ts --bundle --platform=node --target=node18 --format=esm --outfile=dist/cli.js --external:zod --external:@flagshark/core",
20
+ "build": "esbuild src/cli.ts --bundle --platform=node --target=node18 --format=esm --outfile=dist/cli.js --external:zod --external:@flagshark/core --external:yaml",
21
21
  "test": "vitest run",
22
22
  "typecheck": "tsc --noEmit"
23
23
  },
24
24
  "dependencies": {
25
- "@flagshark/core": "1.0.0",
25
+ "@flagshark/core": "1.3.1",
26
+ "yaml": "^2.4.0",
26
27
  "zod": "^3.23.0"
27
28
  },
28
29
  "devDependencies": {