@bacnh85/pi-windows-tools 0.1.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.
@@ -12,7 +12,6 @@ import { execFileSync } from "node:child_process";
12
12
  const sk = Type.Union([Type.Literal("pwsh"), Type.Literal("powershell"), Type.Literal("cmd"), Type.Literal("git-bash"), Type.Literal("wsl")]);
13
13
  const tp = Type.Optional(Type.Number({ description: "Timeout in ms." }));
14
14
  const cs = { timeout_ms: tp };
15
- const EOL = "\n";
16
15
 
17
16
  function tr(text: string) { return Promise.resolve({ content: [{ type: "text" as const, text }], details: {} }); }
18
17
  function rs(shell?: WindowsShellKind): WindowsShellKind {
@@ -22,20 +21,21 @@ function rs(shell?: WindowsShellKind): WindowsShellKind {
22
21
  return getDefaultShell().kind;
23
22
  }
24
23
 
25
- // ponytail: in-memory audit log, no cap, sequence-counter instead of timestamps
24
+ // ponytail: in-memory audit log
26
25
  const _log: { shell: string; command: string; exitCode: number | null; timedOut: boolean }[] = [];
27
26
  function _fmt() {
28
27
  if (!_log.length) return "No commands executed yet.";
29
28
  return _log.map((e, i) => {
30
29
  const tag = e.timedOut ? " [TIMED OUT]" : "";
31
- const cmd = e.command.length > 300 ? e.command.slice(0, 300) + "" : e.command;
30
+ const cmd = e.command.length > 300 ? e.command.slice(0, 300) + "\u2026" : e.command;
32
31
  return `[${i + 1}] ${e.shell} exit:${e.exitCode}${tag}\n cmd: ${cmd}`;
33
- }).join(EOL + EOL);
32
+ }).join("\n\n");
34
33
  }
35
34
 
36
35
  export default function piWindowsToolsExtension(pi: ExtensionAPI) {
36
+ // ── Shell tools ──
37
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)); } });
38
+ execute() { return tr(detectAllShells().map(s => ` ${s.available ? "\u2713" : "\u2717"} ${s.displayName}${s.version ? " " + s.version : ""}`).join("\n")); } });
39
39
 
40
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
41
  promptGuidelines: ["Use instead of generic bash on Windows.", 'Use shell:"wsl" for WSL.', "Dangerous commands require confirmation."],
@@ -45,37 +45,44 @@ export default function piWindowsToolsExtension(pi: ExtensionAPI) {
45
45
  const safe = classifyCommand(p.command);
46
46
  const r = await execCmd(p.command, opts);
47
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("; ")}`;
48
+ let o = `Exit code: ${r.exitCode}\n`;
49
+ if (r.timedOut) o += "Status: TIMED OUT\n";
50
+ if (r.cancelled) o += "Status: CANCELLED\n";
51
+ if (r.stdout) o += `\n--- stdout ---\n${r.stdout}\n`;
52
+ if (r.stderr) o += `\n--- stderr ---\n${r.stderr}\n`;
53
+ if (safe.risk === "confirm") o += `\n\u26a0\ufe0f ${safe.reasons.join("; ")}`;
54
54
  return tr(o);
55
55
  } });
56
56
 
57
+ // ── Audit tools ──
57
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."],
58
59
  parameters: Type.Object({ clear: Type.Optional(Type.Boolean({ description: "Clear after viewing." })), ...cs }),
59
60
  execute(_id, p) { const out = _fmt(); if (p.clear) _log.length = 0; return tr(out); } });
60
61
 
62
+ // ── Path tools ──
61
63
  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
64
  execute(_id, p) { return tr(pathUtils.toWindowsPath(p.path)); } });
63
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 }),
64
66
  execute(_id, p) { return tr(pathUtils.toWslPath(p.path)); } });
65
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 }),
66
- execute(_id, p) { return tr(pathUtils.toGitBashPath(p.path)); } });
68
+ execute(_id, p) { return tr(pathUtils.toPosixPath(p.path)); } });
67
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 }),
68
70
  execute(_id, p) { return tr(pathUtils.quoteForShell(p.path, rs(p.shell as WindowsShellKind | undefined))); } });
71
+
72
+ // ── Safety tools ──
69
73
  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 × ") : ""}`); } });
74
+ execute(_id, p) { const r = classifyCommand(p.command); return tr(`Risk: ${r.risk}${r.reasons.length ? "\nReasons:\n \u2022 " + r.reasons.join("\n \u2022 ") : ""}`); } });
75
+
76
+ // ── Doctor tools ──
71
77
  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
78
  execute(_id, p) { const r = runDoctor(); return tr(p.format === "json" ? JSON.stringify(r, null, 2) : formatDoctorReport(r)); } });
73
79
  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`); } } });
80
+ execute(_id, p) { try { const r = execFileSync("where", [p.name], { encoding: "utf8", timeout: 3000 }); return tr(`\u2713 ${p.name} at:\n${r.split(/\r?\n/).filter(Boolean).map(x => " " + x).join("\n")}`); } catch { return tr(`\u2717 ${p.name} not in PATH`); } } });
75
81
  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."); } } });
82
+ 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 \u2022 " + d.join("\n \u2022 ") : "No WSL distros found."); } catch { return tr("WSL not available."); } } });
77
83
 
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); } });
84
+ // ── Commands ──
85
+ 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()) + "\n"); } });
79
86
  pi.registerCommand("windows-shell", { description: "Show/set default shell.", handler: async (a, ctx) => {
80
87
  const arg = (a || "").trim().toLowerCase();
81
88
  if (arg) {
@@ -89,6 +96,7 @@ export default function piWindowsToolsExtension(pi: ExtensionAPI) {
89
96
  process.stdout.write(`Current shell: ${c.displayName} (${c.kind})\nExecutable: ${c.executable}\n`);
90
97
  } });
91
98
 
99
+ // ── Agent prompt ──
92
100
  pi.on("before_agent_start", async (event) => {
93
101
  if (process.env.PI_WINDOWS_TOOLS_ENABLED === "false" || process.platform !== "win32") return;
94
102
  return { systemPrompt: `${event.systemPrompt}\n\n---\n\n## Windows Environment\n\n${buildShellGuidance(rs())}` };
@@ -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.1.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
- });