diff --git a/electron-builder.yml b/electron-builder.yml index 0e3eda8b3..ac3c6f4bb 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -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 diff --git a/electron/main/index.ts b/electron/main/index.ts index e9d86e8c2..84575d35a 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -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 { 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 }) => { diff --git a/electron/main/ipc-handlers.ts b/electron/main/ipc-handlers.ts index 508fe5861..552e05a03 100644 --- a/electron/main/ipc-handlers.ts +++ b/electron/main/ipc-handlers.ts @@ -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 ==================== diff --git a/electron/preload/index.ts b/electron/preload/index.ts index f75f3d01d..98724a55b 100644 --- a/electron/preload/index.ts +++ b/electron/preload/index.ts @@ -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)) { diff --git a/electron/utils/openclaw-cli.ts b/electron/utils/openclaw-cli.ts index 31a00156e..9305e1b4f 100644 --- a/electron/utils/openclaw-cli.ts +++ b/electron/utils/openclaw-cli.ts @@ -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 { + 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); + }); +} diff --git a/resources/cli/posix/openclaw b/resources/cli/posix/openclaw new file mode 100755 index 000000000..555ea5d27 --- /dev/null +++ b/resources/cli/posix/openclaw @@ -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/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" "$@" diff --git a/resources/cli/win32/openclaw b/resources/cli/win32/openclaw new file mode 100644 index 000000000..c53031869 --- /dev/null +++ b/resources/cli/win32/openclaw @@ -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" "$@" diff --git a/resources/cli/win32/openclaw.cmd b/resources/cli/win32/openclaw.cmd new file mode 100644 index 000000000..3c9916502 --- /dev/null +++ b/resources/cli/win32/openclaw.cmd @@ -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 diff --git a/scripts/installer.nsh b/scripts/installer.nsh index 64280f397..39a7faaa2 100644 --- a/scripts/installer.nsh +++ b/scripts/installer.nsh @@ -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 \ No newline at end of file +!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 diff --git a/scripts/linux/after-install.sh b/scripts/linux/after-install.sh index f8e2e1a15..53447be71 100644 --- a/scripts/linux/after-install.sh +++ b/scripts/linux/after-install.sh @@ -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." diff --git a/scripts/linux/after-remove.sh b/scripts/linux/after-remove.sh index a34b71674..1eade9ebd 100644 --- a/scripts/linux/after-remove.sh +++ b/scripts/linux/after-remove.sh @@ -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 diff --git a/src/pages/Settings/index.tsx b/src/pages/Settings/index.tsx index 10cd56750..1901744d3 100644 --- a/src/pages/Settings/index.tsx +++ b/src/pages/Settings/index.tsx @@ -59,13 +59,9 @@ export function Settings() { const [controlUiInfo, setControlUiInfo] = useState(null); const [openclawCliCommand, setOpenclawCliCommand] = useState(''); const [openclawCliError, setOpenclawCliError] = useState(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 (
@@ -516,22 +486,6 @@ export function Settings() { {t('common:actions.copy')}
- {isMac && !isDev && ( -
- -

- {t('developer.installCmdDesc')} -

-
- )} )}