mixdog 0.7.14 → 0.7.16

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,4 +1,5 @@
1
1
  import { spawn } from 'child_process';
2
+ import { isWSL } from './wsl.mjs';
2
3
 
3
4
  /**
4
5
  * Open a URL in the user's default browser. Best-effort and non-blocking —
@@ -11,27 +12,51 @@ import { spawn } from 'child_process';
11
12
  * treats the first quoted string as the WINDOW TITLE, so the URL is dropped
12
13
  * and nothing opens.
13
14
  * - macOS: `open <url>`.
15
+ * - WSL: xdg-open usually fails (no Linux GUI), so reach the Windows-HOST
16
+ * browser — prefer `wslview` (wslu), else `powershell.exe Start-Process`.
17
+ * cmd.exe `start` is deliberately NOT used: cmd re-parses URL metacharacters
18
+ * so a query-string `&` (e.g. `?a=1&b=2`) is treated as a command separator
19
+ * and the URL truncates. PowerShell's Start-Process takes the URL as one
20
+ * argument and does not reparse `&`; the URL is passed inside a single-quoted
21
+ * PowerShell literal (single quotes doubled) so `&`, spaces and quotes
22
+ * survive and no injection is possible. Falls back through the chain when an
23
+ * opener is missing.
14
24
  * - Linux/other: `xdg-open <url>`.
15
25
  */
16
26
  export function openInBrowser(url) {
17
27
  const u = String(url);
18
- let cmd;
19
- let args;
28
+ // Ordered [cmd, args] candidates. Every platform but WSL has exactly one;
29
+ // WSL has a fallback chain since the first opener may be absent.
30
+ let candidates;
20
31
  if (process.platform === 'win32') {
21
- cmd = 'rundll32';
22
- args = ['url.dll,FileProtocolHandler', u];
32
+ candidates = [['rundll32', ['url.dll,FileProtocolHandler', u]]];
23
33
  } else if (process.platform === 'darwin') {
24
- cmd = 'open';
25
- args = [u];
34
+ candidates = [['open', [u]]];
35
+ } else if (isWSL()) {
36
+ // Single-quoted PowerShell literal: double any embedded single quote.
37
+ // Everything else (incl. `&`) is literal inside a single-quoted string.
38
+ const psUrl = `'${u.replace(/'/g, "''")}'`;
39
+ candidates = [
40
+ ['wslview', [u]],
41
+ ['powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', `Start-Process -FilePath ${psUrl}`]],
42
+ ];
26
43
  } else {
27
- cmd = 'xdg-open';
28
- args = [u];
44
+ candidates = [['xdg-open', [u]]];
29
45
  }
46
+ tryOpenCandidates(candidates, 0);
47
+ }
48
+
49
+ // Spawn candidates[index]; on spawn error (opener missing) advance to the next.
50
+ // Always non-blocking — the caller prints the URL, so exhausting the chain is
51
+ // never fatal.
52
+ function tryOpenCandidates(candidates, index) {
53
+ if (index >= candidates.length) return;
54
+ const [cmd, args] = candidates[index];
30
55
  try {
31
56
  const child = spawn(cmd, args, { stdio: 'ignore', detached: true, windowsHide: true });
32
- child.on('error', () => { /* opener missing user uses the printed URL */ });
57
+ child.on('error', () => { tryOpenCandidates(candidates, index + 1); });
33
58
  child.unref();
34
59
  } catch {
35
- /* spawn threw synchronously — user opens the printed URL manually */
60
+ tryOpenCandidates(candidates, index + 1);
36
61
  }
37
62
  }
@@ -54,8 +54,20 @@ export function _safeProcessCwd() {
54
54
  function _normalizePlatformCwd(p) {
55
55
  if (!p || typeof p !== 'string') return p
56
56
  if (process.platform !== 'win32') return resolve(p)
57
- const m = p.match(/^[\/\\]([a-zA-Z])[\/\\](.*)$/)
58
- const native = m ? `${m[1].toUpperCase()}:\\${m[2].replace(/\//g, '\\')}` : p
57
+ // Map all three POSIX drive-prefix shapes to native Windows paths before
58
+ // resolution, aligned with path-utils.mjs posixPathToWindowsPath:
59
+ // /cygdrive/c/... (Cygwin), /mnt/c/... (WSL), /c/... (MSYS/Git-Bash) → C:\...
60
+ // Cygwin/WSL prefixes must be tested before the bare single-letter form
61
+ // because their slice offsets differ.
62
+ const cyg = p.match(/^\/cygdrive\/([a-zA-Z])\//)
63
+ const wsl = p.match(/^\/mnt\/([a-zA-Z])\//)
64
+ let native = p
65
+ if (cyg) native = `${cyg[1].toUpperCase()}:\\${p.slice(11).replace(/\//g, '\\')}`
66
+ else if (wsl) native = `${wsl[1].toUpperCase()}:\\${p.slice(7).replace(/\//g, '\\')}`
67
+ else {
68
+ const m = p.match(/^[\/\\]([a-zA-Z])[\/\\](.*)$/)
69
+ if (m) native = `${m[1].toUpperCase()}:\\${m[2].replace(/\//g, '\\')}`
70
+ }
59
71
  return resolve(native)
60
72
  }
61
73
 
@@ -0,0 +1,35 @@
1
+ // WSL (Windows Subsystem for Linux) detection.
2
+ //
3
+ // process.platform reports 'linux' inside WSL, so any caller that needs
4
+ // Windows-HOST behavior — opening the Windows default browser, reaching the
5
+ // Windows credential store, translating Windows drive paths — must branch on
6
+ // this explicitly rather than on process.platform alone. Plain-Linux behavior
7
+ // stays the default; this only flags the WSL special case.
8
+ //
9
+ // Detection order (cheapest / most reliable first):
10
+ // 1. WSL_DISTRO_NAME / WSL_INTEROP env vars (set by the WSL init for every
11
+ // interactive and most non-interactive sessions).
12
+ // 2. /proc/version containing "microsoft"/"wsl" (the kernel is a Microsoft
13
+ // build under WSL1/WSL2). Read once and memoized.
14
+ import { readFileSync } from 'node:fs';
15
+
16
+ let _isWSL;
17
+
18
+ export function isWSL() {
19
+ if (_isWSL !== undefined) return _isWSL;
20
+ if (process.platform !== 'linux') {
21
+ _isWSL = false;
22
+ return _isWSL;
23
+ }
24
+ if (process.env.WSL_DISTRO_NAME || process.env.WSL_INTEROP) {
25
+ _isWSL = true;
26
+ return _isWSL;
27
+ }
28
+ try {
29
+ const v = readFileSync('/proc/version', 'utf8').toLowerCase();
30
+ _isWSL = v.includes('microsoft') || v.includes('wsl');
31
+ } catch {
32
+ _isWSL = false;
33
+ }
34
+ return _isWSL;
35
+ }