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:
@@ -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
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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 ====================
|
||||
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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
48
resources/cli/posix/openclaw
Executable 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" "$@"
|
||||
17
resources/cli/win32/openclaw
Normal file
17
resources/cli/win32/openclaw
Normal 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" "$@"
|
||||
16
resources/cli/win32/openclaw.cmd
Normal file
16
resources/cli/win32/openclaw.cmd
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user