@fermindi/pwn-cli 0.1.1 → 0.3.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/LICENSE +21 -21
- package/README.md +265 -251
- package/cli/batch.js +333 -333
- package/cli/codespaces.js +303 -303
- package/cli/index.js +112 -91
- package/cli/inject.js +90 -67
- package/cli/knowledge.js +531 -531
- package/cli/migrate.js +466 -0
- package/cli/notify.js +135 -135
- package/cli/patterns.js +665 -665
- package/cli/save.js +206 -0
- package/cli/status.js +91 -91
- package/cli/update.js +189 -0
- package/cli/validate.js +61 -61
- package/package.json +70 -70
- package/src/core/inject.js +300 -204
- package/src/core/state.js +91 -91
- package/src/core/validate.js +202 -202
- package/src/core/workspace.js +176 -176
- package/src/index.js +20 -20
- package/src/knowledge/gc.js +308 -308
- package/src/knowledge/lifecycle.js +401 -401
- package/src/knowledge/promote.js +364 -364
- package/src/knowledge/references.js +342 -342
- package/src/patterns/matcher.js +218 -218
- package/src/patterns/registry.js +375 -375
- package/src/patterns/triggers.js +423 -423
- package/src/services/batch-service.js +849 -849
- package/src/services/notification-service.js +342 -342
- package/templates/codespaces/devcontainer.json +52 -52
- package/templates/codespaces/setup.sh +70 -70
- package/templates/workspace/.ai/README.md +164 -164
- package/templates/workspace/.ai/agents/README.md +204 -204
- package/templates/workspace/.ai/agents/claude.md +625 -625
- package/templates/workspace/.ai/config/README.md +79 -79
- package/templates/workspace/.ai/config/notifications.template.json +20 -20
- package/templates/workspace/.ai/memory/deadends.md +79 -79
- package/templates/workspace/.ai/memory/decisions.md +58 -58
- package/templates/workspace/.ai/memory/patterns.md +65 -65
- package/templates/workspace/.ai/patterns/backend/README.md +126 -126
- package/templates/workspace/.ai/patterns/frontend/README.md +103 -103
- package/templates/workspace/.ai/patterns/index.md +256 -256
- package/templates/workspace/.ai/patterns/triggers.json +1087 -1087
- package/templates/workspace/.ai/patterns/universal/README.md +141 -141
- package/templates/workspace/.ai/state.template.json +8 -8
- package/templates/workspace/.ai/tasks/active.md +77 -77
- package/templates/workspace/.ai/tasks/backlog.md +95 -95
- package/templates/workspace/.ai/workflows/batch-task.md +356 -356
package/src/patterns/registry.js
CHANGED
|
@@ -1,375 +1,375 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pattern Registry - Manages pattern storage and retrieval
|
|
3
|
-
*
|
|
4
|
-
* Patterns are organized in categories:
|
|
5
|
-
* - frontend/ (react, vue, svelte, styling, component-libs)
|
|
6
|
-
* - backend/ (express, fastapi, database, auth)
|
|
7
|
-
* - universal/ (typescript, testing, error-handling, git, build-tools)
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync, statSync } from 'fs';
|
|
11
|
-
import { join, relative, basename, dirname } from 'path';
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Pattern entry structure
|
|
15
|
-
* @typedef {Object} Pattern
|
|
16
|
-
* @property {string} id - Unique identifier (category/name)
|
|
17
|
-
* @property {string} name - Human-readable name
|
|
18
|
-
* @property {string} category - Pattern category (frontend, backend, universal)
|
|
19
|
-
* @property {string} path - Relative path to pattern directory
|
|
20
|
-
* @property {string} description - Pattern description
|
|
21
|
-
* @property {string[]} triggers - Associated trigger IDs
|
|
22
|
-
* @property {Object} metadata - Additional metadata
|
|
23
|
-
* @property {number} metadata.usageCount - How many times pattern was applied
|
|
24
|
-
* @property {string} metadata.lastUsed - ISO timestamp of last usage
|
|
25
|
-
* @property {string} metadata.createdAt - ISO timestamp of creation
|
|
26
|
-
*/
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* PatternRegistry - Singleton for managing patterns
|
|
30
|
-
*/
|
|
31
|
-
export class PatternRegistry {
|
|
32
|
-
constructor(workspacePath) {
|
|
33
|
-
this.workspacePath = workspacePath;
|
|
34
|
-
this.aiPath = join(workspacePath, '.ai');
|
|
35
|
-
this.patternsPath = join(this.aiPath, 'patterns');
|
|
36
|
-
this.registryPath = join(this.patternsPath, 'registry.json');
|
|
37
|
-
this.patterns = new Map();
|
|
38
|
-
this.loaded = false;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Initialize or load the registry
|
|
43
|
-
*/
|
|
44
|
-
async load() {
|
|
45
|
-
if (this.loaded) return;
|
|
46
|
-
|
|
47
|
-
// Create patterns directory if it doesn't exist
|
|
48
|
-
if (!existsSync(this.patternsPath)) {
|
|
49
|
-
mkdirSync(this.patternsPath, { recursive: true });
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// Load existing registry or scan for patterns
|
|
53
|
-
if (existsSync(this.registryPath)) {
|
|
54
|
-
try {
|
|
55
|
-
const data = JSON.parse(readFileSync(this.registryPath, 'utf8'));
|
|
56
|
-
for (const pattern of data.patterns || []) {
|
|
57
|
-
this.patterns.set(pattern.id, pattern);
|
|
58
|
-
}
|
|
59
|
-
} catch (error) {
|
|
60
|
-
console.warn('⚠️ Could not load registry, scanning patterns directory...');
|
|
61
|
-
await this.scan();
|
|
62
|
-
}
|
|
63
|
-
} else {
|
|
64
|
-
await this.scan();
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
this.loaded = true;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Scan patterns directory and build registry
|
|
72
|
-
*/
|
|
73
|
-
async scan() {
|
|
74
|
-
this.patterns.clear();
|
|
75
|
-
|
|
76
|
-
const categories = ['frontend', 'backend', 'universal'];
|
|
77
|
-
|
|
78
|
-
for (const category of categories) {
|
|
79
|
-
const categoryPath = join(this.patternsPath, category);
|
|
80
|
-
if (!existsSync(categoryPath)) continue;
|
|
81
|
-
|
|
82
|
-
const entries = readdirSync(categoryPath, { withFileTypes: true });
|
|
83
|
-
|
|
84
|
-
for (const entry of entries) {
|
|
85
|
-
if (!entry.isDirectory()) continue;
|
|
86
|
-
|
|
87
|
-
const patternPath = join(categoryPath, entry.name);
|
|
88
|
-
const readmePath = join(patternPath, 'README.md');
|
|
89
|
-
|
|
90
|
-
const pattern = {
|
|
91
|
-
id: `${category}/${entry.name}`,
|
|
92
|
-
name: this.formatName(entry.name),
|
|
93
|
-
category,
|
|
94
|
-
path: relative(this.aiPath, patternPath),
|
|
95
|
-
description: this.extractDescription(readmePath),
|
|
96
|
-
triggers: [],
|
|
97
|
-
metadata: {
|
|
98
|
-
usageCount: 0,
|
|
99
|
-
lastUsed: null,
|
|
100
|
-
createdAt: new Date().toISOString()
|
|
101
|
-
}
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
this.patterns.set(pattern.id, pattern);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// Save the scanned registry
|
|
109
|
-
await this.save();
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Extract description from README.md first line
|
|
114
|
-
*/
|
|
115
|
-
extractDescription(readmePath) {
|
|
116
|
-
if (!existsSync(readmePath)) {
|
|
117
|
-
return 'No description available';
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
try {
|
|
121
|
-
const content = readFileSync(readmePath, 'utf8');
|
|
122
|
-
const lines = content.split('\n');
|
|
123
|
-
|
|
124
|
-
// Find first non-empty, non-heading line
|
|
125
|
-
for (const line of lines) {
|
|
126
|
-
const trimmed = line.trim();
|
|
127
|
-
if (trimmed && !trimmed.startsWith('#') && !trimmed.startsWith('---')) {
|
|
128
|
-
return trimmed.slice(0, 100) + (trimmed.length > 100 ? '...' : '');
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
return 'Pattern documentation available';
|
|
133
|
-
} catch {
|
|
134
|
-
return 'No description available';
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Format pattern name from directory name
|
|
140
|
-
*/
|
|
141
|
-
formatName(dirName) {
|
|
142
|
-
return dirName
|
|
143
|
-
.split(/[-_]/)
|
|
144
|
-
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
145
|
-
.join(' ');
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* Save registry to disk
|
|
150
|
-
*/
|
|
151
|
-
async save() {
|
|
152
|
-
const data = {
|
|
153
|
-
version: '1.0.0',
|
|
154
|
-
lastUpdated: new Date().toISOString(),
|
|
155
|
-
patterns: Array.from(this.patterns.values())
|
|
156
|
-
};
|
|
157
|
-
|
|
158
|
-
writeFileSync(this.registryPath, JSON.stringify(data, null, 2));
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* Get a pattern by ID
|
|
163
|
-
* @param {string} id - Pattern ID (e.g., 'frontend/react')
|
|
164
|
-
* @returns {Pattern|null}
|
|
165
|
-
*/
|
|
166
|
-
get(id) {
|
|
167
|
-
return this.patterns.get(id) || null;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* Get all patterns
|
|
172
|
-
* @returns {Pattern[]}
|
|
173
|
-
*/
|
|
174
|
-
getAll() {
|
|
175
|
-
return Array.from(this.patterns.values());
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/**
|
|
179
|
-
* Get patterns by category
|
|
180
|
-
* @param {string} category - Category name
|
|
181
|
-
* @returns {Pattern[]}
|
|
182
|
-
*/
|
|
183
|
-
getByCategory(category) {
|
|
184
|
-
return this.getAll().filter(p => p.category === category);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* Search patterns by name or description
|
|
189
|
-
* @param {string} query - Search query
|
|
190
|
-
* @returns {Pattern[]}
|
|
191
|
-
*/
|
|
192
|
-
search(query) {
|
|
193
|
-
const lowerQuery = query.toLowerCase();
|
|
194
|
-
return this.getAll().filter(p =>
|
|
195
|
-
p.name.toLowerCase().includes(lowerQuery) ||
|
|
196
|
-
p.description.toLowerCase().includes(lowerQuery) ||
|
|
197
|
-
p.id.toLowerCase().includes(lowerQuery)
|
|
198
|
-
);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
/**
|
|
202
|
-
* Register a new pattern
|
|
203
|
-
* @param {Pattern} pattern - Pattern to register
|
|
204
|
-
*/
|
|
205
|
-
async register(pattern) {
|
|
206
|
-
// Ensure required fields
|
|
207
|
-
if (!pattern.id || !pattern.name || !pattern.category) {
|
|
208
|
-
throw new Error('Pattern must have id, name, and category');
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
// Create pattern directory if it doesn't exist
|
|
212
|
-
const patternDir = join(this.patternsPath, pattern.path || pattern.id);
|
|
213
|
-
if (!existsSync(patternDir)) {
|
|
214
|
-
mkdirSync(patternDir, { recursive: true });
|
|
215
|
-
|
|
216
|
-
// Create README.md with description
|
|
217
|
-
const readmePath = join(patternDir, 'README.md');
|
|
218
|
-
writeFileSync(readmePath, `# ${pattern.name}\n\n${pattern.description || ''}\n`);
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// Set metadata
|
|
222
|
-
pattern.metadata = pattern.metadata || {
|
|
223
|
-
usageCount: 0,
|
|
224
|
-
lastUsed: null,
|
|
225
|
-
createdAt: new Date().toISOString()
|
|
226
|
-
};
|
|
227
|
-
|
|
228
|
-
this.patterns.set(pattern.id, pattern);
|
|
229
|
-
await this.save();
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
/**
|
|
233
|
-
* Remove a pattern
|
|
234
|
-
* @param {string} id - Pattern ID
|
|
235
|
-
*/
|
|
236
|
-
async remove(id) {
|
|
237
|
-
this.patterns.delete(id);
|
|
238
|
-
await this.save();
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
/**
|
|
242
|
-
* Record pattern usage
|
|
243
|
-
* @param {string} id - Pattern ID
|
|
244
|
-
*/
|
|
245
|
-
async recordUsage(id) {
|
|
246
|
-
const pattern = this.patterns.get(id);
|
|
247
|
-
if (pattern) {
|
|
248
|
-
pattern.metadata.usageCount++;
|
|
249
|
-
pattern.metadata.lastUsed = new Date().toISOString();
|
|
250
|
-
await this.save();
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
/**
|
|
255
|
-
* Get pattern content (README.md)
|
|
256
|
-
* @param {string} id - Pattern ID
|
|
257
|
-
* @returns {string|null}
|
|
258
|
-
*/
|
|
259
|
-
getContent(id) {
|
|
260
|
-
const pattern = this.patterns.get(id);
|
|
261
|
-
if (!pattern) return null;
|
|
262
|
-
|
|
263
|
-
// pattern.path is relative to aiPath (e.g., "patterns/frontend/react")
|
|
264
|
-
const readmePath = join(this.aiPath, pattern.path, 'README.md');
|
|
265
|
-
if (!existsSync(readmePath)) return null;
|
|
266
|
-
|
|
267
|
-
return readFileSync(readmePath, 'utf8');
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
/**
|
|
271
|
-
* Get all files in a pattern directory
|
|
272
|
-
* @param {string} id - Pattern ID
|
|
273
|
-
* @returns {Object[]} Array of {name, path, content}
|
|
274
|
-
*/
|
|
275
|
-
getPatternFiles(id) {
|
|
276
|
-
const pattern = this.patterns.get(id);
|
|
277
|
-
if (!pattern) return [];
|
|
278
|
-
|
|
279
|
-
// pattern.path is relative to aiPath (e.g., "patterns/frontend/react")
|
|
280
|
-
const patternDir = join(this.aiPath, pattern.path);
|
|
281
|
-
if (!existsSync(patternDir)) return [];
|
|
282
|
-
|
|
283
|
-
const files = [];
|
|
284
|
-
const entries = readdirSync(patternDir, { withFileTypes: true });
|
|
285
|
-
|
|
286
|
-
for (const entry of entries) {
|
|
287
|
-
if (entry.isFile()) {
|
|
288
|
-
const filePath = join(patternDir, entry.name);
|
|
289
|
-
files.push({
|
|
290
|
-
name: entry.name,
|
|
291
|
-
path: relative(this.aiPath, filePath),
|
|
292
|
-
content: readFileSync(filePath, 'utf8')
|
|
293
|
-
});
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
return files;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
/**
|
|
301
|
-
* Get usage statistics
|
|
302
|
-
* @returns {Object}
|
|
303
|
-
*/
|
|
304
|
-
getStats() {
|
|
305
|
-
const patterns = this.getAll();
|
|
306
|
-
const byCategory = {};
|
|
307
|
-
let totalUsage = 0;
|
|
308
|
-
let mostUsed = null;
|
|
309
|
-
|
|
310
|
-
for (const pattern of patterns) {
|
|
311
|
-
// Count by category
|
|
312
|
-
byCategory[pattern.category] = (byCategory[pattern.category] || 0) + 1;
|
|
313
|
-
|
|
314
|
-
// Track total usage
|
|
315
|
-
totalUsage += pattern.metadata.usageCount;
|
|
316
|
-
|
|
317
|
-
// Track most used
|
|
318
|
-
if (!mostUsed || pattern.metadata.usageCount > mostUsed.metadata.usageCount) {
|
|
319
|
-
mostUsed = pattern;
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
return {
|
|
324
|
-
totalPatterns: patterns.length,
|
|
325
|
-
byCategory,
|
|
326
|
-
totalUsage,
|
|
327
|
-
mostUsed: mostUsed ? { id: mostUsed.id, count: mostUsed.metadata.usageCount } : null
|
|
328
|
-
};
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
/**
|
|
332
|
-
* Associate trigger with pattern
|
|
333
|
-
* @param {string} patternId - Pattern ID
|
|
334
|
-
* @param {string} triggerId - Trigger ID
|
|
335
|
-
*/
|
|
336
|
-
async addTrigger(patternId, triggerId) {
|
|
337
|
-
const pattern = this.patterns.get(patternId);
|
|
338
|
-
if (pattern && !pattern.triggers.includes(triggerId)) {
|
|
339
|
-
pattern.triggers.push(triggerId);
|
|
340
|
-
await this.save();
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
/**
|
|
345
|
-
* Remove trigger from pattern
|
|
346
|
-
* @param {string} patternId - Pattern ID
|
|
347
|
-
* @param {string} triggerId - Trigger ID
|
|
348
|
-
*/
|
|
349
|
-
async removeTrigger(patternId, triggerId) {
|
|
350
|
-
const pattern = this.patterns.get(patternId);
|
|
351
|
-
if (pattern) {
|
|
352
|
-
pattern.triggers = pattern.triggers.filter(t => t !== triggerId);
|
|
353
|
-
await this.save();
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
/**
|
|
359
|
-
* Create a new PatternRegistry instance for the given workspace
|
|
360
|
-
* @param {string} workspacePath - Path to workspace root
|
|
361
|
-
* @returns {PatternRegistry}
|
|
362
|
-
*/
|
|
363
|
-
export function createRegistry(workspacePath) {
|
|
364
|
-
return new PatternRegistry(workspacePath);
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
/**
|
|
368
|
-
* Get pattern registry for current working directory
|
|
369
|
-
* @returns {PatternRegistry}
|
|
370
|
-
*/
|
|
371
|
-
export function getRegistry() {
|
|
372
|
-
return createRegistry(process.cwd());
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
export default { PatternRegistry, createRegistry, getRegistry };
|
|
1
|
+
/**
|
|
2
|
+
* Pattern Registry - Manages pattern storage and retrieval
|
|
3
|
+
*
|
|
4
|
+
* Patterns are organized in categories:
|
|
5
|
+
* - frontend/ (react, vue, svelte, styling, component-libs)
|
|
6
|
+
* - backend/ (express, fastapi, database, auth)
|
|
7
|
+
* - universal/ (typescript, testing, error-handling, git, build-tools)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync, statSync } from 'fs';
|
|
11
|
+
import { join, relative, basename, dirname } from 'path';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Pattern entry structure
|
|
15
|
+
* @typedef {Object} Pattern
|
|
16
|
+
* @property {string} id - Unique identifier (category/name)
|
|
17
|
+
* @property {string} name - Human-readable name
|
|
18
|
+
* @property {string} category - Pattern category (frontend, backend, universal)
|
|
19
|
+
* @property {string} path - Relative path to pattern directory
|
|
20
|
+
* @property {string} description - Pattern description
|
|
21
|
+
* @property {string[]} triggers - Associated trigger IDs
|
|
22
|
+
* @property {Object} metadata - Additional metadata
|
|
23
|
+
* @property {number} metadata.usageCount - How many times pattern was applied
|
|
24
|
+
* @property {string} metadata.lastUsed - ISO timestamp of last usage
|
|
25
|
+
* @property {string} metadata.createdAt - ISO timestamp of creation
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* PatternRegistry - Singleton for managing patterns
|
|
30
|
+
*/
|
|
31
|
+
export class PatternRegistry {
|
|
32
|
+
constructor(workspacePath) {
|
|
33
|
+
this.workspacePath = workspacePath;
|
|
34
|
+
this.aiPath = join(workspacePath, '.ai');
|
|
35
|
+
this.patternsPath = join(this.aiPath, 'patterns');
|
|
36
|
+
this.registryPath = join(this.patternsPath, 'registry.json');
|
|
37
|
+
this.patterns = new Map();
|
|
38
|
+
this.loaded = false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Initialize or load the registry
|
|
43
|
+
*/
|
|
44
|
+
async load() {
|
|
45
|
+
if (this.loaded) return;
|
|
46
|
+
|
|
47
|
+
// Create patterns directory if it doesn't exist
|
|
48
|
+
if (!existsSync(this.patternsPath)) {
|
|
49
|
+
mkdirSync(this.patternsPath, { recursive: true });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Load existing registry or scan for patterns
|
|
53
|
+
if (existsSync(this.registryPath)) {
|
|
54
|
+
try {
|
|
55
|
+
const data = JSON.parse(readFileSync(this.registryPath, 'utf8'));
|
|
56
|
+
for (const pattern of data.patterns || []) {
|
|
57
|
+
this.patterns.set(pattern.id, pattern);
|
|
58
|
+
}
|
|
59
|
+
} catch (error) {
|
|
60
|
+
console.warn('⚠️ Could not load registry, scanning patterns directory...');
|
|
61
|
+
await this.scan();
|
|
62
|
+
}
|
|
63
|
+
} else {
|
|
64
|
+
await this.scan();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
this.loaded = true;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Scan patterns directory and build registry
|
|
72
|
+
*/
|
|
73
|
+
async scan() {
|
|
74
|
+
this.patterns.clear();
|
|
75
|
+
|
|
76
|
+
const categories = ['frontend', 'backend', 'universal'];
|
|
77
|
+
|
|
78
|
+
for (const category of categories) {
|
|
79
|
+
const categoryPath = join(this.patternsPath, category);
|
|
80
|
+
if (!existsSync(categoryPath)) continue;
|
|
81
|
+
|
|
82
|
+
const entries = readdirSync(categoryPath, { withFileTypes: true });
|
|
83
|
+
|
|
84
|
+
for (const entry of entries) {
|
|
85
|
+
if (!entry.isDirectory()) continue;
|
|
86
|
+
|
|
87
|
+
const patternPath = join(categoryPath, entry.name);
|
|
88
|
+
const readmePath = join(patternPath, 'README.md');
|
|
89
|
+
|
|
90
|
+
const pattern = {
|
|
91
|
+
id: `${category}/${entry.name}`,
|
|
92
|
+
name: this.formatName(entry.name),
|
|
93
|
+
category,
|
|
94
|
+
path: relative(this.aiPath, patternPath),
|
|
95
|
+
description: this.extractDescription(readmePath),
|
|
96
|
+
triggers: [],
|
|
97
|
+
metadata: {
|
|
98
|
+
usageCount: 0,
|
|
99
|
+
lastUsed: null,
|
|
100
|
+
createdAt: new Date().toISOString()
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
this.patterns.set(pattern.id, pattern);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Save the scanned registry
|
|
109
|
+
await this.save();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Extract description from README.md first line
|
|
114
|
+
*/
|
|
115
|
+
extractDescription(readmePath) {
|
|
116
|
+
if (!existsSync(readmePath)) {
|
|
117
|
+
return 'No description available';
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
const content = readFileSync(readmePath, 'utf8');
|
|
122
|
+
const lines = content.split('\n');
|
|
123
|
+
|
|
124
|
+
// Find first non-empty, non-heading line
|
|
125
|
+
for (const line of lines) {
|
|
126
|
+
const trimmed = line.trim();
|
|
127
|
+
if (trimmed && !trimmed.startsWith('#') && !trimmed.startsWith('---')) {
|
|
128
|
+
return trimmed.slice(0, 100) + (trimmed.length > 100 ? '...' : '');
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return 'Pattern documentation available';
|
|
133
|
+
} catch {
|
|
134
|
+
return 'No description available';
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Format pattern name from directory name
|
|
140
|
+
*/
|
|
141
|
+
formatName(dirName) {
|
|
142
|
+
return dirName
|
|
143
|
+
.split(/[-_]/)
|
|
144
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
145
|
+
.join(' ');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Save registry to disk
|
|
150
|
+
*/
|
|
151
|
+
async save() {
|
|
152
|
+
const data = {
|
|
153
|
+
version: '1.0.0',
|
|
154
|
+
lastUpdated: new Date().toISOString(),
|
|
155
|
+
patterns: Array.from(this.patterns.values())
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
writeFileSync(this.registryPath, JSON.stringify(data, null, 2));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Get a pattern by ID
|
|
163
|
+
* @param {string} id - Pattern ID (e.g., 'frontend/react')
|
|
164
|
+
* @returns {Pattern|null}
|
|
165
|
+
*/
|
|
166
|
+
get(id) {
|
|
167
|
+
return this.patterns.get(id) || null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Get all patterns
|
|
172
|
+
* @returns {Pattern[]}
|
|
173
|
+
*/
|
|
174
|
+
getAll() {
|
|
175
|
+
return Array.from(this.patterns.values());
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Get patterns by category
|
|
180
|
+
* @param {string} category - Category name
|
|
181
|
+
* @returns {Pattern[]}
|
|
182
|
+
*/
|
|
183
|
+
getByCategory(category) {
|
|
184
|
+
return this.getAll().filter(p => p.category === category);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Search patterns by name or description
|
|
189
|
+
* @param {string} query - Search query
|
|
190
|
+
* @returns {Pattern[]}
|
|
191
|
+
*/
|
|
192
|
+
search(query) {
|
|
193
|
+
const lowerQuery = query.toLowerCase();
|
|
194
|
+
return this.getAll().filter(p =>
|
|
195
|
+
p.name.toLowerCase().includes(lowerQuery) ||
|
|
196
|
+
p.description.toLowerCase().includes(lowerQuery) ||
|
|
197
|
+
p.id.toLowerCase().includes(lowerQuery)
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Register a new pattern
|
|
203
|
+
* @param {Pattern} pattern - Pattern to register
|
|
204
|
+
*/
|
|
205
|
+
async register(pattern) {
|
|
206
|
+
// Ensure required fields
|
|
207
|
+
if (!pattern.id || !pattern.name || !pattern.category) {
|
|
208
|
+
throw new Error('Pattern must have id, name, and category');
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Create pattern directory if it doesn't exist
|
|
212
|
+
const patternDir = join(this.patternsPath, pattern.path || pattern.id);
|
|
213
|
+
if (!existsSync(patternDir)) {
|
|
214
|
+
mkdirSync(patternDir, { recursive: true });
|
|
215
|
+
|
|
216
|
+
// Create README.md with description
|
|
217
|
+
const readmePath = join(patternDir, 'README.md');
|
|
218
|
+
writeFileSync(readmePath, `# ${pattern.name}\n\n${pattern.description || ''}\n`);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Set metadata
|
|
222
|
+
pattern.metadata = pattern.metadata || {
|
|
223
|
+
usageCount: 0,
|
|
224
|
+
lastUsed: null,
|
|
225
|
+
createdAt: new Date().toISOString()
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
this.patterns.set(pattern.id, pattern);
|
|
229
|
+
await this.save();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Remove a pattern
|
|
234
|
+
* @param {string} id - Pattern ID
|
|
235
|
+
*/
|
|
236
|
+
async remove(id) {
|
|
237
|
+
this.patterns.delete(id);
|
|
238
|
+
await this.save();
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Record pattern usage
|
|
243
|
+
* @param {string} id - Pattern ID
|
|
244
|
+
*/
|
|
245
|
+
async recordUsage(id) {
|
|
246
|
+
const pattern = this.patterns.get(id);
|
|
247
|
+
if (pattern) {
|
|
248
|
+
pattern.metadata.usageCount++;
|
|
249
|
+
pattern.metadata.lastUsed = new Date().toISOString();
|
|
250
|
+
await this.save();
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Get pattern content (README.md)
|
|
256
|
+
* @param {string} id - Pattern ID
|
|
257
|
+
* @returns {string|null}
|
|
258
|
+
*/
|
|
259
|
+
getContent(id) {
|
|
260
|
+
const pattern = this.patterns.get(id);
|
|
261
|
+
if (!pattern) return null;
|
|
262
|
+
|
|
263
|
+
// pattern.path is relative to aiPath (e.g., "patterns/frontend/react")
|
|
264
|
+
const readmePath = join(this.aiPath, pattern.path, 'README.md');
|
|
265
|
+
if (!existsSync(readmePath)) return null;
|
|
266
|
+
|
|
267
|
+
return readFileSync(readmePath, 'utf8');
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Get all files in a pattern directory
|
|
272
|
+
* @param {string} id - Pattern ID
|
|
273
|
+
* @returns {Object[]} Array of {name, path, content}
|
|
274
|
+
*/
|
|
275
|
+
getPatternFiles(id) {
|
|
276
|
+
const pattern = this.patterns.get(id);
|
|
277
|
+
if (!pattern) return [];
|
|
278
|
+
|
|
279
|
+
// pattern.path is relative to aiPath (e.g., "patterns/frontend/react")
|
|
280
|
+
const patternDir = join(this.aiPath, pattern.path);
|
|
281
|
+
if (!existsSync(patternDir)) return [];
|
|
282
|
+
|
|
283
|
+
const files = [];
|
|
284
|
+
const entries = readdirSync(patternDir, { withFileTypes: true });
|
|
285
|
+
|
|
286
|
+
for (const entry of entries) {
|
|
287
|
+
if (entry.isFile()) {
|
|
288
|
+
const filePath = join(patternDir, entry.name);
|
|
289
|
+
files.push({
|
|
290
|
+
name: entry.name,
|
|
291
|
+
path: relative(this.aiPath, filePath),
|
|
292
|
+
content: readFileSync(filePath, 'utf8')
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return files;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Get usage statistics
|
|
302
|
+
* @returns {Object}
|
|
303
|
+
*/
|
|
304
|
+
getStats() {
|
|
305
|
+
const patterns = this.getAll();
|
|
306
|
+
const byCategory = {};
|
|
307
|
+
let totalUsage = 0;
|
|
308
|
+
let mostUsed = null;
|
|
309
|
+
|
|
310
|
+
for (const pattern of patterns) {
|
|
311
|
+
// Count by category
|
|
312
|
+
byCategory[pattern.category] = (byCategory[pattern.category] || 0) + 1;
|
|
313
|
+
|
|
314
|
+
// Track total usage
|
|
315
|
+
totalUsage += pattern.metadata.usageCount;
|
|
316
|
+
|
|
317
|
+
// Track most used
|
|
318
|
+
if (!mostUsed || pattern.metadata.usageCount > mostUsed.metadata.usageCount) {
|
|
319
|
+
mostUsed = pattern;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return {
|
|
324
|
+
totalPatterns: patterns.length,
|
|
325
|
+
byCategory,
|
|
326
|
+
totalUsage,
|
|
327
|
+
mostUsed: mostUsed ? { id: mostUsed.id, count: mostUsed.metadata.usageCount } : null
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Associate trigger with pattern
|
|
333
|
+
* @param {string} patternId - Pattern ID
|
|
334
|
+
* @param {string} triggerId - Trigger ID
|
|
335
|
+
*/
|
|
336
|
+
async addTrigger(patternId, triggerId) {
|
|
337
|
+
const pattern = this.patterns.get(patternId);
|
|
338
|
+
if (pattern && !pattern.triggers.includes(triggerId)) {
|
|
339
|
+
pattern.triggers.push(triggerId);
|
|
340
|
+
await this.save();
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Remove trigger from pattern
|
|
346
|
+
* @param {string} patternId - Pattern ID
|
|
347
|
+
* @param {string} triggerId - Trigger ID
|
|
348
|
+
*/
|
|
349
|
+
async removeTrigger(patternId, triggerId) {
|
|
350
|
+
const pattern = this.patterns.get(patternId);
|
|
351
|
+
if (pattern) {
|
|
352
|
+
pattern.triggers = pattern.triggers.filter(t => t !== triggerId);
|
|
353
|
+
await this.save();
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Create a new PatternRegistry instance for the given workspace
|
|
360
|
+
* @param {string} workspacePath - Path to workspace root
|
|
361
|
+
* @returns {PatternRegistry}
|
|
362
|
+
*/
|
|
363
|
+
export function createRegistry(workspacePath) {
|
|
364
|
+
return new PatternRegistry(workspacePath);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Get pattern registry for current working directory
|
|
369
|
+
* @returns {PatternRegistry}
|
|
370
|
+
*/
|
|
371
|
+
export function getRegistry() {
|
|
372
|
+
return createRegistry(process.cwd());
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
export default { PatternRegistry, createRegistry, getRegistry };
|