soluser 1.0.8 → 1.0.9

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 CHANGED
@@ -3,11 +3,23 @@
3
3
  ```shell
4
4
  npm install -g soluser@latest
5
5
  ```
6
+ ### 通过下载代码做本地安装的方法
7
+ ```
8
+ git clone git@github.com:nextuser/soluser.git
9
+ cd soluser
10
+ npm link
11
+ ```
12
+
13
+ ## 查看版本
14
+ ```shell
15
+ $ soluser --version
16
+ ```
17
+
6
18
  ## 新建账号
7
19
  ```shell
8
- $ soluser new charlie
9
- $ soluser new alice --word-length 12
10
- $ soluser new bob --word-length 24 --without-passphrase
20
+ $ soluser new alice
21
+ $ soluser new bob --word-length 12
22
+ $ soluser new charlie --word-length 24 --without-passphrase
11
23
  ```
12
24
 
13
25
  ## 切换账号
package/bin/index.js CHANGED
@@ -5,6 +5,10 @@ const newAccount = require('../src/commands/new');
5
5
  const switchAccount = require('../src/commands/switch');
6
6
  const listAccounts = require('../src/commands/list');
7
7
  const removeAccount = require('../src/commands/remove');
8
+ const { showExamples } = require('../src/utils/example');
9
+ const pruneAccount = require('../src/commands/prune');
10
+ const clear = require('../src/commands/clear');
11
+
8
12
 
9
13
  // 导入地址查询命令
10
14
  const showAddress = require('../src/commands/address');
@@ -15,12 +19,14 @@ const requestAirdrop = require('../src/commands/airdrop');
15
19
 
16
20
  // Get version from package.json
17
21
  const packageJson = require('../package.json');
22
+ const version = packageJson.version;
18
23
  // Set program version
19
- program.version(packageJson.version);
24
+ program.version(version);
20
25
 
21
26
  // 定义地址查询命令
22
27
  program
23
28
  .command('address')
29
+ .alias('a')
24
30
  .description('Output the base58 address of a Solana account')
25
31
  .argument('<alias>', 'Alias of the account to get address') // 接收别名作为位置参数
