Skip to content

Commit aac67d0

Browse files
committed
fix: bash path discovery, dev runtime windows specific improvements made
1 parent b8d6cab commit aac67d0

File tree

12 files changed

+267
-21
lines changed

12 files changed

+267
-21
lines changed

Makefile

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ endif
4141
# Include formatting rules
4242
include fmt.mk
4343

44-
.PHONY: all build dev start clean help
44+
.PHONY: all build dev start clean clean-cache help
4545
.PHONY: build-renderer version build-icons build-static
4646
.PHONY: lint lint-fix typecheck static-check
4747
.PHONY: test test-unit test-integration test-watch test-coverage test-e2e
@@ -100,20 +100,24 @@ help: ## Show this help message
100100
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-20s\033[0m %s\n", $$1, $$2}'
101101

102102
## Development
103-
dev: node_modules/.installed build-main ## Start development server (Vite + tsgo watcher for 10x faster type checking)
104-
@npx concurrently -k --raw \
105-
"$(TSGO) -w -p tsconfig.main.json" \
106-
"bun x tsc-alias -w -p tsconfig.main.json" \
103+
dev: node_modules/.installed build-main clean-cache ## Start development server (Vite + nodemon watcher for Windows compatibility)
104+
@echo "Starting dev mode (2 watchers: nodemon for main process, vite for renderer)..."
105+
@NODE_OPTIONS="--max-old-space-size=4096" npx concurrently -k --raw \
106+
"npx nodemon --exec node scripts/build-main-watch.js" \
107107
"vite"
108108

109+
clean-cache: ## Clean Vite cache (helps with EMFILE errors on Windows)
110+
@echo "Cleaning Vite cache..."
111+
@rm -rf node_modules/.vite
112+
109113
dev-server: node_modules/.installed build-main ## Start server mode with hot reload (backend :3000 + frontend :5173). Use VITE_HOST=0.0.0.0 BACKEND_HOST=0.0.0.0 for remote access
110114
@echo "Starting dev-server..."
111115
@echo " Backend (IPC/WebSocket): http://$(or $(BACKEND_HOST),localhost):$(or $(BACKEND_PORT),3000)"
112116
@echo " Frontend (with HMR): http://$(or $(VITE_HOST),localhost):$(or $(VITE_PORT),5173)"
113117
@echo ""
114118
@echo "For remote access: make dev-server VITE_HOST=0.0.0.0 BACKEND_HOST=0.0.0.0"
115119
@npx concurrently -k \
116-
"npx concurrently \"$(TSGO) -w -p tsconfig.main.json\" \"bun x tsc-alias -w -p tsconfig.main.json\"" \
120+
"npx nodemon --exec node scripts/build-main-watch.js" \
117121
"bun x nodemon --watch dist/main.js --watch dist/main-server.js --delay 500ms --exec \"node dist/main.js server --host $(or $(BACKEND_HOST),localhost) --port $(or $(BACKEND_PORT),3000)\"" \
118122
"$(SHELL) -lc \"CMUX_VITE_HOST=$(or $(VITE_HOST),127.0.0.1) CMUX_VITE_PORT=$(or $(VITE_PORT),5173) VITE_BACKEND_URL=http://$(or $(BACKEND_HOST),localhost):$(or $(BACKEND_PORT),3000) vite\""
119123

@@ -197,7 +201,7 @@ lint-fix: node_modules/.installed ## Run linter with --fix
197201
@./scripts/lint.sh --fix
198202

199203
typecheck: node_modules/.installed src/version.ts ## Run TypeScript type checking (uses tsgo for 10x speedup)
200-
@bun x concurrently -g \
204+
@npx concurrently -g \
201205
"$(TSGO) --noEmit" \
202206
"$(TSGO) --noEmit -p tsconfig.main.json"
203207

bun.lock

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"lockfileVersion": 1,
3+
"configVersion": 0,
34
"workspaces": {
45
"": {
56
"name": "mux",

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,9 @@
204204
"artifactName": "${productName}-${version}-${arch}.${ext}"
205205
},
206206
"win": {
207-
"target": "nsis"
207+
"target": "nsis",
208+
"icon": "build/icon.png",
209+
"artifactName": "${productName}-${version}-${arch}.${ext}"
208210
}
209211
}
210212
}

