@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
@@ -1,128 +1,208 @@
1
- import { existsSync, cpSync, renameSync, readFileSync, appendFileSync, writeFileSync } 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
- * Get the path to the workspace template
11
- * @returns {string} Path to template directory
12
- */
13
- export function getTemplatePath() {
14
- return join(__dirname, '../../templates/workspace/.ai');
15
- }
16
-
17
- /**
18
- * Inject PWN workspace into a project
19
- * @param {object} options - Injection options
20
- * @param {string} options.cwd - Target directory (defaults to process.cwd())
21
- * @param {boolean} options.force - Force overwrite existing .ai/ directory
22
- * @param {boolean} options.silent - Suppress console output
23
- * @returns {object} Result with success status and message
24
- */
25
- export async function inject(options = {}) {
26
- const {
27
- cwd = process.cwd(),
28
- force = false,
29
- silent = false
30
- } = options;
31
-
32
- const templateDir = getTemplatePath();
33
- const targetDir = join(cwd, '.ai');
34
-
35
- const log = silent ? () => {} : console.log;
36
-
37
- // Check if .ai/ already exists
38
- if (existsSync(targetDir) && !force) {
39
- return {
40
- success: false,
41
- error: 'ALREADY_EXISTS',
42
- message: '.ai/ directory already exists. Use --force to overwrite.'
43
- };
44
- }
45
-
46
- try {
47
- // Copy workspace template
48
- log('📦 Copying workspace template...');
49
- cpSync(templateDir, targetDir, { recursive: true });
50
-
51
- // Rename state.template.json → state.json
52
- const templateState = join(targetDir, 'state.template.json');
53
- const stateFile = join(targetDir, 'state.json');
54
-
55
- if (existsSync(templateState)) {
56
- renameSync(templateState, stateFile);
57
- }
58
-
59
- // Rename notifications.template.json → notifications.json and generate unique topic
60
- const templateNotify = join(targetDir, 'config', 'notifications.template.json');
61
- const notifyFile = join(targetDir, 'config', 'notifications.json');
62
-
63
- if (existsSync(templateNotify)) {
64
- renameSync(templateNotify, notifyFile);
65
- initNotifications(notifyFile);
66
- }
67
-
68
- // Initialize state.json with current user
69
- initState(cwd);
70
-
71
- // Update .gitignore
72
- updateGitignore(cwd, silent);
73
-
74
- return {
75
- success: true,
76
- message: 'PWN workspace injected successfully',
77
- path: targetDir
78
- };
79
-
80
- } catch (error) {
81
- return {
82
- success: false,
83
- error: 'INJECTION_FAILED',
84
- message: error.message
85
- };
86
- }
87
- }
88
-
89
- /**
90
- * Initialize notifications.json with unique topic
91
- * @param {string} notifyFile - Path to notifications.json
92
- */
93
- function initNotifications(notifyFile) {
94
- try {
95
- const content = readFileSync(notifyFile, 'utf8');
96
- const config = JSON.parse(content);
97
-
98
- // Generate unique topic ID
99
- const uniqueId = randomUUID().split('-')[0]; // First segment: 8 chars
100
- config.channels.ntfy.topic = `pwn-${uniqueId}`;
101
-
102
- writeFileSync(notifyFile, JSON.stringify(config, null, 2));
103
- } catch {
104
- // Ignore errors - notifications will use defaults
105
- }
106
- }
107
-
108
- /**
109
- * Update .gitignore to exclude PWN personal files
110
- * @param {string} cwd - Working directory
111
- * @param {boolean} silent - Suppress output
112
- */
113
- function updateGitignore(cwd, silent = false) {
114
- const gitignorePath = join(cwd, '.gitignore');
115
- let gitignoreContent = '';
116
-
117
- if (existsSync(gitignorePath)) {
118
- gitignoreContent = readFileSync(gitignorePath, 'utf8');
119
- }
120
-
121
- if (!gitignoreContent.includes('.ai/state.json')) {
122
- const pwnSection = '\n# PWN\n.ai/state.json\n.ai/config/notifications.json\n';
123
- appendFileSync(gitignorePath, pwnSection);
124
- if (!silent) {
125
- console.log('📝 Updated .gitignore');
126
- }
127
- }
128
- }
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
+ }