Skip to content

Commit 53d2644

Browse files
committed
feat(http-server): add HTTP server package with binary wrapper and README
1 parent 07137d1 commit 53d2644

File tree

5 files changed

+284
-22
lines changed

5 files changed

+284
-22
lines changed

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,7 @@ packages/desktop/src-tauri/binaries/ui-server-*
2929
rust/target/
3030

3131
# IDE specific files
32-
.idea/
32+
.idea/
33+
34+
# Binaries
35+
packages/**/binaries

packages/http-server/README.md

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# @leanspec/http-server
2+
3+
High-performance Rust HTTP server for LeanSpec UI.
4+
5+
## Features
6+
7+
- **Fast**: Built with Rust and Axum web framework
8+
- **Lightweight**: <30MB bundle size
9+
- **Multi-project**: Support for multiple project workspaces
10+
- **RESTful API**: JSON API for all spec operations
11+
- **CORS-enabled**: Configurable cross-origin resource sharing
12+
13+
## Installation
14+
15+
```bash
16+
npm install @leanspec/http-server
17+
```
18+
19+
## Usage
20+
21+
### As a standalone server
22+
23+
```bash
24+
npx leanspec-http
25+
```
26+
27+
Options:
28+
- `--host <host>` - Server host (default: 127.0.0.1)
29+
- `--port <port>` - Server port (default: 3333)
30+
- `--help` - Show help message
31+
32+
### As a library
33+
34+
```javascript
35+
import { spawn } from 'child_process';
36+
37+
const server = spawn('leanspec-http', ['--port', '3333']);
38+
```
39+
40+
## Configuration
41+
42+
The server reads configuration from `~/.lean-spec/config.json`:
43+
44+
```json
45+
{
46+
"server": {
47+
"host": "127.0.0.1",
48+
"port": 3333,
49+
"cors": {
50+
"enabled": true,
51+
"origins": [
52+
"http://localhost:5173",
53+
"http://localhost:3000"
54+
]
55+
}
56+
}
57+
}
58+
```
59+
60+
## API Endpoints
61+
62+
### Projects
63+
- `GET /api/projects` - List all projects
64+
- `POST /api/projects` - Add new project
65+
- `GET /api/projects/:id` - Get project details
66+
- `PATCH /api/projects/:id` - Update project
67+
- `DELETE /api/projects/:id` - Remove project
68+
- `POST /api/projects/:id/switch` - Switch to project
69+
70+
### Specs
71+
- `GET /api/specs` - List specs (with filters)
72+
- `GET /api/specs/:spec` - Get spec detail
73+
- `POST /api/search` - Search specs
74+
- `GET /api/stats` - Project statistics
75+
- `GET /api/deps/:spec` - Dependency graph
76+
- `GET /api/validate` - Validate all specs
77+
78+
### Health
79+
- `GET /health` - Health check
80+
81+
## Platform Support
82+
83+
- macOS (x64, arm64)
84+
- Linux (x64, arm64)
85+
- Windows (x64)
86+
87+
## License
88+
89+
MIT
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
#!/usr/bin/env node
2+
/**
3+
* LeanSpec HTTP Server Binary Wrapper
4+
*
5+
* This script detects the current platform and architecture,
6+
* then spawns the appropriate Rust HTTP server binary.
7+
*
8+
* The wrapper looks for binaries in the following locations:
9+
* 1. Platform-specific npm package (@leanspec/http-darwin-x64, etc.)
10+
* 2. Local binaries directory (for development)
11+
* 3. Rust target directory (for local development)
12+
*/
13+
14+
import { spawn } from 'child_process';
15+
import { createRequire } from 'module';
16+
import { fileURLToPath } from 'url';
17+
import { dirname, join } from 'path';
18+
import { accessSync } from 'fs';
19+
20+
const require = createRequire(import.meta.url);
21+
const __filename = fileURLToPath(import.meta.url);
22+
const __dirname = dirname(__filename);
23+
24+
// Debug mode - enable with LEANSPEC_DEBUG=1
25+
const DEBUG = process.env.LEANSPEC_DEBUG === '1';
26+
const debug = (...args) => DEBUG && console.error('[leanspec-http debug]', ...args);
27+
28+
// Platform detection mapping
29+
const PLATFORM_MAP = {
30+
darwin: { x64: 'darwin-x64', arm64: 'darwin-arm64' },
31+
linux: { x64: 'linux-x64', arm64: 'linux-arm64' },
32+
win32: { x64: 'windows-x64', arm64: 'windows-arm64' }
33+
};
34+
35+
function getBinaryPath() {
36+
const platform = process.platform;
37+
const arch = process.arch;
38+
39+
debug('Platform detection:', { platform, arch });
40+
41+
const platformKey = PLATFORM_MAP[platform]?.[arch];
42+
if (!platformKey) {
43+
console.error(`Unsupported platform: ${platform}-${arch}`);
44+
console.error('Supported: macOS (x64/arm64), Linux (x64/arm64), Windows (x64)');
45+
process.exit(1);
46+
}
47+
48+
const isWindows = platform === 'win32';
49+
const binaryName = isWindows ? 'leanspec-http.exe' : 'leanspec-http';
50+
const packageName = `@leanspec/http-${platformKey}`;
51+
52+
debug('Binary info:', { platformKey, binaryName, packageName });
53+
54+
// Try to resolve platform package
55+
try {
56+
const resolvedPath = require.resolve(`${packageName}/${binaryName}`);
57+
debug('Found platform package binary:', resolvedPath);
58+
return resolvedPath;
59+
} catch (e) {
60+
debug('Platform package not found:', packageName, '-', e.message);
61+
}
62+
63+
// Try local binaries directory (for development/testing)
64+
try {
65+
const localPath = join(__dirname, '..', 'binaries', platformKey, binaryName);
66+
debug('Trying local binary:', localPath);
67+
accessSync(localPath);
68+
debug('Found local binary:', localPath);
69+
return localPath;
70+
} catch (e) {
71+
debug('Local binary not found:', e.message);
72+
}
73+
74+
// Try rust/target/release directory (for local development)
75+
try {
76+
const rustTargetPath = join(__dirname, '..', '..', '..', 'rust', 'target', 'release', binaryName);
77+
debug('Trying rust target binary:', rustTargetPath);
78+
accessSync(rustTargetPath);
79+
debug('Found rust target binary:', rustTargetPath);
80+
return rustTargetPath;
81+
} catch (e) {
82+
debug('Rust target binary not found:', e.message);
83+
}
84+
85+
console.error(`Binary not found for ${platform}-${arch}`);
86+
console.error(`Expected package: ${packageName}`);
87+
console.error('');
88+
console.error('To install:');
89+
console.error(' npm install @leanspec/http-server');
90+
console.error('');
91+
process.exit(1);
92+
}
93+
94+
// Execute binary
95+
const binaryPath = getBinaryPath();
96+
const args = process.argv.slice(2);
97+
98+
debug('Spawning binary:', binaryPath);
99+
debug('Arguments:', args);
100+
101+
const child = spawn(binaryPath, args, {
102+
stdio: 'inherit',
103+
windowsHide: true,
104+
});
105+
106+
child.on('exit', (code) => {
107+
debug('Binary exited with code:', code);
108+
process.exit(code ?? 1);
109+
});
110+
111+
child.on('error', (err) => {
112+
console.error('Failed to start leanspec-http:', err.message);
113+
debug('Spawn error:', err);
114+
process.exit(1);
115+
});

