@fermindi/pwn-cli 0.1.1 → 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 -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/status.js +91 -91
  12. package/cli/validate.js +61 -61
  13. package/package.json +70 -70
  14. package/src/core/inject.js +208 -204
  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
@@ -1,204 +1,208 @@
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 } 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
+ try {
125
+ // Copy workspace template
126
+ log('📦 Copying workspace template...');
127
+ cpSync(templateDir, targetDir, { recursive: true });
128
+
129
+ // Rename state.template.json → state.json
130
+ const templateState = join(targetDir, 'state.template.json');
131
+ const stateFile = join(targetDir, 'state.json');
132
+
133
+ if (existsSync(templateState)) {
134
+ renameSync(templateState, stateFile);
135
+ }
136
+
137
+ // Rename notifications.template.json → notifications.json and generate unique topic
138
+ const templateNotify = join(targetDir, 'config', 'notifications.template.json');
139
+ const notifyFile = join(targetDir, 'config', 'notifications.json');
140
+
141
+ if (existsSync(templateNotify)) {
142
+ renameSync(templateNotify, notifyFile);
143
+ initNotifications(notifyFile);
144
+ }
145
+
146
+ // Initialize state.json with current user
147
+ initState(cwd);
148
+
149
+ // Update .gitignore
150
+ updateGitignore(cwd, silent);
151
+
152
+ return {
153
+ success: true,
154
+ message: 'PWN workspace injected successfully',
155
+ path: targetDir,
156
+ detected: detectedFiles
157
+ };
158
+
159
+ } catch (error) {
160
+ return {
161
+ success: false,
162
+ error: 'INJECTION_FAILED',
163
+ message: error.message,
164
+ detected: detectedFiles
165
+ };
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Initialize notifications.json with unique topic
171
+ * @param {string} notifyFile - Path to notifications.json
172
+ */
173
+ function initNotifications(notifyFile) {
174
+ try {
175
+ const content = readFileSync(notifyFile, 'utf8');
176
+ const config = JSON.parse(content);
177
+
178
+ // Generate unique topic ID
179
+ const uniqueId = randomUUID().split('-')[0]; // First segment: 8 chars
180
+ config.channels.ntfy.topic = `pwn-${uniqueId}`;
181
+
182
+ writeFileSync(notifyFile, JSON.stringify(config, null, 2));
183
+ } catch {
184
+ // Ignore errors - notifications will use defaults
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Update .gitignore to exclude PWN personal files
190
+ * @param {string} cwd - Working directory
191
+ * @param {boolean} silent - Suppress output
192
+ */
193
+ function updateGitignore(cwd, silent = false) {
194
+ const gitignorePath = join(cwd, '.gitignore');
195
+ let gitignoreContent = '';
196
+
197
+ if (existsSync(gitignorePath)) {
198
+ gitignoreContent = readFileSync(gitignorePath, 'utf8');
199
+ }
200
+
201
+ if (!gitignoreContent.includes('.ai/state.json')) {
202
+ const pwnSection = '\n# PWN\n.ai/state.json\n.ai/config/notifications.json\n';
203
+ appendFileSync(gitignorePath, pwnSection);
204
+ if (!silent) {
205
+ console.log('📝 Updated .gitignore');
206
+ }
207
+ }
208
+ }
package/src/core/state.js CHANGED
@@ -1,91 +1,91 @@
1
- import { readFileSync, writeFileSync, existsSync } from 'fs';
2
- import { join } from 'path';
3
- import { execSync } from 'child_process';
4
-
5
- /**
6
- * Get the path to state.json in a workspace
7
- * @param {string} cwd - Working directory (defaults to process.cwd())
8
- * @returns {string} Path to state.json
9
- */
10
- export function getStatePath(cwd = process.cwd()) {
11
- return join(cwd, '.ai', 'state.json');
12
- }
13
-
14
- /**
15
- * Check if a PWN workspace exists in the given directory
16
- * @param {string} cwd - Working directory
17
- * @returns {boolean}
18
- */
19
- export function hasWorkspace(cwd = process.cwd()) {
20
- return existsSync(join(cwd, '.ai'));
21
- }
22
-
23
- /**
24
- * Get the current git username
25
- * @returns {string} Git username or 'unknown'
26
- */
27
- export function getGitUser() {
28
- try {
29
- return execSync('git config user.name', { encoding: 'utf8' }).trim();
30
- } catch {
31
- return 'unknown';
32
- }
33
- }
34
-
35
- /**
36
- * Read the current state from state.json
37
- * @param {string} cwd - Working directory
38
- * @returns {object|null} State object or null if not found
39
- */
40
- export function getState(cwd = process.cwd()) {
41
- const statePath = getStatePath(cwd);
42
-
43
- if (!existsSync(statePath)) {
44
- return null;
45
- }
46
-
47
- try {
48
- const content = readFileSync(statePath, 'utf8');
49
- return JSON.parse(content);
50
- } catch {
51
- return null;
52
- }
53
- }
54
-
55
- /**
56
- * Update the state.json file
57
- * @param {object} updates - Fields to update
58
- * @param {string} cwd - Working directory
59
- * @returns {object} Updated state
60
- */
61
- export function updateState(updates, cwd = process.cwd()) {
62
- const statePath = getStatePath(cwd);
63
- const currentState = getState(cwd) || {};
64
-
65
- const newState = {
66
- ...currentState,
67
- ...updates,
68
- last_updated: new Date().toISOString()
69
- };
70
-
71
- writeFileSync(statePath, JSON.stringify(newState, null, 2));
72
- return newState;
73
- }
74
-
75
- /**
76
- * Initialize a new state.json file
77
- * @param {string} cwd - Working directory
78
- * @returns {object} Initial state
79
- */
80
- export function initState(cwd = process.cwd()) {
81
- const state = {
82
- developer: getGitUser(),
83
- session_started: new Date().toISOString(),
84
- current_task: null,
85
- context_loaded: []
86
- };
87
-
88
- const statePath = getStatePath(cwd);
89
- writeFileSync(statePath, JSON.stringify(state, null, 2));
90
- return state;
91
- }
1
+ import { readFileSync, writeFileSync, existsSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { execSync } from 'child_process';
4
+
5
+ /**
6
+ * Get the path to state.json in a workspace
7
+ * @param {string} cwd - Working directory (defaults to process.cwd())
8
+ * @returns {string} Path to state.json
9
+ */
10
+ export function getStatePath(cwd = process.cwd()) {
11
+ return join(cwd, '.ai', 'state.json');
12
+ }
13
+
14
+ /**
15
+ * Check if a PWN workspace exists in the given directory
16
+ * @param {string} cwd - Working directory
17
+ * @returns {boolean}
18
+ */
19
+ export function hasWorkspace(cwd = process.cwd()) {
20
+ return existsSync(join(cwd, '.ai'));
21
+ }
22
+
23
+ /**
24
+ * Get the current git username
25
+ * @returns {string} Git username or 'unknown'
26
+ */
27
+ export function getGitUser() {
28
+ try {
29
+ return execSync('git config user.name', { encoding: 'utf8' }).trim();
30
+ } catch {
31
+ return 'unknown';
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Read the current state from state.json
37
+ * @param {string} cwd - Working directory
38
+ * @returns {object|null} State object or null if not found
39
+ */
40
+ export function getState(cwd = process.cwd()) {
41
+ const statePath = getStatePath(cwd);
42
+
43
+ if (!existsSync(statePath)) {
44
+ return null;
45
+ }
46
+
47
+ try {
48
+ const content = readFileSync(statePath, 'utf8');
49
+ return JSON.parse(content);
50
+ } catch {
51
+ return null;
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Update the state.json file
57
+ * @param {object} updates - Fields to update
58
+ * @param {string} cwd - Working directory
59
+ * @returns {object} Updated state
60
+ */
61
+ export function updateState(updates, cwd = process.cwd()) {
62
+ const statePath = getStatePath(cwd);
63
+ const currentState = getState(cwd) || {};
64
+
65
+ const newState = {
66
+ ...currentState,
67
+ ...updates,
68
+ last_updated: new Date().toISOString()
69
+ };
70
+
71
+ writeFileSync(statePath, JSON.stringify(newState, null, 2));
72
+ return newState;
73
+ }
74
+
75
+ /**
76
+ * Initialize a new state.json file
77
+ * @param {string} cwd - Working directory
78
+ * @returns {object} Initial state
79
+ */
80
+ export function initState(cwd = process.cwd()) {
81
+ const state = {
82
+ developer: getGitUser(),
83
+ session_started: new Date().toISOString(),
84
+ current_task: null,
85
+ context_loaded: []
86
+ };
87
+
88
+ const statePath = getStatePath(cwd);
89
+ writeFileSync(statePath, JSON.stringify(state, null, 2));
90
+ return state;
91
+ }