@fermindi/pwn-cli 0.5.0 → 0.7.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.
@@ -1,8 +1,9 @@
1
1
  import { existsSync, cpSync, renameSync, readFileSync, appendFileSync, writeFileSync, readdirSync, mkdirSync } from 'fs';
2
2
  import { join, dirname } from 'path';
3
3
  import { fileURLToPath } from 'url';
4
- import { randomUUID } from 'crypto';
5
4
  import { initState } from './state.js';
5
+ import { convertBacklogToPrd, detectAvailableGates } from '../services/batch-service.js';
6
+ import { updateState } from './state.js';
6
7
 
7
8
  const __dirname = dirname(fileURLToPath(import.meta.url));
8
9
 
@@ -147,6 +148,20 @@ export async function inject(options = {}) {
147
148
  }
148
149
  }
149
150
 
151
+ // Migrate backlog.md → prd.json before overwriting (force path)
152
+ let migratedPrd = null;
153
+ if (force) {
154
+ const backlogPath = join(targetDir, 'tasks', 'backlog.md');
155
+ if (existsSync(backlogPath)) {
156
+ const backlogContent = readFileSync(backlogPath, 'utf8');
157
+ const prd = convertBacklogToPrd(backlogContent);
158
+ if (prd.stories.length > 0) {
159
+ migratedPrd = prd;
160
+ log(`šŸ”„ Detected backlog.md with ${prd.stories.length} stories, will convert to prd.json`);
161
+ }
162
+ }
163
+ }
164
+
150
165
  try {
151
166
  // Copy workspace template
152
167
  log('šŸ“¦ Copying workspace template...');
@@ -160,43 +175,51 @@ export async function inject(options = {}) {
160
175
  renameSync(templateState, stateFile);
161
176
  }
162
177
 
163
- // Rename notifications.template.json → notifications.json and generate unique topic
164
- const templateNotify = join(targetDir, 'config', 'notifications.template.json');
165
- const notifyFile = join(targetDir, 'config', 'notifications.json');
178
+ // Initialize state.json with current user
179
+ initState(cwd);
166
180
 
167
- if (existsSync(templateNotify)) {
168
- renameSync(templateNotify, notifyFile);
169
- initNotifications(notifyFile);
181
+ // Detect available quality gates and auto-configure skip_gates
182
+ const gates = detectAvailableGates(cwd);
183
+ log('\nāš™ļø Quality Gates Detection');
184
+ for (const gate of gates.available) {
185
+ log(` āœ… ${gate.padEnd(12)} ${gates.details[gate]}`);
170
186
  }
187
+ for (const gate of gates.missing) {
188
+ log(` āš ļø ${gate.padEnd(12)} not detected`);
189
+ }
190
+ if (gates.missing.length > 0) {
191
+ log(` → Auto-configured skip_gates: ${JSON.stringify(gates.missing)}`);
192
+ updateState({ batch_config: { skip_gates: gates.missing } }, cwd);
193
+ }
194
+ log('');
171
195
 
172
- // Initialize state.json with current user
173
- initState(cwd);
196
+ // Write migrated prd.json (from backlog.md) if available
197
+ if (migratedPrd) {
198
+ const prdPath = join(targetDir, 'tasks', 'prd.json');
199
+ writeFileSync(prdPath, JSON.stringify(migratedPrd, null, 2));
200
+ log(`šŸ“ Created prd.json from backlog.md (${migratedPrd.stories.length} stories)`);
201
+ }
174
202
 
175
203
  // Update .gitignore
176
204
  updateGitignore(cwd, silent);
177
205
 
178
- // Handle CLAUDE.md: backup existing (only if different) and copy PWN template to root
206
+ // Handle CLAUDE.md: backup existing and copy PWN template to root
179
207
  let backupInfo = { backed_up: [] };
180
208
  const claudeMdPath = join(cwd, 'CLAUDE.md');
181
209
  const backupClaudeMdPath = join(cwd, '~CLAUDE.md');
182
210
  const templateClaudeMd = join(targetDir, 'agents', 'claude.md');
183
- const templateContent = existsSync(templateClaudeMd) ? readFileSync(templateClaudeMd, 'utf8') : '';
184
211
 
185
- // Backup existing CLAUDE.md if present AND different from template
212
+ // Backup existing CLAUDE.md if present
186
213
  if (backedUpContent['CLAUDE.md'] || backedUpContent['claude.md']) {
187
214
  const originalName = backedUpContent['CLAUDE.md'] ? 'CLAUDE.md' : 'claude.md';
188
215
  const originalPath = join(cwd, originalName);
189
- const existingContent = backedUpContent[originalName]?.content || '';
190
216
 
191
- // Only backup if content is different from PWN template
192
- if (existsSync(originalPath) && existingContent !== templateContent) {
217
+ if (existsSync(originalPath)) {
193
218
  renameSync(originalPath, backupClaudeMdPath);
194
219
  backupInfo.backed_up.push({ from: originalName, to: '~CLAUDE.md' });
195
220
  if (!silent) {
196
221
  console.log(`šŸ“¦ Backed up ${originalName} → ~CLAUDE.md`);
197
222
  }
198
- } else if (existsSync(originalPath) && !silent) {
199
- console.log(`ā­ļø Skipped backup: ${originalName} is identical to PWN template`);
200
223
  }
201
224
  }
202
225
 
@@ -225,15 +248,6 @@ export async function inject(options = {}) {
225
248
  }
226
249
  }
227
250
 
228
- // Copy settings.json with hooks (if doesn't exist)
229
- const settingsSource = join(claudeTemplateDir, 'settings.json');
230
- const settingsTarget = join(claudeDir, 'settings.json');
231
- if (existsSync(settingsSource) && !existsSync(settingsTarget)) {
232
- cpSync(settingsSource, settingsTarget);
233
- if (!silent) {
234
- console.log('šŸ“ Created .claude/settings.json with notification hooks');
235
- }
236
- }
237
251
  }
238
252
 
239
253
  // Backup other AI files (not CLAUDE.md) to .ai/
@@ -263,24 +277,6 @@ export async function inject(options = {}) {
263
277
  }
264
278
  }
