@bacnh85/pi-windows-tools 0.2.0 → 0.3.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.
@@ -1,7 +1,5 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import { Type } from "@sinclair/typebox";
3
- import { readFileSync, writeFileSync } from "node:fs";
4
- import { resolve } from "node:path";
5
3
  import { detectShell, detectAllShells, getDefaultShell } from "./lib/shell-detect";
6
4
  import type { WindowsShellKind } from "./lib/shell-detect";
7
5
  import { executeCommand as execCmd } from "./lib/shell-exec";
@@ -56,30 +54,6 @@ export default function piWindowsToolsExtension(pi: ExtensionAPI) {
56
54
  return tr(o);
57
55
  } });
58
56
 
59
- // ── File edit tool (reliable replacement for built-in edit) ──
60
- pi.registerTool({ name: "windows_file_edit", label: "Windows: Edit File", description: "Replace literal text in a file. Reads actual file bytes so no line-ending/whitespace mismatch issues.",
61
- promptSnippet: "Edit a file reliably using Node.js",
62
- promptGuidelines: [
63
- "Use instead of the built-in edit tool on all platforms.",
64
- "Reads actual file bytes so replacements always match regardless of line endings.",
65
- "Throws a clear error if oldText is not found.",
66
- ],
67
- parameters: Type.Object({
68
- path: Type.String({ description: "Path to the file (absolute or relative to cwd)." }),
69
- oldText: Type.String({ description: "Literal text to find and replace." }),
70
- newText: Type.String({ description: "Replacement text." }),
71
- ...cs,
72
- }),
73
- execute(_id, p, _s, _u, ctx) {
74
- const filePath = resolve(ctx?.cwd || process.cwd(), p.path);
75
- const content = readFileSync(filePath, "utf8");
76
- const updated = content.replace(p.oldText, p.newText);
77
- if (updated === content) throw new Error(`Not found or no change: ${filePath}`);
78
-
79
- writeFileSync(filePath, updated, "utf8");
80
- return tr(`Edited ${filePath}`);
81
- } });
82
-
83
57
  // ── Audit tools ──
84
58
  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."],
85
59
  parameters: Type.Object({ clear: Type.Optional(Type.Boolean({ description: "Clear after viewing." })), ...cs }),
@@ -91,7 +65,7 @@ export default function piWindowsToolsExtension(pi: ExtensionAPI) {
91
65
  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 }),
92
66
  execute(_id, p) { return tr(pathUtils.toWslPath(p.path)); } });
93
67
  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 }),
94
- execute(_id, p) { return tr(pathUtils.toGitBashPath(p.path)); } });
68
+ execute(_id, p) { return tr(pathUtils.toPosixPath(p.path)); } });
95
69
  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 }),
96
70
  execute(_id, p) { return tr(pathUtils.quoteForShell(p.path, rs(p.shell as WindowsShellKind | undefined))); } });
97
71
 
