@dreki-gg/pi-plan-mode 0.4.0 → 0.5.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/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # @dreki-gg/pi-plan-mode
2
2
 
3
+ ## 0.5.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [`32797ff`](https://github.com/dreki-gg/pi-extensions/commit/32797ff18d968e22c6c44e95c46e3393d8928cef) Thanks [@jalbarrang](https://github.com/jalbarrang)! - feat(plan-mode): add Windows compatibility — replace Unix shell commands with cross-platform Bun/Node APIs
8
+
9
+ Plan-mode no longer shells out to `cat`, `bash`, or `mkdir` via `pi.exec()`. File I/O now uses `Bun.file()` / `Bun.write()` and `node:fs/promises` `mkdir`, making the extension fully cross-platform. Destructive and safe command pattern lists now include Windows equivalents (`del`, `rd`, `copy`, `move`, `powershell`, `dir`, `where`, `tasklist`, etc.).
10
+
11
+ Also fixes Windows compatibility in three other packages:
12
+
13
+ - **browser-tools**: `spawn` now uses `shell: true` on Windows so `.cmd` wrappers resolve correctly; `shellEscape` uses double-quote style on Windows; install guidance is platform-aware (Homebrew shown only on macOS).
14
+ - **subagent**: `spawn` uses `shell: true` on Windows when the command is bare `pi`, allowing `pi.cmd` resolution.
15
+ - **lsp**: `globalConfigPath()` now uses `os.homedir()` on Windows instead of the unreliable `process.env.HOME`.
16
+
3
17
  ## 0.4.0
4
18
 
5
19
  ### Minor Changes
@@ -22,6 +22,7 @@ import type { AgentMessage } from '@earendil-works/pi-agent-core';
22
22
  import type { AssistantMessage, TextContent } from '@earendil-works/pi-ai';
23
23
  import type { ExtensionAPI, ExtensionContext } from '@earendil-works/pi-coding-agent';
24
24
  import { Key } from '@earendil-works/pi-tui';
