@fermindi/pwn-cli 0.9.0 → 0.9.2

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.0",
3
+ "version": "0.9.2",
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,10 +9,13 @@
9
9
  * Completed files are cleaned up at the end; failed are kept for review.
10
10
  */
11
11
 
12
- import { spawn } from 'child_process';
12
+ import { spawn, exec } 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';
15
16
  import ora from 'ora';
17
+
18
+ const execAsync = promisify(exec);
16
19
  import chalk from 'chalk';
17
20
  import {
18
21
  parsePrdTasks,
@@ -434,7 +437,10 @@ export async function runBatch(options = {}, cwd = process.cwd()) {
434
437
  if (result.signal) {
435
438
  console.log(chalk.yellow(` Killed by ${result.signal}`));
436
439
  if (taskBranch) {
437
- try { await checkoutBranch(batchBranch, cwd, { force: true }); } catch {}
440
+ try {
441
+ try { await execAsync('git stash --include-untracked', { cwd }); } catch {}
442
+ await checkoutBranch(batchBranch, cwd, { force: true });
443
+ } catch {}
438
444
  }
439
445
  clearBatchState(cwd);
440
446
  printSummary(cwd, iteration, storiesCompleted, batchStart, branchesCreated, batchBranch, originalBranch, mergedCount, unmergedBranches);
@@ -544,15 +550,21 @@ export async function runBatch(options = {}, cwd = process.cwd()) {
544
550
  // Merge successful task branch into batch branch
545
551
  if (taskBranch) {
546
552
  try {
553
+ // Stash uncommitted metadata (task files etc.) before switching
554
+ await execAsync('git stash --include-untracked', { cwd });
547
555
  await checkoutBranch(batchBranch, cwd);
548
556
  await mergeBranch(taskBranch, cwd);
549
557
  mergedCount++;
550
558
  mergedBranches.push(taskBranch);
551
559
  console.log(chalk.green(` Merged ${taskBranch} → ${batchBranch}`));
560
+ // Restore stashed metadata on batch branch
561
+ try { await execAsync('git stash pop', { cwd }); } catch {}
552
562
  } catch (err) {
553
563
  console.log(chalk.red(` Merge failed: ${err.message}`));
554
564
  console.log(chalk.yellow(` Branch ${taskBranch} available for manual merge`));
555
565
  unmergedBranches.push(taskBranch);
566
+ // Drop stash if checkout/merge failed (still on feat branch)
567
+ try { await execAsync('git stash drop', { cwd }); } catch {}
556
568
  }
557
569
  }
558
570
  } else {
@@ -586,10 +598,12 @@ export async function runBatch(options = {}, cwd = process.cwd()) {
586
598
  noProgressCount++;
587
599
  }
588
600
 
589
- // --- Return to batch branch (force: discard dirty state from failed tasks) ---
601
+ // --- Return to batch branch (stash untracked + force for dirty state) ---
590
602
  if (taskBranch) {
591
603
  try {
604
+ try { await execAsync('git stash --include-untracked', { cwd }); } catch {}
592
605
  await checkoutBranch(batchBranch, cwd, { force: true });
606
+ try { await execAsync('git stash pop', { cwd }); } catch {}
593
607
  console.log(chalk.dim(` Returned to branch: ${batchBranch}`));
594
608
  } catch (err) {
595
609
  console.log(chalk.red(` Warning: failed to return to ${batchBranch}: ${err.message}`));
@@ -34,18 +34,36 @@ export async function getCurrentBranch(cwd = process.cwd()) {
34
34
  */
35
35
  export async function createTaskBranch(taskId, cwd = process.cwd()) {
36
36
  const branch = `feat/${taskId}`;
37
+ let exists = false;
37
38
  try {
38
39
  await execAsync(`git rev-parse --verify ${branch}`, { cwd });
39
- // Branch exists (rerun) — checkout and reset to current HEAD
40
+ exists = true;
41
+ } catch {
42
+ // Branch doesn't exist
43
+ }
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
+ if (exists) {
53
+ // Rerun — checkout and reset to current HEAD
40
54
  // so we don't carry stale/dirty state from previous attempt
41
55
  const { stdout } = await execAsync('git rev-parse HEAD', { cwd });
42
56
  const baseRef = stdout.trim();
43
57
  await execAsync(`git checkout ${branch}`, { cwd });
44
58
  await execAsync(`git reset --hard ${baseRef}`, { cwd });
45
- } catch {
46
- // Branch doesn't exist — create
59
+ } else {
47
60
  await execAsync(`git checkout -b ${branch}`, { cwd });
48
61
  }
62
+
63
+ // Restore stashed files on the new branch
64
+ if (stashed) {
65
+ try { await execAsync('git stash pop', { cwd }); } catch {}
66
+ }
49
67
  return branch;
50
68
  }
51
69