public/service-worker.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,20 @@ self.addEventListener("fetch", (event) => {
5151
})
5252
.catch(() => {
5353
// If network fails, try cache
54-
return caches.match(event.request);
54+
return caches.match(event.request).then((cachedResponse) => {
55+
// If cache has it, return it; otherwise return a proper error response
56+
if (cachedResponse) {
57+
return cachedResponse;
58+
}
59+
// Return a proper Response object for failed requests
60+
return new Response("Network error and no cached version available", {
61+
status: 503,
62+
statusText: "Service Unavailable",
63+
headers: new Headers({
64+
"Content-Type": "text/plain",
65+
}),
66+
});
67+
});
5568
})
5669
);
5770
});

scripts/build-main-watch.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
#!/usr/bin/env node
2+
/**
3+
* Build script for main process in watch mode
4+
* Used by nodemon - ignores file arguments passed by nodemon
5+
*/
6+
7+
const { execSync } = require('child_process');
8+
const path = require('path');
9+
10+
const rootDir = path.join(__dirname, '..');
11+
const tsgoPath = path.join(rootDir, 'node_modules/@typescript/native-preview/bin/tsgo.js');
12+
const tscAliasPath = path.join(rootDir, 'node_modules/tsc-alias/dist/bin/index.js');
13+
14+
try {
15+
console.log('Building main process...');
16+
17+
// Run tsgo
18+
execSync(`node "${tsgoPath}" -p tsconfig.main.json`, {
19+
cwd: rootDir,
20+
stdio: 'inherit',
21+
env: { ...process.env, NODE_ENV: 'development' }
22+
});
23+
24+
// Run tsc-alias
25+
execSync(`node "${tscAliasPath}" -p tsconfig.main.json`, {
26+
cwd: rootDir,
27+
stdio: 'inherit',
28+
env: { ...process.env, NODE_ENV: 'development' }
29+
});
30+
31+
console.log('✓ Main process build complete');
32+
} catch (error) {
33+
console.error('Build failed:', error.message);
34+
process.exit(1);
35+
}
36+

src/runtime/LocalRuntime.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import type {
1818
} from "./Runtime";
1919
import { RuntimeError as RuntimeErrorClass } from "./Runtime";
2020
import { NON_INTERACTIVE_ENV_VARS } from "../constants/env";
21+
import { getBashPath } from "../utils/bashPath";
2122
import { EXIT_CODE_ABORTED, EXIT_CODE_TIMEOUT } from "../constants/exitCodes";
2223
import { listLocalBranches } from "../git";
2324
import { checkInitHookExists, getInitHookPath, createLineBufferedLoggers } from "./initHook";
@@ -56,11 +57,13 @@ export class LocalRuntime implements Runtime {
5657
);
5758
}
5859

59-
// If niceness is specified, spawn nice directly to avoid escaping issues
60-
const spawnCommand = options.niceness !== undefined ? "nice" : "bash";
61-
const bashPath = "bash";
60+
// If niceness is specified on Unix/Linux, spawn nice directly to avoid escaping issues
61+
// Windows doesn't have nice command, so just spawn bash directly
62+
const isWindows = process.platform === "win32";
63+
const bashPath = getBashPath();
64+
const spawnCommand = options.niceness !== undefined && !isWindows ? "nice" : bashPath;
6265
const spawnArgs =
63-
options.niceness !== undefined
66+
options.niceness !== undefined && !isWindows
6467
? ["-n", options.niceness.toString(), bashPath, "-c", command]
6568
: ["-c", command];
6669

