@fermindi/pwn-cli 0.1.0 → 0.2.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.
Files changed (46) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +265 -251
  3. package/cli/batch.js +333 -333
  4. package/cli/codespaces.js +303 -303
  5. package/cli/index.js +98 -91
  6. package/cli/inject.js +78 -53
  7. package/cli/knowledge.js +531 -531
  8. package/cli/migrate.js +466 -0
  9. package/cli/notify.js +135 -135
  10. package/cli/patterns.js +665 -665
  11. package/cli/status.js +91 -91
  12. package/cli/validate.js +61 -61
  13. package/package.json +70 -70
  14. package/src/core/inject.js +208 -128
  15. package/src/core/state.js +91 -91
  16. package/src/core/validate.js +202 -202
  17. package/src/core/workspace.js +176 -176
  18. package/src/index.js +20 -20
  19. package/src/knowledge/gc.js +308 -308
  20. package/src/knowledge/lifecycle.js +401 -401
  21. package/src/knowledge/promote.js +364 -364
  22. package/src/knowledge/references.js +342 -342
  23. package/src/patterns/matcher.js +218 -218
  24. package/src/patterns/registry.js +375 -375
  25. package/src/patterns/triggers.js +423 -423
  26. package/src/services/batch-service.js +849 -849
  27. package/src/services/notification-service.js +342 -342
  28. package/templates/codespaces/devcontainer.json +52 -52
  29. package/templates/codespaces/setup.sh +70 -70
  30. package/templates/workspace/.ai/README.md +164 -164
  31. package/templates/workspace/.ai/agents/README.md +204 -204
  32. package/templates/workspace/.ai/agents/claude.md +625 -625
  33. package/templates/workspace/.ai/config/README.md +79 -79
  34. package/templates/workspace/.ai/config/notifications.template.json +20 -20
  35. package/templates/workspace/.ai/memory/deadends.md +79 -79
  36. package/templates/workspace/.ai/memory/decisions.md +58 -58
  37. package/templates/workspace/.ai/memory/patterns.md +65 -65
  38. package/templates/workspace/.ai/patterns/backend/README.md +126 -126
  39. package/templates/workspace/.ai/patterns/frontend/README.md +103 -103
  40. package/templates/workspace/.ai/patterns/index.md +256 -256
  41. package/templates/workspace/.ai/patterns/triggers.json +1087 -1087
  42. package/templates/workspace/.ai/patterns/universal/README.md +141 -141
  43. package/templates/workspace/.ai/state.template.json +8 -8
  44. package/templates/workspace/.ai/tasks/active.md +77 -77
  45. package/templates/workspace/.ai/tasks/backlog.md +95 -95
  46. package/templates/workspace/.ai/workflows/batch-task.md +356 -356
