@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.
Files changed (48) 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 +112 -91
  6. package/cli/inject.js +90 -67
  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/save.js +206 -0
  12. package/cli/status.js +91 -91
  13. package/cli/update.js +189 -0
  14. package/cli/validate.js +61 -61
  15. package/package.json +70 -70
  16. package/src/core/inject.js +300 -204
  17. package/src/core/state.js +91 -91
  18. package/src/core/validate.js +202 -202
  19. package/src/core/workspace.js +176 -176
  20. package/src/index.js +20 -20
  21. package/src/knowledge/gc.js +308 -308
  22. package/src/knowledge/lifecycle.js +401 -401
  23. package/src/knowledge/promote.js +364 -364
  24. package/src/knowledge/references.js +342 -342
  25. package/src/patterns/matcher.js +218 -218
  26. package/src/patterns/registry.js +375 -375
  27. package/src/patterns/triggers.js +423 -423
  28. package/src/services/batch-service.js +849 -849
  29. package/src/services/notification-service.js +342 -342
  30. package/templates/codespaces/devcontainer.json +52 -52
  31. package/templates/codespaces/setup.sh +70 -70
  32. package/templates/workspace/.ai/README.md +164 -164
  33. package/templates/workspace/.ai/agents/README.md +204 -204
  34. package/templates/workspace/.ai/agents/claude.md +625 -625
  35. package/templates/workspace/.ai/config/README.md +79 -79
  36. package/templates/workspace/.ai/config/notifications.template.json +20 -20
  37. package/templates/workspace/.ai/memory/deadends.md +79 -79
  38. package/templates/workspace/.ai/memory/decisions.md +58 -58
  39. package/templates/workspace/.ai/memory/patterns.md +65 -65
  40. package/templates/workspace/.ai/patterns/backend/README.md +126 -126
  41. package/templates/workspace/.ai/patterns/frontend/README.md +103 -103
  42. package/templates/workspace/.ai/patterns/index.md +256 -256
  43. package/templates/workspace/.ai/patterns/triggers.json +1087 -1087
  44. package/templates/workspace/.ai/patterns/universal/README.md +141 -141
  45. package/templates/workspace/.ai/state.template.json +8 -8
  46. package/templates/workspace/.ai/tasks/active.md +77 -77
  47. package/templates/workspace/.ai/tasks/backlog.md +95 -95
  48. package/templates/workspace/.ai/workflows/batch-task.md +356 -356
