Skip to content

Commit ef8286d

Browse files
committed
fix: enable windows builds
fix: improved windows compatibility 🤖 fix: publish Windows .exe in release workflow\n\n- Add Windows job to build and publish NSIS .exe\n- Install ImageMagick on Windows in setup-cmux action\n- Set Windows icon and artifactName in electron-builder config\n\n_Generated with _ fix: chalk tests improvements fix: codex review WIP 🤖 feat: add unified cross-platform path utilities for Windows compatibility - Created PlatformPaths utility class for OS-aware path operations - Added UI-specific path formatting utilities for renderer process - Updated all path handling to use centralized utilities - Refactored pathAbbreviation to support Windows paths (C:\U\j\D\project) - Updated components: ProjectSidebar, NewWorkspaceModal, SecretsModal - Updated config.ts to use PlatformPaths for project name extraction - Eliminated 6+ instances of duplicated path splitting logic - Added comprehensive test suite (33 tests, all passing) - Added src/utils/platform/ to ESLint main-process exclusions This change prepares cmux for Windows support by centralizing all path operations into platform-aware utilities, ensuring correct behavior on Windows, macOS, and Linux. WIP fix: path cleanup fix: fmt fix: tests & fmt fix: exclude heavy dirs from watch fix: ssh workdir fix: test WIP fix: fmt
1 parent 0bc0e7e commit ef8286d

34 files changed

+1186
-259
lines changed

.github/actions/setup-cmux/action.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,14 @@ runs:
6262
sudo apt-get install -y --no-install-recommends imagemagick
6363
fi
6464
convert --version | head -1
65+
- name: Install ImageMagick (Windows)
66+
if: inputs.install-imagemagick == 'true' && runner.os == 'Windows'
67+
shell: powershell
68+
run: |
69+
if (Get-Command magick -ErrorAction SilentlyContinue) {
70+
Write-Host "✅ ImageMagick already available"
71+
} else {
72+
Write-Host "📦 Installing ImageMagick..."
73+
choco install -y imagemagick
74+
}
75+
magick --version | Select-Object -First 1

.github/workflows/release.yml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,34 @@ jobs:
5858
run: bun x electron-builder --linux --publish always
5959
env:
6060
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
61+
62+
build-windows:
63+
name: Build and Release Windows
64+
runs-on: windows-latest
65+
steps:
66+
- name: Checkout code
67+
uses: actions/checkout@v4
68+
with:
69+
fetch-depth: 0 # Required for git describe to find tags
70+
71+
- uses: ./.github/actions/setup-cmux
72+
with:
73+
install-imagemagick: true
74+
75+
- name: Install GNU Make (for build)
76+
run: choco install -y make
77+
78+
- name: Verify tools
79+
shell: bash
80+
run: |
81+
make --version
82+
bun --version
83+
magick --version | head -1
84+
85+
- name: Build application
86+
run: bun run build
87+
88+
- name: Package and publish for Windows (.exe)
89+
run: bun x electron-builder --win --publish always
90+
env:
91+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Makefile

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,15 @@
2424
# Branches reduce reproducibility - builds should fail fast with clear errors
2525
# if dependencies are missing, not silently fall back to different behavior.
2626

27+
# Use PATH-resolved bash on Windows to avoid hardcoded /usr/bin/bash which doesn't
28+
# exist in Chocolatey's make environment or on GitHub Actions windows-latest.
29+
ifeq ($(OS),Windows_NT)
30+
SHELL := bash
31+
else
32+
SHELL := /bin/bash
33+
endif
34+
.SHELLFLAGS := -eu -o pipefail -c
35+
2736
# Enable parallel execution by default (only if user didn't specify -j)
2837
ifeq (,$(filter -j%,$(MAKEFLAGS)))
2938
MAKEFLAGS += -j
@@ -92,8 +101,9 @@ help: ## Show this help message
92101

93102
## Development
94103
dev: node_modules/.installed build-main ## Start development server (Vite + tsgo watcher for 10x faster type checking)
95-
@bun x concurrently -k \
96-
"bun x concurrently \"$(TSGO) -w -p tsconfig.main.json\" \"bun x tsc-alias -w -p tsconfig.main.json\"" \
104+
@npx concurrently -k --raw \
105+
"$(TSGO) -w -p tsconfig.main.json" \
106+
"bun x tsc-alias -w -p tsconfig.main.json" \
97107
"vite"
98108

