@tomagranate/toolui 0.2.6 → 0.3.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.
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tomagranate/toolui",
3
- "version": "0.2.6",
3
+ "version": "0.3.0",
4
4
  "description": "A Terminal User Interface (TUI) for running multiple local development servers and tools simultaneously",
5
5
  "type": "module",
6
6
  "bin": {
@@ -5,7 +5,6 @@
5
5
  * Downloads the appropriate binary for the current platform.
6
6
  */
7
7
 
8
- const https = require("node:https");
9
8
  const fs = require("node:fs");
10
9
  const path = require("node:path");
11
10
  const { execSync } = require("node:child_process");
@@ -13,6 +12,7 @@ const zlib = require("node:zlib");
13
12
 
14
13
  // Configuration
15
14
  const REPO = "tomagranate/toolui";
15
+ const R2_CDN = "https://toolui-releases.jetsail.xyz";
16
16
  const GITHUB_RELEASES = `https://github.com/${REPO}/releases`;
17
17
 
18
18
  // Platform mappings
@@ -27,44 +27,143 @@ const ARCH_MAP = {
27
27
  x64: "x64",
28
28
  };
29
29
 
30
+ // Colors (respects NO_COLOR env var)
31
+ const useColor = !process.env.NO_COLOR && process.stdout.isTTY;
32
+ const colors = {
33
+ reset: useColor ? "\x1b[0m" : "",
34
+ bold: useColor ? "\x1b[1m" : "",
35
+ dim: useColor ? "\x1b[2m" : "",
36
+ cyan: useColor ? "\x1b[36m" : "",
37
+ green: useColor ? "\x1b[32m" : "",
38
+ yellow: useColor ? "\x1b[33m" : "",
39
+ red: useColor ? "\x1b[31m" : "",
40
+ };
41
+
30
42
  /**
31
- * Make an HTTPS GET request
43
+ * Print styled header
32
44
  */
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
- }
45
+ function printHeader() {
46
+ console.log();
47
+ console.log(
48
+ `${colors.cyan}${colors.bold} ╭─────────────────────────────────╮${colors.reset}`,
49
+ );
50
+ console.log(
51
+ `${colors.cyan}${colors.bold} │ toolui postinstall │${colors.reset}`,
52
+ );
53
+ console.log(
54
+ `${colors.cyan}${colors.bold} ╰─────────────────────────────────╯${colors.reset}`,
55
+ );
56
+ console.log();
57
+ }
55
58
 
