@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.
- package/README.md +14 -10
- package/cli/backlog.js +60 -0
- package/cli/batch.js +112 -12
- package/cli/index.js +9 -31
- package/cli/inject.js +8 -32
- package/cli/status.js +1 -1
- package/cli/update.js +78 -27
- package/package.json +6 -3
- package/src/core/inject.js +41 -45
- package/src/core/state.js +0 -1
- package/src/core/validate.js +14 -1
- package/src/core/workspace.js +13 -11
- package/src/index.js +0 -1
- package/src/services/batch-runner.js +769 -0
- package/src/services/batch-service.js +185 -74
- package/src/ui/backlog-viewer.js +394 -0
- package/templates/workspace/.ai/agents/claude.md +47 -146
- package/templates/workspace/.ai/batch/tasks/.gitkeep +0 -0
- package/templates/workspace/.ai/memory/patterns.md +57 -11
- package/templates/workspace/.ai/tasks/active.md +1 -1
- package/templates/workspace/.ai/workflows/batch-task.md +43 -67
- package/templates/workspace/.claude/commands/save.md +0 -42
- 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/.ai/tasks/backlog.md +0 -95
- package/templates/workspace/.claude/commands/mode.md +0 -103
- package/templates/workspace/.claude/settings.json +0 -15
package/src/core/inject.js
CHANGED
|
@@ -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
|
-
//
|
|
164
|
-
|
|
165
|
-
const notifyFile = join(targetDir, 'config', 'notifications.json');
|
|
178
|
+
// Initialize state.json with current user
|
|
179
|
+
initState(cwd);
|
|
166
180
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
//
|
|
173
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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 }
|
package/src/core/validate.js
CHANGED
|
@@ -24,7 +24,11 @@ const REQUIRED_STRUCTURE = {
|
|
|
24
24
|
],
|
|
25
25
|
taskFiles: [
|
|
26
26
|
'tasks/active.md',
|
|
27
|
-
'tasks/
|
|
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
|
];
|
package/src/core/workspace.js
CHANGED
|
@@ -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
|
|
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
|
|
67
|
-
if (existsSync(
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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