25
+ import { mkdir } from 'node:fs/promises';
25
26
  import {
26
27
  extractTodoItems,
27
28
  isSafeCommand,
@@ -111,7 +112,7 @@ export default function planMode(pi: ExtensionAPI): void {
111
112
  status: 'in-progress' | 'done',
112
113
  title?: string,
113
114
  ): Promise<void> {
114
- const manifest = await readPlansJson((cmd, args) => pi.exec(cmd, args));
115
+ const manifest = await readPlansJson();
115
116
  const existing = manifest[planName];
116
117
  const now = new Date().toISOString();
117
118
 
@@ -122,10 +123,9 @@ export default function planMode(pi: ExtensionAPI): void {
122
123
  completed: status === 'done' ? now : null,
123
124
  };
124
125
 
125
- await pi.exec('mkdir', ['-p', '.plans']);
126
+ await mkdir('.plans', { recursive: true });
126
127
  const content = serializePlansJson(manifest);
127
- // Write via a temp approach — use bash echo to avoid needing the write tool
128
- await pi.exec('bash', ['-c', `cat > .plans/plans.json << 'PLANS_EOF'\n${content}PLANS_EOF`]);
128
+ await Bun.write('.plans/plans.json', content);
129
129
  }
130
130
 
131
131
  // ── UI updates ────────────────────────────────────────────────────────────
@@ -419,10 +419,8 @@ Execute each step in order. You MUST include [DONE:n] in your response after com
419
419
  let title = 'Untitled plan';
420
420
  if (path.endsWith('PLAN.md')) {
421
421
  try {
422
- const result = await pi.exec('cat', [path]);
423
- if (result.code === 0) {
424
- title = extractPlanTitle(result.stdout);
425
- }
422
+ const content = await Bun.file(path).text();
423
+ title = extractPlanTitle(content);
426
424
  } catch {
427
425
  // Fall through
428
426
  }
@@ -432,11 +430,9 @@ Execute each step in order. You MUST include [DONE:n] in your response after com
432
430
  } else if (match && planDir && path.endsWith('PLAN.md')) {
433
431
  // planDir already set but PLAN.md just written — update title
434
432
  try {
435
- const result = await pi.exec('cat', [path]);
436
- if (result.code === 0) {
437
- const title = extractPlanTitle(result.stdout);
438
- await updatePlansManifest(match[1], 'in-progress', title);
439
- }
433
+ const content = await Bun.file(path).text();
434
+ const title = extractPlanTitle(content);
435
+ await updatePlansManifest(match[1], 'in-progress', title);
440
436
  } catch {
441
437
  // Fall through
442
438
  }
@@ -500,10 +496,7 @@ Execute each step in order. You MUST include [DONE:n] in your response after com
500
496
  // Read the plan to extract todos
501
497
  let planContent = '';
502
498
  try {
503
- const result = await pi.exec('cat', [planMdPath]);
504
- if (result.code === 0) {
505
- planContent = result.stdout;
506
- }
499
+ planContent = await Bun.file(planMdPath).text();
507
500
  } catch {
508
501
  // Fall through — will use empty plan content
509
502
  }
@@ -516,10 +509,7 @@ Execute each step in order. You MUST include [DONE:n] in your response after com
516
509
  // Read the start prompt for clean handoff
517
510
  let startPrompt = '';
518
511
  try {
519
- const result = await pi.exec('cat', [startPromptPath]);
520
- if (result.code === 0) {
521
- startPrompt = result.stdout.trim();
522
- }
512
+ startPrompt = (await Bun.file(startPromptPath).text()).trim();
523
513
  } catch {
524
514
  // Fall through
525
515
  }
@@ -25,12 +25,15 @@ export type PlansManifest = Record<string, PlanEntry>;
25
25
 
26
26
  const PLANS_JSON = '.plans/plans.json';
27
27
 
28
- /** Read plans.json via pi.exec, returning current manifest (empty object if missing). */
29
- export async function readPlansJson(exec: (cmd: string, args: string[]) => Promise<{ code: number; stdout: string }>): Promise<PlansManifest> {
28
+ /** Read plans.json, returning current manifest (empty object if missing). */
29
+ export async function readPlansJson(): Promise<PlansManifest> {
30
30
  try {
31
- const result = await exec('cat', [PLANS_JSON]);
32
- if (result.code === 0 && result.stdout.trim()) {
33
- return JSON.parse(result.stdout) as PlansManifest;
31
+ const file = Bun.file(PLANS_JSON);
32
+ if (await file.exists()) {
33
+ const text = await file.text();
34
+ if (text.trim()) {
35
+ return JSON.parse(text) as PlansManifest;
36
+ }
34
37
  }
35
38
  } catch {
36
39
  // File doesn't exist or isn't valid JSON
@@ -43,6 +43,17 @@ const DESTRUCTIVE_PATTERNS = [
43
43
  /\bsystemctl\s+(start|stop|restart|enable|disable)/i,
44
44
  /\bservice\s+\S+\s+(start|stop|restart)/i,
45
45
  /\b(vim?|nano|emacs|code|subl)\b/i,
46
+ // Windows equivalents
47
+ /\bdel\b/i,
48
+ /\brd\b/i,
49
+ /\bcopy\b/i,
50
+ /\bmove\b/i,
51
+ /\bren\b/i,
52
+ /\brename\b/i,
53
+ /\bicacls\b/i,
54
+ /\battrib\b/i,
55
+ /\bpowershell\b/i,
56
+ /\bpwsh\b/i,
46
57
  ];
47
58
 
48
59
  // ── Safe read-only bash patterns (allowed in plan mode) ─────────────────────
@@ -98,6 +109,12 @@ const SAFE_PATTERNS = [
98
109
  /^\s*fd\b/,
99
110
  /^\s*bat\b/,
100
111
  /^\s*eza\b/,
112
+ // Windows equivalents
113
+ /^\s*dir\b/,
114
+ /^\s*where\b/,
115
+ /^\s*set\b/,
116
+ /^\s*systeminfo\b/,
117
+ /^\s*tasklist\b/,
101
118
  ];
102
119
 
103
120
  export function isSafeCommand(command: string): boolean {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dreki-gg/pi-plan-mode",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Two-phase planning workflow for pi — plan with claude-opus-4-6:medium, execute with gpt-5.5:low, with .plans/ file-based handoff",
5
5
  "keywords": [
6
6
  "pi-package"
@@ -36,6 +36,7 @@
36
36
  },
37
37
  "devDependencies": {
38
38
  "@types/node": "24",
39
+ "bun-types": "latest",
39
40
  "oxfmt": "^0.43.0",
40
41
  "oxlint": "^1.58.0",
41
42
  "typescript": "^6.0.0"