s9n-devops-agent 1.6.2 → 1.7.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/docs/AUTOMATED_TESTING_STRATEGY.md +316 -0
- package/docs/branch-management.md +101 -0
- package/package.json +1 -1
- package/src/branch-config-manager.js +544 -0
- package/src/enhanced-close-session.js +492 -0
- package/src/orphan-cleaner.js +538 -0
- package/src/weekly-consolidator.js +403 -0
- package/start-devops-session.sh +1 -1
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Weekly Branch Consolidator
|
|
5
|
+
* Automatically consolidates daily branches into weekly branches to prevent branch proliferation
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const { execSync } = require('child_process');
|
|
11
|
+
|
|
12
|
+
// Configuration
|
|
13
|
+
const CONFIG = {
|
|
14
|
+
colors: {
|
|
15
|
+
reset: '\x1b[0m',
|
|
16
|
+
bright: '\x1b[1m',
|
|
17
|
+
red: '\x1b[31m',
|
|
18
|
+
green: '\x1b[32m',
|
|
19
|
+
yellow: '\x1b[33m',
|
|
20
|
+
blue: '\x1b[34m',
|
|
21
|
+
cyan: '\x1b[36m',
|
|
22
|
+
dim: '\x1b[2m'
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
class WeeklyConsolidator {
|
|
27
|
+
constructor() {
|
|
28
|
+
this.repoRoot = this.getRepoRoot();
|
|
29
|
+
this.projectSettingsPath = path.join(this.repoRoot, 'local_deploy', 'project-settings.json');
|
|
30
|
+
this.projectSettings = this.loadProjectSettings();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
getRepoRoot() {
|
|
34
|
+
try {
|
|
35
|
+
return execSync('git rev-parse --show-toplevel', { encoding: 'utf8' }).trim();
|
|
36
|
+
} catch (error) {
|
|
37
|
+
console.error(`${CONFIG.colors.red}Error: Not in a git repository${CONFIG.colors.reset}`);
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
loadProjectSettings() {
|
|
43
|
+
try {
|
|
44
|
+
if (fs.existsSync(this.projectSettingsPath)) {
|
|
45
|
+
return JSON.parse(fs.readFileSync(this.projectSettingsPath, 'utf8'));
|
|
46
|
+
}
|
|
47
|
+
} catch (error) {
|
|
48
|
+
console.warn(`${CONFIG.colors.yellow}Warning: Could not load project settings${CONFIG.colors.reset}`);
|
|
49
|
+
}
|
|
50
|
+
return {};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Get all branches from remote
|
|
55
|
+
*/
|
|
56
|
+
getAllBranches() {
|
|
57
|
+
try {
|
|
58
|
+
const output = execSync('git branch -r --format="%(refname:short)"', { encoding: 'utf8' });
|
|
59
|
+
return output.split('\n')
|
|
60
|
+
.filter(branch => branch.trim())
|
|
61
|
+
.map(branch => branch.replace('origin/', '').trim())
|
|
62
|
+
.filter(branch => !branch.includes('HEAD'));
|
|
63
|
+
} catch (error) {
|
|
64
|
+
console.error(`${CONFIG.colors.red}Error getting branches: ${error.message}${CONFIG.colors.reset}`);
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Get daily branches from the past week
|
|
71
|
+
*/
|
|
72
|
+
getLastWeekDailyBranches() {
|
|
73
|
+
const allBranches = this.getAllBranches();
|
|
74
|
+
const dailyBranches = allBranches.filter(branch => branch.startsWith('daily/'));
|
|
75
|
+
|
|
76
|
+
// Calculate date range for last week (Monday to Sunday)
|
|
77
|
+
const now = new Date();
|
|
78
|
+
const lastSunday = new Date(now);
|
|
79
|
+
lastSunday.setDate(now.getDate() - now.getDay()); // Last Sunday
|
|
80
|
+
lastSunday.setHours(0, 0, 0, 0);
|
|
81
|
+
|
|
82
|
+
const weekAgo = new Date(lastSunday);
|
|
83
|
+
weekAgo.setDate(lastSunday.getDate() - 7); // Previous Sunday
|
|
84
|
+
|
|
85
|
+
console.log(`${CONFIG.colors.dim}Looking for daily branches between ${weekAgo.toISOString().split('T')[0]} and ${lastSunday.toISOString().split('T')[0]}${CONFIG.colors.reset}`);
|
|
86
|
+
|
|
87
|
+
const lastWeekBranches = dailyBranches.filter(branch => {
|
|
88
|
+
const dateStr = branch.replace('daily/', '');
|
|
89
|
+
const branchDate = new Date(dateStr + 'T00:00:00Z');
|
|
90
|
+
return branchDate >= weekAgo && branchDate < lastSunday;
|
|
91
|
+
}).sort();
|
|
92
|
+
|
|
93
|
+
return lastWeekBranches;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Generate weekly branch name from daily branches
|
|
98
|
+
*/
|
|
99
|
+
generateWeeklyBranchName(dailyBranches) {
|
|
100
|
+
if (dailyBranches.length === 0) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const firstDate = dailyBranches[0].replace('daily/', '');
|
|
105
|
+
const lastDate = dailyBranches[dailyBranches.length - 1].replace('daily/', '');
|
|
106
|
+
|
|
107
|
+
return `weekly/${firstDate}_to_${lastDate}`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Check if a branch exists locally or remotely
|
|
112
|
+
*/
|
|
113
|
+
async branchExists(branchName) {
|
|
114
|
+
try {
|
|
115
|
+
// Check local first
|
|
116
|
+
execSync(`git show-ref --verify --quiet refs/heads/${branchName}`, { stdio: 'ignore' });
|
|
117
|
+
return true;
|
|
118
|
+
} catch {
|
|
119
|
+
try {
|
|
120
|
+
// Check remote
|
|
121
|
+
execSync(`git show-ref --verify --quiet refs/remotes/origin/${branchName}`, { stdio: 'ignore' });
|
|
122
|
+
return true;
|
|
123
|
+
} catch {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Create a new branch from a base branch
|
|
131
|
+
*/
|
|
132
|
+
async createBranch(branchName, baseBranch) {
|
|
133
|
+
try {
|
|
134
|
+
console.log(`${CONFIG.colors.blue}Creating branch: ${branchName} from ${baseBranch}${CONFIG.colors.reset}`);
|
|
135
|
+
|
|
136
|
+
// Fetch latest changes
|
|
137
|
+
execSync('git fetch --all --prune', { stdio: 'ignore' });
|
|
138
|
+
|
|
139
|
+
// Create and checkout new branch
|
|
140
|
+
execSync(`git checkout -b ${branchName} origin/${baseBranch}`, { stdio: 'ignore' });
|
|
141
|
+
|
|
142
|
+
// Push to remote
|
|
143
|
+
execSync(`git push -u origin ${branchName}`, { stdio: 'ignore' });
|
|
144
|
+
|
|
145
|
+
return true;
|
|
146
|
+
} catch (error) {
|
|
147
|
+
console.error(`${CONFIG.colors.red}Failed to create branch ${branchName}: ${error.message}${CONFIG.colors.reset}`);
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Merge one branch into another
|
|
154
|
+
*/
|
|
155
|
+
async mergeBranch(sourceBranch, targetBranch) {
|
|
156
|
+
try {
|
|
157
|
+
console.log(`${CONFIG.colors.blue}Merging ${sourceBranch} → ${targetBranch}${CONFIG.colors.reset}`);
|
|
158
|
+
|
|
159
|
+
// Checkout target branch
|
|
160
|
+
execSync(`git checkout ${targetBranch}`, { stdio: 'ignore' });
|
|
161
|
+
|
|
162
|
+
// Pull latest changes
|
|
163
|
+
execSync(`git pull origin ${targetBranch}`, { stdio: 'ignore' });
|
|
164
|
+
|
|
165
|
+
// Merge source branch
|
|
166
|
+
const mergeMessage = `Merge daily branch ${sourceBranch} into weekly consolidation`;
|
|
167
|
+
execSync(`git merge --no-ff origin/${sourceBranch} -m "${mergeMessage}"`, { stdio: 'ignore' });
|
|
168
|
+
|
|
169
|
+
// Push the merge
|
|
170
|
+
execSync(`git push origin ${targetBranch}`, { stdio: 'ignore' });
|
|
171
|
+
|
|
172
|
+
console.log(`${CONFIG.colors.green}✓ Successfully merged ${sourceBranch} → ${targetBranch}${CONFIG.colors.reset}`);
|
|
173
|
+
return true;
|
|
174
|
+
} catch (error) {
|
|
175
|
+
console.error(`${CONFIG.colors.red}✗ Failed to merge ${sourceBranch} → ${targetBranch}${CONFIG.colors.reset}`);
|
|
176
|
+
console.error(`${CONFIG.colors.dim}Error: ${error.message}${CONFIG.colors.reset}`);
|
|
177
|
+
|
|
178
|
+
// Reset any partial merge state
|
|
179
|
+
try {
|
|
180
|
+
execSync('git merge --abort', { stdio: 'ignore' });
|
|
181
|
+
} catch {}
|
|
182
|
+
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Delete branches after successful consolidation
|
|
189
|
+
*/
|
|
190
|
+
async cleanupDailyBranches(dailyBranches) {
|
|
191
|
+
console.log(`\n${CONFIG.colors.bright}Cleaning up consolidated daily branches...${CONFIG.colors.reset}`);
|
|
192
|
+
|
|
193
|
+
for (const branch of dailyBranches) {
|
|
194
|
+
try {
|
|
195
|
+
console.log(`${CONFIG.colors.blue}Deleting branch: ${branch}${CONFIG.colors.reset}`);
|
|
196
|
+
|
|
197
|
+
// Delete local branch if it exists
|
|
198
|
+
try {
|
|
199
|
+
execSync(`git branch -D ${branch}`, { stdio: 'ignore' });
|
|
200
|
+
} catch {}
|
|
201
|
+
|
|
202
|
+
// Delete remote branch
|
|
203
|
+
execSync(`git push origin --delete ${branch}`, { stdio: 'ignore' });
|
|
204
|
+
|
|
205
|
+
console.log(`${CONFIG.colors.green}✓ Deleted ${branch}${CONFIG.colors.reset}`);
|
|
206
|
+
} catch (error) {
|
|
207
|
+
console.error(`${CONFIG.colors.red}✗ Failed to delete ${branch}: ${error.message}${CONFIG.colors.reset}`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Perform dual merge to target branch if enabled
|
|
214
|
+
*/
|
|
215
|
+
async performDualMergeToTarget(weeklyBranch) {
|
|
216
|
+
const enableDualMerge = this.projectSettings?.branchManagement?.enableDualMerge;
|
|
217
|
+
const targetBranch = this.projectSettings?.branchManagement?.defaultMergeTarget;
|
|
218
|
+
|
|
219
|
+
if (!enableDualMerge || !targetBranch) {
|
|
220
|
+
console.log(`${CONFIG.colors.dim}Dual merge not enabled or target branch not configured${CONFIG.colors.reset}`);
|
|
221
|
+
return true;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
console.log(`\n${CONFIG.colors.bright}Performing dual merge to target branch...${CONFIG.colors.reset}`);
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
const success = await this.mergeBranch(weeklyBranch, targetBranch);
|
|
228
|
+
if (success) {
|
|
229
|
+
console.log(`${CONFIG.colors.green}✓ Weekly branch merged to target: ${weeklyBranch} → ${targetBranch}${CONFIG.colors.reset}`);
|
|
230
|
+
}
|
|
231
|
+
return success;
|
|
232
|
+
} catch (error) {
|
|
233
|
+
console.error(`${CONFIG.colors.red}✗ Failed to merge weekly branch to target${CONFIG.colors.reset}`);
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Main consolidation process
|
|
240
|
+
*/
|
|
241
|
+
async consolidateWeeklyBranches() {
|
|
242
|
+
console.log(`\n${CONFIG.colors.bright}${CONFIG.colors.blue}Weekly Branch Consolidation${CONFIG.colors.reset}`);
|
|
243
|
+
console.log(`${CONFIG.colors.dim}Repository: ${this.repoRoot}${CONFIG.colors.reset}\n`);
|
|
244
|
+
|
|
245
|
+
// Check if weekly consolidation is enabled
|
|
246
|
+
const enableWeeklyConsolidation = this.projectSettings?.branchManagement?.enableWeeklyConsolidation;
|
|
247
|
+
if (enableWeeklyConsolidation === false) {
|
|
248
|
+
console.log(`${CONFIG.colors.yellow}Weekly consolidation is disabled in project settings${CONFIG.colors.reset}`);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Get daily branches from last week
|
|
253
|
+
const lastWeekDailies = this.getLastWeekDailyBranches();
|
|
254
|
+
|
|
255
|
+
if (lastWeekDailies.length === 0) {
|
|
256
|
+
console.log(`${CONFIG.colors.yellow}No daily branches found for consolidation${CONFIG.colors.reset}`);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
console.log(`${CONFIG.colors.bright}Found ${lastWeekDailies.length} daily branches to consolidate:${CONFIG.colors.reset}`);
|
|
261
|
+
lastWeekDailies.forEach(branch => {
|
|
262
|
+
console.log(` • ${branch}`);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// Generate weekly branch name
|
|
266
|
+
const weeklyBranchName = this.generateWeeklyBranchName(lastWeekDailies);
|
|
267
|
+
console.log(`\n${CONFIG.colors.bright}Creating weekly branch: ${weeklyBranchName}${CONFIG.colors.reset}`);
|
|
268
|
+
|
|
269
|
+
// Check if weekly branch already exists
|
|
270
|
+
if (await this.branchExists(weeklyBranchName)) {
|
|
271
|
+
console.log(`${CONFIG.colors.yellow}Weekly branch ${weeklyBranchName} already exists, skipping consolidation${CONFIG.colors.reset}`);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
try {
|
|
276
|
+
// Create weekly branch from first daily branch
|
|
277
|
+
const success = await this.createBranch(weeklyBranchName, lastWeekDailies[0]);
|
|
278
|
+
if (!success) {
|
|
279
|
+
throw new Error('Failed to create weekly branch');
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Merge remaining daily branches into weekly branch
|
|
283
|
+
let allMergesSuccessful = true;
|
|
284
|
+
for (let i = 1; i < lastWeekDailies.length; i++) {
|
|
285
|
+
const mergeSuccess = await this.mergeBranch(lastWeekDailies[i], weeklyBranchName);
|
|
286
|
+
if (!mergeSuccess) {
|
|
287
|
+
allMergesSuccessful = false;
|
|
288
|
+
console.error(`${CONFIG.colors.red}Stopping consolidation due to merge failure${CONFIG.colors.reset}`);
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (!allMergesSuccessful) {
|
|
294
|
+
console.error(`${CONFIG.colors.red}❌ Weekly consolidation failed due to merge conflicts${CONFIG.colors.reset}`);
|
|
295
|
+
console.log(`${CONFIG.colors.dim}Weekly branch ${weeklyBranchName} created but not all dailies were merged${CONFIG.colors.reset}`);
|
|
296
|
+
console.log(`${CONFIG.colors.dim}Manual intervention required to resolve conflicts${CONFIG.colors.reset}`);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Perform dual merge to target branch if enabled
|
|
301
|
+
await this.performDualMergeToTarget(weeklyBranchName);
|
|
302
|
+
|
|
303
|
+
// Clean up daily branches after successful consolidation
|
|
304
|
+
await this.cleanupDailyBranches(lastWeekDailies);
|
|
305
|
+
|
|
306
|
+
console.log(`\n${CONFIG.colors.green}✅ Weekly consolidation completed successfully${CONFIG.colors.reset}`);
|
|
307
|
+
console.log(`${CONFIG.colors.bright}Weekly branch: ${weeklyBranchName}${CONFIG.colors.reset}`);
|
|
308
|
+
console.log(`${CONFIG.colors.dim}Consolidated ${lastWeekDailies.length} daily branches${CONFIG.colors.reset}`);
|
|
309
|
+
|
|
310
|
+
} catch (error) {
|
|
311
|
+
console.error(`\n${CONFIG.colors.red}❌ Weekly consolidation failed: ${error.message}${CONFIG.colors.reset}`);
|
|
312
|
+
throw error;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* List existing weekly branches
|
|
318
|
+
*/
|
|
319
|
+
listWeeklyBranches() {
|
|
320
|
+
const allBranches = this.getAllBranches();
|
|
321
|
+
const weeklyBranches = allBranches.filter(branch => branch.startsWith('weekly/')).sort();
|
|
322
|
+
|
|
323
|
+
if (weeklyBranches.length === 0) {
|
|
324
|
+
console.log(`${CONFIG.colors.yellow}No weekly branches found${CONFIG.colors.reset}`);
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
console.log(`\n${CONFIG.colors.bright}Existing Weekly Branches:${CONFIG.colors.reset}`);
|
|
329
|
+
weeklyBranches.forEach(branch => {
|
|
330
|
+
console.log(` • ${branch}`);
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Clean up old weekly branches (keep only recent ones)
|
|
336
|
+
*/
|
|
337
|
+
async cleanupOldWeeklyBranches() {
|
|
338
|
+
const retainWeeks = this.projectSettings?.cleanup?.retainWeeklyBranches || 12;
|
|
339
|
+
const allBranches = this.getAllBranches();
|
|
340
|
+
const weeklyBranches = allBranches.filter(branch => branch.startsWith('weekly/')).sort();
|
|
341
|
+
|
|
342
|
+
if (weeklyBranches.length <= retainWeeks) {
|
|
343
|
+
console.log(`${CONFIG.colors.green}No old weekly branches to clean up (${weeklyBranches.length} <= ${retainWeeks})${CONFIG.colors.reset}`);
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const branchesToDelete = weeklyBranches.slice(0, weeklyBranches.length - retainWeeks);
|
|
348
|
+
|
|
349
|
+
console.log(`\n${CONFIG.colors.bright}Cleaning up old weekly branches (keeping ${retainWeeks} most recent):${CONFIG.colors.reset}`);
|
|
350
|
+
|
|
351
|
+
for (const branch of branchesToDelete) {
|
|
352
|
+
try {
|
|
353
|
+
console.log(`${CONFIG.colors.blue}Deleting old weekly branch: ${branch}${CONFIG.colors.reset}`);
|
|
354
|
+
|
|
355
|
+
// Delete local branch if it exists
|
|
356
|
+
try {
|
|
357
|
+
execSync(`git branch -D ${branch}`, { stdio: 'ignore' });
|
|
358
|
+
} catch {}
|
|
359
|
+
|
|
360
|
+
// Delete remote branch
|
|
361
|
+
execSync(`git push origin --delete ${branch}`, { stdio: 'ignore' });
|
|
362
|
+
|
|
363
|
+
console.log(`${CONFIG.colors.green}✓ Deleted ${branch}${CONFIG.colors.reset}`);
|
|
364
|
+
} catch (error) {
|
|
365
|
+
console.error(`${CONFIG.colors.red}✗ Failed to delete ${branch}: ${error.message}${CONFIG.colors.reset}`);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Main execution function
|
|
372
|
+
*/
|
|
373
|
+
async run(command = 'consolidate') {
|
|
374
|
+
try {
|
|
375
|
+
switch (command) {
|
|
376
|
+
case 'consolidate':
|
|
377
|
+
await this.consolidateWeeklyBranches();
|
|
378
|
+
break;
|
|
379
|
+
case 'list':
|
|
380
|
+
this.listWeeklyBranches();
|
|
381
|
+
break;
|
|
382
|
+
case 'cleanup':
|
|
383
|
+
await this.cleanupOldWeeklyBranches();
|
|
384
|
+
break;
|
|
385
|
+
default:
|
|
386
|
+
console.log(`${CONFIG.colors.red}Unknown command: ${command}${CONFIG.colors.reset}`);
|
|
387
|
+
console.log('Available commands: consolidate, list, cleanup');
|
|
388
|
+
}
|
|
389
|
+
} catch (error) {
|
|
390
|
+
console.error(`${CONFIG.colors.red}❌ Operation failed: ${error.message}${CONFIG.colors.reset}`);
|
|
391
|
+
process.exit(1);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// CLI execution
|
|
397
|
+
if (require.main === module) {
|
|
398
|
+
const command = process.argv[2] || 'consolidate';
|
|
399
|
+
const consolidator = new WeeklyConsolidator();
|
|
400
|
+
consolidator.run(command);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
module.exports = WeeklyConsolidator;
|
package/start-devops-session.sh
CHANGED
|
@@ -41,7 +41,7 @@ show_copyright() {
|
|
|
41
41
|
echo "======================================================================"
|
|
42
42
|
echo
|
|
43
43
|
echo " CS_DevOpsAgent - Intelligent Git Automation System"
|
|
44
|
-
echo " Version 1.
|
|
44
|
+
echo " Version 1.7.0 | Build 20251010.01"
|
|
45
45
|
echo " "
|
|
46
46
|
echo " Copyright (c) 2024 SecondBrain Labs"
|
|
47
47
|
echo " Author: Sachin Dev Duggal"
|