99109
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
@@ -102,10 +112,10 @@ dev-server: node_modules/.installed build-main ## Start server mode with hot rel
102112
@echo " Frontend (with HMR): http://$(or $(VITE_HOST),localhost):$(or $(VITE_PORT),5173)"
103113
@echo ""
104114
@echo "For remote access: make dev-server VITE_HOST=0.0.0.0 BACKEND_HOST=0.0.0.0"
105-
@bun x concurrently -k \
106-
"bun x concurrently \"$(TSGO) -w -p tsconfig.main.json\" \"bun x tsc-alias -w -p tsconfig.main.json\"" \
107-
"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)'" \
108-
"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"
115+
@npx concurrently -k \
116+
"npx concurrently \"$(TSGO) -w -p tsconfig.main.json\" \"bun x tsc-alias -w -p tsconfig.main.json\"" \
117+
"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)\"" \
118+
"$(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\""
109119

110120

111121

@@ -162,16 +172,16 @@ MAGICK_CMD := $(shell command -v magick 2>/dev/null || command -v convert 2>/dev
162172
build/icon.png: docs/img/logo.webp
163173
@echo "Generating Linux icon..."
164174
@mkdir -p build
165-
@$(MAGICK_CMD) docs/img/logo.webp -resize 512x512 build/icon.png
175+
@"$(MAGICK_CMD)" docs/img/logo.webp -resize 512x512 build/icon.png
166176

167177
build/icon.icns: docs/img/logo.webp
168178
@echo "Generating macOS icon..."
169179
@mkdir -p build/icon.iconset
170180
@for size in 16 32 64 128 256 512; do \
171-
$(MAGICK_CMD) docs/img/logo.webp -resize $${size}x$${size} build/icon.iconset/icon_$${size}x$${size}.png; \
181+
"$(MAGICK_CMD)" docs/img/logo.webp -resize $${size}x$${size} build/icon.iconset/icon_$${size}x$${size}.png; \
172182
if [ $$size -le 256 ]; then \
173183
double=$$((size * 2)); \
174-
$(MAGICK_CMD) docs/img/logo.webp -resize $${double}x$${double} build/icon.iconset/icon_$${size}x$${size}@2x.png; \
184+
"$(MAGICK_CMD)" docs/img/logo.webp -resize $${double}x$${double} build/icon.iconset/icon_$${size}x$${size}@2x.png; \
175185
fi; \
176186
done
177187
@iconutil -c icns build/icon.iconset -o build/icon.icns

bun.lock

Lines changed: 3 additions & 6 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",
@@ -72,7 +73,7 @@
7273
"class-variance-authority": "^0.7.1",
7374
"clsx": "^2.1.1",
7475
"cmdk": "^1.0.0",
75-
"concurrently": "^8.2.0",
76+
"concurrently": "^9.2.1",
7677
"dotenv": "^17.2.3",
7778
"electron": "^38.2.1",
7879
"electron-builder": "^24.6.0",
@@ -1202,7 +1203,7 @@
12021203

12031204
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
12041205

1205-
"concurrently": ["concurrently@8.2.2", "", { "dependencies": { "chalk": "^4.1.2", "date-fns": "^2.30.0", "lodash": "^4.17.21", "rxjs": "^7.8.1", "shell-quote": "^1.8.1", "spawn-command": "0.0.2", "supports-color": "^8.1.1", "tree-kill": "^1.2.2", "yargs": "^17.7.2" }, "bin": { "conc": "dist/bin/concurrently.js", "concurrently": "dist/bin/concurrently.js" } }, "sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg=="],
1206+
"concurrently": ["concurrently@9.2.1", "", { "dependencies": { "chalk": "4.1.2", "rxjs": "7.8.2", "shell-quote": "1.8.3", "supports-color": "8.1.1", "tree-kill": "1.2.2", "yargs": "17.7.2" }, "bin": { "conc": "dist/bin/concurrently.js", "concurrently": "dist/bin/concurrently.js" } }, "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng=="],
12061207

12071208
"confbox": ["confbox@0.2.2", "", {}, "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ=="],
12081209

@@ -1320,8 +1321,6 @@
13201321

13211322
"data-view-byte-offset": ["data-view-byte-offset@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" } }, "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ=="],
13221323