@@ -40,13 +40,6 @@ export function toWindowsPath(posixPath: string): string {
40
40
  return posixPath.replace(/\//g, "\\");
41
41
  }
42
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
43
  /**
51
44
  * Convert a Windows path to WSL format (/mnt/c/Users/...).
52
45
  * Ignores input already in WSL or POSIX format.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bacnh85/pi-windows-tools",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Pi extension for Windows-native tool manipulation — shell profiles, path conversion, command execution, WSL bridge, safety policy, and developer tool discovery.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -29,7 +29,9 @@
29
29
  },
30
30
  "files": [
31
31
  "README.md",
32
- "extensions/",
32
+ "extensions/index.ts",
33
+ "extensions/lib/",
34
+ "extensions/package.json",
33
35
  "skills/",
34
36
  "docs/"
35
37
  ],
@@ -1,47 +0,0 @@
1
- import { describe, it } from "mocha";
2
- import { expect } from "chai";
3
- import { record, entries, clear, format } from "../lib/audit";
4
-
5
- describe("audit", () => {
6
- beforeEach(() => clear());
7
-
8
- it("starts empty", () => {
9
- expect(format()).to.equal("No commands executed yet.");
10
- });
11
-
12
- it("records an entry", () => {
13
- record({ timestamp: "a", shell: "pwsh", command: "echo hi", cwd: "/", exitCode: 0, timedOut: false, cancelled: false });
14
- expect(entries()).to.have.length(1);
15
- expect(entries()[0].command).to.equal("echo hi");
16
- });
17
-
18
- it("clear removes all entries", () => {
19
- record({ timestamp: "a", shell: "pwsh", command: "c1", cwd: "/", exitCode: 0, timedOut: false, cancelled: false });
20
- clear();
21
- expect(entries()).to.have.length(0);
22
- });
23
-
24
- it("format shows command and exit code", () => {
25
- record({ timestamp: "t", shell: "pwsh", command: "echo hi", cwd: "/", exitCode: 0, timedOut: false, cancelled: false });
26
- const out = format();
27
- expect(out).to.include("echo hi");
28
- expect(out).to.include("exit:0");
29
- });
30
-
31
- it("format shows timed out flag", () => {
32
- record({ timestamp: "a", shell: "cmd", command: "sleep 10", cwd: "/", exitCode: null, timedOut: true, cancelled: false });
33
- const out = format();
34
- expect(out).to.include("TIMED OUT");
35
- });
36
-
37
- it("format shows cancelled flag", () => {
38
- record({ timestamp: "a", shell: "cmd", command: "x", cwd: "/", exitCode: null, timedOut: false, cancelled: true });
39
- const out = format();
40
- expect(out).to.include("CANCELLED");
41
- });
42
-
43
- it("truncates long commands", () => {
44
- record({ timestamp: "a", shell: "pwsh", command: "x".repeat(500), cwd: "/", exitCode: 0, timedOut: false, cancelled: false });
45
- expect(format()).to.include("\u2026");
46
- });
47
- });
@@ -1,89 +0,0 @@
1
- import { describe, it } from "mocha";
2
- import { expect } from "chai";
3
- import { formatDoctorReport } from "../lib/doctor";
4
- import type { DoctorReport } from "../lib/doctor";
5
-
6
- describe("doctor", () => {
7
-
8
- const sampleReport: DoctorReport = {
9
- os: "Windows_NT",
10
- osVersion: "10.0.22631",
11
- architecture: "x64",
12
- defaultShell: "pwsh",
13
- tools: [
14
- { name: "pwsh", found: true, path: "C:\\Program Files\\PowerShell\\7\\pwsh.exe", version: "7.4.0" },
15
- { name: "powershell", found: true, path: "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe", version: "5.1.22621" },
16
- { name: "cmd", found: true, path: "C:\\Windows\\System32\\cmd.exe" },
17
- { name: "git", found: true, path: "C:\\Program Files\\Git\\cmd\\git.exe", version: "2.42.0.windows.2" },
18
- { name: "bash (Git Bash)", found: true, path: "C:\\Program Files\\Git\\bin\\bash.exe", version: "5.2.15" },
19
- { name: "wsl", found: true, path: "C:\\Windows\\System32\\wsl.exe" },
20
- { name: "node", found: true, path: "C:\\Program Files\\nodejs\\node.exe", version: "v22.0.0" },
21
- { name: "npm", found: true, path: "C:\\Program Files\\nodejs\\npm.cmd", version: "10.5.0" },
22
- { name: "pnpm", found: false },
23
- { name: "yarn", found: false },
24
- { name: "python", found: true, path: "C:\\Python312\\python.exe", version: "3.12.0" },
25
- { name: "py launcher", found: true, path: "C:\\Windows\\py.exe", version: "3.12" },
26
- { name: "dotnet", found: true, path: "C:\\Program Files\\dotnet\\dotnet.exe", version: "8.0.100" },
27
- { name: "cmake", found: false },
28
- { name: "ninja", found: false },
29
- { name: "winget", found: true, path: "C:\\Users\\me\\AppData\\Local\\Microsoft\\WindowsApps\\winget.exe" },
30
- { name: "choco", found: false },
31
- { name: "scoop", found: false },
32
- { name: "ssh", found: true, path: "C:\\Windows\\System32\\OpenSSH\\ssh.exe" },
33
- { name: "msbuild", found: false },
34
- { name: "cl", found: false },
35
- { name: "devenv", found: false },
36
- { name: "reg", found: true, path: "C:\\Windows\\System32\\reg.exe" },
37
- { name: "sc", found: true, path: "C:\\Windows\\System32\\sc.exe" },
38
- { name: "netsh", found: true, path: "C:\\Windows\\System32\\netsh.exe" },
39
- ],
40
- wslDistros: ["Ubuntu-24.04", "Debian"],
41
- longPathsEnabled: true,
42
- developerMode: true,
43
- };
44
-
45
- it("formatDoctorReport produces expected header", () => {
46
- const output = formatDoctorReport(sampleReport);
47
- expect(output).to.include("Windows Tools Doctor");
48
- expect(output).to.include("OS: Windows_NT 10.0.22631");
49
- expect(output).to.include("Architecture: x64");
50
- });
51
-
52
- it("includes tool status with ✓ and ✗", () => {
53
- const output = formatDoctorReport(sampleReport);
54
- expect(output).to.include("✓ pwsh");
55
- expect(output).to.include("✗ pnpm");
56
- });
57
-
58
- it("includes WSL distros", () => {
59
- const output = formatDoctorReport(sampleReport);
60
- expect(output).to.include("Ubuntu-24.04");
61
- expect(output).to.include("Debian");
62
- });
63
-
64
- it("includes long paths and dev mode", () => {
65
- const output = formatDoctorReport(sampleReport);
66
- expect(output).to.include("Long paths: enabled");
67
- expect(output).to.include("Developer Mode: enabled");
68
- });
69
-
70
- it("handles empty tools list", () => {
71
- const empty: DoctorReport = {
72
- os: "Windows_NT",
73
- osVersion: "",
74
- architecture: "x64",
75
- defaultShell: "cmd",
76
- tools: [],
77
- wslDistros: [],
78
- longPathsEnabled: null,
79
- developerMode: null,
80
- };
81
- const output = formatDoctorReport(empty);
82
- expect(output).to.include("Long paths: unknown");
83
- });
84
-
85
- it("includes version when present", () => {
86
- const output = formatDoctorReport(sampleReport);
87
- expect(output).to.include("7.4.0");
88
- });
89
- });
@@ -1,144 +0,0 @@
1
- import { describe, it } from "mocha";
2
- import { expect } from "chai";
3
- import {
4
- toPosixPath,
5
- toWindowsPath,
6
- toWslPath,
7
- toGitBashPath,
8
- normalizeWindowsPath,
9
- isWindowsAbsolutePath,
10
- quoteForShell,
11
- } from "../lib/path-utils";
12
- import type { WindowsShellKind } from "../lib/shell-detect";
13
-
14
- describe("path-utils", () => {
15
-
16
- describe("toPosixPath", () => {
17
- it("converts C:\\foo\\bar to /c/foo/bar", () => {
18
- expect(toPosixPath("C:\\foo\\bar")).to.equal("/c/foo/bar");
19
- });
20
- it("converts C:/foo/bar to /c/foo/bar", () => {
21
- expect(toPosixPath("C:/foo/bar")).to.equal("/c/foo/bar");
22
- });
23
- it("converts D:\\path with spaces to /d/path with spaces", () => {
24
- expect(toPosixPath("D:\\path with spaces")).to.equal("/d/path with spaces");
25
- });
26
- it("handles lowercase drive letter", () => {
27
- expect(toPosixPath("c:\\windows\\system32")).to.equal("/c/windows/system32");
28
- });
29
- it("strips \\\\?\\ long-path prefix", () => {
30
- expect(toPosixPath("\\\\?\\C:\\foo")).to.equal("/c/foo");
31
- });
32
- it("handles drive-relative path C:foo\\bar", () => {
33
- expect(toPosixPath("C:foo\\bar")).to.equal("/c/foo/bar");
34
- });
35
- });
36
-
37
- describe("toWindowsPath", () => {
38
- it("converts /c/foo/bar to C:\\foo\\bar", () => {
39
- expect(toWindowsPath("/c/foo/bar")).to.equal("C:\\foo\\bar");
40
- });
41
- it("converts /mnt/c/foo/bar to C:\\foo\\bar", () => {
42
- expect(toWindowsPath("/mnt/c/foo/bar")).to.equal("C:\\foo\\bar");
43
- });
44
- it("preserves relative paths", () => {
45
- expect(toWindowsPath("relative/path")).to.equal("relative\\path");
46
- });
47
- it("handles lowercase drive", () => {
48
- expect(toWindowsPath("/d/work/repo")).to.equal("D:\\work\\repo");
49
- });
50
- it("preserves Windows-native paths already in C:\\ format", () => {
51
- expect(toWindowsPath("C:\\Users\\me")).to.equal("C:\\Users\\me");
52
- });
53
- });
54
-
55
- describe("toWslPath", () => {
56
- it("converts C:\\Users\\me to /mnt/c/Users/me", () => {
57
- expect(toWslPath("C:\\Users\\me")).to.equal("/mnt/c/Users/me");
58
- });
59
- it("converts D:\\work to /mnt/d/work", () => {
60
- expect(toWslPath("D:\\work")).to.equal("/mnt/d/work");
61
- });
62
- it("does not double-wrap already-WSL paths", () => {
63
- expect(toWslPath("/mnt/c/Users/me")).to.equal("/mnt/c/Users/me");
64
- });
65
- it("wraps Git Bash paths to /mnt/", () => {
66
- expect(toWslPath("/c/foo")).to.equal("/mnt/c/foo");
67
- });
68
- });
69
-
70
- describe("toGitBashPath", () => {
71
- it("converts C:\\Users to /c/Users", () => {
72
- expect(toGitBashPath("C:\\Users")).to.equal("/c/Users");
73
- });
74
- it("same as toPosixPath", () => {
75
- expect(toGitBashPath("D:\\temp")).to.equal(toPosixPath("D:\\temp"));
76
- });
77
- });
78
-
79
- describe("normalizeWindowsPath", () => {
80
- it("converts forward slashes to backslashes", () => {
81
- expect(normalizeWindowsPath("C:/foo/bar")).to.equal("C:\\foo\\bar");
82
- });
83
- it("uppercases drive letter", () => {
84
- expect(normalizeWindowsPath("c:\\windows")).to.equal("C:\\windows");
85
- });
86
- it("resolves . and ..", () => {
87
- expect(normalizeWindowsPath("C:\\foo\\.\\bar\\..\\baz")).to.equal("C:\\foo\\baz");
88
- });
89
- it("deduplicates backslashes", () => {
90
- expect(normalizeWindowsPath("C:\\foo\\\\\\bar")).to.equal("C:\\foo\\bar");
91
- });
92
- });
93
-
94
- describe("isWindowsAbsolutePath", () => {
95
- it("detects C:\\ as absolute", () => {
96
- expect(isWindowsAbsolutePath("C:\\foo")).to.be.true;
97
- });
98
- it("detects C:/ as absolute", () => {
99
- expect(isWindowsAbsolutePath("C:/foo")).to.be.true;
100
- });
101
- it("detects UNC as absolute", () => {
102
- expect(isWindowsAbsolutePath("\\\\server\\share")).to.be.true;
103
- });
104
- it("detects \\\\?\\ long path prefix as absolute", () => {
105
- expect(isWindowsAbsolutePath("\\\\?\\C:\\foo")).to.be.true;
106
- });
107
- it("detects \\\\.\\ device paths as absolute", () => {
108
- expect(isWindowsAbsolutePath("\\\\.\\COM1")).to.be.true;
109
- });
110
- it("rejects relative paths", () => {
111
- expect(isWindowsAbsolutePath("relative\\path")).to.be.false;
112
- });
113
- it("rejects POSIX absolute paths", () => {
114
- expect(isWindowsAbsolutePath("/c/foo")).to.be.false;
115
- });
116
- });
117
-
118
- describe("quoteForShell", () => {
119
- const shells: WindowsShellKind[] = ["pwsh", "powershell", "cmd", "git-bash", "wsl"];
120
-
121
- it("does not quote paths without spaces", () => {
122
- for (const s of shells) {
123
- expect(quoteForShell("C:\\foo\\bar", s)).to.equal("C:\\foo\\bar");
124
- }
125
- });
126
- it("quotes paths with spaces for PowerShell", () => {
127
- expect(quoteForShell("C:\\Program Files\\Git", "pwsh")).to.include("'");
128
- });
129
- it("quotes paths with spaces for cmd", () => {
130
- expect(quoteForShell("C:\\Program Files\\Git", "cmd")).to.include('"');
131
- });
132
- it("quotes paths with spaces for git-bash", () => {
133
- expect(quoteForShell("/c/Program Files", "git-bash")).to.include("'");
134
- });
135
- it("quotes paths with spaces for wsl", () => {
136
- expect(quoteForShell("/mnt/c/Program Files", "wsl")).to.include("'");
137
- });
138
- it("handles empty path", () => {
139
- for (const s of shells) {
140
- expect(quoteForShell("", s)).to.equal('""');
141
- }
142
- });
143
- });
144
- });
@@ -1,96 +0,0 @@
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
- });
@@ -1,53 +0,0 @@
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
- });
@@ -1,76 +0,0 @@
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
- });