@fermindi/pwn-cli 0.9.2 → 0.9.3
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/package.json
CHANGED
|
@@ -9,13 +9,10 @@
|
|
|
9
9
|
* Completed files are cleaned up at the end; failed are kept for review.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { spawn
|
|
12
|
+
import { spawn } from 'child_process';
|
|
13
13
|
import { existsSync, readFileSync, writeFileSync, mkdirSync, createWriteStream, appendFileSync, unlinkSync, readdirSync } from 'fs';
|
|
14
14
|
import { join } from 'path';
|
|
15
|
-
import { promisify } from 'util';
|
|
16
15
|
import ora from 'ora';
|
|
17
|
-
|
|
18
|
-
const execAsync = promisify(exec);
|
|
19
16
|
import chalk from 'chalk';
|
|
20
17
|
import {
|
|
21
18
|
parsePrdTasks,
|
|
@@ -34,7 +31,7 @@ import {
|
|
|
34
31
|
} from './batch-service.js';
|
|
35
32
|
|
|
36
33
|
// --- Constants ---
|
|
37
|
-
const RUNNER_VERSION = '2.
|
|
34
|
+
const RUNNER_VERSION = '2.2';
|
|
38
35
|
const DEFAULT_TIMEOUT_MS = 900_000; // 15 minutes fallback
|
|
39
36
|
const MIN_TIMEOUT_MS = 300_000; // 5 minutes minimum (claude init ~30-40s + real work)
|
|
40
37
|
|
|
@@ -437,10 +434,7 @@ export async function runBatch(options = {}, cwd = process.cwd()) {
|
|
|
437
434
|
if (result.signal) {
|
|
438
435
|
console.log(chalk.yellow(` Killed by ${result.signal}`));
|
|
439
436
|
if (taskBranch) {
|
|
440
|
-
try {
|
|
441
|
-
try { await execAsync('git stash --include-untracked', { cwd }); } catch {}
|
|
442
|
-
await checkoutBranch(batchBranch, cwd, { force: true });
|
|
443
|
-
} catch {}
|
|
437
|
+
try { await checkoutBranch(batchBranch, cwd, { force: true, stash: true }); } catch {}
|
|
444
438
|
}
|
|
445
439
|
clearBatchState(cwd);
|
|
446
440
|
printSummary(cwd, iteration, storiesCompleted, batchStart, branchesCreated, batchBranch, originalBranch, mergedCount, unmergedBranches);
|
|
@@ -550,21 +544,15 @@ export async function runBatch(options = {}, cwd = process.cwd()) {
|
|
|
550
544
|
// Merge successful task branch into batch branch
|
|
551
545
|
if (taskBranch) {
|
|
552
546
|
try {
|
|
553
|
-
|
|
554
|
-
await execAsync('git stash --include-untracked', { cwd });
|
|
555
|
-
await checkoutBranch(batchBranch, cwd);
|
|
547
|
+
await checkoutBranch(batchBranch, cwd, { stash: true });
|
|
556
548
|
await mergeBranch(taskBranch, cwd);
|
|
557
549
|
mergedCount++;
|
|
558
550
|
mergedBranches.push(taskBranch);
|
|
559
551
|
console.log(chalk.green(` Merged ${taskBranch} → ${batchBranch}`));
|
|
560
|
-
// Restore stashed metadata on batch branch
|
|
561
|
-
try { await execAsync('git stash pop', { cwd }); } catch {}
|
|
562
552
|
} catch (err) {
|
|
563
553
|
console.log(chalk.red(` Merge failed: ${err.message}`));
|
|
564
554
|
console.log(chalk.yellow(` Branch ${taskBranch} available for manual merge`));
|
|
565
555
|
unmergedBranches.push(taskBranch);
|
|
566
|
-
// Drop stash if checkout/merge failed (still on feat branch)
|
|
567
|
-
try { await execAsync('git stash drop', { cwd }); } catch {}
|
|
568
556
|
}
|
|
569
557
|
}
|
|
570
558
|
} else {
|
|
@@ -598,12 +586,10 @@ export async function runBatch(options = {}, cwd = process.cwd()) {
|
|
|
598
586
|
noProgressCount++;
|
|
599
587
|
}
|
|
600
588
|
|
|
601
|
-
// --- Return to batch branch
|
|
589
|
+
// --- Return to batch branch ---
|
|
602
590
|
if (taskBranch) {
|
|
603
591
|
try {
|
|
604
|
-
|
|
605
|
-
await checkoutBranch(batchBranch, cwd, { force: true });
|
|
606
|
-
try { await execAsync('git stash pop', { cwd }); } catch {}
|
|
592
|
+
await checkoutBranch(batchBranch, cwd, { force: true, stash: true });
|
|
607
593
|
console.log(chalk.dim(` Returned to branch: ${batchBranch}`));
|
|
608
594
|
} catch (err) {
|
|
609
595
|
console.log(chalk.red(` Warning: failed to return to ${batchBranch}: ${err.message}`));
|
|
@@ -42,39 +42,57 @@ export async function createTaskBranch(taskId, cwd = process.cwd()) {
|
|
|
42
42
|
// Branch doesn't exist
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
// Stash untracked files (task metadata etc.) that block checkout
|
|
46
|
-
let stashed = false;
|
|
47
|
-
try {
|
|
48
|
-
const { stdout: stashOut } = await execAsync('git stash --include-untracked', { cwd });
|
|
49
|
-
stashed = !stashOut.includes('No local changes');
|
|
50
|
-
} catch {}
|
|
51
|
-
|
|
52
45
|
if (exists) {
|
|
53
46
|
// Rerun — checkout and reset to current HEAD
|
|
54
47
|
// so we don't carry stale/dirty state from previous attempt
|
|
55
48
|
const { stdout } = await execAsync('git rev-parse HEAD', { cwd });
|
|
56
49
|
const baseRef = stdout.trim();
|
|
57
|
-
await
|
|
50
|
+
await checkoutBranch(branch, cwd, { stash: true });
|
|
58
51
|
await execAsync(`git reset --hard ${baseRef}`, { cwd });
|
|
59
52
|
} else {
|
|
53
|
+
// Stash before -b too (untracked files block checkout -b as well)
|
|
54
|
+
let didStash = false;
|
|
55
|
+
try {
|
|
56
|
+
const { stdout } = await execAsync('git stash --include-untracked', { cwd });
|
|
57
|
+
didStash = !stdout.includes('No local changes');
|
|
58
|
+
} catch {}
|
|
60
59
|
await execAsync(`git checkout -b ${branch}`, { cwd });
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
if (stashed) {
|
|
65
|
-
try { await execAsync('git stash pop', { cwd }); } catch {}
|
|
60
|
+
if (didStash) {
|
|
61
|
+
try { await execAsync('git stash pop', { cwd }); } catch {}
|
|
62
|
+
}
|
|
66
63
|
}
|
|
67
64
|
return branch;
|
|
68
65
|
}
|
|
69
66
|
|
|
70
67
|
/**
|
|
71
|
-
* Checkout an existing branch
|
|
68
|
+
* Checkout an existing branch.
|
|
69
|
+
* When stash=true, stashes untracked/dirty files before checkout and
|
|
70
|
+
* restores them after — prevents .ai/batch/tasks/*.json from blocking.
|
|
72
71
|
* @param {string} branch - Branch name
|
|
73
72
|
* @param {string} cwd - Working directory
|
|
73
|
+
* @param {{ force?: boolean, stash?: boolean }} options
|
|
74
74
|
*/
|
|
75
|
-
export async function checkoutBranch(branch, cwd = process.cwd(), { force = false } = {}) {
|
|
75
|
+
export async function checkoutBranch(branch, cwd = process.cwd(), { force = false, stash = false } = {}) {
|
|
76
|
+
let didStash = false;
|
|
77
|
+
if (stash) {
|
|
78
|
+
try {
|
|
79
|
+
const { stdout } = await execAsync('git stash --include-untracked', { cwd });
|
|
80
|
+
didStash = !stdout.includes('No local changes');
|
|
81
|
+
} catch {}
|
|
82
|
+
}
|
|
83
|
+
|
|
76
84
|
const flag = force ? ' --force' : '';
|
|
77
|
-
|
|
85
|
+
try {
|
|
86
|
+
await execAsync(`git checkout${flag} ${branch}`, { cwd });
|
|
87
|
+
} catch (err) {
|
|
88
|
+
// If checkout failed, drop stash so it doesn't pile up
|
|
89
|
+
if (didStash) { try { await execAsync('git stash drop', { cwd }); } catch {} }
|
|
90
|
+
throw err;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (didStash) {
|
|
94
|
+
try { await execAsync('git stash pop', { cwd }); } catch {}
|
|
95
|
+
}
|
|
78
96
|
}
|
|
79
97
|
|
|
80
98
|
/**
|