@fermindi/pwn-cli 0.9.7 → 0.9.8

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.7",
3
+ "version": "0.9.8",
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": {
@@ -840,6 +840,54 @@ function spawnClaude(prompt, task, iteration, maxIter, done, total, phase, logFi
840
840
  });
841
841
  }
842
842
 
843
+ /**
844
+ * Ensure Python dev tools (pytest, ruff, mypy, etc.) exist in the venv.
845
+ * Extracts tool names from gate_commands and verifies they're importable.
846
+ * Installs missing ones to prevent wasted retries on environment issues.
847
+ * @param {string} taskCwd - Worktree directory (has .venv symlink)
848
+ * @param {string} mainCwd - Main repo directory for config
849
+ */
850
+ async function ensureDevTools(taskCwd, mainCwd) {
851
+ const config = loadConfig(mainCwd);
852
+ const gateCommands = config.gate_commands;
853
+ if (!gateCommands) return;
854
+
855
+ // Find the python binary from gate commands
856
+ const sampleCmd = Object.values(gateCommands)[0] || '';
857
+ const pythonMatch = sampleCmd.match(/^(\S*python\S*)\s/);
858
+ if (!pythonMatch) return; // not a Python project
859
+
860
+ const python = pythonMatch[1];
861
+
862
+ // Extract module names from commands like ".venv/bin/python -m pytest --tb=short"
863
+ const modules = new Set();
864
+ for (const cmd of Object.values(gateCommands)) {
865
+ const modMatch = cmd.match(/-m\s+(\w+)/);
866
+ if (modMatch) modules.add(modMatch[1]);
867
+ }
868
+ if (modules.size === 0) return;
869
+
870
+ // Check which modules are missing
871
+ const missing = [];
872
+ for (const mod of modules) {
873
+ try {
874
+ await execAsync(`${python} -c "import ${mod}"`, { cwd: taskCwd, timeout: 10_000 });
875
+ } catch {
876
+ missing.push(mod);
877
+ }
878
+ }
879
+
880
+ if (missing.length > 0) {
881
+ console.log(chalk.yellow(` Dev tools missing in venv: ${missing.join(', ')}. Installing...`));
882
+ try {
883
+ await execAsync(`${python} -m pip install ${missing.join(' ')} --quiet`, { cwd: taskCwd, timeout: 60_000 });
884
+ console.log(chalk.green(` Installed: ${missing.join(', ')}`));
885
+ } catch (err) {
886
+ console.log(chalk.red(` Failed to install dev tools: ${err.message}`));
887
+ }
888
+ }
889
+ }
890
+
843
891
  /**
844
892
  * Run quality gates with real-time PASS/FAIL display.
845
893
  * @param {string} taskCwd - Directory where gates run (task worktree)
@@ -852,6 +900,9 @@ async function runGatesWithStatus(taskCwd, mainCwd = taskCwd) {
852
900
  let allPassed = true;
853
901
  let errorOutput = '';
854
902
 
903
+ // Pre-flight: ensure dev tools exist before running gates
904
+ await ensureDevTools(taskCwd, mainCwd);
905
+
855
906
  for (const gate of gates) {
856
907
  if (skipGates.includes(gate)) {
857
908
  console.log(chalk.dim(` ${gate}: SKIP`));
@@ -5,7 +5,7 @@
5
5
  * checkpointing, and signal handling.
6
6
  */
7
7
 
8
- import { existsSync, readFileSync, writeFileSync, readdirSync, appendFileSync, symlinkSync, unlinkSync, lstatSync } from 'fs';
8
+ import { existsSync, readFileSync, writeFileSync, readdirSync, appendFileSync, symlinkSync, unlinkSync, lstatSync, readlinkSync } from 'fs';
9
9
  import { join } from 'path';
10
10
  import os from 'os';
11
11
  import { exec } from 'child_process';
@@ -244,7 +244,22 @@ export function prepareWorktree(worktreePath, mainCwd) {
244
244
  for (const dir of DEP_DIRS) {
245
245
  const source = join(mainCwd, dir);
246
246
  const target = join(worktreePath, dir);
247
- if (existsSync(source) && !existsSync(target)) {
247
+ if (!existsSync(source)) continue;
248
+
249
+ // Validate existing symlinks (git may restore stale ones from history)
250
+ try {
251
+ const stats = lstatSync(target);
252
+ if (stats.isSymbolicLink()) {
253
+ if (readlinkSync(target) !== source) {
254
+ unlinkSync(target);
255
+ symlinkSync(source, target);
256
+ }
257
+ continue; // symlink exists and is correct
258
+ }
259
+ // Not a symlink (real dir) — leave it alone
260
+ continue;
261
+ } catch {
262
+ // Doesn't exist — create it
248
263
  symlinkSync(source, target);
249
264
  }
250
265
  }