Skip to content

Commit 9282f85

Browse files
committed
Add Ruff formatter integration for Python code formatting support
- Implemented downloading and extraction of the Ruff binary in build scripts for Linux and macOS. - Enhanced pyright-bridge.ts to handle formatting requests using Ruff, with support for Unix platforms only. - Updated README.md to document the new formatting feature and its platform limitations. - Introduced formatter.ts for managing Ruff execution and formatting logic.
1 parent b1e150f commit 9282f85

File tree

6 files changed

+412
-7
lines changed

6 files changed

+412
-7
lines changed

.github/workflows/build-and-release.yml

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,61 @@ jobs:
6262
fi
6363
cd ../../..
6464
65+
# Download and extract Ruff binary
66+
RUFF_VERSION="0.14.3"
67+
RUFF_ARCHIVE=""
68+
RUFF_SUBDIR=""
69+
70+
case "${NODE_ARCH}" in
71+
"linux-x64")
72+
RUFF_ARCHIVE="ruff-x86_64-unknown-linux-gnu.tar.gz"
73+
RUFF_SUBDIR="ruff-x86_64-unknown-linux-gnu"
74+
;;
75+
"linux-arm64")
76+
RUFF_ARCHIVE="ruff-aarch64-unknown-linux-gnu.tar.gz"
77+
RUFF_SUBDIR="ruff-aarch64-unknown-linux-gnu"
78+
;;
79+
*)
80+
echo "⚠️ Warning: Unsupported platform for ruff, skipping..."
81+
RUFF_ARCHIVE=""
82+
;;
83+
esac
84+
85+
if [ -n "${RUFF_ARCHIVE}" ]; then
86+
RUFF_URL="https://github.com/astral-sh/ruff/releases/download/v${RUFF_VERSION}/${RUFF_ARCHIVE}"
87+
RUFF_FILE="/tmp/${RUFF_ARCHIVE}"
88+
RUFF_EXTRACT_DIR="/tmp/ruff-extract-${PLATFORM}"
89+
90+
if [ ! -f "${RUFF_FILE}" ]; then
91+
curl -L "${RUFF_URL}" -o "${RUFF_FILE}" || {
92+
echo "⚠️ Warning: Failed to download Ruff, formatting will not be available"
93+
RUFF_ARCHIVE=""
94+
}
95+
else
96+
echo "✓ Using cached Ruff"
97+
fi
98+
99+
if [ -n "${RUFF_ARCHIVE}" ]; then
100+
echo "📂 Extracting Ruff..."
101+
mkdir -p "${RUFF_EXTRACT_DIR}"
102+
tar -xzf "${RUFF_FILE}" -C "${RUFF_EXTRACT_DIR}"
103+
mkdir -p "output/${PLATFORM}/bin"
104+
105+
if [ -f "${RUFF_EXTRACT_DIR}/${RUFF_SUBDIR}/ruff" ]; then
106+
cp "${RUFF_EXTRACT_DIR}/${RUFF_SUBDIR}/ruff" "output/${PLATFORM}/bin/ruff"
107+
chmod +x "output/${PLATFORM}/bin/ruff"
108+
echo "✓ Ruff extracted to output/${PLATFORM}/bin/ruff"
109+
else
110+
echo "⚠️ Warning: Ruff binary not found in archive at ${RUFF_EXTRACT_DIR}/${RUFF_SUBDIR}/ruff"
111+
ls -la "${RUFF_EXTRACT_DIR}/" || true
112+
fi
113+
114+
rm -rf "${RUFF_EXTRACT_DIR}" 2>/dev/null || true
115+
fi
116+
else
117+
echo "⚠️ Skipping Ruff for unsupported platform"
118+
fi
119+
65120
# Install production dependencies
66121
cp package.json output/${PLATFORM}/
67122
cd output/${PLATFORM}
@@ -96,6 +151,7 @@ jobs:
96151
cat > output/${PLATFORM}/start.sh << 'EOF'
97152
#!/bin/bash
98153
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
154+
export PATH="${DIR}/bin:${PATH}"
99155
"${DIR}/node/bin/node" "${DIR}/bundle.js" "$@"
100156
EOF
101157
chmod +x output/${PLATFORM}/start.sh

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,15 @@ start.bat --port 9011 --bot-root C:\path\to\jesse-bot --jesse-root C:\path\to\je
8484

8585
The Pyright language server is configured via `pyrightconfig.json`. The bridge automatically deploys this configuration file to the bot root directory specified by `--bot-root` on startup. You can customize type checking behavior, Python version, include/exclude patterns, and more.
8686

