Changes from background agent bc-b072b4af-98b7-4de6-bc1c-8faa623cdb13 (#210)

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Haze <hazeone@users.noreply.github.com>
This commit is contained in:
Haze
2026-02-28 15:37:02 +08:00
committed by GitHub
Unverified
parent 163099add8
commit 6859656847
12 changed files with 526 additions and 107 deletions

View File

@@ -59,6 +59,8 @@ mac:
extraResources:
- from: resources/bin/darwin-${arch}
to: bin
- from: resources/cli/posix/
to: cli/
category: public.app-category.productivity
icon: resources/icons/icon.icns
target:
@@ -104,6 +106,8 @@ win:
extraResources:
- from: resources/bin/win32-${arch}
to: bin
- from: resources/cli/win32/
to: cli/
icon: resources/icons/icon.ico
target:
- target: nsis
@@ -131,6 +135,8 @@ linux:
extraResources:
- from: resources/bin/linux-${arch}
to: bin
- from: resources/cli/posix/
to: cli/
icon: resources/icons
target:
- target: AppImage

View File

@@ -15,6 +15,7 @@ import { warmupNetworkOptimization } from '../utils/uv-env';
import { ClawHubService } from '../gateway/clawhub';
import { ensureClawXContext, repairClawXOnlyBootstrapFiles } from '../utils/openclaw-workspace';
import { autoInstallCliIfNeeded, generateCompletionCache, installCompletionToProfile } from '../utils/openclaw-cli';
import { isQuitting, setQuitting } from './app-state';
// Disable GPU hardware acceleration globally for maximum stability across
@@ -203,6 +204,16 @@ async function initialize(): Promise<void> {
logger.warn('Failed to merge ClawX context into workspace:', error);
});
// Auto-install openclaw CLI and shell completions (non-blocking).
void autoInstallCliIfNeeded((installedPath) => {
mainWindow?.webContents.send('openclaw:cli-installed', installedPath);
}).then(() => {
generateCompletionCache();
installCompletionToProfile();
}).catch((error) => {
logger.warn('CLI auto-install failed:', error);
});
// Re-apply ClawX context after every gateway restart because the gateway
// may re-seed workspace files with clean templates (losing ClawX markers).
gatewayManager.on('status', (status: { state: string }) => {

View File

@@ -23,7 +23,7 @@ import {
type ProviderConfig,
} from '../utils/secure-storage';
import { getOpenClawStatus, getOpenClawDir, getOpenClawConfigDir, getOpenClawSkillsDir, ensureDir } from '../utils/paths';
import { getOpenClawCliCommand, installOpenClawCliMac } from '../utils/openclaw-cli';
import { getOpenClawCliCommand } from '../utils/openclaw-cli';
import { getSetting } from '../utils/store';
import {
saveProviderKeyToOpenClaw,
@@ -692,10 +692,6 @@ function registerOpenClawHandlers(gatewayManager: GatewayManager): void {
}
});
// Install a system-wide openclaw command on macOS (requires admin prompt)
ipcMain.handle('openclaw:installCliMac', async () => {
return installOpenClawCliMac();
});
// ==================== Channel Configuration Handlers ====================

View File

@@ -130,7 +130,6 @@ const electronAPI = {
'openclaw:getConfigDir',
'openclaw:getSkillsDir',
'openclaw:getCliCommand',
'openclaw:installCliMac',
];
if (validChannels.includes(channel)) {
@@ -168,6 +167,7 @@ const electronAPI = {
'oauth:code',
'oauth:success',
'oauth:error',
'openclaw:cli-installed',
];
if (validChannels.includes(channel)) {

View File

@@ -1,13 +1,24 @@
/**
* OpenClaw CLI utilities
* OpenClaw CLI utilities — cross-platform auto-install
*/
import { app } from 'electron';
import { chmodSync, existsSync, mkdirSync, writeFileSync } from 'node:fs';
import {
appendFileSync,
chmodSync,
existsSync,
mkdirSync,
readFileSync,
symlinkSync,
unlinkSync,
} from 'node:fs';
import { spawn } from 'node:child_process';
import { homedir } from 'node:os';
import { dirname, join } from 'node:path';
import { join, dirname } from 'node:path';
import { getOpenClawDir, getOpenClawEntryPath } from './paths';
import { logger } from './logger';
// ── Quoting helpers ──────────────────────────────────────────────────────────
function escapeForDoubleQuotes(value: string): string {
return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
}
@@ -20,17 +31,25 @@ function quoteForPowerShell(value: string): string {
return `'${value.replace(/'/g, "''")}'`;
}
// ── CLI command string (for display / copy) ──────────────────────────────────
export function getOpenClawCliCommand(): string {
const entryPath = getOpenClawEntryPath();
const platform = process.platform;
if (platform === 'darwin') {
if (platform === 'darwin' || platform === 'linux') {
const localBinPath = join(homedir(), '.local', 'bin', 'openclaw');
if (existsSync(localBinPath)) {
return quoteForPosix(localBinPath);
}
}
if (platform === 'linux') {
if (existsSync('/usr/local/bin/openclaw')) {
return '/usr/local/bin/openclaw';
}
}
if (!app.isPackaged) {
const openclawDir = getOpenClawDir();
const nodeModulesDir = dirname(openclawDir);
@@ -46,6 +65,14 @@ export function getOpenClawCliCommand(): string {
}
if (app.isPackaged) {
if (platform === 'win32') {
const cliDir = join(process.resourcesPath, 'cli');
const cmdPath = join(cliDir, 'openclaw.cmd');
if (existsSync(cmdPath)) {
return quoteForPowerShell(cmdPath);
}
}
const execPath = process.execPath;
if (platform === 'win32') {
return `$env:ELECTRON_RUN_AS_NODE=1; & ${quoteForPowerShell(execPath)} ${quoteForPowerShell(entryPath)}`;
@@ -60,34 +87,236 @@ export function getOpenClawCliCommand(): string {
return `node ${quoteForPosix(entryPath)}`;
}
export async function installOpenClawCliMac(): Promise<{ success: boolean; path?: string; error?: string }>
{
if (process.platform !== 'darwin') {
return { success: false, error: 'Install is only supported on macOS.' };
// ── Packaged CLI wrapper path ────────────────────────────────────────────────
function getPackagedCliWrapperPath(): string | null {
if (!app.isPackaged) return null;
const platform = process.platform;
if (platform === 'darwin' || platform === 'linux') {
const wrapper = join(process.resourcesPath, 'cli', 'openclaw');
return existsSync(wrapper) ? wrapper : null;
}
if (platform === 'win32') {
const wrapper = join(process.resourcesPath, 'cli', 'openclaw.cmd');
return existsSync(wrapper) ? wrapper : null;
}
return null;
}
// ── macOS / Linux install ────────────────────────────────────────────────────
function getCliTargetPath(): string {
return join(homedir(), '.local', 'bin', 'openclaw');
}
export async function installOpenClawCli(): Promise<{
success: boolean; path?: string; error?: string;
}> {
const platform = process.platform;
if (platform === 'win32') {
return { success: false, error: 'Windows CLI is configured by the installer.' };
}
const entryPath = getOpenClawEntryPath();
if (!existsSync(entryPath)) {
return { success: false, error: `OpenClaw entry not found at: ${entryPath}` };
if (!app.isPackaged) {
return { success: false, error: 'CLI install is only available in packaged builds.' };
}
const wrapperSrc = getPackagedCliWrapperPath();
if (!wrapperSrc) {
return { success: false, error: 'CLI wrapper not found in app resources.' };
}
const execPath = process.execPath;
const targetDir = join(homedir(), '.local', 'bin');
const target = join(targetDir, 'openclaw');
const target = getCliTargetPath();
try {
const script = [
'#!/bin/sh',
`ELECTRON_RUN_AS_NODE=1 "${escapeForDoubleQuotes(execPath)}" "${escapeForDoubleQuotes(entryPath)}" "$@"`,
'',
].join('\n');
mkdirSync(targetDir, { recursive: true });
writeFileSync(target, script, { mode: 0o755 });
chmodSync(target, 0o755);
// Remove existing file/symlink to avoid EEXIST
if (existsSync(target)) {
unlinkSync(target);
}
symlinkSync(wrapperSrc, target);
chmodSync(wrapperSrc, 0o755);
logger.info(`OpenClaw CLI symlink created: ${target} -> ${wrapperSrc}`);
return { success: true, path: target };
} catch (error) {
logger.error('Failed to install OpenClaw CLI:', error);
return { success: false, error: String(error) };
}
}
// ── Auto-install on first launch ─────────────────────────────────────────────
function isCliInstalled(): boolean {
const platform = process.platform;
if (platform === 'win32') return true; // handled by NSIS installer
const target = getCliTargetPath();
if (!existsSync(target)) return false;
// Also check /usr/local/bin/openclaw for deb installs
if (platform === 'linux' && existsSync('/usr/local/bin/openclaw')) return true;
return true;
}
function ensureLocalBinInPath(): void {
if (process.platform === 'win32') return;
const localBin = join(homedir(), '.local', 'bin');
const pathEnv = process.env.PATH || '';
if (pathEnv.split(':').includes(localBin)) return;
const shell = process.env.SHELL || '/bin/zsh';
const profileFile = shell.includes('zsh')
? join(homedir(), '.zshrc')
: shell.includes('fish')
? join(homedir(), '.config', 'fish', 'config.fish')
: join(homedir(), '.bashrc');
try {
const marker = '.local/bin';
let content = '';
try {
content = readFileSync(profileFile, 'utf-8');
} catch {
// file doesn't exist yet
}
if (content.includes(marker)) return;
const line = shell.includes('fish')
? '\n# Added by ClawX\nfish_add_path "$HOME/.local/bin"\n'
: '\n# Added by ClawX\nexport PATH="$HOME/.local/bin:$PATH"\n';
appendFileSync(profileFile, line);
logger.info(`Added ~/.local/bin to PATH in ${profileFile}`);
} catch (error) {
logger.warn('Failed to add ~/.local/bin to PATH:', error);
}
}
export async function autoInstallCliIfNeeded(
notify?: (path: string) => void,
): Promise<void> {
if (!app.isPackaged) return;
if (process.platform === 'win32') return; // NSIS handles it
const target = getCliTargetPath();
const wrapperSrc = getPackagedCliWrapperPath();
if (isCliInstalled()) {
if (target && wrapperSrc && existsSync(target)) {
try {
unlinkSync(target);
symlinkSync(wrapperSrc, target);
logger.debug(`Refreshed CLI symlink: ${target} -> ${wrapperSrc}`);
} catch {
// non-critical
}
}
return;
}
logger.info('Auto-installing openclaw CLI...');
const result = await installOpenClawCli();
if (result.success) {
logger.info(`CLI auto-installed at ${result.path}`);
ensureLocalBinInPath();
if (result.path) notify?.(result.path);
} else {
logger.warn(`CLI auto-install failed: ${result.error}`);
}
}
// ── Completion helpers ───────────────────────────────────────────────────────
function getNodeExecForCli(): string {
if (process.platform === 'darwin' && app.isPackaged) {
const appName = app.getName();
const helperName = `${appName} Helper`;
const helperPath = join(
dirname(process.execPath),
'../Frameworks',
`${helperName}.app`,
'Contents/MacOS',
helperName,
);
if (existsSync(helperPath)) return helperPath;
}
return process.execPath;
}
export function generateCompletionCache(): void {
if (!app.isPackaged) return;
const entryPath = getOpenClawEntryPath();
if (!existsSync(entryPath)) return;
const execPath = getNodeExecForCli();
const child = spawn(execPath, [entryPath, 'completion', '--write-state'], {
env: {
...process.env,
ELECTRON_RUN_AS_NODE: '1',
OPENCLAW_NO_RESPAWN: '1',
OPENCLAW_EMBEDDED_IN: 'ClawX',
},
stdio: 'ignore',
detached: false,
});
child.on('close', (code) => {
if (code === 0) {
logger.info('OpenClaw completion cache generated');
} else {
logger.warn(`OpenClaw completion cache generation exited with code ${code}`);
}
});
child.on('error', (err) => {
logger.warn('Failed to generate completion cache:', err);
});
}
export function installCompletionToProfile(): void {
if (!app.isPackaged) return;
if (process.platform === 'win32') return;
const entryPath = getOpenClawEntryPath();
if (!existsSync(entryPath)) return;
const execPath = getNodeExecForCli();
const child = spawn(
execPath,
[entryPath, 'completion', '--install', '-y'],
{
env: {
...process.env,
ELECTRON_RUN_AS_NODE: '1',
OPENCLAW_NO_RESPAWN: '1',
OPENCLAW_EMBEDDED_IN: 'ClawX',
},
stdio: 'ignore',
detached: false,
},
);
child.on('close', (code) => {
if (code === 0) {
logger.info('OpenClaw completion installed to shell profile');
} else {
logger.warn(`OpenClaw completion install exited with code ${code}`);
}
});
child.on('error', (err) => {
logger.warn('Failed to install completion to shell profile:', err);
});
}

48
resources/cli/posix/openclaw Executable file
View File

@@ -0,0 +1,48 @@
#!/bin/sh
# OpenClaw CLI — managed by ClawX
# Do not edit manually. Regenerated on ClawX updates.
# Resolve the real path of this script (follow symlinks)
SCRIPT="$0"
while [ -L "$SCRIPT" ]; do
SCRIPT_DIR="$(cd -P "$(dirname "$SCRIPT")" && pwd)"
SCRIPT="$(readlink "$SCRIPT")"
[ "${SCRIPT#/}" = "$SCRIPT" ] && SCRIPT="$SCRIPT_DIR/$SCRIPT"
done
SCRIPT_DIR="$(cd -P "$(dirname "$SCRIPT")" && pwd)"
if [ "$(uname)" = "Darwin" ]; then
# macOS: <App>.app/Contents/Resources/cli/openclaw
# SCRIPT_DIR = .../Contents/Resources/cli
CONTENTS_DIR="$(dirname "$(dirname "$SCRIPT_DIR")")"
ELECTRON="$CONTENTS_DIR/MacOS/ClawX"
CLI="$CONTENTS_DIR/Resources/openclaw/openclaw.mjs"
else
# Linux: /opt/ClawX/resources/cli/openclaw
# SCRIPT_DIR = .../resources/cli
INSTALL_DIR="$(dirname "$(dirname "$SCRIPT_DIR")")"
ELECTRON="$INSTALL_DIR/clawx"
CLI="$INSTALL_DIR/resources/openclaw/openclaw.mjs"
fi
if [ ! -f "$ELECTRON" ]; then
echo "Error: ClawX executable not found at $ELECTRON" >&2
echo "Please reinstall ClawX or remove this script: $0" >&2
exit 1
fi
case "$1" in
update)
echo "openclaw is managed by ClawX (bundled version)."
echo ""
echo "To update openclaw, update ClawX:"
echo " Open ClawX > Settings > Check for Updates"
echo " Or download the latest version from https://clawx.app"
echo ""
ELECTRON_RUN_AS_NODE=1 "$ELECTRON" "$CLI" --version 2>/dev/null || true
exit 0
;;
esac
export OPENCLAW_EMBEDDED_IN="ClawX"
ELECTRON_RUN_AS_NODE=1 exec "$ELECTRON" "$CLI" "$@"

View File

@@ -0,0 +1,17 @@
#!/bin/sh
# OpenClaw CLI wrapper for Git Bash / MSYS2 on Windows
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
INSTALL_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
case "$1" in
update)
echo "openclaw is managed by ClawX (bundled version)."
echo ""
echo "To update openclaw, update ClawX:"
echo " Open ClawX > Settings > Check for Updates"
exit 0
;;
esac
export OPENCLAW_EMBEDDED_IN="ClawX"
ELECTRON_RUN_AS_NODE=1 exec "$INSTALL_DIR/ClawX.exe" "$INSTALL_DIR/resources/openclaw/openclaw.mjs" "$@"

View File

@@ -0,0 +1,16 @@
@echo off
setlocal
if /i "%1"=="update" (
echo openclaw is managed by ClawX ^(bundled version^).
echo.
echo To update openclaw, update ClawX:
echo Open ClawX ^> Settings ^> Check for Updates
echo Or download the latest version from https://clawx.app
exit /b 0
)
set ELECTRON_RUN_AS_NODE=1
set OPENCLAW_EMBEDDED_IN=ClawX
"%~dp0..\..\ClawX.exe" "%~dp0..\openclaw\openclaw.mjs" %*
endlocal

View File

@@ -1,4 +1,7 @@
; ClawX Custom NSIS Installer/Uninstaller Script
;
; Install: enables long paths, adds resources\cli to user PATH for openclaw CLI.
; Uninstall: removes the PATH entry and optionally deletes user data.
!macro customInstall
; Enable Windows long path support (Windows 10 1607+ / Windows 11).
@@ -6,9 +9,86 @@
; Writing to HKLM requires admin privileges; on per-user installs without
; elevation this call silently fails — no crash, just no key written.
WriteRegDWORD HKLM "SYSTEM\CurrentControlSet\Control\FileSystem" "LongPathsEnabled" 1
; Add resources\cli to the current user's PATH for openclaw CLI.
; Read current PATH, skip if already present, append otherwise.
ReadRegStr $0 HKCU "Environment" "Path"
StrCmp $0 "" _ci_setNew
; Check if our CLI dir is already in PATH
Push "$INSTDIR\resources\cli"
Push $0
Call _ci_StrContains
Pop $1
StrCmp $1 "" 0 _ci_done
; Append to existing PATH
StrCpy $0 "$0;$INSTDIR\resources\cli"
Goto _ci_write
_ci_setNew:
StrCpy $0 "$INSTDIR\resources\cli"
_ci_write:
WriteRegExpandStr HKCU "Environment" "Path" $0
; Broadcast WM_SETTINGCHANGE so running Explorer/terminals pick up the change
SendMessage ${HWND_BROADCAST} ${WM_SETTINGCHANGE} 0 "STR:Environment" /TIMEOUT=500
_ci_done:
!macroend
; Helper: check if $R0 (needle) is found within $R1 (haystack).
; Pushes needle then haystack before call; pops result (needle if found, "" if not).
Function _ci_StrContains
Exch $R1 ; haystack
Exch
Exch $R0 ; needle
Push $R2
Push $R3
Push $R4
StrLen $R3 $R0
StrLen $R4 $R1
IntOp $R4 $R4 - $R3
StrCpy $R2 0
_ci_loop:
IntCmp $R2 $R4 0 0 _ci_notfound
StrCpy $1 $R1 $R3 $R2
StrCmp $1 $R0 _ci_found
IntOp $R2 $R2 + 1
Goto _ci_loop
_ci_found:
StrCpy $R0 $R0
Goto _ci_end
_ci_notfound:
StrCpy $R0 ""
_ci_end:
Pop $R4
Pop $R3
Pop $R2
Pop $R1
Exch $R0
FunctionEnd
!macro customUnInstall
; Remove resources\cli from user PATH
ReadRegStr $0 HKCU "Environment" "Path"
StrCmp $0 "" _cu_pathDone
; Remove our entry (with leading or trailing semicolons)
Push $0
Push "$INSTDIR\resources\cli"
Call un._cu_RemoveFromPath
Pop $0
WriteRegExpandStr HKCU "Environment" "Path" $0
SendMessage ${HWND_BROADCAST} ${WM_SETTINGCHANGE} 0 "STR:Environment" /TIMEOUT=500
_cu_pathDone:
; Ask user if they want to completely remove all user data
MessageBox MB_YESNO|MB_ICONQUESTION \
"Do you want to completely remove all ClawX user data?$\r$\n$\r$\nThis will delete:$\r$\n • .openclaw folder (configuration & skills)$\r$\n • AppData\Local\clawx (local app data)$\r$\n • AppData\Roaming\clawx (roaming app data)$\r$\n$\r$\nSelect 'No' to keep your data for future reinstallation." \
@@ -21,32 +101,18 @@
RMDir /r "$APPDATA\clawx"
; --- For per-machine (all users) installs, enumerate all user profiles ---
; Registry key HKLM\...\ProfileList contains a subkey for each user SID.
; Each subkey has a ProfileImagePath value like "C:\Users\username"
; (which may contain unexpanded env vars like %SystemDrive%).
; We iterate all profiles, expand the path, skip the current user
; (already cleaned above), and remove data for every other user.
; RMDir /r silently does nothing if the directory doesn't exist or
; we lack permissions, so this is safe for per-user installs too.
StrCpy $R0 0 ; Registry enum index
StrCpy $R0 0
_cu_enumLoop:
EnumRegKey $R1 HKLM "SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList" $R0
StrCmp $R1 "" _cu_enumDone ; No more subkeys -> done
StrCmp $R1 "" _cu_enumDone
; Read ProfileImagePath for this SID
ReadRegStr $R2 HKLM "SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\$R1" "ProfileImagePath"
StrCmp $R2 "" _cu_enumNext ; Skip entries without a path
StrCmp $R2 "" _cu_enumNext
; ProfileImagePath may contain unexpanded env vars (e.g. %SystemDrive%),
; expand them to get the real path.
ExpandEnvStrings $R2 $R2
; Skip the current user's profile (already cleaned above)
StrCmp $R2 $PROFILE _cu_enumNext
; Remove .openclaw and AppData for this user profile
RMDir /r "$R2\.openclaw"
RMDir /r "$R2\AppData\Local\clawx"
RMDir /r "$R2\AppData\Roaming\clawx"
@@ -57,4 +123,72 @@
_cu_enumDone:
_cu_skipRemove:
!macroend
!macroend
; Uninstaller helper: remove a substring from a semicolon-delimited PATH string.
; Push haystack, push needle before call; pops cleaned string.
Function un._cu_RemoveFromPath
Exch $R0 ; needle
Exch
Exch $R1 ; haystack
; Try removing ";needle" (entry in the middle or end)
Push "$R1"
Push ";$R0"
Call un._ci_StrReplace
Pop $R1
; Try removing "needle;" (entry at the start)
Push "$R1"
Push "$R0;"
Call un._ci_StrReplace
Pop $R1
; Try removing exact match (only entry)
StrCmp $R1 $R0 0 +2
StrCpy $R1 ""
Pop $R0
Exch $R1
FunctionEnd
; Uninstaller helper: remove first occurrence of needle from haystack.
; Push haystack, push needle; pops result.
Function un._ci_StrReplace
Exch $R0 ; needle
Exch
Exch $R1 ; haystack
Push $R2
Push $R3
Push $R4
Push $R5
StrLen $R3 $R0
StrLen $R4 $R1
StrCpy $R5 ""
StrCpy $R2 0
_usr_loop:
IntCmp $R2 $R4 _usr_done _usr_done
StrCpy $1 $R1 $R3 $R2
StrCmp $1 $R0 _usr_found
StrCpy $1 $R1 1 $R2
StrCpy $R5 "$R5$1"
IntOp $R2 $R2 + 1
Goto _usr_loop
_usr_found:
; Copy the part after the needle
IntOp $R2 $R2 + $R3
StrCpy $1 $R1 "" $R2
StrCpy $R5 "$R5$1"
_usr_done:
StrCpy $R1 $R5
Pop $R5
Pop $R4
Pop $R3
Pop $R2
Pop $R0
Exch $R1
FunctionEnd

View File

@@ -14,9 +14,16 @@ if command -v gtk-update-icon-cache &> /dev/null; then
gtk-update-icon-cache -q /usr/share/icons/hicolor || true
fi
# Create symbolic link for CLI access (optional)
# Create symbolic link for ClawX app binary
if [ -x /opt/ClawX/clawx ]; then
ln -sf /opt/ClawX/clawx /usr/local/bin/clawx 2>/dev/null || true
fi
# Create symbolic link for openclaw CLI
OPENCLAW_WRAPPER="/opt/ClawX/resources/cli/openclaw"
if [ -f "$OPENCLAW_WRAPPER" ]; then
chmod +x "$OPENCLAW_WRAPPER" 2>/dev/null || true
ln -sf "$OPENCLAW_WRAPPER" /usr/local/bin/openclaw 2>/dev/null || true
fi
echo "ClawX has been installed successfully."

View File

@@ -4,8 +4,9 @@
set -e
# Remove symbolic link
# Remove symbolic links
rm -f /usr/local/bin/clawx 2>/dev/null || true
rm -f /usr/local/bin/openclaw 2>/dev/null || true
# Update desktop database
if command -v update-desktop-database &> /dev/null; then

View File

@@ -59,13 +59,9 @@ export function Settings() {
const [controlUiInfo, setControlUiInfo] = useState<ControlUiInfo | null>(null);
const [openclawCliCommand, setOpenclawCliCommand] = useState('');
const [openclawCliError, setOpenclawCliError] = useState<string | null>(null);
const [installingCli, setInstallingCli] = useState(false);
const isMac = window.electron.platform === 'darwin';
const isWindows = window.electron.platform === 'win32';
const isLinux = window.electron.platform === 'linux';
const isDev = window.electron.isDev;
const showCliTools = isMac || isWindows || isLinux;
const showCliTools = true;
const [showLogs, setShowLogs] = useState(false);
const [logContent, setLogContent] = useState('');
@@ -142,7 +138,7 @@ export function Settings() {
if (!showCliTools) return;
let cancelled = false;
const loadCliCommand = async () => {
(async () => {
try {
const result = await window.electron.ipcRenderer.invoke('openclaw:getCliCommand') as {
success: boolean;
@@ -162,12 +158,9 @@ export function Settings() {
setOpenclawCliCommand('');
setOpenclawCliError(String(error));
}
};
})();
loadCliCommand();
return () => {
cancelled = true;
};
return () => { cancelled = true; };
}, [devModeUnlocked, showCliTools]);
const handleCopyCliCommand = async () => {
@@ -180,39 +173,16 @@ export function Settings() {
}
};
const handleInstallCliCommand = async () => {
if (!isMac || installingCli) return;
try {
const confirmation = await window.electron.ipcRenderer.invoke('dialog:message', {
type: 'question',
title: t('developer.installTitle'),
message: t('developer.installMessage'),
detail: t('developer.installDetail'),
buttons: ['Cancel', 'Install'],
defaultId: 1,
cancelId: 0,
}) as { response: number };
if (confirmation.response !== 1) return;
setInstallingCli(true);
const result = await window.electron.ipcRenderer.invoke('openclaw:installCliMac') as {
success: boolean;
path?: string;
error?: string;
};
if (result.success) {
toast.success(`Installed command at ${result.path ?? '/usr/local/bin/openclaw'}`);
} else {
toast.error(result.error || 'Failed to install command');
}
} catch (error) {
toast.error(`Install failed: ${String(error)}`);
} finally {
setInstallingCli(false);
}
};
useEffect(() => {
const unsubscribe = window.electron.ipcRenderer.on(
'openclaw:cli-installed',
(...args: unknown[]) => {
const installedPath = typeof args[0] === 'string' ? args[0] : '';
toast.success(`openclaw CLI installed at ${installedPath}`);
},
);
return () => { unsubscribe?.(); };
}, []);
return (
<div className="space-y-6 p-6">
@@ -516,22 +486,6 @@ export function Settings() {
{t('common:actions.copy')}
</Button>
</div>
{isMac && !isDev && (
<div className="space-y-1">
<Button
type="button"
variant="outline"
onClick={handleInstallCliCommand}
disabled={installingCli}
>
<Terminal className="h-4 w-4 mr-2" />
{t('developer.installCmd')}
</Button>
<p className="text-xs text-muted-foreground">
{t('developer.installCmdDesc')}
</p>
</div>
)}
</div>
</>
)}