packages/http-server/package.json

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
{
2+
"name": "@leanspec/http-server",
3+
"version": "0.2.10",
4+
"description": "Rust HTTP server for LeanSpec UI",
5+
"type": "module",
6+
"bin": {
7+
"leanspec-http": "./bin/leanspec-http.js"
8+
},
9+
"scripts": {
10+
"typecheck": "echo 'No TypeScript source to check'"
11+
},
12+
"keywords": [
13+
"leanspec",
14+
"http",
15+
"server",
16+
"api",
17+
"rust"
18+
],
19+
"author": "Marvin Zhang",
20+
"license": "MIT",
21+
"repository": {
22+
"type": "git",
23+
"url": "https://github.com/codervisor/lean-spec.git",
24+
"directory": "packages/http-server"
25+
},
26+
"homepage": "https://lean-spec.dev",
27+
"bugs": {
28+
"url": "https://github.com/codervisor/lean-spec/issues"
29+
},
30+
"files": [
31+
"bin/",
32+
"binaries/",
33+
"README.md",
34+
"LICENSE"
35+
],
36+
"optionalDependencies": {
37+
"@leanspec/http-darwin-arm64": "0.2.10",
38+
"@leanspec/http-darwin-x64": "0.2.10",
39+
"@leanspec/http-linux-arm64": "0.2.10",
40+
"@leanspec/http-linux-x64": "0.2.10",
41+
"@leanspec/http-windows-x64": "0.2.10"
42+
},
43+
"engines": {
44+
"node": ">=20"
45+
}
46+
}

scripts/copy-rust-binaries.mjs

