agentic-team-templates 0.3.0 → 0.4.1
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 +2 -2
- package/package.json +5 -5
- package/src/index.js +205 -9
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
1
|
+
# agentic-team-templates
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/cursor-templates)
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
@@ -277,4 +277,4 @@ Changes to shared rules affect all templates, so be thoughtful with modification
|
|
|
277
277
|
|
|
278
278
|
## License
|
|
279
279
|
|
|
280
|
-
MIT © [David Mendez](https://github.com/
|
|
280
|
+
MIT © [David Mendez](https://github.com/djm204)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentic-team-templates",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "AI coding assistant templates for Cursor IDE. Pre-configured rules and guidelines that help AI assistants write better code. - use at your own risk",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cursor",
|
|
@@ -14,15 +14,15 @@
|
|
|
14
14
|
"developer-tools",
|
|
15
15
|
"code-quality"
|
|
16
16
|
],
|
|
17
|
-
"author": "David Josef Mendez <
|
|
17
|
+
"author": "David Josef Mendez <me@davidmendez.dev>",
|
|
18
18
|
"license": "MIT",
|
|
19
19
|
"repository": {
|
|
20
20
|
"type": "git",
|
|
21
|
-
"url": "git+https://github.com/djm204/
|
|
21
|
+
"url": "git+https://github.com/djm204/agentic-team-templates.git"
|
|
22
22
|
},
|
|
23
|
-
"homepage": "https://github.com/djm204/
|
|
23
|
+
"homepage": "https://github.com/djm204/agentic-team-templates#readme",
|
|
24
24
|
"bugs": {
|
|
25
|
-
"url": "https://github.com/djm204/
|
|
25
|
+
"url": "https://github.com/djm204/agentic-team-templates/issues"
|
|
26
26
|
},
|
|
27
27
|
"bin": {
|
|
28
28
|
"cursor-templates": "./bin/cli.js"
|
package/src/index.js
CHANGED
|
@@ -86,6 +86,7 @@ ${colors.yellow('Examples:')}
|
|
|
86
86
|
|
|
87
87
|
${colors.dim('Shared rules (code-quality, security, git-workflow, etc.) are always included.')}
|
|
88
88
|
${colors.dim('Identical files are skipped. Modified files are preserved; ours saved as *-1.md.')}
|
|
89
|
+
${colors.dim('CLAUDE.md: missing sections are intelligently merged (not overwritten).')}
|
|
89
90
|
`);
|
|
90
91
|
}
|
|
91
92
|
|
|
@@ -117,6 +118,179 @@ function filesMatch(file1, file2) {
|
|
|
117
118
|
}
|
|
118
119
|
}
|
|
119
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Parse markdown content into sections by ## headings
|
|
123
|
+
* @param {string} content - Markdown content
|
|
124
|
+
* @returns {Array<{heading: string, content: string, signature: string}>}
|
|
125
|
+
*/
|
|
126
|
+
function parseMarkdownSections(content) {
|
|
127
|
+
const lines = content.split('\n');
|
|
128
|
+
const sections = [];
|
|
129
|
+
let currentSection = null;
|
|
130
|
+
let preamble = [];
|
|
131
|
+
|
|
132
|
+
for (const line of lines) {
|
|
133
|
+
if (line.startsWith('## ')) {
|
|
134
|
+
// Save previous section
|
|
135
|
+
if (currentSection) {
|
|
136
|
+
currentSection.content = currentSection.lines.join('\n');
|
|
137
|
+
currentSection.signature = generateSectionSignature(currentSection.heading, currentSection.lines);
|
|
138
|
+
delete currentSection.lines;
|
|
139
|
+
sections.push(currentSection);
|
|
140
|
+
}
|
|
141
|
+
// Start new section
|
|
142
|
+
currentSection = {
|
|
143
|
+
heading: line.slice(3).trim(),
|
|
144
|
+
lines: []
|
|
145
|
+
};
|
|
146
|
+
} else if (currentSection) {
|
|
147
|
+
currentSection.lines.push(line);
|
|
148
|
+
} else {
|
|
149
|
+
// Content before first ## heading (preamble)
|
|
150
|
+
preamble.push(line);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Don't forget the last section
|
|
155
|
+
if (currentSection) {
|
|
156
|
+
currentSection.content = currentSection.lines.join('\n');
|
|
157
|
+
currentSection.signature = generateSectionSignature(currentSection.heading, currentSection.lines);
|
|
158
|
+
delete currentSection.lines;
|
|
159
|
+
sections.push(currentSection);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return { preamble: preamble.join('\n'), sections };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Generate a signature for a section based on heading + first meaningful lines
|
|
167
|
+
* Used for matching sections even if heading text differs slightly
|
|
168
|
+
* @param {string} heading
|
|
169
|
+
* @param {string[]} lines
|
|
170
|
+
* @returns {string}
|
|
171
|
+
*/
|
|
172
|
+
function generateSectionSignature(heading, lines) {
|
|
173
|
+
// Normalize heading: lowercase, remove special chars, collapse whitespace
|
|
174
|
+
const normalizedHeading = heading.toLowerCase()
|
|
175
|
+
.replace(/[^a-z0-9\s]/g, '')
|
|
176
|
+
.replace(/\s+/g, ' ')
|
|
177
|
+
.trim();
|
|
178
|
+
|
|
179
|
+
// Get first 3 non-empty, non-heading lines for content signature
|
|
180
|
+
const meaningfulLines = lines
|
|
181
|
+
.filter(l => l.trim() && !l.startsWith('#') && !l.startsWith('|') && !l.startsWith('-'))
|
|
182
|
+
.slice(0, 3)
|
|
183
|
+
.map(l => l.toLowerCase().replace(/[^a-z0-9\s]/g, '').replace(/\s+/g, ' ').trim())
|
|
184
|
+
.join(' ');
|
|
185
|
+
|
|
186
|
+
return `${normalizedHeading}::${meaningfulLines.slice(0, 100)}`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Find sections from template that are missing in existing content
|
|
191
|
+
* @param {string} existingContent
|
|
192
|
+
* @param {string} templateContent
|
|
193
|
+
* @returns {{missing: Array<{heading: string, content: string}>, matchedCount: number}}
|
|
194
|
+
*/
|
|
195
|
+
function findMissingSections(existingContent, templateContent) {
|
|
196
|
+
const existing = parseMarkdownSections(existingContent);
|
|
197
|
+
const template = parseMarkdownSections(templateContent);
|
|
198
|
+
|
|
199
|
+
const existingSignatures = new Set(existing.sections.map(s => s.signature));
|
|
200
|
+
const existingHeadings = new Set(existing.sections.map(s => s.heading.toLowerCase()));
|
|
201
|
+
|
|
202
|
+
const missing = [];
|
|
203
|
+
let matchedCount = 0;
|
|
204
|
+
|
|
205
|
+
for (const section of template.sections) {
|
|
206
|
+
// Check by signature first (heading + content), then by heading alone
|
|
207
|
+
const signatureMatch = existingSignatures.has(section.signature);
|
|
208
|
+
const headingMatch = existingHeadings.has(section.heading.toLowerCase());
|
|
209
|
+
|
|
210
|
+
if (signatureMatch || headingMatch) {
|
|
211
|
+
matchedCount++;
|
|
212
|
+
} else {
|
|
213
|
+
missing.push(section);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return { missing, matchedCount };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Merge template sections into existing content, inserting missing sections in template order
|
|
222
|
+
* @param {string} existingContent
|
|
223
|
+
* @param {string} templateContent
|
|
224
|
+
* @returns {{merged: string, addedSections: string[]}}
|
|
225
|
+
*/
|
|
226
|
+
function mergeClaudeContent(existingContent, templateContent) {
|
|
227
|
+
const existing = parseMarkdownSections(existingContent);
|
|
228
|
+
const template = parseMarkdownSections(templateContent);
|
|
229
|
+
const { missing } = findMissingSections(existingContent, templateContent);
|
|
230
|
+
|
|
231
|
+
if (missing.length === 0) {
|
|
232
|
+
return { merged: existingContent, addedSections: [] };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Build a map of existing sections by normalized heading for insertion point lookup
|
|
236
|
+
const existingByHeading = new Map();
|
|
237
|
+
existing.sections.forEach((s, i) => {
|
|
238
|
+
existingByHeading.set(s.heading.toLowerCase(), i);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// Find template section order and determine where to insert missing sections
|
|
242
|
+
const templateOrder = template.sections.map(s => s.heading.toLowerCase());
|
|
243
|
+
|
|
244
|
+
// For each missing section, find the best insertion point based on template order
|
|
245
|
+
const insertions = []; // { afterIndex: number, section: section }
|
|
246
|
+
|
|
247
|
+
for (const missingSection of missing) {
|
|
248
|
+
const missingIndex = templateOrder.indexOf(missingSection.heading.toLowerCase());
|
|
249
|
+
|
|
250
|
+
// Find the closest preceding section that exists in the existing content
|
|
251
|
+
let insertAfterIndex = -1; // -1 means insert at beginning (after preamble)
|
|
252
|
+
|
|
253
|
+
for (let i = missingIndex - 1; i >= 0; i--) {
|
|
254
|
+
const precedingHeading = templateOrder[i];
|
|
255
|
+
if (existingByHeading.has(precedingHeading)) {
|
|
256
|
+
insertAfterIndex = existingByHeading.get(precedingHeading);
|
|
257
|
+
break;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
insertions.push({ afterIndex: insertAfterIndex, section: missingSection });
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Sort insertions by afterIndex (descending) so we insert from bottom to top
|
|
265
|
+
// This preserves indices as we insert
|
|
266
|
+
insertions.sort((a, b) => b.afterIndex - a.afterIndex);
|
|
267
|
+
|
|
268
|
+
// Build the merged content
|
|
269
|
+
const mergedSections = [...existing.sections];
|
|
270
|
+
const addedSections = [];
|
|
271
|
+
|
|
272
|
+
for (const { afterIndex, section } of insertions) {
|
|
273
|
+
const insertAt = afterIndex + 1;
|
|
274
|
+
mergedSections.splice(insertAt, 0, section);
|
|
275
|
+
addedSections.push(section.heading);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Reconstruct the markdown
|
|
279
|
+
let merged = existing.preamble;
|
|
280
|
+
if (merged && !merged.endsWith('\n\n')) {
|
|
281
|
+
merged = merged.trimEnd() + '\n\n';
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
for (const section of mergedSections) {
|
|
285
|
+
merged += `## ${section.heading}\n${section.content}\n`;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// addedSections is in reverse order due to sorting, reverse it back
|
|
289
|
+
addedSections.reverse();
|
|
290
|
+
|
|
291
|
+
return { merged: merged.trimEnd() + '\n', addedSections };
|
|
292
|
+
}
|
|
293
|
+
|
|
120
294
|
/**
|
|
121
295
|
* Get alternate filename with -1 suffix (e.g., code-quality.md -> code-quality-1.md)
|
|
122
296
|
*/
|
|
@@ -305,6 +479,7 @@ function install(targetDir, templates, dryRun = false, force = false) {
|
|
|
305
479
|
console.log(`${colors.blue('Installing to:')} ${targetDir}`);
|
|
306
480
|
if (!force) {
|
|
307
481
|
console.log(colors.dim('(identical files skipped, modified files preserved with ours saved as *-1.md)'));
|
|
482
|
+
console.log(colors.dim('(CLAUDE.md: missing sections merged intelligently)'));
|
|
308
483
|
}
|
|
309
484
|
console.log();
|
|
310
485
|
|
|
@@ -386,6 +561,7 @@ function install(targetDir, templates, dryRun = false, force = false) {
|
|
|
386
561
|
// 3. Generate CLAUDE.md
|
|
387
562
|
const claudePath = path.join(targetDir, 'CLAUDE.md');
|
|
388
563
|
const claudeExists = fs.existsSync(claudePath);
|
|
564
|
+
const templateContent = generateClaudeMdContent(templates);
|
|
389
565
|
|
|
390
566
|
console.log(colors.green('► Generating CLAUDE.md...'));
|
|
391
567
|
if (dryRun) {
|
|
@@ -394,23 +570,43 @@ function install(targetDir, templates, dryRun = false, force = false) {
|
|
|
394
570
|
} else if (force) {
|
|
395
571
|
console.log(` ${colors.dim('[update]')} CLAUDE.md`);
|
|
396
572
|
} else {
|
|
397
|
-
|
|
573
|
+
// Check what would be merged
|
|
574
|
+
const existingContent = fs.readFileSync(claudePath, 'utf8');
|
|
575
|
+
const { missing } = findMissingSections(existingContent, templateContent);
|
|
576
|
+
if (missing.length === 0) {
|
|
577
|
+
console.log(` ${colors.yellow('[skip]')} CLAUDE.md (all sections present)`);
|
|
578
|
+
} else {
|
|
579
|
+
console.log(` ${colors.blue('[merge]')} CLAUDE.md (would add ${missing.length} section(s))`);
|
|
580
|
+
for (const section of missing) {
|
|
581
|
+
console.log(` ${colors.dim('+')} ${section.heading}`);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
398
584
|
}
|
|
399
585
|
} else if (!claudeExists) {
|
|
400
|
-
|
|
586
|
+
fs.writeFileSync(claudePath, templateContent);
|
|
401
587
|
console.log(` ${colors.dim('[copied]')} CLAUDE.md`);
|
|
402
588
|
stats.copied++;
|
|
403
589
|
} else if (force) {
|
|
404
|
-
|
|
590
|
+
fs.writeFileSync(claudePath, templateContent);
|
|
405
591
|
console.log(` ${colors.dim('[updated]')} CLAUDE.md`);
|
|
406
592
|
stats.updated++;
|
|
407
593
|
} else {
|
|
408
|
-
//
|
|
409
|
-
const
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
594
|
+
// Intelligent merge: append only missing sections
|
|
595
|
+
const existingContent = fs.readFileSync(claudePath, 'utf8');
|
|
596
|
+
const { merged, addedSections } = mergeClaudeContent(existingContent, templateContent);
|
|
597
|
+
|
|
598
|
+
if (addedSections.length === 0) {
|
|
599
|
+
console.log(` ${colors.yellow('[skip]')} CLAUDE.md (all sections present)`);
|
|
600
|
+
stats.skipped++;
|
|
601
|
+
} else {
|
|
602
|
+
fs.writeFileSync(claudePath, merged);
|
|
603
|
+
console.log(` ${colors.blue('[merged]')} CLAUDE.md`);
|
|
604
|
+
console.log(` ${colors.green('Added sections:')}`);
|
|
605
|
+
for (const heading of addedSections) {
|
|
606
|
+
console.log(` ${colors.dim('+')} ${heading}`);
|
|
607
|
+
}
|
|
608
|
+
stats.updated++;
|
|
609
|
+
}
|
|
414
610
|
}
|
|
415
611
|
console.log();
|
|
416
612
|
|