flagshark 1.0.0 → 1.1.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/package.json CHANGED
@@ -1,56 +1,33 @@
1
1
  {
2
2
  "name": "flagshark",
3
- "version": "1.0.0",
4
- "description": "Find stale feature flags in your codebase",
3
+ "version": "1.1.0",
5
4
  "type": "module",
5
+ "description": "Find stale feature flags in your codebase",
6
6
  "license": "MIT",
7
+ "homepage": "https://flagshark.com",
7
8
  "repository": {
8
9
  "type": "git",
9
- "url": "https://github.com/FlagShark/flagshark.git"
10
- },
11
- "homepage": "https://flagshark.com",
12
- "keywords": [
13
- "feature-flags",
14
- "launchdarkly",
15
- "unleash",
16
- "flipt",
17
- "split",
18
- "posthog",
19
- "cleanup",
20
- "stale-flags",
21
- "technical-debt",
22
- "github-action"
23
- ],
24
- "bin": {
25
- "flagshark": "./bin/flagshark.mjs"
10
+ "url": "https://github.com/FlagShark/flagshark.git",
11
+ "directory": "packages/cli"
26
12
  },
13
+ "keywords": ["feature-flags", "stale-flags", "cleanup", "technical-debt"],
14
+ "bin": { "flagshark": "./bin/flagshark.mjs" },
27
15
  "main": "./dist/cli.js",
28
- "files": [
29
- "dist/",
30
- "bin/",
31
- "action/"
32
- ],
16
+ "files": ["dist/", "bin/"],
33
17
  "scripts": {
34
- "build": "npm run build:cli && npm run build:action",
35
- "build:cli": "esbuild src/cli.ts --bundle --platform=node --target=node18 --format=esm --outfile=dist/cli.js --external:zod",
36
- "build:action": "esbuild action/index.ts --bundle --platform=node --target=node18 --format=esm --outfile=dist/action.js --external:@actions/core --external:@actions/github --external:zod",
18
+ "build": "esbuild src/cli.ts --bundle --platform=node --target=node18 --format=esm --outfile=dist/cli.js --external:zod --external:@flagshark/core",
37
19
  "test": "vitest run",
38
- "test:watch": "vitest",
39
- "prepublishOnly": "npm run build && npm test"
20
+ "typecheck": "tsc --noEmit"
40
21
  },
41
22
  "dependencies": {
42
- "p-limit": "^6.0.0",
23
+ "@flagshark/core": "workspace:*",
43
24
  "zod": "^3.23.0"
44
25
  },
45
26
  "devDependencies": {
46
- "@actions/core": "^1.10.0",
47
- "@actions/github": "^6.0.0",
48
27
  "@types/node": "^22.0.0",
49
28
  "esbuild": "^0.24.0",
50
29
  "typescript": "^5.7.0",
51
30
  "vitest": "^3.0.0"
52
31
  },
53
- "engines": {
54
- "node": ">=18.0.0"
55
- }
32
+ "engines": { "node": ">=18.0.0" }
56
33
  }
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 FlagShark
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
package/README.md DELETED
@@ -1,175 +0,0 @@
1
- # FlagShark
2
-
3
- Find stale feature flags in your codebase. CLI tool + GitHub Action.
4
-
5
- ```bash
6
- npx flagshark scan
7
- ```
8
-
9
- ```
10
- 🦈 FlagShark v1.0.0
11
-
12
- Scanned 156 files across 4 languages
13
- Detected providers: LaunchDarkly (JS SDK), Unleash (Go SDK)
14
- Found 23 feature flags, 7 stale
15
-
16
- Stale flags:
17
- ┌──────────────────┬────────────────────────┬───────────────┬──────────────────────────────┐
18
- │ Flag │ File │ Added │ Signal │
19
- ├──────────────────┼────────────────────────┼───────────────┼──────────────────────────────┤
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 │
23
- └──────────────────┴────────────────────────┴───────────────┴──────────────────────────────┘
24
-
25
- Flag Health Score: 70/100 (7/23 flags are stale)
26
- ```
27
-
28
- ## Install
29
-
30
- ```bash
31
- # Run without installing
32
- npx flagshark scan
33
-
34
- # Or install globally
35
- npm install -g flagshark
36
- ```
37
-
38
- ## CLI Usage
39
-
40
- ```bash
41
- # Scan current directory
42
- flagshark scan
43
-
44
- # JSON output (for piping to other tools)
45
- flagshark scan --json
46
-
47
- # Only scan files changed since a git ref
48
- flagshark scan --diff HEAD~1
49
- flagshark scan --diff main
50
-
51
- # Custom staleness threshold (default: 6 months)
52
- flagshark scan --threshold 3
53
-
54
- # Show all stale flags (default shows top 10)
55
- flagshark scan --verbose
56
- ```
57
-
58
- ### Exit codes
59
-
60
- | Code | Meaning |
61
- |------|---------|
62
- | 0 | No stale flags found |
63
- | 1 | Stale flags detected |
64
- | 2 | Runtime error |
65
-
66
- ## GitHub Action
67
-
68
- Add to your workflow:
69
-
70
- ```yaml
71
- name: FlagShark
72
- on: [pull_request]
73
-
74
- permissions:
75
- contents: read
76
- pull-requests: write
77
-
78
- jobs:
79
- flagshark:
80
- runs-on: ubuntu-latest
81
- steps:
82
- - uses: actions/checkout@v4
83
- with:
84
- fetch-depth: 0 # Required for git blame age detection
85
- - uses: FlagShark/flagshark@v1
86
- ```
87
-
88
- ### Action Inputs
89
-
90
- | Input | Default | Description |
91
- |-------|---------|-------------|
92
- | `scan` | `changed` | `changed` (PR files only) or `full` (entire repo) |
93
- | `threshold` | `6` | Staleness threshold in months |
94
- | `fail-threshold` | `0` | Health score below which the check fails (0 = never fail) |
95
-
96
- ### What the Action does
97
-
98
- On every PR, FlagShark comments with a table of stale flags found in the changed files:
99
-
100
- > ### 🦈 FlagShark found 3 stale flags
101
- >
102
- > | Flag | File | Added | Signal |
103
- > |------|------|-------|--------|
104
- > | `CHECKOUT_V2` | src/checkout.ts:47 | 14 months ago | Age > 6 months |
105
- > | `NEW_NAV` | src/layout.tsx:12 | 8 months ago | Single file |
106
- >
107
- > **Flag Health:** 70/100
108
-
109
- It also sets a GitHub status check that can optionally block merge if health drops below a threshold.
110
-
111
- ## Supported Languages
112
-
113
- FlagShark detects feature flags across 13 languages:
114
-
115
- | Language | Extensions |
116
- |----------|-----------|
117
- | TypeScript/JavaScript | .ts, .tsx, .js, .jsx, .mjs, .cjs |
118
- | Go | .go |
119
- | Python | .py |
120
- | Java | .java |
121
- | Kotlin | .kt |
122
- | Swift | .swift |
123
- | Ruby | .rb |
124
- | C# | .cs |
125
- | PHP | .php |
126
- | Rust | .rs |
127
- | C/C++ | .c, .cpp, .h, .hpp |
128
- | Objective-C | .m |
129
-
130
- ## Supported Providers
131
-
132
- Auto-detected from imports (no configuration needed):
133
-
134
- - LaunchDarkly
135
- - Unleash
136
- - Flipt
137
- - Split.io
138
- - PostHog
139
- - Flagsmith
140
- - ConfigCat
141
- - Statsig
142
- - GrowthBook
143
- - DevCycle
144
- - Eppo
145
- - Optimizely
146
- - Custom flag implementations
147
-
148
- ## How Staleness Works
149
-
150
- A flag is marked stale if **any** of these signals fires:
151
-
152
- 1. **Age:** `git blame` shows the flag reference was added more than 6 months ago (configurable with `--threshold`)
153
- 2. **Single file:** The flag name appears in only one file across the entire repo, suggesting a completed rollout
154
-
155
- 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.
156
-
157
- ## Configuration
158
-
159
- ### `.flagsharkignore`
160
-
161
- Exclude files or specific flags:
162
-
163
- ```
164
- # Glob patterns for files
165
- test/**
166
- fixtures/**
167
-
168
- # Specific flag names (prefix with flag:)
169
- flag:PERMANENT_ADMIN_OVERRIDE
170
- flag:MAINTENANCE_MODE
171
- ```
172
-
173
- ## License
174
-
175
- MIT
package/action/index.ts DELETED
@@ -1,243 +0,0 @@
1
- /**
2
- * GitHub Action entry point for FlagShark.
3
- *
4
- * This is a thin wrapper around the same detection engine used by the CLI.
5
- * It runs the scan, posts a PR comment with results, and sets a status check.
6
- *
7
- * Architecture:
8
- * action/index.ts (this file)
9
- * │
10
- * ├─ Runs the same scanner + staleness logic as the CLI
11
- * ├─ Posts a PR comment with markdown table
12
- * └─ Sets a GitHub check status (pass/fail)
13
- */
14
-
15
- import { readFileSync, readdirSync, statSync } from 'node:fs'
16
- import { join, extname } from 'node:path'
17
-
18
- import * as core from '@actions/core'
19
- import * as github from '@actions/github'
20
-
21
- import { createDefaultRegistry } from '../src/detection/index.js'
22
- import { PolyglotAnalyzer } from '../src/detection/polyglot-analyzer.js'
23
- import { analyzeStaleness } from '../src/staleness.js'
24
-
25
- import type { FeatureFlag } from '../src/detection/feature-flag.js'
26
- import type { StaleFlag } from '../src/staleness.js'
27
-
28
- const COMMENT_MARKER = '<!-- flagshark-action -->'
29
- const SKIP_DIRS = new Set([
30
- 'node_modules',
31
- 'vendor',
32
- '.git',
33
- 'dist',
34
- 'build',
35
- 'coverage',
36
- '__pycache__',
37
- '.next',
38
- '.turbo',
39
- ])
40
-
41
- const logger = {
42
- debug: (...args: unknown[]) => core.debug(args.map(String).join(' ')),
43
- info: (...args: unknown[]) => core.info(args.map(String).join(' ')),
44
- warn: (...args: unknown[]) => core.warning(args.map(String).join(' ')),
45
- error: (...args: unknown[]) => core.error(args.map(String).join(' ')),
46
- }
47
-
48
- async function run(): Promise<void> {
49
- const startTime = Date.now()
50
-
51
- try {
52
- const scanMode = core.getInput('scan') || 'changed'
53
- const threshold = parseInt(core.getInput('threshold') || '6', 10)
54
- const failThreshold = parseInt(core.getInput('fail-threshold') || '0', 10)
55
-
56
- const registry = createDefaultRegistry()
57
- const supportedExts = new Set(registry.getSupportedExtensions())
58
- const analyzer = new PolyglotAnalyzer(registry, logger)
59
-
60
- // Determine files to scan
61
- let filePaths: string[]
62
-
63
- if (scanMode === 'changed' && github.context.payload.pull_request) {
64
- const token = process.env.GITHUB_TOKEN || core.getInput('token')
65
- if (!token) {
66
- core.setFailed('GITHUB_TOKEN is required for changed-file scanning')
67
- return
68
- }
69
- const octokit = github.getOctokit(token)
70
- const { data: prFiles } = await octokit.rest.pulls.listFiles({
71
- ...github.context.repo,
72
- pull_number: github.context.payload.pull_request.number,
73
- per_page: 100,
74
- })
75
- filePaths = prFiles
76
- .filter((f) => f.status !== 'removed')
77
- .map((f) => f.filename)
78
- .filter((f) => supportedExts.has(extname(f)))
79
- } else {
80
- filePaths = walkDir('.', supportedExts)
81
- }
82
-
83
- // Read file contents
84
- const files = new Map<string, string>()
85
- for (const fp of filePaths) {
86
- try {
87
- const stat = statSync(fp)
88
- if (stat.size > 5 * 1024 * 1024) {
89
- continue
90
- }
91
- files.set(fp, readFileSync(fp, 'utf-8'))
92
- } catch {
93
- // Skip unreadable files
94
- }
95
- }
96
-
97
- core.info(`Scanning ${files.size} files...`)
98
-
99
- // Run detection
100
- const result = await analyzer.analyzeFiles(files)
101
- const totalFlags = result.totalFlags.size
102
-
103
- // Run staleness analysis
104
- const staleFlags = await analyzeStaleness(result.totalFlags as Map<string, FeatureFlag[]>, {
105
- thresholdMonths: threshold,
106
- repoRoot: process.cwd(),
107
- })
108
-
109
- // Use unique stale flag names (not occurrences) for health score — matches CLI formula
110
- const uniqueStaleNames = new Set(staleFlags.map((f) => f.name)).size
111
- const healthScore =
112
- totalFlags > 0 ? Math.round(((totalFlags - uniqueStaleNames) / totalFlags) * 100) : 100
113
-
114
- const scanDuration = Date.now() - startTime
115
-
116
- // Set outputs
117
- core.setOutput('health-score', healthScore.toString())
118
- core.setOutput('stale-count', staleFlags.length.toString())
119
- core.setOutput('total-count', totalFlags.toString())
120
-
121
- // Post PR comment (only on PRs)
122
- if (github.context.payload.pull_request && staleFlags.length > 0) {
123
- const token = process.env.GITHUB_TOKEN || core.getInput('token')
124
- if (token) {
125
- await postComment(token, staleFlags, totalFlags, healthScore)
126
- }
127
- }
128
-
129
- // Set status check
130
- if (failThreshold > 0 && healthScore < failThreshold) {
131
- core.setFailed(
132
- `Flag health score ${healthScore}/100 is below threshold ${failThreshold}/100. ` +
133
- `${staleFlags.length} stale flags found.`,
134
- )
135
- } else {
136
- core.info(`Flag Health Score: ${healthScore}/100 (${staleFlags.length}/${totalFlags} stale)`)
137
- }
138
-
139
- // Summary
140
- core.summary
141
- .addHeading('FlagShark Results', 2)
142
- .addRaw(`**Health Score:** ${healthScore}/100\n\n`)
143
- .addRaw(`**Flags:** ${totalFlags} total, ${staleFlags.length} stale\n\n`)
144
- .addRaw(`**Scan time:** ${scanDuration}ms\n`)
145
- await core.summary.write()
146
- } catch (error) {
147
- if (error instanceof Error) {
148
- core.setFailed(error.message)
149
- } else {
150
- core.setFailed('An unexpected error occurred')
151
- }
152
- }
153
- }
154
-
155
- async function postComment(
156
- token: string,
157
- staleFlags: StaleFlag[],
158
- totalFlags: number,
159
- healthScore: number,
160
- ): Promise<void> {
161
- const octokit = github.getOctokit(token)
162
- const { owner, repo } = github.context.repo
163
- const prNumber = github.context.payload.pull_request!.number
164
-
165
- const displayFlags = staleFlags.slice(0, 10)
166
- const remaining = staleFlags.length - displayFlags.length
167
-
168
- let body = `${COMMENT_MARKER}\n`
169
- body += `### 🦈 FlagShark found ${staleFlags.length} stale flag${staleFlags.length !== 1 ? 's' : ''}\n\n`
170
- body += '| Flag | File | Added | Signal |\n'
171
- body += '|------|------|-------|--------|\n'
172
-
173
- for (const flag of displayFlags) {
174
- const signals = flag.signals.map((s) => s.description).join(', ')
175
- body += `| \`${flag.name}\` | ${flag.filePath}:${flag.lineNumber} | ${flag.age || 'unknown'} | ${signals} |\n`
176
- }
177
-
178
- if (remaining > 0) {
179
- body += `\n... and ${remaining} more stale flags.\n`
180
- }
181
-
182
- body += `\n**Flag Health:** ${healthScore}/100 (${totalFlags} total, ${staleFlags.length} stale)\n`
183
- body +=
184
- '\nFull analysis → [FlagShark](https://github.com/FlagShark/flagshark)\n'
185
-
186
- // Find existing comment to update
187
- const { data: comments } = await octokit.rest.issues.listComments({
188
- owner,
189
- repo,
190
- issue_number: prNumber,
191
- per_page: 100,
192
- })
193
-
194
- const existing = comments.find((c) => c.body?.includes(COMMENT_MARKER))
195
-
196
- if (existing) {
197
- await octokit.rest.issues.updateComment({
198
- owner,
199
- repo,
200
- comment_id: existing.id,
201
- body,
202
- })
203
- core.info('Updated existing FlagShark comment')
204
- } else {
205
- await octokit.rest.issues.createComment({
206
- owner,
207
- repo,
208
- issue_number: prNumber,
209
- body,
210
- })
211
- core.info('Posted new FlagShark comment')
212
- }
213
- }
214
-
215
- function walkDir(dir: string, supportedExts: Set<string>): string[] {
216
- const results: string[] = []
217
-
218
- try {
219
- const entries = readdirSync(dir, { withFileTypes: true })
220
- for (const entry of entries) {
221
- if (SKIP_DIRS.has(entry.name)) {
222
- continue
223
- }
224
- if (entry.name.startsWith('.')) {
225
- continue
226
- }
227
-
228
- const fullPath = join(dir, entry.name)
229
-
230
- if (entry.isDirectory()) {
231
- results.push(...walkDir(fullPath, supportedExts))
232
- } else if (entry.isFile() && supportedExts.has(extname(entry.name))) {
233
- results.push(fullPath)
234
- }
235
- }
236
- } catch {
237
- // Skip unreadable directories
238
- }
239
-
240
- return results
241
- }
242
-
243
- run()