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,271 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Cerber SOLO - Performance Budget Enforcer
|
|
5
|
+
*
|
|
6
|
+
* Extends Cerber Core with automation for solo developers
|
|
7
|
+
*
|
|
8
|
+
* Enforces:
|
|
9
|
+
* - Total bundle size limit
|
|
10
|
+
* - Largest chunk limit
|
|
11
|
+
* - Image size constraints
|
|
12
|
+
* - Blocks build if exceeded
|
|
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 - Performance Budget Enforcer\n');
|
|
24
|
+
|
|
25
|
+
// Load configuration
|
|
26
|
+
function loadConfig() {
|
|
27
|
+
const configPath = path.join(__dirname, '../config/performance-budget.json');
|
|
28
|
+
|
|
29
|
+
if (!fs.existsSync(configPath)) {
|
|
30
|
+
console.log('⚠️ No performance-budget.json found, using defaults');
|
|
31
|
+
return {
|
|
32
|
+
bundleSize: { max: 500, warning: 400, unit: 'KB' },
|
|
33
|
+
largestChunk: { max: 250, warning: 200, unit: 'KB' },
|
|
34
|
+
images: { max: 200, unit: 'KB' }
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
return JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
40
|
+
} catch (error) {
|
|
41
|
+
console.log(`❌ Error loading config: ${error.message}`);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const config = loadConfig();
|
|
47
|
+
const violations = [];
|
|
48
|
+
const warnings = [];
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Get file size in KB
|
|
52
|
+
*/
|
|
53
|
+
function getFileSizeKB(filePath) {
|
|
54
|
+
const stats = fs.statSync(filePath);
|
|
55
|
+
return stats.size / 1024;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Find all JS bundle files
|
|
60
|
+
*/
|
|
61
|
+
function findBundleFiles() {
|
|
62
|
+
const distDirs = ['dist', 'build', 'out', '.next'];
|
|
63
|
+
|
|
64
|
+
for (const dir of distDirs) {
|
|
65
|
+
const distPath = path.join(process.cwd(), dir);
|
|
66
|
+
if (fs.existsSync(distPath)) {
|
|
67
|
+
console.log(`📁 Found build directory: ${dir}`);
|
|
68
|
+
return { dir, path: distPath };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Recursively find all files matching pattern
|
|
77
|
+
*/
|
|
78
|
+
function findFiles(dir, pattern, results = []) {
|
|
79
|
+
if (!fs.existsSync(dir)) return results;
|
|
80
|
+
|
|
81
|
+
const files = fs.readdirSync(dir);
|
|
82
|
+
|
|
83
|
+
files.forEach(file => {
|
|
84
|
+
const filePath = path.join(dir, file);
|
|
85
|
+
const stat = fs.statSync(filePath);
|
|
86
|
+
|
|
87
|
+
if (stat.isDirectory()) {
|
|
88
|
+
findFiles(filePath, pattern, results);
|
|
89
|
+
} else if (pattern.test(file)) {
|
|
90
|
+
results.push(filePath);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
return results;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Check bundle sizes
|
|
99
|
+
*/
|
|
100
|
+
function checkBundleSizes() {
|
|
101
|
+
console.log('\n📦 Checking bundle sizes...\n');
|
|
102
|
+
|
|
103
|
+
const buildInfo = findBundleFiles();
|
|
104
|
+
|
|
105
|
+
if (!buildInfo) {
|
|
106
|
+
console.log('⚠️ No build directory found (dist/build/out/.next)');
|
|
107
|
+
console.log(' Run your build command first (npm run build)');
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Find all JS files
|
|
112
|
+
const jsFiles = findFiles(buildInfo.path, /\.js$/);
|
|
113
|
+
|
|
114
|
+
if (jsFiles.length === 0) {
|
|
115
|
+
console.log('ℹ️ No JavaScript bundles found');
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
let totalSize = 0;
|
|
120
|
+
let largestFile = null;
|
|
121
|
+
let largestSize = 0;
|
|
122
|
+
|
|
123
|
+
jsFiles.forEach(file => {
|
|
124
|
+
const sizeKB = getFileSizeKB(file);
|
|
125
|
+
totalSize += sizeKB;
|
|
126
|
+
|
|
127
|
+
if (sizeKB > largestSize) {
|
|
128
|
+
largestSize = sizeKB;
|
|
129
|
+
largestFile = file;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const relativePath = path.relative(process.cwd(), file);
|
|
133
|
+
|
|
134
|
+
// Check individual chunk size
|
|
135
|
+
if (config.largestChunk && sizeKB > config.largestChunk.max) {
|
|
136
|
+
console.log(` 🔴 ${relativePath}: ${sizeKB.toFixed(1)} KB (EXCEEDS ${config.largestChunk.max} KB)`);
|
|
137
|
+
violations.push({
|
|
138
|
+
type: 'chunk-size',
|
|
139
|
+
file: relativePath,
|
|
140
|
+
size: sizeKB,
|
|
141
|
+
limit: config.largestChunk.max
|
|
142
|
+
});
|
|
143
|
+
} else if (config.largestChunk && sizeKB > config.largestChunk.warning) {
|
|
144
|
+
console.log(` 🟡 ${relativePath}: ${sizeKB.toFixed(1)} KB (warning: ${config.largestChunk.warning} KB)`);
|
|
145
|
+
warnings.push({
|
|
146
|
+
type: 'chunk-size',
|
|
147
|
+
file: relativePath,
|
|
148
|
+
size: sizeKB,
|
|
149
|
+
limit: config.largestChunk.warning
|
|
150
|
+
});
|
|
151
|
+
} else {
|
|
152
|
+
console.log(` ✅ ${relativePath}: ${sizeKB.toFixed(1)} KB`);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
console.log(`\n📊 Total bundle size: ${totalSize.toFixed(1)} KB`);
|
|
157
|
+
|
|
158
|
+
if (config.bundleSize) {
|
|
159
|
+
if (totalSize > config.bundleSize.max) {
|
|
160
|
+
console.log(` 🔴 EXCEEDS limit of ${config.bundleSize.max} KB`);
|
|
161
|
+
violations.push({
|
|
162
|
+
type: 'total-size',
|
|
163
|
+
size: totalSize,
|
|
164
|
+
limit: config.bundleSize.max
|
|
165
|
+
});
|
|
166
|
+
} else if (totalSize > config.bundleSize.warning) {
|
|
167
|
+
console.log(` 🟡 Warning: Approaching limit (${config.bundleSize.warning} KB)`);
|
|
168
|
+
warnings.push({
|
|
169
|
+
type: 'total-size',
|
|
170
|
+
size: totalSize,
|
|
171
|
+
limit: config.bundleSize.warning
|
|
172
|
+
});
|
|
173
|
+
} else {
|
|
174
|
+
console.log(` ✅ Within budget (${config.bundleSize.max} KB)`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (largestFile) {
|
|
179
|
+
console.log(`\n📦 Largest chunk: ${path.relative(process.cwd(), largestFile)} (${largestSize.toFixed(1)} KB)`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Check image sizes
|
|
185
|
+
*/
|
|
186
|
+
function checkImageSizes() {
|
|
187
|
+
console.log('\n🖼️ Checking image sizes...\n');
|
|
188
|
+
|
|
189
|
+
const imageDirs = ['public', 'static', 'assets', 'src/assets'];
|
|
190
|
+
let imagesFound = false;
|
|
191
|
+
|
|
192
|
+
imageDirs.forEach(dir => {
|
|
193
|
+
const dirPath = path.join(process.cwd(), dir);
|
|
194
|
+
if (!fs.existsSync(dirPath)) return;
|
|
195
|
+
|
|
196
|
+
const images = findFiles(dirPath, /\.(jpg|jpeg|png|gif|webp|svg)$/i);
|
|
197
|
+
|
|
198
|
+
if (images.length === 0) return;
|
|
199
|
+
|
|
200
|
+
imagesFound = true;
|
|
201
|
+
|
|
202
|
+
images.forEach(file => {
|
|
203
|
+
const sizeKB = getFileSizeKB(file);
|
|
204
|
+
const relativePath = path.relative(process.cwd(), file);
|
|
205
|
+
|
|
206
|
+
if (config.images && sizeKB > config.images.max) {
|
|
207
|
+
console.log(` 🔴 ${relativePath}: ${sizeKB.toFixed(1)} KB (EXCEEDS ${config.images.max} KB)`);
|
|
208
|
+
violations.push({
|
|
209
|
+
type: 'image-size',
|
|
210
|
+
file: relativePath,
|
|
211
|
+
size: sizeKB,
|
|
212
|
+
limit: config.images.max
|
|
213
|
+
});
|
|
214
|
+
} else if (sizeKB > 100) {
|
|
215
|
+
console.log(` 🟡 ${relativePath}: ${sizeKB.toFixed(1)} KB`);
|
|
216
|
+
} else {
|
|
217
|
+
console.log(` ✅ ${relativePath}: ${sizeKB.toFixed(1)} KB`);
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
if (!imagesFound) {
|
|
223
|
+
console.log('ℹ️ No images found in common directories');
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Run checks
|
|
228
|
+
checkBundleSizes();
|
|
229
|
+
checkImageSizes();
|
|
230
|
+
|
|
231
|
+
// Summary
|
|
232
|
+
console.log('\n' + '='.repeat(60));
|
|
233
|
+
|
|
234
|
+
if (violations.length > 0) {
|
|
235
|
+
console.log('\n❌ PERFORMANCE BUDGET VIOLATED\n');
|
|
236
|
+
violations.forEach(v => {
|
|
237
|
+
if (v.type === 'total-size') {
|
|
238
|
+
console.log(` 🔴 Total bundle size: ${v.size.toFixed(1)} KB (limit: ${v.limit} KB)`);
|
|
239
|
+
} else if (v.type === 'chunk-size') {
|
|
240
|
+
console.log(` 🔴 ${v.file}: ${v.size.toFixed(1)} KB (limit: ${v.limit} KB)`);
|
|
241
|
+
} else if (v.type === 'image-size') {
|
|
242
|
+
console.log(` 🔴 ${v.file}: ${v.size.toFixed(1)} KB (limit: ${v.limit} KB)`);
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
console.log('\n💡 Recommendations:');
|
|
247
|
+
console.log(' - Enable code splitting');
|
|
248
|
+
console.log(' - Use dynamic imports');
|
|
249
|
+
console.log(' - Compress images');
|
|
250
|
+
console.log(' - Remove unused dependencies');
|
|
251
|
+
console.log(' - Analyze bundle with webpack-bundle-analyzer');
|
|
252
|
+
|
|
253
|
+
console.log('\n='.repeat(60));
|
|
254
|
+
process.exit(1);
|
|
255
|
+
} else if (warnings.length > 0) {
|
|
256
|
+
console.log('\n⚠️ Performance warnings detected\n');
|
|
257
|
+
warnings.forEach(w => {
|
|
258
|
+
if (w.type === 'total-size') {
|
|
259
|
+
console.log(` 🟡 Total bundle size: ${w.size.toFixed(1)} KB (warning: ${w.limit} KB)`);
|
|
260
|
+
} else if (w.type === 'chunk-size') {
|
|
261
|
+
console.log(` 🟡 ${w.file}: ${w.size.toFixed(1)} KB (warning: ${w.limit} KB)`);
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
console.log('\n='.repeat(60));
|
|
266
|
+
process.exit(0);
|
|
267
|
+
} else {
|
|
268
|
+
console.log('\n✅ All performance budgets met!\n');
|
|
269
|
+
console.log('='.repeat(60));
|
|
270
|
+
process.exit(0);
|
|
271
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Cerber SOLO - Smart Rollback
|
|
5
|
+
*
|
|
6
|
+
* Extends Cerber Core with automation for solo developers
|
|
7
|
+
*
|
|
8
|
+
* Features:
|
|
9
|
+
* - Rollback specific file from commit
|
|
10
|
+
* - Conflict detection
|
|
11
|
+
* - Safety checks
|
|
12
|
+
* - Dry-run mode
|
|
13
|
+
*
|
|
14
|
+
* Usage: node cerber-rollback.js <commit-hash> --file=path/to/file.ts [--dry-run]
|
|
15
|
+
*
|
|
16
|
+
* @author Stefan Pitek
|
|
17
|
+
* @copyright 2026 Stefan Pitek
|
|
18
|
+
* @license MIT
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const { execSync } = require('child_process');
|
|
22
|
+
const fs = require('fs');
|
|
23
|
+
const path = require('path');
|
|
24
|
+
|
|
25
|
+
console.log('⏪ Cerber SOLO - Smart Rollback\n');
|
|
26
|
+
|
|
27
|
+
// Parse arguments
|
|
28
|
+
const args = process.argv.slice(2);
|
|
29
|
+
|
|
30
|
+
if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
|
|
31
|
+
console.log('Usage: node cerber-rollback.js <commit-hash> --file=path/to/file.ts [--dry-run]\n');
|
|
32
|
+
console.log('Options:');
|
|
33
|
+
console.log(' <commit-hash> Git commit hash to rollback from');
|
|
34
|
+
console.log(' --file=<path> File path to rollback');
|
|
35
|
+
console.log(' --dry-run Show what would be done without making changes');
|
|
36
|
+
console.log(' --help, -h Show this help message\n');
|
|
37
|
+
console.log('Examples:');
|
|
38
|
+
console.log(' node cerber-rollback.js abc123 --file=src/api/users.ts');
|
|
39
|
+
console.log(' node cerber-rollback.js abc123 --file=src/api/users.ts --dry-run');
|
|
40
|
+
process.exit(0);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let commitHash = null;
|
|
44
|
+
let filePath = null;
|
|
45
|
+
let isDryRun = false;
|
|
46
|
+
|
|
47
|
+
args.forEach(arg => {
|
|
48
|
+
if (arg === '--dry-run') {
|
|
49
|
+
isDryRun = true;
|
|
50
|
+
} else if (arg.startsWith('--file=')) {
|
|
51
|
+
filePath = arg.split('=')[1];
|
|
52
|
+
} else if (!arg.startsWith('--')) {
|
|
53
|
+
commitHash = arg;
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
if (!commitHash) {
|
|
58
|
+
console.error('❌ Error: Commit hash is required\n');
|
|
59
|
+
console.log('Usage: node cerber-rollback.js <commit-hash> --file=path/to/file.ts\n');
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (!filePath) {
|
|
64
|
+
console.error('❌ Error: File path is required\n');
|
|
65
|
+
console.log('Usage: node cerber-rollback.js <commit-hash> --file=path/to/file.ts\n');
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
console.log(`Commit: ${commitHash}`);
|
|
70
|
+
console.log(`File: ${filePath}`);
|
|
71
|
+
console.log(`Mode: ${isDryRun ? 'DRY RUN' : 'LIVE'}\n`);
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Check if we're in a git repository
|
|
75
|
+
*/
|
|
76
|
+
function checkGitRepo() {
|
|
77
|
+
try {
|
|
78
|
+
execSync('git rev-parse --git-dir', { stdio: 'pipe' });
|
|
79
|
+
console.log('✅ Git repository detected\n');
|
|
80
|
+
return true;
|
|
81
|
+
} catch (error) {
|
|
82
|
+
console.error('❌ Error: Not a git repository\n');
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Verify commit exists
|
|
89
|
+
*/
|
|
90
|
+
function verifyCommit(hash) {
|
|
91
|
+
try {
|
|
92
|
+
execSync(`git cat-file -t ${hash}`, { stdio: 'pipe' });
|
|
93
|
+
const commitMsg = execSync(`git log -1 --pretty=format:"%s" ${hash}`, {
|
|
94
|
+
encoding: 'utf8',
|
|
95
|
+
stdio: 'pipe'
|
|
96
|
+
});
|
|
97
|
+
console.log(`✅ Commit found: "${commitMsg}"\n`);
|
|
98
|
+
return true;
|
|
99
|
+
} catch (error) {
|
|
100
|
+
console.error(`❌ Error: Commit ${hash} not found\n`);
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Check if file exists in commit
|
|
107
|
+
*/
|
|
108
|
+
function checkFileInCommit(hash, file) {
|
|
109
|
+
try {
|
|
110
|
+
execSync(`git cat-file -e ${hash}:${file}`, { stdio: 'pipe' });
|
|
111
|
+
console.log(`✅ File exists in commit ${hash}\n`);
|
|
112
|
+
return true;
|
|
113
|
+
} catch (error) {
|
|
114
|
+
console.error(`❌ Error: File ${file} does not exist in commit ${hash}\n`);
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Check for uncommitted changes
|
|
121
|
+
*/
|
|
122
|
+
function checkUncommittedChanges(file) {
|
|
123
|
+
try {
|
|
124
|
+
const status = execSync(`git status --porcelain ${file}`, {
|
|
125
|
+
encoding: 'utf8',
|
|
126
|
+
stdio: 'pipe'
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
if (status.trim()) {
|
|
130
|
+
console.log(`⚠️ Warning: File ${file} has uncommitted changes\n`);
|
|
131
|
+
console.log(' Current changes will be lost if you proceed!\n');
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
console.log(`✅ No uncommitted changes for ${file}\n`);
|
|
136
|
+
return false;
|
|
137
|
+
} catch (error) {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Show file diff
|
|
144
|
+
*/
|
|
145
|
+
function showDiff(hash, file) {
|
|
146
|
+
try {
|
|
147
|
+
console.log('📊 Changes that will be rolled back:\n');
|
|
148
|
+
console.log('─'.repeat(60));
|
|
149
|
+
|
|
150
|
+
const diff = execSync(`git diff HEAD ${hash} -- ${file}`, {
|
|
151
|
+
encoding: 'utf8',
|
|
152
|
+
maxBuffer: 10 * 1024 * 1024
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
if (diff) {
|
|
156
|
+
// Show first 30 lines
|
|
157
|
+
const lines = diff.split('\n').slice(0, 30);
|
|
158
|
+
console.log(lines.join('\n'));
|
|
159
|
+
|
|
160
|
+
if (diff.split('\n').length > 30) {
|
|
161
|
+
console.log('\n... (diff truncated, showing first 30 lines) ...');
|
|
162
|
+
}
|
|
163
|
+
} else {
|
|
164
|
+
console.log('(no changes - file is identical)');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
console.log('─'.repeat(60));
|
|
168
|
+
console.log();
|
|
169
|
+
} catch (error) {
|
|
170
|
+
console.log('⚠️ Could not generate diff\n');
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Perform rollback
|
|
176
|
+
*/
|
|
177
|
+
function rollbackFile(hash, file) {
|
|
178
|
+
try {
|
|
179
|
+
console.log(`🔄 Rolling back ${file} to ${hash}...\n`);
|
|
180
|
+
|
|
181
|
+
if (!isDryRun) {
|
|
182
|
+
execSync(`git checkout ${hash} -- ${file}`, { stdio: 'inherit' });
|
|
183
|
+
console.log('✅ Rollback successful!\n');
|
|
184
|
+
console.log('💡 Changes are staged. Run `git status` to review.\n');
|
|
185
|
+
console.log(' To commit: git commit -m "rollback: revert changes to ${file}"');
|
|
186
|
+
console.log(' To undo: git checkout HEAD -- ${file}\n');
|
|
187
|
+
} else {
|
|
188
|
+
console.log('🔍 DRY RUN: Would execute: git checkout ${hash} -- ${file}\n');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return true;
|
|
192
|
+
} catch (error) {
|
|
193
|
+
console.error(`❌ Error during rollback: ${error.message}\n`);
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Run checks and rollback
|
|
199
|
+
if (!checkGitRepo()) {
|
|
200
|
+
process.exit(1);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (!verifyCommit(commitHash)) {
|
|
204
|
+
process.exit(1);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (!checkFileInCommit(commitHash, filePath)) {
|
|
208
|
+
process.exit(1);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const hasUncommitted = checkUncommittedChanges(filePath);
|
|
212
|
+
|
|
213
|
+
if (hasUncommitted && !isDryRun) {
|
|
214
|
+
console.log('⚠️ SAFETY CHECK: Uncommitted changes detected\n');
|
|
215
|
+
console.log(' Options:');
|
|
216
|
+
console.log(' 1. Commit your current changes first');
|
|
217
|
+
console.log(' 2. Stash your changes: git stash');
|
|
218
|
+
console.log(' 3. Run with --dry-run to preview\n');
|
|
219
|
+
console.log('❌ Rollback aborted for safety\n');
|
|
220
|
+
process.exit(1);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
showDiff(commitHash, filePath);
|
|
224
|
+
|
|
225
|
+
const success = rollbackFile(commitHash, filePath);
|
|
226
|
+
|
|
227
|
+
console.log('='.repeat(60));
|
|
228
|
+
|
|
229
|
+
process.exit(success ? 0 : 1);
|