flagshark 1.0.1 โ†’ 1.1.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.
package/package.json CHANGED
@@ -1,56 +1,33 @@
1
1
  {
2
2
  "name": "flagshark",
3
- "version": "1.0.1",
4
- "description": "Find stale feature flags in your codebase",
3
+ "version": "1.1.1",
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=cjs --outfile=dist/action.cjs",
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": "^1.0.0",
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
- env:
87
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
88
- ```
89
-
90
- ### Action Inputs
91
-
92
- | Input | Default | Description |
93
- |-------|---------|-------------|
94
- | `scan` | `changed` | `changed` (PR files only) or `full` (entire repo) |
95
- | `threshold` | `6` | Staleness threshold in months |
96
- | `fail-threshold` | `0` | Health score below which the check fails (0 = never fail) |
97
-
98
- ### Scan Modes
99
-
100
- **`scan: changed`** (default) scans only files modified in the PR. Fast, focused on what you're changing.
101
-
102
- **`scan: full`** scans the entire repository. Shows your full flag health score and finds stale flags everywhere, not just in changed files. Great for seeing the big picture:
103
-
104
- ```yaml
105
- - uses: FlagShark/flagshark@v1
106
- with:
107
- scan: full
108
- env:
109
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
110
- ```
111
-
112
- ### What the Action does
113
-
114
- On every PR, FlagShark comments with a table of stale flags:
115
-
116
- > ### ๐Ÿฆˆ FlagShark found 3 stale flags
117
- >
118
- > | Flag | File | Added | Signal |
119
- > |------|------|-------|--------|
120
- > | `CHECKOUT_V2` | src/checkout.ts:47 | 14 months ago | Age > 6 months |
121
- > | `NEW_NAV` | src/layout.tsx:12 | 8 months ago | Single file |
122
- >
123
- > **Flag Health:** 70/100
124
-
125
- It also sets a GitHub status check that can optionally block merge if health drops below a threshold.
126
-
127
- ## Supported Languages
128
-
129
- FlagShark detects feature flags across 13 languages:
130
-
131
- | Language | Extensions |
132
- |----------|-----------|
133
- | TypeScript/JavaScript | .ts, .tsx, .js, .jsx, .mjs, .cjs |
134
- | Go | .go |
135
- | Python | .py |
136
- | Java | .java |
137
- | Kotlin | .kt |
138
- | Swift | .swift |
139
- | Ruby | .rb |
140
- | C# | .cs |
141
- | PHP | .php |
142
- | Rust | .rs |
143
- | C/C++ | .c, .cpp, .h, .hpp |
144
- | Objective-C | .m |
145
-
146
- ## Supported Providers
147
-
148
- Auto-detected from imports (no configuration needed):
149
-
150
- - LaunchDarkly
151
- - Unleash
152
- - Flipt
153
- - Split.io
154
- - PostHog
155
- - Flagsmith
156
- - ConfigCat
157
- - Statsig
158
- - GrowthBook
159
- - DevCycle
160
- - Eppo
161
- - Optimizely
162
- - Custom flag implementations
163
-
164
- ## How Staleness Works
165
-
166
- A flag is marked stale if **any** of these signals fires:
167
-
168
- 1. **Age:** `git blame` shows the flag reference was added more than 6 months ago (configurable with `--threshold`)
169
- 2. **Single file:** The flag name appears in only one file across the entire repo, suggesting a completed rollout
170
-
171
- 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.
172
-
173
- ## License
174
-
175
- MIT
package/action/index.ts DELETED
@@ -1,316 +0,0 @@
1
- /**
2
- * GitHub Action entry point for FlagShark.
3
- *
4
- * Scans a repo for stale feature flags, posts a rich PR comment,
5
- * writes a GitHub Actions job summary, and sets a status check.
6
- */
7
-
8
- import { readFileSync, readdirSync, statSync } from 'node:fs'
9
- import { join, extname } from 'node:path'
10
-
11
- import * as core from '@actions/core'
12
- import * as github from '@actions/github'
13
-
14
- import { createDefaultRegistry } from '../src/detection/index.js'
15
- import { PolyglotAnalyzer } from '../src/detection/polyglot-analyzer.js'
16
- import { analyzeStaleness } from '../src/staleness.js'
17
-
18
- import type { FeatureFlag } from '../src/detection/feature-flag.js'
19
- import type { StaleFlag } from '../src/staleness.js'
20
-
21
- const COMMENT_MARKER = '<!-- flagshark-action -->'
22
- const SKIP_DIRS = new Set([
23
- 'node_modules', 'vendor', '.git', 'dist', 'build',
24
- 'coverage', '__pycache__', '.next', '.turbo',
25
- ])
26
-
27
- // Logger that serializes objects properly instead of [object Object]
28
- const logger = {
29
- debug: (...args: unknown[]) => core.debug(formatLogArgs(args)),
30
- info: (...args: unknown[]) => core.info(formatLogArgs(args)),
31
- warn: (...args: unknown[]) => core.warning(formatLogArgs(args)),
32
- error: (...args: unknown[]) => core.error(formatLogArgs(args)),
33
- }
34
-
35
- function formatLogArgs(args: unknown[]): string {
36
- return args.map(a =>
37
- typeof a === 'object' && a !== null ? JSON.stringify(a, null, 2) : String(a)
38
- ).join(' ')
39
- }
40
-
41
- async function run(): Promise<void> {
42
- const startTime = Date.now()
43
-
44
- try {
45
- const scanMode = core.getInput('scan') || 'changed'
46
- const threshold = parseInt(core.getInput('threshold') || '6', 10)
47
- const failThreshold = parseInt(core.getInput('fail-threshold') || '0', 10)
48
-
49
- const registry = createDefaultRegistry()
50
- const supportedExts = new Set(registry.getSupportedExtensions())
51
- const analyzer = new PolyglotAnalyzer(registry, logger)
52
-
53
- // Determine files to scan
54
- let filePaths: string[]
55
-
56
- if (scanMode === 'changed' && github.context.payload.pull_request) {
57
- const token = process.env.GITHUB_TOKEN || core.getInput('token')
58
- if (!token) {
59
- core.setFailed('GITHUB_TOKEN is required for changed-file scanning')
60
- return
61
- }
62
- const octokit = github.getOctokit(token)
63
- const { data: prFiles } = await octokit.rest.pulls.listFiles({
64
- ...github.context.repo,
65
- pull_number: github.context.payload.pull_request.number,
66
- per_page: 100,
67
- })
68
- filePaths = prFiles
69
- .filter((f) => f.status !== 'removed')
70
- .map((f) => f.filename)
71
- .filter((f) => supportedExts.has(extname(f)))
72
- } else {
73
- filePaths = walkDir('.', supportedExts)
74
- }
75
-
76
- // Read file contents
77
- const files = new Map<string, string>()
78
- for (const fp of filePaths) {
79
- try {
80
- const stat = statSync(fp)
81
- if (stat.size > 5 * 1024 * 1024) continue
82
- files.set(fp, readFileSync(fp, 'utf-8'))
83
- } catch {
84
- // Skip unreadable files
85
- }
86
- }
87
-
88
- core.info(`Scanning ${files.size} files...`)
89
-
90
- // Run detection
91
- const result = await analyzer.analyzeFiles(files)
92
- const totalFlags = result.totalFlags.size
93
-
94
- // Collect language stats
95
- const langStats: Record<string, number> = {}
96
- for (const [lang, count] of result.languages) {
97
- langStats[lang] = count
98
- }
99
-
100
- // Collect detected providers
101
- const allFlags: FeatureFlag[] = []
102
- for (const flags of result.totalFlags.values()) {
103
- allFlags.push(...(flags as FeatureFlag[]))
104
- }
105
- const providers = [...new Set(
106
- allFlags.map(f => f.provider).filter((p): p is string => p !== null && p !== undefined && p !== '')
107
- )]
108
-
109
- core.info(`Detection complete: ${totalFlags} unique flags across ${Object.keys(langStats).length} languages`)
110
-
111
- // Run staleness analysis
112
- const staleFlags = await analyzeStaleness(result.totalFlags as Map<string, FeatureFlag[]>, {
113
- thresholdMonths: threshold,
114
- repoRoot: process.cwd(),
115
- })
116
-
117
- const uniqueStaleNames = new Set(staleFlags.map((f) => f.name)).size
118
- const healthScore =
119
- totalFlags > 0 ? Math.round(((totalFlags - uniqueStaleNames) / totalFlags) * 100) : 100
120
- const scanDuration = Date.now() - startTime
121
-
122
- // Pretty log output
123
- core.info('')
124
- core.info('โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”')
125
- core.info('โ”‚ ๐Ÿฆˆ FlagShark Scan Results โ”‚')
126
- core.info('โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค')
127
- core.info(`โ”‚ Files scanned: ${String(files.size).padStart(6)} โ”‚`)
128
- core.info(`โ”‚ Languages: ${String(Object.keys(langStats).length).padStart(6)} โ”‚`)
129
- core.info(`โ”‚ Flags detected: ${String(totalFlags).padStart(6)} โ”‚`)
130
- core.info(`โ”‚ Stale flags: ${String(uniqueStaleNames).padStart(6)} โ”‚`)
131
- core.info(`โ”‚ Health score: ${String(healthScore).padStart(3)}/100 โ”‚`)
132
- core.info(`โ”‚ Scan time: ${String(scanDuration).padStart(5)}ms โ”‚`)
133
- core.info('โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜')
134
- core.info('')
135
-
136
- if (providers.length > 0) {
137
- core.info(`Detected providers: ${providers.slice(0, 8).join(', ')}${providers.length > 8 ? ` (+${providers.length - 8} more)` : ''}`)
138
- }
139
-
140
- // Set outputs
141
- core.setOutput('health-score', healthScore.toString())
142
- core.setOutput('stale-count', uniqueStaleNames.toString())
143
- core.setOutput('total-count', totalFlags.toString())
144
-
145
- // Post PR comment
146
- if (github.context.payload.pull_request && totalFlags > 0) {
147
- const token = process.env.GITHUB_TOKEN || core.getInput('token')
148
- if (token) {
149
- await postComment(token, staleFlags, totalFlags, healthScore, scanMode, langStats, providers, scanDuration)
150
- }
151
- }
152
-
153
- // Set status check
154
- if (failThreshold > 0 && healthScore < failThreshold) {
155
- core.setFailed(
156
- `Flag health score ${healthScore}/100 is below threshold ${failThreshold}/100. ` +
157
- `${uniqueStaleNames} stale flags found.`,
158
- )
159
- }
160
-
161
- // Job summary (visible in Actions UI under "Summary" tab)
162
- const healthEmoji = healthScore >= 90 ? '๐ŸŸข' : healthScore >= 70 ? '๐ŸŸก' : healthScore >= 40 ? '๐ŸŸ ' : '๐Ÿ”ด'
163
-
164
- core.summary.addHeading('๐Ÿฆˆ FlagShark Scan Results', 2)
165
- core.summary.addRaw(`\n${healthEmoji} **Health Score: ${healthScore}/100**\n\n`)
166
- core.summary.addTable([
167
- [{ data: 'Metric', header: true }, { data: 'Value', header: true }],
168
- ['Files scanned', files.size.toString()],
169
- ['Languages', Object.keys(langStats).join(', ') || 'none'],
170
- ['Total flags', totalFlags.toString()],
171
- ['Stale flags', uniqueStaleNames.toString()],
172
- ['Scan mode', scanMode],
173
- ['Scan time', `${scanDuration}ms`],
174
- ])
175
-
176
- if (providers.length > 0) {
177
- core.summary.addRaw(`\n**Detected providers:** ${providers.join(', ')}\n`)
178
- }
179
-
180
- if (uniqueStaleNames > 0) {
181
- core.summary.addRaw('\n### Top stale flags\n\n')
182
- core.summary.addTable([
183
- [{ data: 'Flag', header: true }, { data: 'File', header: true }, { data: 'Age', header: true }, { data: 'Signal', header: true }],
184
- ...staleFlags.slice(0, 15).map(f => [
185
- `\`${f.name}\``,
186
- `${f.filePath}:${f.lineNumber}`,
187
- f.age || 'unknown',
188
- f.signals.map(s => s.description).join(', '),
189
- ]),
190
- ])
191
- if (staleFlags.length > 15) {
192
- core.summary.addRaw(`\n*... and ${staleFlags.length - 15} more stale flags*\n`)
193
- }
194
- }
195
-
196
- core.summary.addRaw('\n---\n')
197
- core.summary.addRaw('*Powered by [FlagShark](https://github.com/FlagShark/flagshark) โ€” find stale feature flags before they cause incidents*\n')
198
- core.summary.addRaw('\n[Automate flag cleanup](https://flagshark.com) ยท [Open source CLI](https://github.com/FlagShark/flagshark) ยท [Report an issue](https://github.com/FlagShark/flagshark/issues)\n')
199
-
200
- await core.summary.write()
201
-
202
- } catch (error) {
203
- if (error instanceof Error) {
204
- core.setFailed(error.message)
205
- } else {
206
- core.setFailed('An unexpected error occurred')
207
- }
208
- }
209
- }
210
-
211
- async function postComment(
212
- token: string,
213
- staleFlags: StaleFlag[],
214
- totalFlags: number,
215
- healthScore: number,
216
- scanMode: string,
217
- langStats: Record<string, number>,
218
- providers: string[],
219
- scanDuration: number,
220
- ): Promise<void> {
221
- const octokit = github.getOctokit(token)
222
- const { owner, repo } = github.context.repo
223
- const prNumber = github.context.payload.pull_request!.number
224
-
225
- const uniqueStaleCount = new Set(staleFlags.map((f) => f.name)).size
226
- const modeLabel = scanMode === 'full' ? 'Full repo scan' : 'Changed files only'
227
- const healthEmoji = healthScore >= 90 ? '๐ŸŸข' : healthScore >= 70 ? '๐ŸŸก' : healthScore >= 40 ? '๐ŸŸ ' : '๐Ÿ”ด'
228
- const langList = Object.entries(langStats).map(([l, c]) => `${l} (${c})`).join(', ')
229
- const providerList = providers.length > 0
230
- ? providers.slice(0, 5).join(', ') + (providers.length > 5 ? ` +${providers.length - 5} more` : '')
231
- : 'none detected'
232
-
233
- let body = `${COMMENT_MARKER}\n`
234
-
235
- // Header
236
- if (uniqueStaleCount === 0) {
237
- body += `## ๐Ÿฆˆ FlagShark โ€” All flags healthy\n\n`
238
- } else {
239
- body += `## ๐Ÿฆˆ FlagShark โ€” ${uniqueStaleCount} stale flag${uniqueStaleCount !== 1 ? 's' : ''} found\n\n`
240
- }
241
-
242
- // Health score badge
243
- body += `${healthEmoji} **Health Score: ${healthScore}/100**\n\n`
244
-
245
- // Stats row
246
- body += `| Metric | Value |\n`
247
- body += `|--------|-------|\n`
248
- body += `| Flags detected | ${totalFlags} |\n`
249
- body += `| Stale flags | ${uniqueStaleCount} |\n`
250
- body += `| Languages | ${langList} |\n`
251
- body += `| Providers | ${providerList} |\n`
252
- body += `| Scan mode | ${modeLabel} |\n`
253
- body += `| Scan time | ${scanDuration}ms |\n\n`
254
-
255
- // Stale flags table
256
- if (uniqueStaleCount > 0) {
257
- body += `<details${uniqueStaleCount <= 5 ? ' open' : ''}>\n`
258
- body += `<summary><strong>Stale flags (${uniqueStaleCount})</strong></summary>\n\n`
259
- body += '| Flag | File | Age | Why it looks stale |\n'
260
- body += '|------|------|-----|--------------------|\n'
261
-
262
- const displayFlags = staleFlags.slice(0, 20)
263
- for (const flag of displayFlags) {
264
- const signals = flag.signals.map(s => s.description).join(', ')
265
- const shortPath = flag.filePath.replace(/^\.\//, '')
266
- body += `| \`${flag.name}\` | \`${shortPath}:${flag.lineNumber}\` | ${flag.age || 'unknown'} | ${signals} |\n`
267
- }
268
-
269
- if (staleFlags.length > 20) {
270
- body += `\n*... and ${staleFlags.length - 20} more. Run \`npx flagshark scan --verbose\` locally for the full list.*\n`
271
- }
272
-
273
- body += '\n</details>\n\n'
274
- }
275
-
276
- // Footer with links
277
- body += '---\n'
278
- body += `*[FlagShark](https://github.com/FlagShark/flagshark) finds stale feature flags before they cause incidents*\n\n`
279
- body += `[Automate flag cleanup](https://flagshark.com) ยท `
280
- body += `[Install CLI](https://www.npmjs.com/package/flagshark) ยท `
281
- body += `[Open source](https://github.com/FlagShark/flagshark)\n`
282
-
283
- // Find existing comment to update
284
- const { data: comments } = await octokit.rest.issues.listComments({
285
- owner, repo, issue_number: prNumber, per_page: 100,
286
- })
287
-
288
- const existing = comments.find((c) => c.body?.includes(COMMENT_MARKER))
289
-
290
- if (existing) {
291
- await octokit.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body })
292
- core.info('Updated existing FlagShark comment')
293
- } else {
294
- await octokit.rest.issues.createComment({ owner, repo, issue_number: prNumber, body })
295
- core.info('Posted new FlagShark comment')
296
- }
297
- }
298
-
299
- function walkDir(dir: string, supportedExts: Set<string>): string[] {
300
- const results: string[] = []
301
- try {
302
- const entries = readdirSync(dir, { withFileTypes: true })
303
- for (const entry of entries) {
304
- if (SKIP_DIRS.has(entry.name) || entry.name.startsWith('.')) continue
305
- const fullPath = join(dir, entry.name)
306
- if (entry.isDirectory()) {
307
- results.push(...walkDir(fullPath, supportedExts))
308
- } else if (entry.isFile() && supportedExts.has(extname(entry.name))) {
309
- results.push(fullPath)
310
- }
311
- }
312
- } catch { /* skip unreadable dirs */ }
313
- return results
314
- }
315
-
316
- run()