Skip to content

Commit ceab9fc

Browse files
committed
Merge main branch and fix tempDir -> runtimeTempDir for SSH tests
2 parents 6b04903 + 2cac68f commit ceab9fc

File tree

10 files changed

+411
-80
lines changed

10 files changed

+411
-80
lines changed
Lines changed: 17 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
name: "Setup Cmux"
22
description: "Setup Bun and install dependencies with caching"
3+
inputs:
4+
install-imagemagick:
5+
description: "Install ImageMagick (needed for electron-builder icon generation)"
6+
required: false
7+
default: "false"
38
runs:
49
using: "composite"
510
steps:
@@ -29,42 +34,31 @@ runs:
2934
restore-keys: |
3035
${{ runner.os }}-bun-cache-
3136
32-
- name: Cache Homebrew (macOS)
33-
if: runner.os == 'macOS'
34-
uses: actions/cache@v4
35-
with:
36-
path: ~/Library/Caches/Homebrew
37-
key: ${{ runner.os }}-brew-cache-${{ hashFiles('**/bun.lock') }}
38-
restore-keys: |
39-
${{ runner.os }}-brew-cache-
40-
4137
- name: Install dependencies
4238
shell: bash
4339
run: bun install --frozen-lockfile
4440

4541
- name: Install ImageMagick (macOS)
46-
if: runner.os == 'macOS'
42+
if: inputs.install-imagemagick == 'true' && runner.os == 'macOS'
4743
shell: bash
4844
run: |
49-
if ! brew list imagemagick &>/dev/null; then
50-
echo "📦 Installing ImageMagick..."
51-
time brew install imagemagick
45+
if command -v magick &>/dev/null; then
46+
echo "✅ ImageMagick already available"
5247
else
53-
echo "✅ ImageMagick already installed"
54-
brew list imagemagick --versions
48+
echo "📦 Installing ImageMagick..."
49+
HOMEBREW_NO_AUTO_UPDATE=1 brew install imagemagick
5550
fi
56-
# Verify it's in PATH
57-
which magick
5851
magick --version | head -1
5952
6053
- name: Install ImageMagick (Linux)
61-
if: runner.os == 'Linux'
54+
if: inputs.install-imagemagick == 'true' && runner.os == 'Linux'
6255
shell: bash
6356
run: |
64-
if ! command -v convert &> /dev/null; then
65-
echo "Installing ImageMagick..."
66-
sudo apt-get update -qq
67-
sudo apt-get install -y imagemagick
57+
if command -v convert &>/dev/null; then
58+
echo "✅ ImageMagick already available"
6859
else
69-
echo "ImageMagick already installed"
60+
echo "📦 Installing ImageMagick..."
61+
sudo apt-get update -qq
62+
sudo apt-get install -y --no-install-recommends imagemagick
7063
fi
64+
convert --version | head -1

.github/workflows/build.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ jobs:
1717
fetch-depth: 0 # Required for git describe to find tags
1818

1919
- uses: ./.github/actions/setup-cmux
20+
with:
21+
install-imagemagick: true
2022

2123
- name: Build application
2224
run: bun run build
@@ -71,6 +73,8 @@ jobs:
7173
fetch-depth: 0 # Required for git describe to find tags
7274

7375
- uses: ./.github/actions/setup-cmux
76+
with:
77+
install-imagemagick: true
7478

7579
- name: Build application
7680
run: bun run build

.github/workflows/release.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ jobs:
1818
fetch-depth: 0 # Required for git describe to find tags
1919

2020
- uses: ./.github/actions/setup-cmux
21+
with:
22+
install-imagemagick: true
2123

2224
- name: Build application
2325
run: bun run build
@@ -46,6 +48,8 @@ jobs:
4648
fetch-depth: 0 # Required for git describe to find tags
4749

4850
- uses: ./.github/actions/setup-cmux
51+
with:
52+
install-imagemagick: true
4953

5054
- name: Build application
5155
run: bun run build

src/runtime/LocalRuntime.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,16 @@ export class LocalRuntime implements Runtime {
303303
}
304304
}
305305