265
279
 
266
- /**
267
- * Initialize notifications.json with unique topic
268
- * @param {string} notifyFile - Path to notifications.json
269
- */
270
- function initNotifications(notifyFile) {
271
- try {
272
- const content = readFileSync(notifyFile, 'utf8');
273
- const config = JSON.parse(content);
274
-
275
- // Generate unique topic ID
276
- const uniqueId = randomUUID().split('-')[0]; // First segment: 8 chars
277
- config.channels.ntfy.topic = `pwn-${uniqueId}`;
278
-
279
- writeFileSync(notifyFile, JSON.stringify(config, null, 2));
280
- } catch {
281
- // Ignore errors - notifications will use defaults
282
- }
283
- }
284
280
 
285
281
  /**
286
282
  * Backup existing AI files to .ai/ root with ~ prefix
@@ -334,7 +330,7 @@ function updateGitignore(cwd, silent = false) {
334
330
  }
335
331
 
336
332
  if (!gitignoreContent.includes('.ai/state.json')) {
337
- const pwnSection = '\n# PWN\n.ai/state.json\n.ai/config/notifications.json\n';
333
+ const pwnSection = '\n# PWN\n.ai/state.json\n';
338
334
  appendFileSync(gitignorePath, pwnSection);
339
335
  if (!silent) {
340
336
  console.log('šŸ“ Updated .gitignore');
package/src/core/state.js CHANGED
@@ -95,7 +95,6 @@ export function initState(cwd = process.cwd()) {
95
95
  const state = {
96
96
  developer: getGitUser(),
97
97
  session_started: new Date().toISOString(),
98
- session_mode: 'interactive',
99
98
  current_task: null,
100
99
  context_loaded: [],
101
100
  batch_config: { ...DEFAULT_BATCH_CONFIG }
@@ -24,7 +24,11 @@ const REQUIRED_STRUCTURE = {
24
24
  ],
25
25
  taskFiles: [
26
26
  'tasks/active.md',
27
- 'tasks/backlog.md'
27
+ 'tasks/prd.json'
28
+ ],
29
+ batchFiles: [
30
+ 'batch/prompt.md',
31
+ 'batch/progress.txt'
28
32
  ],
29
33
  agentFiles: [
30
34
  'agents/README.md',
@@ -88,6 +92,14 @@ export function validate(cwd = process.cwd()) {
88
92
  }
89
93
  }
90
94
 
95
+ // Check batch files
96
+ for (const file of REQUIRED_STRUCTURE.batchFiles) {
97
+ const filePath = join(aiDir, file);
98
+ if (!existsSync(filePath)) {
99
+ warnings.push(`Missing batch file: .ai/${file}`);
100
+ }
101
+ }
102
+
91
103
  // Check agent files
92
104
  for (const file of REQUIRED_STRUCTURE.agentFiles) {
93
105
  const filePath = join(aiDir, file);
@@ -189,6 +201,7 @@ export function getStructureReport(cwd = process.cwd()) {
189
201
  ...REQUIRED_STRUCTURE.files,
190
202
  ...REQUIRED_STRUCTURE.memoryFiles,
191
203
  ...REQUIRED_STRUCTURE.taskFiles,
204
+ ...REQUIRED_STRUCTURE.batchFiles,
192
205
  ...REQUIRED_STRUCTURE.agentFiles,
193
206
  ...REQUIRED_STRUCTURE.patternFiles
194
207
  ];
@@ -39,11 +39,11 @@ export function getWorkspaceInfo(cwd = process.cwd()) {
39
39
  */