87+
### Formatting Support
88+
89+
The bridge includes Python code formatting using Ruff (bundled binary). **Formatting is only available on Unix platforms (Linux, macOS)**, not Windows.
90+
91+
- **Unix (Linux/macOS)**: Formatting is automatically available via `textDocument/formatting` LSP requests
92+
- **Windows**: Formatting requests are not supported (will return "not supported")
93+
94+
Ruff formatting options can be configured via `pyproject.toml` or `ruff.toml` in the project root.
95+
8796
## Features
8897

8998
- ✅ Bundled Node.js runtime (no system dependencies)
@@ -92,6 +101,7 @@ The Pyright language server is configured via `pyrightconfig.json`. The bridge a
92101
- ✅ Production-ready dependencies only
93102
- ✅ WebSocket-based communication
94103
- ✅ Full Pyright LSP capabilities
104+
- ✅ Python code formatting with Ruff (Unix only: Linux, macOS)
95105

96106
## Architecture
97107

@@ -107,10 +117,12 @@ Messages are translated between WebSocket and the Language Server Protocol, enab
107117

108118
- `index.ts` - Entry point and CLI argument handling
109119
- `pyright-bridge.ts` - WebSocket bridge implementation
120+
- `formatter.ts` - Python code formatting with Ruff (Unix only)
110121
- `pyrightconfig.json` - Pyright configuration
111122
- `package.json` - Node.js project configuration
112123
- `build.sh` - Build script for Linux x64
113124
- `build-all.sh` - Build script for all platforms
125+
- `build-windows.sh` - Build script for Windows x64
114126
- `output/` - Build output directory (generated)
115127

116128
## License

build-all.sh

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,78 @@ build_platform() {
9999
fi
100100
echo "✓ Node.js extracted"
101101

102+
# Step 2.5: Download and extract Ruff binary
103+
echo "📥 Downloading Ruff formatter binary..."
104+
105+
RUFF_VERSION="0.14.3"
106+
local ruff_archive=""
107+
local ruff_subdir=""
108+
109+
case "${platform}" in
110+
"linux-x64")
111+
ruff_archive="ruff-x86_64-unknown-linux-gnu.tar.gz"
112+
ruff_subdir="ruff-x86_64-unknown-linux-gnu"
113+
;;
114+
"linux-arm64")
115+
ruff_archive="ruff-aarch64-unknown-linux-gnu.tar.gz"
116+
ruff_subdir="ruff-aarch64-unknown-linux-gnu"
117+
;;
118+
"darwin-x64")
119+
ruff_archive="ruff-x86_64-apple-darwin.tar.gz"
120+
ruff_subdir="ruff-x86_64-apple-darwin"
121+
;;
122+
"darwin-arm64")
123+
ruff_archive="ruff-aarch64-apple-darwin.tar.gz"
124+
ruff_subdir="ruff-aarch64-apple-darwin"
125+
;;
126+
"win32-x64")
127+
# Ruff formatter excluded from Windows builds (formatting only available on Unix)
128+
ruff_archive=""
129+
;;
130+
*)
131+
echo "⚠️ Warning: Unsupported platform for ruff, skipping..."
132+
ruff_archive=""
133+
;;
134+
esac
135+
136+
if [ -n "${ruff_archive}" ]; then
137+
local ruff_url="https://github.com/astral-sh/ruff/releases/download/v${RUFF_VERSION}/${ruff_archive}"
138+
local ruff_file="/tmp/${ruff_archive}"
139+
local ruff_extract_dir="/tmp/ruff-extract-${platform}"
140+
141+
if [ ! -f "${ruff_file}" ]; then
142+
curl -L "${ruff_url}" -o "${ruff_file}" || {
143+
echo "⚠️ Warning: Failed to download Ruff, formatting will not be available"
144+
ruff_archive=""
145+
}
146+
else
147+
echo "✓ Using cached Ruff"
148+
fi
149+
150+
if [ -n "${ruff_archive}" ]; then
151+
echo "📂 Extracting Ruff..."
152+
mkdir -p "output/${platform}/bin"
153+
154+
# Windows builds skip ruff (formatting only available on Unix)
155+
if [ "$os" != "win32" ]; then
156+
# Linux/macOS use .tar.gz
157+
mkdir -p "${ruff_extract_dir}"
158+
tar -xzf "${ruff_file}" -C "${ruff_extract_dir}"
159+
if [ -f "${ruff_extract_dir}/${ruff_subdir}/ruff" ]; then
160+
cp "${ruff_extract_dir}/${ruff_subdir}/ruff" "output/${platform}/bin/ruff"
161+
chmod +x "output/${platform}/bin/ruff"
162+
echo "✓ Ruff extracted to output/${platform}/bin/ruff"
163+
else
164+
echo "⚠️ Warning: Ruff binary not found in archive at ${ruff_extract_dir}/${ruff_subdir}/ruff"
165+
ls -la "${ruff_extract_dir}/" || true
166+
fi
167+
rm -rf "${ruff_extract_dir}" 2>/dev/null || true
168+
fi
169+
fi
170+
else
171+
echo "⚠️ Skipping Ruff for unsupported platform"
172+
fi
173+
102174
# Step 3: Strip unnecessary files from Node.js
103175
echo "🧹 Optimizing Node.js runtime..."
104176
cd "output/${platform}/node"
@@ -183,6 +255,7 @@ EOF
183255
# Usage: ./start.sh --port <PORT> --project-root <ROOT> --jesse-relative-path <JESSE> --bot-relative-path <BOT>
184256
185257
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
258+
export PATH="${DIR}/bin:${PATH}"
186259
"${DIR}/node/bin/node" "${DIR}/bundle.js" "$@"
187260
EOF
188261
chmod +x "output/${platform}/start.sh"
@@ -253,9 +326,12 @@ echo "📁 Each archive contains:"
253326
echo " - bundle.js (your bridge code)"
254327
echo " - node/ (optimized Node.js runtime)"
255328
echo " - node_modules/ (Pyright + pruned dependencies)"
329+
echo " - bin/ (ruff formatter binary - Unix only, not included in Windows builds)"
256330
echo " - pyrightconfig.json (config template)"
257331
echo " - start.sh or start.bat (startup script)"
258332
echo ""
333+
echo "ℹ️ Note: Formatting support is only available on Unix (Linux/macOS), not Windows"
334+
echo ""
259335
echo "🚀 To deploy (Linux/macOS):"
260336
echo " 1. Upload: scp output/linux-x64.tar.gz server:/opt/"
261337
echo " 2. Extract: tar -xzf linux-x64.tar.gz"

