cerber-core 1.0.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/.cerber-example/BIBLE.md +132 -0
- package/.cerber-example/CERBER_LAW.md +200 -0
- package/.cerber-example/connections/contracts/booking-to-pricing.json +44 -0
- package/.cerber-example/connections/contracts/pricing-to-booking.json +37 -0
- package/.cerber-example/modules/booking-calendar/MODULE.md +225 -0
- package/.cerber-example/modules/booking-calendar/contract.json +106 -0
- package/.cerber-example/modules/booking-calendar/dependencies.json +8 -0
- package/.cerber-example/modules/pricing-engine/MODULE.md +160 -0
- package/.cerber-example/modules/pricing-engine/contract.json +64 -0
- package/.cerber-example/modules/pricing-engine/dependencies.json +8 -0
- package/CHANGELOG.md +68 -0
- package/LICENSE +21 -0
- package/README.md +1379 -0
- package/bin/cerber +105 -0
- package/bin/cerber-focus +31 -0
- package/bin/cerber-guardian +90 -0
- package/bin/cerber-health +113 -0
- package/bin/cerber-morning +19 -0
- package/bin/cerber-repair +21 -0
- package/dist/cerber/index.d.ts +47 -0
- package/dist/cerber/index.d.ts.map +1 -0
- package/dist/cerber/index.js +154 -0
- package/dist/cerber/index.js.map +1 -0
- package/dist/guardian/index.d.ts +70 -0
- package/dist/guardian/index.d.ts.map +1 -0
- package/dist/guardian/index.js +271 -0
- package/dist/guardian/index.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +76 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/examples/backend-schema.ts +72 -0
- package/examples/frontend-schema.ts +67 -0
- package/examples/health-checks.ts +196 -0
- package/examples/solo-integration/README.md +457 -0
- package/examples/solo-integration/package.json +47 -0
- package/examples/team-integration/README.md +347 -0
- package/examples/team-integration/package.json +23 -0
- package/package.json +104 -0
- package/solo/README.md +258 -0
- package/solo/config/performance-budget.json +53 -0
- package/solo/config/solo-contract.json +71 -0
- package/solo/lib/feature-flags.ts +177 -0
- package/solo/scripts/cerber-auto-repair.js +260 -0
- package/solo/scripts/cerber-daily-check.js +282 -0
- package/solo/scripts/cerber-dashboard.js +191 -0
- package/solo/scripts/cerber-deps-health.js +247 -0
- package/solo/scripts/cerber-docs-sync.js +304 -0
- package/solo/scripts/cerber-flags-check.js +229 -0
- package/solo/scripts/cerber-performance-budget.js +271 -0
- package/solo/scripts/cerber-rollback.js +229 -0
- package/solo/scripts/cerber-snapshot.js +319 -0
- package/team/README.md +327 -0
- package/team/config/team-contract.json +27 -0
- package/team/lib/module-system.ts +157 -0
- package/team/scripts/cerber-add-module.sh +195 -0
- package/team/scripts/cerber-connections-check.sh +186 -0
- package/team/scripts/cerber-focus.sh +170 -0
- package/team/scripts/cerber-module-check.sh +165 -0
- package/team/scripts/cerber-team-morning.sh +210 -0
- package/team/templates/BIBLE_TEMPLATE.md +52 -0
- package/team/templates/CONNECTION_TEMPLATE.json +20 -0
- package/team/templates/MODULE_TEMPLATE.md +60 -0
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Cerber SOLO - Documentation Sync Validator
|
|
5
|
+
*
|
|
6
|
+
* Extends Cerber Core with automation for solo developers
|
|
7
|
+
*
|
|
8
|
+
* Validates:
|
|
9
|
+
* - Extract API endpoints from code
|
|
10
|
+
* - Compare with README
|
|
11
|
+
* - Find ENV vars in code vs docs
|
|
12
|
+
* - Detect stale documentation
|
|
13
|
+
*
|
|
14
|
+
* @author Stefan Pitek
|
|
15
|
+
* @copyright 2026 Stefan Pitek
|
|
16
|
+
* @license MIT
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
const path = require('path');
|
|
21
|
+
const { execSync } = require('child_process');
|
|
22
|
+
|
|
23
|
+
console.log('đ Cerber SOLO - Documentation Sync Validator\n');
|
|
24
|
+
|
|
25
|
+
const issues = [];
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Extract API endpoints from code
|
|
29
|
+
*/
|
|
30
|
+
function extractAPIEndpoints() {
|
|
31
|
+
console.log('đ Extracting API endpoints from code...\n');
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
// Find app.get, app.post, router.get, router.post patterns
|
|
35
|
+
const patterns = [
|
|
36
|
+
'app\\.get\\(',
|
|
37
|
+
'app\\.post\\(',
|
|
38
|
+
'app\\.put\\(',
|
|
39
|
+
'app\\.delete\\(',
|
|
40
|
+
'router\\.get\\(',
|
|
41
|
+
'router\\.post\\(',
|
|
42
|
+
'router\\.put\\(',
|
|
43
|
+
'router\\.delete\\('
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
const endpoints = new Set();
|
|
47
|
+
|
|
48
|
+
patterns.forEach(pattern => {
|
|
49
|
+
try {
|
|
50
|
+
const cmd = `grep -r "${pattern}" --include="*.ts" --include="*.js" . 2>/dev/null || true`;
|
|
51
|
+
const results = execSync(cmd, { encoding: 'utf8', maxBuffer: 10 * 1024 * 1024 });
|
|
52
|
+
|
|
53
|
+
if (results) {
|
|
54
|
+
// Extract endpoint paths
|
|
55
|
+
const pathRegex = /['"]([\/][^'"]+)['"]/g;
|
|
56
|
+
let match;
|
|
57
|
+
while ((match = pathRegex.exec(results)) !== null) {
|
|
58
|
+
endpoints.add(match[1]);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
} catch (error) {
|
|
62
|
+
// Continue on error
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
if (endpoints.size > 0) {
|
|
67
|
+
console.log(` â
Found ${endpoints.size} API endpoints in code:`);
|
|
68
|
+
Array.from(endpoints).slice(0, 10).forEach(endpoint => {
|
|
69
|
+
console.log(` ${endpoint}`);
|
|
70
|
+
});
|
|
71
|
+
if (endpoints.size > 10) {
|
|
72
|
+
console.log(` ... and ${endpoints.size - 10} more`);
|
|
73
|
+
}
|
|
74
|
+
} else {
|
|
75
|
+
console.log(' âšī¸ No API endpoints found in code');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return endpoints;
|
|
79
|
+
} catch (error) {
|
|
80
|
+
console.log(` â ī¸ Error extracting endpoints: ${error.message}`);
|
|
81
|
+
return new Set();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Check if endpoints are documented in README
|
|
87
|
+
*/
|
|
88
|
+
function checkEndpointsInReadme(endpoints) {
|
|
89
|
+
console.log('\nđ Checking README for endpoint documentation...\n');
|
|
90
|
+
|
|
91
|
+
const readmePath = path.join(process.cwd(), 'README.md');
|
|
92
|
+
|
|
93
|
+
if (!fs.existsSync(readmePath)) {
|
|
94
|
+
console.log(' â ī¸ No README.md found');
|
|
95
|
+
issues.push({
|
|
96
|
+
type: 'missing-readme',
|
|
97
|
+
severity: 'high',
|
|
98
|
+
message: 'README.md not found'
|
|
99
|
+
});
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
const readme = fs.readFileSync(readmePath, 'utf8');
|
|
105
|
+
const undocumented = [];
|
|
106
|
+
|
|
107
|
+
endpoints.forEach(endpoint => {
|
|
108
|
+
if (!readme.includes(endpoint)) {
|
|
109
|
+
undocumented.push(endpoint);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
if (undocumented.length > 0) {
|
|
114
|
+
console.log(` â ī¸ ${undocumented.length} endpoints not documented in README:`);
|
|
115
|
+
undocumented.slice(0, 5).forEach(endpoint => {
|
|
116
|
+
console.log(` - ${endpoint}`);
|
|
117
|
+
});
|
|
118
|
+
if (undocumented.length > 5) {
|
|
119
|
+
console.log(` ... and ${undocumented.length - 5} more`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
issues.push({
|
|
123
|
+
type: 'undocumented-endpoints',
|
|
124
|
+
severity: 'moderate',
|
|
125
|
+
message: `${undocumented.length} endpoints missing from README`,
|
|
126
|
+
endpoints: undocumented
|
|
127
|
+
});
|
|
128
|
+
} else if (endpoints.size > 0) {
|
|
129
|
+
console.log(' â
All endpoints are documented in README');
|
|
130
|
+
}
|
|
131
|
+
} catch (error) {
|
|
132
|
+
console.log(` â Error reading README: ${error.message}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Extract environment variables from code
|
|
138
|
+
*/
|
|
139
|
+
function extractEnvVars() {
|
|
140
|
+
console.log('\nđ Extracting environment variables from code...\n');
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
const cmd = `grep -r "process\\.env\\." --include="*.ts" --include="*.js" . 2>/dev/null || true`;
|
|
144
|
+
const results = execSync(cmd, { encoding: 'utf8', maxBuffer: 10 * 1024 * 1024 });
|
|
145
|
+
|
|
146
|
+
const envVars = new Set();
|
|
147
|
+
const envRegex = /process\.env\.([A-Z_][A-Z0-9_]*)/g;
|
|
148
|
+
|
|
149
|
+
let match;
|
|
150
|
+
while ((match = envRegex.exec(results)) !== null) {
|
|
151
|
+
envVars.add(match[1]);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (envVars.size > 0) {
|
|
155
|
+
console.log(` â
Found ${envVars.size} environment variables in code:`);
|
|
156
|
+
Array.from(envVars).slice(0, 10).forEach(varName => {
|
|
157
|
+
console.log(` ${varName}`);
|
|
158
|
+
});
|
|
159
|
+
if (envVars.size > 10) {
|
|
160
|
+
console.log(` ... and ${envVars.size - 10} more`);
|
|
161
|
+
}
|
|
162
|
+
} else {
|
|
163
|
+
console.log(' âšī¸ No environment variables found');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return envVars;
|
|
167
|
+
} catch (error) {
|
|
168
|
+
console.log(` â ī¸ Error extracting env vars: ${error.message}`);
|
|
169
|
+
return new Set();
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Check if env vars are documented
|
|
175
|
+
*/
|
|
176
|
+
function checkEnvVarsInDocs(envVars) {
|
|
177
|
+
console.log('\nđ Checking environment variable documentation...\n');
|
|
178
|
+
|
|
179
|
+
const readmePath = path.join(process.cwd(), 'README.md');
|
|
180
|
+
const envExamplePath = path.join(process.cwd(), '.env.example');
|
|
181
|
+
|
|
182
|
+
let readme = '';
|
|
183
|
+
let envExample = '';
|
|
184
|
+
|
|
185
|
+
if (fs.existsSync(readmePath)) {
|
|
186
|
+
readme = fs.readFileSync(readmePath, 'utf8');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (fs.existsSync(envExamplePath)) {
|
|
190
|
+
envExample = fs.readFileSync(envExamplePath, 'utf8');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const undocumented = [];
|
|
194
|
+
const missingFromExample = [];
|
|
195
|
+
|
|
196
|
+
envVars.forEach(varName => {
|
|
197
|
+
if (!readme.includes(varName)) {
|
|
198
|
+
undocumented.push(varName);
|
|
199
|
+
}
|
|
200
|
+
if (!envExample.includes(varName)) {
|
|
201
|
+
missingFromExample.push(varName);
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
if (undocumented.length > 0) {
|
|
206
|
+
console.log(` â ī¸ ${undocumented.length} env vars not in README:`);
|
|
207
|
+
undocumented.slice(0, 5).forEach(varName => {
|
|
208
|
+
console.log(` - ${varName}`);
|
|
209
|
+
});
|
|
210
|
+
if (undocumented.length > 5) {
|
|
211
|
+
console.log(` ... and ${undocumented.length - 5} more`);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
issues.push({
|
|
215
|
+
type: 'undocumented-env-vars',
|
|
216
|
+
severity: 'low',
|
|
217
|
+
message: `${undocumented.length} env vars not documented`,
|
|
218
|
+
vars: undocumented
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (missingFromExample.length > 0) {
|
|
223
|
+
console.log(`\n â ī¸ ${missingFromExample.length} env vars not in .env.example:`);
|
|
224
|
+
missingFromExample.slice(0, 5).forEach(varName => {
|
|
225
|
+
console.log(` - ${varName}`);
|
|
226
|
+
});
|
|
227
|
+
if (missingFromExample.length > 5) {
|
|
228
|
+
console.log(` ... and ${missingFromExample.length - 5} more`);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
issues.push({
|
|
232
|
+
type: 'missing-env-example',
|
|
233
|
+
severity: 'moderate',
|
|
234
|
+
message: `${missingFromExample.length} env vars not in .env.example`,
|
|
235
|
+
vars: missingFromExample
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (undocumented.length === 0 && missingFromExample.length === 0 && envVars.size > 0) {
|
|
240
|
+
console.log(' â
All env vars are documented');
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Check for TODO and FIXME comments
|
|
246
|
+
*/
|
|
247
|
+
function checkTodoComments() {
|
|
248
|
+
console.log('\nđ Checking for TODO/FIXME comments...\n');
|
|
249
|
+
|
|
250
|
+
try {
|
|
251
|
+
const cmd = `grep -rn "TODO\\|FIXME" --include="*.ts" --include="*.js" --include="*.md" . 2>/dev/null || true`;
|
|
252
|
+
const results = execSync(cmd, { encoding: 'utf8', maxBuffer: 10 * 1024 * 1024 });
|
|
253
|
+
|
|
254
|
+
if (results) {
|
|
255
|
+
const lines = results.split('\n').filter(line => line.trim());
|
|
256
|
+
console.log(` âšī¸ Found ${lines.length} TODO/FIXME comments:`);
|
|
257
|
+
lines.slice(0, 5).forEach(line => {
|
|
258
|
+
console.log(` ${line.substring(0, 80)}${line.length > 80 ? '...' : ''}`);
|
|
259
|
+
});
|
|
260
|
+
if (lines.length > 5) {
|
|
261
|
+
console.log(` ... and ${lines.length - 5} more`);
|
|
262
|
+
}
|
|
263
|
+
} else {
|
|
264
|
+
console.log(' â
No TODO/FIXME comments found');
|
|
265
|
+
}
|
|
266
|
+
} catch (error) {
|
|
267
|
+
// Continue on error
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Run all checks
|
|
272
|
+
const endpoints = extractAPIEndpoints();
|
|
273
|
+
checkEndpointsInReadme(endpoints);
|
|
274
|
+
|
|
275
|
+
const envVars = extractEnvVars();
|
|
276
|
+
checkEnvVarsInDocs(envVars);
|
|
277
|
+
|
|
278
|
+
checkTodoComments();
|
|
279
|
+
|
|
280
|
+
// Summary
|
|
281
|
+
console.log('\n' + '='.repeat(60));
|
|
282
|
+
|
|
283
|
+
if (issues.length > 0) {
|
|
284
|
+
console.log('\nđ Documentation Sync Report\n');
|
|
285
|
+
console.log(`Issues found: ${issues.length}\n`);
|
|
286
|
+
|
|
287
|
+
issues.forEach((issue, idx) => {
|
|
288
|
+
const severityEmoji = issue.severity === 'high' ? 'đ´' :
|
|
289
|
+
issue.severity === 'moderate' ? 'đĄ' : 'đĩ';
|
|
290
|
+
console.log(`${idx + 1}. ${severityEmoji} [${issue.severity.toUpperCase()}] ${issue.message}`);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
console.log('\nđĄ Recommended Actions:');
|
|
294
|
+
console.log(' 1. Update README.md with missing endpoints');
|
|
295
|
+
console.log(' 2. Add missing vars to .env.example');
|
|
296
|
+
console.log(' 3. Document environment variables');
|
|
297
|
+
console.log(' 4. Run: npm run cerber:repair (to auto-sync .env.example)');
|
|
298
|
+
} else {
|
|
299
|
+
console.log('\nâ
Documentation is in sync!\n');
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
console.log('\n' + '='.repeat(60));
|
|
303
|
+
|
|
304
|
+
process.exit(issues.length > 0 ? 1 : 0);
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Cerber SOLO - Feature Flags Checker
|
|
5
|
+
*
|
|
6
|
+
* Extends Cerber Core with automation for solo developers
|
|
7
|
+
*
|
|
8
|
+
* Features:
|
|
9
|
+
* - List active flags
|
|
10
|
+
* - Detect expired flags
|
|
11
|
+
* - Per-environment status
|
|
12
|
+
* - Cleanup suggestions
|
|
13
|
+
*
|
|
14
|
+
* @author Stefan Pitek
|
|
15
|
+
* @copyright 2026 Stefan Pitek
|
|
16
|
+
* @license MIT
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
const path = require('path');
|
|
21
|
+
|
|
22
|
+
console.log('đŠ Cerber SOLO - Feature Flags Checker\n');
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Find feature flag files
|
|
26
|
+
*/
|
|
27
|
+
function findFeatureFlagFiles() {
|
|
28
|
+
const possiblePaths = [
|
|
29
|
+
'solo/lib/feature-flags.ts',
|
|
30
|
+
'src/lib/feature-flags.ts',
|
|
31
|
+
'lib/feature-flags.ts',
|
|
32
|
+
'src/config/feature-flags.ts',
|
|
33
|
+
'config/feature-flags.ts',
|
|
34
|
+
'src/features/flags.ts'
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
const found = [];
|
|
38
|
+
|
|
39
|
+
possiblePaths.forEach(p => {
|
|
40
|
+
const fullPath = path.join(process.cwd(), p);
|
|
41
|
+
if (fs.existsSync(fullPath)) {
|
|
42
|
+
found.push(fullPath);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
return found;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Parse feature flags from file
|
|
51
|
+
*/
|
|
52
|
+
function parseFeatureFlags(filePath) {
|
|
53
|
+
try {
|
|
54
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
55
|
+
const flags = [];
|
|
56
|
+
|
|
57
|
+
// Look for flag definitions (simplified parsing)
|
|
58
|
+
const flagRegex = /['"]([a-z-_]+)['"]\s*:\s*{[\s\S]*?enabled:\s*(true|false)[\s\S]*?}/gi;
|
|
59
|
+
let match;
|
|
60
|
+
|
|
61
|
+
while ((match = flagRegex.exec(content)) !== null) {
|
|
62
|
+
flags.push({
|
|
63
|
+
name: match[1],
|
|
64
|
+
enabled: match[2] === 'true',
|
|
65
|
+
source: filePath
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Also look for simple boolean flags
|
|
70
|
+
const simpleFlagRegex = /export\s+const\s+([A-Z_]+)\s*=\s*(true|false)/gi;
|
|
71
|
+
|
|
72
|
+
while ((match = simpleFlagRegex.exec(content)) !== null) {
|
|
73
|
+
flags.push({
|
|
74
|
+
name: match[1],
|
|
75
|
+
enabled: match[2] === 'true',
|
|
76
|
+
source: filePath
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return flags;
|
|
81
|
+
} catch (error) {
|
|
82
|
+
console.log(`â ī¸ Error parsing ${filePath}: ${error.message}`);
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Check for expired flags
|
|
89
|
+
*/
|
|
90
|
+
function checkExpiredFlags(filePath) {
|
|
91
|
+
try {
|
|
92
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
93
|
+
const expired = [];
|
|
94
|
+
|
|
95
|
+
// Look for expiresAt dates
|
|
96
|
+
const expiryRegex = /expiresAt:\s*['"](\d{4}-\d{2}-\d{2})['"]/g;
|
|
97
|
+
let match;
|
|
98
|
+
|
|
99
|
+
const now = new Date();
|
|
100
|
+
|
|
101
|
+
while ((match = expiryRegex.exec(content)) !== null) {
|
|
102
|
+
const expiryDate = new Date(match[1]);
|
|
103
|
+
|
|
104
|
+
if (expiryDate < now) {
|
|
105
|
+
// Find flag name near this expiry
|
|
106
|
+
const before = content.substring(Math.max(0, match.index - 200), match.index);
|
|
107
|
+
const nameMatch = before.match(/['"]([a-z-_]+)['"]\s*:\s*{[^}]*$/i);
|
|
108
|
+
|
|
109
|
+
if (nameMatch) {
|
|
110
|
+
expired.push({
|
|
111
|
+
name: nameMatch[1],
|
|
112
|
+
expiryDate: match[1]
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return expired;
|
|
119
|
+
} catch (error) {
|
|
120
|
+
return [];
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Find flag usage in codebase
|
|
126
|
+
*/
|
|
127
|
+
function findFlagUsage(flagName) {
|
|
128
|
+
try {
|
|
129
|
+
const { execSync } = require('child_process');
|
|
130
|
+
const cmd = `grep -r "${flagName}" --include="*.ts" --include="*.tsx" --include="*.js" --include="*.jsx" . 2>/dev/null | wc -l`;
|
|
131
|
+
const count = parseInt(execSync(cmd, { encoding: 'utf8' }).trim());
|
|
132
|
+
return count;
|
|
133
|
+
} catch (error) {
|
|
134
|
+
return 0;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Find and analyze feature flags
|
|
139
|
+
const flagFiles = findFeatureFlagFiles();
|
|
140
|
+
|
|
141
|
+
if (flagFiles.length === 0) {
|
|
142
|
+
console.log('âšī¸ No feature flag files found\n');
|
|
143
|
+
console.log('Expected locations:');
|
|
144
|
+
console.log(' - solo/lib/feature-flags.ts');
|
|
145
|
+
console.log(' - src/lib/feature-flags.ts');
|
|
146
|
+
console.log(' - src/config/feature-flags.ts\n');
|
|
147
|
+
console.log('đĄ Create feature-flags.ts to use this checker');
|
|
148
|
+
process.exit(0);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
console.log(`đ Found ${flagFiles.length} feature flag file(s):\n`);
|
|
152
|
+
|
|
153
|
+
let totalFlags = 0;
|
|
154
|
+
let enabledFlags = 0;
|
|
155
|
+
let disabledFlags = 0;
|
|
156
|
+
let allExpiredFlags = [];
|
|
157
|
+
|
|
158
|
+
flagFiles.forEach(file => {
|
|
159
|
+
const relativePath = path.relative(process.cwd(), file);
|
|
160
|
+
console.log(` ${relativePath}`);
|
|
161
|
+
|
|
162
|
+
const flags = parseFeatureFlags(file);
|
|
163
|
+
const expired = checkExpiredFlags(file);
|
|
164
|
+
|
|
165
|
+
totalFlags += flags.length;
|
|
166
|
+
|
|
167
|
+
flags.forEach(flag => {
|
|
168
|
+
if (flag.enabled) {
|
|
169
|
+
enabledFlags++;
|
|
170
|
+
} else {
|
|
171
|
+
disabledFlags++;
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
allExpiredFlags = allExpiredFlags.concat(expired);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
console.log();
|
|
179
|
+
|
|
180
|
+
if (totalFlags === 0) {
|
|
181
|
+
console.log('âšī¸ No feature flags detected in files\n');
|
|
182
|
+
console.log('đĄ Define flags in your feature-flags.ts file:\n');
|
|
183
|
+
console.log(' export const FLAGS = {');
|
|
184
|
+
console.log(' "new-feature": { enabled: true, description: "..." },');
|
|
185
|
+
console.log(' "beta-ui": { enabled: false, description: "..." }');
|
|
186
|
+
console.log(' };\n');
|
|
187
|
+
process.exit(0);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Summary
|
|
191
|
+
console.log('đ Feature Flags Summary\n');
|
|
192
|
+
console.log(` Total flags: ${totalFlags}`);
|
|
193
|
+
console.log(` â
Enabled: ${enabledFlags}`);
|
|
194
|
+
console.log(` â Disabled: ${disabledFlags}\n`);
|
|
195
|
+
|
|
196
|
+
// Expired flags
|
|
197
|
+
if (allExpiredFlags.length > 0) {
|
|
198
|
+
console.log('â° Expired Flags (cleanup recommended)\n');
|
|
199
|
+
allExpiredFlags.forEach(flag => {
|
|
200
|
+
console.log(` đ´ ${flag.name} (expired: ${flag.expiryDate})`);
|
|
201
|
+
});
|
|
202
|
+
console.log();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Environment detection
|
|
206
|
+
console.log('đ Environment Detection\n');
|
|
207
|
+
const env = process.env.NODE_ENV || 'development';
|
|
208
|
+
console.log(` Current environment: ${env}\n`);
|
|
209
|
+
|
|
210
|
+
// Recommendations
|
|
211
|
+
console.log('đĄ Recommendations\n');
|
|
212
|
+
|
|
213
|
+
if (allExpiredFlags.length > 0) {
|
|
214
|
+
console.log(' 1. Remove expired feature flags from code');
|
|
215
|
+
console.log(' 2. Clean up flag-related code branches');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (disabledFlags > enabledFlags * 2) {
|
|
219
|
+
console.log(' 3. Review disabled flags - can any be removed?');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (totalFlags > 20) {
|
|
223
|
+
console.log(' 4. Consider flag cleanup - you have many flags');
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
console.log();
|
|
227
|
+
console.log('='.repeat(60));
|
|
228
|
+
|
|
229
|
+
process.exit(allExpiredFlags.length > 0 ? 1 : 0);
|