306+
normalizePath(targetPath: string, basePath: string): string {
307+
// For local runtime, use Node.js path resolution
308+
// Handle special case: current directory
309+
const target = targetPath.trim();
310+
if (target === ".") {
311+
return path.resolve(basePath);
312+
}
313+
return path.resolve(basePath, target);
314+
}
315+
306316
getWorkspacePath(projectPath: string, workspaceName: string): string {
307317
const projectName = getProjectName(projectPath);
308318
return path.join(this.srcBaseDir, projectName, workspaceName);

src/runtime/Runtime.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,25 @@ export interface Runtime {
195195
*/
196196
stat(path: string): Promise<FileStat>;
197197

198+
/**
199+
* Normalize a path for comparison purposes within this runtime's context.
200+
* Handles runtime-specific path semantics (local vs remote).
201+
*
202+
* @param targetPath Path to normalize (may be relative or absolute)
203+
* @param basePath Base path to resolve relative paths against
204+
* @returns Normalized path suitable for string comparison
205+
*
206+
* @example
207+
* // LocalRuntime
208+
* runtime.normalizePath(".", "/home/user") // => "/home/user"
209+
* runtime.normalizePath("../other", "/home/user/project") // => "/home/user/other"
210+
*
211+
* // SSHRuntime
212+
* runtime.normalizePath(".", "/home/user") // => "/home/user"
213+
* runtime.normalizePath("~/project", "~") // => "~/project"
214+
*/
215+
normalizePath(targetPath: string, basePath: string): string;
216+
198217
/**
199218
* Compute absolute workspace path from project and workspace name.
200219
* This is the SINGLE source of truth for workspace path computation.

src/runtime/SSHRuntime.ts

Lines changed: 89 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { expandTildeForSSH, cdCommandForSSH } from "./tildeExpansion";
2424
import { findBashPath } from "./executablePaths";
2525
import { getProjectName } from "../utils/runtime/helpers";
2626
import { getErrorMessage } from "../utils/errors";
27+
import { execAsync } from "../utils/disposableExec";
2728

2829
/**
2930
* Shescape instance for bash shell escaping.
@@ -317,6 +318,41 @@ export class SSHRuntime implements Runtime {
317318
};
318319
}
319320

321+
normalizePath(targetPath: string, basePath: string): string {
322+
// For SSH, handle paths in a POSIX-like manner without accessing the remote filesystem
323+
const target = targetPath.trim();
324+
let base = basePath.trim();
325+
326+
// Normalize base path - remove trailing slash (except for root "/")
327+
if (base.length > 1 && base.endsWith("/")) {
328+
base = base.slice(0, -1);
329+
}
330+
331+
// Handle special case: current directory
332+
if (target === ".") {
333+
return base;
334+
}
335+
336+
// Handle tilde expansion - keep as-is for comparison
337+
let normalizedTarget = target;
338+
if (target === "~" || target.startsWith("~/")) {
339+
normalizedTarget = target;
340+
} else if (target.startsWith("/")) {
341+
// Absolute path - use as-is
342+
normalizedTarget = target;
343+
} else {
344+
// Relative path - resolve against base using POSIX path joining
345+
normalizedTarget = base.endsWith("/") ? base + target : base + "/" + target;
346+
}
347+
348+
// Remove trailing slash for comparison (except for root "/")
349+
if (normalizedTarget.length > 1 && normalizedTarget.endsWith("/")) {
350+
normalizedTarget = normalizedTarget.slice(0, -1);
351+
}
352+
353+
return normalizedTarget;
354+
}
355+
320356
/**
321357
* Build common SSH arguments based on runtime config
322358
* @param includeHost - Whether to include the host in the args (for direct ssh commands)
@@ -372,11 +408,28 @@ export class SSHRuntime implements Runtime {
372408
const bundleTempPath = `~/.cmux-bundle-${timestamp}.bundle`;
373409

374410
try {
375-
// Step 1: Create bundle locally and pipe to remote file via SSH
411+
// Step 1: Get origin URL from local repository (if it exists)
412+
let originUrl: string | null = null;
413+
try {
414+
using proc = execAsync(
415+
`cd ${shescape.quote(projectPath)} && git remote get-url origin 2>/dev/null || true`
416+
);
417+
const { stdout } = await proc.result;
418+
const url = stdout.trim();
419+
// Only use URL if it's not a bundle path (avoids propagating bundle paths)
420+
if (url && !url.includes(".bundle") && !url.includes(".cmux-bundle")) {
421+
originUrl = url;
422+
}
423+
} catch (error) {
424+
// If we can't get origin, continue without it
425+
initLogger.logStderr(`Could not get origin URL: ${getErrorMessage(error)}`);
426+
}
427+
428+
// Step 2: Create bundle locally and pipe to remote file via SSH
376429
initLogger.logStep(`Creating git bundle...`);
377430
await new Promise<void>((resolve, reject) => {
378431
const sshArgs = this.buildSSHArgs(true);
379-
const command = `cd ${JSON.stringify(projectPath)} && git bundle create - --all | ssh ${sshArgs.join(" ")} "cat > ${bundleTempPath}"`;
432+
const command = `cd ${shescape.quote(projectPath)} && git bundle create - --all | ssh ${sshArgs.join(" ")} "cat > ${bundleTempPath}"`;
380433

381434
log.debug(`Creating bundle: ${command}`);
382435
const bashPath = findBashPath();
@@ -405,7 +458,7 @@ export class SSHRuntime implements Runtime {
405458
});
406459
});
407460

408-
// Step 2: Clone from bundle on remote using this.exec
461+
// Step 3: Clone from bundle on remote using this.exec
409462
initLogger.logStep(`Cloning repository on remote...`);
410463

411464
// Expand tilde in destination path for git clone
@@ -427,7 +480,37 @@ export class SSHRuntime implements Runtime {
427480
throw new Error(`Failed to clone repository: ${cloneStderr || cloneStdout}`);
428481
}
429482

430-
// Step 3: Remove bundle file
483+
// Step 4: Update origin remote if we have an origin URL
484+
if (originUrl) {
485+
initLogger.logStep(`Setting origin remote to ${originUrl}...`);
486+
const setOriginStream = await this.exec(
487+
`git -C ${cloneDestPath} remote set-url origin ${shescape.quote(originUrl)}`,
488+
{
489+
cwd: "~",
490+
timeout: 10,
491+
}
492+
);
493+
494+
const setOriginExitCode = await setOriginStream.exitCode;
495+
if (setOriginExitCode !== 0) {
496+
const stderr = await streamToString(setOriginStream.stderr);
497+
log.info(`Failed to set origin remote: ${stderr}`);
498+
// Continue anyway - this is not fatal
499+
}
500+
} else {
501+
// No origin in local repo, remove the origin that points to bundle
502+
initLogger.logStep(`Removing bundle origin remote...`);
503+
const removeOriginStream = await this.exec(
504+
`git -C ${cloneDestPath} remote remove origin 2>/dev/null || true`,
505+
{
506+
cwd: "~",
507+
timeout: 10,
508+
}
509+
);
510+
await removeOriginStream.exitCode;
511+
}
512+
513+
// Step 5: Remove bundle file
431514
initLogger.logStep(`Cleaning up bundle file...`);
432515
const rmStream = await this.exec(`rm ${bundleTempPath}`, {
433516
cwd: "~",
@@ -615,7 +698,7 @@ export class SSHRuntime implements Runtime {
615698
// We create new branches from HEAD instead of the trunkBranch name to avoid issues
616699
// where the local repo's trunk name doesn't match the cloned repo's default branch
617700
initLogger.logStep(`Checking out branch: ${branchName}`);
618-
const checkoutCmd = `(git checkout ${JSON.stringify(branchName)} 2>/dev/null || git checkout -b ${JSON.stringify(branchName)} HEAD)`;
701+
const checkoutCmd = `(git checkout ${shescape.quote(branchName)} 2>/dev/null || git checkout -b ${shescape.quote(branchName)} HEAD)`;
619702

620703
const checkoutStream = await this.exec(checkoutCmd, {
621704
cwd: workspacePath, // Use the full workspace path for git operations
@@ -826,7 +909,7 @@ export class SSHRuntime implements Runtime {
826909
/**
827910
* Helper to convert a ReadableStream to a string
828911
*/
829-
async function streamToString(stream: ReadableStream<Uint8Array>): Promise<string> {
912+
export async function streamToString(stream: ReadableStream<Uint8Array>): Promise<string> {
830913
const reader = stream.getReader();
831914
const decoder = new TextDecoder("utf-8");
832915
let result = "";

0 commit comments

Comments
 (0)