Skip to content

Commit 644a391

Browse files
committed
fix: bash path discovery, dev runtime windows specific improvements made
1 parent 43546b2 commit 644a391

File tree

11 files changed

+265
-20
lines changed

11 files changed

+265
-20
lines changed

Makefile

Lines changed: 10 additions & 6 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
@@ -105,12 +105,16 @@ help: ## Show this help message
105105
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-20s\033[0m %s\n", $$1, $$2}'
106106

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

114+
clean-cache: ## Clean Vite cache (helps with EMFILE errors on Windows)
115+
@echo "Cleaning Vite cache..."
116+
@rm -rf node_modules/.vite
117+
114118
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
115119
@echo "Starting dev-server..."
116120
@echo " Backend (IPC/WebSocket): http://$(or $(BACKEND_HOST),localhost):$(or $(BACKEND_PORT),3000)"
@@ -202,7 +206,7 @@ lint-fix: node_modules/.installed ## Run linter with --fix
202206
@./scripts/lint.sh --fix
203207

204208
typecheck: node_modules/.installed src/version.ts ## Run TypeScript type checking (uses tsgo for 10x speedup)
205-
@bun x concurrently -g \
209+
@npx concurrently -g \
206210
"$(TSGO) --noEmit" \
207211
"$(TSGO) --noEmit -p tsconfig.main.json"
208212

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 {
@@ -61,11 +62,13 @@ export class LocalRuntime implements Runtime {
6162
);
6263
}
6364

64-
// If niceness is specified, spawn nice directly to avoid escaping issues
65-
const spawnCommand = options.niceness !== undefined ? "nice" : "bash";
66-
const bashPath = "bash";
65+
// If niceness is specified on Unix/Linux, spawn nice directly to avoid escaping issues
66+
// Windows doesn't have nice command, so just spawn bash directly
67+
const isWindows = process.platform === "win32";
68+
const bashPath = getBashPath();
69+
const spawnCommand = options.niceness !== undefined && !isWindows ? "nice" : bashPath;
6770
const spawnArgs =
68-
options.niceness !== undefined
71+
options.niceness !== undefined && !isWindows
6972
? ["-n", options.niceness.toString(), bashPath, "-c", command]
7073
: ["-c", command];
7174

@@ -417,7 +420,8 @@ export class LocalRuntime implements Runtime {
417420
const loggers = createLineBufferedLoggers(initLogger);
418421

419422
return new Promise<void>((resolve) => {
420-
const proc = spawn("bash", ["-c", `"${hookPath}"`], {
423+
const bashPath = getBashPath();
424+
const proc = spawn(bashPath, ["-c", `"${hookPath}"`], {
421425
cwd: workspacePath,
422426
stdio: ["ignore", "pipe", "pipe"],
423427
env: {

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",

tsconfig.main.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66
"noEmit": false,
77
"sourceMap": true
88
},
9+
"watchOptions": {
10+
"excludeDirectories": ["**/node_modules", "**/dist", "**/build", "**/.git"],
11+
"excludeFiles": ["**/*.d.ts.map"]
12+
},
913
"include": [
1014
"src/main.ts",
1115
"src/main-server.ts",

0 commit comments

Comments
 (0)