@@ -412,7 +415,8 @@ export class LocalRuntime implements Runtime {
412415
const loggers = createLineBufferedLoggers(initLogger);
413416

414417
return new Promise<void>((resolve) => {
415-
const proc = spawn("bash", ["-c", `"${hookPath}"`], {
418+
const bashPath = getBashPath();
419+
const proc = spawn(bashPath, ["-c", `"${hookPath}"`], {
416420
cwd: workspacePath,
417421
stdio: ["ignore", "pipe", "pipe"],
418422
});

src/runtime/SSHRuntime.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { spawn } from "child_process";
22
import { Readable, Writable } from "stream";
33
import * as path from "path";
4-
import { Shescape } from "shescape";
54
import type {
65
Runtime,
76
ExecOptions,
@@ -25,12 +24,20 @@ import { getProjectName } from "../utils/runtime/helpers";
2524
import { getErrorMessage } from "../utils/errors";
2625
import { execAsync, DisposableProcess } from "../utils/disposableExec";
2726
import { getControlPath } from "./sshConnectionPool";
27+
import { getBashPath } from "../utils/bashPath";
2828

2929
/**
30-
* Shescape instance for bash shell escaping.
30+
* Shell-escape helper for remote bash.
3131
* Reused across all SSH runtime operations for performance.
3232
*/
33-
const shescape = new Shescape({ shell: "bash" });
33+
const shescape = {
34+
quote(value: unknown): string {
35+
const s = String(value);
36+
if (s.length === 0) return "''";
37+
// Use POSIX-safe pattern to embed single quotes within single-quoted strings
38+
return "'" + s.replace(/'/g, "'\"'\"'") + "'";
39+
},
40+
};
3441

3542
/**
3643
* SSH Runtime Configuration
@@ -574,7 +581,8 @@ export class SSHRuntime implements Runtime {
574581
const command = `cd ${shescape.quote(projectPath)} && git bundle create - --all | ssh ${sshArgs.join(" ")} "cat > ${bundleTempPath}"`;
575582

576583
log.debug(`Creating bundle: ${command}`);
577-
const proc = spawn("bash", ["-c", command]);
584+
const bashPath = getBashPath();
585+
const proc = spawn(bashPath, ["-c", command]);
578586

579587
const cleanup = streamProcessToLogger(proc, initLogger, {
580588
logStdout: false,

src/services/bashExecutionService.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { spawn } from "child_process";
22
import type { ChildProcess } from "child_process";
33
import { log } from "./log";
4+
import { getBashPath } from "../utils/bashPath";
45

56
/**
67
* Configuration for bash execution
@@ -120,10 +121,13 @@ export class BashExecutionService {
120121
`BashExecutionService: Script: ${script.substring(0, 100)}${script.length > 100 ? "..." : ""}`
121122
);
122123

123-
const spawnCommand = config.niceness !== undefined ? "nice" : "bash";
124+
// Windows doesn't have nice command, so just spawn bash directly
125+
const isWindows = process.platform === "win32";
126+
const bashPath = getBashPath();
127+
const spawnCommand = config.niceness !== undefined && !isWindows ? "nice" : bashPath;
124128
const spawnArgs =
125-
config.niceness !== undefined
126-
? ["-n", config.niceness.toString(), "bash", "-c", script]
129+
config.niceness !== undefined && !isWindows
130+
? ["-n", config.niceness.toString(), bashPath, "-c", script]
127131
: ["-c", script];
128132

129133
const child = spawn(spawnCommand, spawnArgs, {

src/utils/bashPath.ts

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
/**
2+
* Platform-specific bash path resolution
3+
*
4+
* On Unix/Linux/macOS, bash is in PATH by default.
5+
* On Windows, bash comes from Git Bash and needs to be located.
6+
*/
7+
8+
import { execSync } from "child_process";
9+
import { existsSync } from "fs";
10+
import path from "path";
11+
12+
let cachedBashPath: string | null = null;
13+
14+
/**
15+
* Find bash executable path on Windows
16+
* Checks common Git Bash installation locations
17+
*/
18+
function findWindowsBash(): string | null {
19+
// Common Git Bash installation paths
20+
const commonPaths = [
21+
// Git for Windows default paths
22+
"C:\\Program Files\\Git\\bin\\bash.exe",
23+
"C:\\Program Files (x86)\\Git\\bin\\bash.exe",
24+
// User-local Git installation
25+
path.join(process.env.LOCALAPPDATA || "", "Programs", "Git", "bin", "bash.exe"),
26+
// Portable Git
27+
path.join(process.env.USERPROFILE || "", "scoop", "apps", "git", "current", "bin", "bash.exe"),
28+
// Chocolatey installation
29+
"C:\\tools\\git\\bin\\bash.exe",
30+
];
31+
32+
// Check if bash is in PATH first
33+
try {
34+
const result = execSync("where bash", { encoding: "utf8", stdio: ["pipe", "pipe", "ignore"] });
35+
const firstPath = result.split("\n")[0].trim();
36+
if (firstPath && existsSync(firstPath)) {
37+
return firstPath;
38+
}
39+
} catch {
40+
// Not in PATH, continue to check common locations
41+
}
42+
43+
// Check common installation paths
44+
for (const bashPath of commonPaths) {
45+
if (existsSync(bashPath)) {
46+
return bashPath;
47+
}
48+
}
49+
50+
// Also check if Git is in PATH and derive bash path from it
51+
try {
52+
const gitPath = execSync("where git", { encoding: "utf8", stdio: ["pipe", "pipe", "ignore"] });
53+
const firstGitPath = gitPath.split("\n")[0].trim();
54+
if (firstGitPath) {
55+
// Git is usually in Git/cmd/git.exe, bash is in Git/bin/bash.exe
56+
const gitDir = path.dirname(path.dirname(firstGitPath));
57+
const bashPath = path.join(gitDir, "bin", "bash.exe");
58+
if (existsSync(bashPath)) {
59+
return bashPath;
60+
}
61+
// Also try usr/bin/bash.exe (newer Git for Windows structure)
62+
const usrBashPath = path.join(gitDir, "usr", "bin", "bash.exe");
63+
if (existsSync(usrBashPath)) {
64+
return usrBashPath;
65+
}
66+
}
67+
} catch {
68+
// Git not in PATH
69+
}
70+
71+
return null;
72+
}
73+
74+
/**
75+
* Get the bash executable path for the current platform
76+
*
77+
* @returns Path to bash executable. On Unix/macOS returns "bash",
78+
* on Windows returns full path to bash.exe if found.
79+
* @throws Error if bash cannot be found on Windows
80+
*/
81+
export function getBashPath(): string {
82+
// On Unix/Linux/macOS, bash is in PATH
83+
if (process.platform !== "win32") {
84+
return "bash";
85+
}
86+
87+
// Use cached path if available
88+
if (cachedBashPath !== null) {
89+
return cachedBashPath;
90+
}
91+
92+
// Find bash on Windows
93+
const bashPath = findWindowsBash();
94+
if (!bashPath) {
95+
throw new Error(
96+
"Git Bash not found. Please install Git for Windows from https://git-scm.com/download/win"
97+
);
98+
}
99+
100+
cachedBashPath = bashPath;
101+
return bashPath;
102+
}
103+
104+
/**
105+
* Check if bash is available on the system
106+
*
107+
* @returns true if bash is available, false otherwise
108+
*/
109+
export function isBashAvailable(): boolean {
110+
try {
111+
getBashPath();
112+
return true;
113+
} catch {
114+
return false;
115+
}
116+
}
117+
118+

tsconfig.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@
1818
"@/*": ["./src/*"]
1919
}
2020
},
21+
"watchOptions": {
22+
"excludeDirectories": ["**/node_modules", "**/dist", "**/build", "**/.git"],
23+
"excludeFiles": ["**/*.d.ts.map"]
24+
},
2125
"include": [
2226
"src/**/*.tsx",
2327
"src/**/*.ts",

0 commit comments

Comments
 (0)