@fermindi/pwn-cli 0.6.0 ā 0.8.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.
- package/cli/backlog.js +73 -0
- package/cli/batch.js +106 -14
- package/cli/index.js +7 -29
- package/cli/inject.js +8 -33
- package/cli/update.js +31 -24
- package/package.json +6 -3
- package/src/core/inject.js +18 -39
- package/src/core/state.js +0 -1
- package/src/core/validate.js +1 -3
- package/src/index.js +0 -1
- package/src/services/batch-runner.js +860 -0
- package/src/services/batch-service.js +115 -21
- package/src/ui/backlog-viewer.js +394 -0
- package/templates/workspace/.ai/README.md +20 -0
- package/templates/workspace/.ai/batch/prompt.md +36 -0
- package/templates/workspace/.ai/batch/tasks/.gitkeep +0 -0
- package/cli/codespaces.js +0 -303
- package/cli/migrate.js +0 -466
- package/cli/mode.js +0 -206
- package/cli/notify.js +0 -135
- package/src/services/notification-service.js +0 -342
- package/templates/codespaces/devcontainer.json +0 -52
- package/templates/codespaces/setup.sh +0 -70
- package/templates/workspace/.ai/config/notifications.template.json +0 -20
- package/templates/workspace/.claude/commands/mode.md +0 -104
- package/templates/workspace/.claude/settings.json +0 -24
package/cli/backlog.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { hasWorkspace } from '../src/core/state.js';
|
|
3
|
+
import { parsePrdTasks } from '../src/services/batch-service.js';
|
|
4
|
+
import { listTaskFiles } from '../src/services/batch-runner.js';
|
|
5
|
+
import { startViewer, printPlain } from '../src/ui/backlog-viewer.js';
|
|
6
|
+
import { existsSync, readFileSync } from 'fs';
|
|
7
|
+
import { join } from 'path';
|
|
8
|
+
|
|
9
|
+
export default async function backlogCommand(args = []) {
|
|
10
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
11
|
+
showHelp();
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (!hasWorkspace()) {
|
|
16
|
+
console.log('ā No PWN workspace found\n');
|
|
17
|
+
console.log(' Run: pwn inject');
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const cwd = process.cwd();
|
|
22
|
+
let stories = parsePrdTasks(cwd);
|
|
23
|
+
const taskFiles = listTaskFiles(cwd);
|
|
24
|
+
|
|
25
|
+
// Parse --filter
|
|
26
|
+
const filterIdx = args.findIndex(a => a === '--filter');
|
|
27
|
+
const filter = filterIdx !== -1 ? args[filterIdx + 1] : null;
|
|
28
|
+
if (filter) {
|
|
29
|
+
const re = new RegExp(filter, 'i');
|
|
30
|
+
stories = stories.filter(s => re.test(s.id) || re.test(s.title));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Read project name from prd.json
|
|
34
|
+
let project = 'project';
|
|
35
|
+
const prdPath = join(cwd, '.ai', 'tasks', 'prd.json');
|
|
36
|
+
if (existsSync(prdPath)) {
|
|
37
|
+
try {
|
|
38
|
+
const prd = JSON.parse(readFileSync(prdPath, 'utf8'));
|
|
39
|
+
project = prd.project || project;
|
|
40
|
+
} catch {}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const noInteractive = args.includes('--no-interactive') || !process.stdout.isTTY;
|
|
44
|
+
const label = filter ? `${project} (filter: ${filter})` : project;
|
|
45
|
+
|
|
46
|
+
if (noInteractive) {
|
|
47
|
+
printPlain({ project: label, stories, taskFiles });
|
|
48
|
+
} else {
|
|
49
|
+
await startViewer({ project: label, stories, taskFiles });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function showHelp() {
|
|
54
|
+
console.log('š PWN Backlog Viewer\n');
|
|
55
|
+
console.log('Usage: pwn backlog [options]\n');
|
|
56
|
+
console.log('Options:');
|
|
57
|
+
console.log(' --filter <pattern> Filter stories by ID or title (regex, case-insensitive)');
|
|
58
|
+
console.log(' --no-interactive Plain text output (for CI/piping)');
|
|
59
|
+
console.log(' --help, -h Show this help\n');
|
|
60
|
+
console.log('Examples:');
|
|
61
|
+
console.log(' pwn backlog --filter SEC # Only SEC-* stories');
|
|
62
|
+
console.log(' pwn backlog --filter "API|AUTH" # Stories matching API or AUTH\n');
|
|
63
|
+
console.log('Keybindings (list view):');
|
|
64
|
+
console.log(' ā/k Move up');
|
|
65
|
+
console.log(' ā/j Move down');
|
|
66
|
+
console.log(' Enter Open full detail view');
|
|
67
|
+
console.log(' Home/End First/last story');
|
|
68
|
+
console.log(' q/Ctrl+C Quit\n');
|
|
69
|
+
console.log('Keybindings (detail view):');
|
|
70
|
+
console.log(' ā/k ā/j Scroll content');
|
|
71
|
+
console.log(' ā/ā Previous/next story');
|
|
72
|
+
console.log(' Esc/Bksp Back to list');
|
|
73
|
+
}
|
package/cli/batch.js
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { execSync } from 'child_process';
|
|
3
|
-
import { join } from 'path';
|
|
4
2
|
import * as batch from '../src/services/batch-service.js';
|
|
5
3
|
import { hasWorkspace } from '../src/core/state.js';
|
|
6
4
|
|
|
@@ -29,16 +27,14 @@ export default async function batchCommand(args = []) {
|
|
|
29
27
|
return showConfig();
|
|
30
28
|
}
|
|
31
29
|
|
|
30
|
+
if (subcommand === 'tasks') {
|
|
31
|
+
return showTasks(args.slice(1));
|
|
32
|
+
}
|
|
33
|
+
|
|
32
34
|
if (subcommand === 'run') {
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
cwd: process.cwd(),
|
|
37
|
-
stdio: 'inherit'
|
|
38
|
-
});
|
|
39
|
-
} catch (error) {
|
|
40
|
-
process.exit(error.status || 1);
|
|
41
|
-
}
|
|
35
|
+
const runArgs = parseRunOptions(args.slice(1));
|
|
36
|
+
const { runBatch } = await import('../src/services/batch-runner.js');
|
|
37
|
+
await runBatch(runArgs, process.cwd());
|
|
42
38
|
return;
|
|
43
39
|
}
|
|
44
40
|
|
|
@@ -94,6 +90,33 @@ function parseOptions(args) {
|
|
|
94
90
|
return options;
|
|
95
91
|
}
|
|
96
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Parse options for `pwn batch run`
|
|
95
|
+
*/
|
|
96
|
+
function parseRunOptions(args) {
|
|
97
|
+
const options = {};
|
|
98
|
+
|
|
99
|
+
for (let i = 0; i < args.length; i++) {
|
|
100
|
+
const arg = args[i];
|
|
101
|
+
|
|
102
|
+
if (arg === '--dry-run') {
|
|
103
|
+
options.dryRun = true;
|
|
104
|
+
} else if (arg === '--phase') {
|
|
105
|
+
options.phase = args[++i];
|
|
106
|
+
} else if (arg === '--filter') {
|
|
107
|
+
options.filter = args[++i];
|
|
108
|
+
} else if (arg === '--no-plan') {
|
|
109
|
+
options.noPlan = true;
|
|
110
|
+
} else if (arg === '--rate-limit-wait') {
|
|
111
|
+
options.rateLimitWait = parseInt(args[++i], 10);
|
|
112
|
+
} else if (/^\d+$/.test(arg)) {
|
|
113
|
+
options.maxIterations = parseInt(arg, 10);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return options;
|
|
118
|
+
}
|
|
119
|
+
|
|
97
120
|
/**
|
|
98
121
|
* Show batch status
|
|
99
122
|
*/
|
|
@@ -167,8 +190,6 @@ function showConfig() {
|
|
|
167
190
|
console.log(` Create PR: ${config.create_pr ? 'Yes' : 'No'}`);
|
|
168
191
|
console.log(` Branch Format: ${config.branch_format}`);
|
|
169
192
|
console.log(` Commit Format: ${config.commit_format}`);
|
|
170
|
-
console.log(`\n Notify Complete: ${config.notify_on_complete ? 'Yes' : 'No'}`);
|
|
171
|
-
console.log(` Notify Error: ${config.notify_on_error ? 'Yes' : 'No'}`);
|
|
172
193
|
console.log('\nš Config location: .ai/state.json (batch_config)');
|
|
173
194
|
}
|
|
174
195
|
|
|
@@ -308,6 +329,67 @@ async function resumeBatch(options) {
|
|
|
308
329
|
console.log(` Completed: ${result.completed?.length || 0} tasks`);
|
|
309
330
|
}
|
|
310
331
|
|
|
332
|
+
/**
|
|
333
|
+
* Show task files from .ai/batch/tasks/
|
|
334
|
+
*/
|
|
335
|
+
async function showTasks(args) {
|
|
336
|
+
const { listTaskFiles, deleteTaskFile } = await import('../src/services/batch-runner.js');
|
|
337
|
+
const cwd = process.cwd();
|
|
338
|
+
const failedOnly = args.includes('--failed');
|
|
339
|
+
const cleanMode = args.includes('--clean');
|
|
340
|
+
|
|
341
|
+
if (cleanMode) {
|
|
342
|
+
const completed = listTaskFiles(cwd, { statusFilter: 'completed' });
|
|
343
|
+
if (completed.length === 0) {
|
|
344
|
+
console.log('No completed task files to clean.');
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
let cleaned = 0;
|
|
348
|
+
for (const task of completed) {
|
|
349
|
+
if (deleteTaskFile(task.id, cwd)) cleaned++;
|
|
350
|
+
}
|
|
351
|
+
console.log(`Cleaned ${cleaned} completed task file(s).`);
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const filter = failedOnly ? { statusFilter: 'failed' } : {};
|
|
356
|
+
const tasks = listTaskFiles(cwd, filter);
|
|
357
|
+
|
|
358
|
+
if (tasks.length === 0) {
|
|
359
|
+
console.log(failedOnly ? 'No failed tasks.' : 'No task files found.');
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const title = failedOnly ? 'Failed Tasks' : 'Batch Task Files';
|
|
364
|
+
console.log(`\n${title}\n`);
|
|
365
|
+
|
|
366
|
+
for (const task of tasks) {
|
|
367
|
+
const statusColor = task.status === 'completed' ? 'green'
|
|
368
|
+
: task.status === 'failed' ? 'red'
|
|
369
|
+
: 'yellow';
|
|
370
|
+
const statusLabel = chalk[statusColor](task.status.toUpperCase());
|
|
371
|
+
const complexity = task.complexity ? ` [${task.complexity}]` : '';
|
|
372
|
+
const estimate = task.estimated_time_seconds
|
|
373
|
+
? ` ~${Math.round(task.estimated_time_seconds)}s`
|
|
374
|
+
: '';
|
|
375
|
+
|
|
376
|
+
console.log(` ${task.id}: ${task.title}`);
|
|
377
|
+
console.log(` Status: ${statusLabel}${complexity}${estimate}`);
|
|
378
|
+
|
|
379
|
+
if (task.status === 'failed' && task.failure_reason) {
|
|
380
|
+
const reason = task.failure_reason.length > 120
|
|
381
|
+
? task.failure_reason.slice(0, 120) + '...'
|
|
382
|
+
: task.failure_reason;
|
|
383
|
+
console.log(` Reason: ${chalk.dim(reason)}`);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (task.plan && task.plan.length > 0 && task.plan[0] !== 'fallback - no plan available') {
|
|
387
|
+
console.log(` Plan: ${chalk.dim(task.plan.join(' ā '))}`);
|
|
388
|
+
}
|
|
389
|
+
console.log('');
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
311
393
|
/**
|
|
312
394
|
* Show help
|
|
313
395
|
*/
|
|
@@ -316,7 +398,8 @@ function showHelp() {
|
|
|
316
398
|
console.log('Usage: pwn batch [command] [options]\n');
|
|
317
399
|
console.log('Commands:');
|
|
318
400
|
console.log(' (default) Execute next available task(s)');
|
|
319
|
-
console.log(' run Run
|
|
401
|
+
console.log(' run Run autonomous batch loop (Node.js TUI)');
|
|
402
|
+
console.log(' tasks List batch task files');
|
|
320
403
|
console.log(' status Show batch status');
|
|
321
404
|
console.log(' config Show batch configuration\n');
|
|
322
405
|
console.log('Options:');
|
|
@@ -329,12 +412,21 @@ function showHelp() {
|
|
|
329
412
|
console.log(' --continue Continue on errors');
|
|
330
413
|
console.log(' --no-commit Skip auto-commit');
|
|
331
414
|
console.log(' --no-branch Skip branch creation');
|
|
415
|
+
console.log(' --filter <pattern> Filter tasks by ID or title (regex, case-insensitive)');
|
|
416
|
+
console.log(' --no-plan Skip planning phase (use fixed 10min timeout)');
|
|
417
|
+
console.log(' --rate-limit-wait <s> Seconds to wait on rate limit (default: 1800)');
|
|
332
418
|
console.log(' --help, -h Show this help\n');
|
|
333
419
|
console.log('Examples:');
|
|
334
420
|
console.log(' pwn batch # Execute next task');
|
|
335
421
|
console.log(' pwn batch run # Run autonomous batch loop');
|
|
336
422
|
console.log(' pwn batch run --dry-run # Preview next story');
|
|
337
423
|
console.log(' pwn batch run --phase 3 # Run specific phase');
|
|
424
|
+
console.log(' pwn batch run --filter ORCH # Run only ORCH-* tasks');
|
|
425
|
+
console.log(' pwn batch run -f "UNI|DATA" # Run UNI or DATA tasks');
|
|
426
|
+
console.log(' pwn batch run --no-plan # Skip planning, fixed timeout');
|
|
427
|
+
console.log(' pwn batch tasks # List all task files');
|
|
428
|
+
console.log(' pwn batch tasks --failed # Show only failed tasks');
|
|
429
|
+
console.log(' pwn batch tasks --clean # Delete completed task files');
|
|
338
430
|
console.log(' pwn batch --count 5 # Execute 5 tasks');
|
|
339
431
|
console.log(' pwn batch --dry-run # Preview execution');
|
|
340
432
|
console.log(' pwn batch --resume # Resume paused batch');
|
package/cli/index.js
CHANGED
|
@@ -22,31 +22,24 @@ if (!command || command === '--help' || command === '-h') {
|
|
|
22
22
|
console.log('Commands:');
|
|
23
23
|
console.log(' inject Inject .ai/ workspace into current project');
|
|
24
24
|
console.log(' update Update PWN framework files to latest version');
|
|
25
|
-
console.log(' migrate Migrate existing AI files to PWN structure');
|
|
26
25
|
console.log(' save Save session context to memory');
|
|
27
26
|
console.log(' status Show workspace status');
|
|
28
27
|
console.log(' validate Validate workspace structure');
|
|
29
|
-
console.log('
|
|
28
|
+
console.log(' backlog Interactive backlog viewer (prd.json)');
|
|
30
29
|
console.log(' batch Execute tasks (batch run = autonomous loop)');
|
|
31
|
-
console.log(' mode Manage session mode (interactive/batch)');
|
|
32
30
|
console.log(' patterns Manage patterns and triggers');
|
|
33
31
|
console.log(' knowledge Knowledge lifecycle management');
|
|
34
|
-
console.log(' codespaces GitHub Codespaces integration');
|
|
35
32
|
console.log(' --version, -v Show version');
|
|
36
33
|
console.log(' --help, -h Show help\n');
|
|
37
34
|
console.log('Options:');
|
|
38
35
|
console.log(' inject --force Overwrite existing .ai/ directory');
|
|
39
36
|
console.log(' update --dry-run Preview update without changes');
|
|
40
|
-
console.log(' migrate --dry-run Preview migration without changes');
|
|
41
37
|
console.log(' save --message=X Save with custom summary');
|
|
42
38
|
console.log(' validate --verbose Show detailed structure report');
|
|
43
|
-
console.log(' notify test [ch] Test notification channel');
|
|
44
39
|
console.log(' batch run Run autonomous batch loop');
|
|
45
|
-
console.log(' mode batch --max-tasks=3 Configure batch mode');
|
|
46
40
|
console.log(' patterns eval <f> Evaluate triggers for file');
|
|
47
|
-
console.log(' knowledge status Show knowledge system status');
|
|
48
|
-
console.log('
|
|
49
|
-
console.log('Documentation: https://github.com/anthropics/pwn');
|
|
41
|
+
console.log(' knowledge status Show knowledge system status\n');
|
|
42
|
+
console.log('Documentation: https://github.com/fermindi/pwn');
|
|
50
43
|
process.exit(0);
|
|
51
44
|
}
|
|
52
45
|
|
|
@@ -62,11 +55,6 @@ switch (command) {
|
|
|
62
55
|
await update(args);
|
|
63
56
|
break;
|
|
64
57
|
|
|
65
|
-
case 'migrate':
|
|
66
|
-
const { default: migrate } = await import('./migrate.js');
|
|
67
|
-
await migrate(args);
|
|
68
|
-
break;
|
|
69
|
-
|
|
70
58
|
case 'save':
|
|
71
59
|
const { default: save } = await import('./save.js');
|
|
72
60
|
await save(args);
|
|
@@ -82,9 +70,9 @@ switch (command) {
|
|
|
82
70
|
await validate(args);
|
|
83
71
|
break;
|
|
84
72
|
|
|
85
|
-
case '
|
|
86
|
-
const { default:
|
|
87
|
-
await
|
|
73
|
+
case 'backlog':
|
|
74
|
+
const { default: backlogCmd } = await import('./backlog.js');
|
|
75
|
+
await backlogCmd(args);
|
|
88
76
|
break;
|
|
89
77
|
|
|
90
78
|
case 'batch':
|
|
@@ -92,11 +80,6 @@ switch (command) {
|
|
|
92
80
|
await batchCmd(args);
|
|
93
81
|
break;
|
|
94
82
|
|
|
95
|
-
case 'mode':
|
|
96
|
-
const { default: modeCmd } = await import('./mode.js');
|
|
97
|
-
await modeCmd(args);
|
|
98
|
-
break;
|
|
99
|
-
|
|
100
83
|
case 'patterns':
|
|
101
84
|
const { default: patternsCmd } = await import('./patterns.js');
|
|
102
85
|
await patternsCmd(args);
|
|
@@ -107,13 +90,8 @@ switch (command) {
|
|
|
107
90
|
await knowledgeCmd(args);
|
|
108
91
|
break;
|
|
109
92
|
|
|
110
|
-
case 'codespaces':
|
|
111
|
-
const { default: codespacesCmd } = await import('./codespaces.js');
|
|
112
|
-
await codespacesCmd(args);
|
|
113
|
-
break;
|
|
114
|
-
|
|
115
93
|
default:
|
|
116
|
-
console.log(
|
|
94
|
+
console.log(`Unknown command: ${command}`);
|
|
117
95
|
console.log(' Run: pwn --help');
|
|
118
96
|
process.exit(1);
|
|
119
97
|
}
|
package/cli/inject.js
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { existsSync, readFileSync } from 'fs';
|
|
3
|
-
import { join } from 'path';
|
|
4
2
|
import { inject, detectKnownAIFiles } from '../src/core/inject.js';
|
|
5
3
|
|
|
6
4
|
export default async function injectCommand(args = []) {
|
|
@@ -22,17 +20,11 @@ export default async function injectCommand(args = []) {
|
|
|
22
20
|
console.log(` Type: ${file.type} - ${file.description}`);
|
|
23
21
|
}
|
|
24
22
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
console.log('\n š” Consider migrating content manually to .ai/ structure:');
|
|
31
|
-
console.log(' - Instructions ā .ai/agents/claude.md');
|
|
32
|
-
console.log(' - Decisions ā .ai/memory/decisions.md');
|
|
33
|
-
console.log(' - Patterns ā .ai/memory/patterns.md');
|
|
34
|
-
console.log(' - Tasks ā .ai/tasks/active.md\n');
|
|
35
|
-
}
|
|
23
|
+
console.log('\n š” Consider migrating content manually to .ai/ structure:');
|
|
24
|
+
console.log(' - Instructions ā .ai/agents/claude.md');
|
|
25
|
+
console.log(' - Decisions ā .ai/memory/decisions.md');
|
|
26
|
+
console.log(' - Patterns ā .ai/memory/patterns.md');
|
|
27
|
+
console.log(' - Tasks ā .ai/tasks/active.md\n');
|
|
36
28
|
}
|
|
37
29
|
|
|
38
30
|
if (!result.success) {
|
|
@@ -63,30 +55,13 @@ export default async function injectCommand(args = []) {
|
|
|
63
55
|
console.log(' āāā memory/ (decisions, patterns, dead-ends)');
|
|
64
56
|
console.log(' āāā tasks/ (active work, prd.json)');
|
|
65
57
|
console.log(' āāā patterns/ (auto-applied patterns)');
|
|
66
|
-
console.log(' āāā batch/ (batch runner, prompts)
|
|
67
|
-
āāā workflows/ (batch execution)');
|
|
58
|
+
console.log(' āāā batch/ (batch runner, prompts)');
|
|
59
|
+
console.log(' āāā workflows/ (batch execution)');
|
|
68
60
|
console.log(' āāā agents/ (AI agent configs)');
|
|
69
|
-
console.log(' āāā config/ (
|
|
61
|
+
console.log(' āāā config/ (project config)');
|
|
70
62
|
console.log(' .claude/');
|
|
71
63
|
console.log(' āāā commands/ (slash commands: /save)\n');
|
|
72
64
|
|
|
73
|
-
// Show ntfy topic if generated
|
|
74
|
-
const notifyPath = join(process.cwd(), '.ai', 'config', 'notifications.json');
|
|
75
|
-
if (existsSync(notifyPath)) {
|
|
76
|
-
try {
|
|
77
|
-
const config = JSON.parse(readFileSync(notifyPath, 'utf8'));
|
|
78
|
-
const topic = config.channels?.ntfy?.topic;
|
|
79
|
-
if (topic && !topic.includes('your-unique')) {
|
|
80
|
-
console.log('š Notifications:');
|
|
81
|
-
console.log(` ntfy topic: ${topic}`);
|
|
82
|
-
console.log(` Subscribe: https://ntfy.sh/${topic}`);
|
|
83
|
-
console.log(' Enable: Edit .ai/config/notifications.json\n');
|
|
84
|
-
}
|
|
85
|
-
} catch {
|
|
86
|
-
// Ignore
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
65
|
console.log('š Next steps:');
|
|
91
66
|
console.log(' 1. Read: .ai/README.md');
|
|
92
67
|
console.log(' 2. Start working with AI assistance\n');
|
package/cli/update.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { existsSync, readFileSync, writeFileSync, cpSync, renameSync, mkdirSync, readdirSync } from 'fs';
|
|
3
3
|
import { join, dirname } from 'path';
|
|
4
4
|
import { fileURLToPath } from 'url';
|
|
5
|
-
import { convertBacklogToPrd } from '../src/services/batch-service.js';
|
|
5
|
+
import { convertBacklogToPrd, detectAvailableGates } from '../src/services/batch-service.js';
|
|
6
6
|
|
|
7
7
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
8
|
|
|
@@ -18,10 +18,8 @@ const FRAMEWORK_FILES = [
|
|
|
18
18
|
'patterns/backend/backend.template.md',
|
|
19
19
|
'patterns/universal/universal.template.md',
|
|
20
20
|
'workflows/batch-task.md',
|
|
21
|
-
'batch/batch_runner.sh',
|
|
22
21
|
'batch/prompt.md',
|
|
23
22
|
'batch/progress.txt',
|
|
24
|
-
'batch/prd_status.sh',
|
|
25
23
|
'config/README.md',
|
|
26
24
|
'README.md',
|
|
27
25
|
];
|
|
@@ -31,7 +29,6 @@ const FRAMEWORK_FILES = [
|
|
|
31
29
|
*/
|
|
32
30
|
const CLAUDE_COMMANDS = [
|
|
33
31
|
'save.md',
|
|
34
|
-
'mode.md',
|
|
35
32
|
];
|
|
36
33
|
|
|
37
34
|
/**
|
|
@@ -46,7 +43,6 @@ const USER_FILES = [
|
|
|
46
43
|
'tasks/active.md',
|
|
47
44
|
'tasks/prd.json',
|
|
48
45
|
'state.json',
|
|
49
|
-
'config/notifications.json',
|
|
50
46
|
];
|
|
51
47
|
|
|
52
48
|
export default async function updateCommand(args = []) {
|
|
@@ -262,25 +258,6 @@ export default async function updateCommand(args = []) {
|
|
|
262
258
|
}
|
|
263
259
|
}
|
|
264
260
|
|
|
265
|
-
// Update .claude/settings.json (hooks) - only if doesn't exist (user config)
|
|
266
|
-
const claudeSettingsTemplate = join(__dirname, '../templates/workspace/.claude/settings.json');
|
|
267
|
-
const claudeSettingsTarget = join(cwd, '.claude', 'settings.json');
|
|
268
|
-
|
|
269
|
-
if (existsSync(claudeSettingsTemplate) && !existsSync(claudeSettingsTarget)) {
|
|
270
|
-
if (dryRun) {
|
|
271
|
-
console.log(` š Would create: .claude/settings.json (notification hooks)`);
|
|
272
|
-
} else {
|
|
273
|
-
const claudeDir = join(cwd, '.claude');
|
|
274
|
-
if (!existsSync(claudeDir)) {
|
|
275
|
-
mkdirSync(claudeDir, { recursive: true });
|
|
276
|
-
}
|
|
277
|
-
const templateContent = readFileSync(claudeSettingsTemplate, 'utf8');
|
|
278
|
-
writeFileSync(claudeSettingsTarget, templateContent);
|
|
279
|
-
console.log(` š Created: .claude/settings.json (notification hooks)`);
|
|
280
|
-
}
|
|
281
|
-
updated.push('.claude/settings.json');
|
|
282
|
-
}
|
|
283
|
-
|
|
284
261
|
// Update state.json with new version
|
|
285
262
|
if (!dryRun && existsSync(statePath)) {
|
|
286
263
|
try {
|
|
@@ -293,6 +270,36 @@ export default async function updateCommand(args = []) {
|
|
|
293
270
|
}
|
|
294
271
|
}
|
|
295
272
|
|
|
273
|
+
// Re-detect quality gates and update skip_gates
|
|
274
|
+
if (!dryRun) {
|
|
275
|
+
const gates = detectAvailableGates(cwd);
|
|
276
|
+
console.log('\nāļø Quality Gates Detection');
|
|
277
|
+
for (const gate of gates.available) {
|
|
278
|
+
console.log(` ā
${gate.padEnd(12)} ${gates.details[gate]}`);
|
|
279
|
+
}
|
|
280
|
+
for (const gate of gates.missing) {
|
|
281
|
+
console.log(` ā ļø ${gate.padEnd(12)} not detected`);
|
|
282
|
+
}
|
|
283
|
+
if (gates.missing.length > 0) {
|
|
284
|
+
console.log(` ā Auto-configured skip_gates: ${JSON.stringify(gates.missing)}`);
|
|
285
|
+
} else {
|
|
286
|
+
console.log(` ā All gates available`);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Update skip_gates in state.json
|
|
290
|
+
if (existsSync(statePath)) {
|
|
291
|
+
try {
|
|
292
|
+
const state = JSON.parse(readFileSync(statePath, 'utf8'));
|
|
293
|
+
if (!state.batch_config) state.batch_config = {};
|
|
294
|
+
state.batch_config.skip_gates = gates.missing;
|
|
295
|
+
state.last_updated = new Date().toISOString();
|
|
296
|
+
writeFileSync(statePath, JSON.stringify(state, null, 2));
|
|
297
|
+
} catch {
|
|
298
|
+
// Ignore
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
296
303
|
// Summary
|
|
297
304
|
console.log('');
|
|
298
305
|
if (updated.length === 0) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fermindi/pwn-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Professional AI Workspace - Inject structured memory and automation into any project for AI-powered development",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -14,7 +14,6 @@
|
|
|
14
14
|
"./core/validate": "./src/core/validate.js",
|
|
15
15
|
"./core/workspace": "./src/core/workspace.js",
|
|
16
16
|
"./services/batch": "./src/services/batch-service.js",
|
|
17
|
-
"./services/notifications": "./src/services/notification-service.js",
|
|
18
17
|
"./patterns/registry": "./src/patterns/registry.js",
|
|
19
18
|
"./patterns/triggers": "./src/patterns/triggers.js",
|
|
20
19
|
"./knowledge/lifecycle": "./src/knowledge/lifecycle.js",
|
|
@@ -47,7 +46,7 @@
|
|
|
47
46
|
"memory",
|
|
48
47
|
"decisions",
|
|
49
48
|
"batch",
|
|
50
|
-
"
|
|
49
|
+
"claude-code",
|
|
51
50
|
"developer-tools",
|
|
52
51
|
"productivity"
|
|
53
52
|
],
|
|
@@ -66,5 +65,9 @@
|
|
|
66
65
|
},
|
|
67
66
|
"devDependencies": {
|
|
68
67
|
"vitest": "^2.0.0"
|
|
68
|
+
},
|
|
69
|
+
"dependencies": {
|
|
70
|
+
"chalk": "^5.6.2",
|
|
71
|
+
"ora": "^9.3.0"
|
|
69
72
|
}
|
|
70
73
|
}
|
package/src/core/inject.js
CHANGED
|
@@ -1,9 +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';
|
|
6
|
-
import { convertBacklogToPrd } from '../services/batch-service.js';
|
|
5
|
+
import { convertBacklogToPrd, detectAvailableGates } from '../services/batch-service.js';
|
|
6
|
+
import { updateState } from './state.js';
|
|
7
7
|
|
|
8
8
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
9
9
|
|
|
@@ -175,18 +175,24 @@ export async function inject(options = {}) {
|
|
|
175
175
|
renameSync(templateState, stateFile);
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
-
// Rename notifications.template.json ā notifications.json and generate unique topic
|
|
179
|
-
const templateNotify = join(targetDir, 'config', 'notifications.template.json');
|
|
180
|
-
const notifyFile = join(targetDir, 'config', 'notifications.json');
|
|
181
|
-
|
|
182
|
-
if (existsSync(templateNotify)) {
|
|
183
|
-
renameSync(templateNotify, notifyFile);
|
|
184
|
-
initNotifications(notifyFile);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
178
|
// Initialize state.json with current user
|
|
188
179
|
initState(cwd);
|
|
189
180
|
|
|
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]}`);
|
|
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('');
|
|
195
|
+
|
|
190
196
|
// Write migrated prd.json (from backlog.md) if available
|
|
191
197
|
if (migratedPrd) {
|
|
192
198
|
const prdPath = join(targetDir, 'tasks', 'prd.json');
|
|
@@ -242,15 +248,6 @@ export async function inject(options = {}) {
|
|
|
242
248
|
}
|
|
243
249
|
}
|
|
244
250
|
|
|
245
|
-
// Copy settings.json with hooks (if doesn't exist)
|
|
246
|
-
const settingsSource = join(claudeTemplateDir, 'settings.json');
|
|
247
|
-
const settingsTarget = join(claudeDir, 'settings.json');
|
|
248
|
-
if (existsSync(settingsSource) && !existsSync(settingsTarget)) {
|
|
249
|
-
cpSync(settingsSource, settingsTarget);
|
|
250
|
-
if (!silent) {
|
|
251
|
-
console.log('š Created .claude/settings.json with notification hooks');
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
251
|
}
|
|
255
252
|
|
|
256
253
|
// Backup other AI files (not CLAUDE.md) to .ai/
|
|
@@ -280,24 +277,6 @@ export async function inject(options = {}) {
|
|
|
280
277
|
}
|
|
281
278
|
}
|
|
282
279
|
|
|
283
|
-
/**
|
|
284
|
-
* Initialize notifications.json with unique topic
|
|
285
|
-
* @param {string} notifyFile - Path to notifications.json
|
|
286
|
-
*/
|
|
287
|
-
function initNotifications(notifyFile) {
|
|
288
|
-
try {
|
|
289
|
-
const content = readFileSync(notifyFile, 'utf8');
|
|
290
|
-
const config = JSON.parse(content);
|
|
291
|
-
|
|
292
|
-
// Generate unique topic ID
|
|
293
|
-
const uniqueId = randomUUID().split('-')[0]; // First segment: 8 chars
|
|
294
|
-
config.channels.ntfy.topic = `pwn-${uniqueId}`;
|
|
295
|
-
|
|
296
|
-
writeFileSync(notifyFile, JSON.stringify(config, null, 2));
|
|
297
|
-
} catch {
|
|
298
|
-
// Ignore errors - notifications will use defaults
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
280
|
|
|
302
281
|
/**
|
|
303
282
|
* Backup existing AI files to .ai/ root with ~ prefix
|
|
@@ -351,7 +330,7 @@ function updateGitignore(cwd, silent = false) {
|
|
|
351
330
|
}
|
|
352
331
|
|
|
353
332
|
if (!gitignoreContent.includes('.ai/state.json')) {
|
|
354
|
-
const pwnSection = '\n# PWN\n.ai/state.json\n
|
|
333
|
+
const pwnSection = '\n# PWN\n.ai/state.json\n';
|
|
355
334
|
appendFileSync(gitignorePath, pwnSection);
|
|
356
335
|
if (!silent) {
|
|
357
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 }
|
package/src/core/validate.js
CHANGED
package/src/index.js
CHANGED