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.
- package/bin/hopsule +0 -0
- package/install.js +150 -15
- 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
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
res.on("
|
|
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",
|
|
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(`
|
|
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(`
|
|
146
|
-
console.log(`
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
208
|
-
console.error(`
|
|
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
|
});
|