s9n-devops-agent 1.2.1 → 1.3.3
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/README.md +210 -56
- package/docs/INSTALLATION_GUIDE.md +366 -0
- package/docs/houserules.md +51 -0
- package/package.json +5 -1
- package/src/display-utils.cjs +350 -0
- package/src/file-coordinator.cjs +356 -0
- package/src/file-monitor-enhanced.cjs +338 -0
- package/src/house-rules-manager.js +610 -0
- package/src/session-coordinator.js +122 -17
- package/src/setup-cs-devops-agent.js +5 -3
- package/start-devops-session.sh +188 -18
|
@@ -0,0 +1,610 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* House Rules Version Manager
|
|
3
|
+
* ============================
|
|
4
|
+
*
|
|
5
|
+
* Manages versioning and updates of DevOps Agent sections in house rules.
|
|
6
|
+
* Allows intelligent updates while preserving user customizations.
|
|
7
|
+
*
|
|
8
|
+
* Each managed section includes:
|
|
9
|
+
* - Version marker: <!-- DEVOPS_AGENT_SECTION:name:version:checksum -->
|
|
10
|
+
* - Content checksum for change detection
|
|
11
|
+
* - End marker: <!-- END_DEVOPS_AGENT_SECTION:name -->
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import fs from 'fs';
|
|
15
|
+
import path from 'path';
|
|
16
|
+
import crypto from 'crypto';
|
|
17
|
+
import { fileURLToPath } from 'url';
|
|
18
|
+
import { dirname } from 'path';
|
|
19
|
+
import { execSync } from 'child_process';
|
|
20
|
+
|
|
21
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
22
|
+
const __dirname = dirname(__filename);
|
|
23
|
+
|
|
24
|
+
// Version of our house rules sections
|
|
25
|
+
const HOUSERULES_VERSION = '1.3.0';
|
|
26
|
+
|
|
27
|
+
// Managed sections with their content
|
|
28
|
+
const MANAGED_SECTIONS = {
|
|
29
|
+
'file-coordination': {
|
|
30
|
+
version: '1.3.0',
|
|
31
|
+
title: '## 🚨 CRITICAL: File Coordination Protocol (MUST FOLLOW)',
|
|
32
|
+
content: `**IMPORTANT: Always check the house rules at the beginning of each session!**
|
|
33
|
+
|
|
34
|
+
To prevent conflicts with other agents editing the same files, you MUST follow this protocol:
|
|
35
|
+
|
|
36
|
+
### Before Editing ANY Files:
|
|
37
|
+
|
|
38
|
+
1. **DECLARE YOUR INTENT FIRST**
|
|
39
|
+
Create a file at \`.file-coordination/active-edits/<your-name>-<session>.json\` with:
|
|
40
|
+
\`\`\`json
|
|
41
|
+
{
|
|
42
|
+
"agent": "<your-name>",
|
|
43
|
+
"session": "<session-id>",
|
|
44
|
+
"files": ["list", "of", "files", "you", "will", "edit"],
|
|
45
|
+
"operation": "edit",
|
|
46
|
+
"reason": "Brief description of what you're doing",
|
|
47
|
+
"declaredAt": "<current-ISO-8601-timestamp>",
|
|
48
|
+
"estimatedDuration": 300
|
|
49
|
+
}
|
|
50
|
+
\`\`\`
|
|
51
|
+
|
|
52
|
+
2. **CHECK FOR CONFLICTS**
|
|
53
|
+
- Read ALL files in \`.file-coordination/active-edits/\`
|
|
54
|
+
- If ANY other agent has declared the same files, you must:
|
|
55
|
+
- WAIT for them to finish, OR
|
|
56
|
+
- Choose different files to edit
|
|
57
|
+
|
|
58
|
+
3. **ONLY EDIT DECLARED FILES**
|
|
59
|
+
- Never edit files you haven't declared
|
|
60
|
+
- Stay within your declared scope
|
|
61
|
+
|
|
62
|
+
4. **RELEASE WHEN DONE**
|
|
63
|
+
- Delete your declaration file after completing edits
|
|
64
|
+
- Or move it to \`.file-coordination/completed-edits/\`
|
|
65
|
+
|
|
66
|
+
### If You Detect a Conflict:
|
|
67
|
+
- DO NOT proceed with edits
|
|
68
|
+
- Report the conflict to the user
|
|
69
|
+
- Wait or choose alternative files
|
|
70
|
+
|
|
71
|
+
### Helper Scripts Available:
|
|
72
|
+
- \`./scripts/coordination/check-file-availability.sh <files>\` - Check if files are available
|
|
73
|
+
- \`./scripts/coordination/declare-file-edits.sh <agent> <session> <files>\` - Declare your intent
|
|
74
|
+
- \`./scripts/coordination/release-file-edits.sh <agent> <session>\` - Release files after editing
|
|
75
|
+
|
|
76
|
+
**This coordination prevents wasted work and merge conflicts!**`
|
|
77
|
+
},
|
|
78
|
+
'core-principles': {
|
|
79
|
+
version: '1.0.0',
|
|
80
|
+
title: '## Core Principles',
|
|
81
|
+
content: `1. **Always preserve existing functionality** - Never break working code
|
|
82
|
+
2. **Follow existing patterns** - Match the codebase style and conventions
|
|
83
|
+
3. **Communicate clearly** - Document your changes and reasoning
|
|
84
|
+
4. **Coordinate with others** - Follow the file coordination protocol below`
|
|
85
|
+
},
|
|
86
|
+
'project-conventions': {
|
|
87
|
+
version: '1.0.0',
|
|
88
|
+
title: '## Project Conventions',
|
|
89
|
+
content: `### Code Style
|
|
90
|
+
- Follow existing indentation and formatting patterns
|
|
91
|
+
- Maintain consistent naming conventions used in the project
|
|
92
|
+
- Keep functions small and focused
|
|
93
|
+
- Write clear, descriptive variable and function names
|
|
94
|
+
|
|
95
|
+
### Git Workflow
|
|
96
|
+
- Write clear, descriptive commit messages
|
|
97
|
+
- Follow conventional commit format when applicable (feat:, fix:, docs:, etc.)
|
|
98
|
+
- Keep commits atomic and focused on a single change
|
|
99
|
+
- Never commit sensitive information or credentials
|
|
100
|
+
|
|
101
|
+
### Testing
|
|
102
|
+
- Write tests for new functionality
|
|
103
|
+
- Ensure existing tests pass before committing
|
|
104
|
+
- Update tests when changing functionality
|
|
105
|
+
|
|
106
|
+
### Documentation
|
|
107
|
+
- Update README files when adding new features
|
|
108
|
+
- Document complex logic with clear comments
|
|
109
|
+
- Keep API documentation up to date
|
|
110
|
+
- Update CHANGELOG for significant changes`
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
class HouseRulesManager {
|
|
115
|
+
constructor(projectRoot) {
|
|
116
|
+
const cwd = projectRoot || process.cwd();
|
|
117
|
+
|
|
118
|
+
// If we're running from within a DevOpsAgent directory, we need to find the parent project
|
|
119
|
+
if (cwd.includes('/DevOpsAgent') || cwd.includes('/CS_DevOpsAgent')) {
|
|
120
|
+
// Parse the path to find where the DevOpsAgent directory is
|
|
121
|
+
const pathParts = cwd.split(path.sep);
|
|
122
|
+
|
|
123
|
+
// Find the index of DevOpsAgent or CS_DevOpsAgent
|
|
124
|
+
let targetIndex = -1;
|
|
125
|
+
for (let i = pathParts.length - 1; i >= 0; i--) {
|
|
126
|
+
if (pathParts[i] === 'CS_DevOpsAgent' || pathParts[i] === 'DevOpsAgent') {
|
|
127
|
+
targetIndex = i;
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// If found, use the parent directory as the project root
|
|
133
|
+
if (targetIndex > 0) {
|
|
134
|
+
// Check if the parent directory is 'Scripts_Dev' or similar subdirectory
|
|
135
|
+
// If so, go up one more level to reach the actual project root
|
|
136
|
+
let parentIndex = targetIndex;
|
|
137
|
+
if (targetIndex > 1 && (pathParts[targetIndex - 1] === 'Scripts_Dev' ||
|
|
138
|
+
pathParts[targetIndex - 1] === 'scripts' ||
|
|
139
|
+
pathParts[targetIndex - 1] === 'tools')) {
|
|
140
|
+
parentIndex = targetIndex - 1;
|
|
141
|
+
}
|
|
142
|
+
this.projectRoot = pathParts.slice(0, parentIndex).join(path.sep);
|
|
143
|
+
} else {
|
|
144
|
+
// Fallback: try to use git to find the parent repo
|
|
145
|
+
try {
|
|
146
|
+
const gitRoot = execSync('git rev-parse --show-toplevel', { cwd, encoding: 'utf8' }).trim();
|
|
147
|
+
// Only use git root if it's not a DevOpsAgent directory
|
|
148
|
+
if (!gitRoot.includes('/DevOpsAgent') && !gitRoot.includes('/CS_DevOpsAgent')) {
|
|
149
|
+
this.projectRoot = gitRoot;
|
|
150
|
+
} else {
|
|
151
|
+
// Use parent of cwd
|
|
152
|
+
this.projectRoot = path.dirname(path.dirname(cwd));
|
|
153
|
+
}
|
|
154
|
+
} catch (err) {
|
|
155
|
+
// Default to parent of parent directory
|
|
156
|
+
this.projectRoot = path.dirname(path.dirname(cwd));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
} else {
|
|
160
|
+
this.projectRoot = cwd;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
this.houseRulesPath = null;
|
|
164
|
+
this.findHouseRulesFile();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Find existing house rules file
|
|
169
|
+
* Searches across the repository, excluding the DevOps agent directories
|
|
170
|
+
*/
|
|
171
|
+
findHouseRulesFile() {
|
|
172
|
+
// First try the standard locations
|
|
173
|
+
const possiblePaths = [
|
|
174
|
+
'houserules.md',
|
|
175
|
+
'HOUSERULES.md',
|
|
176
|
+
'.github/HOUSERULES.md',
|
|
177
|
+
'docs/houserules.md',
|
|
178
|
+
'docs/HOUSERULES.md'
|
|
179
|
+
];
|
|
180
|
+
|
|
181
|
+
for (const relativePath of possiblePaths) {
|
|
182
|
+
const fullPath = path.join(this.projectRoot, relativePath);
|
|
183
|
+
if (fs.existsSync(fullPath)) {
|
|
184
|
+
// NEVER use house rules from within DevOpsAgent directories
|
|
185
|
+
if (fullPath.includes('/DevOpsAgent/') || fullPath.includes('/CS_DevOpsAgent/')) {
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
this.houseRulesPath = fullPath;
|
|
190
|
+
// Only log if not running as CLI
|
|
191
|
+
if (!process.argv[1]?.endsWith('house-rules-manager.js')) {
|
|
192
|
+
console.log(`Found house rules at: ${fullPath}`);
|
|
193
|
+
}
|
|
194
|
+
return fullPath;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// If not found in standard locations, search the repository
|
|
199
|
+
// excluding DevOps agent directories
|
|
200
|
+
const foundPath = this.searchForHouseRules(this.projectRoot);
|
|
201
|
+
if (foundPath) {
|
|
202
|
+
this.houseRulesPath = foundPath;
|
|
203
|
+
// Only log if not running as CLI
|
|
204
|
+
if (!process.argv[1]?.endsWith('house-rules-manager.js')) {
|
|
205
|
+
console.log(`Found house rules at: ${foundPath}`);
|
|
206
|
+
}
|
|
207
|
+
return foundPath;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Recursively search for house rules files
|
|
215
|
+
* @param {string} dir Directory to search
|
|
216
|
+
* @param {number} depth Current depth (to prevent infinite recursion)
|
|
217
|
+
* @returns {string|null} Path to house rules file or null
|
|
218
|
+
*/
|
|
219
|
+
searchForHouseRules(dir, depth = 0) {
|
|
220
|
+
// Limit search depth to prevent excessive recursion
|
|
221
|
+
if (depth > 5) return null;
|
|
222
|
+
|
|
223
|
+
try {
|
|
224
|
+
const files = fs.readdirSync(dir);
|
|
225
|
+
|
|
226
|
+
for (const file of files) {
|
|
227
|
+
const filePath = path.join(dir, file);
|
|
228
|
+
const stat = fs.statSync(filePath);
|
|
229
|
+
|
|
230
|
+
// Check if it's a house rules file
|
|
231
|
+
if (stat.isFile() && /^houserules\.md$/i.test(file)) {
|
|
232
|
+
// Skip if it's in a DevOps agent directory
|
|
233
|
+
if (!filePath.includes('DevOpsAgent') &&
|
|
234
|
+
!filePath.includes('CS_DevOpsAgent') &&
|
|
235
|
+
!filePath.includes('node_modules') &&
|
|
236
|
+
!filePath.includes('.git')) {
|
|
237
|
+
return filePath;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Recursively search subdirectories
|
|
242
|
+
if (stat.isDirectory() &&
|
|
243
|
+
!file.startsWith('.') &&
|
|
244
|
+
file !== 'node_modules' &&
|
|
245
|
+
file !== 'DevOpsAgent' &&
|
|
246
|
+
file !== 'CS_DevOpsAgent' &&
|
|
247
|
+
file !== '.git') {
|
|
248
|
+
const found = this.searchForHouseRules(filePath, depth + 1);
|
|
249
|
+
if (found) return found;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
} catch (err) {
|
|
253
|
+
// Ignore permission errors and continue searching
|
|
254
|
+
if (err.code !== 'EACCES' && err.code !== 'EPERM') {
|
|
255
|
+
console.error(`Error searching ${dir}:`, err.message);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Calculate checksum for content
|
|
264
|
+
*/
|
|
265
|
+
calculateChecksum(content) {
|
|
266
|
+
return crypto.createHash('md5').update(content.trim()).digest('hex').substring(0, 8);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Create section marker
|
|
271
|
+
*/
|
|
272
|
+
createSectionMarker(sectionName, version, checksum) {
|
|
273
|
+
return `<!-- DEVOPS_AGENT_SECTION:${sectionName}:${version}:${checksum} -->`;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Create end marker
|
|
278
|
+
*/
|
|
279
|
+
createEndMarker(sectionName) {
|
|
280
|
+
return `<!-- END_DEVOPS_AGENT_SECTION:${sectionName} -->`;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Extract managed sections from existing house rules
|
|
285
|
+
*/
|
|
286
|
+
extractManagedSections(content) {
|
|
287
|
+
const sections = {};
|
|
288
|
+
const pattern = /<!-- DEVOPS_AGENT_SECTION:(\w+[-\w]*):([0-9.]+):([a-f0-9]+) -->([\s\S]*?)<!-- END_DEVOPS_AGENT_SECTION:\1 -->/g;
|
|
289
|
+
|
|
290
|
+
let match;
|
|
291
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
292
|
+
sections[match[1]] = {
|
|
293
|
+
name: match[1],
|
|
294
|
+
version: match[2],
|
|
295
|
+
checksum: match[3],
|
|
296
|
+
content: match[4].trim(),
|
|
297
|
+
startIndex: match.index,
|
|
298
|
+
endIndex: match.index + match[0].length
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return sections;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Check if a section needs updating
|
|
307
|
+
*/
|
|
308
|
+
needsUpdate(sectionName, existingVersion, existingChecksum) {
|
|
309
|
+
const currentSection = MANAGED_SECTIONS[sectionName];
|
|
310
|
+
if (!currentSection) return false;
|
|
311
|
+
|
|
312
|
+
// Check version
|
|
313
|
+
if (this.compareVersions(currentSection.version, existingVersion) > 0) {
|
|
314
|
+
return true;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Check checksum (in case we updated content without version bump)
|
|
318
|
+
const currentChecksum = this.calculateChecksum(currentSection.content);
|
|
319
|
+
return currentChecksum !== existingChecksum;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Compare semantic versions
|
|
324
|
+
*/
|
|
325
|
+
compareVersions(v1, v2) {
|
|
326
|
+
const parts1 = v1.split('.').map(Number);
|
|
327
|
+
const parts2 = v2.split('.').map(Number);
|
|
328
|
+
|
|
329
|
+
for (let i = 0; i < 3; i++) {
|
|
330
|
+
if (parts1[i] > parts2[i]) return 1;
|
|
331
|
+
if (parts1[i] < parts2[i]) return -1;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return 0;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Format a managed section with markers
|
|
339
|
+
*/
|
|
340
|
+
formatSection(sectionName) {
|
|
341
|
+
const section = MANAGED_SECTIONS[sectionName];
|
|
342
|
+
if (!section) return '';
|
|
343
|
+
|
|
344
|
+
const checksum = this.calculateChecksum(section.content);
|
|
345
|
+
const marker = this.createSectionMarker(sectionName, section.version, checksum);
|
|
346
|
+
const endMarker = this.createEndMarker(sectionName);
|
|
347
|
+
|
|
348
|
+
return `${marker}
|
|
349
|
+
${section.title}
|
|
350
|
+
|
|
351
|
+
${section.content}
|
|
352
|
+
${endMarker}`;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Create new house rules with all managed sections
|
|
357
|
+
*/
|
|
358
|
+
createNewHouseRules() {
|
|
359
|
+
const sections = [];
|
|
360
|
+
|
|
361
|
+
// Header
|
|
362
|
+
sections.push(`# House Rules for AI Agents
|
|
363
|
+
|
|
364
|
+
**IMPORTANT: All AI agents (Claude, Cline, Copilot, etc.) must read and follow these rules at the start of each session.**
|
|
365
|
+
`);
|
|
366
|
+
|
|
367
|
+
// Add managed sections
|
|
368
|
+
sections.push(this.formatSection('file-coordination'));
|
|
369
|
+
sections.push('');
|
|
370
|
+
sections.push(this.formatSection('core-principles'));
|
|
371
|
+
sections.push('');
|
|
372
|
+
sections.push(this.formatSection('project-conventions'));
|
|
373
|
+
|
|
374
|
+
return sections.join('\n');
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Update house rules intelligently
|
|
379
|
+
*/
|
|
380
|
+
async updateHouseRules(options = {}) {
|
|
381
|
+
const { createIfMissing = true, backupExisting = true } = options;
|
|
382
|
+
|
|
383
|
+
// Find or create house rules file
|
|
384
|
+
if (!this.houseRulesPath) {
|
|
385
|
+
if (!createIfMissing) {
|
|
386
|
+
return { updated: false, reason: 'No house rules file found' };
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
this.houseRulesPath = path.join(this.projectRoot, 'houserules.md');
|
|
390
|
+
const content = this.createNewHouseRules();
|
|
391
|
+
fs.writeFileSync(this.houseRulesPath, content);
|
|
392
|
+
|
|
393
|
+
return {
|
|
394
|
+
updated: true,
|
|
395
|
+
created: true,
|
|
396
|
+
path: this.houseRulesPath,
|
|
397
|
+
sections: Object.keys(MANAGED_SECTIONS)
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Read existing content
|
|
402
|
+
const existingContent = fs.readFileSync(this.houseRulesPath, 'utf8');
|
|
403
|
+
const existingSections = this.extractManagedSections(existingContent);
|
|
404
|
+
|
|
405
|
+
// Check what needs updating
|
|
406
|
+
const updates = [];
|
|
407
|
+
const additions = [];
|
|
408
|
+
|
|
409
|
+
for (const [sectionName, sectionData] of Object.entries(MANAGED_SECTIONS)) {
|
|
410
|
+
if (existingSections[sectionName]) {
|
|
411
|
+
// Section exists - check if needs update
|
|
412
|
+
if (this.needsUpdate(sectionName, existingSections[sectionName].version, existingSections[sectionName].checksum)) {
|
|
413
|
+
updates.push(sectionName);
|
|
414
|
+
}
|
|
415
|
+
} else {
|
|
416
|
+
// Section doesn't exist - needs to be added
|
|
417
|
+
additions.push(sectionName);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// If no updates needed, return
|
|
422
|
+
if (updates.length === 0 && additions.length === 0) {
|
|
423
|
+
return { updated: false, reason: 'All sections are up to date' };
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Create backup if requested
|
|
427
|
+
if (backupExisting) {
|
|
428
|
+
// Create backup in DevopsAgent_Backups folder
|
|
429
|
+
const backupDir = path.join(this.projectRoot, 'DevopsAgent_Backups');
|
|
430
|
+
if (!fs.existsSync(backupDir)) {
|
|
431
|
+
fs.mkdirSync(backupDir, { recursive: true });
|
|
432
|
+
// Ensure backup folder is in gitignore
|
|
433
|
+
this.ensureBackupInGitignore();
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5);
|
|
437
|
+
const backupFileName = `houserules_backup_${timestamp}.md`;
|
|
438
|
+
const backupPath = path.join(backupDir, backupFileName);
|
|
439
|
+
|
|
440
|
+
fs.copyFileSync(this.houseRulesPath, backupPath);
|
|
441
|
+
|
|
442
|
+
// Only log if not running as CLI
|
|
443
|
+
if (!process.argv[1]?.endsWith('house-rules-manager.js')) {
|
|
444
|
+
console.log(`Backup created: ${backupPath}`);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Build new content
|
|
449
|
+
let newContent = existingContent;
|
|
450
|
+
|
|
451
|
+
// Update existing sections
|
|
452
|
+
for (const sectionName of updates) {
|
|
453
|
+
const existingSection = existingSections[sectionName];
|
|
454
|
+
const newSection = this.formatSection(sectionName);
|
|
455
|
+
|
|
456
|
+
// Replace the old section with the new one
|
|
457
|
+
const beforeSection = newContent.substring(0, existingSection.startIndex);
|
|
458
|
+
const afterSection = newContent.substring(existingSection.endIndex);
|
|
459
|
+
newContent = beforeSection + newSection + afterSection;
|
|
460
|
+
|
|
461
|
+
// Recalculate positions for remaining sections
|
|
462
|
+
const diff = newSection.length - (existingSection.endIndex - existingSection.startIndex);
|
|
463
|
+
for (const section of Object.values(existingSections)) {
|
|
464
|
+
if (section.startIndex > existingSection.startIndex) {
|
|
465
|
+
section.startIndex += diff;
|
|
466
|
+
section.endIndex += diff;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Add new sections
|
|
472
|
+
if (additions.length > 0) {
|
|
473
|
+
// Find the best place to insert new sections
|
|
474
|
+
let insertPosition = 0;
|
|
475
|
+
|
|
476
|
+
// Try to insert after the main header
|
|
477
|
+
const headerMatch = /^# .+\n/m.exec(newContent);
|
|
478
|
+
if (headerMatch) {
|
|
479
|
+
insertPosition = headerMatch.index + headerMatch[0].length;
|
|
480
|
+
|
|
481
|
+
// Skip any immediate description
|
|
482
|
+
const descMatch = /\*\*IMPORTANT:.+\*\*\n/m.exec(newContent.substring(insertPosition));
|
|
483
|
+
if (descMatch && descMatch.index === 0) {
|
|
484
|
+
insertPosition += descMatch[0].length;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Add each new section
|
|
489
|
+
const sectionsToAdd = additions.map(name => '\n' + this.formatSection(name) + '\n').join('');
|
|
490
|
+
newContent = newContent.substring(0, insertPosition) + sectionsToAdd + newContent.substring(insertPosition);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Write updated content
|
|
494
|
+
fs.writeFileSync(this.houseRulesPath, newContent);
|
|
495
|
+
|
|
496
|
+
return {
|
|
497
|
+
updated: true,
|
|
498
|
+
path: this.houseRulesPath,
|
|
499
|
+
updatedSections: updates,
|
|
500
|
+
addedSections: additions,
|
|
501
|
+
totalChanges: updates.length + additions.length
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Ensure backup folder is in gitignore
|
|
507
|
+
*/
|
|
508
|
+
ensureBackupInGitignore() {
|
|
509
|
+
const gitignorePath = path.join(this.projectRoot, '.gitignore');
|
|
510
|
+
const backupPattern = 'DevopsAgent_Backups/';
|
|
511
|
+
|
|
512
|
+
try {
|
|
513
|
+
if (fs.existsSync(gitignorePath)) {
|
|
514
|
+
const content = fs.readFileSync(gitignorePath, 'utf8');
|
|
515
|
+
|
|
516
|
+
// Check if pattern already exists
|
|
517
|
+
if (!content.includes(backupPattern)) {
|
|
518
|
+
// Add to gitignore
|
|
519
|
+
const updatedContent = content.trimEnd() + '\n\n# DevOps Agent backup files\n' + backupPattern + '\n';
|
|
520
|
+
fs.writeFileSync(gitignorePath, updatedContent);
|
|
521
|
+
}
|
|
522
|
+
} else {
|
|
523
|
+
// Create gitignore with the pattern
|
|
524
|
+
const content = '# DevOps Agent backup files\n' + backupPattern + '\n';
|
|
525
|
+
fs.writeFileSync(gitignorePath, content);
|
|
526
|
+
}
|
|
527
|
+
} catch (err) {
|
|
528
|
+
// Silently fail - not critical if gitignore can't be updated
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Get status of house rules
|
|
534
|
+
*/
|
|
535
|
+
getStatus() {
|
|
536
|
+
if (!this.houseRulesPath) {
|
|
537
|
+
return {
|
|
538
|
+
exists: false,
|
|
539
|
+
path: null,
|
|
540
|
+
managedSections: {},
|
|
541
|
+
needsUpdate: true
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const content = fs.readFileSync(this.houseRulesPath, 'utf8');
|
|
546
|
+
const existingSections = this.extractManagedSections(content);
|
|
547
|
+
const status = {
|
|
548
|
+
exists: true,
|
|
549
|
+
path: this.houseRulesPath,
|
|
550
|
+
managedSections: {},
|
|
551
|
+
needsUpdate: false
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
// Check each managed section
|
|
555
|
+
for (const [sectionName, sectionData] of Object.entries(MANAGED_SECTIONS)) {
|
|
556
|
+
if (existingSections[sectionName]) {
|
|
557
|
+
const existing = existingSections[sectionName];
|
|
558
|
+
const needsUpdate = this.needsUpdate(sectionName, existing.version, existing.checksum);
|
|
559
|
+
|
|
560
|
+
status.managedSections[sectionName] = {
|
|
561
|
+
installed: true,
|
|
562
|
+
installedVersion: existing.version,
|
|
563
|
+
currentVersion: sectionData.version,
|
|
564
|
+
needsUpdate
|
|
565
|
+
};
|
|
566
|
+
|
|
567
|
+
if (needsUpdate) {
|
|
568
|
+
status.needsUpdate = true;
|
|
569
|
+
}
|
|
570
|
+
} else {
|
|
571
|
+
status.managedSections[sectionName] = {
|
|
572
|
+
installed: false,
|
|
573
|
+
currentVersion: sectionData.version,
|
|
574
|
+
needsUpdate: true
|
|
575
|
+
};
|
|
576
|
+
status.needsUpdate = true;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
return status;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
export default HouseRulesManager;
|
|
585
|
+
|
|
586
|
+
// CLI interface when run directly
|
|
587
|
+
// Check if this is the main module being run
|
|
588
|
+
const isMainModule = import.meta.url === `file://${process.argv[1]}` ||
|
|
589
|
+
process.argv[1]?.endsWith('house-rules-manager.js');
|
|
590
|
+
|
|
591
|
+
if (isMainModule) {
|
|
592
|
+
const manager = new HouseRulesManager();
|
|
593
|
+
const command = process.argv[2];
|
|
594
|
+
|
|
595
|
+
switch (command) {
|
|
596
|
+
case 'status':
|
|
597
|
+
// Output only the JSON for the bash script to parse
|
|
598
|
+
console.log(JSON.stringify(manager.getStatus()));
|
|
599
|
+
break;
|
|
600
|
+
|
|
601
|
+
case 'update':
|
|
602
|
+
manager.updateHouseRules().then(result => {
|
|
603
|
+
console.log(JSON.stringify(result));
|
|
604
|
+
});
|
|
605
|
+
break;
|
|
606
|
+
|
|
607
|
+
default:
|
|
608
|
+
console.log('Usage: node house-rules-manager.js [status|update]');
|
|
609
|
+
}
|
|
610
|
+
}
|