diff --git a/README.md b/README.md index 389d146e..829dba79 100644 --- a/README.md +++ b/README.md @@ -145,9 +145,12 @@ This project includes a `CLAUDE.md` file that Claude reads automatically. It con - **Node.js 18+** — `brew install node` (or [download](https://nodejs.org/)) - **Figma Desktop** (free account works) - **Claude Code** ([get it here](https://www.anthropic.com/claude-code)) -- **macOS or Windows** (macOS recommended, Windows supported) +- **macOS or Windows** +- **Linux:** unofficial `figma-linux` package installed via `apt` only - **macOS Full Disk Access** for Terminal (Yolo Mode only -- not needed for [Safe Mode](#-safe-mode--for-restricted-environments)) +**Linux note:** Linux support is limited to the unofficial `figma-linux` desktop package installed outside Snap. Snap installs are not supported. + --- ## Setup @@ -157,7 +160,7 @@ git clone https://github.com/silships/figma-cli.git cd figma-cli npm install npm run setup-alias -source ~/.zshrc +source ~/.zshrc # or ~/.bashrc ``` That's it. Now open a **new terminal** and type: diff --git a/bin/fig-start b/bin/fig-start index ebf871b9..89e32e0d 100755 --- a/bin/fig-start +++ b/bin/fig-start @@ -19,6 +19,26 @@ BOLD='\033[1m' DIM='\033[2m' NC='\033[0m' +start_figma() { + case "$(uname -s)" in + Darwin) + open -a Figma + ;; + Linux) + if command -v figma-linux >/dev/null 2>&1; then + nohup figma-linux >/dev/null 2>&1 & + elif command -v figma >/dev/null 2>&1; then + nohup figma >/dev/null 2>&1 & + else + return 1 + fi + ;; + *) + return 1 + ;; + esac +} + # ── Config helpers ── get_daemon_token() { @@ -168,9 +188,13 @@ fi echo "" # Step 1: Check if Figma is running, start if not -if ! pgrep -x "Figma" > /dev/null 2>&1; then +if ! pgrep -f "/figma-linux|/figma |/Figma" > /dev/null 2>&1; then echo -e " ${YELLOW}Starting Figma...${NC}" - open -a Figma + if ! start_figma; then + echo -e " ${YELLOW}Could not start Figma automatically.${NC}" + echo -e " Start it manually, then rerun ${BOLD}fig-start${NC}." + exit 1 + fi sleep 3 fi diff --git a/bin/setup-alias.sh b/bin/setup-alias.sh index 80a34db5..05124d2e 100755 --- a/bin/setup-alias.sh +++ b/bin/setup-alias.sh @@ -20,7 +20,11 @@ fi # Check if alias already exists if grep -q "alias fig-start=" "$RC_FILE" 2>/dev/null; then # Update existing alias (path may have changed) - sed -i '' "/alias fig-start=/d" "$RC_FILE" + if sed --version >/dev/null 2>&1; then + sed -i "/alias fig-start=/d" "$RC_FILE" + else + sed -i '' "/alias fig-start=/d" "$RC_FILE" + fi fi # Add alias diff --git a/src/figma-patch.js b/src/figma-patch.js index 6e501818..19509a69 100644 --- a/src/figma-patch.js +++ b/src/figma-patch.js @@ -86,10 +86,18 @@ export function patchFigma() { throw new Error('Cannot detect Figma installation path for this platform'); } + if (process.platform === 'linux' && asarPath.startsWith('/snap/')) { + throw new Error( + 'Snap-installed figma-linux is not supported. Install figma-linux outside Snap to use this tool on Linux.' + ); + } + // Check write access first if (!canPatchFigma()) { if (process.platform === 'darwin') { throw new Error('No write access to Figma. Grant Terminal "Full Disk Access" in System Settings → Privacy & Security'); + } else if (process.platform === 'linux') { + throw new Error('No write access to the Figma installation. On Linux, use an apt-installed figma-linux package and run with permissions that can modify app.asar.'); } else { throw new Error('No write access to Figma. Try running as administrator.'); } diff --git a/src/index.js b/src/index.js index 33467aa4..ae6dfcf9 100755 --- a/src/index.js +++ b/src/index.js @@ -1110,6 +1110,9 @@ program console.log(chalk.cyan(' Step 5: ') + chalk.white('Reopen Terminal and try again\n')); console.log(chalk.gray(' Or use Safe Mode: ') + chalk.cyan('node src/index.js connect --safe\n')); + } else if (process.platform === 'linux' && err.message.includes('Snap')) { + console.log(chalk.yellow('\n Snap-installed figma-linux is not supported.\n')); + console.log(chalk.gray(' Install the unofficial apt package instead if you want Linux support.\n')); } else { console.log(chalk.yellow('\n Try running as administrator.\n')); console.log(chalk.gray(' Or use Safe Mode: ') + chalk.cyan('node src/index.js connect --safe\n')); @@ -5174,7 +5177,10 @@ program // 7. figma-use availability try { - execSync('which figma-use 2>/dev/null || where figma-use 2>nul', { encoding: 'utf8' }); + const figmaUseCheck = process.platform === 'win32' + ? 'where figma-use 2>nul' + : 'which figma-use 2>/dev/null'; + execSync(figmaUseCheck, { encoding: 'utf8' }); console.log(chalk.green('✓ figma-use installed')); } catch { console.log(chalk.yellow('○ figma-use not in PATH (some features limited)')); diff --git a/src/platform.js b/src/platform.js index ffe35a0b..015dcd15 100644 --- a/src/platform.js +++ b/src/platform.js @@ -38,6 +38,23 @@ function killPortWindows(port) { export const killPort = PLATFORM === 'win32' ? killPortWindows : killPortUnix; +function firstExistingPath(paths) { + for (const path of paths) { + if (path && existsSync(path)) return path; + } + return null; +} + +function commandExists(cmd) { + try { + const checkCmd = PLATFORM === 'win32' ? `where ${cmd}` : `command -v ${cmd}`; + const resolved = execSync(checkCmd, { encoding: 'utf8', stdio: 'pipe' }).trim().split('\n')[0]; + return resolved || null; + } catch { + return null; + } +} + // --- Get PID listening on port --- function getPortPidUnix(port) { return execSync(`lsof -ti:${port} 2>/dev/null || true`, { encoding: 'utf8', stdio: 'pipe' }).trim() || null; @@ -69,6 +86,7 @@ export function startFigmaApp(figmaPath, port) { if (PLATFORM === 'darwin') { execSync(`open -a Figma --args --remote-debugging-port=${port}`, { stdio: 'pipe' }); } else { + if (!figmaPath) throw new Error('Figma binary not found'); spawn(figmaPath, [`--remote-debugging-port=${port}`], { detached: true, stdio: 'ignore' }).unref(); } } @@ -81,6 +99,8 @@ export function killFigmaApp() { } else if (PLATFORM === 'win32') { execSync('taskkill /IM Figma.exe /F 2>nul', { stdio: 'pipe' }); } else { + execSync('pkill -f "/figma-linux" 2>/dev/null || true', { stdio: 'pipe' }); + execSync('pkill -x figma-linux 2>/dev/null || true', { stdio: 'pipe' }); execSync('pkill -x figma 2>/dev/null || true', { stdio: 'pipe' }); } } catch {} @@ -148,8 +168,35 @@ const ASAR_PATHS = { linux: '/opt/figma/resources/app.asar' }; +function getLinuxAsarPath() { + if (process.env.FIGMA_CLI_ASAR_PATH) return process.env.FIGMA_CLI_ASAR_PATH; + + return firstExistingPath([ + '/snap/figma-linux/current/resources/app.asar', + '/opt/figma/resources/app.asar', + '/opt/figma-linux/resources/app.asar', + '/usr/lib/figma/resources/app.asar', + '/usr/lib/figma-linux/resources/app.asar' + ]); +} + +function getLinuxBinaryPath() { + if (process.env.FIGMA_CLI_BINARY_PATH) return process.env.FIGMA_CLI_BINARY_PATH; + + return firstExistingPath([ + '/snap/bin/figma-linux', + '/usr/bin/figma-linux', + '/usr/local/bin/figma-linux', + '/usr/bin/figma', + '/usr/local/bin/figma', + commandExists('figma-linux'), + commandExists('figma') + ]); +} + export function getAsarPath() { if (PLATFORM === 'win32') return findWindowsFigmaPath(); + if (PLATFORM === 'linux') return getLinuxAsarPath(); return ASAR_PATHS[PLATFORM] || null; } @@ -160,7 +207,7 @@ export function getFigmaBinaryPath() { case 'win32': return findWindowsFigmaExe() || `${process.env.LOCALAPPDATA}\\Figma\\Figma.exe`; case 'linux': - return '/usr/bin/figma'; + return getLinuxBinaryPath(); default: return null; } @@ -175,8 +222,10 @@ export function getFigmaCommand(port = 9222) { if (exePath) return `"${exePath}" --remote-debugging-port=${port}`; return `"%LOCALAPPDATA%\\Figma\\Figma.exe" --remote-debugging-port=${port}`; } - case 'linux': - return `figma --remote-debugging-port=${port}`; + case 'linux': { + const binaryPath = getLinuxBinaryPath(); + return binaryPath ? `${binaryPath} --remote-debugging-port=${port}` : `figma-linux --remote-debugging-port=${port}`; + } default: return null; } @@ -188,14 +237,25 @@ export function getFigmaVersion() { return execSync('defaults read /Applications/Figma.app/Contents/Info.plist CFBundleShortVersionString 2>/dev/null', { encoding: 'utf8' }).trim(); } else if (PLATFORM === 'win32') { return execSync('powershell -command "(Get-Item \\"$env:LOCALAPPDATA\\Figma\\Figma.exe\\").VersionInfo.ProductVersion" 2>nul', { encoding: 'utf8' }).trim() || 'unknown'; + } else if (PLATFORM === 'linux') { + const binaryPath = getLinuxBinaryPath(); + if (!binaryPath) return 'unknown'; + try { + return execSync(`${binaryPath} --version 2>/dev/null`, { encoding: 'utf8', stdio: 'pipe', timeout: 2000 }).trim() || 'unknown'; + } catch { + return 'unknown'; + } } return 'unknown'; } export function isFigmaRunning() { - if (PLATFORM === 'darwin' || PLATFORM === 'linux') { + if (PLATFORM === 'darwin') { const ps = execSync('pgrep -f Figma 2>/dev/null || true', { encoding: 'utf8' }); return ps.trim().length > 0; + } else if (PLATFORM === 'linux') { + const ps = execSync('pgrep -f "/figma-linux|/figma " 2>/dev/null || true', { encoding: 'utf8' }); + return ps.trim().length > 0; } else if (PLATFORM === 'win32') { const ps = execSync('tasklist /FI "IMAGENAME eq Figma.exe" 2>nul', { encoding: 'utf8' }); return ps.includes('Figma.exe');