@tomagranate/toolui 0.2.6 → 0.3.1

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.1",
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");
@@ -27,44 +26,136 @@ const ARCH_MAP = {
27
26
  x64: "x64",
28
27
  };
29
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
+
30
41
  /**
31
- * Make an HTTPS GET request
42
+ * Print styled header
32
43
  */
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
- }
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} │ toolui postinstall │${colors.reset}`,
51
+ );
52
+ console.log(
53
+ `${colors.cyan}${colors.bold} ╰─────────────────────────────────╯${colors.reset}`,
54
+ );
55
+ console.log();
56
+ }
55
57
 
56
- if (res.statusCode !== 200) {
57
- reject(new Error(`HTTP ${res.statusCode}: ${url}`));
58
- return;
59
- }
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
+ }
60
128
 
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);
129
+ /**
130
+ * Download file with streaming and progress
131
+ */
132
+ async function downloadWithProgress(url) {
133
+ const response = await fetch(url, {
134
+ headers: { "User-Agent": "toolui-postinstall" },
67
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);
68
159
  }
69
160
 
70
161
  /**
@@ -78,13 +169,9 @@ function getVersion() {
78
169
  }
79
170
 
80
171
  /**
81
- * Download and extract the binary
172
+ * Extract tar.gz archive to get the binary
82
173
  */
83
- async function downloadBinary(url, destPath) {
84
- console.log(`Downloading from ${url}...`);
85
- const data = await httpsGet(url);
86
-
87
- // Extract tar.gz
174
+ function extractTarGz(data, destPath) {
88
175
  return new Promise((resolve, reject) => {
89
176
  const gunzip = zlib.createGunzip();
90
177
  const chunks = [];
@@ -92,20 +179,16 @@ async function downloadBinary(url, destPath) {
92
179
  gunzip.on("data", (chunk) => chunks.push(chunk));
93
180
  gunzip.on("end", () => {
94
181
  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
182
  let offset = 0;
98
183
  while (offset < tarData.length) {
99
184
  const header = tarData.slice(offset, offset + 512);
100
- if (header[0] === 0) break; // End of archive
185
+ if (header[0] === 0) break;
101
186
 
102
- // Get filename (bytes 0-99)
103
187
  const filename = header
104
188
  .slice(0, 100)
105
189
  .toString("utf-8")
106
190
  .replace(/\0/g, "");
107
191
 
108
- // Get file size (bytes 124-135, octal)
109
192
  const sizeStr = header
110
193
  .slice(124, 136)
111
194
  .toString("utf-8")
@@ -113,7 +196,7 @@ async function downloadBinary(url, destPath) {
113
196
  .trim();
114
197
  const size = parseInt(sizeStr, 8) || 0;
115
198
 
116
- offset += 512; // Move past header
199
+ offset += 512;
117
200
 
118
201
  if (filename && size > 0 && filename.startsWith("toolui")) {
119
202
  const content = tarData.slice(offset, offset + size);
@@ -123,10 +206,9 @@ async function downloadBinary(url, destPath) {
123
206
  return;
124
207
  }
125
208
 
126
- // Move to next file (content is padded to 512-byte blocks)
127
209
  offset += Math.ceil(size / 512) * 512;
128
210
  }
129
- reject(new Error("Could not find toolui binary in archive"));
211
+ reject(new Error("Binary not found in archive"));
130
212
  });
131
213
  gunzip.on("error", reject);
132
214
 
@@ -135,17 +217,22 @@ async function downloadBinary(url, destPath) {
135
217
  }
136
218
 
137
219
  /**
138
- * Download Windows zip and extract
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
139
229
  */
140
230
  async function downloadWindowsBinary(url, destPath) {
141
- console.log(`Downloading from ${url}...`);
142
- const data = await httpsGet(url);
231
+ const data = await downloadWithProgress(url);
143
232
 
144
- // Write zip to temp file
145
233
  const zipPath = `${destPath}.zip`;
146
234
  fs.writeFileSync(zipPath, data);
147
235
 
148
- // Use unzip if available, otherwise try PowerShell
149
236
  try {
150
237
  if (process.platform === "win32") {
151
238
  execSync(
@@ -162,14 +249,29 @@ async function downloadWindowsBinary(url, destPath) {
162
249
  }
163
250
  }
164
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
+
165
263
  async function main() {
166
264
  const platform = PLATFORM_MAP[process.platform];
167
265
  const arch = ARCH_MAP[process.arch];
168
266
 
169
267
  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
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);
173
275
  }
174
276
 
175
277
  const version = getVersion();
@@ -182,36 +284,48 @@ async function main() {
182
284
 
183
285
  // Skip if binary already exists
184
286
  if (fs.existsSync(destPath)) {
185
- console.log(`Binary already exists: ${destPath}`);
287
+ console.log();
288
+ console.log(` ${colors.dim}Binary already installed${colors.reset}`);
289
+ console.log();
186
290
  return;
187
291
  }
188
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
+
189
299
  // Ensure bin directory exists
190
300
  if (!fs.existsSync(binDir)) {
191
301
  fs.mkdirSync(binDir, { recursive: true });
192
302
  }
193
303
 
194
304
  const archiveExt = platform === "windows" ? "zip" : "tar.gz";
195
- const downloadUrl = `${GITHUB_RELEASES}/download/v${version}/${binaryName}.${archiveExt}`;
305
+ const url = `${GITHUB_RELEASES}/download/v${version}/${binaryName}.${archiveExt}`;
196
306
 
197
307
  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}`);
308
+ step("Downloading binary...");
309
+ await downloadFromUrl(url, destPath, platform === "windows");
310
+
311
+ step("Extracting...");
312
+ success(`toolui v${version} ready`);
313
+
314
+ console.log(` ${colors.dim}Get started:${colors.reset}`);
315
+ console.log(` ${colors.cyan}$${colors.reset} toolui init`);
316
+ console.log(` ${colors.cyan}$${colors.reset} toolui`);
317
+ console.log();
204
318
  } 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(
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(
212
326
  ` curl -fsSL https://raw.githubusercontent.com/${REPO}/main/install.sh | bash`,
213
327
  );
214
- // Don't fail the install - the user can still use the package
328
+ console.log();
215
329
  process.exit(0);
216
330
  }
217
331
  }