1323-
"date-fns": ["date-fns@2.30.0", "", { "dependencies": { "@babel/runtime": "^7.21.0" } }, "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw=="],
1324-
13251324
"dayjs": ["dayjs@1.11.18", "", {}, "sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA=="],
13261325

13271326
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
@@ -2578,8 +2577,6 @@
25782577

25792578
"space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="],
25802579

2581-
"spawn-command": ["spawn-command@0.0.2", "", {}, "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ=="],
2582-
25832580
"spawn-wrap": ["spawn-wrap@2.0.0", "", { "dependencies": { "foreground-child": "^2.0.0", "is-windows": "^1.0.2", "make-dir": "^3.0.0", "rimraf": "^3.0.0", "signal-exit": "^3.0.2", "which": "^2.0.1" } }, "sha512-EeajNjfN9zMnULLwhZZQU3GWBoFNkbngTUPfaawT4RkMiviTxcX0qfhVbGey39mfctfDHkWtuecgQ8NJcyQWHg=="],
25842581

25852582
"spawnd": ["spawnd@5.0.0", "", { "dependencies": { "exit": "^0.1.2", "signal-exit": "^3.0.3", "tree-kill": "^1.2.2", "wait-port": "^0.2.9" } }, "sha512-28+AJr82moMVWolQvlAIv3JcYDkjkFTEmfDc503wxrF5l2rQ3dFz6DpbXp3kD4zmgGGldfM4xM4v1sFj/ZaIOA=="],