Lines changed: 30 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,11 @@ function getCurrentPlatform() {
3838
const platform = process.platform;
3939
const arch = process.arch;
4040
const platformKey = PLATFORM_MAP[platform]?.[arch];
41-
41+
4242
if (!platformKey) {
4343
throw new Error(`Unsupported platform: ${platform}-${arch}`);
4444
}
45-
45+
4646
return platformKey;
4747
}
4848

@@ -51,20 +51,20 @@ async function killProcessesUsingBinary(binaryPath) {
5151
// Windows: use handle.exe or just try to copy
5252
return;
5353
}
54-
54+
5555
try {
5656
// Use lsof to find processes using the binary
5757
const { execSync } = await import('node:child_process');
5858
const output = execSync(`lsof "${binaryPath}" 2>/dev/null || true`, { encoding: 'utf-8' });
59-
59+
6060
if (!output.trim()) {
6161
return; // No processes using the file
6262
}
63-
63+
6464
// Extract PIDs (skip header line)
6565
const lines = output.trim().split('\n').slice(1);
6666
const pids = [...new Set(lines.map(line => line.split(/\s+/)[1]).filter(Boolean))];
67-
67+
6868
if (pids.length > 0) {
6969
console.log(`⚠️ Found ${pids.length} process(es) using ${path.basename(binaryPath)}, stopping them...`);
7070
for (const pid of pids) {
@@ -86,54 +86,63 @@ async function copyBinary(binaryName, platformKey) {
8686
const isWindows = platformKey.startsWith('windows-');
8787
const sourceExt = isWindows ? '.exe' : '';
8888
const sourcePath = path.join(ROOT, 'rust', 'target', 'release', `${binaryName}${sourceExt}`);
89-
89+
9090
// Determine destination based on binary name
91-
const packagePath = binaryName === 'lean-spec' ? 'cli' : 'mcp';
91+
let packagePath;
92+
if (binaryName === 'lean-spec') {
93+
packagePath = 'cli';
94+
} else if (binaryName === 'leanspec-mcp') {
95+
packagePath = 'mcp';
96+
} else if (binaryName === 'leanspec-http') {
97+
packagePath = 'http-server';
98+
} else {
99+
throw new Error(`Unknown binary: ${binaryName}`);
100+
}
92101
const destDir = path.join(ROOT, 'packages', packagePath, 'binaries', platformKey);
93102
const destPath = path.join(destDir, binaryName + sourceExt);
94-
103+
95104
// Check if source exists
96105
try {
97106
await fs.access(sourcePath);
98107
} catch (e) {
99108
console.warn(`⚠️ Source binary not found: ${sourcePath}`);
100109
return false;
101110
}
102-
111+
103112
// Ensure destination directory exists
104113
await fs.mkdir(destDir, { recursive: true });
105-
114+
106115
// Kill any processes using the destination binary
107116
try {
108117
await fs.access(destPath);
109118
await killProcessesUsingBinary(destPath);
110119
} catch (e) {
111120
// Destination doesn't exist yet, that's fine
112121
}
113-
122+
114123
// Copy binary
115124
await fs.copyFile(sourcePath, destPath);
116-
125+
117126
// Make executable on Unix
118127
if (!isWindows) {
119128
await fs.chmod(destPath, 0o755);
120129
}
121-
130+
122131
console.log(`✅ Copied ${binaryName} to ${packagePath}/binaries/${platformKey}/`);
123132
return true;
124133
}
125134

126135
async function main() {
127136
const args = process.argv.slice(2);
128137
const copyAll = args.includes('--all');
129-
138+
130139
console.log('🔧 Copying Rust binaries...\n');
131-
132-
const binaries = ['lean-spec', 'leanspec-mcp'];
133-
140+
141+
const binaries = ['lean-spec', 'leanspec-mcp', 'leanspec-http'];
142+
134143
if (copyAll) {
135144
console.log('📦 Copying all platforms (requires cross-compiled binaries)\n');
136-
145+
137146
for (const platformKey of ALL_PLATFORMS) {
138147
console.log(`\nPlatform: ${platformKey}`);
139148
for (const binary of binaries) {
@@ -143,12 +152,12 @@ async function main() {
143152
} else {
144153
const currentPlatform = getCurrentPlatform();
145154
console.log(`📦 Copying for current platform: ${currentPlatform}\n`);
146-
155+
147156
for (const binary of binaries) {
148157
await copyBinary(binary, currentPlatform);
149158
}
150159
}
151-
160+
152161
console.log('\n✨ Done!');
153162
}
154163

0 commit comments

Comments
 (0)