@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 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();