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.
Files changed (3) hide show
  1. package/README.md +111 -106
  2. package/dist/cli.js +93 -157
  3. 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. 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,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
- // src/formatter.ts
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
- process.stderr.write(`Error: --engine must be 'regex' or 'tree-sitter', got '${value}'
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 main() {
249
- const args = parseArgs(process.argv);
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
- process.stdout.write(`flagshark v${VERSION2}
163
+ io.stdout.write(`flagshark v${VERSION}
252
164
  `);
253
- process.exit(0);
165
+ return 0;
254
166
  }
255
167
  if (args.help) {
256
- process.stdout.write(HELP_TEXT + "\n");
257
- process.exit(0);
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
- process.stderr.write(`Error: config file not found: ${args.configPath}
180
+ io.stderr.write(`Error: config file not found: ${args.configPath}
269
181
  `);
270
- process.exit(2);
182
+ return 2;
271
183
  }
272
184
  const raw = readFileSync(args.configPath, "utf-8");
273
- const parsed = parseYaml(raw);
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
- process.stderr.write(`Error: invalid config at ${args.configPath}: ${configResult.error.message}
195
+ io.stderr.write(`Error: invalid config at ${args.configPath}: ${configResult.error.message}
277
196
  `);
278
- process.exit(2);
197
+ return 2;
279
198
  }
280
199
  configOverride = configResult.data;
281
200
  }
282
201
  const result = await scanRepo({
283
- cwd: process.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
- process.stderr.write("Effective excludes:\n");
303
- for (const rule of allRules) process.stderr.write(` ${rule}
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 output = args.json ? formatJson(result) + "\n" : formatText(result, { json: false, verbose: args.verbose, maxDisplay: 10 }) + "\n";
308
- const exitCode = result.staleFlags.length > 0 ? 1 : 0;
309
- if (process.stdout.write(output)) {
310
- process.exit(exitCode);
311
- } else {
312
- process.stdout.once("drain", () => process.exit(exitCode));
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
- main().catch((err) => {
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.0",
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/cli.ts --bundle --platform=node --target=node18 --format=esm --outfile=dist/cli.js --external:zod --external:@flagshark/core --external:yaml",
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.2.0",
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"