26
32
  .action((alias) => {
@@ -42,6 +48,7 @@ program
42
48
  // 定义余额查询命令
43
49
  program
44
50
  .command('balance')
51
+ .alias('b')
45
52
  .description('Check the SOL balance of a Solana account')
46
53
  .argument('<alias>', 'Alias of the account to check balance') // 接收别名作为位置参数
47
54
  .action((alias) => {
@@ -53,6 +60,7 @@ program
53
60
  // 定义新建账号命令(修改后)
54
61
  program
55
62
  .command('new')
63
+ .alias('add')
56
64
  .description('Create a new Solana account')
57
65
  .argument('<alias>', 'Alias for the new account (must start with a letter, contain letters, digits, hyphens, or underscores)') // 新增位置参数
58
66
  .option('--word-length <number>', 'Number of words in seed phrase (12,15,18,21,24)', 12)
@@ -73,6 +81,7 @@ program
73
81
  // 定义切换账号命令(修改后)
74
82
  program
75
83
  .command('switch')
84
+ .alias('s')
76
85
  .description('Switch active Solana account')
77
86
  .argument('<alias>', 'Alias of the account to switch to') // 新增位置参数
78
87
  .action((alias) => { // 直接使用位置参数 alias
@@ -83,18 +92,53 @@ program
83
92
  // 定义列出账号命令
84
93
  program
85
94
  .command('list')
95
+ .alias('l')
86
96
  .description('List all Solana accounts')
87
97
  .action(listAccounts);
88
98
 
89
99
  // 定义删除账号命令
90
100
  program
91
- .command('remove <alias>') // <alias> 为必填参数
92
- .description('Delete a Solana account (permanently removes the key file)')
101
+ .command('remove <alias>')
102
+ .alias('r')
103
+ .description('remove alias from list, Move a Solana account to backup directory with timestamp')
104
+ .action((alias) => {
105
+ removeAccount(alias); // 使用新的移动逻辑
106
+ });
107
+
108
+ program
109
+ .command('clear')
110
+ .alias('c')
111
+ .description('Move all Solana accounts to backup directory with timestamp')
112
+ .action(() => {
113
+ clear();
114
+ });
115
+
116
+ // 定义删除账号命令
117
+ program
118
+ .command('prune <alias>') // <alias> 为必填参数
119
+ .alias('p')
120
+ .description('prune a Solana account (prune the private key file)')
93
121
  .action((alias) => {
94
- removeAccount(alias);
122
+ pruneAccount(alias);
95
123
  });
96
124
 
97
125
 
126
+
127
+
128
+ program.on('--help', () => {
129
+ console.log("version:" ,version);
130
+ showExamples();
131
+ process.exit(1);
132
+ });
133
+
134
+
135
+
98
136
  // 解析命令行参数
99
137
  program.parse(process.argv);
100
138
 
139
+ // 处理无参数情况(显示帮助)
140
+ if (process.argv.length === 2) {
141
+
142
+ program.help();//program.outputHelp();
143
+ }
144
+
package/package.json CHANGED
@@ -1,11 +1,16 @@
1
1
  {
2
2
  "name": "soluser",
3
- "version": "1.0.8",
3
+ "version": "1.0.9",
4
4
  "description": "A CLI tool to manage Solana accounts (new, switch, list)",
5
5
  "main": "bin/index.js",
6
6
  "bin": {
7
7
  "soluser": "./bin/index.js"
8
8
  },
9
+ "scripts": {
10
+ "test": "mocha test/**/*.test.js --noparallel",
11
+ "test:watch": "mocha test/**/*.test.js --watch",
12
+ "test:coverage": "nyc mocha test/**/*.test.js"
13
+ },
9
14
  "keywords": [
10
15
  "solana",
11
16
  "cli",
@@ -15,9 +20,23 @@
15
20
  "author": "guahuzi",
16
21
  "license": "MIT",
17
22
  "dependencies": {
23
+ "chalk": "4.1.2",
18
24
  "cli-table3": "^0.6.5",
19
- "commander": "^14.0.2"
25
+ "commander": "^14.0.2",
26
+ "readline": "^1.3.0"
27
+ },
28
+ "devDependencies": {
29
+ "chai": "^4.3.7",
30
+ "mocha": "^10.2.0",
31
+ "nyc": "^15.1.0"
20
32
  },
33
+ "files": [
34
+ "src",
35
+ "bin",
36
+ "package.json",
37
+ "package-lock.json",
38
+ "README.md"
39
+ ],
21
40
  "repository": {
22
41
  "type": "git",
23
42
  "url": "https://github.com/nextuser/soluser.git"
@@ -0,0 +1,83 @@
1
+ // src/commands/clear.js
2
+ const clear = async () => {
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const readline = require('readline');
6
+
7
+ // 创建 readline 接口用于用户输入
8
+ const rl = readline.createInterface({
9
+ input: process.stdin,
10
+ output: process.stdout
11
+ });
12
+
13
+ // 显示警告并等待用户确认
14
+ console.log('\x1b[33m%s\x1b[0m', 'WARNING: This will move ALL Solana account files to backup directory!');
15
+ console.log('This operation affects all accounts in your keys directory.');
16
+
17
+ let answer = await new Promise((resolve) => {
18
+ rl.question('Are you sure you want to continue? Type "yes" to proceed: ', (input) => {
19
+ resolve(input.trim().toLowerCase());
20
+ });
21
+ });
22
+
23
+ rl.close();
24
+ if(answer) {
25
+ answer = answer.trim().toLowerCase();
26
+ }
27
+
28
+ if (answer !== 'yes' && answer !== 'y') {
29
+ console.log('Operation cancelled.');
30
+ return;
31
+ }
32
+
33
+
34
+ // 获取当前时间戳为 YYMMDDHHMMSS 格式
35
+ const now = new Date();
36
+ const timestamp = `${now.getFullYear().toString().slice(-2)}${(now.getMonth() + 1).toString().padStart(2, '0')}${now.getDate().toString().padStart(2, '0')}${now.getHours().toString().padStart(2, '0')}${now.getMinutes().toString().padStart(2, '0')}${now.getSeconds().toString().padStart(2, '0')}`;
37
+
38
+ // 定义密钥目录和备份目录
39
+ const keysDir = path.join(require('os').homedir(), '.config', 'solana', 'keys');
40
+ const backupDir = path.join(require('os').homedir(), '.config', 'solana', '.bak');
41
+
42
+ // 确保备份目录存在
43
+ if (!fs.existsSync(backupDir)) {
44
+ fs.mkdirSync(backupDir, { recursive: true });
45
+ }
46
+
47
+ // 读取密钥目录中的所有文件
48
+ if (fs.existsSync(keysDir)) {
49
+ const files = fs.readdirSync(keysDir);
50
+
51
+ // 过滤出 .json 文件并移动
52
+ const jsonFiles = files.filter(file => path.extname(file) === '.json');
53
+
54
+ if (jsonFiles.length > 0) {
55
+ let successCount = 0;
56
+ jsonFiles.forEach(file => {
57
+ const sourceFile = path.join(keysDir, file);
58
+ const fileNameWithoutExt = path.basename(file, '.json');
59
+ const destFile = path.join(backupDir, `${fileNameWithoutExt}_${timestamp}.json`);
60
+
61
+ try {
62
+ fs.renameSync(sourceFile, destFile);
63
+ console.log(`Moved ${file} to backup directory as ${fileNameWithoutExt}_${timestamp}.json`);
64
+ console.log('All accounts have been removed.')
65
+ successCount++;
66
+ } catch (error) {
67
+ console.error(`Failed to move ${file}: ${error.message}`);
68
+ }
69
+ });
70
+
71
+ console.log(`\nSuccessfully moved ${successCount}/${jsonFiles.length} account(s) to backup directory`);
72
+ } else {
73
+
74
+ console.log('All accounts have been removed.')
75
+ console.log('No account files found to move');
76
+ }
77
+ } else {
78
+ console.error('Keys directory does not exist');
79
+ process.exit(1);
80
+ }
81
+ };
82
+
83
+ module.exports = clear;
@@ -7,13 +7,13 @@ const { getAddress, getActiveKeyPath } = require('../utils/solana');
7
7
  function listAccounts() {
8
8
  // 1. 读取密钥目录下的所有 json 文件
9
9
  if (!fs.existsSync(KEYS_DIR)) {
10
- console.log('No accounts found. Create one with "soluser new --alias <name>".');
10
+ console.log('No accounts found. Create one with "soluser new <alias name>".');
11
11
  return;
12
12
  }
13
13
 
14
14
  const files = fs.readdirSync(KEYS_DIR).filter(file => file.endsWith('.json'));
15
15
  if (files.length === 0) {
16
- console.log('No accounts found. Create one with "soluser new --alias <name>".');
16
+ console.log('No accounts found. Create one with "soluser new <alias name>".');
17
17
  return;
18
18
  }
19
19
 
@@ -0,0 +1,63 @@
1
+ const fs = require('fs');
2
+ const { validateAlias, getKeyFilePath } = require('../utils/path');
3
+ const { getActiveKeyPath } = require('../utils/solana');
4
+ const path = require('path');
5
+ const ThrowErorr = require('../utils/throw_error');
6
+ const chalk = require('chalk');
7
+ const { getAddress } = require('../utils/solana');
8
+
9
+ async function pruneAccount(alias) {
10
+ // 1. 验证别名格式
11
+ validateAlias(alias);
12
+
13
+ // 2. 检查密钥文件是否存在
14
+ const keyPath = getKeyFilePath(alias);
15
+ if (!fs.existsSync(keyPath)) {
16
+ ThrowErorr(`Account "${alias}" not found. Use "soluser list" to check existing accounts.`);
17
+ }
18
+
19
+ // 3. 检查是否为当前活跃账号
20
+ const activeKeyPath = getActiveKeyPath();
21
+ const isActive = activeKeyPath === keyPath;
22
+ if (isActive) {
23
+ console.warn(`⚠️ Warning: "${alias}" is currently the active account. Deleting it will break current Solana config.`);
24
+ }
25
+
26
+
27
+
28
+ const address = getAddress(alias);
29
+
30
+ const prune_confirm = `
31
+ WARNING: This action will irreversibly erase your local Solana key file of user ${alias}, address(${address}).
32
+ Without a valid backup, you will lose permanent access to all funds, assets, and permissions linked to the corresponding account.
33
+ Confirm continuation? (yes/no)
34
+ `
35
+
36
+ // 4. 确认删除(简单交互提示)
37
+ const readline = require('readline').createInterface({
38
+ input: process.stdin,
39
+ output: process.stdout
40
+ });
41
+
42
+ let answer = await new Promise((resolve) => {
43
+ readline.question(chalk.red(prune_confirm), (input) => {
44
+ resolve(input.trim().toLowerCase());
45
+ });
46
+ });
47
+ readline.close();
48
+
49
+
50
+ if (answer === 'y' || answer === 'yes') {
51
+ // 执行删除
52
+ fs.unlinkSync(keyPath);
53
+ console.log(`✅ Successfully Deleted account: ${alias}`);
54
+ if (isActive) {
55
+ console.log('ℹ️ Tip: Use "soluser switch --address <alias>" to set a new active account.');
56
+ }
57
+ } else {
58
+ console.log('❌ Deletion cancelled.');
59
+ }
60
+
61
+ }
62
+
63
+ module.exports = pruneAccount;
@@ -1,44 +1,79 @@
1
- const fs = require('fs');
2
- const { validateAlias, getKeyFilePath } = require('../utils/path');
3
- const { getActiveKeyPath } = require('../utils/solana');
4
- const path = require('path');
5
- const ThrowErorr = require('../utils/throw_error');
6
- function removeAccount(alias) {
7
- // 1. 验证别名格式
8
- validateAlias(alias);
9
-
10
- // 2. 检查密钥文件是否存在
11
- const keyPath = getKeyFilePath(alias);
12
- if (!fs.existsSync(keyPath)) {
13
- ThrowErorr(`Account "${alias}" not found. Use "soluser list" to check existing accounts.`);
14
- }
15
-
16
- // 3. 检查是否为当前活跃账号
17
- const activeKeyPath = getActiveKeyPath();
18
- const isActive = activeKeyPath === keyPath;
19
- if (isActive) {
20
- console.warn(`⚠️ Warning: "${alias}" is currently the active account. Deleting it will break current Solana config.`);
21
- }
22
-
23
- // 4. 确认删除(简单交互提示)
24
- const readline = require('readline').createInterface({
1
+ async function confirm(alias){
2
+ const readline = require('readline');
3
+
4
+ // 创建 readline 接口用于用户输入
5
+ const rl = readline.createInterface({
25
6
  input: process.stdin,
26
7
  output: process.stdout
27
8
  });
28
9
 
29
- readline.question(`Are you sure you want to delete "${alias}"? This will permanently remove the key file (y/N): `, (answer) => {
30
- readline.close();
31
- if (answer.trim().toLowerCase() === 'y') {
32
- // 执行删除
33
- fs.unlinkSync(keyPath);
34
- console.log(`✅ Successfully deleted account: ${alias}`);
35
- if (isActive) {
36
- console.log('ℹ️ Tip: Use "soluser switch --address <alias>" to set a new active account.');
37
- }
38
- } else {
39
- console.log('❌ Deletion cancelled.');
10
+ // 显示警告并等待用户确认
11
+ console.log(`\x1b[33mWARNING: You are about to remove account "${alias}"!\x1b[0m`);
12
+ console.log('This will move the account file to backup directory.');
13
+
14
+ // const answer = await new Promise((resolve) => {
15
+ // rl.question('Are you sure you want to continue? (yes/no): ', (input) => {
16
+ // resolve(input.trim().toLowerCase());
17
+ // });
18
+ // });
19
+ // rl.close();
20
+
21
+ // if (answer !== 'yes' && answer !== 'y') {
22
+ // console.log('Operation cancelled.');
23
+ // return false;
24
+ // }
25
+ // return true;
26
+
27
+ let answer = await new Promise((resolve) => {
28
+ rl.question('Are you sure you want to continue? Type "yes" to proceed: ', (input) => {
29
+ resolve(input.trim().toLowerCase());
30
+ });
31
+ });
32
+
33
+ rl.close();
34
+ if(answer) {
35
+ answer = answer.trim().toLowerCase();
40
36
  }
41
- });
37
+
38
+ if (answer !== 'yes' && answer !== 'y') {
39
+
40
+ return false;
41
+ }
42
+ return true;
43
+
42
44
  }
43
45
 
44
- module.exports = removeAccount;
46
+ const removeAccount = async (alias) => {
47
+ const fs = require('fs');
48
+ const path = require('path');
49
+
50
+ // 获取当前时间戳
51
+ const now = new Date();
52
+ const timestamp = `${now.getFullYear().toString().slice(-2)}${(now.getMonth() + 1).toString().padStart(2, '0')}${now.getDate().toString().padStart(2, '0')}${now.getHours().toString().padStart(2, '0')}${now.getMinutes().toString().padStart(2, '0')}${now.getSeconds().toString().padStart(2, '0')}`;
53
+
54
+
55
+ // 确保备份目录存在
56
+ const backupDir = path.join(require('os').homedir(), '.config', 'solana', 'keys', '.bak');
57
+ if (!fs.existsSync(backupDir)) {
58
+ fs.mkdirSync(backupDir, { recursive: true });
59
+ }
60
+
61
+ // 构造源文件和目标文件路径
62
+ const sourceFile = path.join(require('os').homedir(), '.config', 'solana', 'keys', `${alias}.json`);
63
+ const destFile = path.join(backupDir, `${alias}_${timestamp}.json`);
64
+
65
+ if (fs.existsSync(sourceFile)) {
66
+ const confirmed = await confirm(alias);
67
+ if (!confirmed) {
68
+ console.log('Operation cancelled.');
69
+ return;
70
+ }
71
+ fs.renameSync(sourceFile, destFile);
72
+ console.log(`Removed account "${alias}" from list`);
73
+ } else {
74
+ console.error(`Account "${alias}" not found`);
75
+ process.exit(1);
76
+ }
77
+ };
78
+
79
+ module.exports = removeAccount;
@@ -0,0 +1,26 @@
1
+ //const {config}= require('dotenv');
2
+ //config({debug:false});
3
+
4
+ let debugEnable = false;
5
+ if(process.env.DEBUG){
6
+ console.log("enable debug logging");
7
+ debugEnable = true;
8
+ }
9
+ function debug(...args ){
10
+ if(!debugEnable) return;
11
+
12
+ console.error(... args )
13
+ }
14
+
15
+ function handleError(err){
16
+ const chalk = require('chalk');
17
+
18
+ console.error(chalk.red(err.message));
19
+ if(debugEnable) throw err;
20
+ process.exit(1);
21
+ }
22
+
23
+ module.exports = {
24
+ debug,
25
+ handleError
26
+ }
@@ -0,0 +1,60 @@
1
+ const examples = `
2
+
3
+ ## 查看版本
4
+
5
+ $ soluser --version
6
+
7
+
8
+ ## 新建账号
9
+
10
+ $ soluser new alice
11
+ $ soluser new bob --word-length 12
12
+ $ soluser new charlie --word-length 24 --without-passphrase
13
+
14
+
15
+ ## 切换账号
16
+
17
+ $ soluser switch bob
18
+
19
+
20
+ ## 列出账号
21
+
22
+ $ soluser list
23
+
24
+
25
+ ## 删除账号
26
+
27
+ $ soluser remove alice
28
+
29
+ ## 查看alias对应的地址
30
+
31
+ $ soluser address alice
32
+
33
+ ## 查看alias对应的余额
34
+
35
+ $ soluser balance alice
36
+
37
+ ## airdrop 给alias
38
+
39
+ $ soluser airdrop 5 alice
40
+ `;
41
+
42
+ const chalk = require('chalk');
43
+ function getExamples() {
44
+ let msg = '';
45
+ examples.split('\n').forEach(line => {
46
+ if(line.trim().startsWith('#')){
47
+ msg += chalk.gray(line.trim()) + '\n';
48
+ } else if(line.trim().startsWith('$')){
49
+ msg += chalk.cyan(line.trim()) + '\n';
50
+ }
51
+ })
52
+ console.log(msg);
53
+ }
54
+
55
+ function showExamples(){
56
+ console.log(getExamples());
57
+ };
58
+
59
+ //showExamples();
60
+ module.exports = { getExamples, showExamples };
@@ -0,0 +1,104 @@
1
+ const { expect } = require('chai');
2
+ const { execSync, spawn } = require('child_process');
3
+ const {debug} =require('./debug');
4
+ const { stdout } = require('process');
5
+ function execExpectInput(inputArgs, prompt , input,expectOut,done){
6
+
7
+ return new Promise((resolve, reject) => {
8
+ const child = spawn('node',inputArgs);
9
+ const command = "node " + inputArgs.join(" ");
10
+ debug("execExpectInput command:",command);
11
+ let output = '';
12
+ let errMsg = '';
13
+
14
+ child.stdout.on('data', (data) => {
15
+ //debug("stdout:",data.toString());
16
+ output += data.toString();
17
+ if (output.includes(prompt)) {
18
+ //debug("incluse prompt:",prompt);
19
+ child.stdin.write( input + '\n');
20
+ }
21
+ });
22
+
23
+ child.stderr.on('data', (data) => {
24
+ errMsg += data.toString();
25
+ output += data.toString();
26
+ });
27
+
28
+ child.on('error', (err) => {
29
+ reject(err);
30
+ });
31
+
32
+ child.on('close', (code) => {
33
+
34
+ try{
35
+ expect(output).to.include(expectOut);
36
+ if(done) done(output);
37
+ debug("execute sucessfuly,command:",command )
38
+ resolve(output);
39
+ } catch(err){
40
+ console.error("error to execute : node ", inputArgs, `output=${output},prompt=${prompt} expectOut: ${expectOut} errMsg=${errMsg}` );
41
+ reject(err);
42
+ }
43
+ });
44
+ });
45
+
46
+ }
47
+
48
+
49
+ function execExpectOutput(inputArgs, expectOut,finish_callback){
50
+
51
+ return new Promise((resolve, reject) => {
52
+ const child = spawn('node',inputArgs);
53
+
54
+ let output = '';
55
+ let errMsg = '';
56
+ child.stdout.resume();
57
+ child.stdout.on('data', (data) => {
58
+ output += data.toString();
59
+ });
60
+
61
+ child.stderr.on('data', (data) => {
62
+ errMsg += data.toString();
63
+ output += data.toString();
64
+ });
65
+
66
+
67
+
68
+ child.on('close', (code) => {
69
+ try{
70
+ //debug("output :",output);
71
+ //debug("code",code);
72
+ if(expectOut) expect(output).to.include(expectOut);
73
+ if(finish_callback) finish_callback(output);
74
+ debug("execute sucessfuly,command:",command )
75
+
76
+ resolve(output);
77
+ } catch(err){
78
+ console.error("error to execute : node ", inputArgs, `output=${output}, expectOut: ${expectOut} errMsg=${errMsg}` );
79
+ reject(err);
80
+ }
81
+ });
82
+ const command = "node " + inputArgs.join(" ");
83
+ debug("execExpectOutput command:",command);
84
+ });
85
+
86
+ }
87
+
88
+
89
+ function execExpectOutputOld(inputArgs, expectOut,finish_callback){
90
+ let command = "node " + inputArgs.join(" ");
91
+ debug("command:",command);
92
+ let out = execSync(command)
93
+ try{
94
+ if(expectOut) expect(output).to.include(expectOut);
95
+ if(finish_callback) finish_callback(output);
96
+ } catch(err){
97
+ console.error("error to execute : node ", `command:${command}, expectOut: ${expectOut}` );
98
+ throw err;
99
+ }
100
+
101
+ }
102
+
103
+
104
+ module.exports = {execExpectInput,execExpectOutput};