eslint.config.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,7 @@ export default defineConfig([
407407
"src/services/**",
408408
"src/runtime/**",
409409
"src/utils/main/**",
410+
"src/utils/platform/**",
410411
"src/utils/providers/**",
411412
"src/telemetry/**",
412413
"src/git.ts",

fmt.mk

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -34,36 +34,35 @@ fmt-prettier-check:
3434

3535
fmt-shell:
3636
ifeq ($(SHFMT),)
37-
@echo "Error: shfmt not found. Install with: brew install shfmt"
38-
@exit 1
37+
@echo "shfmt not found; skipping shell script formatting"
3938
else
4039
@echo "Formatting shell scripts..."
4140
@shfmt -i 2 -ci -bn -w $(SHELL_SCRIPTS)
4241
endif
4342

4443
fmt-shell-check:
4544
ifeq ($(SHFMT),)
46-
@echo "Error: shfmt not found. Install with: brew install shfmt"
47-
@exit 1
45+
@echo "shfmt not found; skipping shell script format check"
4846
else
4947
@echo "Checking shell script formatting..."
5048
@shfmt -i 2 -ci -bn -d $(SHELL_SCRIPTS)
5149
endif
5250

53-
# Helper target to check for uvx
54-
.check-uvx:
51+
fmt-python:
5552
ifeq ($(UVX),)
56-
@echo "Error: uvx not found. Install with: curl -LsSf https://astral.sh/uv/install.sh | sh"
57-
@exit 1
58-
endif
59-
60-
fmt-python: .check-uvx
53+
@echo "uvx not found; skipping Python formatting"
54+
else
6155
@echo "Formatting Python files..."
6256
@$(UVX) ruff format $(PYTHON_DIRS)
57+
endif
6358

64-
fmt-python-check: .check-uvx
59+
fmt-python-check:
60+
ifeq ($(UVX),)
61+
@echo "uvx not found; skipping Python format check"
62+
else
6563
@echo "Checking Python formatting..."
6664
@$(UVX) ruff format --check $(PYTHON_DIRS)
65+
endif
6766

6867
fmt-nix:
6968
ifeq ($(NIX),)

package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@
113113
"class-variance-authority": "^0.7.1",
114114
"clsx": "^2.1.1",
115115
"cmdk": "^1.0.0",
116-
"concurrently": "^8.2.0",
116+
"concurrently": "^9.2.1",
117117
"dotenv": "^17.2.3",
118118
"electron": "^38.2.1",
119119
"electron-builder": "^24.6.0",
@@ -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
}

src/App.tsx

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ function AppInner() {
177177
);
178178

179179
const handleAddWorkspace = useCallback(async (projectPath: string) => {
180-
const projectName = projectPath.split("/").pop() ?? projectPath.split("\\").pop() ?? "project";
180+
const projectName = projectPath.replace(/\\/g, "/").split("/").pop() ?? "project";
181181

182182
workspaceModalProjectRef.current = projectPath;
183183
setWorkspaceModalProject(projectPath);
@@ -637,15 +637,22 @@ function AppInner() {
637637
<div className="mobile-layout flex flex-1 overflow-hidden">
638638
{selectedWorkspace ? (
639639
<ErrorBoundary
640-
workspaceInfo={`${selectedWorkspace.projectName}/${selectedWorkspace.namedWorkspacePath?.split("/").pop() ?? selectedWorkspace.workspaceId}`}
640+
workspaceInfo={`${selectedWorkspace.projectName}/${
641+
(selectedWorkspace.namedWorkspacePath ?? "")
642+
.replace(/\\/g, "/")
643+
.split("/")
644+
.pop() ?? selectedWorkspace.workspaceId
645+
}`}
641646
>
642647
<AIView
643648
key={selectedWorkspace.workspaceId}
644649
workspaceId={selectedWorkspace.workspaceId}
645650
projectName={selectedWorkspace.projectName}
646651
branch={
647-
selectedWorkspace.namedWorkspacePath?.split("/").pop() ??
648-
selectedWorkspace.workspaceId
652+
(selectedWorkspace.namedWorkspacePath ?? "")
653+
.replace(/\\/g, "/")
654+
.split("/")
655+
.pop() ?? selectedWorkspace.workspaceId
649656
}
650657
namedWorkspacePath={selectedWorkspace.namedWorkspacePath ?? ""}
651658
runtimeConfig={

src/components/NewWorkspaceModal.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { TooltipWrapper, Tooltip } from "./Tooltip";
44
import { formatNewCommand } from "@/utils/chatCommands";
55
import { useNewWorkspaceOptions } from "@/hooks/useNewWorkspaceOptions";
66
import { RUNTIME_MODE } from "@/types/runtime";
7+
import { formatWorkspaceLocation, formatSSHHostPath } from "@/utils/ui/pathFormatting";
78

89
interface NewWorkspaceModalProps {
910
isOpen: boolean;
@@ -247,8 +248,8 @@ const NewWorkspaceModal: React.FC<NewWorkspaceModalProps> = ({
247248
<p>This will create a workspace at:</p>
248249
<code className="block break-all">
249250
{runtimeMode === RUNTIME_MODE.SSH
250-
? `${sshHost || "<host>"}:~/cmux/${branchName || "<branch-name>"}`
251-
: `~/.cmux/src/${projectName}/${branchName || "<branch-name>"}`}
251+
? formatSSHHostPath(sshHost || "<host>", `cmux/${branchName || "<branch-name>"}`)
252+
: formatWorkspaceLocation(projectName, branchName || "<branch-name>", false)}
252253
</code>
253254
</ModalInfo>
254255

src/components/ProjectSidebar.tsx

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { useDrag, useDrop, useDragLayer } from "react-dnd";
1010
import { sortProjectsByOrder, reorderProjects, normalizeOrder } from "@/utils/projectOrdering";
1111
import { matchesKeybind, formatKeybind, KEYBINDS } from "@/utils/ui/keybinds";
1212
import { abbreviatePath, splitAbbreviatedPath } from "@/utils/ui/pathAbbreviation";
13+
import { PlatformPaths } from "@/utils/platform/paths";
1314
import {
1415
partitionWorkspacesByAge,
1516
formatOldWorkspaceThreshold,
@@ -234,12 +235,7 @@ const ProjectSidebarInner: React.FC<ProjectSidebarProps> = ({
234235
anchor: { top: number; left: number } | null;
235236
} | null>(null);
236237

237-
const getProjectName = (path: string) => {
238-
if (!path || typeof path !== "string") {
239-
return "Unknown";
240-
}
241-
return path.split("/").pop() ?? path.split("\\").pop() ?? path;
242-
};
238+
const getProjectName = (path: string) => PlatformPaths.getProjectName(path);
243239

244240
const toggleProject = (projectPath: string) => {
245241
const newExpanded = new Set(expandedProjects);

0 commit comments

Comments
 (0)