Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/nightly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -212,15 +212,19 @@ jobs:
set -euo pipefail
APP_BINARY="build-universal/Build/Products/Release/cmux.app/Contents/MacOS/cmux"
CLI_BINARY="build-universal/Build/Products/Release/cmux.app/Contents/Resources/bin/cmux"
CLOUD_CLI_BINARY="build-universal/Build/Products/Release/cmux.app/Contents/Resources/bin/cmux-cloud"
HELPER_BINARY="build-universal/Build/Products/Release/cmux.app/Contents/Resources/bin/ghostty"
APP_ARCHS="$(lipo -archs "$APP_BINARY")"
CLI_ARCHS="$(lipo -archs "$CLI_BINARY")"
CLOUD_CLI_ARCHS="$(lipo -archs "$CLOUD_CLI_BINARY")"
HELPER_ARCHS="$(lipo -archs "$HELPER_BINARY")"
echo "App binary architectures: $APP_ARCHS"
echo "CLI binary architectures: $CLI_ARCHS"
echo "Cloud CLI binary architectures: $CLOUD_CLI_ARCHS"
echo "Ghostty helper architectures: $HELPER_ARCHS"
[[ "$APP_ARCHS" == *arm64* && "$APP_ARCHS" == *x86_64* ]]
[[ "$CLI_ARCHS" == *arm64* && "$CLI_ARCHS" == *x86_64* ]]
[[ "$CLOUD_CLI_ARCHS" == *arm64* && "$CLOUD_CLI_ARCHS" == *x86_64* ]]
[[ "$HELPER_ARCHS" == *arm64* && "$HELPER_ARCHS" == *x86_64* ]]

- name: Run CLI version memory guard regression
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -176,15 +176,19 @@ jobs:
set -euo pipefail
APP_BINARY="build-universal/Build/Products/Release/cmux.app/Contents/MacOS/cmux"
CLI_BINARY="build-universal/Build/Products/Release/cmux.app/Contents/Resources/bin/cmux"
CLOUD_CLI_BINARY="build-universal/Build/Products/Release/cmux.app/Contents/Resources/bin/cmux-cloud"
HELPER_BINARY="build-universal/Build/Products/Release/cmux.app/Contents/Resources/bin/ghostty"
APP_ARCHS="$(lipo -archs "$APP_BINARY")"
CLI_ARCHS="$(lipo -archs "$CLI_BINARY")"
CLOUD_CLI_ARCHS="$(lipo -archs "$CLOUD_CLI_BINARY")"
HELPER_ARCHS="$(lipo -archs "$HELPER_BINARY")"
echo "App binary architectures: $APP_ARCHS"
echo "CLI binary architectures: $CLI_ARCHS"
echo "Cloud CLI binary architectures: $CLOUD_CLI_ARCHS"
echo "Ghostty helper architectures: $HELPER_ARCHS"
[[ "$APP_ARCHS" == *arm64* && "$APP_ARCHS" == *x86_64* ]]
[[ "$CLI_ARCHS" == *arm64* && "$CLI_ARCHS" == *x86_64* ]]
[[ "$CLOUD_CLI_ARCHS" == *arm64* && "$CLOUD_CLI_ARCHS" == *x86_64* ]]
[[ "$HELPER_ARCHS" == *arm64* && "$HELPER_ARCHS" == *x86_64* ]]

- name: Build remote daemon release assets and inject manifest
Expand Down
114 changes: 108 additions & 6 deletions CLI/cmux.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1524,6 +1524,10 @@ final class SocketClient {
relayEndpoint != nil
}

static func isRelaySocketPath(_ raw: String) -> Bool {
parseRelayEndpoint(raw) != nil
}

