Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand Down
28 changes: 26 additions & 2 deletions bin/fig-start
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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

Expand Down
6 changes: 5 additions & 1 deletion bin/setup-alias.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions src/figma-patch.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.');
}
Expand Down
8 changes: 7 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'));

Copilot AI Mar 29, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this Snap-specific failure branch, the output omits the Safe Mode fallback hint that other setup-failure branches include. Adding the same "connect --safe" suggestion here would keep guidance consistent and help users proceed even when Snap installs are unsupported.

Suggested change
console.log(chalk.gray(' Install the unofficial apt package instead if you want Linux support.\n'));
console.log(chalk.gray(' Install the unofficial apt package instead if you want Linux support.\n'));
console.log(chalk.gray(' Or use Safe Mode: ') + chalk.cyan('node src/index.js connect --safe\n'));

Copilot uses AI. Check for mistakes.
} 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'));
Expand Down Expand Up @@ -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)'));
Expand Down
68 changes: 64 additions & 4 deletions src/platform.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
}
}
Expand All @@ -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 {}
Expand Down Expand Up @@ -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;
}

Expand All @@ -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;
}
Expand All @@ -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}`;
}
Comment on lines +225 to +228

Copilot AI Mar 29, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On Linux, getFigmaCommand returns an unquoted binaryPath. If FIGMA_CLI_BINARY_PATH contains spaces (or other shell-sensitive chars), the printed manual command will be incorrect. Consider shell-quoting the path or returning an argv-style array from getFigmaCommand and formatting it for display safely at the call site.

Copilot uses AI. Check for mistakes.
default:
return null;
}
Expand All @@ -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 {
Comment on lines +240 to +245

Copilot AI Mar 29, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getFigmaVersion() builds a shell command with string interpolation: execSync(${binaryPath} --version ...). This breaks when binaryPath contains spaces and also needlessly invokes a shell. Use execFileSync(binaryPath, ['--version'], ...) (or spawnSync) to avoid quoting issues and shell interpretation.

Copilot uses AI. Check for mistakes.
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' });

Copilot AI Mar 29, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Linux isFigmaRunning() uses pgrep -f "/figma-linux|/figma ". This can fail when the process command line is just figma/figma-linux (no leading slash) or when the executable is running without arguments (no trailing space). Prefer pgrep -x figma-linux || pgrep -x figma or a regex that matches both path and bare names, e.g. (figma-linux|figma)( |$).

Suggested change
const ps = execSync('pgrep -f "/figma-linux|/figma " 2>/dev/null || true', { encoding: 'utf8' });
const ps = execSync('pgrep -f "(figma-linux|figma)( |$)" 2>/dev/null || true', { encoding: 'utf8' });

Copilot uses AI. Check for mistakes.
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');
Expand Down
Loading