@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.
@@ -0,0 +1,96 @@
1
+ import { describe, it } from "mocha";
2
+ import { expect } from "chai";
3
+ import { classifyCommand, isSensitivePath } from "../lib/safety";
4
+
5
+ describe("safety", () => {
6
+
7
+ describe("classifyCommand", () => {
8
+ it("returns safe for echo", () => {
9
+ const r = classifyCommand("echo hello");
10
+ expect(r.risk).to.equal("safe");
11
+ });
12
+
13
+ it("returns confirm for rm -rf", () => {
14
+ const r = classifyCommand("rm -rf /some/dir");
15
+ expect(r.risk).to.equal("confirm");
16
+ expect(r.reasons.length).to.be.greaterThan(0);
17
+ });
18
+
19
+ it("returns confirm for Remove-Item -Recurse -Force", () => {
20
+ const r = classifyCommand("Remove-Item -Recurse -Force C:\\temp");
21
+ expect(r.risk).to.equal("confirm");
22
+ });
23
+
24
+ it("returns confirm for git push --force", () => {
25
+ const r = classifyCommand("git push --force origin main");
26
+ expect(r.risk).to.equal("confirm");
27
+ });
28
+
29
+ it("returns confirm for git clean -fdx", () => {
30
+ const r = classifyCommand("git clean -fdx");
31
+ expect(r.risk).to.equal("confirm");
32
+ });
33
+
34
+ it("returns confirm for npm publish", () => {
35
+ const r = classifyCommand("npm publish");
36
+ expect(r.risk).to.equal("confirm");
37
+ });
38
+
39
+ it("returns confirm for diskpart", () => {
40
+ const r = classifyCommand("diskpart");
41
+ expect(r.risk).to.equal("confirm");
42
+ });
43
+
44
+ it("returns confirm for format command", () => {
45
+ const r = classifyCommand("format D: /fs:ntfs");
46
+ expect(r.risk).to.equal("confirm");
47
+ });
48
+
49
+ it("returns confirm for takeown", () => {
50
+ const r = classifyCommand("takeown /f C:\\somefile");
51
+ expect(r.risk).to.equal("confirm");
52
+ });
53
+
54
+ it("detects sensitive file paths in command", () => {
55
+ const r = classifyCommand("cat .env");
56
+ expect(r.risk).to.equal("confirm");
57
+ expect(r.reasons.some(r => r.includes("Environment"))).to.be.true;
58
+ });
59
+
60
+ it("detects SSH key paths", () => {
61
+ const r = classifyCommand("cat ~/.ssh/id_rsa");
62
+ expect(r.risk).to.equal("confirm");
63
+ });
64
+
65
+ it("returns safe for dir listing", () => {
66
+ const r = classifyCommand("Get-ChildItem -Recurse -Filter *.ts");
67
+ expect(r.risk).to.equal("safe");
68
+ });
69
+
70
+ it("returns safe for git status", () => {
71
+ const r = classifyCommand("git status");
72
+ expect(r.risk).to.equal("safe");
73
+ });
74
+ });
75
+
76
+ describe("isSensitivePath", () => {
77
+ it("detects .env", () => {
78
+ expect(isSensitivePath("C:\\project\\.env")).to.be.true;
79
+ });
80
+ it("detects .env.local", () => {
81
+ expect(isSensitivePath("C:\\project\\.env.local")).to.be.true;
82
+ });
83
+ it("detects .pem files", () => {
84
+ expect(isSensitivePath("C:\\keys\\cert.pem")).to.be.true;
85
+ });
86
+ it("detects .ssh dir", () => {
87
+ expect(isSensitivePath("C:\\Users\\me\\.ssh\\config")).to.be.true;
88
+ });
89
+ it("detects .aws dir", () => {
90
+ expect(isSensitivePath("/home/me/.aws/credentials")).to.be.true;
91
+ });
92
+ it("allows normal files", () => {
93
+ expect(isSensitivePath("C:\\project\\src\\index.ts")).to.be.false;
94
+ });
95
+ });
96
+ });
@@ -0,0 +1,53 @@
1
+ import { describe, it } from "mocha";
2
+ import { expect } from "chai";
3
+ import { detectAllShells, detectShell, getAvailableShells, getDefaultShell } from "../lib/shell-detect";
4
+ import type { WindowsShellKind } from "../lib/shell-detect";
5
+
6
+ describe("shell-detect", () => {
7
+ it("detectAllShells returns 5 entries", () => {
8
+ const shells = detectAllShells();
9
+ expect(shells).to.have.length(5);
10
+ const kinds = shells.map(s => s.kind);
11
+ expect(kinds).to.include.members(["pwsh", "powershell", "cmd", "git-bash", "wsl"]);
12
+ });
13
+
14
+ it("each shell has kind, displayName, executable, available", () => {
15
+ for (const s of detectAllShells()) {
16
+ expect(s.kind).to.be.a("string");
17
+ expect(s.displayName).to.be.a("string");
18
+ expect(s.executable).to.be.a("string");
19
+ expect(s.available).to.be.a("boolean");
20
+ }
21
+ });
22
+
23
+ it("cmd is available on Windows", function () {
24
+ if (process.platform !== "win32") this.skip();
25
+ const cmd = detectShell("cmd");
26
+ expect(cmd.available).to.be.true;
27
+ expect(cmd.executable).to.match(/cmd(\.exe)?$/i);
28
+ });
29
+
30
+ it("detectShell returns info for each kind", () => {
31
+ const kinds: WindowsShellKind[] = ["pwsh", "powershell", "cmd", "git-bash", "wsl"];
32
+ for (const k of kinds) {
33
+ const info = detectShell(k);
34
+ expect(info.kind).to.equal(k);
35
+ expect(info.executable).to.be.a("string").and.not.empty;
36
+ }
37
+ });
38
+
39
+ it("getAvailableShells returns only available shells", function () {
40
+ if (process.platform !== "win32") this.skip();
41
+ const avail = getAvailableShells();
42
+ for (const s of avail) {
43
+ expect(s.available).to.be.true;
44
+ }
45
+ });
46
+
47
+ it("getDefaultShell returns a shell", function () {
48
+ if (process.platform !== "win32") this.skip();
49
+ const def = getDefaultShell();
50
+ expect(def.kind).to.be.a("string");
51
+ expect(def.available).to.be.true;
52
+ });
53
+ });
@@ -0,0 +1,76 @@
1
+ import { describe, it } from "mocha";
2
+ import { expect } from "chai";
3
+ import { buildShellArgs, mergeEnv } from "../lib/shell-exec";
4
+
5
+ describe("shell-exec", () => {
6
+
7
+ describe("buildShellArgs", () => {
8
+ it("pwsh: builds correct args", () => {
9
+ const { exe, args } = buildShellArgs("pwsh", "echo hello");
10
+ expect(exe).to.match(/pwsh(\.exe)?$/i);
11
+ expect(args).to.include("-NoLogo");
12
+ expect(args).to.include("-NoProfile");
13
+ expect(args).to.include("-NonInteractive");
14
+ expect(args).to.include("-ExecutionPolicy");
15
+ expect(args).to.include("Bypass");
16
+ expect(args).to.include("-Command");
17
+ expect(args).to.include("echo hello");
18
+ });
19
+
20
+ it("powershell: same args as pwsh", () => {
21
+ const { exe, args } = buildShellArgs("powershell", "Get-ChildItem");
22
+ expect(exe).to.match(/powershell(\.exe)?$/i);
23
+ expect(args).to.include("-NoLogo");
24
+ expect(args).to.include("-Command");
25
+ expect(args).to.include("Get-ChildItem");
26
+ });
27
+
28
+ it("cmd: builds /c command", () => {
29
+ const { exe, args } = buildShellArgs("cmd", "dir");
30
+ expect(args).to.deep.equal(["/c", "dir"]);
31
+ });
32
+
33
+ it("git-bash: builds -lc command", () => {
34
+ const { exe, args } = buildShellArgs("git-bash", "ls -la");
35
+ expect(args).to.deep.equal(["-lc", "ls -la"]);
36
+ });
37
+
38
+ it("wsl: builds wsl.exe bash -lc command", () => {
39
+ const { exe, args } = buildShellArgs("wsl", "uname -a");
40
+ expect(exe).to.equal("wsl.exe");
41
+ expect(args).to.deep.equal(["--", "bash", "-lc", "uname -a"]);
42
+ });
43
+
44
+ it("wsl with distro: includes -d flag", () => {
45
+ const { args } = buildShellArgs("wsl", "echo hi", "Ubuntu-24.04");
46
+ expect(args).to.deep.equal(["-d", "Ubuntu-24.04", "--", "bash", "-lc", "echo hi"]);
47
+ });
48
+ });
49
+
50
+ describe("mergeEnv", () => {
51
+ it("includes all process.env keys", () => {
52
+ const merged = mergeEnv({});
53
+ expect(merged.PATH || merged.Path).to.be.a("string");
54
+ });
55
+
56
+ it("custom env overrides process.env", () => {
57
+ const merged = mergeEnv({ NODE_ENV: "test" });
58
+ expect(merged.NODE_ENV).to.equal("test");
59
+ });
60
+
61
+ it("deduplicates case-insensitively", () => {
62
+ // Simulate process.env having "PATH" and custom having "Path"
63
+ // mergeEnv should not produce both
64
+ const merged = mergeEnv({ Path: "/custom/path" });
65
+ // Should have either PATH or Path, not both duplicated
66
+ const pathKeys = Object.keys(merged).filter(k => k.toLowerCase() === "path");
67
+ expect(pathKeys.length).to.equal(1);
68
+ expect(merged[pathKeys[0]]).to.equal("/custom/path");
69
+ });
70
+
71
+ it("handles empty custom env", () => {
72
+ const merged = mergeEnv({});
73
+ expect(Object.keys(merged).length).to.be.greaterThan(0);
74
+ });
75
+ });
76
+ });
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@bacnh85/pi-windows-tools",
3
+ "version": "0.1.0",
4
+ "description": "Pi extension for Windows-native tool manipulation — shell profiles, path conversion, command execution, WSL bridge, safety policy, and developer tool discovery.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "publishConfig": {
8
+ "access": "public"
9
+ },
10
+ "homepage": "https://github.com/bacnh85/pi-extensions#readme",
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/bacnh85/pi-extensions.git",
14
+ "directory": "pi-windows-tools"
15
+ },
16
+ "keywords": [
17
+ "pi-package",
18
+ "pi-extension",
19
+ "windows",
20
+ "powershell",
21
+ "wsl",
22
+ "cmd",
23
+ "shell",
24
+ "developer-tools"
25
+ ],
26
+ "scripts": {
27
+ "test": "cd extensions && mocha",
28
+ "typecheck": "tsc --noEmit"
29
+ },
30
+ "files": [
31
+ "README.md",
32
+ "extensions/",
33
+ "skills/",
34
+ "docs/"
35
+ ],
36
+ "pi": {
37
+ "extensions": [
38
+ "./extensions/index.ts"
39
+ ],
40
+ "skills": [
41
+ "./skills"
42
+ ]
43
+ },
44
+ "peerDependencies": {
45
+ "@earendil-works/pi-coding-agent": "*",
46
+ "@sinclair/typebox": "*"
47
+ },
48
+ "devDependencies": {
49
+ "@sinclair/typebox": "^0.34.41",
50
+ "@types/chai": "^4.3.20",
51
+ "@types/mocha": "^10.0.10",
52
+ "@types/node": "^22.0.0",
53
+ "chai": "^4.5.0",
54
+ "mocha": "^10.8.2",
55
+ "tsx": "^4.22.4",
56
+ "typescript": "^5.0.0"
57
+ }
58
+ }
@@ -0,0 +1,53 @@
1
+ ---
2
+ name: pi-windows-tools
3
+ description: Provides Windows-native tool manipulation: shell profiles, path conversion, command execution, WSL bridge, safety policy, and developer tool discovery. Enables Pi to work correctly with PowerShell, cmd, Git Bash, and WSL.
4
+ ---
5
+
6
+ # pi-windows-tools
7
+
8
+ Provides Windows-native tool manipulation: shell profiles, path conversion, command execution, WSL bridge, safety policy, and developer tool discovery. Enables Pi to work correctly with PowerShell, cmd, Git Bash, and WSL.
9
+
10
+ ## When to use
11
+
12
+ - You are working on Windows and Pi needs to execute shell commands
13
+ - You need to convert paths between Windows, Git Bash, and WSL formats
14
+ - You need to know what developer tools are installed (doctor)
15
+ - You want safety checks on dangerous Windows commands
16
+ - You need to run commands inside WSL
17
+
18
+ ## Configuration
19
+
20
+ ```json
21
+ {
22
+ "extensions": ["pi-windows-tools"],
23
+ "windowsTools": {
24
+ "enabled": true,
25
+ "defaultShell": "pwsh"
26
+ }
27
+ }
28
+ ```
29
+
30
+ ## Tools
31
+
32
+ | Tool | What it does |
33
+ |------|-------------|
34
+ | `windows_shell_detect` | List available shells with versions |
35
+ | `windows_shell_exec` | Execute a command through a specific shell |
36
+ | `windows_path_to_windows` | Convert `/c/` or `/mnt/c/` to `C:\` |
37
+ | `windows_path_to_wsl` | Convert `C:\` to `/mnt/c/` |
38
+ | `windows_path_to_gitbash` | Convert `C:\` to `/c/` |
39
+ | `windows_path_quote` | Quote a path for a specific shell |
40
+ | `windows_doctor` | Detect installed developer tools |
41
+ | `windows_wsl_exec` | Run a Linux command in WSL |
42
+ | `windows_safety_classify` | Check command for dangerous operations |
43
+ | `windows_tool_discover` | Check if a tool exists in PATH |
44
+ | `windows_wsl_list_distros` | Show installed WSL distros |
45
+
46
+ ## Commands
47
+
48
+ - `pi windows doctor` — full system health report
49
+ - `pi windows shell [name]` — show or set default shell
50
+
51
+ ## Prompt guidance
52
+
53
+ On Windows, Pi automatically injects shell-specific syntax guidance (PowerShell by default) so it doesn't generate Bash-only commands.