Vendor OpenClaw source as Adolf fork baseline
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11), free to diverge. Tree copied sans upstream .git; upstream remote added for future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19. Preserves docs/ARCHITECTURE.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
This commit is contained in:
296
apps/macos/Sources/OpenClaw/CLIInstaller.swift
Normal file
296
apps/macos/Sources/OpenClaw/CLIInstaller.swift
Normal file
@@ -0,0 +1,296 @@
|
||||
import Foundation
|
||||
|
||||
@MainActor
|
||||
enum CLIInstaller {
|
||||
enum LocalGatewayActivation: Equatable {
|
||||
case ready
|
||||
case deferred
|
||||
case failed
|
||||
}
|
||||
|
||||
enum Status: Equatable {
|
||||
case ready(location: String, version: String)
|
||||
case missing(location: String)
|
||||
case unusable(location: String)
|
||||
case incompatible(location: String, found: String, required: String)
|
||||
|
||||
var isReady: Bool {
|
||||
if case .ready = self { return true }
|
||||
return false
|
||||
}
|
||||
|
||||
var location: String {
|
||||
switch self {
|
||||
case let .ready(location, _),
|
||||
let .missing(location),
|
||||
let .unusable(location),
|
||||
let .incompatible(location, _, _):
|
||||
location
|
||||
}
|
||||
}
|
||||
|
||||
var message: String {
|
||||
switch self {
|
||||
case let .ready(_, version):
|
||||
"OpenClaw Gateway \(version) is ready."
|
||||
case .missing:
|
||||
"OpenClaw Gateway is not installed yet."
|
||||
case .unusable:
|
||||
"The OpenClaw Gateway could not be verified. Setup will repair it."
|
||||
case let .incompatible(_, found, required):
|
||||
"Gateway \(found) does not match app \(required). Setup will update it."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static func installedLocation() -> String? {
|
||||
self.installedLocations(
|
||||
searchPaths: CommandResolver.preferredPaths(),
|
||||
fileManager: .default).first
|
||||
}
|
||||
|
||||
static func installedLocation(
|
||||
searchPaths: [String],
|
||||
fileManager: FileManager) -> String?
|
||||
{
|
||||
self.installedLocations(searchPaths: searchPaths, fileManager: fileManager).first
|
||||
}
|
||||
|
||||
static func installedLocations(
|
||||
searchPaths: [String],
|
||||
fileManager: FileManager) -> [String]
|
||||
{
|
||||
var locations: [String] = []
|
||||
for basePath in searchPaths {
|
||||
let candidate = URL(fileURLWithPath: basePath).appendingPathComponent("openclaw").path
|
||||
var isDirectory: ObjCBool = false
|
||||
|
||||
guard fileManager.fileExists(atPath: candidate, isDirectory: &isDirectory),
|
||||
!isDirectory.boolValue
|
||||
else {
|
||||
continue
|
||||
}
|
||||
|
||||
guard fileManager.isExecutableFile(atPath: candidate) else { continue }
|
||||
|
||||
locations.append(candidate)
|
||||
}
|
||||
return locations
|
||||
}
|
||||
|
||||
static func isInstalled() -> Bool {
|
||||
self.installedLocation() != nil
|
||||
}
|
||||
|
||||
static func managedExecutableLocation() -> String {
|
||||
URL(fileURLWithPath: self.installPrefix())
|
||||
.appendingPathComponent("bin/openclaw")
|
||||
.path
|
||||
}
|
||||
|
||||
static func status() async -> Status {
|
||||
let locations = self.installedLocations(
|
||||
searchPaths: CommandResolver.preferredPaths(),
|
||||
fileManager: .default)
|
||||
guard !locations.isEmpty else {
|
||||
return .missing(location: self.managedExecutableLocation())
|
||||
}
|
||||
|
||||
var fallbackStatus: Status?
|
||||
for location in locations {
|
||||
let status = await self.status(location: location)
|
||||
if status.isReady {
|
||||
self.rememberValidated(status)
|
||||
return status
|
||||
}
|
||||
fallbackStatus = fallbackStatus ?? status
|
||||
}
|
||||
return fallbackStatus ?? .missing(location: self.managedExecutableLocation())
|
||||
}
|
||||
|
||||
static func managedStatus() async -> Status {
|
||||
let location = self.managedExecutableLocation()
|
||||
guard FileManager.default.isExecutableFile(atPath: location) else {
|
||||
return .missing(location: location)
|
||||
}
|
||||
|
||||
let status = await self.status(location: location)
|
||||
if status.isReady {
|
||||
self.rememberValidated(status)
|
||||
}
|
||||
return status
|
||||
}
|
||||
|
||||
static func status(location: String) async -> Status {
|
||||
let environment = self.probeEnvironment(location: location)
|
||||
let response = await ShellExecutor.runDetailed(
|
||||
command: [location, "--version"],
|
||||
cwd: nil,
|
||||
env: environment,
|
||||
timeout: 5)
|
||||
guard response.success else {
|
||||
return .unusable(location: location)
|
||||
}
|
||||
let versionStatus = self.classifyVersion(
|
||||
location: location,
|
||||
output: response.stdout,
|
||||
expectedVersion: GatewayEnvironment.expectedGatewayVersionString())
|
||||
guard versionStatus.isReady else { return versionStatus }
|
||||
guard await self.runtimeIsCompatible(environment: environment) else {
|
||||
return .unusable(location: location)
|
||||
}
|
||||
return versionStatus
|
||||
}
|
||||
|
||||
private static func runtimeIsCompatible(environment: [String: String]) async -> Bool {
|
||||
let paths = environment["PATH"]?.split(separator: ":").map(String.init) ?? []
|
||||
return await Task.detached(priority: .utility) {
|
||||
if case .success = RuntimeLocator.resolve(searchPaths: paths) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}.value
|
||||
}
|
||||
|
||||
static func classifyVersion(
|
||||
location: String,
|
||||
output: String?,
|
||||
expectedVersion: String?) -> Status
|
||||
{
|
||||
let normalized = GatewayEnvironment.normalizeGatewayVersionOutput(output)
|
||||
guard let normalized, let installed = Semver.parse(normalized) else {
|
||||
return .unusable(location: location)
|
||||
}
|
||||
guard let required = Semver.parse(expectedVersion) else {
|
||||
return .ready(location: location, version: normalized)
|
||||
}
|
||||
guard installed.compatible(with: required) else {
|
||||
return .incompatible(
|
||||
location: location,
|
||||
found: normalized,
|
||||
required: expectedVersion ?? required.description)
|
||||
}
|
||||
return .ready(location: location, version: normalized)
|
||||
}
|
||||
|
||||
static func probeEnvironment(
|
||||
location: String,
|
||||
processEnvironment: [String: String] = ProcessInfo.processInfo.environment,
|
||||
preferredPaths: [String] = CommandResolver.preferredPaths(),
|
||||
managedExecutable: String? = nil,
|
||||
managedRuntimeDirectory: String? = nil) -> [String: String]
|
||||
{
|
||||
var environment = processEnvironment
|
||||
let executableDirectory = URL(fileURLWithPath: location).deletingLastPathComponent().path
|
||||
let effectiveManagedExecutable = managedExecutable ?? self.managedExecutableLocation()
|
||||
let effectiveManagedRuntimeDirectory = managedRuntimeDirectory ?? URL(fileURLWithPath: self.installPrefix())
|
||||
.appendingPathComponent("tools/node/bin")
|
||||
.path
|
||||
let initialPaths = location == effectiveManagedExecutable
|
||||
? [executableDirectory, effectiveManagedRuntimeDirectory]
|
||||
: [executableDirectory]
|
||||
var seen = Set<String>()
|
||||
let paths = (initialPaths + preferredPaths).filter { seen.insert($0).inserted }
|
||||
environment["PATH"] = paths.joined(separator: ":")
|
||||
return environment
|
||||
}
|
||||
|
||||
private static func rememberValidated(_ status: Status) {
|
||||
guard case let .ready(location, version) = status else { return }
|
||||
UserDefaults.standard.set(location, forKey: cliValidatedExecutableKey)
|
||||
UserDefaults.standard.set(version, forKey: cliValidatedVersionKey)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
static func install(statusHandler: @escaping @MainActor @Sendable (String) async -> Void) async -> Bool {
|
||||
let expected = GatewayEnvironment.expectedGatewayVersionString() ?? "latest"
|
||||
let prefix = Self.installPrefix()
|
||||
await statusHandler("Installing openclaw CLI…")
|
||||
guard let installerURL = Bundle.main.url(forResource: "install-cli", withExtension: "sh") else {
|
||||
await statusHandler("Install failed: installer resource is missing. Reinstall OpenClaw.")
|
||||
return false
|
||||
}
|
||||
let cmd = self.installScriptCommand(
|
||||
version: expected,
|
||||
prefix: prefix,
|
||||
scriptPath: installerURL.path)
|
||||
let response = await ShellExecutor.runDetailed(command: cmd, cwd: nil, env: nil, timeout: 900)
|
||||
|
||||
if response.success {
|
||||
let managedStatus = await self.managedStatus()
|
||||
guard managedStatus.isReady else {
|
||||
await statusHandler("Install failed: \(managedStatus.message)")
|
||||
return false
|
||||
}
|
||||
let parsed = self.parseInstallEvents(response.stdout)
|
||||
let installedVersion = parsed.last { $0.event == "done" }?.version
|
||||
let summary = installedVersion.map { "Installed openclaw \($0)." } ?? "Installed openclaw."
|
||||
await statusHandler(summary)
|
||||
return true
|
||||
}
|
||||
|
||||
let parsed = self.parseInstallEvents(response.stdout)
|
||||
if let error = parsed.last(where: { $0.event == "error" })?.message {
|
||||
await statusHandler("Install failed: \(error)")
|
||||
return false
|
||||
}
|
||||
|
||||
let detail = response.stderr.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let fallback = response.errorMessage ?? "install failed"
|
||||
await statusHandler("Install failed: \(detail.isEmpty ? fallback : detail)")
|
||||
return false
|
||||
}
|
||||
|
||||
private static func installPrefix() -> String {
|
||||
FileManager().homeDirectoryForCurrentUser
|
||||
.appendingPathComponent(".openclaw")
|
||||
.path
|
||||
}
|
||||
|
||||
static func installScriptCommand(version: String, prefix: String, scriptPath: String) -> [String] {
|
||||
[
|
||||
"/bin/bash",
|
||||
scriptPath,
|
||||
"--json",
|
||||
"--no-onboard",
|
||||
"--prefix",
|
||||
prefix,
|
||||
"--version",
|
||||
version,
|
||||
]
|
||||
}
|
||||
|
||||
static func activateLocalGateway(
|
||||
mode: AppState.ConnectionMode = AppStateStore.shared.connectionMode,
|
||||
paused: Bool = AppStateStore.shared.isPaused,
|
||||
start: @MainActor () -> Void = { GatewayProcessManager.shared.setActive(true) },
|
||||
waitUntilReady: @MainActor () async -> Bool = {
|
||||
await GatewayProcessManager.shared.waitForGatewayReady(timeout: 12)
|
||||
}) async -> LocalGatewayActivation
|
||||
{
|
||||
guard mode == .local, !paused else { return .deferred }
|
||||
start()
|
||||
return await waitUntilReady() ? .ready : .failed
|
||||
}
|
||||
|
||||
private static func parseInstallEvents(_ output: String) -> [InstallEvent] {
|
||||
let decoder = JSONDecoder()
|
||||
let lines = output
|
||||
.split(whereSeparator: \.isNewline)
|
||||
.map { String($0) }
|
||||
var events: [InstallEvent] = []
|
||||
for line in lines {
|
||||
guard let data = line.data(using: .utf8) else { continue }
|
||||
if let event = try? decoder.decode(InstallEvent.self, from: data) {
|
||||
events.append(event)
|
||||
}
|
||||
}
|
||||
return events
|
||||
}
|
||||
}
|
||||
|
||||
private struct InstallEvent: Decodable {
|
||||
let event: String
|
||||
let version: String?
|
||||
let message: String?
|
||||
}
|
||||
Reference in New Issue
Block a user