@mmmbuto/nexuscli 0.5.0

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 (148) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +172 -0
  3. package/bin/nexuscli.js +117 -0
  4. package/frontend/dist/apple-touch-icon.png +0 -0
  5. package/frontend/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  6. package/frontend/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  7. package/frontend/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  8. package/frontend/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  9. package/frontend/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  10. package/frontend/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  11. package/frontend/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  12. package/frontend/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  13. package/frontend/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  14. package/frontend/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  15. package/frontend/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  16. package/frontend/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  17. package/frontend/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  18. package/frontend/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  19. package/frontend/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  20. package/frontend/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  21. package/frontend/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  22. package/frontend/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  23. package/frontend/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  24. package/frontend/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  25. package/frontend/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  26. package/frontend/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  27. package/frontend/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  28. package/frontend/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  29. package/frontend/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  30. package/frontend/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  31. package/frontend/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  32. package/frontend/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  33. package/frontend/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  34. package/frontend/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  35. package/frontend/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  36. package/frontend/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  37. package/frontend/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  38. package/frontend/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  39. package/frontend/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  40. package/frontend/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  41. package/frontend/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  42. package/frontend/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  43. package/frontend/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  44. package/frontend/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  45. package/frontend/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  46. package/frontend/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  47. package/frontend/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  48. package/frontend/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  49. package/frontend/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  50. package/frontend/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  51. package/frontend/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  52. package/frontend/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  53. package/frontend/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  54. package/frontend/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  55. package/frontend/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  56. package/frontend/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  57. package/frontend/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  58. package/frontend/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  59. package/frontend/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  60. package/frontend/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  61. package/frontend/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  62. package/frontend/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  63. package/frontend/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  64. package/frontend/dist/assets/index-Bn_l1e6e.css +1 -0
  65. package/frontend/dist/assets/index-CikJbUR5.js +8617 -0
  66. package/frontend/dist/browserconfig.xml +12 -0
  67. package/frontend/dist/favicon-16x16.png +0 -0
  68. package/frontend/dist/favicon-32x32.png +0 -0
  69. package/frontend/dist/favicon-48x48.png +0 -0
  70. package/frontend/dist/favicon.ico +0 -0
  71. package/frontend/dist/icon-192.png +0 -0
  72. package/frontend/dist/icon-512.png +0 -0
  73. package/frontend/dist/icon-maskable-192.png +0 -0
  74. package/frontend/dist/icon-maskable-512.png +0 -0
  75. package/frontend/dist/index.html +79 -0
  76. package/frontend/dist/manifest.json +75 -0
  77. package/frontend/dist/sw.js +122 -0
  78. package/frontend/package.json +28 -0
  79. package/lib/cli/api.js +156 -0
  80. package/lib/cli/boot.js +172 -0
  81. package/lib/cli/config.js +185 -0
  82. package/lib/cli/engines.js +257 -0
  83. package/lib/cli/init.js +660 -0
  84. package/lib/cli/logs.js +72 -0
  85. package/lib/cli/start.js +220 -0
  86. package/lib/cli/status.js +187 -0
  87. package/lib/cli/stop.js +64 -0
  88. package/lib/cli/uninstall.js +194 -0
  89. package/lib/cli/users.js +295 -0
  90. package/lib/cli/workspaces.js +337 -0
  91. package/lib/config/manager.js +233 -0
  92. package/lib/server/.env.example +20 -0
  93. package/lib/server/db/adapter.js +314 -0
  94. package/lib/server/db/drivers/better-sqlite3.js +38 -0
  95. package/lib/server/db/drivers/sql-js.js +75 -0
  96. package/lib/server/db/migrate.js +174 -0
  97. package/lib/server/db/migrations/001_ultra_light_schema.sql +96 -0
  98. package/lib/server/db/migrations/002_session_conversation_mapping.sql +19 -0
  99. package/lib/server/db/migrations/003_message_engine_tracking.sql +18 -0
  100. package/lib/server/db/migrations/004_performance_indexes.sql +16 -0
  101. package/lib/server/db.js +2 -0
  102. package/lib/server/lib/cli-wrapper.js +164 -0
  103. package/lib/server/lib/output-parser.js +132 -0
  104. package/lib/server/lib/pty-adapter.js +57 -0
  105. package/lib/server/middleware/auth.js +103 -0
  106. package/lib/server/models/Conversation.js +259 -0
  107. package/lib/server/models/Message.js +228 -0
  108. package/lib/server/models/User.js +115 -0
  109. package/lib/server/package-lock.json +5895 -0
  110. package/lib/server/routes/auth.js +168 -0
  111. package/lib/server/routes/chat.js +206 -0
  112. package/lib/server/routes/codex.js +205 -0
  113. package/lib/server/routes/conversations.js +224 -0
  114. package/lib/server/routes/gemini.js +228 -0
  115. package/lib/server/routes/jobs.js +317 -0
  116. package/lib/server/routes/messages.js +60 -0
  117. package/lib/server/routes/models.js +198 -0
  118. package/lib/server/routes/sessions.js +285 -0
  119. package/lib/server/routes/upload.js +134 -0
  120. package/lib/server/routes/wake-lock.js +95 -0
  121. package/lib/server/routes/workspace.js +80 -0
  122. package/lib/server/routes/workspaces.js +142 -0
  123. package/lib/server/scripts/cleanup-ghost-sessions.js +71 -0
  124. package/lib/server/scripts/seed-users.js +37 -0
  125. package/lib/server/scripts/test-history-access.js +50 -0
  126. package/lib/server/server.js +227 -0
  127. package/lib/server/services/cache.js +85 -0
  128. package/lib/server/services/claude-wrapper.js +312 -0
  129. package/lib/server/services/cli-loader.js +384 -0
  130. package/lib/server/services/codex-output-parser.js +277 -0
  131. package/lib/server/services/codex-wrapper.js +224 -0
  132. package/lib/server/services/context-bridge.js +289 -0
  133. package/lib/server/services/gemini-output-parser.js +398 -0
  134. package/lib/server/services/gemini-wrapper.js +249 -0
  135. package/lib/server/services/history-sync.js +407 -0
  136. package/lib/server/services/output-parser.js +415 -0
  137. package/lib/server/services/session-manager.js +465 -0
  138. package/lib/server/services/summary-generator.js +259 -0
  139. package/lib/server/services/workspace-manager.js +516 -0
  140. package/lib/server/tests/history-sync.test.js +90 -0
  141. package/lib/server/tests/integration-session-sync.test.js +151 -0
  142. package/lib/server/tests/integration.test.js +76 -0
  143. package/lib/server/tests/performance.test.js +118 -0
  144. package/lib/server/tests/services.test.js +160 -0
  145. package/lib/setup/postinstall.js +216 -0
  146. package/lib/utils/paths.js +107 -0
  147. package/lib/utils/termux.js +145 -0
  148. package/package.json +82 -0