build.sh

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,75 @@ tar -xzf "${NODE_FILE}" -C /tmp/
6565
mv "/tmp/${NODE_PKG}" "output/${PLATFORM}/node"
6666
echo "✓ Node.js extracted to output/${PLATFORM}/node"
6767

68+
# Step 2.5: Download and extract Ruff binary
69+
echo ""
70+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
71+
echo "📥 Downloading Ruff formatter binary..."
72+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
73+
74+
RUFF_VERSION="0.14.3"
75+
RUFF_ARCHIVE=""
76+
RUFF_SUBDIR=""
77+
78+
case "${NODE_ARCH}" in
79+
"linux-x64")
80+
RUFF_ARCHIVE="ruff-x86_64-unknown-linux-gnu.tar.gz"
81+
RUFF_SUBDIR="ruff-x86_64-unknown-linux-gnu"
82+
;;
83+
"linux-arm64")
84+
RUFF_ARCHIVE="ruff-aarch64-unknown-linux-gnu.tar.gz"
85+
RUFF_SUBDIR="ruff-aarch64-unknown-linux-gnu"
86+
;;
87+
"darwin-x64")
88+
RUFF_ARCHIVE="ruff-x86_64-apple-darwin.tar.gz"
89+
RUFF_SUBDIR="ruff-x86_64-apple-darwin"
90+
;;
91+
"darwin-arm64")
92+
RUFF_ARCHIVE="ruff-aarch64-apple-darwin.tar.gz"
93+
RUFF_SUBDIR="ruff-aarch64-apple-darwin"
94+
;;
95+
*)
96+
echo "⚠️ Warning: Unsupported platform for ruff, skipping..."
97+
RUFF_ARCHIVE=""
98+
;;
99+
esac
100+
101+
if [ -n "${RUFF_ARCHIVE}" ]; then
102+
RUFF_URL="https://github.com/astral-sh/ruff/releases/download/v${RUFF_VERSION}/${RUFF_ARCHIVE}"
103+
RUFF_FILE="/tmp/${RUFF_ARCHIVE}"
104+
RUFF_EXTRACT_DIR="/tmp/ruff-extract-${PLATFORM}"
105+
106+
if [ ! -f "${RUFF_FILE}" ]; then
107+
curl -L "${RUFF_URL}" -o "${RUFF_FILE}" || {
108+
echo "⚠️ Warning: Failed to download Ruff, formatting will not be available"
109+
RUFF_ARCHIVE=""
110+
}
111+
else
112+
echo "✓ Using cached Ruff"
113+
fi
114+
115+
if [ -n "${RUFF_ARCHIVE}" ]; then
116+
echo "📂 Extracting Ruff..."
117+
mkdir -p "${RUFF_EXTRACT_DIR}"
118+
tar -xzf "${RUFF_FILE}" -C "${RUFF_EXTRACT_DIR}"
119+
mkdir -p "output/${PLATFORM}/bin"
120+
121+
if [ -f "${RUFF_EXTRACT_DIR}/${RUFF_SUBDIR}/ruff" ]; then
122+
cp "${RUFF_EXTRACT_DIR}/${RUFF_SUBDIR}/ruff" "output/${PLATFORM}/bin/ruff"
123+
chmod +x "output/${PLATFORM}/bin/ruff"
124+
echo "✓ Ruff extracted to output/${PLATFORM}/bin/ruff"
125+
else
126+
echo "⚠️ Warning: Ruff binary not found in archive at ${RUFF_EXTRACT_DIR}/${RUFF_SUBDIR}/ruff"
127+
ls -la "${RUFF_EXTRACT_DIR}/" || true
128+
fi
129+
130+
rm -rf "${RUFF_EXTRACT_DIR}" 2>/dev/null || true
131+
fi
132+
else
133+
echo "⚠️ Skipping Ruff for unsupported platform"
134+
fi
135+
echo ""
136+
68137
# Strip unnecessary files from Node.js to reduce size
69138
echo "🧹 Stripping unnecessary files from Node.js..."
70139
cd "output/${PLATFORM}/node"
@@ -143,6 +212,7 @@ cat > "output/${PLATFORM}/start.sh" << 'EOF'
143212
# Usage: ./start.sh --port <PORT> --project-root <ROOT> --jesse-relative-path <JESSE> --bot-relative-path <BOT>
144213
145214
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
215+
export PATH="${DIR}/bin:${PATH}"
146216
"${DIR}/node/bin/node" "${DIR}/bundle.js" "$@"
147217
EOF
148218
chmod +x "output/${PLATFORM}/start.sh"
@@ -179,6 +249,7 @@ echo "📁 Archive contains:"
179249
echo " - bundle.js (all your bridge code bundled)"
180250
echo " - node/ (Node.js ${NODE_VERSION} runtime, optimized)"
181251
echo " - node_modules/ (Pyright + pruned dependencies)"
252+
echo " - bin/ (ruff formatter binary)"
182253
echo " - pyrightconfig.json (config template)"
183254
echo " - start.sh (startup script)"
184255
echo ""

