@0xobelisk/sui-cli 1.2.0-pre.1 → 1.2.0-pre.100
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/README.md +3 -3
- package/dist/dubhe.js +125 -66
- package/dist/dubhe.js.map +1 -1
- package/package.json +31 -19
- package/src/commands/build.ts +47 -16
- package/src/commands/call.ts +83 -83
- package/src/commands/checkBalance.ts +12 -5
- package/src/commands/configStore.ts +12 -4
- package/src/commands/convertJson.ts +70 -0
- package/src/commands/doctor.ts +1515 -0
- package/src/commands/faucet.ts +11 -7
- package/src/commands/generateKey.ts +3 -2
- package/src/commands/index.ts +16 -7
- package/src/commands/info.ts +55 -0
- package/src/commands/loadMetadata.ts +57 -0
- package/src/commands/localnode.ts +22 -12
- package/src/commands/publish.ts +21 -7
- package/src/commands/query.ts +101 -101
- package/src/commands/schemagen.ts +15 -4
- package/src/commands/shell.ts +198 -0
- package/src/commands/switchEnv.ts +26 -0
- package/src/commands/test.ts +54 -11
- package/src/commands/upgrade.ts +11 -4
- package/src/commands/wait.ts +333 -22
- package/src/commands/watch.ts +2 -1
- package/src/dubhe.ts +12 -4
- package/src/utils/axios-downloader.ts +116 -0
- package/src/utils/callHandler.ts +118 -118
- package/src/utils/constants.ts +5 -0
- package/src/utils/generateAccount.ts +1 -1
- package/src/utils/index.ts +4 -3
- package/src/utils/metadataHandler.ts +16 -0
- package/src/utils/publishHandler.ts +295 -290
- package/src/utils/queryStorage.ts +141 -141
- package/src/utils/startNode.ts +165 -108
- package/src/utils/storeConfig.ts +6 -12
- package/src/utils/upgradeHandler.ts +147 -86
- package/src/utils/utils.ts +771 -54
package/src/commands/wait.ts
CHANGED
|
@@ -2,56 +2,367 @@ import type { CommandModule } from 'yargs';
|
|
|
2
2
|
import waitOn from 'wait-on';
|
|
3
3
|
import ora from 'ora';
|
|
4
4
|
import chalk from 'chalk';
|
|
5
|
+
import net from 'net';
|
|
6
|
+
import { handlerExit } from './shell';
|
|
5
7
|
|
|
6
8
|
interface WaitOptions {
|
|
7
|
-
url
|
|
9
|
+
url?: string;
|
|
10
|
+
localnet?: boolean;
|
|
11
|
+
'local-database'?: boolean;
|
|
12
|
+
'local-node'?: boolean;
|
|
13
|
+
'local-indexer'?: boolean;
|
|
8
14
|
timeout: number;
|
|
9
15
|
interval: number;
|
|
10
16
|
}
|
|
11
17
|
|
|
18
|
+
async function withoutProxy<T>(fn: () => Promise<T>): Promise<T> {
|
|
19
|
+
const originalProxy = {
|
|
20
|
+
HTTP_PROXY: process.env.HTTP_PROXY,
|
|
21
|
+
HTTPS_PROXY: process.env.HTTPS_PROXY,
|
|
22
|
+
http_proxy: process.env.http_proxy,
|
|
23
|
+
https_proxy: process.env.https_proxy,
|
|
24
|
+
NO_PROXY: process.env.NO_PROXY,
|
|
25
|
+
no_proxy: process.env.no_proxy
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
delete process.env.HTTP_PROXY;
|
|
29
|
+
delete process.env.HTTPS_PROXY;
|
|
30
|
+
delete process.env.http_proxy;
|
|
31
|
+
delete process.env.https_proxy;
|
|
32
|
+
process.env.NO_PROXY = '127.0.0.1,localhost,*.local';
|
|
33
|
+
process.env.no_proxy = '127.0.0.1,localhost,*.local';
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
return await fn();
|
|
37
|
+
} finally {
|
|
38
|
+
Object.keys(originalProxy).forEach((key) => {
|
|
39
|
+
const value = originalProxy[key as keyof typeof originalProxy];
|
|
40
|
+
if (value !== undefined) {
|
|
41
|
+
process.env[key] = value;
|
|
42
|
+
} else {
|
|
43
|
+
delete process.env[key];
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Check if PostgreSQL port is occupied (service is running)
|
|
50
|
+
async function checkPostgreSQLRunning(): Promise<boolean> {
|
|
51
|
+
return checkPortRunning(5432);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Generic port checking function
|
|
55
|
+
async function checkPortRunning(port: number, host: string = '127.0.0.1'): Promise<boolean> {
|
|
56
|
+
return new Promise((resolve) => {
|
|
57
|
+
const socket = new net.Socket();
|
|
58
|
+
let isConnected = false;
|
|
59
|
+
|
|
60
|
+
// Set timeout for connection attempt
|
|
61
|
+
const timeout = setTimeout(() => {
|
|
62
|
+
socket.destroy();
|
|
63
|
+
if (!isConnected) {
|
|
64
|
+
resolve(false);
|
|
65
|
+
}
|
|
66
|
+
}, 2000);
|
|
67
|
+
|
|
68
|
+
socket.connect(port, host, () => {
|
|
69
|
+
isConnected = true;
|
|
70
|
+
clearTimeout(timeout);
|
|
71
|
+
socket.destroy();
|
|
72
|
+
resolve(true); // Port is occupied, service is running
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
socket.on('error', () => {
|
|
76
|
+
clearTimeout(timeout);
|
|
77
|
+
if (!isConnected) {
|
|
78
|
+
resolve(false); // Connection failed, service not running
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Check indexer health endpoint
|
|
85
|
+
async function checkIndexerHealth(): Promise<boolean> {
|
|
86
|
+
try {
|
|
87
|
+
const controller = new AbortController();
|
|
88
|
+
const timeout = setTimeout(() => controller.abort(), 2000);
|
|
89
|
+
|
|
90
|
+
const response = await fetch('http://127.0.0.1:8080/health', {
|
|
91
|
+
signal: controller.signal,
|
|
92
|
+
headers: {
|
|
93
|
+
Accept: 'application/json'
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
clearTimeout(timeout);
|
|
98
|
+
return response.status === 200;
|
|
99
|
+
} catch {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Wait for all localnet services with custom checks
|
|
105
|
+
async function waitForLocalnetServices(options: WaitOptions): Promise<void> {
|
|
106
|
+
const spinner = ora({
|
|
107
|
+
text: 'Waiting for dubhe localnet services...',
|
|
108
|
+
color: 'cyan'
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
spinner.start();
|
|
112
|
+
|
|
113
|
+
const startTime = Date.now();
|
|
114
|
+
|
|
115
|
+
while (Date.now() - startTime < options.timeout) {
|
|
116
|
+
try {
|
|
117
|
+
// Check HTTP services using wait-on (excluding 9000 port)
|
|
118
|
+
await withoutProxy(() =>
|
|
119
|
+
waitOn({
|
|
120
|
+
resources: [
|
|
121
|
+
'http://127.0.0.1:9123', // Sui faucet
|
|
122
|
+
'http://127.0.0.1:4000' // GraphQL server
|
|
123
|
+
],
|
|
124
|
+
timeout: options.interval,
|
|
125
|
+
interval: 500,
|
|
126
|
+
validateStatus: (status: number) => status === 200
|
|
127
|
+
})
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
// Check PostgreSQL separately
|
|
131
|
+
const postgresRunning = await checkPostgreSQLRunning();
|
|
132
|
+
|
|
133
|
+
if (postgresRunning) {
|
|
134
|
+
spinner.succeed(chalk.green('All dubhe localnet services are ready!'));
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
} catch (_error) {
|
|
138
|
+
// Continue waiting...
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Wait before next check
|
|
142
|
+
await new Promise((resolve) => setTimeout(resolve, options.interval));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Timeout reached
|
|
146
|
+
throw new Error('Timeout waiting for services');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Wait for local database
|
|
150
|
+
async function waitForLocalDatabase(options: WaitOptions): Promise<void> {
|
|
151
|
+
const spinner = ora({
|
|
152
|
+
text: 'Waiting for local database...',
|
|
153
|
+
color: 'cyan'
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
spinner.start();
|
|
157
|
+
|
|
158
|
+
const startTime = Date.now();
|
|
159
|
+
|
|
160
|
+
while (Date.now() - startTime < options.timeout) {
|
|
161
|
+
const isRunning = await checkPostgreSQLRunning();
|
|
162
|
+
|
|
163
|
+
if (isRunning) {
|
|
164
|
+
spinner.succeed(chalk.green('Local database is ready!'));
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Wait before next check
|
|
169
|
+
await new Promise((resolve) => setTimeout(resolve, options.interval));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Timeout reached
|
|
173
|
+
throw new Error('Timeout waiting for local database');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Wait for local Sui node
|
|
177
|
+
async function waitForLocalNode(options: WaitOptions): Promise<void> {
|
|
178
|
+
const spinner = ora({
|
|
179
|
+
text: 'Waiting for local Sui node...',
|
|
180
|
+
color: 'cyan'
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
spinner.start();
|
|
184
|
+
|
|
185
|
+
const startTime = Date.now();
|
|
186
|
+
|
|
187
|
+
while (Date.now() - startTime < options.timeout) {
|
|
188
|
+
const isRunning = await checkPortRunning(9123);
|
|
189
|
+
|
|
190
|
+
if (isRunning) {
|
|
191
|
+
spinner.succeed(chalk.green('Local Sui node is ready!'));
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Wait before next check
|
|
196
|
+
await new Promise((resolve) => setTimeout(resolve, options.interval));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Timeout reached
|
|
200
|
+
throw new Error('Timeout waiting for local Sui node');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Wait for local indexer
|
|
204
|
+
async function waitForLocalIndexer(options: WaitOptions): Promise<void> {
|
|
205
|
+
const spinner = ora({
|
|
206
|
+
text: 'Waiting for local indexer...',
|
|
207
|
+
color: 'cyan'
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
spinner.start();
|
|
211
|
+
|
|
212
|
+
const startTime = Date.now();
|
|
213
|
+
|
|
214
|
+
while (Date.now() - startTime < options.timeout) {
|
|
215
|
+
const isRunning = await checkIndexerHealth();
|
|
216
|
+
|
|
217
|
+
if (isRunning) {
|
|
218
|
+
spinner.succeed(chalk.green('Local indexer is ready!'));
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Wait before next check
|
|
223
|
+
await new Promise((resolve) => setTimeout(resolve, options.interval));
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Timeout reached
|
|
227
|
+
throw new Error('Timeout waiting for local indexer');
|
|
228
|
+
}
|
|
229
|
+
|
|
12
230
|
const commandModule: CommandModule = {
|
|
13
231
|
command: 'wait',
|
|
14
|
-
describe: 'Wait for service to be ready',
|
|
232
|
+
describe: 'Wait for service(s) to be ready',
|
|
15
233
|
builder(yargs) {
|
|
16
234
|
return yargs
|
|
17
235
|
.option('url', {
|
|
18
236
|
type: 'string',
|
|
19
|
-
description: 'URL to wait for'
|
|
237
|
+
description: 'URL to wait for (single service)'
|
|
238
|
+
})
|
|
239
|
+
.option('localnet', {
|
|
240
|
+
type: 'boolean',
|
|
241
|
+
description:
|
|
242
|
+
'Wait for all dubhe localnet services (sui localnode:9000&9123, postgres:5432, graphql:4000)',
|
|
243
|
+
default: false
|
|
244
|
+
})
|
|
245
|
+
.option('local-database', {
|
|
246
|
+
type: 'boolean',
|
|
247
|
+
description: 'Wait for local database (PostgreSQL on port 5432)',
|
|
248
|
+
default: false
|
|
249
|
+
})
|
|
250
|
+
.option('local-node', {
|
|
251
|
+
type: 'boolean',
|
|
252
|
+
description: 'Wait for local Sui node (port 9123)',
|
|
253
|
+
default: false
|
|
254
|
+
})
|
|
255
|
+
.option('local-indexer', {
|
|
256
|
+
type: 'boolean',
|
|
257
|
+
description: 'Wait for local indexer (health check at http://127.0.0.1:8080/health)',
|
|
258
|
+
default: false
|
|
20
259
|
})
|
|
21
260
|
.option('timeout', {
|
|
22
261
|
type: 'number',
|
|
23
262
|
description: 'Timeout (in milliseconds)',
|
|
24
|
-
default:
|
|
263
|
+
default: 24 * 60 * 60 * 1000 // 24 hours, effectively no timeout
|
|
25
264
|
})
|
|
26
265
|
.option('interval', {
|
|
27
266
|
type: 'number',
|
|
28
267
|
description: 'Check interval (in milliseconds)',
|
|
29
268
|
default: 1000
|
|
269
|
+
})
|
|
270
|
+
.check((argv) => {
|
|
271
|
+
const hasUrl = !!argv.url;
|
|
272
|
+
const hasLocalnet = !!argv.localnet;
|
|
273
|
+
const hasLocalDatabase = !!argv['local-database'];
|
|
274
|
+
const hasLocalNode = !!argv['local-node'];
|
|
275
|
+
const hasLocalIndexer = !!argv['local-indexer'];
|
|
276
|
+
|
|
277
|
+
const optionCount = [
|
|
278
|
+
hasUrl,
|
|
279
|
+
hasLocalnet,
|
|
280
|
+
hasLocalDatabase,
|
|
281
|
+
hasLocalNode,
|
|
282
|
+
hasLocalIndexer
|
|
283
|
+
].filter(Boolean).length;
|
|
284
|
+
|
|
285
|
+
if (optionCount === 0) {
|
|
286
|
+
throw new Error(
|
|
287
|
+
'Please provide at least one option: --url, --localnet, --local-database, --local-node, or --local-indexer'
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (hasUrl && optionCount > 1) {
|
|
292
|
+
throw new Error('Cannot use --url together with other options');
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (hasLocalnet && (hasLocalDatabase || hasLocalNode || hasLocalIndexer)) {
|
|
296
|
+
throw new Error('Cannot use --localnet together with individual service options');
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return true;
|
|
30
300
|
});
|
|
31
301
|
},
|
|
32
302
|
async handler(argv) {
|
|
33
303
|
const options = argv as unknown as WaitOptions;
|
|
34
|
-
const spinner = ora({
|
|
35
|
-
text: `Waiting for service to start ${chalk.cyan(options.url)}...`,
|
|
36
|
-
color: 'cyan'
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
spinner.start();
|
|
40
304
|
|
|
41
305
|
try {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
306
|
+
if (options.localnet) {
|
|
307
|
+
await waitForLocalnetServices(options);
|
|
308
|
+
} else if (options['local-database']) {
|
|
309
|
+
await waitForLocalDatabase(options);
|
|
310
|
+
} else if (options['local-node']) {
|
|
311
|
+
await waitForLocalNode(options);
|
|
312
|
+
} else if (options['local-indexer']) {
|
|
313
|
+
await waitForLocalIndexer(options);
|
|
314
|
+
} else {
|
|
315
|
+
// Single URL mode - use original wait-on logic
|
|
316
|
+
const spinner = ora({
|
|
317
|
+
text: `Waiting for ${options.url}...`,
|
|
318
|
+
color: 'cyan'
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
spinner.start();
|
|
322
|
+
|
|
323
|
+
await withoutProxy(() =>
|
|
324
|
+
waitOn({
|
|
325
|
+
resources: [options.url!],
|
|
326
|
+
timeout: options.timeout,
|
|
327
|
+
interval: options.interval,
|
|
328
|
+
validateStatus: (status: number) => status === 200
|
|
329
|
+
})
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
spinner.succeed(chalk.green('Service is ready!'));
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
handlerExit();
|
|
336
|
+
} catch (_error) {
|
|
337
|
+
const spinner = ora();
|
|
338
|
+
|
|
339
|
+
let errorMessage = 'Timeout waiting for service';
|
|
340
|
+
let helpMessage = 'Please make sure the service is running...';
|
|
341
|
+
|
|
342
|
+
if (options.localnet) {
|
|
343
|
+
errorMessage = 'Timeout waiting for dubhe localnet services';
|
|
344
|
+
helpMessage =
|
|
345
|
+
'Please make sure all required services are running:\n' +
|
|
346
|
+
'- Sui localnode on port 9000\n' +
|
|
347
|
+
'- Sui faucet on port 9123\n' +
|
|
348
|
+
'- PostgreSQL database on port 5432\n' +
|
|
349
|
+
'- Dubhe GraphQL server on port 4000';
|
|
350
|
+
} else if (options['local-database']) {
|
|
351
|
+
errorMessage = 'Timeout waiting for local database';
|
|
352
|
+
helpMessage = 'Please make sure PostgreSQL is running on port 5432';
|
|
353
|
+
} else if (options['local-node']) {
|
|
354
|
+
errorMessage = 'Timeout waiting for local Sui node';
|
|
355
|
+
helpMessage = 'Please make sure Sui localnode is running on port 9123';
|
|
356
|
+
} else if (options['local-indexer']) {
|
|
357
|
+
errorMessage = 'Timeout waiting for local indexer';
|
|
358
|
+
helpMessage =
|
|
359
|
+
'Please make sure indexer is running and health endpoint is available at http://127.0.0.1:8080/health';
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
spinner.fail(chalk.red(errorMessage));
|
|
363
|
+
console.error(chalk.yellow(helpMessage));
|
|
48
364
|
|
|
49
|
-
|
|
50
|
-
process.exit(0);
|
|
51
|
-
} catch (error) {
|
|
52
|
-
spinner.fail(chalk.red('Timeout waiting for service'));
|
|
53
|
-
console.error(chalk.yellow('Please make sure the service is running...'));
|
|
54
|
-
process.exit(1);
|
|
365
|
+
handlerExit(1);
|
|
55
366
|
}
|
|
56
367
|
}
|
|
57
368
|
};
|
package/src/commands/watch.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { CommandModule } from 'yargs';
|
|
2
2
|
import chokidar from 'chokidar';
|
|
3
3
|
import { exec } from 'child_process';
|
|
4
|
+
import { handlerExit } from './shell';
|
|
4
5
|
|
|
5
6
|
const commandModule: CommandModule = {
|
|
6
7
|
command: 'watch',
|
|
@@ -42,7 +43,7 @@ const commandModule: CommandModule = {
|
|
|
42
43
|
process.on('SIGINT', () => {
|
|
43
44
|
watcher.close();
|
|
44
45
|
console.log('\nWatch stopped.');
|
|
45
|
-
|
|
46
|
+
handlerExit();
|
|
46
47
|
});
|
|
47
48
|
}
|
|
48
49
|
};
|
package/src/dubhe.ts
CHANGED
|
@@ -2,25 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
import yargs from 'yargs';
|
|
4
4
|
import { hideBin } from 'yargs/helpers';
|
|
5
|
+
import chalk from 'chalk';
|
|
5
6
|
import { commands } from './commands';
|
|
6
7
|
import { logError } from './utils/errors';
|
|
8
|
+
import packageJson from '../package.json';
|
|
7
9
|
|
|
8
10
|
// Load .env file into process.env
|
|
9
11
|
import * as dotenv from 'dotenv';
|
|
10
|
-
import chalk from 'chalk';
|
|
11
12
|
dotenv.config();
|
|
12
13
|
|
|
13
14
|
yargs(hideBin(process.argv))
|
|
14
15
|
// Explicit name to display in help (by default it's the entry file, which may not be "dubhe" for e.g. ts-node)
|
|
15
16
|
.scriptName('dubhe')
|
|
16
17
|
// Use the commands directory to scaffold
|
|
17
|
-
|
|
18
|
+
|
|
18
19
|
.command(commands as any)
|
|
20
|
+
.version(packageJson.version)
|
|
21
|
+
.demandCommand(1, 'Please provide a command')
|
|
22
|
+
.recommendCommands()
|
|
19
23
|
// Enable strict mode.
|
|
20
24
|
.strict()
|
|
21
25
|
// Custom error handler
|
|
22
26
|
.fail((msg, err) => {
|
|
23
27
|
console.error(chalk.red(msg));
|
|
28
|
+
|
|
24
29
|
if (msg.includes('Missing required argument')) {
|
|
25
30
|
console.log(
|
|
26
31
|
chalk.yellow(
|
|
@@ -29,8 +34,11 @@ yargs(hideBin(process.argv))
|
|
|
29
34
|
);
|
|
30
35
|
}
|
|
31
36
|
console.log('');
|
|
32
|
-
|
|
33
|
-
|
|
37
|
+
// Even though `.fail` type says we should get an `Error`, this can sometimes be undefined
|
|
38
|
+
if (err != null) {
|
|
39
|
+
logError(err);
|
|
40
|
+
console.log('');
|
|
41
|
+
}
|
|
34
42
|
|
|
35
43
|
process.exit(1);
|
|
36
44
|
})
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// Better download implementation using axios
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import * as cliProgress from 'cli-progress';
|
|
4
|
+
import * as fs from 'fs';
|
|
5
|
+
import axios from 'axios';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Download file using axios
|
|
9
|
+
*/
|
|
10
|
+
export async function downloadWithAxios(url: string, outputPath: string): Promise<void> {
|
|
11
|
+
try {
|
|
12
|
+
const response = await axios.get(url, {
|
|
13
|
+
responseType: 'stream',
|
|
14
|
+
timeout: 30000,
|
|
15
|
+
headers: { 'User-Agent': 'dubhe-cli' },
|
|
16
|
+
maxRedirects: 5,
|
|
17
|
+
validateStatus: (status) => status < 400 // Accept all status codes < 400
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
await streamToFile(response, outputPath);
|
|
21
|
+
console.log(chalk.green(` ✓ Successfully downloaded`));
|
|
22
|
+
} catch (error: any) {
|
|
23
|
+
// Handle specific network error cases with more descriptive messages
|
|
24
|
+
if (error.code === 'ENOTFOUND') {
|
|
25
|
+
throw new Error(
|
|
26
|
+
`DNS resolution failed: ${error.message}. Please check your internet connection.`
|
|
27
|
+
);
|
|
28
|
+
} else if (error.code === 'ECONNRESET') {
|
|
29
|
+
throw new Error(`Connection reset: ${error.message}. Please check your network connection.`);
|
|
30
|
+
} else if (error.code === 'ETIMEDOUT') {
|
|
31
|
+
throw new Error(
|
|
32
|
+
`Connection timeout: ${error.message}. Please check your network connection.`
|
|
33
|
+
);
|
|
34
|
+
} else if (error.message.includes('protocol mismatch')) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
`Protocol mismatch: ${error.message}. Please check your network configuration.`
|
|
37
|
+
);
|
|
38
|
+
} else if (error.response) {
|
|
39
|
+
throw new Error(`HTTP ${error.response.status}: ${error.response.statusText}`);
|
|
40
|
+
} else {
|
|
41
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
42
|
+
throw new Error(`Download failed: ${errorMsg}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Stream response data to file with progress bar
|
|
49
|
+
*/
|
|
50
|
+
async function streamToFile(response: any, outputPath: string): Promise<void> {
|
|
51
|
+
const totalSize = parseInt(response.headers['content-length'] || '0');
|
|
52
|
+
|
|
53
|
+
// Create progress bar
|
|
54
|
+
const progressBar = new cliProgress.SingleBar({
|
|
55
|
+
format:
|
|
56
|
+
chalk.cyan('Download Progress') +
|
|
57
|
+
' |{bar}| {percentage}% | {value}/{total} MB | Speed: {speed} MB/s | ETA: {eta}s',
|
|
58
|
+
barCompleteChar: '\u2588',
|
|
59
|
+
barIncompleteChar: '\u2591',
|
|
60
|
+
hideCursor: true,
|
|
61
|
+
barsize: 30,
|
|
62
|
+
forceRedraw: true
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
if (totalSize > 0) {
|
|
66
|
+
progressBar.start(Math.round((totalSize / 1024 / 1024) * 100) / 100, 0, {
|
|
67
|
+
speed: '0.00'
|
|
68
|
+
});
|
|
69
|
+
} else {
|
|
70
|
+
console.log(chalk.blue('📥 Downloading... (unable to get file size)'));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const writer = fs.createWriteStream(outputPath);
|
|
74
|
+
let downloadedBytes = 0;
|
|
75
|
+
const startTime = Date.now();
|
|
76
|
+
|
|
77
|
+
return new Promise((resolve, reject) => {
|
|
78
|
+
response.data.on('data', (chunk: Buffer) => {
|
|
79
|
+
downloadedBytes += chunk.length;
|
|
80
|
+
|
|
81
|
+
if (totalSize > 0) {
|
|
82
|
+
const downloadedMB = Math.round((downloadedBytes / 1024 / 1024) * 100) / 100;
|
|
83
|
+
const elapsedTime = (Date.now() - startTime) / 1000;
|
|
84
|
+
const speed = elapsedTime > 0 ? Math.round((downloadedMB / elapsedTime) * 100) / 100 : 0;
|
|
85
|
+
|
|
86
|
+
progressBar.update(downloadedMB, {
|
|
87
|
+
speed: speed.toFixed(2)
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
response.data.pipe(writer);
|
|
93
|
+
|
|
94
|
+
writer.on('finish', () => {
|
|
95
|
+
if (totalSize > 0) {
|
|
96
|
+
progressBar.stop();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const totalMB = Math.round((downloadedBytes / 1024 / 1024) * 100) / 100;
|
|
100
|
+
const elapsedTime = (Date.now() - startTime) / 1000;
|
|
101
|
+
const avgSpeed = elapsedTime > 0 ? Math.round((totalMB / elapsedTime) * 100) / 100 : 0;
|
|
102
|
+
|
|
103
|
+
console.log(
|
|
104
|
+
chalk.green(`✓ Download completed! ${totalMB} MB, average speed: ${avgSpeed} MB/s`)
|
|
105
|
+
);
|
|
106
|
+
resolve();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
writer.on('error', (error) => {
|
|
110
|
+
if (totalSize > 0) {
|
|
111
|
+
progressBar.stop();
|
|
112
|
+
}
|
|
113
|
+
reject(error);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
}
|