@bacnh85/pi-windows-tools 0.1.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/README.md ADDED
@@ -0,0 +1,47 @@
1
+ # pi-windows-tools
2
+
3
+ Pi extension for Windows-native tool manipulation — shell profiles, path conversion, command execution, WSL bridge, safety policy, and developer tool discovery.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @bacnh85/pi-windows-tools
9
+ ```
10
+
11
+ ## Configuration
12
+
13
+ ```json
14
+ {
15
+ "extensions": ["pi-windows-tools"],
16
+ "windowsTools": {
17
+ "enabled": true,
18
+ "defaultShell": "pwsh",
19
+ "allowShellFallback": true,
20
+ "gitBashPath": null,
21
+ "wslDistro": null,
22
+ "preferWslFor": ["openwrt", "yocto", "linux-kernel", "embedded-linux"]
23
+ }
24
+ }
25
+ ```
26
+
27
+ Environment overrides:
28
+
29
+ ```
30
+ PI_WINDOWS_TOOLS_ENABLED=true
31
+ PI_WINDOWS_SHELL=pwsh|powershell|cmd|git-bash|wsl
32
+ PI_GIT_BASH_PATH=C:\Program Files\Git\bin\bash.exe
33
+ PI_WSL_DISTRO=Ubuntu-24.04
34
+ ```
35
+
36
+ ## Commands
37
+
38
+ - `pi windows doctor` — detect installed developer tools and system state
39
+ - `pi windows shell [shell]` — set default shell (pwsh/powershell/cmd/git-bash/wsl)
40
+
41
+ ## Shell priority
42
+
43
+ 1. pwsh (PowerShell 7+)
44
+ 2. powershell (Windows PowerShell 5.1)
45
+ 3. Git Bash
46
+ 4. cmd
47
+ 5. WSL (only when explicitly requested or project config prefers it)
@@ -0,0 +1,191 @@
1
+ # pi-windows-tools
2
+
3
+ ## Overview
4
+
5
+ `pi-windows-tools` is a Pi extension that gives the Pi Coding Agent excellent Windows-native tool manipulation capability — shell profiles, path conversion, command execution, WSL bridge, safety policy, and developer tool discovery.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install @bacnh85/pi-windows-tools
11
+ ```
12
+
13
+ Then add to your Pi configuration:
14
+
15
+ ```json
16
+ {
17
+ "extensions": ["pi-windows-tools"]
18
+ }
19
+ ```
20
+
21
+ ## Quick Start
22
+
23
+ After installation, Pi automatically detects your Windows shell environment and injects PowerShell syntax guidance. Run the doctor to see what's available:
24
+
25
+ ```
26
+ pi windows doctor
27
+ ```
28
+
29
+ ## Configuration
30
+
31
+ ```json
32
+ {
33
+ "windowsTools": {
34
+ "enabled": true,
35
+ "defaultShell": "pwsh",
36
+ "allowShellFallback": true,
37
+ "gitBashPath": null,
38
+ "wslDistro": null,
39
+ "preferWslFor": [
40
+ "openwrt",
41
+ "yocto",
42
+ "linux-kernel",
43
+ "embedded-linux"
44
+ ]
45
+ }
46
+ }
47
+ ```
48
+
49
+ ### Environment Variables
50
+
51
+ | Variable | Description |
52
+ |----------|-------------|
53
+ | `PI_WINDOWS_TOOLS_ENABLED` | Set to `false` to disable (`true` by default) |
54
+ | `PI_WINDOWS_SHELL` | Override default shell: `pwsh`, `powershell`, `cmd`, `git-bash`, `wsl` |
55
+ | `PI_GIT_BASH_PATH` | Explicit path to Git Bash executable |
56
+ | `PI_WSL_DISTRO` | Default WSL distro name |
57
+
58
+ ## Shell Selection
59
+
60
+ ### Priority Order
61
+
62
+ 1. **pwsh** (PowerShell 7+) — preferred, best developer experience
63
+ 2. **powershell** (Windows PowerShell 5.1) — built-in, widely available
64
+ 3. **git-bash** (Git for Windows Bash) — POSIX commands on Windows
65
+ 4. **cmd** (Command Prompt) — legacy, minimal capability
66
+ 5. **wsl** (WSL2) — only when explicitly selected or project config prefers it
67
+
68
+ ### Changing Shell
69
+
70
+ ```
71
+ pi windows shell pwsh
72
+ pi windows shell git-bash
73
+ pi windows shell wsl
74
+ ```
75
+
76
+ ## Commands
77
+
78
+ ### `pi windows doctor`
79
+
80
+ Detects installed developer tools and system configuration:
81
+
82
+ ```
83
+ Windows Tools Doctor
84
+ ━━━━━━━━━━━━━━━━━━━
85
+ OS: Windows_NT 10.0.22631
86
+ Architecture: x64
87
+ Default shell: pwsh
88
+
89
+ ── Tools ──
90
+ ✓ pwsh 7.4.0
91
+ ✓ powershell 5.1.22621
92
+ ✓ cmd
93
+ ✓ git 2.42.0.windows.2
94
+ ✓ bash (Git Bash) 5.2.15
95
+ ✓ wsl
96
+ ✓ node v22.0.0
97
+ ✓ npm 10.5.0
98
+ ✗ pnpm
99
+ ...
100
+
101
+ ── WSL Distros ──
102
+ • Ubuntu-24.04
103
+ • Debian
104
+
105
+ ── System Features ──
106
+ Long paths: enabled
107
+ Developer Mode: enabled
108
+ ```
109
+
110
+ ### `pi windows shell [shell]`
111
+
112
+ Show or set the default Windows shell.
113
+
114
+ ## Tools
115
+
116
+ The extension registers these tools:
117
+
118
+ | Tool | Description |
119
+ |------|-------------|
120
+ | `windows_shell_detect` | Detect available shells with versions |
121
+ | `windows_shell_exec` | Execute command through a shell |
122
+ | `windows_path_to_windows` | Convert POSIX/WSL path to Windows format |
123
+ | `windows_path_to_wsl` | Convert Windows path to WSL `/mnt/c/...` |
124
+ | `windows_path_to_gitbash` | Convert Windows path to Git Bash `/c/...` |
125
+ | `windows_path_quote` | Quote a path for a specific shell |
126
+ | `windows_safety_classify` | Check command for dangerous patterns |
127
+ | `windows_doctor` | Full doctor report |
128
+ | `windows_wsl_exec` | Execute command inside WSL |
129
+ | `windows_tool_discover` | Check if a tool is in PATH |
130
+ | `windows_wsl_list_distros` | List installed WSL distros |
131
+
132
+ ## Safety Policy
133
+
134
+ Commands matching dangerous patterns (rm -rf, diskpart, format, git push --force, npm publish, etc.) are classified as requiring confirmation. Sensitive file paths (.env, .pem, .ssh, .aws, etc.) trigger the same behavior.
135
+
136
+ ## Path Conversion Examples
137
+
138
+ | Windows | Git Bash | WSL |
139
+ |---------|----------|-----|
140
+ | `C:\Users\bacnh\project` | `/c/Users/bacnh/project` | `/mnt/c/Users/bacnh/project` |
141
+ | `D:\work\repo` | `/d/work/repo` | `/mnt/d/work/repo` |
142
+ | `C:\Program Files\Git` | `/c/Program Files/Git` | `/mnt/c/Program Files/Git` |
143
+
144
+ ## Recommended Setup
145
+
146
+ 1. Install [PowerShell 7](https://github.com/PowerShell/PowerShell) (recommended)
147
+ 2. Install [Git for Windows](https://git-scm.com/download/win) (includes Git Bash)
148
+ 3. Install [Windows Terminal](https://learn.microsoft.com/en-us/windows/terminal/install) (optional, better UX)
149
+ 4. Optional: Install [WSL2](https://learn.microsoft.com/en-us/windows/wsl/install) with Ubuntu
150
+
151
+ ## Project-Specific Config
152
+
153
+ For Linux-native projects (OpenWrt, Yocto, kernel builds):
154
+
155
+ ```json
156
+ {
157
+ "windowsTools": {
158
+ "defaultShell": "wsl",
159
+ "wslDistro": "Ubuntu-24.04",
160
+ "preferWslFor": ["openwrt", "yocto", "linux-kernel"]
161
+ }
162
+ }
163
+ ```
164
+
165
+ ## Common Syntax Mistakes
166
+
167
+ When using PowerShell (default), these commands **don't work**:
168
+
169
+ ```
170
+ ❌ NODE_ENV=test npm test → ✅ $env:NODE_ENV = "test"; npm test
171
+ ❌ rm -rf dist → ✅ Remove-Item -Recurse -Force .\dist
172
+ ❌ cp README.md dist/ → ✅ Copy-Item .\README.md .\dist\
173
+ ❌ grep -R "abc" src/ → ✅ Select-String -Path .\src\* -Pattern "abc"
174
+ ❌ export FOO=bar → ✅ $env:FOO = "bar"
175
+ ```
176
+
177
+ When using Git Bash, POSIX commands work but paths need conversion:
178
+
179
+ ```
180
+ # Git Bash: use /c/... instead of C:\...
181
+ cd /c/Users/bacnh/project
182
+ ls -la
183
+ ```
184
+
185
+ When using WSL, always use `/mnt/c/...` for Windows paths:
186
+
187
+ ```
188
+ # WSL: Windows C: drive is at /mnt/c/
189
+ cd /mnt/c/Users/bacnh/project
190
+ make
191
+ ```
@@ -0,0 +1,96 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { Type } from "@sinclair/typebox";
3
+ import { detectShell, detectAllShells, getDefaultShell } from "./lib/shell-detect";
4
+ import type { WindowsShellKind } from "./lib/shell-detect";
5
+ import { executeCommand as execCmd } from "./lib/shell-exec";
6
+ import * as pathUtils from "./lib/path-utils";
7
+ import { classifyCommand } from "./lib/safety";
8
+ import { runDoctor, formatDoctorReport } from "./lib/doctor";
9
+ import { buildShellGuidance } from "./lib/prompts";
10
+ import { execFileSync } from "node:child_process";
11
+
12
+ const sk = Type.Union([Type.Literal("pwsh"), Type.Literal("powershell"), Type.Literal("cmd"), Type.Literal("git-bash"), Type.Literal("wsl")]);
13
+ const tp = Type.Optional(Type.Number({ description: "Timeout in ms." }));
14
+ const cs = { timeout_ms: tp };
15
+ const EOL = "\n";
16
+
17
+ function tr(text: string) { return Promise.resolve({ content: [{ type: "text" as const, text }], details: {} }); }
18
+ function rs(shell?: WindowsShellKind): WindowsShellKind {
19
+ if (shell) return shell;
20
+ const e = process.env.PI_WINDOWS_SHELL as WindowsShellKind | undefined;
21
+ if (e && ["pwsh", "powershell", "cmd", "git-bash", "wsl"].includes(e)) return e;
22
+ return getDefaultShell().kind;
23
+ }
24
+
25
+ // ponytail: in-memory audit log, no cap, sequence-counter instead of timestamps
26
+ const _log: { shell: string; command: string; exitCode: number | null; timedOut: boolean }[] = [];
27
+ function _fmt() {
28
+ if (!_log.length) return "No commands executed yet.";
29
+ return _log.map((e, i) => {
30
+ const tag = e.timedOut ? " [TIMED OUT]" : "";
31
+ const cmd = e.command.length > 300 ? e.command.slice(0, 300) + "…" : e.command;
32
+ return `[${i + 1}] ${e.shell} exit:${e.exitCode}${tag}\n cmd: ${cmd}`;
33
+ }).join(EOL + EOL);
34
+ }
35
+
36
+ export default function piWindowsToolsExtension(pi: ExtensionAPI) {
37
+ pi.registerTool({ name: "windows_shell_detect", label: "Windows: Detect Shells", description: "Detect available Windows shells.", promptSnippet: "Detect available Windows shells", promptGuidelines: ["Use to check what shells are available."], parameters: Type.Object({ ...cs }),
38
+ execute() { return tr(detectAllShells().map(s => ` ${s.available ? "✓" : "✗"} ${s.displayName}${s.version ? ` ${s.version}` : ""}`).join(EOL)); } });
39
+
40
+ pi.registerTool({ name: "windows_shell_exec", label: "Windows: Execute Command", description: "Execute a command through a Windows shell.", promptSnippet: "Execute a command through a Windows shell",
41
+ promptGuidelines: ["Use instead of generic bash on Windows.", 'Use shell:"wsl" for WSL.', "Dangerous commands require confirmation."],
42
+ parameters: Type.Object({ command: Type.String(), shell: Type.Optional(sk), cwd: Type.Optional(Type.String()), timeout_ms: tp }),
43
+ async execute(_id, p, _s, _u, ctx) {
44
+ const opts = { shell: rs(p.shell as WindowsShellKind | undefined), cwd: p.cwd || ctx?.cwd || process.cwd(), timeoutMs: p.timeout_ms };
45
+ const safe = classifyCommand(p.command);
46
+ const r = await execCmd(p.command, opts);
47
+ _log.push({ shell: opts.shell as string, command: p.command, exitCode: r.exitCode, timedOut: r.timedOut });
48
+ let o = `Exit code: ${r.exitCode}${EOL}`;
49
+ if (r.timedOut) o += "Status: TIMED OUT" + EOL;
50
+ if (r.cancelled) o += "Status: CANCELLED" + EOL;
51
+ if (r.stdout) o += `${EOL}── stdout ──${EOL}${r.stdout}${EOL}`;
52
+ if (r.stderr) o += `${EOL}── stderr ──${EOL}${r.stderr}${EOL}`;
53
+ if (safe.risk === "confirm") o += `${EOL}⚠️ ${safe.reasons.join("; ")}`;
54
+ return tr(o);
55
+ } });
56
+
57
+ pi.registerTool({ name: "windows_audit_log", label: "Windows: Audit Log", description: "Show command history and exit codes.", promptSnippet: "Show Windows command audit log", promptGuidelines: ["Use to see what was executed."],
58
+ parameters: Type.Object({ clear: Type.Optional(Type.Boolean({ description: "Clear after viewing." })), ...cs }),
59
+ execute(_id, p) { const out = _fmt(); if (p.clear) _log.length = 0; return tr(out); } });
60
+
61
+ pi.registerTool({ name: "windows_path_to_windows", label: "Windows: Convert to Windows Format", description: "Convert POSIX/WSL path to C:\\...", promptSnippet: "Convert path to Windows", promptGuidelines: ["Use when you have /c/ or /mnt/c/ path."], parameters: Type.Object({ path: Type.String(), ...cs }),
62
+ execute(_id, p) { return tr(pathUtils.toWindowsPath(p.path)); } });
63
+ pi.registerTool({ name: "windows_path_to_wsl", label: "Windows: Convert to WSL", description: "Convert Windows path to /mnt/c/...", promptSnippet: "Convert path to WSL", promptGuidelines: ["Use to pass Windows path to WSL."], parameters: Type.Object({ path: Type.String(), ...cs }),
64
+ execute(_id, p) { return tr(pathUtils.toWslPath(p.path)); } });
65
+ pi.registerTool({ name: "windows_path_to_gitbash", label: "Windows: Convert to Git Bash", description: "Convert Windows path to /c/...", promptSnippet: "Convert path to Git Bash", promptGuidelines: ["Use to pass Windows path to Git Bash."], parameters: Type.Object({ path: Type.String(), ...cs }),
66
+ execute(_id, p) { return tr(pathUtils.toGitBashPath(p.path)); } });
67
+ pi.registerTool({ name: "windows_path_quote", label: "Windows: Quote Path", description: "Quote a path for a Windows shell.", promptSnippet: "Quote path for shell", promptGuidelines: ["Each shell has different quoting rules."], parameters: Type.Object({ path: Type.String(), shell: Type.Optional(sk), ...cs }),
68
+ execute(_id, p) { return tr(pathUtils.quoteForShell(p.path, rs(p.shell as WindowsShellKind | undefined))); } });
69
+ pi.registerTool({ name: "windows_safety_classify", label: "Windows: Classify Safety", description: "Check if command is dangerous.", promptSnippet: "Classify command safety", promptGuidelines: ["Returns 'safe' or 'confirm'."], parameters: Type.Object({ command: Type.String(), ...cs }),
70
+ execute(_id, p) { const r = classifyCommand(p.command); return tr(`Risk: ${r.risk}${r.reasons.length ? "\nReasons:\n × " + r.reasons.join("\n × ") : ""}`); } });
71
+ pi.registerTool({ name: "windows_doctor", label: "Windows: Doctor", description: "Detect installed developer tools.", promptSnippet: "Run Windows doctor", promptGuidelines: ["Checks PATH, WSL, long paths, dev mode."], parameters: Type.Object({ format: Type.Optional(Type.Union([Type.Literal("text"), Type.Literal("json")])), ...cs }),
72
+ execute(_id, p) { const r = runDoctor(); return tr(p.format === "json" ? JSON.stringify(r, null, 2) : formatDoctorReport(r)); } });
73
+ pi.registerTool({ name: "windows_tool_discover", label: "Windows: Discover Tool", description: "Check if a tool is in PATH.", promptSnippet: "Check tool availability", promptGuidelines: ["Use to verify a tool is installed."], parameters: Type.Object({ name: Type.String(), ...cs }),
74
+ execute(_id, p) { try { const r = execFileSync("where", [p.name], { encoding: "utf8", timeout: 3000 }); return tr(`✓ ${p.name} at:\n${r.split(/\r?\n/).filter(Boolean).map(x => ` ${x}`).join(EOL)}`); } catch { return tr(`✗ ${p.name} not in PATH`); } } });
75
+ pi.registerTool({ name: "windows_wsl_list_distros", label: "Windows: List WSL Distros", description: "List installed WSL distros.", promptSnippet: "List WSL distros", promptGuidelines: ["See what distros are available."], parameters: Type.Object({ ...cs }),
76
+ execute() { try { const r = execFileSync("wsl.exe", ["-l", "-q"], { encoding: "utf8", timeout: 5000 }); const d = r.split(/\r?\n/).map(s => s.trim()).filter(s => s && !s.toLowerCase().includes("noinstall") && !s.startsWith("Windows")); return tr(d.length ? "Installed WSL distros:\n × " + d.join("\n × ") : "No WSL distros found."); } catch { return tr("WSL not available."); } } });
77
+
78
+ pi.registerCommand("windows-doctor", { description: "Run Windows Tools Doctor.", handler: async (_a, ctx) => { ctx?.ui?.notify?.("Windows Doctor complete.", "info"); process.stdout.write(formatDoctorReport(runDoctor()) + EOL); } });
79
+ pi.registerCommand("windows-shell", { description: "Show/set default shell.", handler: async (a, ctx) => {
80
+ const arg = (a || "").trim().toLowerCase();
81
+ if (arg) {
82
+ if (!["pwsh", "powershell", "cmd", "git-bash", "wsl"].includes(arg)) { ctx?.ui?.notify?.("Invalid shell", "warning"); return; }
83
+ const info = detectShell(arg as WindowsShellKind);
84
+ if (!info.available) { ctx?.ui?.notify?.(`${info.displayName} unavailable.`, "warning"); return; }
85
+ process.env.PI_WINDOWS_SHELL = arg;
86
+ ctx?.ui?.notify?.(`Shell: ${info.displayName}`, "info"); return;
87
+ }
88
+ const c = getDefaultShell();
89
+ process.stdout.write(`Current shell: ${c.displayName} (${c.kind})\nExecutable: ${c.executable}\n`);
90
+ } });
91
+
92
+ pi.on("before_agent_start", async (event) => {
93
+ if (process.env.PI_WINDOWS_TOOLS_ENABLED === "false" || process.platform !== "win32") return;
94
+ return { systemPrompt: `${event.systemPrompt}\n\n---\n\n## Windows Environment\n\n${buildShellGuidance(rs())}` };
95
+ });
96
+ }
@@ -0,0 +1,34 @@
1
+ export interface AuditEntry {
2
+ timestamp: string;
3
+ shell: string;
4
+ command: string;
5
+ cwd: string;
6
+ exitCode: number | null;
7
+ timedOut: boolean;
8
+ cancelled: boolean;
9
+ }
10
+
11
+ const log: AuditEntry[] = [];
12
+ const MAX = 500;
13
+
14
+ export function record(e: AuditEntry): void {
15
+ log.push(e);
16
+ if (log.length > MAX) log.shift();
17
+ }
18
+
19
+ export function entries(): readonly AuditEntry[] { return log; }
20
+ export function clear(): void { log.length = 0; }
21
+
22
+ function truncate(s: string, n: number): string {
23
+ return s.length <= n ? s : s.slice(0, n) + "\u2026";
24
+ }
25
+
26
+ export function format(items?: readonly AuditEntry[]): string {
27
+ const list = items ?? log;
28
+ if (list.length === 0) return "No commands executed yet.";
29
+ return list.map((e, i) => {
30
+ const flags = [e.timedOut && "TIMED OUT", e.cancelled && "CANCELLED"].filter(Boolean);
31
+ const tag = flags.length ? " [" + flags.join(", ") + "]" : "";
32
+ return "[" + (i + 1) + "] " + e.timestamp + " " + e.shell + " exit:" + e.exitCode + tag + "\n cmd: " + truncate(e.command, 300) + "\n cwd: " + e.cwd;
33
+ }).join("\n\n");
34
+ }
@@ -0,0 +1,58 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import * as os from "node:os";
3
+
4
+ export interface ToolInfo { name: string; found: boolean; path?: string; version?: string; }
5
+ export interface DoctorReport {
6
+ os: string; osVersion: string; architecture: string; defaultShell: string;
7
+ tools: ToolInfo[]; wslDistros: string[]; longPathsEnabled: boolean | null; developerMode: boolean | null;
8
+ }
9
+
10
+ function which(cmd: string): string | null {
11
+ try { return execFileSync("where", [cmd], { encoding: "utf8", timeout: 3000 }).split(/\r?\n/)[0]?.trim() || null; } catch { return null; }
12
+ }
13
+ function checkTool(name: string, cmd: string, va: string[] = ["--version"]): ToolInfo {
14
+ const p = which(cmd); let v: string | undefined;
15
+ if (p) try { v = execFileSync(p, va, { encoding: "utf8", timeout: 3000 }).split(/\r?\n/)[0]?.trim(); } catch {}
16
+ return { name, found: !!p, path: p || undefined, version: v };
17
+ }
18
+
19
+ function wslDistros(): string[] {
20
+ try { return execFileSync("wsl.exe", ["-l", "-q"], { encoding: "utf8", timeout: 5000 }).split(/\r?\n/).map(s => s.trim()).filter(s => s && !s.toLowerCase().includes("noinstall") && !s.startsWith("Windows")); } catch { return []; }
21
+ }
22
+ function regDword(key: string, val: string): boolean | null {
23
+ try {
24
+ const out = execFileSync("reg", ["query", key, "/v", val], { encoding: "utf8", timeout: 3000 });
25
+ const m = out.match(new RegExp(`${val}\\s+REG_DWORD\\s+(0x[0-9a-f]+)`, "i"));
26
+ return m ? parseInt(m[1], 16) === 1 : null;
27
+ } catch { return null; }
28
+ }
29
+
30
+ export function runDoctor(): DoctorReport {
31
+ const osInfo = { os: os.type(), osVersion: os.release(), architecture: os.arch() };
32
+ const tools = [
33
+ checkTool("pwsh", "pwsh"), checkTool("powershell", "powershell"), checkTool("cmd", "cmd"),
34
+ checkTool("git", "git"), checkTool("bash (Git Bash)", "bash"), checkTool("wsl", "wsl"),
35
+ checkTool("node", "node"), checkTool("npm", "npm"), checkTool("pnpm", "pnpm"),
36
+ checkTool("yarn", "yarn"), checkTool("python", "python"), checkTool("py launcher", "py"),
37
+ checkTool("dotnet", "dotnet"), checkTool("cmake", "cmake"), checkTool("ninja", "ninja"),
38
+ checkTool("winget", "winget"), checkTool("choco", "choco"), checkTool("scoop", "scoop"),
39
+ checkTool("ssh", "ssh"), checkTool("msbuild", "msbuild"), checkTool("cl", "cl"),
40
+ checkTool("devenv", "devenv"), checkTool("reg", "reg"), checkTool("sc", "sc"), checkTool("netsh", "netsh"),
41
+ ];
42
+ const wslTool = tools.find(t => t.name === "wsl");
43
+ return {
44
+ ...osInfo, defaultShell: process.env.PI_WINDOWS_SHELL || "pwsh",
45
+ tools, wslDistros: wslTool?.found ? wslDistros() : [],
46
+ longPathsEnabled: regDword("HKLM\\SYSTEM\\CurrentControlSet\\Control\\FileSystem", "LongPathsEnabled"),
47
+ developerMode: regDword("HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\AppModelUnlock", "AllowDevelopmentWithoutDevLicense"),
48
+ };
49
+ }
50
+
51
+ export function formatDoctorReport(r: DoctorReport): string {
52
+ const lines = [`Windows Tools Doctor`, `━━━━━━━━━━━━━━━━━━━`, `OS: ${r.os} ${r.osVersion}`, `Architecture: ${r.architecture}`, `Default shell: ${r.defaultShell}`, "", "── Tools ──"];
53
+ for (const t of r.tools) lines.push(` ${t.found ? "✓" : "✗"} ${t.name}${t.version ? ` ${t.version}` : ""}`);
54
+ if (r.wslDistros.length) { lines.push("", "── WSL Distros ──"); for (const d of r.wslDistros) lines.push(` • ${d}`); }
55
+ lines.push("", "── System Features ──", ` Long paths: ${f3(r.longPathsEnabled)}`, ` Developer Mode: ${f3(r.developerMode)}`);
56
+ return lines.join("\n");
57
+ }
58
+ function f3(v: boolean | null): string { return v === true ? "enabled" : v === false ? "disabled" : "unknown"; }
@@ -0,0 +1,104 @@
1
+ import type { WindowsShellKind } from "./shell-detect";
2
+
3
+ /**
4
+ * Convert a Windows path (C:\foo\bar) to a Unix-style path (/c/foo/bar).
5
+ * Handles forward-slash Windows paths, UNC paths, and long-path prefixes.
6
+ */
7
+ export function toPosixPath(windowsPath: string): string {
8
+ let p = windowsPath.replace(/\\/g, "/");
9
+
10
+ // Strip \\?\ long-path prefix
11
+ p = p.replace(/^\/\/\?\/+/, "");
12
+
13
+ // Handle device paths (\\.\COM1 → \\.\COM1, keep as-is)
14
+ if (p.match(/^\/{2}\.\//)) return p;
15
+
16
+ // Handle UNC paths (\\server\share → //server/share)
17
+ if (p.startsWith("//") && !p.startsWith("//?/")) {
18
+ return p.replace(/^\/\/([^/])/, "//$1");
19
+ }
20
+
21
+ // Handle C:/foo → /c/foo or C:foo\bar → /c/foo\bar
22
+ if (/^[A-Za-z]:/.test(p)) {
23
+ return p.replace(/^([A-Za-z]):\/?/, (_, d) => `/${d.toLowerCase()}/`);
24
+ }
25
+
26
+ return p;
27
+ }
28
+
29
+ /**
30
+ * Convert a POSIX path (/c/foo/bar) to a Windows path (C:\foo\bar).
31
+ */
32
+ export function toWindowsPath(posixPath: string): string {
33
+ if (/^[A-Za-z]:[\\/]/.test(posixPath)) {
34
+ return posixPath.replace(/\//g, "\\");
35
+ }
36
+ const wsl = posixPath.match(/^\/mnt\/([a-zA-Z])\/(.+)/);
37
+ if (wsl) return `${wsl[1].toUpperCase()}:\\${wsl[2].replace(/\//g, "\\")}`;
38
+ const gb = posixPath.match(/^\/([a-zA-Z])\/(.+)/);
39
+ if (gb) return `${gb[1].toUpperCase()}:\\${gb[2].replace(/\//g, "\\")}`;
40
+ return posixPath.replace(/\//g, "\\");
41
+ }
42
+
43
+ /**
44
+ * Convert a Windows path to Git Bash format (/c/Users/...).
45
+ */
46
+ export function toGitBashPath(windowsPath: string): string {
47
+ return toPosixPath(windowsPath);
48
+ }
49
+
50
+ /**
51
+ * Convert a Windows path to WSL format (/mnt/c/Users/...).
52
+ * Ignores input already in WSL or POSIX format.
53
+ */
54
+ export function toWslPath(windowsPath: string): string {
55
+ if (windowsPath.startsWith("/mnt/")) return windowsPath;
56
+ const posix = toPosixPath(windowsPath);
57
+ return posix.startsWith("/mnt/") ? posix : `/mnt${posix}`;
58
+ }
59
+
60
+ /**
61
+ * Normalize a Windows path: backslashes, uppercase drive, resolve . and ..,
62
+ * collapse repeated separators.
63
+ */
64
+ export function normalizeWindowsPath(path: string): string {
65
+ if (!path) return path;
66
+ const isUnc = path.startsWith("\\\\") || path.startsWith("//");
67
+ let n = path.replace(/\//g, "\\").replace(/^([a-z]):\\/, (_, d) => `${d.toUpperCase()}:\\`);
68
+ if (isUnc) n = "\\" + n.slice(2).replace(/\\\\+/g, "\\");
69
+ else n = n.replace(/\\\\+/g, "\\");
70
+ const parts = n.split("\\");
71
+ const r: string[] = [];
72
+ for (const p of parts) {
73
+ if (p === "." || p === "") continue;
74
+ if (p === "..") { if (r.length > 1) r.pop(); continue; }
75
+ r.push(p);
76
+ }
77
+ return r.join("\\") || (n.endsWith("\\") ? "\\" : "");
78
+ }
79
+
80
+ /**
81
+ * Check if a path looks like a Windows absolute path
82
+ * (C:\..., \\server\..., \\?\..., \\.\...).
83
+ */
84
+ export function isWindowsAbsolutePath(path: string): boolean {
85
+ return /^[A-Za-z]:[\\/]|^[\\/]{2}(?:[\\/]?[?.]|[^\\/]+[\\/])/.test(path);
86
+ }
87
+
88
+ /**
89
+ * Quote a path for a specific Windows shell.
90
+ */
91
+ export function quoteForShell(path: string, shell: WindowsShellKind): string {
92
+ if (!path) return '""';
93
+ const q = path.includes(" ") || path.includes("\t") || path.includes('"');
94
+ switch (shell) {
95
+ case "pwsh":
96
+ case "powershell":
97
+ return q ? `'${path.replace(/'/g, "''")}'` : path;
98
+ case "cmd":
99
+ return q ? `"${path.replace(/"/g, '""')}"` : path;
100
+ case "git-bash":
101
+ case "wsl":
102
+ return q ? `'${path.replace(/'/g, "'\\''")}'` : path;
103
+ }
104
+ }
@@ -0,0 +1,115 @@
1
+ import type { WindowsShellKind } from "./shell-detect";
2
+
3
+ export interface ShellPrompt {
4
+ kind: WindowsShellKind;
5
+ guidance: string;
6
+ syntaxNote: string;
7
+ forbiddenPatterns: string[];
8
+ preferredCommands: string[];
9
+ }
10
+
11
+ const pwshPrompt: ShellPrompt = {
12
+ kind: "pwsh",
13
+ guidance: "You are running on Windows using PowerShell 7+.",
14
+ syntaxNote: `
15
+ Use PowerShell syntax:
16
+ - Environment variables: $env:NAME = "value"
17
+ - Filesystem: Get-ChildItem, Get-Content, Set-Content, Copy-Item, Move-Item, Remove-Item, New-Item
18
+ - Text search: Select-String -Path <file> -Pattern <regex>
19
+ - Command separator: ;
20
+ - Native exit codes: $LASTEXITCODE
21
+ - Quote paths with spaces
22
+ - Prefer Node.js internal file tools for editing files
23
+ `,
24
+ forbiddenPatterns: [
25
+ "rm -rf (use Remove-Item -Recurse -Force)",
26
+ "cp (use Copy-Item)",
27
+ "mv (use Move-Item)",
28
+ "grep (use Select-String)",
29
+ "export VAR=value (use $env:VAR = 'value')",
30
+ "sed/awk (use PowerShell string operations or -replace)",
31
+ ],
32
+ preferredCommands: ["Get-ChildItem", "Get-Content", "Set-Content", "Copy-Item", "Move-Item", "Remove-Item", "New-Item", "Select-String"],
33
+ };
34
+
35
+ const cmdPrompt: ShellPrompt = {
36
+ kind: "cmd",
37
+ guidance: "You are running on Windows using cmd.exe.",
38
+ syntaxNote: `
39
+ Use cmd.exe syntax:
40
+ - Environment variables: set NAME=value
41
+ - Filesystem: dir, type, copy, move, del, rmdir
42
+ - Prefer PowerShell if available for complex operations
43
+ - Command separator: &&
44
+ - Paths with spaces should be double-quoted
45
+ `,
46
+ forbiddenPatterns: [
47
+ "rm (use del or rmdir)",
48
+ "ls (use dir)",
49
+ "export (use set)",
50
+ ],
51
+ preferredCommands: ["dir", "type", "copy", "move", "del", "rmdir", "mkdir"],
52
+ };
53
+
54
+ const gitBashPrompt: ShellPrompt = {
55
+ kind: "git-bash",
56
+ guidance: "You are running on Windows using Git Bash.",
57
+ syntaxNote: `
58
+ Use POSIX-style commands (bash syntax):
59
+ - Environment: export NAME=value
60
+ - Commands: ls, cp, mv, rm, grep, sed, awk
61
+ - Windows paths need conversion: C:\\Users\\... → /c/Users/...
62
+ - Be careful when passing paths to Windows-native tools
63
+ `,
64
+ forbiddenPatterns: [],
65
+ preferredCommands: ["ls", "cp", "mv", "rm", "grep", "find", "cat", "echo"],
66
+ };
67
+
68
+ const wslPrompt: ShellPrompt = {
69
+ kind: "wsl",
70
+ guidance: "You are running through WSL2 (Windows Subsystem for Linux).",
71
+ syntaxNote: `
72
+ Use Linux shell syntax (bash):
73
+ - Environment: export NAME=value
74
+ - Commands: ls, cp, mv, rm, grep, sed, awk
75
+ - Workspace paths should use /mnt/c/... convention
76
+ - Do NOT call Windows-only tools unless explicitly routed through powershell.exe or cmd.exe
77
+ - Use WSL for Linux-native projects: OpenWrt, Yocto, kernel builds
78
+ `,
79
+ forbiddenPatterns: [
80
+ "C:\\ paths (convert to /mnt/c/...)",
81
+ ],
82
+ preferredCommands: ["ls", "cp", "mv", "rm", "grep", "find", "cat", "echo", "make", "gcc"],
83
+ };
84
+
85
+ export function getPromptForShell(kind: WindowsShellKind): ShellPrompt {
86
+ switch (kind) {
87
+ case "pwsh": return pwshPrompt;
88
+ case "powershell": return pwshPrompt; // same guidance
89
+ case "cmd": return cmdPrompt;
90
+ case "git-bash": return gitBashPrompt;
91
+ case "wsl": return wslPrompt;
92
+ }
93
+ }
94
+
95
+ export function buildShellGuidance(kind: WindowsShellKind): string {
96
+ const prompt = getPromptForShell(kind);
97
+ const lines = [
98
+ prompt.guidance,
99
+ prompt.syntaxNote.trim(),
100
+ ];
101
+
102
+ if (prompt.forbiddenPatterns.length > 0) {
103
+ lines.push("", "Avoid these commands (they don't work in this shell):");
104
+ for (const p of prompt.forbiddenPatterns) {
105
+ lines.push(` • ${p}`);
106
+ }
107
+ }
108
+
109
+ lines.push("", "Preferred commands:");
110
+ for (const cmd of prompt.preferredCommands) {
111
+ lines.push(` • ${cmd}`);
112
+ }
113
+
114
+ return lines.join("\n");
115
+ }