package/cli/migrate.js ADDED
@@ -0,0 +1,466 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync, readFileSync, writeFileSync, readdirSync, statSync } from 'fs';
3
+ import { join } from 'path';
4
+ import { detectKnownAIFiles } from '../src/core/inject.js';
5
+
6
+ /**
7
+ * Migration strategies for different AI file types
8
+ */
9
+ const MIGRATION_STRATEGIES = {
10
+ 'claude': migrateClaude,
11
+ 'cursor': migrateCursor,
12
+ 'memory-bank': migrateMemoryBank,
13
+ 'copilot': migrateCopilot,
14
+ };
15
+
16
+ /**
17
+ * Parse markdown content to extract decisions, patterns, and tasks
18
+ */
19
+ function parseMarkdownContent(content) {
20
+ const result = {
21
+ decisions: [],
22
+ patterns: [],
23
+ deadends: [],
24
+ tasks: [],
25
+ instructions: [],
26
+ raw: content
27
+ };
28
+
29
+ const lines = content.split('\n');
30
+ let currentSection = null;
31
+ let currentItem = [];
32
+
33
+ for (const line of lines) {
34
+ // Detect section headers
35
+ const h2Match = line.match(/^##\s+(.+)/);
36
+ if (h2Match) {
37
+ // Save previous item
38
+ if (currentItem.length > 0) {
39
+ categorizeItem(currentItem.join('\n'), currentSection, result);
40
+ }
41
+ currentSection = h2Match[1].toLowerCase();
42
+ currentItem = [line];
43
+ continue;
44
+ }
45
+
46
+ if (currentSection) {
47
+ currentItem.push(line);
48
+ }
49
+ }
50
+
51
+ // Save last item
52
+ if (currentItem.length > 0) {
53
+ categorizeItem(currentItem.join('\n'), currentSection, result);
54
+ }
55
+
56
+ return result;
57
+ }
58
+
59
+ /**
60
+ * Categorize a parsed item into the appropriate bucket
61
+ */
62
+ function categorizeItem(content, sectionName, result) {
63
+ const lowerContent = content.toLowerCase();
64
+ const lowerSection = sectionName || '';
65
+
66
+ // Decision indicators
67
+ if (lowerSection.includes('decision') ||
68
+ lowerSection.includes('architecture') ||
69
+ lowerSection.includes('tech stack') ||
70
+ lowerContent.includes('we decided') ||
71
+ lowerContent.includes('decision:')) {
72
+ result.decisions.push(content);
73
+ }
74
+ // Dead-end indicators
75
+ else if (lowerSection.includes('avoid') ||
76
+ lowerSection.includes('don\'t') ||
77
+ lowerSection.includes('failed') ||
78
+ lowerSection.includes('dead') ||
79
+ lowerContent.includes('didn\'t work') ||
80
+ lowerContent.includes('don\'t use')) {
81
+ result.deadends.push(content);
82
+ }
83
+ // Task indicators
84
+ else if (lowerSection.includes('task') ||
85
+ lowerSection.includes('todo') ||
86
+ lowerSection.includes('backlog') ||
87
+ lowerSection.includes('current work') ||
88
+ content.includes('- [ ]') ||
89
+ content.includes('- [x]')) {
90
+ result.tasks.push(content);
91
+ }
92
+ // Pattern indicators
93
+ else if (lowerSection.includes('pattern') ||
94
+ lowerSection.includes('convention') ||
95
+ lowerSection.includes('style') ||
96
+ lowerSection.includes('rule')) {
97
+ result.patterns.push(content);
98
+ }
99
+ // Everything else goes to instructions
100
+ else {
101
+ result.instructions.push(content);
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Generate DEC-XXX ID based on existing count
107
+ */
108
+ function generateDecisionId(existingContent) {
109
+ const matches = existingContent.match(/DEC-(\d+)/g) || [];
110
+ const maxId = matches.reduce((max, match) => {
111
+ const num = parseInt(match.replace('DEC-', ''));
112
+ return num > max ? num : max;
113
+ }, 0);
114
+ return `DEC-${String(maxId + 1).padStart(3, '0')}`;
115
+ }
116
+
117
+ /**
118
+ * Generate DE-XXX ID based on existing count
119
+ */
120
+ function generateDeadendId(existingContent) {
121
+ const matches = existingContent.match(/DE-(\d+)/g) || [];
122
+ const maxId = matches.reduce((max, match) => {
123
+ const num = parseInt(match.replace('DE-', ''));
124
+ return num > max ? num : max;
125
+ }, 0);
126
+ return `DE-${String(maxId + 1).padStart(3, '0')}`;
127
+ }
128
+
129
+ /**
130
+ * Migrate CLAUDE.md or claude.md
131
+ */
132
+ function migrateClaude(filePath, cwd) {
133
+ const content = readFileSync(filePath, 'utf8');
134
+ const parsed = parseMarkdownContent(content);
135
+
136
+ return {
137
+ source: filePath,
138
+ type: 'claude',
139
+ parsed,
140
+ actions: generateMigrationActions(parsed, cwd)
141
+ };
142
+ }
143
+
144
+ /**
145
+ * Migrate .cursorrules or .cursor/rules
146
+ */
147
+ function migrateCursor(filePath, cwd) {
148
+ const content = readFileSync(filePath, 'utf8');
149
+ const parsed = parseMarkdownContent(content);
150
+
151
+ return {
152
+ source: filePath,
153
+ type: 'cursor',
154
+ parsed,
155
+ actions: generateMigrationActions(parsed, cwd)
156
+ };
157
+ }
158
+
159
+ /**
160
+ * Migrate memory-bank/ directory
161
+ */
162
+ function migrateMemoryBank(dirPath, cwd) {
163
+ const parsed = {
164
+ decisions: [],
165
+ patterns: [],
166
+ deadends: [],
167
+ tasks: [],
168
+ instructions: [],
169
+ raw: ''
170
+ };
171
+
172
+ const fileMapping = {
173
+ 'projectbrief.md': 'instructions',
174
+ 'productContext.md': 'decisions',
175
+ 'systemPatterns.md': 'patterns',
176
+ 'techContext.md': 'decisions',
177
+ 'activeContext.md': 'tasks',
178
+ 'progress.md': 'tasks',
179
+ };
180
+
181
+ try {
182
+ const files = readdirSync(dirPath);
183
+ for (const file of files) {
184
+ const filePath = join(dirPath, file);
185
+ if (!statSync(filePath).isFile()) continue;
186
+
187
+ const content = readFileSync(filePath, 'utf8');
188
+ const category = fileMapping[file] || 'instructions';
189
+
190
+ if (category === 'decisions') {
191
+ parsed.decisions.push(`<!-- From ${file} -->\n${content}`);
192
+ } else if (category === 'patterns') {
193
+ parsed.patterns.push(`<!-- From ${file} -->\n${content}`);
194
+ } else if (category === 'tasks') {
195
+ parsed.tasks.push(`<!-- From ${file} -->\n${content}`);
196
+ } else {
197
+ parsed.instructions.push(`<!-- From ${file} -->\n${content}`);
198
+ }
199
+ }
200
+ } catch (err) {
201
+ console.error(`Error reading memory-bank: ${err.message}`);
202
+ }
203
+
204
+ return {
205
+ source: dirPath,
206
+ type: 'memory-bank',
207
+ parsed,
208
+ actions: generateMigrationActions(parsed, cwd)
209
+ };
210
+ }
211
+
212
+ /**
213
+ * Migrate GitHub Copilot instructions
214
+ */
215
+ function migrateCopilot(filePath, cwd) {
216
+ const content = readFileSync(filePath, 'utf8');
217
+ const parsed = parseMarkdownContent(content);
218
+
219
+ return {
220
+ source: filePath,
221
+ type: 'copilot',
222
+ parsed,
223
+ actions: generateMigrationActions(parsed, cwd)
224
+ };
225
+ }
226
+
227
+ /**
228
+ * Generate migration actions based on parsed content
229
+ */
230
+ function generateMigrationActions(parsed, cwd) {
231
+ const actions = [];
232
+ const today = new Date().toISOString().split('T')[0];
233
+
234
+ // Decisions
235
+ if (parsed.decisions.length > 0) {
236
+ const targetPath = join(cwd, '.ai', 'memory', 'decisions.md');
237
+ let existingContent = '';
238
+ if (existsSync(targetPath)) {
239
+ existingContent = readFileSync(targetPath, 'utf8');
240
+ }
241
+
242
+ let newContent = '';
243
+ for (const decision of parsed.decisions) {
244
+ const id = generateDecisionId(existingContent + newContent);
245
+ newContent += `\n## ${id}: Migrated Decision\n`;
246
+ newContent += `**Date:** ${today}\n`;
247
+ newContent += `**Status:** Active\n`;
248
+ newContent += `**Source:** Migration\n\n`;
249
+ newContent += decision.replace(/^##\s+.+\n/, '') + '\n';
250
+ }
251
+
252
+ actions.push({
253
+ type: 'append',
254
+ target: targetPath,
255
+ content: newContent,
256
+ description: `Add ${parsed.decisions.length} decision(s) to decisions.md`
257
+ });
258
+ }
259
+
260
+ // Patterns
261
+ if (parsed.patterns.length > 0) {
262
+ const targetPath = join(cwd, '.ai', 'memory', 'patterns.md');
263
+ let newContent = '\n## Migrated Patterns\n\n';
264
+ for (const pattern of parsed.patterns) {
265
+ newContent += pattern.replace(/^##\s+.+\n/, '') + '\n\n';
266
+ }
267
+
268
+ actions.push({
269
+ type: 'append',
270
+ target: targetPath,
271
+ content: newContent,
272
+ description: `Add ${parsed.patterns.length} pattern(s) to patterns.md`
273
+ });
274
+ }
275
+
276
+ // Dead-ends
277
+ if (parsed.deadends.length > 0) {
278
+ const targetPath = join(cwd, '.ai', 'memory', 'deadends.md');
279
+ let existingContent = '';
280
+ if (existsSync(targetPath)) {
281
+ existingContent = readFileSync(targetPath, 'utf8');
282
+ }
283
+
284
+ let newContent = '';
285
+ for (const deadend of parsed.deadends) {
286
+ const id = generateDeadendId(existingContent + newContent);
287
+ newContent += `\n## ${id}: Migrated Dead-end\n`;
288
+ newContent += `**Date:** ${today}\n`;
289
+ newContent += `**Source:** Migration\n\n`;
290
+ newContent += deadend.replace(/^##\s+.+\n/, '') + '\n';
291
+ }
292
+
293
+ actions.push({
294
+ type: 'append',
295
+ target: targetPath,
296
+ content: newContent,
297
+ description: `Add ${parsed.deadends.length} dead-end(s) to deadends.md`
298
+ });
299
+ }
300
+
301
+ // Tasks
302
+ if (parsed.tasks.length > 0) {
303
+ const targetPath = join(cwd, '.ai', 'tasks', 'backlog.md');
304
+ let newContent = '\n## Migrated Tasks\n\n';
305
+ for (const task of parsed.tasks) {
306
+ newContent += task.replace(/^##\s+.+\n/, '') + '\n';
307
+ }
308
+
309
+ actions.push({
310
+ type: 'append',
311
+ target: targetPath,
312
+ content: newContent,
313
+ description: `Add ${parsed.tasks.length} task section(s) to backlog.md`
314
+ });
315
+ }
316
+
317
+ // Instructions (append to claude.md)
318
+ if (parsed.instructions.length > 0) {
319
+ const targetPath = join(cwd, '.ai', 'agents', 'claude.md');
320
+ let newContent = '\n\n---\n\n## Migrated Instructions\n\n';
321
+ for (const instruction of parsed.instructions) {
322
+ newContent += instruction + '\n\n';
323
+ }
324
+
325
+ actions.push({
326
+ type: 'append',
327
+ target: targetPath,
328
+ content: newContent,
329
+ description: `Add ${parsed.instructions.length} instruction section(s) to claude.md`
330
+ });
331
+ }
332
+
333
+ return actions;
334
+ }
335
+
336
+ /**
337
+ * Execute migration actions
338
+ */
339
+ function executeMigrationActions(actions, dryRun = false) {
340
+ const results = [];
341
+
342
+ for (const action of actions) {
343
+ if (dryRun) {
344
+ results.push({ ...action, status: 'would-execute' });
345
+ continue;
346
+ }
347
+
348
+ try {
349
+ if (action.type === 'append') {
350
+ let existing = '';
351
+ if (existsSync(action.target)) {
352
+ existing = readFileSync(action.target, 'utf8');
353
+ }
354
+ writeFileSync(action.target, existing + action.content);
355
+ results.push({ ...action, status: 'success' });
356
+ }
357
+ } catch (err) {
358
+ results.push({ ...action, status: 'error', error: err.message });
359
+ }
360
+ }
361
+
362
+ return results;
363
+ }
364
+
365
+ /**
366
+ * Main migrate command
367
+ */
368
+ export default async function migrateCommand(args = []) {
369
+ const dryRun = args.includes('--dry-run');
370
+ const cwd = process.cwd();
371
+
372
+ console.log('šŸ”„ PWN Migration\n');
373
+
374
+ // Check if .ai/ exists
375
+ if (!existsSync(join(cwd, '.ai'))) {
376
+ console.log('āŒ No .ai/ directory found.');
377
+ console.log(' Run "pwn inject" first to create the PWN workspace.\n');
378
+ process.exit(1);
379
+ }
380
+
381
+ // Detect migratable files
382
+ const detected = detectKnownAIFiles(cwd);
383
+ const migratable = detected.filter(f => f.migratable);
384
+
385
+ if (migratable.length === 0) {
386
+ console.log('āœ… No migratable AI files detected.\n');
387
+ console.log('Supported sources:');
388
+ console.log(' - CLAUDE.md / claude.md');
389
+ console.log(' - .cursorrules / .cursor/rules');
390
+ console.log(' - memory-bank/');
391
+ console.log(' - .github/copilot-instructions.md\n');
392
+ process.exit(0);
393
+ }
394
+
395
+ console.log(`šŸ“„ Found ${migratable.length} migratable source(s):\n`);
396
+ for (const file of migratable) {
397
+ console.log(` ${file.file} (${file.type})`);
398
+ }
399
+ console.log('');
400
+
401
+ // Process each file
402
+ const allActions = [];
403
+
404
+ for (const file of migratable) {
405
+ const strategy = MIGRATION_STRATEGIES[file.type];
406
+ if (!strategy) {
407
+ console.log(` āš ļø No migration strategy for ${file.type}`);
408
+ continue;
409
+ }
410
+
411
+ console.log(`šŸ“¦ Analyzing ${file.file}...`);
412
+ const result = strategy(file.path, cwd);
413
+
414
+ console.log(` Found: ${result.parsed.decisions.length} decisions, ${result.parsed.patterns.length} patterns, ${result.parsed.deadends.length} dead-ends, ${result.parsed.tasks.length} tasks, ${result.parsed.instructions.length} instructions`);
415
+
416
+ allActions.push(...result.actions);
417
+ }
418
+
419
+ if (allActions.length === 0) {
420
+ console.log('\nāœ… Nothing to migrate (content may already be structured).\n');
421
+ process.exit(0);
422
+ }
423
+
424
+ console.log(`\nšŸ“‹ Migration plan (${allActions.length} actions):\n`);
425
+ for (const action of allActions) {
426
+ const targetShort = action.target.replace(cwd, '.');
427
+ console.log(` ${action.type}: ${targetShort}`);
428
+ console.log(` → ${action.description}`);
429
+ }
430
+
431
+ if (dryRun) {
432
+ console.log('\nšŸ” Dry run - no changes made.');
433
+ console.log(' Remove --dry-run to execute migration.\n');
434
+ process.exit(0);
435
+ }
436
+
437
+ console.log('\nšŸš€ Executing migration...\n');
438
+ const results = executeMigrationActions(allActions);
439
+
440
+ let successCount = 0;
441
+ let errorCount = 0;
442
+
443
+ for (const result of results) {
444
+ const targetShort = result.target.replace(cwd, '.');
445
+ if (result.status === 'success') {
446
+ console.log(` āœ… ${targetShort}`);
447
+ successCount++;
448
+ } else {
449
+ console.log(` āŒ ${targetShort}: ${result.error}`);
450
+ errorCount++;
451
+ }
452
+ }
453
+
454
+ console.log(`\nšŸ“Š Migration complete: ${successCount} succeeded, ${errorCount} failed\n`);
455
+
456
+ if (successCount > 0) {
457
+ console.log('šŸ’” Next steps:');
458
+ console.log(' 1. Review migrated content in .ai/memory/');
459
+ console.log(' 2. Adjust DEC-XXX and DE-XXX entries as needed');
460
+ console.log(' 3. Backup and remove original files:\n');
461
+ for (const file of migratable) {
462
+ console.log(` mv ${file.file} .ai-migration-backup/`);
463
+ }
464
+ console.log('');
465
+ }
466
+ }