@@ -0,0 +1,194 @@
1
+ /**
2
+ * NexusCLI - Uninstall Command
3
+ * Prepares for clean uninstallation with optional data removal
4
+ */
5
+
6
+ const chalk = require('chalk');
7
+ const readline = require('readline');
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+ const { execSync } = require('child_process');
11
+
12
+ const DATA_DIR = path.join(process.env.HOME, '.nexuscli');
13
+ const BOOT_SCRIPT = path.join(process.env.HOME, '.termux/boot/nexuscli-boot.sh');
14
+
15
+ function prompt(question) {
16
+ const rl = readline.createInterface({
17
+ input: process.stdin,
18
+ output: process.stdout
19
+ });
20
+
21
+ return new Promise((resolve) => {
22
+ rl.question(question, (answer) => {
23
+ rl.close();
24
+ resolve(answer.toLowerCase().trim());
25
+ });
26
+ });
27
+ }
28
+
29
+ function formatSize(bytes) {
30
+ if (bytes < 1024) return bytes + ' B';
31
+ if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
32
+ return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
33
+ }
34
+
35
+ function getDirSize(dirPath) {
36
+ let totalSize = 0;
37
+ try {
38
+ const files = fs.readdirSync(dirPath, { withFileTypes: true });
39
+ for (const file of files) {
40
+ const filePath = path.join(dirPath, file.name);
41
+ if (file.isDirectory()) {
42
+ totalSize += getDirSize(filePath);
43
+ } else {
44
+ totalSize += fs.statSync(filePath).size;
45
+ }
46
+ }
47
+ } catch (e) {
48
+ // Ignore errors
49
+ }
50
+ return totalSize;
51
+ }
52
+
53
+ async function uninstallCommand(options) {
54
+ console.log('');
55
+ console.log(chalk.red('╔══════════════════════════════════════════════╗'));
56
+ console.log(chalk.red('║') + chalk.white.bold(' 🗑️ NexusCLI Uninstall ') + chalk.red('║'));
57
+ console.log(chalk.red('╚══════════════════════════════════════════════╝'));
58
+ console.log('');
59
+
60
+ // Check if data directory exists
61
+ const dataExists = fs.existsSync(DATA_DIR);
62
+ const bootExists = fs.existsSync(BOOT_SCRIPT);
63
+
64
+ if (!dataExists && !bootExists) {
65
+ console.log(chalk.yellow('Nessun dato NexusCLI trovato.'));
66
+ console.log('');
67
+ console.log('Per rimuovere il pacchetto npm, esegui:');
68
+ console.log(chalk.cyan(' npm uninstall -g @mmmbuto/nexuscli'));
69
+ console.log('');
70
+ return;
71
+ }
72
+
73
+ // Show what will be affected
74
+ console.log(chalk.white.bold('Dati trovati:'));
75
+ console.log('');
76
+
77
+ if (dataExists) {
78
+ const dataSize = getDirSize(DATA_DIR);
79
+ console.log(chalk.cyan(` 📁 ${DATA_DIR}`));
80
+ console.log(chalk.gray(` Dimensione: ${formatSize(dataSize)}`));
81
+
82
+ // List contents
83
+ const contents = fs.readdirSync(DATA_DIR);
84
+ for (const item of contents) {
85
+ const itemPath = path.join(DATA_DIR, item);
86
+ const stat = fs.statSync(itemPath);
87
+ const icon = stat.isDirectory() ? '📂' : '📄';
88
+ const size = stat.isDirectory() ? formatSize(getDirSize(itemPath)) : formatSize(stat.size);
89
+ console.log(chalk.gray(` ${icon} ${item} (${size})`));
90
+ }
91
+ console.log('');
92
+ }
93
+
94
+ if (bootExists) {
95
+ console.log(chalk.cyan(` 🚀 ${BOOT_SCRIPT}`));
96
+ console.log(chalk.gray(' Script avvio automatico Termux'));
97
+ console.log('');
98
+ }
99
+
100
+ // Stop server if running
101
+ console.log(chalk.yellow('Arresto server in corso...'));
102
+ try {
103
+ execSync('pkill -f "node.*nexuscli" 2>/dev/null || true', { stdio: 'ignore' });
104
+ console.log(chalk.green('✓ Server arrestato'));
105
+ } catch (e) {
106
+ console.log(chalk.gray(' Server non in esecuzione'));
107
+ }
108
+ console.log('');
109
+
110
+ // Ask what to do
111
+ console.log(chalk.white.bold('Cosa vuoi fare?'));
112
+ console.log('');
113
+ console.log(chalk.cyan(' 1)') + ' Rimuovi TUTTO (dati + configurazione + boot script)');
114
+ console.log(chalk.cyan(' 2)') + ' Mantieni i dati (solo rimuovi boot script)');
115
+ console.log(chalk.cyan(' 3)') + ' Annulla');
116
+ console.log('');
117
+
118
+ const choice = await prompt(chalk.yellow('Scelta [1/2/3]: '));
119
+
120
+ if (choice === '3' || choice === 'n' || choice === 'no' || choice === '') {
121
+ console.log('');
122
+ console.log(chalk.green('Operazione annullata.'));
123
+ return;
124
+ }
125
+
126
+ if (choice === '1') {
127
+ // Remove everything
128
+ console.log('');
129
+ const confirm = await prompt(chalk.red.bold('⚠️ ATTENZIONE: Tutti i dati saranno eliminati! Confermi? [s/N]: '));
130
+
131
+ if (confirm !== 's' && confirm !== 'si' && confirm !== 'y' && confirm !== 'yes') {
132
+ console.log('');
133
+ console.log(chalk.green('Operazione annullata.'));
134
+ return;
135
+ }
136
+
137
+ console.log('');
138
+ console.log(chalk.yellow('Rimozione in corso...'));
139
+
140
+ // Remove boot script
141
+ if (bootExists) {
142
+ try {
143
+ fs.unlinkSync(BOOT_SCRIPT);
144
+ console.log(chalk.green(`✓ Rimosso ${BOOT_SCRIPT}`));
145
+ } catch (e) {
146
+ console.log(chalk.red(`✗ Errore rimozione boot script: ${e.message}`));
147
+ }
148
+ }
149
+
150
+ // Remove data directory
151
+ if (dataExists) {
152
+ try {
153
+ fs.rmSync(DATA_DIR, { recursive: true, force: true });
154
+ console.log(chalk.green(`✓ Rimosso ${DATA_DIR}`));
155
+ } catch (e) {
156
+ console.log(chalk.red(`✗ Errore rimozione dati: ${e.message}`));
157
+ }
158
+ }
159
+
160
+ console.log('');
161
+ console.log(chalk.green.bold('✓ Pulizia completata!'));
162
+
163
+ } else if (choice === '2') {
164
+ // Keep data, only remove boot script
165
+ console.log('');
166
+
167
+ if (bootExists) {
168
+ try {
169
+ fs.unlinkSync(BOOT_SCRIPT);
170
+ console.log(chalk.green(`✓ Rimosso ${BOOT_SCRIPT}`));
171
+ } catch (e) {
172
+ console.log(chalk.red(`✗ Errore rimozione boot script: ${e.message}`));
173
+ }
174
+ }
175
+
176
+ console.log('');
177
+ console.log(chalk.green.bold('✓ Boot script rimosso'));
178
+ console.log(chalk.cyan(` I tuoi dati sono stati mantenuti in: ${DATA_DIR}`));
179
+ console.log(chalk.gray(' Reinstallando NexusCLI, ritroverai le tue configurazioni.'));
180
+
181
+ } else {
182
+ console.log('');
183
+ console.log(chalk.red('Scelta non valida. Operazione annullata.'));
184
+ return;
185
+ }
186
+
187
+ console.log('');
188
+ console.log(chalk.white.bold('Per completare la disinstallazione, esegui:'));
189
+ console.log('');
190
+ console.log(chalk.cyan(' npm uninstall -g @mmmbuto/nexuscli'));
191
+ console.log('');
192
+ }
193
+
194
+ module.exports = uninstallCommand;
@@ -0,0 +1,295 @@
1
+ /**
2
+ * nexuscli users - User management
3
+ */
4
+
5
+ const chalk = require('chalk');
6
+ const inquirer = require('inquirer');
7
+ const { isInitialized } = require('../config/manager');
8
+ const { initDb, prepare } = require('../server/db');
9
+
10
+ /**
11
+ * List all users
12
+ */
13
+ async function listUsers() {
14
+ const { getConfig } = require('../config/manager');
15
+ const config = getConfig();
16
+
17
+ console.log('');
18
+ console.log(chalk.bold('Users:'));
19
+ console.log('');
20
+
21
+ // Show config user (main admin from init)
22
+ if (config.auth && config.auth.user) {
23
+ console.log(` ${chalk.cyan(config.auth.user)} [${chalk.red('admin')}] ${chalk.gray('(config)')}`);
24
+ }
25
+
26
+ // Also check database for additional users
27
+ try {
28
+ await initDb();
29
+ const dbUsers = prepare('SELECT id, username, role FROM users ORDER BY created_at').all();
30
+
31
+ for (const user of dbUsers) {
32
+ // Skip if same as config user
33
+ if (config.auth && user.username === config.auth.user) continue;
34
+
35
+ const role = user.role === 'admin' ? chalk.red('admin') : chalk.gray('user');
36
+ console.log(` ${chalk.cyan(user.username)} [${role}]`);
37
+ }
38
+ } catch (err) {
39
+ // DB not available, just show config user
40
+ }
41
+
42
+ if (!config.auth?.user) {
43
+ console.log(chalk.yellow('No users found.'));
44
+ }
45
+
46
+ console.log('');
47
+ }
48
+
49
+ /**
50
+ * Add a new user
51
+ */
52
+ async function addUser(options) {
53
+ await initDb();
54
+ const User = require('../server/models/User');
55
+
56
+ let username = options.username;
57
+ let password = options.password;
58
+ let role = options.admin ? 'admin' : 'user';
59
+
60
+ // Interactive mode if no username provided
61
+ if (!username) {
62
+ const answers = await inquirer.prompt([
63
+ {
64
+ type: 'input',
65
+ name: 'username',
66
+ message: 'Username:',
67
+ validate: (input) => input.length >= 3 || 'Username must be at least 3 characters'
68
+ },
69
+ {
70
+ type: 'password',
71
+ name: 'password',
72
+ message: 'Password:',
73
+ mask: '*',
74
+ validate: (input) => input.length >= 4 || 'Password must be at least 4 characters'
75
+ },
76
+ {
77
+ type: 'confirm',
78
+ name: 'isAdmin',
79
+ message: 'Admin user?',
80
+ default: false
81
+ }
82
+ ]);
83
+
84
+ username = answers.username;
85
+ password = answers.password;
86
+ role = answers.isAdmin ? 'admin' : 'user';
87
+ }
88
+
89
+ // Check if user exists
90
+ const existing = prepare('SELECT id FROM users WHERE username = ?').get(username);
91
+ if (existing) {
92
+ console.log(chalk.red(`User '${username}' already exists.`));
93
+ process.exit(1);
94
+ }
95
+
96
+ // Create user
97
+ try {
98
+ const user = User.create(username, password, role);
99
+ console.log('');
100
+ console.log(chalk.green(`✓ User '${user.username}' created (${user.role})`));
101
+ console.log('');
102
+ } catch (error) {
103
+ console.log(chalk.red(`Error creating user: ${error.message}`));
104
+ process.exit(1);
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Change user password
110
+ */
111
+ async function changePassword(options) {
112
+ await initDb();
113
+ const bcrypt = require('bcryptjs');
114
+ const { getDb, saveDb } = require('../server/db');
115
+
116
+ let username = options.username;
117
+ let newPassword;
118
+
119
+ // Get list of users for selection
120
+ const users = prepare('SELECT username FROM users ORDER BY username').all();
121
+
122
+ if (users.length === 0) {
123
+ console.log(chalk.yellow('No users found.'));
124
+ return;
125
+ }
126
+
127
+ // Interactive mode
128
+ if (!username) {
129
+ const answers = await inquirer.prompt([
130
+ {
131
+ type: 'list',
132
+ name: 'username',
133
+ message: 'Select user:',
134
+ choices: users.map(u => u.username)
135
+ },
136
+ {
137
+ type: 'password',
138
+ name: 'newPassword',
139
+ message: 'New password:',
140
+ mask: '*',
141
+ validate: (input) => input.length >= 4 || 'Password must be at least 4 characters'
142
+ },
143
+ {
144
+ type: 'password',
145
+ name: 'confirmPassword',
146
+ message: 'Confirm password:',
147
+ mask: '*'
148
+ }
149
+ ]);
150
+
151
+ if (answers.newPassword !== answers.confirmPassword) {
152
+ console.log(chalk.red('Passwords do not match.'));
153
+ process.exit(1);
154
+ }
155
+
156
+ username = answers.username;
157
+ newPassword = answers.newPassword;
158
+ } else {
159
+ // Non-interactive: prompt only for password
160
+ const answers = await inquirer.prompt([
161
+ {
162
+ type: 'password',
163
+ name: 'newPassword',
164
+ message: 'New password:',
165
+ mask: '*',
166
+ validate: (input) => input.length >= 4 || 'Password must be at least 4 characters'
167
+ }
168
+ ]);
169
+ newPassword = answers.newPassword;
170
+ }
171
+
172
+ // Update password
173
+ const user = prepare('SELECT id FROM users WHERE username = ?').get(username);
174
+ if (!user) {
175
+ console.log(chalk.red(`User '${username}' not found.`));
176
+ process.exit(1);
177
+ }
178
+
179
+ const hash = bcrypt.hashSync(newPassword, 10);
180
+ const db = getDb();
181
+ db.run('UPDATE users SET password_hash = ? WHERE username = ?', [hash, username]);
182
+ saveDb();
183
+
184
+ console.log('');
185
+ console.log(chalk.green(`✓ Password changed for '${username}'`));
186
+ console.log('');
187
+ }
188
+
189
+ /**
190
+ * Delete a user
191
+ */
192
+ async function deleteUser(options) {
193
+ await initDb();
194
+ const { getDb, saveDb } = require('../server/db');
195
+
196
+ let username = options.username;
197
+
198
+ // Get list of users
199
+ const users = prepare('SELECT username FROM users ORDER BY username').all();
200
+
201
+ if (users.length === 0) {
202
+ console.log(chalk.yellow('No users found.'));
203
+ return;
204
+ }
205
+
206
+ if (users.length === 1) {
207
+ console.log(chalk.yellow('Cannot delete the only user.'));
208
+ return;
209
+ }
210
+
211
+ // Interactive mode
212
+ if (!username) {
213
+ const answers = await inquirer.prompt([
214
+ {
215
+ type: 'list',
216
+ name: 'username',
217
+ message: 'Select user to delete:',
218
+ choices: users.map(u => u.username)
219
+ },
220
+ {
221
+ type: 'confirm',
222
+ name: 'confirm',
223
+ message: 'Are you sure?',
224
+ default: false
225
+ }
226
+ ]);
227
+
228
+ if (!answers.confirm) {
229
+ console.log(chalk.gray('Cancelled.'));
230
+ return;
231
+ }
232
+
233
+ username = answers.username;
234
+ }
235
+
236
+ // Delete user
237
+ const db = getDb();
238
+ const result = db.run('DELETE FROM users WHERE username = ?', [username]);
239
+ saveDb();
240
+
241
+ if (result.changes > 0) {
242
+ console.log('');
243
+ console.log(chalk.green(`✓ User '${username}' deleted`));
244
+ console.log('');
245
+ } else {
246
+ console.log(chalk.red(`User '${username}' not found.`));
247
+ }
248
+ }
249
+
250
+ /**
251
+ * Main users command
252
+ */
253
+ async function users(subcommand, options = {}) {
254
+ console.log('');
255
+
256
+ // Check if initialized
257
+ if (!isInitialized()) {
258
+ console.log(chalk.yellow('NexusCLI is not configured.'));
259
+ console.log(`Run ${chalk.cyan('nexuscli init')} first.`);
260
+ console.log('');
261
+ process.exit(1);
262
+ }
263
+
264
+ switch (subcommand) {
265
+ case 'list':
266
+ case 'ls':
267
+ await listUsers();
268
+ break;
269
+ case 'add':
270
+ case 'create':
271
+ await addUser(options);
272
+ break;
273
+ case 'passwd':
274
+ case 'password':
275
+ await changePassword(options);
276
+ break;
277
+ case 'delete':
278
+ case 'del':
279
+ case 'rm':
280
+ await deleteUser(options);
281
+ break;
282
+ default:
283
+ // Show help
284
+ console.log(chalk.bold('User Management'));
285
+ console.log('');
286
+ console.log('Commands:');
287
+ console.log(` ${chalk.cyan('nexuscli users list')} List all users`);
288
+ console.log(` ${chalk.cyan('nexuscli users add')} Add a new user`);
289
+ console.log(` ${chalk.cyan('nexuscli users passwd')} Change password`);
290
+ console.log(` ${chalk.cyan('nexuscli users delete')} Delete a user`);
291
+ console.log('');
292
+ }
293
+ }
294
+
295
+ module.exports = users;