40
40
  function getTasksSummary(cwd) {
41
41
  const activePath = join(cwd, '.ai', 'tasks', 'active.md');
42
- const backlogPath = join(cwd, '.ai', 'tasks', 'backlog.md');
42
+ const prdPath = join(cwd, '.ai', 'tasks', 'prd.json');
43
43
 
44
44
  const summary = {
45
45
  active: { total: 0, completed: 0, pending: 0 },
46
- backlog: { total: 0 }
46
+ backlog: { total: 0, done: 0, pending: 0 }
47
47
  };
48
48
 
49
49
  // Parse active.md
@@ -63,15 +63,17 @@ function getTasksSummary(cwd) {
63
63
  }
64
64
  }
65
65
 
66
- // Parse backlog.md
67
- if (existsSync(backlogPath)) {
68
- const content = readFileSync(backlogPath, 'utf8');
69
- const lines = content.split('\n');
70
-
71
- for (const line of lines) {
72
- if (line.match(/^- \[[ ]\]/) || line.match(/^\d+\./)) {
73
- summary.backlog.total++;
74
- }
66
+ // Parse prd.json
67
+ if (existsSync(prdPath)) {
68
+ try {
69
+ const prd = JSON.parse(readFileSync(prdPath, 'utf8'));
70
+ summary.backlog = {
71
+ total: prd.stories.length,
72
+ done: prd.stories.filter(s => s.passes).length,
73
+ pending: prd.stories.filter(s => !s.passes).length
74
+ };
75
+ } catch {
76
+ // Invalid JSON, leave defaults
75
77
  }
76
78
  }
77
79
 
package/src/index.js CHANGED
@@ -5,7 +5,6 @@ export { validate } from './core/validate.js';
5
5
  export { getWorkspaceInfo } from './core/workspace.js';
6
6
 
7
7
  // Services
8
- export * as notifications from './services/notification-service.js';
9
8
  export * as batch from './services/batch-service.js';
10
9
 
11
10
  // Patterns