formatter.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { execSync } from 'child_process'
2+
import { readFileSync, writeFileSync, existsSync, chmodSync, unlinkSync } from 'fs'
3+
import path, { dirname, join } from 'path'
4+
import { fileURLToPath } from 'url'
5+
6+
const __filename = fileURLToPath(import.meta.url)
7+
const __dirname = dirname(__filename)
8+
9+
export function getRuffPath(): string {
10+
const ruffPath = join(__dirname, 'bin', 'ruff')
11+
if (process.platform === 'win32') {
12+
return ruffPath + '.exe'
13+
}
14+
return ruffPath
15+
}
16+
17+
export function isRuffAvailable(): boolean {
18+
try {
19+
const ruffPath = getRuffPath()
20+
if (existsSync(ruffPath)) {
21+
try {
22+
chmodSync(ruffPath, 0o755)
23+
} catch {
24+
}
25+
return true
26+
}
27+
} catch {
28+
try {
29+
execSync('ruff --version', { stdio: 'ignore' })
30+
return true
31+
} catch {
32+
return false
33+
}
34+
}
35+
return false
36+
}
37+
38+
export async function formatPythonCode(
39+
filePath: string,
40+
content: string,
41+
botRoot: string
42+
): Promise<any[]> {
43+
if (!isRuffAvailable()) {
44+
throw new Error('Ruff formatter not available')
45+
}
46+
47+
try {
48+
const ruffPath = getRuffPath()
49+
const useBundled = existsSync(ruffPath)
50+
const ruffCmd = useBundled ? ruffPath : 'ruff'
51+
52+
// Create temp file path - use path utilities for cross-platform compatibility
53+
const tempFile = path.join(path.dirname(filePath), path.basename(filePath) + '.ruff-format.tmp')
54+
writeFileSync(tempFile, content, 'utf-8')
55+
56+
try {
57+
// Execute ruff format command with proper quoting for cross-platform compatibility
58+
// Using array form for better cross-platform support (avoids shell interpretation issues)
59+
execSync(`"${ruffCmd}" format "${tempFile}"`, {
60+
cwd: botRoot,
61+
stdio: 'pipe',
62+
shell: 'true' // Use shell for proper path handling on all platforms
63+
})
64+
65+
const formatted = readFileSync(tempFile, 'utf-8')
66+
67+
const lines = content.split('\n')
68+
const fullRange = {
69+
start: { line: 0, character: 0 },
70+
end: {
71+
line: Math.max(0, lines.length - 1),
72+
character: lines.length > 0 ? lines[lines.length - 1].length : 0
73+
}
74+
}
75+
76+
return [{
77+
range: fullRange,
78+
newText: formatted
79+
}]
80+
} finally {
81+
if (existsSync(tempFile)) {
82+
try {
83+
unlinkSync(tempFile)
84+
} catch {
85+
}
86+
}
87+
}
88+
} catch (error: any) {
89+
console.error('Formatting error:', error)
90+
throw error
91+
}
92+
}
93+

0 commit comments

Comments
 (0)