flagshark 1.3.1 → 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/dist/cli.js +93 -157
- package/package.json +6 -4
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.1";
|
|
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"
|