@tomagranate/corsa 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +226 -0
- package/bin/corsa +62 -0
- package/bin/corsa-linux-x64 +0 -0
- package/bin/toolui +62 -0
- package/package.json +61 -0
- package/scripts/build-all.ts +81 -0
- package/scripts/postinstall.cjs +333 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 corsa contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
# Corsa
|
|
2
|
+
|
|
3
|
+
A Terminal User Interface (TUI) for managing multiple local development processes. View real-time logs, monitor status, and control all your dev servers from a single dashboard.
|
|
4
|
+
|
|
5
|
+
Built with [OpenTUI](https://github.com/anomalyco/opentui).
|
|
6
|
+
|
|
7
|
+
## Why Corsa?
|
|
8
|
+
|
|
9
|
+
When working on a full-stack project, you often need to run multiple processes simultaneously—a frontend dev server, a backend API, database containers, workers, etc. Corsa gives you:
|
|
10
|
+
|
|
11
|
+
- **Single dashboard** for all your processes with tabbed log viewing
|
|
12
|
+
- **Real-time logs** with search and ANSI color support
|
|
13
|
+
- **Status monitoring** to see at a glance what's running, stopped, or crashed
|
|
14
|
+
- **Health checks** to monitor service availability
|
|
15
|
+
- **AI integration** via MCP to let your IDE assistant read logs and control processes
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
### Homebrew (macOS and Linux)
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
brew install tomagranate/tap/corsa
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### NPM
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm install -g @tomagranate/corsa
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### curl (macOS and Linux)
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
curl -fsSL https://raw.githubusercontent.com/tomagranate/corsa/main/install.sh | bash
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Manual Download
|
|
38
|
+
|
|
39
|
+
Download the latest binary for your platform from [Releases](https://github.com/tomagranate/corsa/releases).
|
|
40
|
+
|
|
41
|
+
| Platform | Download |
|
|
42
|
+
|----------|----------|
|
|
43
|
+
| macOS (Apple Silicon) | `corsa-darwin-arm64.tar.gz` |
|
|
44
|
+
| macOS (Intel) | `corsa-darwin-x64.tar.gz` |
|
|
45
|
+
| Linux (x64) | `corsa-linux-x64.tar.gz` |
|
|
46
|
+
| Linux (ARM64) | `corsa-linux-arm64.tar.gz` |
|
|
47
|
+
| Windows (x64) | `corsa-windows-x64.zip` |
|
|
48
|
+
|
|
49
|
+
## Quick Start
|
|
50
|
+
|
|
51
|
+
1. Create a config file in your project:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
corsa init
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
2. Edit `corsa.config.toml` to add your processes:
|
|
58
|
+
|
|
59
|
+
```toml
|
|
60
|
+
[[tools]]
|
|
61
|
+
name = "frontend"
|
|
62
|
+
command = "npm"
|
|
63
|
+
args = ["run", "dev"]
|
|
64
|
+
cwd = "./frontend"
|
|
65
|
+
|
|
66
|
+
[[tools]]
|
|
67
|
+
name = "backend"
|
|
68
|
+
command = "python"
|
|
69
|
+
args = ["-m", "uvicorn", "main:app", "--reload"]
|
|
70
|
+
cwd = "./backend"
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
3. Start the dashboard:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
corsa
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## CLI Reference
|
|
80
|
+
|
|
81
|
+
### Commands
|
|
82
|
+
|
|
83
|
+
| Command | Description |
|
|
84
|
+
|---------|-------------|
|
|
85
|
+
| `corsa` | Start the TUI dashboard |
|
|
86
|
+
| `corsa init` | Create a sample config file in the current directory |
|
|
87
|
+
| `corsa mcp` | Start the MCP server for AI agent integration |
|
|
88
|
+
|
|
89
|
+
### Options
|
|
90
|
+
|
|
91
|
+
| Option | Description |
|
|
92
|
+
|--------|-------------|
|
|
93
|
+
| `-c, --config <path>` | Path to config file (default: `corsa.config.toml`) |
|
|
94
|
+
| `-h, --help` | Show help message |
|
|
95
|
+
|
|
96
|
+
### Examples
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
# Start with default config
|
|
100
|
+
corsa
|
|
101
|
+
|
|
102
|
+
# Use a custom config file
|
|
103
|
+
corsa --config ./configs/dev.toml
|
|
104
|
+
corsa -c ./configs/dev.toml
|
|
105
|
+
|
|
106
|
+
# Create a new config file
|
|
107
|
+
corsa init
|
|
108
|
+
|
|
109
|
+
# Start MCP server for AI integration
|
|
110
|
+
corsa mcp
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Configuration
|
|
114
|
+
|
|
115
|
+
Corsa is configured via a TOML file. By default, it looks for `corsa.config.toml` in the current directory.
|
|
116
|
+
|
|
117
|
+
### Minimal Example
|
|
118
|
+
|
|
119
|
+
```toml
|
|
120
|
+
[[tools]]
|
|
121
|
+
name = "server"
|
|
122
|
+
command = "npm"
|
|
123
|
+
args = ["run", "dev"]
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Full Example
|
|
127
|
+
|
|
128
|
+
```toml
|
|
129
|
+
[home]
|
|
130
|
+
enabled = true
|
|
131
|
+
title = "My Project"
|
|
132
|
+
|
|
133
|
+
[ui]
|
|
134
|
+
theme = "mist"
|
|
135
|
+
showTabNumbers = true
|
|
136
|
+
|
|
137
|
+
[mcp]
|
|
138
|
+
enabled = true
|
|
139
|
+
|
|
140
|
+
[[tools]]
|
|
141
|
+
name = "web"
|
|
142
|
+
command = "npm"
|
|
143
|
+
args = ["run", "dev"]
|
|
144
|
+
cwd = "./web"
|
|
145
|
+
description = "Next.js frontend"
|
|
146
|
+
|
|
147
|
+
[tools.ui]
|
|
148
|
+
label = "Open App"
|
|
149
|
+
url = "http://localhost:3000"
|
|
150
|
+
|
|
151
|
+
[tools.healthCheck]
|
|
152
|
+
url = "http://localhost:3000/api/health"
|
|
153
|
+
interval = 5000
|
|
154
|
+
|
|
155
|
+
[[tools]]
|
|
156
|
+
name = "api"
|
|
157
|
+
command = "cargo"
|
|
158
|
+
args = ["watch", "-x", "run"]
|
|
159
|
+
cwd = "./api"
|
|
160
|
+
description = "Rust API server"
|
|
161
|
+
cleanup = ["pkill -f 'target/debug/api'"]
|
|
162
|
+
|
|
163
|
+
[tools.env]
|
|
164
|
+
RUST_LOG = "debug"
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
For a complete reference of all configuration options, see the [sample config file](src/sample-config.toml).
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
## Themes
|
|
172
|
+
|
|
173
|
+
Corsa includes several built-in themes. Set in your config:
|
|
174
|
+
|
|
175
|
+
```toml
|
|
176
|
+
[ui]
|
|
177
|
+
theme = "mist"
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Available themes: `default` (Moss), `mist`, `cappuccino`, `synthwave`, `terminal` (auto-detect from your terminal).
|
|
181
|
+
|
|
182
|
+
## MCP Integration
|
|
183
|
+
|
|
184
|
+
Corsa can expose an HTTP API for AI agents (Cursor, Claude, etc.) via the Model Context Protocol.
|
|
185
|
+
|
|
186
|
+
### Enable in Config
|
|
187
|
+
|
|
188
|
+
```toml
|
|
189
|
+
[mcp]
|
|
190
|
+
enabled = true
|
|
191
|
+
port = 18765
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Configure Your IDE
|
|
195
|
+
|
|
196
|
+
Add to your MCP configuration (e.g., `~/.cursor/mcp.json`):
|
|
197
|
+
|
|
198
|
+
```json
|
|
199
|
+
{
|
|
200
|
+
"mcpServers": {
|
|
201
|
+
"corsa": {
|
|
202
|
+
"command": "corsa",
|
|
203
|
+
"args": ["mcp"]
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### Available MCP Tools
|
|
210
|
+
|
|
211
|
+
| Tool | Description |
|
|
212
|
+
|------|-------------|
|
|
213
|
+
| `list_processes` | List all processes with status, health, and last 20 log lines |
|
|
214
|
+
| `get_logs` | Get recent logs (supports search and line limits) |
|
|
215
|
+
| `stop_process` | Stop a running process |
|
|
216
|
+
| `restart_process` | Restart a process |
|
|
217
|
+
| `clear_logs` | Clear logs for a process |
|
|
218
|
+
| `reload_config` | Reload config file and restart all processes |
|
|
219
|
+
|
|
220
|
+
## Contributing
|
|
221
|
+
|
|
222
|
+
See the [Contributing Guide](CONTRIBUTING.md) for development setup and guidelines.
|
|
223
|
+
|
|
224
|
+
## License
|
|
225
|
+
|
|
226
|
+
MIT
|
package/bin/corsa
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
const binDir = dirname(__filename);
|
|
10
|
+
const platform = process.platform;
|
|
11
|
+
const arch = process.arch;
|
|
12
|
+
|
|
13
|
+
// Map Node.js platform/arch to our binary names
|
|
14
|
+
const platformMap = {
|
|
15
|
+
darwin: "darwin",
|
|
16
|
+
linux: "linux",
|
|
17
|
+
win32: "windows",
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const archMap = {
|
|
21
|
+
arm64: "arm64",
|
|
22
|
+
x64: "x64",
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const os = platformMap[platform];
|
|
26
|
+
const cpu = archMap[arch];
|
|
27
|
+
|
|
28
|
+
if (!os || !cpu) {
|
|
29
|
+
console.error(`Unsupported platform: ${platform}-${arch}`);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const binaryName = `corsa-${os}-${cpu}${platform === "win32" ? ".exe" : ""}`;
|
|
34
|
+
const binaryPath = join(binDir, binaryName);
|
|
35
|
+
|
|
36
|
+
if (!existsSync(binaryPath)) {
|
|
37
|
+
console.error(`Binary not found: ${binaryPath}`);
|
|
38
|
+
console.error(
|
|
39
|
+
"This may happen if the postinstall script failed to download the binary.",
|
|
40
|
+
);
|
|
41
|
+
console.error("Try reinstalling: npm install -g corsa");
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Execute the binary with all arguments passed through
|
|
46
|
+
const child = spawn(binaryPath, process.argv.slice(2), {
|
|
47
|
+
stdio: "inherit",
|
|
48
|
+
windowsHide: true,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
child.on("error", (err) => {
|
|
52
|
+
console.error(`Failed to start corsa: ${err.message}`);
|
|
53
|
+
process.exit(1);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
child.on("exit", (code, signal) => {
|
|
57
|
+
if (signal) {
|
|
58
|
+
process.kill(process.pid, signal);
|
|
59
|
+
} else {
|
|
60
|
+
process.exit(code ?? 0);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
Binary file
|
package/bin/toolui
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
const binDir = dirname(__filename);
|
|
10
|
+
const platform = process.platform;
|
|
11
|
+
const arch = process.arch;
|
|
12
|
+
|
|
13
|
+
// Map Node.js platform/arch to our binary names
|
|
14
|
+
const platformMap = {
|
|
15
|
+
darwin: "darwin",
|
|
16
|
+
linux: "linux",
|
|
17
|
+
win32: "windows",
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const archMap = {
|
|
21
|
+
arm64: "arm64",
|
|
22
|
+
x64: "x64",
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const os = platformMap[platform];
|
|
26
|
+
const cpu = archMap[arch];
|
|
27
|
+
|
|
28
|
+
if (!os || !cpu) {
|
|
29
|
+
console.error(`Unsupported platform: ${platform}-${arch}`);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const binaryName = `corsa-${os}-${cpu}${platform === "win32" ? ".exe" : ""}`;
|
|
34
|
+
const binaryPath = join(binDir, binaryName);
|
|
35
|
+
|
|
36
|
+
if (!existsSync(binaryPath)) {
|
|
37
|
+
console.error(`Binary not found: ${binaryPath}`);
|
|
38
|
+
console.error(
|
|
39
|
+
"This may happen if the postinstall script failed to download the binary.",
|
|
40
|
+
);
|
|
41
|
+
console.error("Try reinstalling: npm install -g corsa");
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Execute the binary with all arguments passed through
|
|
46
|
+
const child = spawn(binaryPath, process.argv.slice(2), {
|
|
47
|
+
stdio: "inherit",
|
|
48
|
+
windowsHide: true,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
child.on("error", (err) => {
|
|
52
|
+
console.error(`Failed to start corsa: ${err.message}`);
|
|
53
|
+
process.exit(1);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
child.on("exit", (code, signal) => {
|
|
57
|
+
if (signal) {
|
|
58
|
+
process.kill(process.pid, signal);
|
|
59
|
+
} else {
|
|
60
|
+
process.exit(code ?? 0);
|
|
61
|
+
}
|
|
62
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tomagranate/corsa",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A Terminal User Interface (TUI) for running multiple local development servers and tools simultaneously",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"corsa": "bin/corsa"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"scripts"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"dev": "bun run src/index.tsx",
|
|
15
|
+
"build": "bun build src/index.tsx --compile --minify --outfile dist/corsa",
|
|
16
|
+
"build:all": "bun run scripts/build-all.ts",
|
|
17
|
+
"postinstall": "node scripts/postinstall.cjs",
|
|
18
|
+
"check": "biome check --fix",
|
|
19
|
+
"check:nofix": "biome check",
|
|
20
|
+
"lint": "bun check",
|
|
21
|
+
"format": "bun check",
|
|
22
|
+
"typecheck": "tsc --noEmit",
|
|
23
|
+
"test": "bun test",
|
|
24
|
+
"test:watch": "bun test --watch",
|
|
25
|
+
"prepublishOnly": "bun run typecheck && bun run check:nofix"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"cli",
|
|
29
|
+
"tui",
|
|
30
|
+
"terminal",
|
|
31
|
+
"devtools",
|
|
32
|
+
"process-manager",
|
|
33
|
+
"dev-server",
|
|
34
|
+
"multiplexer"
|
|
35
|
+
],
|
|
36
|
+
"author": "",
|
|
37
|
+
"license": "MIT",
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "git+https://github.com/tomagranate/corsa.git"
|
|
41
|
+
},
|
|
42
|
+
"homepage": "https://github.com/tomagranate/corsa#readme",
|
|
43
|
+
"bugs": {
|
|
44
|
+
"url": "https://github.com/tomagranate/corsa/issues"
|
|
45
|
+
},
|
|
46
|
+
"engines": {
|
|
47
|
+
"node": ">=18.0.0"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@iarna/toml": "^2.2.5",
|
|
51
|
+
"@modelcontextprotocol/sdk": "^1.25.3",
|
|
52
|
+
"@opentui/core": "^0.1.72",
|
|
53
|
+
"@opentui/react": "^0.1.72",
|
|
54
|
+
"fuzzysort": "^3.1.0",
|
|
55
|
+
"react": "^19.2.3",
|
|
56
|
+
"zod": "^4.3.6",
|
|
57
|
+
"@biomejs/biome": "^2.3.11",
|
|
58
|
+
"@types/bun": "latest",
|
|
59
|
+
"typescript": "^5.9.3"
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Build corsa binaries for all supported platforms.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* bun run scripts/build-all.ts
|
|
8
|
+
* bun run scripts/build-all.ts --version 0.1.0
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { mkdir, readFile } from "node:fs/promises";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { $ } from "bun";
|
|
14
|
+
|
|
15
|
+
interface BuildTarget {
|
|
16
|
+
target: string;
|
|
17
|
+
os: string;
|
|
18
|
+
arch: string;
|
|
19
|
+
extension: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const TARGETS: BuildTarget[] = [
|
|
23
|
+
{ target: "bun-darwin-arm64", os: "darwin", arch: "arm64", extension: "" },
|
|
24
|
+
{ target: "bun-darwin-x64", os: "darwin", arch: "x64", extension: "" },
|
|
25
|
+
{ target: "bun-linux-x64", os: "linux", arch: "x64", extension: "" },
|
|
26
|
+
{ target: "bun-linux-arm64", os: "linux", arch: "arm64", extension: "" },
|
|
27
|
+
{
|
|
28
|
+
target: "bun-windows-x64",
|
|
29
|
+
os: "windows",
|
|
30
|
+
arch: "x64",
|
|
31
|
+
extension: ".exe",
|
|
32
|
+
},
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
async function getVersion(): Promise<string> {
|
|
36
|
+
// Check for --version flag
|
|
37
|
+
const versionIndex = process.argv.indexOf("--version");
|
|
38
|
+
const versionArg = process.argv[versionIndex + 1];
|
|
39
|
+
if (versionIndex !== -1 && versionArg) {
|
|
40
|
+
return versionArg;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Read from package.json
|
|
44
|
+
const packageJson = JSON.parse(
|
|
45
|
+
await readFile(join(import.meta.dir, "..", "package.json"), "utf-8"),
|
|
46
|
+
) as { version: string };
|
|
47
|
+
return packageJson.version;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function buildTarget(target: BuildTarget, outDir: string): Promise<void> {
|
|
51
|
+
const outputName = `corsa-${target.os}-${target.arch}${target.extension}`;
|
|
52
|
+
const outputPath = join(outDir, outputName);
|
|
53
|
+
|
|
54
|
+
console.log(`Building ${outputName}...`);
|
|
55
|
+
|
|
56
|
+
await $`bun build src/index.tsx --compile --minify --target=${target.target} --outfile=${outputPath}`;
|
|
57
|
+
|
|
58
|
+
console.log(` ✓ ${outputName}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function main() {
|
|
62
|
+
const version = await getVersion();
|
|
63
|
+
const outDir = join(import.meta.dir, "..", "dist");
|
|
64
|
+
|
|
65
|
+
console.log(`\nBuilding corsa v${version} for all platforms\n`);
|
|
66
|
+
|
|
67
|
+
// Create output directory
|
|
68
|
+
await mkdir(outDir, { recursive: true });
|
|
69
|
+
|
|
70
|
+
// Build all targets
|
|
71
|
+
for (const target of TARGETS) {
|
|
72
|
+
await buildTarget(target, outDir);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
console.log(`\n✓ All binaries built in ${outDir}\n`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
main().catch((error) => {
|
|
79
|
+
console.error("Build failed:", error);
|
|
80
|
+
process.exit(1);
|
|
81
|
+
});
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Postinstall script for corsa NPM package.
|
|
5
|
+
* Downloads the appropriate binary for the current platform.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require("node:fs");
|
|
9
|
+
const path = require("node:path");
|
|
10
|
+
const { execSync } = require("node:child_process");
|
|
11
|
+
const zlib = require("node:zlib");
|
|
12
|
+
|
|
13
|
+
// Configuration
|
|
14
|
+
const REPO = "tomagranate/corsa";
|
|
15
|
+
const GITHUB_RELEASES = `https://github.com/${REPO}/releases`;
|
|
16
|
+
|
|
17
|
+
// Platform mappings
|
|
18
|
+
const PLATFORM_MAP = {
|
|
19
|
+
darwin: "darwin",
|
|
20
|
+
linux: "linux",
|
|
21
|
+
win32: "windows",
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const ARCH_MAP = {
|
|
25
|
+
arm64: "arm64",
|
|
26
|
+
x64: "x64",
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// Colors (respects NO_COLOR env var)
|
|
30
|
+
const useColor = !process.env.NO_COLOR && process.stdout.isTTY;
|
|
31
|
+
const colors = {
|
|
32
|
+
reset: useColor ? "\x1b[0m" : "",
|
|
33
|
+
bold: useColor ? "\x1b[1m" : "",
|
|
34
|
+
dim: useColor ? "\x1b[2m" : "",
|
|
35
|
+
cyan: useColor ? "\x1b[36m" : "",
|
|
36
|
+
green: useColor ? "\x1b[32m" : "",
|
|
37
|
+
yellow: useColor ? "\x1b[33m" : "",
|
|
38
|
+
red: useColor ? "\x1b[31m" : "",
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Print styled header
|
|
43
|
+
*/
|
|
44
|
+
function printHeader() {
|
|
45
|
+
console.log();
|
|
46
|
+
console.log(
|
|
47
|
+
`${colors.cyan}${colors.bold} ╭─────────────────────────────────╮${colors.reset}`,
|
|
48
|
+
);
|
|
49
|
+
console.log(
|
|
50
|
+
`${colors.cyan}${colors.bold} │ corsa postinstall │${colors.reset}`,
|
|
51
|
+
);
|
|
52
|
+
console.log(
|
|
53
|
+
`${colors.cyan}${colors.bold} ╰─────────────────────────────────╯${colors.reset}`,
|
|
54
|
+
);
|
|
55
|
+
console.log();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Print step message
|
|
60
|
+
*/
|
|
61
|
+
function step(msg) {
|
|
62
|
+
console.log(` ${colors.cyan}▸${colors.reset} ${msg}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Print success message
|
|
67
|
+
*/
|
|
68
|
+
function success(msg) {
|
|
69
|
+
console.log();
|
|
70
|
+
console.log(
|
|
71
|
+
`${colors.green}${colors.bold} ╭─────────────────────────────────╮${colors.reset}`,
|
|
72
|
+
);
|
|
73
|
+
console.log(
|
|
74
|
+
`${colors.green}${colors.bold} │${colors.reset} ${colors.green}✓${colors.reset} ${msg.padEnd(27)} ${colors.green}${colors.bold}│${colors.reset}`,
|
|
75
|
+
);
|
|
76
|
+
console.log(
|
|
77
|
+
`${colors.green}${colors.bold} ╰─────────────────────────────────╯${colors.reset}`,
|
|
78
|
+
);
|
|
79
|
+
console.log();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Print error message
|
|
84
|
+
*/
|
|
85
|
+
function printError(msg) {
|
|
86
|
+
console.error(` ${colors.red}✗${colors.reset} ${msg}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Format bytes as human-readable string
|
|
91
|
+
*/
|
|
92
|
+
function formatBytes(bytes) {
|
|
93
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
94
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
95
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Show download progress bar
|
|
100
|
+
*/
|
|
101
|
+
function showProgress(downloaded, total) {
|
|
102
|
+
if (!process.stdout.isTTY) return;
|
|
103
|
+
|
|
104
|
+
const width = 24;
|
|
105
|
+
if (total) {
|
|
106
|
+
const percent = Math.min(100, (downloaded / total) * 100);
|
|
107
|
+
const filled = Math.floor((percent / 100) * width);
|
|
108
|
+
const empty = width - filled;
|
|
109
|
+
const bar = "█".repeat(filled) + "░".repeat(empty);
|
|
110
|
+
const percentStr = percent.toFixed(0).padStart(3);
|
|
111
|
+
process.stdout.write(
|
|
112
|
+
`\r ${colors.dim}${bar}${colors.reset} ${percentStr}% ${colors.dim}(${formatBytes(downloaded)})${colors.reset}`,
|
|
113
|
+
);
|
|
114
|
+
} else {
|
|
115
|
+
process.stdout.write(
|
|
116
|
+
`\r ${colors.dim}Downloading: ${formatBytes(downloaded)}${colors.reset}`,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Clear progress line
|
|
123
|
+
*/
|
|
124
|
+
function clearProgress() {
|
|
125
|
+
if (!process.stdout.isTTY) return;
|
|
126
|
+
process.stdout.write(`\r${" ".repeat(70)}\r`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Download file with streaming and progress
|
|
131
|
+
*/
|
|
132
|
+
async function downloadWithProgress(url) {
|
|
133
|
+
const response = await fetch(url, {
|
|
134
|
+
headers: { "User-Agent": "corsa-postinstall" },
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
if (!response.ok) {
|
|
138
|
+
throw new Error(`HTTP ${response.status}`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const contentLength = response.headers.get("content-length");
|
|
142
|
+
const total = contentLength ? parseInt(contentLength, 10) : null;
|
|
143
|
+
let downloaded = 0;
|
|
144
|
+
|
|
145
|
+
const chunks = [];
|
|
146
|
+
const reader = response.body.getReader();
|
|
147
|
+
|
|
148
|
+
while (true) {
|
|
149
|
+
const { done, value } = await reader.read();
|
|
150
|
+
if (done) break;
|
|
151
|
+
|
|
152
|
+
chunks.push(value);
|
|
153
|
+
downloaded += value.length;
|
|
154
|
+
showProgress(downloaded, total);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
clearProgress();
|
|
158
|
+
return Buffer.concat(chunks);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Get the version from package.json
|
|
163
|
+
*/
|
|
164
|
+
function getVersion() {
|
|
165
|
+
const packageJson = JSON.parse(
|
|
166
|
+
fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf-8"),
|
|
167
|
+
);
|
|
168
|
+
return packageJson.version;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Extract tar.gz archive to get the binary
|
|
173
|
+
*/
|
|
174
|
+
function extractTarGz(data, destPath) {
|
|
175
|
+
return new Promise((resolve, reject) => {
|
|
176
|
+
const gunzip = zlib.createGunzip();
|
|
177
|
+
const chunks = [];
|
|
178
|
+
|
|
179
|
+
gunzip.on("data", (chunk) => chunks.push(chunk));
|
|
180
|
+
gunzip.on("end", () => {
|
|
181
|
+
const tarData = Buffer.concat(chunks);
|
|
182
|
+
let offset = 0;
|
|
183
|
+
while (offset < tarData.length) {
|
|
184
|
+
const header = tarData.slice(offset, offset + 512);
|
|
185
|
+
if (header[0] === 0) break;
|
|
186
|
+
|
|
187
|
+
const filename = header
|
|
188
|
+
.slice(0, 100)
|
|
189
|
+
.toString("utf-8")
|
|
190
|
+
.replace(/\0/g, "");
|
|
191
|
+
|
|
192
|
+
const sizeStr = header
|
|
193
|
+
.slice(124, 136)
|
|
194
|
+
.toString("utf-8")
|
|
195
|
+
.replace(/\0/g, "")
|
|
196
|
+
.trim();
|
|
197
|
+
const size = parseInt(sizeStr, 8) || 0;
|
|
198
|
+
|
|
199
|
+
offset += 512;
|
|
200
|
+
|
|
201
|
+
if (filename && size > 0 && filename.startsWith("corsa")) {
|
|
202
|
+
const content = tarData.slice(offset, offset + size);
|
|
203
|
+
fs.writeFileSync(destPath, content);
|
|
204
|
+
fs.chmodSync(destPath, 0o755);
|
|
205
|
+
resolve();
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
offset += Math.ceil(size / 512) * 512;
|
|
210
|
+
}
|
|
211
|
+
reject(new Error("Binary not found in archive"));
|
|
212
|
+
});
|
|
213
|
+
gunzip.on("error", reject);
|
|
214
|
+
|
|
215
|
+
gunzip.end(data);
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Download and extract Unix binary
|
|
221
|
+
*/
|
|
222
|
+
async function downloadBinary(url, destPath) {
|
|
223
|
+
const data = await downloadWithProgress(url);
|
|
224
|
+
await extractTarGz(data, destPath);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Download and extract Windows binary
|
|
229
|
+
*/
|
|
230
|
+
async function downloadWindowsBinary(url, destPath) {
|
|
231
|
+
const data = await downloadWithProgress(url);
|
|
232
|
+
|
|
233
|
+
const zipPath = `${destPath}.zip`;
|
|
234
|
+
fs.writeFileSync(zipPath, data);
|
|
235
|
+
|
|
236
|
+
try {
|
|
237
|
+
if (process.platform === "win32") {
|
|
238
|
+
execSync(
|
|
239
|
+
`powershell -command "Expand-Archive -Path '${zipPath}' -DestinationPath '${path.dirname(destPath)}' -Force"`,
|
|
240
|
+
{ stdio: "pipe" },
|
|
241
|
+
);
|
|
242
|
+
} else {
|
|
243
|
+
execSync(`unzip -o "${zipPath}" -d "${path.dirname(destPath)}"`, {
|
|
244
|
+
stdio: "pipe",
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
} finally {
|
|
248
|
+
fs.unlinkSync(zipPath);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Download binary from URL
|
|
254
|
+
*/
|
|
255
|
+
async function downloadFromUrl(url, destPath, isWindows) {
|
|
256
|
+
if (isWindows) {
|
|
257
|
+
await downloadWindowsBinary(url, destPath);
|
|
258
|
+
} else {
|
|
259
|
+
await downloadBinary(url, destPath);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async function main() {
|
|
264
|
+
const platform = PLATFORM_MAP[process.platform];
|
|
265
|
+
const arch = ARCH_MAP[process.arch];
|
|
266
|
+
|
|
267
|
+
if (!platform || !arch) {
|
|
268
|
+
console.log();
|
|
269
|
+
console.log(
|
|
270
|
+
` ${colors.yellow}!${colors.reset} Unsupported platform: ${process.platform}-${process.arch}`,
|
|
271
|
+
);
|
|
272
|
+
console.log(` Build from source or download manually.`);
|
|
273
|
+
console.log();
|
|
274
|
+
process.exit(0);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const version = getVersion();
|
|
278
|
+
const binaryName = `corsa-${platform}-${arch}`;
|
|
279
|
+
const binDir = path.join(__dirname, "..", "bin");
|
|
280
|
+
const destPath = path.join(
|
|
281
|
+
binDir,
|
|
282
|
+
binaryName + (platform === "windows" ? ".exe" : ""),
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
// Skip if binary already exists
|
|
286
|
+
if (fs.existsSync(destPath)) {
|
|
287
|
+
console.log();
|
|
288
|
+
console.log(` ${colors.dim}Binary already installed${colors.reset}`);
|
|
289
|
+
console.log();
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
printHeader();
|
|
294
|
+
|
|
295
|
+
step(`Platform: ${colors.bold}${platform}-${arch}${colors.reset}`);
|
|
296
|
+
step(`Version: ${colors.bold}v${version}${colors.reset}`);
|
|
297
|
+
console.log();
|
|
298
|
+
|
|
299
|
+
// Ensure bin directory exists
|
|
300
|
+
if (!fs.existsSync(binDir)) {
|
|
301
|
+
fs.mkdirSync(binDir, { recursive: true });
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const archiveExt = platform === "windows" ? "zip" : "tar.gz";
|
|
305
|
+
const url = `${GITHUB_RELEASES}/download/v${version}/${binaryName}.${archiveExt}`;
|
|
306
|
+
|
|
307
|
+
try {
|
|
308
|
+
step("Downloading binary...");
|
|
309
|
+
await downloadFromUrl(url, destPath, platform === "windows");
|
|
310
|
+
|
|
311
|
+
step("Extracting...");
|
|
312
|
+
success(`corsa v${version} ready`);
|
|
313
|
+
|
|
314
|
+
console.log(` ${colors.dim}Get started:${colors.reset}`);
|
|
315
|
+
console.log(` ${colors.cyan}$${colors.reset} corsa init`);
|
|
316
|
+
console.log(` ${colors.cyan}$${colors.reset} corsa`);
|
|
317
|
+
console.log();
|
|
318
|
+
} catch (error) {
|
|
319
|
+
printError(`Download failed: ${error.message}`);
|
|
320
|
+
console.log();
|
|
321
|
+
console.log(` ${colors.dim}Manual download:${colors.reset}`);
|
|
322
|
+
console.log(` ${GITHUB_RELEASES}/latest`);
|
|
323
|
+
console.log();
|
|
324
|
+
console.log(` ${colors.dim}Or use install script:${colors.reset}`);
|
|
325
|
+
console.log(
|
|
326
|
+
` curl -fsSL https://raw.githubusercontent.com/${REPO}/main/install.sh | bash`,
|
|
327
|
+
);
|
|
328
|
+
console.log();
|
|
329
|
+
process.exit(0);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
main();
|