56
- if (res.statusCode !== 200) {
57
- reject(new Error(`HTTP ${res.statusCode}: ${url}`));
58
- return;
59
- }
59
+ /**
60
+ * Print step message
61
+ */
62
+ function step(msg) {
63
+ console.log(` ${colors.cyan}▸${colors.reset} ${msg}`);
64
+ }
65
+
66
+ /**
67
+ * Print substep message
68
+ */
69
+ function substep(msg) {
70
+ console.log(` ${colors.dim}${msg}${colors.reset}`);
71
+ }
60
72
 
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);
73
+ /**
74
+ * Print success message
75
+ */
76
+ function success(msg) {
77
+ console.log();
78
+ console.log(
79
+ `${colors.green}${colors.bold} ╭─────────────────────────────────╮${colors.reset}`,
80
+ );
81
+ console.log(
82
+ `${colors.green}${colors.bold} │${colors.reset} ${colors.green}✓${colors.reset} ${msg.padEnd(27)} ${colors.green}${colors.bold}│${colors.reset}`,
83
+ );
84
+ console.log(
85
+ `${colors.green}${colors.bold} ╰─────────────────────────────────╯${colors.reset}`,
86
+ );
87
+ console.log();
88
+ }
89
+
90
+ /**
91
+ * Print error message
92
+ */
93
+ function printError(msg) {
94
+ console.error(` ${colors.red}✗${colors.reset} ${msg}`);
95
+ }
96
+
97
+ /**
98
+ * Format bytes as human-readable string
99
+ */
100
+ function formatBytes(bytes) {
101
+ if (bytes < 1024) return `${bytes} B`;
102
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
103
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
104
+ }
105
+
106
+ /**
107
+ * Show download progress bar
108
+ */
109
+ function showProgress(downloaded, total) {
110
+ if (!process.stdout.isTTY) return;
111
+
112
+ const width = 24;
113
+ if (total) {
114
+ const percent = Math.min(100, (downloaded / total) * 100);
115
+ const filled = Math.floor((percent / 100) * width);
116
+ const empty = width - filled;
117
+ const bar = "█".repeat(filled) + "░".repeat(empty);
118
+ const percentStr = percent.toFixed(0).padStart(3);
119
+ process.stdout.write(
120
+ `\r ${colors.dim}${bar}${colors.reset} ${percentStr}% ${colors.dim}(${formatBytes(downloaded)})${colors.reset}`,
121
+ );
122
+ } else {
123
+ process.stdout.write(
124
+ `\r ${colors.dim}Downloading: ${formatBytes(downloaded)}${colors.reset}`,
125
+ );
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Clear progress line
131
+ */
132
+ function clearProgress() {
133
+ if (!process.stdout.isTTY) return;
134
+ process.stdout.write("\r" + " ".repeat(70) + "\r");
135
+ }
136
+
137
+ /**
138
+ * Download file with streaming and progress
139
+ */
140
+ async function downloadWithProgress(url) {
141
+ const response = await fetch(url, {
142
+ headers: { "User-Agent": "toolui-postinstall" },
67
143
  });
144
+
145
+ if (!response.ok) {
146
+ throw new Error(`HTTP ${response.status}`);
147
+ }
148
+
149
+ const contentLength = response.headers.get("content-length");
150
+ const total = contentLength ? parseInt(contentLength, 10) : null;
151
+ let downloaded = 0;
152
+
153
+ const chunks = [];
154
+ const reader = response.body.getReader();
155
+
156
+ while (true) {
157
+ const { done, value } = await reader.read();
158
+ if (done) break;
159
+
160
+ chunks.push(value);
161
+ downloaded += value.length;
162
+ showProgress(downloaded, total);
163
+ }
164
+
165
+ clearProgress();
166
+ return Buffer.concat(chunks);
68
167
  }
69
168
 
70
169
  /**
@@ -78,13 +177,9 @@ function getVersion() {
78
177
  }
79
178
 
80
179
  /**
81
- * Download and extract the binary
180
+ * Extract tar.gz archive to get the binary
82
181
  */
83
- async function downloadBinary(url, destPath) {
84
- console.log(`Downloading from ${url}...`);
85
- const data = await httpsGet(url);
86
-
87
- // Extract tar.gz
182
+ function extractTarGz(data, destPath) {
88
183
  return new Promise((resolve, reject) => {
89
184
  const gunzip = zlib.createGunzip();
90
185
  const chunks = [];
@@ -92,20 +187,16 @@ async function downloadBinary(url, destPath) {
92
187
  gunzip.on("data", (chunk) => chunks.push(chunk));
93
188
  gunzip.on("end", () => {
94
189
  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
190
  let offset = 0;
98
191
  while (offset < tarData.length) {
99
192
  const header = tarData.slice(offset, offset + 512);
100
- if (header[0] === 0) break; // End of archive
193
+ if (header[0] === 0) break;
101
194
 
102
- // Get filename (bytes 0-99)
103
195
  const filename = header
104
196
  .slice(0, 100)
105
197
  .toString("utf-8")
106
198
  .replace(/\0/g, "");
107
199
 
108
- // Get file size (bytes 124-135, octal)
109
200
  const sizeStr = header
110
201
  .slice(124, 136)
111
202
  .toString("utf-8")
@@ -113,7 +204,7 @@ async function downloadBinary(url, destPath) {
113
204
  .trim();
114
205
  const size = parseInt(sizeStr, 8) || 0;
115
206
 
116
- offset += 512; // Move past header
207
+ offset += 512;
117
208
 
118
209
  if (filename && size > 0 && filename.startsWith("toolui")) {
119
210
  const content = tarData.slice(offset, offset + size);
@@ -123,10 +214,9 @@ async function downloadBinary(url, destPath) {
123
214
  return;
124
215
  }
125
216
 
126
- // Move to next file (content is padded to 512-byte blocks)
127
217
  offset += Math.ceil(size / 512) * 512;
128
218
  }
129
- reject(new Error("Could not find toolui binary in archive"));
219
+ reject(new Error("Binary not found in archive"));
130
220
  });
131
221
  gunzip.on("error", reject);
132
222
 
@@ -135,17 +225,22 @@ async function downloadBinary(url, destPath) {
135
225
  }
136
226
 
137
227
  /**
138
- * Download Windows zip and extract
228
+ * Download and extract Unix binary
229
+ */
230
+ async function downloadBinary(url, destPath) {
231
+ const data = await downloadWithProgress(url);
232
+ await extractTarGz(data, destPath);
233
+ }
234
+
235
+ /**
236
+ * Download and extract Windows binary
139
237
  */
140
238
  async function downloadWindowsBinary(url, destPath) {
141
- console.log(`Downloading from ${url}...`);
142
- const data = await httpsGet(url);
239
+ const data = await downloadWithProgress(url);
143
240
 
144
- // Write zip to temp file
145
241
  const zipPath = `${destPath}.zip`;
146
242
  fs.writeFileSync(zipPath, data);
147
243
 
148
- // Use unzip if available, otherwise try PowerShell
149
244
  try {
150
245
  if (process.platform === "win32") {
151
246
  execSync(
@@ -162,14 +257,45 @@ async function downloadWindowsBinary(url, destPath) {
162
257
  }
163
258
  }
164
259
 
260
+ /**
261
+ * Try downloading from multiple URLs with fallback
262
+ */
263
+ async function downloadWithFallback(urls, destPath, isWindows) {
264
+ let lastError;
265
+
266
+ for (let i = 0; i < urls.length; i++) {
267
+ const url = urls[i];
268
+
269
+ try {
270
+ if (isWindows) {
271
+ await downloadWindowsBinary(url, destPath);
272
+ } else {
273
+ await downloadBinary(url, destPath);
274
+ }
275
+ return;
276
+ } catch (error) {
277
+ lastError = error;
278
+ if (i < urls.length - 1) {
279
+ substep("Falling back to GitHub releases...");
280
+ }
281
+ }
282
+ }
283
+
284
+ throw lastError;
285
+ }
286
+
165
287
  async function main() {
166
288
  const platform = PLATFORM_MAP[process.platform];
167
289
  const arch = ARCH_MAP[process.arch];
168
290
 
169
291
  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
292
+ console.log();
293
+ console.log(
294
+ ` ${colors.yellow}!${colors.reset} Unsupported platform: ${process.platform}-${process.arch}`,
295
+ );
296
+ console.log(` Build from source or download manually.`);
297
+ console.log();
298
+ process.exit(0);
173
299
  }
174
300
 
175
301
  const version = getVersion();
@@ -182,36 +308,52 @@ async function main() {
182
308
 
183
309
  // Skip if binary already exists
184
310
  if (fs.existsSync(destPath)) {
185
- console.log(`Binary already exists: ${destPath}`);
311
+ console.log();
312
+ console.log(` ${colors.dim}Binary already installed${colors.reset}`);
313
+ console.log();
186
314
  return;
187
315
  }
188
316
 
317
+ printHeader();
318
+
319
+ step(`Platform: ${colors.bold}${platform}-${arch}${colors.reset}`);
320
+ step(`Version: ${colors.bold}v${version}${colors.reset}`);
321
+ console.log();
322
+
189
323
  // Ensure bin directory exists
190
324
  if (!fs.existsSync(binDir)) {
191
325
  fs.mkdirSync(binDir, { recursive: true });
192
326
  }
193
327
 
194
328
  const archiveExt = platform === "windows" ? "zip" : "tar.gz";
195
- const downloadUrl = `${GITHUB_RELEASES}/download/v${version}/${binaryName}.${archiveExt}`;
329
+
330
+ const urls = [
331
+ `${R2_CDN}/v${version}/${binaryName}.${archiveExt}`,
332
+ `${GITHUB_RELEASES}/download/v${version}/${binaryName}.${archiveExt}`,
333
+ ];
196
334
 
197
335
  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}`);
336
+ step("Downloading binary...");
337
+ await downloadWithFallback(urls, destPath, platform === "windows");
338
+
339
+ step("Extracting...");
340
+ success(`toolui v${version} ready`);
341
+
342
+ console.log(` ${colors.dim}Get started:${colors.reset}`);
343
+ console.log(` ${colors.cyan}$${colors.reset} toolui init`);
344
+ console.log(` ${colors.cyan}$${colors.reset} toolui`);
345
+ console.log();
204
346
  } 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(
347
+ printError(`Download failed: ${error.message}`);
348
+ console.log();
349
+ console.log(` ${colors.dim}Manual download:${colors.reset}`);
350
+ console.log(` ${GITHUB_RELEASES}/latest`);
351
+ console.log();
352
+ console.log(` ${colors.dim}Or use install script:${colors.reset}`);
353
+ console.log(
212
354
  ` curl -fsSL https://raw.githubusercontent.com/${REPO}/main/install.sh | bash`,
213
355
  );
214
- // Don't fail the install - the user can still use the package
356
+ console.log();
215
357
  process.exit(0);
216
358
  }
217
359
  }