@fermindi/pwn-cli 0.9.1 → 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fermindi/pwn-cli",
3
- "version": "0.9.1",
3
+ "version": "0.9.3",
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": {
@@ -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, exec } from 'child_process';
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.1';
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
- // Stash uncommitted metadata (task files etc.) before switching
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 (stash untracked + force for dirty state) ---
589
+ // --- Return to batch branch ---
602
590
  if (taskBranch) {
603
591
  try {
604
- try { await execAsync('git stash --include-untracked', { cwd }); } catch {}
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}`));
@@ -47,22 +47,52 @@ export async function createTaskBranch(taskId, cwd = process.cwd()) {
47
47
  // so we don't carry stale/dirty state from previous attempt
48
48
  const { stdout } = await execAsync('git rev-parse HEAD', { cwd });
49
49
  const baseRef = stdout.trim();
50
- await execAsync(`git checkout ${branch}`, { cwd });
50
+ await checkoutBranch(branch, cwd, { stash: true });
51
51
  await execAsync(`git reset --hard ${baseRef}`, { cwd });
52
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 {}
53
59
  await execAsync(`git checkout -b ${branch}`, { cwd });
60
+ if (didStash) {
61
+ try { await execAsync('git stash pop', { cwd }); } catch {}
62
+ }
54
63
  }
55
64
  return branch;
56
65
  }
57
66
 
58
67
  /**
59
- * 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.
60
71
  * @param {string} branch - Branch name
61
72
  * @param {string} cwd - Working directory
73
+ * @param {{ force?: boolean, stash?: boolean }} options
62
74
  */
63
- 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
+
64
84
  const flag = force ? ' --force' : '';
65
- await execAsync(`git checkout${flag} ${branch}`, { cwd });
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
+ }
66
96
  }
67
97
 
68
98
  /**