@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 +47 -0
- package/docs/windows-tools.md +191 -0
- package/extensions/index.ts +96 -0
- package/extensions/lib/audit.ts +34 -0
- package/extensions/lib/doctor.ts +58 -0
- package/extensions/lib/path-utils.ts +104 -0
- package/extensions/lib/prompts.ts +115 -0
- package/extensions/lib/safety.ts +107 -0
- package/extensions/lib/shell-detect.ts +73 -0
- package/extensions/lib/shell-exec.ts +159 -0
- package/extensions/package.json +3 -0
- package/extensions/test/audit.test.ts +47 -0
- package/extensions/test/doctor.test.ts +89 -0
- package/extensions/test/path-utils.test.ts +144 -0
- package/extensions/test/safety.test.ts +96 -0
- package/extensions/test/shell-detect.test.ts +53 -0
- package/extensions/test/shell-exec.test.ts +76 -0
- package/package.json +58 -0
- package/skills/pi-windows-tools/SKILL.md +53 -0
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
|
+
}
|