hopsule 0.9.1 → 0.9.3

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.
Files changed (3) hide show
  1. package/bin/hopsule +0 -0
  2. package/install.js +150 -15
  3. package/package.json +1 -1
package/bin/hopsule CHANGED
Binary file
package/install.js CHANGED
@@ -12,6 +12,7 @@ const PACKAGE = require("./package.json");
12
12
  const VERSION = PACKAGE.version;
13
13
  const REPO = "Hopsule/cli-tool";
14
14
  const BINARY_NAME = "hopsule";
15
+ const DOWNLOAD_TIMEOUT_MS = 120_000;
15
16
 
16
17
  const PLATFORM_MAP = {
17
18
  darwin: "darwin",
@@ -24,6 +25,43 @@ const ARCH_MAP = {
24
25
  arm64: "arm64",
25
26
  };
26
27
 
28
+ // ── Pretty output helpers ───────────────────────────────────────────
29
+
30
+ const CYAN = "\x1b[36m";
31
+ const GREEN = "\x1b[32m";
32
+ const DIM = "\x1b[2m";
33
+ const BOLD = "\x1b[1m";
34
+ const RESET = "\x1b[0m";
35
+ const RED = "\x1b[31m";
36
+ const YELLOW = "\x1b[33m";
37
+
38
+ function formatBytes(bytes) {
39
+ if (bytes < 1024) return `${bytes} B`;
40
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
41
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
42
+ }
43
+
44
+ function formatSpeed(bytesPerSec) {
45
+ if (bytesPerSec < 1024) return `${bytesPerSec.toFixed(0)} B/s`;
46
+ if (bytesPerSec < 1024 * 1024) return `${(bytesPerSec / 1024).toFixed(1)} KB/s`;
47
+ return `${(bytesPerSec / (1024 * 1024)).toFixed(1)} MB/s`;
48
+ }
49
+
50
+ function drawProgressBar(percent, width = 30) {
51
+ const filled = Math.round(width * percent);
52
+ const empty = width - filled;
53
+ const bar = "█".repeat(filled) + "░".repeat(empty);
54
+ return bar;
55
+ }
56
+
57
+ function clearLine() {
58
+ if (process.stderr.isTTY) {
59
+ process.stderr.write("\r\x1b[K");
60
+ }
61
+ }
62
+
63
+ // ── Platform detection ──────────────────────────────────────────────
64
+
27
65
  function getPlatform() {
28
66
  const platform = PLATFORM_MAP[os.platform()];
29
67
  if (!platform) {
@@ -57,37 +95,111 @@ function getBinaryPath() {
57
95
  return path.join(binDir, `${BINARY_NAME}${ext}`);
58
96
  }
59
97
 
98
+ // ── Download with progress ──────────────────────────────────────────
99
+
60
100
  function downloadFile(url) {
61
101
  return new Promise((resolve, reject) => {
102
+ const startTime = Date.now();
103
+ let timer = null;
104
+
105
+ const cleanup = () => {
106
+ if (timer) clearTimeout(timer);
107
+ };
108
+
62
109
  const follow = (url, redirects = 0) => {
63
110
  if (redirects > 10) {
111
+ cleanup();
64
112
  return reject(new Error("Too many redirects"));
65
113
  }
66
114
 
67
- https
115
+ timer = setTimeout(() => {
116
+ reject(new Error(`Download timed out after ${DOWNLOAD_TIMEOUT_MS / 1000}s`));
117
+ }, DOWNLOAD_TIMEOUT_MS);
118
+
119
+ const req = https
68
120
  .get(url, { headers: { "User-Agent": "hopsule-npm-installer" } }, (res) => {
69
121
  if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
122
+ clearTimeout(timer);
70
123
  return follow(res.headers.location, redirects + 1);
71
124
  }
72
125
 
73
126
  if (res.statusCode !== 200) {
127
+ cleanup();
74
128
  return reject(
75
129
  new Error(`Download failed: HTTP ${res.statusCode} from ${url}`)
76
130
  );
77
131
  }
78
132
 
133
+ const totalSize = parseInt(res.headers["content-length"], 10) || 0;
134
+ let downloaded = 0;
79
135
  const chunks = [];
80
- res.on("data", (chunk) => chunks.push(chunk));
81
- res.on("end", () => resolve(Buffer.concat(chunks)));
82
- res.on("error", reject);
136
+ let lastUpdate = Date.now();
137
+
138
+ res.on("data", (chunk) => {
139
+ chunks.push(chunk);
140
+ downloaded += chunk.length;
141
+
142
+ const now = Date.now();
143
+ if (now - lastUpdate < 100) return;
144
+ lastUpdate = now;
145
+
146
+ const elapsed = (now - startTime) / 1000;
147
+ const speed = downloaded / elapsed;
148
+ const percent = totalSize > 0 ? downloaded / totalSize : 0;
149
+
150
+ if (process.stderr.isTTY) {
151
+ clearLine();
152
+ if (totalSize > 0) {
153
+ const bar = drawProgressBar(percent);
154
+ const pct = (percent * 100).toFixed(0).padStart(3);
155
+ process.stderr.write(
156
+ ` ${CYAN}${bar}${RESET} ${pct}% ${DIM}${formatBytes(downloaded)}/${formatBytes(totalSize)}${RESET} ${DIM}${formatSpeed(speed)}${RESET}`
157
+ );
158
+ } else {
159
+ const spinner = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
160
+ const frame = spinner[Math.floor(elapsed * 4) % spinner.length];
161
+ process.stderr.write(
162
+ ` ${CYAN}${frame}${RESET} Downloading... ${DIM}${formatBytes(downloaded)}${RESET} ${DIM}${formatSpeed(speed)}${RESET}`
163
+ );
164
+ }
165
+ }
166
+ });
167
+
168
+ res.on("end", () => {
169
+ cleanup();
170
+ if (process.stderr.isTTY) {
171
+ clearLine();
172
+ const elapsed = (Date.now() - startTime) / 1000;
173
+ process.stderr.write(
174
+ ` ${GREEN}${drawProgressBar(1)}${RESET} 100% ${DIM}${formatBytes(downloaded)} in ${elapsed.toFixed(1)}s${RESET}\n`
175
+ );
176
+ }
177
+ resolve(Buffer.concat(chunks));
178
+ });
179
+
180
+ res.on("error", (err) => {
181
+ cleanup();
182
+ reject(err);
183
+ });
83
184
  })
84
- .on("error", reject);
185
+ .on("error", (err) => {
186
+ cleanup();
187
+ reject(err);
188
+ });
189
+
190
+ req.on("timeout", () => {
191
+ req.destroy();
192
+ cleanup();
193
+ reject(new Error("Connection timed out"));
194
+ });
85
195
  };
86
196
 
87
197
  follow(url);
88
198
  });
89
199
  }
90
200
 
201
+ // ── Extraction ──────────────────────────────────────────────────────
202
+
91
203
  async function extractTarGz(buffer, destDir) {
92
204
  const tmpFile = path.join(os.tmpdir(), `hopsule-${Date.now()}.tar.gz`);
93
205
  fs.writeFileSync(tmpFile, buffer);
@@ -121,6 +233,8 @@ async function extractZip(buffer, destDir) {
121
233
  }
122
234
  }
123
235
 
236
+ // ── Main install ────────────────────────────────────────────────────
237
+
124
238
  async function install() {
125
239
  const platform = getPlatform();
126
240
  const arch = getArch();
@@ -128,6 +242,11 @@ async function install() {
128
242
  const binDir = path.join(__dirname, "bin");
129
243
  const binaryPath = getBinaryPath();
130
244
 
245
+ console.log("");
246
+ console.log(` ${BOLD}${CYAN}Hopsule${RESET} ${DIM}v${VERSION}${RESET}`);
247
+ console.log(` ${DIM}Decision & Memory Layer for AI teams${RESET}`);
248
+ console.log("");
249
+
131
250
  // Skip if binary already exists with correct version
132
251
  if (fs.existsSync(binaryPath)) {
133
252
  try {
@@ -136,14 +255,16 @@ async function install() {
136
255
  stdio: "pipe",
137
256
  });
138
257
  if (output.includes(VERSION)) {
139
- console.log(`hopsule v${VERSION} already installed.`);
258
+ console.log(` ${GREEN}✓${RESET} Already installed ${DIM}(v${VERSION})${RESET}`);
259
+ console.log("");
140
260
  return;
141
261
  }
142
262
  } catch (_) {}
143
263
  }
144
264
 
145
- console.log(`Installing hopsule v${VERSION} for ${platform}/${arch}...`);
146
- console.log(`Downloading from: ${url}`);
265
+ console.log(` ${DIM}Platform:${RESET} ${platform}/${arch}`);
266
+ console.log(` ${DIM}Source:${RESET} github.com/${REPO}`);
267
+ console.log("");
147
268
 
148
269
  const buffer = await downloadFile(url);
149
270
 
@@ -152,7 +273,8 @@ async function install() {
152
273
  fs.mkdirSync(binDir, { recursive: true });
153
274
  }
154
275
 
155
- // Extract based on platform
276
+ // Extract
277
+ process.stderr.write(` ${DIM}Extracting...${RESET}`);
156
278
  const tmpExtractDir = path.join(os.tmpdir(), `hopsule-extract-${Date.now()}`);
157
279
  fs.mkdirSync(tmpExtractDir, { recursive: true });
158
280
 
@@ -168,7 +290,6 @@ async function install() {
168
290
  const extractedBinary = path.join(tmpExtractDir, `${BINARY_NAME}${ext}`);
169
291
 
170
292
  if (!fs.existsSync(extractedBinary)) {
171
- // Some archives nest in a directory
172
293
  const files = fs.readdirSync(tmpExtractDir);
173
294
  let found = false;
174
295
  for (const f of files) {
@@ -193,7 +314,16 @@ async function install() {
193
314
  fs.chmodSync(binaryPath, 0o755);
194
315
  }
195
316
 
196
- console.log(`hopsule v${VERSION} installed successfully!`);
317
+ clearLine();
318
+ console.log(` ${GREEN}✓${RESET} Extracted`);
319
+ console.log("");
320
+ console.log(` ${GREEN}${BOLD}✓ hopsule v${VERSION} installed successfully!${RESET}`);
321
+ console.log("");
322
+ console.log(` ${DIM}Get started:${RESET}`);
323
+ console.log(` ${CYAN}hopsule login${RESET} ${DIM}Sign in to your account${RESET}`);
324
+ console.log(` ${CYAN}hopsule init${RESET} ${DIM}Initialize a project${RESET}`);
325
+ console.log(` ${CYAN}hopsule${RESET} ${DIM}Open interactive TUI${RESET}`);
326
+ console.log("");
197
327
  } finally {
198
328
  try {
199
329
  fs.rmSync(tmpExtractDir, { recursive: true, force: true });
@@ -202,11 +332,16 @@ async function install() {
202
332
  }
203
333
 
204
334
  install().catch((err) => {
205
- console.error(`Failed to install hopsule: ${err.message}`);
335
+ clearLine();
336
+ console.error("");
337
+ console.error(` ${RED}${BOLD}✗ Failed to install hopsule${RESET}`);
338
+ console.error(` ${RED}${err.message}${RESET}`);
339
+ console.error("");
340
+ console.error(` ${YELLOW}Install manually:${RESET}`);
341
+ console.error(` ${DIM}https://github.com/${REPO}/releases/tag/v${VERSION}${RESET}`);
206
342
  console.error("");
207
- console.error("You can install manually from:");
208
- console.error(` https://github.com/${REPO}/releases/tag/v${VERSION}`);
343
+ console.error(` ${YELLOW}Or use Homebrew:${RESET}`);
344
+ console.error(` ${CYAN}brew install hopsule/tap/hopsule${RESET}`);
209
345
  console.error("");
210
- console.error("Or use Homebrew: brew install hopsule/tap/hopsule");
211
346
  process.exit(1);
212
347
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hopsule",
3
- "version": "0.9.1",
3
+ "version": "0.9.3",
4
4
  "description": "Decision-first, context-aware, portable memory system CLI",
5
5
  "keywords": [
6
6
  "cli",