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 +89 -0
- package/package.json +45 -0
- package/pi-powershell-extension.ts +269 -0
- package/run-bg.ps1 +41 -0
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
|
+
}
|