pi-powershell 1.0.1

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,89 @@
1
+ # pi-powershell
2
+
3
+ PowerShell extension for [pi](https://pi.dev) - the terminal coding harness.
4
+
5
+ ## Features
6
+
7
+ - **Replaces bash with PowerShell** on Windows
8
+ - **Auto-translates bash commands** to PowerShell equivalents
9
+ - **Preserves native PowerShell** cmdlets
10
+
11
+ ## Auto-Translation Support
12
+
13
+ | Bash | PowerShell |
14
+ |------|------------|
15
+ | `ls`, `ll`, `la` | `Get-ChildItem` |
16
+ | `pwd` | `Get-Location` |
17
+ | `cat file` | `Get-Content file` |
18
+ | `echo "text"` | `Write-Output "text"` |
19
+ | `mkdir dir` | `New-Item -ItemType Directory` |
20
+ | `touch file` | `New-Item -ItemType File` |
21
+ | `rm -rf dir` | `Remove-Item -Recurse -Force` |
22
+ | `grep pattern file` | `Select-String -Pattern` |
23
+ | `ps` | `Get-Process` |
24
+ | `which cmd` | `Get-Command \| Select-Object -ExpandProperty Source` |
25
+ | `ls \| grep pattern` | `Get-ChildItem \| Where-Object` |
26
+ | `ls \| wc -l` | `Get-ChildItem \| Measure-Object -Line` |
27
+ | `ls \| head -n` | `Get-ChildItem \| Select-Object -First` |
28
+ | `$HOME`, `$PATH` | `$env:HOME`, `$env:PATH` |
29
+ | `export VAR=value` | `$env:VAR=value` |
30
+
31
+ Native PowerShell cmdlets pass through unchanged:
32
+ ```bash
33
+ !Get-Process | Select-Object -First 5
34
+ !Get-Service | Where-Object {$_.Status -eq 'Running'}
35
+ ```
36
+
37
+ ## Installation
38
+
39
+ ### Option 1: Install via npm
40
+ ```bash
41
+ npm install -g @marcfargas/pi-powershell
42
+ ```
43
+
44
+ ### Option 2: Add to your project
45
+ ```bash
46
+ cd your-project
47
+ pi install @marcfargas/pi-powershell
48
+ ```
49
+
50
+ ### Option 3: Local extension
51
+ ```bash
52
+ pi -e ./pi-powershell-extension.ts
53
+ ```
54
+
55
+ ## Requirements
56
+
57
+ - Windows (PowerShell 7+ or Windows PowerShell)
58
+ - Node.js 18+
59
+
60
+ ## Scripts (Windows)
61
+
62
+ | Command | Description |
63
+ |---------|-------------|
64
+ | `npm run serve` | Start static server (foreground) |
65
+ | `npm run serve:bg` | Start server in new window |
66
+ | `npm run dev:bg` | Start dev server in new window |
67
+ | `npm run http` | Simple HTTP server (port 8080) |
68
+ | `npm run http:bg` | HTTP server in new window |
69
+ | `npm run run -- "cmd"` | Run any command in background |
70
+ | `npm run kill` | Kill all node processes |
71
+
72
+ ### Examples
73
+
74
+ ```bash
75
+ # Start server in new window
76
+ npm run serve:bg
77
+
78
+ # Run any command in background
79
+ npm run run -- "npm run dev"
80
+ npm run run -- "node server.js --port 3000"
81
+ npm run run -- "npx next dev"
82
+
83
+ # Stop all node processes
84
+ npm run kill
85
+ ```
86
+
87
+ ## License
88
+
89
+ MIT
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "pi-powershell",
3
+ "version": "1.0.1",
4
+ "description": "PowerShell extension for pi - replaces bash with PowerShell on Windows and auto-translates bash commands",
5
+ "keywords": [
6
+ "pi-package",
7
+ "pi-extension",
8
+ "powershell",
9
+ "windows",
10
+ "bash-to-powershell"
11
+ ],
12
+ "scripts": {
13
+ "clean": "echo 'cleaning...'",
14
+ "build": "echo 'no build needed'",
15
+ "check": "echo 'no checks'",
16
+ "serve": "node node_modules/serve/build/main.js . -l 3000",
17
+ "serve:bg": "pwsh -File ./run-bg.ps1 \"node node_modules/serve/build/main.js . -l 3000\"",
18
+ "dev": "node node_modules/serve/build/main.js . -l 3000",
19
+ "dev:bg": "pwsh -File ./run-bg.ps1 \"node node_modules/serve/build/main.js . -l 3000\"",
20
+ "http": "node -e \"require('http').createServer((q,s)=>{s.writeHead(200,{'Content-Type':'text/html'});s.end('<h1>Hello</h1>')}).listen(8080);console.log('http://localhost:8080')\"",
21
+ "http:bg": "pwsh -File ./run-bg.ps1 \"node -e require('http').createServer((q,s)=>{s.writeHead(200,{Content-Type:'text/html'});s.end('<h1>Hello</h1>')}).listen(8080)\"",
22
+ "run": "pwsh -File ./run-bg.ps1",
23
+ "kill": "pwsh -Command \"Get-Process -Name node,cmd | Stop-Process -Force -ErrorAction SilentlyContinue; Get-Process -Name pwsh | Where-Object { \\$_.CommandLine -like '*serve*' } | Stop-Process -Force -ErrorAction SilentlyContinue\""
24
+ },
25
+ "pi": {
26
+ "extensions": [
27
+ "./pi-powershell-extension.ts"
28
+ ]
29
+ },
30
+ "author": "ngsoftware",
31
+ "license": "MIT",
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://github.com/ngsoftware/pi-powershell.git"
35
+ },
36
+ "engines": {
37
+ "node": ">=18.0.0"
38
+ },
39
+ "peerDependencies": {
40
+ "@earendil-works/pi-coding-agent": ">=0.75.0"
41
+ },
42
+ "dependencies": {
43
+ "serve": "^14.2.0"
44
+ }
45
+ }
@@ -0,0 +1,269 @@
1
+ /**
2
+ * PowerShell Extension for pi
3
+ *
4
+ * Replaces bash with PowerShell on Windows.
5
+ * Includes auto-translation of bash-style commands to PowerShell.
6
+ *
7
+ * Usage:
8
+ * pi -e ./pi-powershell-extension.ts
9
+ *
10
+ * Or add to settings.json:
11
+ * { "extensions": ["./path/to/pi-powershell-extension.ts"] }
12
+ */
13
+
14
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
15
+ import { existsSync } from "node:fs";
16
+ import { spawn } from "child_process";
17
+
18
+ export default function (pi: ExtensionAPI) {
19
+ const powershellPath = resolvePowerShell();
20
+
21
+ if (!powershellPath) {
22
+ console.warn("[pi-powershell] No PowerShell found. Bash will be used.");
23
+ return;
24
+ }
25
+
26
+ console.log(`[pi-powershell] Using: ${powershellPath}`);
27
+
28
+ // Intercept all user bash commands and redirect to PowerShell
29
+ pi.on("user_bash", (_event, _ctx) => {
30
+ return {
31
+ operations: createPowerShellOperations(powershellPath),
32
+ };
33
+ });
34
+ }
35
+
36
+ function resolvePowerShell(): string | null {
37
+ // Try PowerShell 7+ (pwsh) first
38
+ const pwshPaths = [
39
+ "C:/Program Files/PowerShell/7/pwsh.exe",
40
+ "C:/Program Files (x86)/PowerShell/7/pwsh.exe",
41
+ ];
42
+
43
+ for (const path of pwshPaths) {
44
+ if (path && existsSync(path)) {
45
+ return path;
46
+ }
47
+ }
48
+
49
+ // Fall back to Windows PowerShell
50
+ const windowsPowerShell = "C:/Windows/System32/WindowsPowerShell/v1.0/powershell.exe";
51
+ if (existsSync(windowsPowerShell)) {
52
+ return windowsPowerShell;
53
+ }
54
+
55
+ // Try PATH lookup
56
+ try {
57
+ const { spawnSync } = require("child_process");
58
+ const result = spawnSync("where", ["pwsh.exe"], { encoding: "utf-8", windowsHide: true });
59
+ if (result.status === 0 && result.stdout) {
60
+ const firstMatch = result.stdout.trim().split(/\r?\n/)[0];
61
+ if (firstMatch && existsSync(firstMatch)) {
62
+ return firstMatch;
63
+ }
64
+ }
65
+ } catch {
66
+ // Ignore
67
+ }
68
+
69
+ return null;
70
+ }
71
+
72
+ interface ExecOptions {
73
+ onData: (data: Buffer) => void;
74
+ signal?: AbortSignal;
75
+ timeout?: number;
76
+ env?: NodeJS.ProcessEnv;
77
+ }
78
+
79
+ function createPowerShellOperations(shellPath: string) {
80
+ return {
81
+ exec(
82
+ command: string,
83
+ cwd: string,
84
+ { onData, signal, timeout, env }: ExecOptions
85
+ ): Promise<{ exitCode: number | null }> {
86
+ return new Promise((resolve) => {
87
+ if (!existsSync(cwd)) {
88
+ resolve({ exitCode: 1 });
89
+ return;
90
+ }
91
+
92
+ // Auto-translate bash to PowerShell
93
+ const translatedCommand = translateBashToPowerShell(command);
94
+
95
+ // Set UTF-8 encoding for proper character handling
96
+ const psCommand = `[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; ${translatedCommand}`;
97
+
98
+ const child = spawn(shellPath, [
99
+ "-NoProfile",
100
+ "-NonInteractive",
101
+ "-Command",
102
+ psCommand,
103
+ ], {
104
+ cwd,
105
+ detached: false,
106
+ env: env ?? process.env,
107
+ stdio: ["ignore", "pipe", "pipe"],
108
+ windowsHide: true,
109
+ });
110
+
111
+ let exitCode: number | null = null;
112
+
113
+ child.stdout?.on("data", (data: Buffer) => onData(data));
114
+ child.stderr?.on("data", (data: Buffer) => onData(data));
115
+
116
+ child.on("error", () => {
117
+ exitCode = 1;
118
+ });
119
+
120
+ child.on("close", (code: number) => {
121
+ exitCode = code;
122
+ resolve({ exitCode });
123
+ });
124
+
125
+ if (timeout) {
126
+ const timer = setTimeout(() => child.kill(), timeout * 1000);
127
+ signal?.addEventListener("abort", () => clearTimeout(timer));
128
+ }
129
+
130
+ signal?.addEventListener("abort", () => child.kill());
131
+ });
132
+ },
133
+ };
134
+ }
135
+
136
+ /**
137
+ * Translate bash-style commands to PowerShell equivalents.
138
+ * Skips translation for full PowerShell cmdlets (Get-, Set-, etc.)
139
+ */
140
+ function translateBashToPowerShell(cmd: string): string {
141
+ // Skip translation for full PowerShell cmdlets
142
+ if (/^(Get|Set|New|Remove|Copy|Move|Test|Write|Read|Select|Where|Sort|Measure|Start|Stop|Import|Export)\s/.test(cmd)) {
143
+ return cmd;
144
+ }
145
+
146
+ let result = cmd;
147
+
148
+ // === Basic commands ===
149
+
150
+ // cd
151
+ result = result.replace(/^cd\s+(\S+)/, (_, p) => `Set-Location ${p.replace(/\//g, "\\")}`);
152
+
153
+ // pwd
154
+ if (result === "pwd") return "Get-Location";
155
+
156
+ // ls variants
157
+ result = result.replace(/^ll\b/, "Get-ChildItem -Force");
158
+ result = result.replace(/^la\b/, "Get-ChildItem -Force -Attributes Hidden");
159
+ result = result.replace(/^ls\s+(-[a-zA-Z]+)\s*$/, () => "Get-ChildItem");
160
+ result = result.replace(/^ls\s*$/, "Get-ChildItem");
161
+ result = result.replace(/^ls\s+(-[a-zA-Z]+)/, "Get-ChildItem");
162
+
163
+ // cat -> Get-Content
164
+ result = result.replace(/^cat\s+(\S+)/, (_, f) => `Get-Content ${f.replace(/\//g, "\\")}`);
165
+
166
+ // echo -> Write-Output
167
+ result = result.replace(/^echo\s+['"](.+)['"]/, (_, t) => `Write-Output "${t}"`);
168
+ result = result.replace(/^echo\s+(.+)$/, (_, t) => `Write-Output "${t}"`);
169
+
170
+ // mkdir
171
+ result = result.replace(/^mkdir\s+(?:-\w+\s+)?(\S+)/, (_, d) => `New-Item -ItemType Directory -Path "${d.replace(/\//g, "\\")}" -Force`);
172
+
173
+ // rm -rf
174
+ result = result.replace(/^rm\s+-rf\s+(\S+)/, (_, t) => `Remove-Item "${t.replace(/\//g, "\\")}" -Recurse -Force`);
175
+
176
+ // grep standalone -> Select-String
177
+ result = result.replace(/^grep\s+(-[a-zA-Z]+)?\s+['"](.+)['"]\s*(\S+)?/, (_, flags, pattern, file) => {
178
+ const ci = flags?.includes("i") ? " -CaseSensitive:$false" : "";
179
+ if (file) {
180
+ return `Select-String -Pattern "${pattern}" -Path "${file.replace(/\//g, "\\")}"${ci}`;
181
+ }
182
+ return `Select-String -Pattern "${pattern}"${ci}`;
183
+ });
184
+
185
+ // ps
186
+ if (/^ps\s*(?:aux|-ef)?\s*$/.test(result)) return "Get-Process";
187
+
188
+ // which
189
+ result = result.replace(/^which\s+(\S+)/, (_, c) => `Get-Command ${c} | Select-Object -ExpandProperty Source`);
190
+
191
+ // touch
192
+ result = result.replace(/^touch\s+(\S+)/, (_, f) => `New-Item -ItemType File -Path "${f.replace(/\//g, "\\")}" -Force`);
193
+
194
+ // clear
195
+ if (result === "clear") return "Clear-Host";
196
+
197
+ // env
198
+ if (result === "env") return "Get-ChildItem Env:";
199
+
200
+ // export
201
+ result = result.replace(/^export\s+(\w+)=(.+)/, (_, v, val) => `$env:${v}=${val.trim()}`);
202
+
203
+ // source / dot
204
+ result = result.replace(/^(?:source|\.)\s+(\S+)/, (_, file) => `. "${file.replace(/\//g, "\\")}"`);
205
+
206
+ // exit
207
+ result = result.replace(/^exit\s*(\d*)/, (_, code) => `exit ${code || 0}`);
208
+
209
+ // === Pipes ===
210
+
211
+ // ls | grep -> Get-ChildItem | Where-Object
212
+ result = result.replace(/^Get-ChildItem\s*\|\s*grep\s+(-i)?\s+['"]?(\S+?)['"]?\s*$/, (_, ci, pattern) => {
213
+ const caseF = ci ? " -CaseSensitive:$false" : "";
214
+ return `Get-ChildItem | Where-Object { $_.Name -match "${pattern}"${caseF} }`;
215
+ });
216
+
217
+ result = result.replace(/^ls\s*\|\s*grep\s+(-i)?\s+['"]?(\S+?)['"]?\s*$/, (_, ci, pattern) => {
218
+ const caseF = ci ? " -CaseSensitive:$false" : "";
219
+ return `Get-ChildItem | Where-Object { $_.Name -match "${pattern}"${caseF} }`;
220
+ });
221
+
222
+ // ls | head -> Get-ChildItem | Select-Object -First
223
+ result = result.replace(/^Get-ChildItem\s*\|\s*head\s+(?:-n\s*)?(\d+)/, (_, n) => `Get-ChildItem | Select-Object -First ${n}`);
224
+ result = result.replace(/^ls\s*\|\s*head\s+(?:-n\s*)?(\d+)/, (_, n) => `Get-ChildItem | Select-Object -First ${n}`);
225
+
226
+ // ls | wc -> Get-ChildItem | Measure-Object
227
+ result = result.replace(/\|\s*wc\s+-l\b/, " | Measure-Object -Line | Select-Object -ExpandProperty Lines");
228
+ result = result.replace(/\|\s*wc\b/, " | Measure-Object");
229
+
230
+ // cat | grep -> Select-String
231
+ result = result.replace(/^Get-Content\s+(\S+)\s*\|\s*grep\s+(-i)?\s+['"]?(\S+?)['"]?\s*$/, (_, file, ci, pattern) => {
232
+ const caseF = ci ? " -CaseSensitive:$false" : "";
233
+ return `Select-String -Path "${file.replace(/\//g, "\\")}" -Pattern "${pattern}"${caseF}`;
234
+ });
235
+
236
+ result = result.replace(/^cat\s+(\S+)\s*\|\s*grep\s+(-i)?\s+['"]?(\S+?)['"]?\s*$/, (_, file, ci, pattern) => {
237
+ const caseF = ci ? " -CaseSensitive:$false" : "";
238
+ return `Select-String -Path "${file.replace(/\//g, "\\")}" -Pattern "${pattern}"${caseF}`;
239
+ });
240
+
241
+ // | grep -> | Where-Object
242
+ result = result.replace(/\|\s*grep\s+(-i)?\s+['"]?(.+?)['"]?\s*$/g, (_, ci, pattern) => {
243
+ const caseF = ci ? " -CaseSensitive:$false" : "";
244
+ return ` | Where-Object { $_ -match "${pattern}"${caseF} }`;
245
+ });
246
+
247
+ // | head
248
+ result = result.replace(/\|\s*head\s+(?:-n\s*)?(\d+)/g, " | Select-Object -First $1");
249
+
250
+ // | tail
251
+ result = result.replace(/\|\s*tail\s+(?:-n\s*)?(\d+)/g, " | Select-Object -Last $1");
252
+
253
+ // | sort
254
+ result = result.replace(/\|\s*sort\b/g, " | Sort-Object");
255
+
256
+ // | uniq
257
+ result = result.replace(/\|\s*uniq\b/g, " | Select-Object -Unique");
258
+
259
+ // === Environment variables ===
260
+
261
+ result = result.replace(/\$([A-Z_][A-Z0-9_]{1,10})(?![a-zA-Z0-9_])/g, (m, v) => {
262
+ const known = ["HOME", "USER", "PATH", "PWD", "SHELL", "TERM", "USERNAME", "COMPUTERNAME", "USERPROFILE", "TEMP", "TMP"];
263
+ return known.includes(v) ? `$env:${v}` : m;
264
+ });
265
+
266
+ result = result.replace(/\$\{(\w+)\}/g, (_, v) => `$env:${v}`);
267
+
268
+ return result;
269
+ }
package/run-bg.ps1 ADDED
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env pwsh
2
+ # Generic background command runner
3
+ # Usage: pwsh -File ./run-bg.ps1 "npm run serve"
4
+ # Usage: pwsh -File ./run-bg.ps1 "node server.js --port 3000"
5
+
6
+ param(
7
+ [Parameter(Mandatory=$false, Position=0)]
8
+ [string]$Command = "npm run serve"
9
+ )
10
+
11
+ $ErrorActionPreference = "Stop"
12
+
13
+ # Parse the command
14
+ $parts = $Command -split '\s+'
15
+ $exe = $parts[0]
16
+ $args = $parts[1..($parts.Length - 1)]
17
+
18
+ # Set working directory to script location
19
+ $workDir = Split-Path -Parent $MyInvocation.MyCommand.Path
20
+ if (-not $workDir) {
21
+ $workDir = $PWD.Path
22
+ }
23
+
24
+ Write-Host "Running in background:" -ForegroundColor Cyan
25
+ Write-Host " Command: $Command" -ForegroundColor Gray
26
+ Write-Host " Directory: $workDir" -ForegroundColor Gray
27
+ Write-Host ""
28
+
29
+ # Start in new window
30
+ $proc = Start-Process -FilePath $exe -ArgumentList $args -PassThru -WindowStyle Normal -WorkingDirectory $workDir
31
+
32
+ Write-Host "PID: $($proc.Id)" -ForegroundColor Green
33
+ Write-Host "Press Ctrl+C to stop" -ForegroundColor Yellow
34
+
35
+ try {
36
+ $proc.WaitForExit()
37
+ } catch {
38
+ Write-Host ""
39
+ Write-Host "Stopping..." -ForegroundColor Yellow
40
+ Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue
41
+ }