@@ -1,204 +1,300 @@
1
- import { existsSync, cpSync, renameSync, readFileSync, appendFileSync, writeFileSync, readdirSync } from 'fs';
2
- import { join, dirname } from 'path';
3
- import { fileURLToPath } from 'url';
4
- import { randomUUID } from 'crypto';
5
- import { initState } from './state.js';
6
-
7
- const __dirname = dirname(fileURLToPath(import.meta.url));
8
-
9
- /**
10
- * Known AI instruction files in the industry
11
- * PWN should detect these and warn/offer migration
12
- */
13
- export const KNOWN_AI_FILES = [
14
- // Claude Code
15
- { pattern: 'CLAUDE.md', type: 'claude', description: 'Claude Code instructions' },
16
- { pattern: 'claude.md', type: 'claude', description: 'Claude Code instructions' },
17
- { pattern: '.claude', type: 'claude', description: 'Claude config directory' },
18
-
19
- // Cursor
20
- { pattern: '.cursorrules', type: 'cursor', description: 'Cursor AI rules' },
21
- { pattern: '.cursorignore', type: 'cursor', description: 'Cursor ignore file' },
22
-
23
- // GitHub Copilot
24
- { pattern: '.github/copilot-instructions.md', type: 'copilot', description: 'GitHub Copilot instructions' },
25
-
26
- // Session files (common pattern)
27
- { pattern: 'session.*.md', type: 'session', description: 'Session file (legacy pattern)' },
28
-
29
- // Other AI tools
30
- { pattern: '.aider', type: 'aider', description: 'Aider AI config' },
31
- { pattern: '.continue', type: 'continue', description: 'Continue AI config' },
32
- { pattern: '.codeium', type: 'codeium', description: 'Codeium config' },
33
- ];
34
-
35
- /**
36
- * Detect known AI instruction files in a directory
37
- * @param {string} cwd - Directory to scan
38
- * @returns {Object[]} Array of detected files with metadata
39
- */
40
- export function detectKnownAIFiles(cwd = process.cwd()) {
41
- const detected = [];
42
-
43
- for (const known of KNOWN_AI_FILES) {
44
- // Handle glob patterns like session.*.md
45
- if (known.pattern.includes('*')) {
46
- const regex = new RegExp('^' + known.pattern.replace('.', '\\.').replace('*', '.*') + '$');
47
- try {
48
- const files = readdirSync(cwd);
49
- for (const file of files) {
50
- if (regex.test(file)) {
51
- detected.push({
52
- ...known,
53
- file,
54
- path: join(cwd, file)
55
- });
56
- }
57
- }
58
- } catch {
59
- // Ignore read errors
60
- }
61
- } else {
62
- // Direct file check
63
- const filePath = join(cwd, known.pattern);
64
- if (existsSync(filePath)) {
65
- detected.push({
66
- ...known,
67
- file: known.pattern,
68
- path: filePath
69
- });
70
- }
71
- }
72
- }
73
-
74
- return detected;
75
- }
76
-
77
- /**
78
- * Get the path to the workspace template
79
- * @returns {string} Path to template directory
80
- */
81
- export function getTemplatePath() {
82
- return join(__dirname, '../../templates/workspace/.ai');
83
- }
84
-
85
- /**
86
- * Inject PWN workspace into a project
87
- * @param {object} options - Injection options
88
- * @param {string} options.cwd - Target directory (defaults to process.cwd())
89
- * @param {boolean} options.force - Force overwrite existing .ai/ directory
90
- * @param {boolean} options.silent - Suppress console output
91
- * @param {boolean} options.skipDetection - Skip detection of known AI files
92
- * @returns {object} Result with success status and message
93
- */
94
- export async function inject(options = {}) {
95
- const {
96
- cwd = process.cwd(),
97
- force = false,
98
- silent = false,
99
- skipDetection = false
100
- } = options;
101
-
102
- const templateDir = getTemplatePath();
103
- const targetDir = join(cwd, '.ai');
104
-
105
- const log = silent ? () => {} : console.log;
106
-
107
- // Detect known AI instruction files
108
- const detectedFiles = skipDetection ? [] : detectKnownAIFiles(cwd);
109
-
110
- // Check if .ai/ already exists
111
- if (existsSync(targetDir) && !force) {
112
- return {
113
- success: false,
114
- error: 'ALREADY_EXISTS',
115
- message: '.ai/ directory already exists. Use --force to overwrite.',
116
- detected: detectedFiles
117
- };
118
- }
119
-
120
- try {
121
- // Copy workspace template
122
- log('📦 Copying workspace template...');
123
- cpSync(templateDir, targetDir, { recursive: true });
124
-
125
- // Rename state.template.json → state.json
126
- const templateState = join(targetDir, 'state.template.json');
127
- const stateFile = join(targetDir, 'state.json');
128
-
129
- if (existsSync(templateState)) {
130
- renameSync(templateState, stateFile);
131
- }
132
-
133
- // Rename notifications.template.json → notifications.json and generate unique topic
134
- const templateNotify = join(targetDir, 'config', 'notifications.template.json');
135
- const notifyFile = join(targetDir, 'config', 'notifications.json');
136
-
137
- if (existsSync(templateNotify)) {
138
- renameSync(templateNotify, notifyFile);
139
- initNotifications(notifyFile);
140
- }
141
-
142
- // Initialize state.json with current user
143
- initState(cwd);
144
-
145
- // Update .gitignore
146
- updateGitignore(cwd, silent);
147
-
148
- return {
149
- success: true,
150
- message: 'PWN workspace injected successfully',
151
- path: targetDir,
152
- detected: detectedFiles
153
- };
154
-
155
- } catch (error) {
156
- return {
157
- success: false,
158
- error: 'INJECTION_FAILED',
159
- message: error.message,
160
- detected: detectedFiles
161
- };
162
- }
163
- }
164
-
165
- /**
166
- * Initialize notifications.json with unique topic
167
- * @param {string} notifyFile - Path to notifications.json
168
- */
169
- function initNotifications(notifyFile) {
170
- try {
171
- const content = readFileSync(notifyFile, 'utf8');
172
- const config = JSON.parse(content);
173
-
174
- // Generate unique topic ID
175
- const uniqueId = randomUUID().split('-')[0]; // First segment: 8 chars
176
- config.channels.ntfy.topic = `pwn-${uniqueId}`;
177
-
178
- writeFileSync(notifyFile, JSON.stringify(config, null, 2));
179
- } catch {
180
- // Ignore errors - notifications will use defaults
181
- }
182
- }
183
-
184
- /**
185
- * Update .gitignore to exclude PWN personal files
186
- * @param {string} cwd - Working directory
187
- * @param {boolean} silent - Suppress output
188
- */
189
- function updateGitignore(cwd, silent = false) {
190
- const gitignorePath = join(cwd, '.gitignore');
191
- let gitignoreContent = '';
192
-
193
- if (existsSync(gitignorePath)) {
194
- gitignoreContent = readFileSync(gitignorePath, 'utf8');
195
- }
196
-
197
- if (!gitignoreContent.includes('.ai/state.json')) {
198
- const pwnSection = '\n# PWN\n.ai/state.json\n.ai/config/notifications.json\n';
199
- appendFileSync(gitignorePath, pwnSection);
200
- if (!silent) {
201
- console.log('📝 Updated .gitignore');
202
- }
203
- }
204
- }
1
+ import { existsSync, cpSync, renameSync, readFileSync, appendFileSync, writeFileSync, readdirSync, mkdirSync } from 'fs';
2
+ import { join, dirname } from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import { randomUUID } from 'crypto';
5
+ import { initState } from './state.js';
6
+
7
+ const __dirname = dirname(fileURLToPath(import.meta.url));
8
+
9
+ /**
10
+ * Known AI instruction files in the industry
11
+ * PWN should detect these and warn/offer migration
12
+ */
13
+ export const KNOWN_AI_FILES = [
14
+ // Claude Code
15
+ { pattern: 'CLAUDE.md', type: 'claude', description: 'Claude Code instructions', migratable: true },
16
+ { pattern: 'claude.md', type: 'claude', description: 'Claude Code instructions', migratable: true },
17
+ { pattern: '.claude', type: 'claude', description: 'Claude config directory', migratable: false },
18
+
19
+ // Cursor
20
+ { pattern: '.cursorrules', type: 'cursor', description: 'Cursor AI rules', migratable: true },
21
+ { pattern: '.cursor/rules', type: 'cursor', description: 'Cursor AI rules', migratable: true },
22
+ { pattern: '.cursorignore', type: 'cursor', description: 'Cursor ignore file', migratable: false },
23
+
24
+ // Cline memory-bank
25
+ { pattern: 'memory-bank', type: 'memory-bank', description: 'Cline memory bank', migratable: true },
26
+
27
+ // GitHub Copilot
28
+ { pattern: '.github/copilot-instructions.md', type: 'copilot', description: 'GitHub Copilot instructions', migratable: true },
29
+
30
+ // Session files (common pattern)
31
+ { pattern: 'session.*.md', type: 'session', description: 'Session file (legacy pattern)', migratable: false },
32
+
33
+ // Other AI tools
34
+ { pattern: '.aider', type: 'aider', description: 'Aider AI config', migratable: false },
35
+ { pattern: '.continue', type: 'continue', description: 'Continue AI config', migratable: false },
36
+ { pattern: '.codeium', type: 'codeium', description: 'Codeium config', migratable: false },
37
+ ];
38
+
39
+ /**
40
+ * Detect known AI instruction files in a directory
41
+ * @param {string} cwd - Directory to scan
42
+ * @returns {Object[]} Array of detected files with metadata
43
+ */
44
+ export function detectKnownAIFiles(cwd = process.cwd()) {
45
+ const detected = [];
46
+
47
+ for (const known of KNOWN_AI_FILES) {
48
+ // Handle glob patterns like session.*.md
49
+ if (known.pattern.includes('*')) {
50
+ const regex = new RegExp('^' + known.pattern.replace('.', '\\.').replace('*', '.*') + '$');
51
+ try {
52
+ const files = readdirSync(cwd);
53
+ for (const file of files) {
54
+ if (regex.test(file)) {
55
+ detected.push({
56
+ ...known,
57
+ file,
58
+ path: join(cwd, file)
59
+ });
60
+ }
61
+ }
62
+ } catch {
63
+ // Ignore read errors
64
+ }
65
+ } else {
66
+ // Direct file check
67
+ const filePath = join(cwd, known.pattern);
68
+ if (existsSync(filePath)) {
69
+ detected.push({
70
+ ...known,
71
+ file: known.pattern,
72
+ path: filePath
73
+ });
74
+ }
75
+ }
76
+ }
77
+
78
+ return detected;
79
+ }
80
+
81
+ /**
82
+ * Get the path to the workspace template
83
+ * @returns {string} Path to template directory
84
+ */
85
+ export function getTemplatePath() {
86
+ return join(__dirname, '../../templates/workspace/.ai');
87
+ }
88
+
89
+ /**
90
+ * Inject PWN workspace into a project
91
+ * @param {object} options - Injection options
92
+ * @param {string} options.cwd - Target directory (defaults to process.cwd())
93
+ * @param {boolean} options.force - Force overwrite existing .ai/ directory
94
+ * @param {boolean} options.silent - Suppress console output
95
+ * @param {boolean} options.skipDetection - Skip detection of known AI files
96
+ * @returns {object} Result with success status and message
97
+ */
98
+ export async function inject(options = {}) {
99
+ const {
100
+ cwd = process.cwd(),
101
+ force = false,
102
+ silent = false,
103
+ skipDetection = false
104
+ } = options;
105
+
106
+ const templateDir = getTemplatePath();
107
+ const targetDir = join(cwd, '.ai');
108
+
109
+ const log = silent ? () => {} : console.log;
110
+
111
+ // Detect known AI instruction files
112
+ const detectedFiles = skipDetection ? [] : detectKnownAIFiles(cwd);
113
+
114
+ // Check if .ai/ already exists
115
+ if (existsSync(targetDir) && !force) {
116
+ return {
117
+ success: false,
118
+ error: 'ALREADY_EXISTS',
119
+ message: '.ai/ directory already exists. Use --force to overwrite.',
120
+ detected: detectedFiles
121
+ };
122
+ }
123
+
124
+ // Backup existing AI files content before overwriting
125
+ const backedUpContent = {};
126
+ if (detectedFiles.length > 0 && detectedFiles.some(f => f.migratable)) {
127
+ log('📋 Backing up existing AI files...');
128
+ for (const file of detectedFiles.filter(f => f.migratable)) {
129
+ try {
130
+ backedUpContent[file.file] = {
131
+ content: readFileSync(file.path, 'utf8'),
132
+ type: file.type
133
+ };
134
+ } catch {
135
+ // Ignore read errors
136
+ }
137
+ }
138
+ }
139
+
140
+ try {
141
+ // Copy workspace template
142
+ log('📦 Copying workspace template...');
143
+ cpSync(templateDir, targetDir, { recursive: true });
144
+
145
+ // Rename state.template.json → state.json
146
+ const templateState = join(targetDir, 'state.template.json');
147
+ const stateFile = join(targetDir, 'state.json');
148
+
149
+ if (existsSync(templateState)) {
150
+ renameSync(templateState, stateFile);
151
+ }
152
+
153
+ // Rename notifications.template.json → notifications.json and generate unique topic
154
+ const templateNotify = join(targetDir, 'config', 'notifications.template.json');
155
+ const notifyFile = join(targetDir, 'config', 'notifications.json');
156
+
157
+ if (existsSync(templateNotify)) {
158
+ renameSync(templateNotify, notifyFile);
159
+ initNotifications(notifyFile);
160
+ }
161
+
162
+ // Initialize state.json with current user
163
+ initState(cwd);
164
+
165
+ // Update .gitignore
166
+ updateGitignore(cwd, silent);
167
+
168
+ // Handle CLAUDE.md: backup existing and copy PWN template to root
169
+ let backupInfo = { backed_up: [] };
170
+ const claudeMdPath = join(cwd, 'CLAUDE.md');
171
+ const backupClaudeMdPath = join(cwd, '~CLAUDE.md');
172
+ const templateClaudeMd = join(targetDir, 'agents', 'claude.md');
173
+
174
+ // Backup existing CLAUDE.md if present
175
+ if (backedUpContent['CLAUDE.md'] || backedUpContent['claude.md']) {
176
+ const originalName = backedUpContent['CLAUDE.md'] ? 'CLAUDE.md' : 'claude.md';
177
+ const originalPath = join(cwd, originalName);
178
+
179
+ if (existsSync(originalPath)) {
180
+ renameSync(originalPath, backupClaudeMdPath);
181
+ backupInfo.backed_up.push({ from: originalName, to: '~CLAUDE.md' });
182
+ if (!silent) {
183
+ console.log(`📦 Backed up ${originalName} → ~CLAUDE.md`);
184
+ }
185
+ }
186
+ }
187
+
188
+ // Copy PWN template to CLAUDE.md in root
189
+ if (existsSync(templateClaudeMd)) {
190
+ cpSync(templateClaudeMd, claudeMdPath);
191
+ if (!silent) {
192
+ console.log('📝 Created CLAUDE.md with PWN template');
193
+ }
194
+ }
195
+
196
+ // Backup other AI files (not CLAUDE.md) to .ai/
197
+ const otherFiles = Object.fromEntries(
198
+ Object.entries(backedUpContent).filter(([k]) => k.toLowerCase() !== 'claude.md')
199
+ );
200
+ if (Object.keys(otherFiles).length > 0) {
201
+ const otherBackups = backupExistingAIFiles(targetDir, otherFiles, silent);
202
+ backupInfo.backed_up.push(...otherBackups.backed_up);
203
+ }
204
+
205
+ return {
206
+ success: true,
207
+ message: 'PWN workspace injected successfully',
208
+ path: targetDir,
209
+ detected: detectedFiles,
210
+ backed_up: backupInfo.backed_up
211
+ };
212
+
213
+ } catch (error) {
214
+ return {
215
+ success: false,
216
+ error: 'INJECTION_FAILED',
217
+ message: error.message,
218
+ detected: detectedFiles
219
+ };
220
+ }
221
+ }
222
+
223
+ /**
224
+ * Initialize notifications.json with unique topic
225
+ * @param {string} notifyFile - Path to notifications.json
226
+ */
227
+ function initNotifications(notifyFile) {
228
+ try {
229
+ const content = readFileSync(notifyFile, 'utf8');
230
+ const config = JSON.parse(content);
231
+
232
+ // Generate unique topic ID
233
+ const uniqueId = randomUUID().split('-')[0]; // First segment: 8 chars
234
+ config.channels.ntfy.topic = `pwn-${uniqueId}`;
235
+
236
+ writeFileSync(notifyFile, JSON.stringify(config, null, 2));
237
+ } catch {
238
+ // Ignore errors - notifications will use defaults
239
+ }
240
+ }
241
+
242
+ /**
243
+ * Backup existing AI files to .ai/ root with ~ prefix
244
+ * - PWN template stays as the base (it's the correct one)
245
+ * - User's existing files are backed up for manual merge
246
+ * @param {string} targetDir - Path to .ai/ directory
247
+ * @param {Object} backedUpContent - Map of filename -> {content, type}
248
+ * @param {boolean} silent - Suppress output
249
+ * @returns {Object} Backup info
250
+ */
251
+ function backupExistingAIFiles(targetDir, backedUpContent, silent = false) {
252
+ const result = { backed_up: [] };
253
+
254
+ try {
255
+ // Backup all existing AI files with ~ prefix
256
+ for (const [filename, data] of Object.entries(backedUpContent)) {
257
+ // Convert filename to backup name: CLAUDE.md -> ~CLAUDE.md, .cursorrules -> ~cursorrules.md
258
+ const safeName = filename.replace(/^\./, '').replace(/[\/\\]/g, '-');
259
+ const backupName = `~${safeName}${safeName.endsWith('.md') ? '' : '.md'}`;
260
+ const backupPath = join(targetDir, backupName);
261
+
262
+ writeFileSync(backupPath, data.content);
263
+
264
+ result.backed_up.push({ from: filename, to: backupName });
265
+
266
+ if (!silent) {
267
+ console.log(`📦 Backed up ${filename} → .ai/${backupName}`);
268
+ }
269
+ }
270
+
271
+ } catch (error) {
272
+ if (!silent) {
273
+ console.error('⚠️ Backup warning:', error.message);
274
+ }
275
+ }
276
+
277
+ return result;
278
+ }
279
+
280
+ /**
281
+ * Update .gitignore to exclude PWN personal files
282
+ * @param {string} cwd - Working directory
283
+ * @param {boolean} silent - Suppress output
284
+ */
285
+ function updateGitignore(cwd, silent = false) {
286
+ const gitignorePath = join(cwd, '.gitignore');
287
+ let gitignoreContent = '';
288
+
289
+ if (existsSync(gitignorePath)) {
290
+ gitignoreContent = readFileSync(gitignorePath, 'utf8');
291
+ }
292
+
293
+ if (!gitignoreContent.includes('.ai/state.json')) {
294
+ const pwnSection = '\n# PWN\n.ai/state.json\n.ai/config/notifications.json\n';
295
+ appendFileSync(gitignorePath, pwnSection);
296
+ if (!silent) {
297
+ console.log('📝 Updated .gitignore');
298
+ }
299
+ }
300
+ }