@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.
Files changed (38) hide show
  1. package/README.md +3 -3
  2. package/dist/dubhe.js +125 -66
  3. package/dist/dubhe.js.map +1 -1
  4. package/package.json +31 -19
  5. package/src/commands/build.ts +47 -16
  6. package/src/commands/call.ts +83 -83
  7. package/src/commands/checkBalance.ts +12 -5
  8. package/src/commands/configStore.ts +12 -4
  9. package/src/commands/convertJson.ts +70 -0
  10. package/src/commands/doctor.ts +1515 -0
  11. package/src/commands/faucet.ts +11 -7
  12. package/src/commands/generateKey.ts +3 -2
  13. package/src/commands/index.ts +16 -7
  14. package/src/commands/info.ts +55 -0
  15. package/src/commands/loadMetadata.ts +57 -0
  16. package/src/commands/localnode.ts +22 -12
  17. package/src/commands/publish.ts +21 -7
  18. package/src/commands/query.ts +101 -101
  19. package/src/commands/schemagen.ts +15 -4
  20. package/src/commands/shell.ts +198 -0
  21. package/src/commands/switchEnv.ts +26 -0
  22. package/src/commands/test.ts +54 -11
  23. package/src/commands/upgrade.ts +11 -4
  24. package/src/commands/wait.ts +333 -22
  25. package/src/commands/watch.ts +2 -1
  26. package/src/dubhe.ts +12 -4
  27. package/src/utils/axios-downloader.ts +116 -0
  28. package/src/utils/callHandler.ts +118 -118
  29. package/src/utils/constants.ts +5 -0
  30. package/src/utils/generateAccount.ts +1 -1
  31. package/src/utils/index.ts +4 -3
  32. package/src/utils/metadataHandler.ts +16 -0
  33. package/src/utils/publishHandler.ts +295 -290
  34. package/src/utils/queryStorage.ts +141 -141
  35. package/src/utils/startNode.ts +165 -108
  36. package/src/utils/storeConfig.ts +6 -12
  37. package/src/utils/upgradeHandler.ts +147 -86
  38. package/src/utils/utils.ts +771 -54
@@ -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: string;
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: 180000
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
- await waitOn({
43
- resources: [options.url],
44
- timeout: options.timeout,
45
- interval: options.interval,
46
- validateStatus: (status: number) => status === 200
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
- spinner.succeed(chalk.green('Service is ready!'));
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
  };
@@ -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
- process.exit();
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
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- command array overload isn't typed, see https://github.com/yargs/yargs/blob/main/docs/advanced.md#esm-hierarchy
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
- logError(err);
33
- console.log('');
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
+ }