@tomagranate/toolui 0.1.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 +225 -0
- package/bin/toolui +60 -0
- package/bin/toolui-linux-x64 +0 -0
- package/package.json +65 -0
- package/scripts/build-all.ts +81 -0
- package/scripts/postinstall.cjs +219 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 toolui 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,225 @@
|
|
|
1
|
+
# ToolUI
|
|
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 ToolUI?
|
|
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. ToolUI 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/toolui/toolui
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### NPM
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm install -g @tomagranate/toolui
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### curl (macOS and Linux)
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
curl -fsSL https://raw.githubusercontent.com/tomagranate/toolui/main/install.sh | bash
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Manual Download
|
|
38
|
+
|
|
39
|
+
Download the latest binary for your platform from [Releases](https://github.com/tomagranate/toolui/releases).
|
|
40
|
+
|
|
41
|
+
| Platform | Download |
|
|
42
|
+
|----------|----------|
|
|
43
|
+
| macOS (Apple Silicon) | `toolui-darwin-arm64.tar.gz` |
|
|
44
|
+
| macOS (Intel) | `toolui-darwin-x64.tar.gz` |
|
|
45
|
+
| Linux (x64) | `toolui-linux-x64.tar.gz` |
|
|
46
|
+
| Linux (ARM64) | `toolui-linux-arm64.tar.gz` |
|
|
47
|
+
| Windows (x64) | `toolui-windows-x64.zip` |
|
|
48
|
+
|
|
49
|
+
## Quick Start
|
|
50
|
+
|
|
51
|
+
1. Create a config file in your project:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
toolui init
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
2. Edit `toolui.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
|
+
toolui
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## CLI Reference
|
|
80
|
+
|
|
81
|
+
### Commands
|
|
82
|
+
|
|
83
|
+
| Command | Description |
|
|
84
|
+
|---------|-------------|
|
|
85
|
+
| `toolui` | Start the TUI dashboard |
|
|
86
|
+
| `toolui init` | Create a sample config file in the current directory |
|
|
87
|
+
| `toolui 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: `toolui.config.toml`) |
|
|
94
|
+
| `-h, --help` | Show help message |
|
|
95
|
+
|
|
96
|
+
### Examples
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
# Start with default config
|
|
100
|
+
toolui
|
|
101
|
+
|
|
102
|
+
# Use a custom config file
|
|
103
|
+
toolui --config ./configs/dev.toml
|
|
104
|
+
toolui -c ./configs/dev.toml
|
|
105
|
+
|
|
106
|
+
# Create a new config file
|
|
107
|
+
toolui init
|
|
108
|
+
|
|
109
|
+
# Start MCP server for AI integration
|
|
110
|
+
toolui mcp
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Configuration
|
|
114
|
+
|
|
115
|
+
ToolUI is configured via a TOML file. By default, it looks for `toolui.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
|
+
ToolUI 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
|
+
ToolUI 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
|
+
"toolui": {
|
|
202
|
+
"command": "toolui",
|
|
203
|
+
"args": ["mcp"]
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### Available MCP Tools
|
|
210
|
+
|
|
211
|
+
| Tool | Description |
|
|
212
|
+
|------|-------------|
|
|
213
|
+
| `list_processes` | List all processes with their status |
|
|
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
|
+
|
|
219
|
+
## Contributing
|
|
220
|
+
|
|
221
|
+
See the [Contributing Guide](CONTRIBUTING.md) for development setup and guidelines.
|
|
222
|
+
|
|
223
|
+
## License
|
|
224
|
+
|
|
225
|
+
MIT
|
package/bin/toolui
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { spawn } = require("child_process");
|
|
4
|
+
const { existsSync } = require("fs");
|
|
5
|
+
const { join, dirname } = require("path");
|
|
6
|
+
|
|
7
|
+
const binDir = dirname(__filename);
|
|
8
|
+
const platform = process.platform;
|
|
9
|
+
const arch = process.arch;
|
|
10
|
+
|
|
11
|
+
// Map Node.js platform/arch to our binary names
|
|
12
|
+
const platformMap = {
|
|
13
|
+
darwin: "darwin",
|
|
14
|
+
linux: "linux",
|
|
15
|
+
win32: "windows",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const archMap = {
|
|
19
|
+
arm64: "arm64",
|
|
20
|
+
x64: "x64",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const os = platformMap[platform];
|
|
24
|
+
const cpu = archMap[arch];
|
|
25
|
+
|
|
26
|
+
if (!os || !cpu) {
|
|
27
|
+
console.error(`Unsupported platform: ${platform}-${arch}`);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const binaryName = `toolui-${os}-${cpu}${platform === "win32" ? ".exe" : ""}`;
|
|
32
|
+
const binaryPath = join(binDir, binaryName);
|
|
33
|
+
|
|
34
|
+
if (!existsSync(binaryPath)) {
|
|
35
|
+
console.error(`Binary not found: ${binaryPath}`);
|
|
36
|
+
console.error(
|
|
37
|
+
"This may happen if the postinstall script failed to download the binary."
|
|
38
|
+
);
|
|
39
|
+
console.error("Try reinstalling: npm install -g toolui");
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Execute the binary with all arguments passed through
|
|
44
|
+
const child = spawn(binaryPath, process.argv.slice(2), {
|
|
45
|
+
stdio: "inherit",
|
|
46
|
+
windowsHide: true,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
child.on("error", (err) => {
|
|
50
|
+
console.error(`Failed to start toolui: ${err.message}`);
|
|
51
|
+
process.exit(1);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
child.on("exit", (code, signal) => {
|
|
55
|
+
if (signal) {
|
|
56
|
+
process.kill(process.pid, signal);
|
|
57
|
+
} else {
|
|
58
|
+
process.exit(code ?? 0);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
Binary file
|
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tomagranate/toolui",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A Terminal User Interface (TUI) for running multiple local development servers and tools simultaneously",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"toolui": "bin/toolui"
|
|
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/toolui",
|
|
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/toolui.git"
|
|
41
|
+
},
|
|
42
|
+
"homepage": "https://github.com/tomagranate/toolui#readme",
|
|
43
|
+
"bugs": {
|
|
44
|
+
"url": "https://github.com/tomagranate/toolui/issues"
|
|
45
|
+
},
|
|
46
|
+
"engines": {
|
|
47
|
+
"node": ">=18.0.0"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@biomejs/biome": "^2.3.11",
|
|
51
|
+
"@types/bun": "latest"
|
|
52
|
+
},
|
|
53
|
+
"peerDependencies": {
|
|
54
|
+
"typescript": "^5.9.3"
|
|
55
|
+
},
|
|
56
|
+
"dependencies": {
|
|
57
|
+
"@iarna/toml": "^2.2.5",
|
|
58
|
+
"@modelcontextprotocol/sdk": "^1.25.3",
|
|
59
|
+
"@opentui/core": "^0.1.72",
|
|
60
|
+
"@opentui/react": "^0.1.72",
|
|
61
|
+
"fuzzysort": "^3.1.0",
|
|
62
|
+
"react": "^19.2.3",
|
|
63
|
+
"zod": "^4.3.6"
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Build toolui 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 = `toolui-${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 toolui 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,219 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Postinstall script for toolui NPM package.
|
|
5
|
+
* Downloads the appropriate binary for the current platform.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const https = require("node:https");
|
|
9
|
+
const fs = require("node:fs");
|
|
10
|
+
const path = require("node:path");
|
|
11
|
+
const { execSync } = require("node:child_process");
|
|
12
|
+
const zlib = require("node:zlib");
|
|
13
|
+
|
|
14
|
+
// Configuration
|
|
15
|
+
const REPO = "tomagranate/toolui";
|
|
16
|
+
const GITHUB_RELEASES = `https://github.com/${REPO}/releases`;
|
|
17
|
+
|
|
18
|
+
// Platform mappings
|
|
19
|
+
const PLATFORM_MAP = {
|
|
20
|
+
darwin: "darwin",
|
|
21
|
+
linux: "linux",
|
|
22
|
+
win32: "windows",
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const ARCH_MAP = {
|
|
26
|
+
arm64: "arm64",
|
|
27
|
+
x64: "x64",
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Make an HTTPS GET request
|
|
32
|
+
*/
|
|
33
|
+
function httpsGet(url, options = {}) {
|
|
34
|
+
return new Promise((resolve, reject) => {
|
|
35
|
+
const opts = {
|
|
36
|
+
...options,
|
|
37
|
+
headers: {
|
|
38
|
+
"User-Agent": "toolui-postinstall",
|
|
39
|
+
...options.headers,
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
https
|
|
44
|
+
.get(url, opts, (res) => {
|
|
45
|
+
// Handle redirects
|
|
46
|
+
if (
|
|
47
|
+
res.statusCode >= 300 &&
|
|
48
|
+
res.statusCode < 400 &&
|
|
49
|
+
res.headers.location
|
|
50
|
+
) {
|
|
51
|
+
return httpsGet(res.headers.location, options)
|
|
52
|
+
.then(resolve)
|
|
53
|
+
.catch(reject);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (res.statusCode !== 200) {
|
|
57
|
+
reject(new Error(`HTTP ${res.statusCode}: ${url}`));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const chunks = [];
|
|
62
|
+
res.on("data", (chunk) => chunks.push(chunk));
|
|
63
|
+
res.on("end", () => resolve(Buffer.concat(chunks)));
|
|
64
|
+
res.on("error", reject);
|
|
65
|
+
})
|
|
66
|
+
.on("error", reject);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Get the version from package.json
|
|
72
|
+
*/
|
|
73
|
+
function getVersion() {
|
|
74
|
+
const packageJson = JSON.parse(
|
|
75
|
+
fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf-8"),
|
|
76
|
+
);
|
|
77
|
+
return packageJson.version;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Download and extract the binary
|
|
82
|
+
*/
|
|
83
|
+
async function downloadBinary(url, destPath) {
|
|
84
|
+
console.log(`Downloading from ${url}...`);
|
|
85
|
+
const data = await httpsGet(url);
|
|
86
|
+
|
|
87
|
+
// Extract tar.gz
|
|
88
|
+
return new Promise((resolve, reject) => {
|
|
89
|
+
const gunzip = zlib.createGunzip();
|
|
90
|
+
const chunks = [];
|
|
91
|
+
|
|
92
|
+
gunzip.on("data", (chunk) => chunks.push(chunk));
|
|
93
|
+
gunzip.on("end", () => {
|
|
94
|
+
const tarData = Buffer.concat(chunks);
|
|
95
|
+
// Simple tar extraction - find the file content
|
|
96
|
+
// tar format: 512-byte header blocks followed by file content
|
|
97
|
+
let offset = 0;
|
|
98
|
+
while (offset < tarData.length) {
|
|
99
|
+
const header = tarData.slice(offset, offset + 512);
|
|
100
|
+
if (header[0] === 0) break; // End of archive
|
|
101
|
+
|
|
102
|
+
// Get filename (bytes 0-99)
|
|
103
|
+
const filename = header
|
|
104
|
+
.slice(0, 100)
|
|
105
|
+
.toString("utf-8")
|
|
106
|
+
.replace(/\0/g, "");
|
|
107
|
+
|
|
108
|
+
// Get file size (bytes 124-135, octal)
|
|
109
|
+
const sizeStr = header
|
|
110
|
+
.slice(124, 136)
|
|
111
|
+
.toString("utf-8")
|
|
112
|
+
.replace(/\0/g, "")
|
|
113
|
+
.trim();
|
|
114
|
+
const size = parseInt(sizeStr, 8) || 0;
|
|
115
|
+
|
|
116
|
+
offset += 512; // Move past header
|
|
117
|
+
|
|
118
|
+
if (filename && size > 0 && filename.startsWith("toolui")) {
|
|
119
|
+
const content = tarData.slice(offset, offset + size);
|
|
120
|
+
fs.writeFileSync(destPath, content);
|
|
121
|
+
fs.chmodSync(destPath, 0o755);
|
|
122
|
+
resolve();
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Move to next file (content is padded to 512-byte blocks)
|
|
127
|
+
offset += Math.ceil(size / 512) * 512;
|
|
128
|
+
}
|
|
129
|
+
reject(new Error("Could not find toolui binary in archive"));
|
|
130
|
+
});
|
|
131
|
+
gunzip.on("error", reject);
|
|
132
|
+
|
|
133
|
+
gunzip.end(data);
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Download Windows zip and extract
|
|
139
|
+
*/
|
|
140
|
+
async function downloadWindowsBinary(url, destPath) {
|
|
141
|
+
console.log(`Downloading from ${url}...`);
|
|
142
|
+
const data = await httpsGet(url);
|
|
143
|
+
|
|
144
|
+
// Write zip to temp file
|
|
145
|
+
const zipPath = `${destPath}.zip`;
|
|
146
|
+
fs.writeFileSync(zipPath, data);
|
|
147
|
+
|
|
148
|
+
// Use unzip if available, otherwise try PowerShell
|
|
149
|
+
try {
|
|
150
|
+
if (process.platform === "win32") {
|
|
151
|
+
execSync(
|
|
152
|
+
`powershell -command "Expand-Archive -Path '${zipPath}' -DestinationPath '${path.dirname(destPath)}' -Force"`,
|
|
153
|
+
{ stdio: "pipe" },
|
|
154
|
+
);
|
|
155
|
+
} else {
|
|
156
|
+
execSync(`unzip -o "${zipPath}" -d "${path.dirname(destPath)}"`, {
|
|
157
|
+
stdio: "pipe",
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
} finally {
|
|
161
|
+
fs.unlinkSync(zipPath);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function main() {
|
|
166
|
+
const platform = PLATFORM_MAP[process.platform];
|
|
167
|
+
const arch = ARCH_MAP[process.arch];
|
|
168
|
+
|
|
169
|
+
if (!platform || !arch) {
|
|
170
|
+
console.log(`Unsupported platform: ${process.platform}-${process.arch}`);
|
|
171
|
+
console.log("You may need to build from source or download manually.");
|
|
172
|
+
process.exit(0); // Don't fail installation
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const version = getVersion();
|
|
176
|
+
const binaryName = `toolui-${platform}-${arch}`;
|
|
177
|
+
const binDir = path.join(__dirname, "..", "bin");
|
|
178
|
+
const destPath = path.join(
|
|
179
|
+
binDir,
|
|
180
|
+
binaryName + (platform === "windows" ? ".exe" : ""),
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
// Skip if binary already exists
|
|
184
|
+
if (fs.existsSync(destPath)) {
|
|
185
|
+
console.log(`Binary already exists: ${destPath}`);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Ensure bin directory exists
|
|
190
|
+
if (!fs.existsSync(binDir)) {
|
|
191
|
+
fs.mkdirSync(binDir, { recursive: true });
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const archiveExt = platform === "windows" ? "zip" : "tar.gz";
|
|
195
|
+
const downloadUrl = `${GITHUB_RELEASES}/download/v${version}/${binaryName}.${archiveExt}`;
|
|
196
|
+
|
|
197
|
+
try {
|
|
198
|
+
if (platform === "windows") {
|
|
199
|
+
await downloadWindowsBinary(downloadUrl, destPath);
|
|
200
|
+
} else {
|
|
201
|
+
await downloadBinary(downloadUrl, destPath);
|
|
202
|
+
}
|
|
203
|
+
console.log(`Successfully installed toolui binary to ${destPath}`);
|
|
204
|
+
} catch (error) {
|
|
205
|
+
console.error(`Failed to download binary: ${error.message}`);
|
|
206
|
+
console.error("");
|
|
207
|
+
console.error("You can manually download the binary from:");
|
|
208
|
+
console.error(` ${GITHUB_RELEASES}/latest`);
|
|
209
|
+
console.error("");
|
|
210
|
+
console.error("Or install via the install script:");
|
|
211
|
+
console.error(
|
|
212
|
+
` curl -fsSL https://raw.githubusercontent.com/${REPO}/main/install.sh | bash`,
|
|
213
|
+
);
|
|
214
|
+
// Don't fail the install - the user can still use the package
|
|
215
|
+
process.exit(0);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
main();
|