func connectionAppearsOpen() -> Bool {
if relayEndpoint != nil, socketFD < 0 {
do {
Expand Down Expand Up @@ -2655,6 +2659,92 @@ struct CMUXCLI {
}
}

private static func cloudCLIExecutablePath() -> String? {
let fileManager = FileManager.default
var candidates: [URL] = []
if let override = normalizedEnvValue(ProcessInfo.processInfo.environment["CMUX_CLOUD_CLI_PATH"]) {
candidates.append(URL(fileURLWithPath: override))
}
if let executableURL = CLIExecutableLocator.currentExecutableURL() {
candidates.append(executableURL.deletingLastPathComponent().appendingPathComponent("cmux-cloud"))
}
if let appBundle = CLIExecutableLocator.enclosingAppBundle() {
candidates.append(
appBundle.bundleURL
.appendingPathComponent("Contents", isDirectory: true)
.appendingPathComponent("Resources", isDirectory: true)
.appendingPathComponent("bin", isDirectory: true)
.appendingPathComponent("cmux-cloud", isDirectory: false)
)
}
for candidate in candidates {
let path = candidate.path
var isDirectory: ObjCBool = false
if fileManager.fileExists(atPath: path, isDirectory: &isDirectory),
!isDirectory.boolValue,
fileManager.isExecutableFile(atPath: path)
{
return path
}
}
return nil
}

private func runCloudRustCommand(
commandArgs: [String],
socketPath: String,
explicitPassword: String?,
jsonOutput: Bool,
idFormatArg: String?,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
windowId: String?
) throws -> Never {
guard let helperPath = Self.cloudCLIExecutablePath() else {
throw CLIError(message: String(
localized: "cli.cloud.helper.missing",
defaultValue: "cmux cloud helper is missing. Reload cmux and retry."
))
}

let resolvedPassword = SocketPasswordResolver.resolve(
explicit: explicitPassword,
socketPath: socketPath
)
setenv("CMUX_CLOUD_SOCKET_PATH", socketPath, 1)
setenv("CMUX_SOCKET_PATH", socketPath, 1)
setenv("CMUX_CLOUD_JSON", jsonOutput ? "1" : "0", 1)
if let resolvedPassword {
setenv("CMUX_CLOUD_SOCKET_PASSWORD", resolvedPassword, 1)
} else {
unsetenv("CMUX_CLOUD_SOCKET_PASSWORD")
}
if let idFormatArg {
setenv("CMUX_CLOUD_ID_FORMAT", idFormatArg, 1)
} else {
unsetenv("CMUX_CLOUD_ID_FORMAT")
}
if let windowId {
setenv("CMUX_CLOUD_WINDOW", windowId, 1)
} else {
unsetenv("CMUX_CLOUD_WINDOW")
}
if let currentExecutablePath = CLIExecutableLocator.currentExecutableURL()?.path {
setenv("CMUX_CLOUD_PARENT_CLI", currentExecutablePath, 1)
}

var argv = ([helperPath] + commandArgs).map { strdup($0) }
defer {
for item in argv {
free(item)
}
}
argv.append(nil)
execv(helperPath, &argv)
throw CLIError(message: String(
localized: "cli.cloud.helper.launchFailed",
defaultValue: "Could not start the cmux cloud helper. Reload cmux and retry."
))
}

private static func shouldFocusWindowBeforeDispatch(command: String, commandArgs: [String]) -> Bool {
let normalizedCommand = command.lowercased()
if normalizedCommand == "surface-resume" {
Expand Down Expand Up @@ -3019,6 +3109,17 @@ struct CMUXCLI {
commandArgs: commandArgs
)

if command == "cloud", !SocketClient.isRelaySocketPath(resolvedSocketPath) {
try runCloudRustCommand(
commandArgs: commandArgs,
socketPath: resolvedSocketPath,
explicitPassword: socketPasswordArg,
jsonOutput: jsonOutput,
idFormatArg: idFormatArg,
windowId: windowId
)
}

let client = SocketClient(path: resolvedSocketPath)
if resolvedSocketPath != socketPath {
cliTelemetry.breadcrumb(
Expand Down Expand Up @@ -11602,7 +11703,7 @@ struct CMUXCLI {
return """
Usage: cmux \(command) <new|ls|rm|exec|shell|attach|ssh|ssh-info> [args...]

Manage cloud VMs. `cloud` is an alias for `vm`. Requires `cmux auth login`.
Manage cloud VMs. Requires `cmux auth login`.

Subcommands:
ls List your cloud VMs.
Expand All @@ -11627,10 +11728,10 @@ struct CMUXCLI {
local testing from the web worktree.

Example:
cmux vm new
cmux vm ls
cmux cloud exec <id> -- echo hello
cmux vm rm <id>
cmux \(command) new
cmux \(command) ls
cmux \(command) exec <id> -- echo hello
cmux \(command) rm <id>
"""
case "rpc":
return """
Expand Down Expand Up @@ -29498,7 +29599,8 @@ export default function cmuxPiSessionExtension(pi: ExtensionAPI) {
events [--after <seq>] [--cursor-file <path>] [--name <event>] [--category <category>] [--reconnect] [--limit <n>] [--no-ack] [--no-heartbeat]
auth <status|login|logout>
login | logout (aliases for auth login/logout)
vm <new|ls|rm|exec|shell|ssh> [args...] (alias: cloud)
cloud <new|ls|rm|exec|shell|ssh> [args...]
vm <new|ls|rm|exec|shell|ssh> [args...] (compatibility alias)
rpc <method> [json-params]
identify [--workspace <id|ref|index>] [--surface <id|ref|index>] [--window <id|ref|index>] [--no-caller]
list-windows
Expand Down
107 changes: 107 additions & 0 deletions Native/CloudCLI/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 19 additions & 0 deletions Native/CloudCLI/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[package]
name = "cmux-cloud-cli"
version = "0.1.0"
edition = "2021"
publish = false

[[bin]]
name = "cmux-cloud"
path = "src/main.rs"

[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"

[profile.dev]
panic = "abort"

[profile.release]
panic = "abort"
Loading
Loading