flagshark 1.3.0 โ 1.4.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/README.md +111 -106
- package/dist/cli.js +93 -157
- package/package.json +6 -4
package/README.md
CHANGED
|
@@ -1,60 +1,82 @@
|
|
|
1
|
-
# flagshark
|
|
1
|
+
# ๐ฆ flagshark
|
|
2
2
|
|
|
3
|
-
Find stale feature flags in your codebase
|
|
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.
|
|
10
|
+
๐ฆ FlagShark v1.3.0 โ scanned 156 files in 2.3s
|
|
11
|
+
(47 excluded via .flagsharkignore + test-files preset)
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
|
|
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 โ
|
|
21
|
-
โ NEW_NAV โ src/layout.tsx:12 โ 8 months ago โ
|
|
22
|
-
โ BETA_SEARCH โ src/search.ts:91 โ 11 months ago โ
|
|
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
|
-
|
|
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
|
-
#
|
|
39
|
+
# Recommended โ run without installing
|
|
32
40
|
npx flagshark scan
|
|
33
41
|
|
|
34
|
-
# Or
|
|
42
|
+
# Or globally
|
|
35
43
|
npm install -g flagshark
|
|
36
44
|
```
|
|
37
45
|
|
|
38
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
-
#
|
|
54
|
-
flagshark scan --threshold 3
|
|
75
|
+
# Stricter threshold + JSON for piping
|
|
76
|
+
flagshark scan --threshold 3 --json | jq '.staleFlags'
|
|
55
77
|
|
|
56
|
-
#
|
|
57
|
-
flagshark scan --
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
167
|
+
See the [main repo README](https://github.com/FlagShark/flagshark) for action inputs and full docs.
|
|
93
168
|
|
|
94
|
-
|
|
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
|
-
|
|
171
|
+
Building a tool on top of the engine? Use [`@flagshark/core`](https://www.npmjs.com/package/@flagshark/core) directly:
|
|
101
172
|
|
|
102
|
-
|
|
173
|
+
```ts
|
|
174
|
+
import { scanRepo } from '@flagshark/core'
|
|
103
175
|
|
|
104
|
-
|
|
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,140 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import { readFileSync, existsSync } from "node:fs";
|
|
4
|
+
import { readFileSync, existsSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { parse as parseYaml } from "yaml";
|
|
6
|
-
import { scanRepo, FlagsharkConfigSchema } from "@flagshark/core";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
var VERSION = "1.2.0";
|
|
10
|
-
function pad(str, width) {
|
|
11
|
-
if (str.length > width) {
|
|
12
|
-
return str.slice(0, width - 1) + "\u2026";
|
|
13
|
-
}
|
|
14
|
-
return str.padEnd(width);
|
|
15
|
-
}
|
|
16
|
-
function buildTable(flags) {
|
|
17
|
-
const cols = {
|
|
18
|
-
flag: 16,
|
|
19
|
-
file: 22,
|
|
20
|
-
added: 13,
|
|
21
|
-
signal: 28
|
|
22
|
-
};
|
|
23
|
-
const hBorder = (left, mid, right) => `${left}${"\u2500".repeat(cols.flag + 2)}${mid}${"\u2500".repeat(cols.file + 2)}${mid}${"\u2500".repeat(cols.added + 2)}${mid}${"\u2500".repeat(cols.signal + 2)}${right}`;
|
|
24
|
-
const lines = [];
|
|
25
|
-
lines.push(hBorder("\u250C", "\u252C", "\u2510"));
|
|
26
|
-
lines.push(
|
|
27
|
-
`\u2502 ${pad("Flag", cols.flag)} \u2502 ${pad("File", cols.file)} \u2502 ${pad("Added", cols.added)} \u2502 ${pad("Signal", cols.signal)} \u2502`
|
|
28
|
-
);
|
|
29
|
-
lines.push(hBorder("\u251C", "\u253C", "\u2524"));
|
|
30
|
-
for (const sf of flags) {
|
|
31
|
-
const fileRef = `${sf.filePath}:${sf.lineNumber}`;
|
|
32
|
-
const signalText = sf.signals.map((s) => {
|
|
33
|
-
if (s.type === "age") {
|
|
34
|
-
return "Age > threshold";
|
|
35
|
-
}
|
|
36
|
-
if (s.type === "low-usage") {
|
|
37
|
-
return "Single file";
|
|
38
|
-
}
|
|
39
|
-
return s.description;
|
|
40
|
-
}).join(", ");
|
|
41
|
-
lines.push(
|
|
42
|
-
`\u2502 ${pad(sf.name, cols.flag)} \u2502 ${pad(fileRef, cols.file)} \u2502 ${pad(sf.age ?? "unknown", cols.added)} \u2502 ${pad(signalText, cols.signal)} \u2502`
|
|
43
|
-
);
|
|
44
|
-
}
|
|
45
|
-
lines.push(hBorder("\u2514", "\u2534", "\u2518"));
|
|
46
|
-
return lines.join("\n");
|
|
47
|
-
}
|
|
48
|
-
function formatText(result, options) {
|
|
49
|
-
const lines = [];
|
|
50
|
-
lines.push(`\u{1F988} FlagShark v${VERSION}`);
|
|
51
|
-
lines.push("");
|
|
52
|
-
const langCount = Object.keys(result.languageBreakdown).length;
|
|
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
|
-
}
|
|
57
|
-
if (result.totalFlags === 0) {
|
|
58
|
-
lines.push("No feature flags detected.");
|
|
59
|
-
lines.push("");
|
|
60
|
-
lines.push("Supported providers: LaunchDarkly, Unleash, Flipt, Split.io, PostHog, and more.");
|
|
61
|
-
lines.push("Run flagshark scan --help for configuration options.");
|
|
62
|
-
return lines.join("\n");
|
|
63
|
-
}
|
|
64
|
-
if (result.detectedProviders.length > 0) {
|
|
65
|
-
lines.push(`Detected providers: ${result.detectedProviders.join(", ")}`);
|
|
66
|
-
}
|
|
67
|
-
const uniqueStaleNames = new Set(result.staleFlags.map((f) => f.name));
|
|
68
|
-
const staleCount = uniqueStaleNames.size;
|
|
69
|
-
lines.push(`Found ${result.totalFlags} feature flags, ${staleCount} stale`);
|
|
70
|
-
if (staleCount > 0) {
|
|
71
|
-
lines.push("");
|
|
72
|
-
lines.push("Stale flags:");
|
|
73
|
-
const displayCount = options.verbose ? staleCount : Math.min(staleCount, options.maxDisplay);
|
|
74
|
-
const displayFlags = result.staleFlags.slice(0, displayCount);
|
|
75
|
-
lines.push(buildTable(displayFlags));
|
|
76
|
-
const remaining = staleCount - displayCount;
|
|
77
|
-
if (remaining > 0) {
|
|
78
|
-
lines.push("");
|
|
79
|
-
lines.push(`... and ${remaining} more (use --verbose to see all)`);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
lines.push("");
|
|
83
|
-
if (staleCount === 0) {
|
|
84
|
-
lines.push(`Flag Health Score: ${result.healthScore}/100 \u2713 All flags look healthy!`);
|
|
85
|
-
} else {
|
|
86
|
-
lines.push(
|
|
87
|
-
`Flag Health Score: ${result.healthScore}/100 (${staleCount}/${result.totalFlags} flags are stale)`
|
|
88
|
-
);
|
|
89
|
-
lines.push("");
|
|
90
|
-
lines.push("Automate cleanup \u2192 https://flagshark.com");
|
|
91
|
-
lines.push("Open source CLI \u2192 https://github.com/FlagShark/flagshark");
|
|
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
|
-
}
|
|
100
|
-
return lines.join("\n");
|
|
101
|
-
}
|
|
102
|
-
function formatJson(result) {
|
|
103
|
-
const languages = { ...result.languageBreakdown };
|
|
104
|
-
const flags = result.staleFlags.map((sf) => ({
|
|
105
|
-
name: sf.name,
|
|
106
|
-
file: sf.filePath,
|
|
107
|
-
line: sf.lineNumber,
|
|
108
|
-
language: sf.language,
|
|
109
|
-
provider: sf.provider,
|
|
110
|
-
stale: true,
|
|
111
|
-
signals: sf.signals.map((s) => ({
|
|
112
|
-
type: s.type,
|
|
113
|
-
description: s.description
|
|
114
|
-
})),
|
|
115
|
-
age: sf.age ?? null
|
|
116
|
-
}));
|
|
117
|
-
const output = {
|
|
118
|
-
version: VERSION,
|
|
119
|
-
totalFlags: result.totalFlags,
|
|
120
|
-
staleFlags: new Set(result.staleFlags.map((f) => f.name)).size,
|
|
121
|
-
healthScore: result.healthScore,
|
|
122
|
-
detectedProviders: result.detectedProviders,
|
|
123
|
-
languages,
|
|
124
|
-
flags,
|
|
125
|
-
excludedPaths: result.excludedPaths,
|
|
126
|
-
scanDuration: result.scanDuration,
|
|
127
|
-
links: {
|
|
128
|
-
dashboard: "https://flagshark.com",
|
|
129
|
-
cli: "https://github.com/FlagShark/flagshark",
|
|
130
|
-
npm: "https://www.npmjs.com/package/flagshark"
|
|
131
|
-
}
|
|
132
|
-
};
|
|
133
|
-
return JSON.stringify(output, null, 2);
|
|
6
|
+
import { scanRepo, FlagsharkConfigSchema, selectFormatter } from "@flagshark/core";
|
|
7
|
+
function toErrorMessage(err) {
|
|
8
|
+
return err instanceof Error ? err.message : String(err);
|
|
134
9
|
}
|
|
135
|
-
|
|
136
|
-
// src/cli.ts
|
|
137
|
-
var VERSION2 = "1.3.0";
|
|
10
|
+
var VERSION = "1.3.1";
|
|
138
11
|
var HELP_TEXT = `
|
|
139
12
|
flagshark scan [options]
|
|
140
13
|
|
|
@@ -151,15 +24,27 @@ Configuration:
|
|
|
151
24
|
--no-config Skip config file discovery
|
|
152
25
|
--no-ignore-file Skip .flagsharkignore discovery
|
|
153
26
|
--show-excluded Show excluded files in text output
|
|
27
|
+
|
|
28
|
+
Output:
|
|
29
|
+
--format <fmt> Output format: text | json | markdown | csv | sarif (default: text)
|
|
30
|
+
--output <path> | -o Write output to this file instead of stdout
|
|
31
|
+
--json Shorthand for --format json (deprecated, will be removed in v2)
|
|
32
|
+
|
|
33
|
+
Platform integration:
|
|
34
|
+
--no-cache Skip platform-flag cache, force re-fetch
|
|
35
|
+
--fail-on-error Fail on any missing-in-platform flag (default: true)
|
|
36
|
+
--no-fail-on-error Disable fail-on-error
|
|
154
37
|
`.trim();
|
|
155
38
|
function parseArgs(argv) {
|
|
156
39
|
const args = {
|
|
157
40
|
json: false,
|
|
41
|
+
format: "text",
|
|
158
42
|
diff: null,
|
|
159
43
|
threshold: void 0,
|
|
160
44
|
verbose: false,
|
|
161
45
|
help: false,
|
|
162
|
-
version: false
|
|
46
|
+
version: false,
|
|
47
|
+
failOnError: true
|
|
163
48
|
};
|
|
164
49
|
let i = 2;
|
|
165
50
|
while (i < argv.length) {
|
|
@@ -172,6 +57,19 @@ function parseArgs(argv) {
|
|
|
172
57
|
switch (arg) {
|
|
173
58
|
case "--json":
|
|
174
59
|
args.json = true;
|
|
60
|
+
args.format = "json";
|
|
61
|
+
break;
|
|
62
|
+
case "--format": {
|
|
63
|
+
const v = argv[++i];
|
|
64
|
+
if (!["text", "json", "markdown", "csv", "sarif"].includes(v)) {
|
|
65
|
+
throw new Error(`--format must be one of text, json, markdown, csv, sarif; got '${v}'`);
|
|
66
|
+
}
|
|
67
|
+
args.format = v;
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
case "--output":
|
|
71
|
+
case "-o":
|
|
72
|
+
args.output = argv[++i];
|
|
175
73
|
break;
|
|
176
74
|
case "--diff":
|
|
177
75
|
i++;
|
|
@@ -201,9 +99,7 @@ function parseArgs(argv) {
|
|
|
201
99
|
case "--engine": {
|
|
202
100
|
const value = argv[++i];
|
|
203
101
|
if (value !== "regex" && value !== "tree-sitter") {
|
|
204
|
-
|
|
205
|
-
`);
|
|
206
|
-
process.exit(2);
|
|
102
|
+
throw new Error(`--engine must be 'regex' or 'tree-sitter', got '${value}'`);
|
|
207
103
|
}
|
|
208
104
|
args.engine = value;
|
|
209
105
|
break;
|
|
@@ -224,6 +120,15 @@ function parseArgs(argv) {
|
|
|
224
120
|
case "--show-excluded":
|
|
225
121
|
args.showExcluded = true;
|
|
226
122
|
break;
|
|
123
|
+
case "--no-cache":
|
|
124
|
+
args.noCache = true;
|
|
125
|
+
break;
|
|
126
|
+
case "--no-fail-on-error":
|
|
127
|
+
args.failOnError = false;
|
|
128
|
+
break;
|
|
129
|
+
case "--fail-on-error":
|
|
130
|
+
args.failOnError = true;
|
|
131
|
+
break;
|
|
227
132
|
case "scan":
|
|
228
133
|
break;
|
|
229
134
|
default:
|
|
@@ -245,16 +150,23 @@ function createLogger(verbose) {
|
|
|
245
150
|
error: (...args) => console.error("[error]", ...args)
|
|
246
151
|
};
|
|
247
152
|
}
|
|
248
|
-
async function
|
|
249
|
-
|
|
153
|
+
async function runCli(argv, io) {
|
|
154
|
+
let args;
|
|
155
|
+
try {
|
|
156
|
+
args = parseArgs(argv);
|
|
157
|
+
} catch (err) {
|
|
158
|
+
io.stderr.write(`[error] ${toErrorMessage(err)}
|
|
159
|
+
`);
|
|
160
|
+
return 2;
|
|
161
|
+
}
|
|
250
162
|
if (args.version) {
|
|
251
|
-
|
|
163
|
+
io.stdout.write(`flagshark v${VERSION}
|
|
252
164
|
`);
|
|
253
|
-
|
|
165
|
+
return 0;
|
|
254
166
|
}
|
|
255
167
|
if (args.help) {
|
|
256
|
-
|
|
257
|
-
|
|
168
|
+
io.stdout.write(HELP_TEXT + "\n");
|
|
169
|
+
return 0;
|
|
258
170
|
}
|
|
259
171
|
const logger = createLogger(args.verbose);
|
|
260
172
|
if (args.diff) {
|
|
@@ -265,22 +177,29 @@ async function main() {
|
|
|
265
177
|
let configOverride;
|
|
266
178
|
if (args.configPath) {
|
|
267
179
|
if (!existsSync(args.configPath)) {
|
|
268
|
-
|
|
180
|
+
io.stderr.write(`Error: config file not found: ${args.configPath}
|
|
269
181
|
`);
|
|
270
|
-
|
|
182
|
+
return 2;
|
|
271
183
|
}
|
|
272
184
|
const raw = readFileSync(args.configPath, "utf-8");
|
|
273
|
-
|
|
185
|
+
let parsed;
|
|
186
|
+
try {
|
|
187
|
+
parsed = parseYaml(raw);
|
|
188
|
+
} catch (err) {
|
|
189
|
+
io.stderr.write(`Error: invalid YAML at ${args.configPath}: ${toErrorMessage(err)}
|
|
190
|
+
`);
|
|
191
|
+
return 2;
|
|
192
|
+
}
|
|
274
193
|
const configResult = FlagsharkConfigSchema.safeParse(parsed);
|
|
275
194
|
if (!configResult.success) {
|
|
276
|
-
|
|
195
|
+
io.stderr.write(`Error: invalid config at ${args.configPath}: ${configResult.error.message}
|
|
277
196
|
`);
|
|
278
|
-
|
|
197
|
+
return 2;
|
|
279
198
|
}
|
|
280
199
|
configOverride = configResult.data;
|
|
281
200
|
}
|
|
282
201
|
const result = await scanRepo({
|
|
283
|
-
cwd:
|
|
202
|
+
cwd: io.cwd,
|
|
284
203
|
threshold: args.threshold,
|
|
285
204
|
diff: args.diff ?? void 0,
|
|
286
205
|
engine: args.engine,
|
|
@@ -288,6 +207,7 @@ async function main() {
|
|
|
288
207
|
noConfig: args.noConfig,
|
|
289
208
|
noIgnoreFile: args.noIgnoreFile,
|
|
290
209
|
collectExcludedPaths: args.showExcluded,
|
|
210
|
+
noCache: args.noCache,
|
|
291
211
|
logger
|
|
292
212
|
});
|
|
293
213
|
if (args.verbose && result.effectiveExcludes) {
|
|
@@ -299,20 +219,36 @@ async function main() {
|
|
|
299
219
|
...r.ignoreFile.map((p) => `.flagsharkignore: ${p}`)
|
|
300
220
|
];
|
|
301
221
|
if (allRules.length > 0) {
|
|
302
|
-
|
|
303
|
-
for (const rule of allRules)
|
|
222
|
+
io.stderr.write("Effective excludes:\n");
|
|
223
|
+
for (const rule of allRules) io.stderr.write(` ${rule}
|
|
304
224
|
`);
|
|
305
225
|
}
|
|
306
226
|
}
|
|
307
|
-
const
|
|
308
|
-
const
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
227
|
+
const formatter = selectFormatter(args.format);
|
|
228
|
+
const output = formatter(result, {
|
|
229
|
+
version: VERSION,
|
|
230
|
+
scanMode: args.diff ? "changed" : "full",
|
|
231
|
+
verbose: args.verbose
|
|
232
|
+
});
|
|
233
|
+
const hasErrorSignals = result.staleFlags.some(
|
|
234
|
+
(f) => f.signals.some((s) => s.severity === "error")
|
|
235
|
+
);
|
|
236
|
+
const exitCode = args.failOnError && hasErrorSignals ? 1 : result.staleFlags.length > 0 ? 1 : 0;
|
|
237
|
+
if (args.output) {
|
|
238
|
+
writeFileSync(args.output, output);
|
|
239
|
+
return exitCode;
|
|
313
240
|
}
|
|
241
|
+
const finalOutput = output.endsWith("\n") ? output : output + "\n";
|
|
242
|
+
io.stdout.write(finalOutput);
|
|
243
|
+
return exitCode;
|
|
314
244
|
}
|
|
315
|
-
|
|
245
|
+
|
|
246
|
+
// src/main.ts
|
|
247
|
+
runCli(process.argv, {
|
|
248
|
+
stdout: process.stdout,
|
|
249
|
+
stderr: process.stderr,
|
|
250
|
+
cwd: process.cwd()
|
|
251
|
+
}).then((code) => process.exit(code)).catch((err) => {
|
|
316
252
|
console.error(`[error] ${err instanceof Error ? err.message : String(err)}`);
|
|
317
253
|
process.exit(2);
|
|
318
254
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flagshark",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Find stale feature flags in your codebase",
|
|
6
6
|
"license": "MIT",
|
|
@@ -17,17 +17,19 @@
|
|
|
17
17
|
"main": "./dist/cli.js",
|
|
18
18
|
"files": ["dist/", "bin/"],
|
|
19
19
|
"scripts": {
|
|
20
|
-
"build": "esbuild src/
|
|
21
|
-
"test": "vitest run",
|
|
20
|
+
"build": "esbuild src/main.ts --bundle --platform=node --target=node18 --format=esm --outfile=dist/cli.js --external:zod --external:@flagshark/core --external:yaml",
|
|
21
|
+
"test": "bun run build && vitest run",
|
|
22
|
+
"test:coverage": "bun run build && vitest run --coverage",
|
|
22
23
|
"typecheck": "tsc --noEmit"
|
|
23
24
|
},
|
|
24
25
|
"dependencies": {
|
|
25
|
-
"@flagshark/core": "1.
|
|
26
|
+
"@flagshark/core": "1.4.0",
|
|
26
27
|
"yaml": "^2.4.0",
|
|
27
28
|
"zod": "^3.23.0"
|
|
28
29
|
},
|
|
29
30
|
"devDependencies": {
|
|
30
31
|
"@types/node": "^22.0.0",
|
|
32
|
+
"@vitest/coverage-v8": "^3.0.0",
|
|
31
33
|
"esbuild": "^0.24.0",
|
|
32
34
|
"typescript": "^5.7.0",
|
|
33
35
|
"vitest": "^3.0.0"
|