@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,337 @@
1
+ /**
2
+ * nexuscli workspaces - Manage workspace directories
3
+ *
4
+ * Usage:
5
+ * nexuscli workspaces - List configured workspaces
6
+ * nexuscli workspaces list - List configured workspaces
7
+ * nexuscli workspaces set-default <path> - Set default workspace
8
+ * nexuscli workspaces add <path> - Add workspace to list
9
+ * nexuscli workspaces remove <path> - Remove workspace from list
10
+ * nexuscli workspaces scan - Scan for Claude/Codex sessions
11
+ */
12
+
13
+ const fs = require('fs');
14
+ const path = require('path');
15
+ const os = require('os');
16
+ const chalk = require('chalk');
17
+ const { getConfig, setConfigValue, getConfigValue } = require('../config/manager');
18
+
19
+ // Directories to scan for sessions
20
+ const SCAN_ROOTS = [
21
+ path.join(os.homedir(), 'Dev'),
22
+ path.join(os.homedir(), 'Projects'),
23
+ path.join(os.homedir(), 'projects'),
24
+ path.join(os.homedir(), 'src'),
25
+ path.join(os.homedir(), 'code'),
26
+ path.join(os.homedir(), 'work'),
27
+ os.homedir()
28
+ ];
29
+
30
+ // CLI session directories
31
+ const CLAUDE_PROJECTS = path.join(os.homedir(), '.claude', 'projects');
32
+ const CODEX_SESSIONS = path.join(os.homedir(), '.codex', 'sessions');
33
+
34
+ /**
35
+ * Expand ~ to home directory
36
+ */
37
+ function expandPath(p) {
38
+ if (!p) return p;
39
+ if (p.startsWith('~/')) {
40
+ return path.join(os.homedir(), p.slice(2));
41
+ }
42
+ return path.resolve(p);
43
+ }
44
+
45
+ /**
46
+ * Collapse home directory to ~
47
+ */
48
+ function collapsePath(p) {
49
+ const home = os.homedir();
50
+ if (p.startsWith(home)) {
51
+ return '~' + p.slice(home.length);
52
+ }
53
+ return p;
54
+ }
55
+
56
+ /**
57
+ * Convert workspace path to Claude projects slug
58
+ */
59
+ function pathToSlug(workspacePath) {
60
+ return workspacePath.replace(/\//g, '-').replace(/\./g, '-');
61
+ }
62
+
63
+ /**
64
+ * List configured workspaces
65
+ */
66
+ function listWorkspaces() {
67
+ const config = getConfig();
68
+ const defaultWs = expandPath(config.workspaces?.default || '~/Dev');
69
+ const paths = config.workspaces?.paths || [];
70
+
71
+ console.log('\n' + chalk.cyan('📁 Configured Workspaces:\n'));
72
+
73
+ if (paths.length === 0) {
74
+ console.log(chalk.gray(' No workspaces configured.'));
75
+ console.log(chalk.gray(' Run: nexuscli workspaces scan'));
76
+ } else {
77
+ paths.forEach(ws => {
78
+ const expanded = expandPath(ws);
79
+ const isDefault = expanded === defaultWs;
80
+ const exists = fs.existsSync(expanded);
81
+
82
+ const marker = isDefault ? chalk.green('★ ') : ' ';
83
+ const pathDisplay = collapsePath(expanded);
84
+ const status = exists ? '' : chalk.red(' (not found)');
85
+
86
+ console.log(`${marker}${chalk.white(pathDisplay)}${status}`);
87
+ });
88
+ }
89
+
90
+ console.log('\n' + chalk.gray(`Default: ${collapsePath(defaultWs)}`));
91
+ console.log('');
92
+ }
93
+
94
+ /**
95
+ * Set default workspace
96
+ */
97
+ function setDefault(workspacePath) {
98
+ if (!workspacePath) {
99
+ console.log(chalk.red('\n❌ Usage: nexuscli workspaces set-default <path>\n'));
100
+ return;
101
+ }
102
+
103
+ const expanded = expandPath(workspacePath);
104
+
105
+ if (!fs.existsSync(expanded)) {
106
+ console.log(chalk.red(`\n❌ Path not found: ${expanded}\n`));
107
+ return;
108
+ }
109
+
110
+ if (!fs.statSync(expanded).isDirectory()) {
111
+ console.log(chalk.red(`\n❌ Not a directory: ${expanded}\n`));
112
+ return;
113
+ }
114
+
115
+ // Set as default
116
+ setConfigValue('workspaces.default', collapsePath(expanded));
117
+
118
+ // Also add to paths if not present
119
+ const paths = getConfigValue('workspaces.paths') || [];
120
+ const collapsed = collapsePath(expanded);
121
+ if (!paths.includes(collapsed) && !paths.includes(expanded)) {
122
+ paths.push(collapsed);
123
+ setConfigValue('workspaces.paths', paths);
124
+ }
125
+
126
+ console.log(chalk.green(`\n✅ Default workspace set to: ${collapsed}\n`));
127
+ }
128
+
129
+ /**
130
+ * Add workspace to list
131
+ */
132
+ function addWorkspace(workspacePath) {
133
+ if (!workspacePath) {
134
+ console.log(chalk.red('\n❌ Usage: nexuscli workspaces add <path>\n'));
135
+ return;
136
+ }
137
+
138
+ const expanded = expandPath(workspacePath);
139
+
140
+ if (!fs.existsSync(expanded)) {
141
+ console.log(chalk.red(`\n❌ Path not found: ${expanded}\n`));
142
+ return;
143
+ }
144
+
145
+ if (!fs.statSync(expanded).isDirectory()) {
146
+ console.log(chalk.red(`\n❌ Not a directory: ${expanded}\n`));
147
+ return;
148
+ }
149
+
150
+ const paths = getConfigValue('workspaces.paths') || [];
151
+ const collapsed = collapsePath(expanded);
152
+
153
+ if (paths.includes(collapsed) || paths.includes(expanded)) {
154
+ console.log(chalk.yellow(`\n⚠️ Workspace already in list: ${collapsed}\n`));
155
+ return;
156
+ }
157
+
158
+ paths.push(collapsed);
159
+ setConfigValue('workspaces.paths', paths);
160
+
161
+ console.log(chalk.green(`\n✅ Added workspace: ${collapsed}\n`));
162
+ }
163
+
164
+ /**
165
+ * Remove workspace from list
166
+ */
167
+ function removeWorkspace(workspacePath) {
168
+ if (!workspacePath) {
169
+ console.log(chalk.red('\n❌ Usage: nexuscli workspaces remove <path>\n'));
170
+ return;
171
+ }
172
+
173
+ const expanded = expandPath(workspacePath);
174
+ const collapsed = collapsePath(expanded);
175
+
176
+ let paths = getConfigValue('workspaces.paths') || [];
177
+ const originalLen = paths.length;
178
+
179
+ paths = paths.filter(p => {
180
+ const pExpanded = expandPath(p);
181
+ return pExpanded !== expanded;
182
+ });
183
+
184
+ if (paths.length === originalLen) {
185
+ console.log(chalk.yellow(`\n⚠️ Workspace not in list: ${collapsed}\n`));
186
+ return;
187
+ }
188
+
189
+ setConfigValue('workspaces.paths', paths);
190
+
191
+ // If removed default, clear it
192
+ const defaultWs = expandPath(getConfigValue('workspaces.default') || '');
193
+ if (defaultWs === expanded) {
194
+ setConfigValue('workspaces.default', paths[0] || '~/Dev');
195
+ }
196
+
197
+ console.log(chalk.green(`\n✅ Removed workspace: ${collapsed}\n`));
198
+ }
199
+
200
+ /**
201
+ * Scan for workspaces with Claude/Codex sessions
202
+ */
203
+ function scanWorkspaces() {
204
+ console.log('\n' + chalk.cyan('🔍 Scanning for workspaces with AI sessions...\n'));
205
+
206
+ const foundWorkspaces = new Map(); // path -> { claude: count, codex: count }
207
+
208
+ // 1. Scan Claude projects directory
209
+ if (fs.existsSync(CLAUDE_PROJECTS)) {
210
+ console.log(chalk.gray(` Scanning ${CLAUDE_PROJECTS}...`));
211
+
212
+ const dirs = fs.readdirSync(CLAUDE_PROJECTS, { withFileTypes: true });
213
+
214
+ for (const dir of dirs) {
215
+ if (!dir.isDirectory()) continue;
216
+
217
+ const projectDir = path.join(CLAUDE_PROJECTS, dir.name);
218
+ const sessionFiles = fs.readdirSync(projectDir)
219
+ .filter(f => f.endsWith('.jsonl') && !f.startsWith('agent-'));
220
+
221
+ if (sessionFiles.length > 0) {
222
+ // Read workspace path from first session file
223
+ let workspacePath = null;
224
+ try {
225
+ const firstFile = path.join(projectDir, sessionFiles[0]);
226
+ const firstLine = fs.readFileSync(firstFile, 'utf8').split('\n')[0];
227
+ const entry = JSON.parse(firstLine);
228
+ workspacePath = entry.cwd;
229
+ } catch (err) {
230
+ // Fallback: convert slug back to path
231
+ workspacePath = '/' + dir.name.replace(/^-/, '').replace(/-/g, '/');
232
+ }
233
+
234
+ if (workspacePath && fs.existsSync(workspacePath)) {
235
+ const existing = foundWorkspaces.get(workspacePath) || { claude: 0, codex: 0 };
236
+ existing.claude = sessionFiles.length;
237
+ foundWorkspaces.set(workspacePath, existing);
238
+ }
239
+ }
240
+ }
241
+ }
242
+
243
+ // 2. Scan Codex sessions directory
244
+ if (fs.existsSync(CODEX_SESSIONS)) {
245
+ console.log(chalk.gray(` Scanning ${CODEX_SESSIONS}...`));
246
+
247
+ const sessionFiles = fs.readdirSync(CODEX_SESSIONS)
248
+ .filter(f => f.endsWith('.json'));
249
+
250
+ // Codex sessions might have workspace in their metadata
251
+ // For now, count them without workspace association
252
+ if (sessionFiles.length > 0) {
253
+ console.log(chalk.gray(` Found ${sessionFiles.length} Codex sessions (global)`));
254
+ }
255
+ }
256
+
257
+ // 3. Display results
258
+ console.log('');
259
+
260
+ if (foundWorkspaces.size === 0) {
261
+ console.log(chalk.yellow(' No workspaces with sessions found.\n'));
262
+ return;
263
+ }
264
+
265
+ console.log(chalk.green(`Found ${foundWorkspaces.size} workspaces with sessions:\n`));
266
+
267
+ // Sort by session count
268
+ const sorted = [...foundWorkspaces.entries()]
269
+ .sort((a, b) => (b[1].claude + b[1].codex) - (a[1].claude + a[1].codex));
270
+
271
+ // Get current configured paths
272
+ const currentPaths = (getConfigValue('workspaces.paths') || []).map(expandPath);
273
+
274
+ for (const [ws, counts] of sorted) {
275
+ const isConfigured = currentPaths.includes(ws);
276
+ const marker = isConfigured ? chalk.green('✓') : chalk.yellow('○');
277
+ const claudeCount = counts.claude ? chalk.cyan(`Claude: ${counts.claude}`) : '';
278
+ const codexCount = counts.codex ? chalk.magenta(`Codex: ${counts.codex}`) : '';
279
+ const sessionInfo = [claudeCount, codexCount].filter(Boolean).join(', ');
280
+
281
+ console.log(` ${marker} ${collapsePath(ws)}`);
282
+ console.log(chalk.gray(` ${sessionInfo}`));
283
+ }
284
+
285
+ // Ask to add unconfigured workspaces
286
+ const unconfigured = sorted.filter(([ws]) => !currentPaths.includes(ws));
287
+
288
+ if (unconfigured.length > 0) {
289
+ console.log(chalk.cyan(`\nTo add these workspaces, run:`));
290
+ for (const [ws] of unconfigured) {
291
+ console.log(chalk.gray(` nexuscli workspaces add "${collapsePath(ws)}"`));
292
+ }
293
+ }
294
+
295
+ console.log('');
296
+ }
297
+
298
+ /**
299
+ * Main workspaces command
300
+ */
301
+ async function workspacesCommand(action, arg) {
302
+ if (!action || action === 'list') {
303
+ listWorkspaces();
304
+ return;
305
+ }
306
+
307
+ switch (action) {
308
+ case 'set-default':
309
+ setDefault(arg);
310
+ break;
311
+
312
+ case 'add':
313
+ addWorkspace(arg);
314
+ break;
315
+
316
+ case 'remove':
317
+ case 'rm':
318
+ removeWorkspace(arg);
319
+ break;
320
+
321
+ case 'scan':
322
+ scanWorkspaces();
323
+ break;
324
+
325
+ default:
326
+ console.log(chalk.red(`\n❌ Unknown action: ${action}`));
327
+ console.log('\nUsage:');
328
+ console.log(' nexuscli workspaces - List configured workspaces');
329
+ console.log(' nexuscli workspaces set-default <path> - Set default');
330
+ console.log(' nexuscli workspaces add <path> - Add workspace');
331
+ console.log(' nexuscli workspaces remove <path> - Remove workspace');
332
+ console.log(' nexuscli workspaces scan - Scan for sessions');
333
+ console.log('');
334
+ }
335
+ }
336
+
337
+ module.exports = workspacesCommand;
@@ -0,0 +1,233 @@
1
+ /**
2
+ * Configuration Manager
3
+ * Handles reading/writing ~/.nexuscli/config.json
4
+ */
5
+
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+ const { PATHS, ensureDirectories } = require('../utils/paths');
9
+
10
+ /**
11
+ * Default configuration
12
+ */
13
+ const DEFAULT_CONFIG = {
14
+ version: 1,
15
+ server: {
16
+ port: 41800,
17
+ host: '0.0.0.0'
18
+ },
19
+ auth: {
20
+ jwt_secret: null, // Generated on init
21
+ user: 'tux',
22
+ pass_hash: null // Generated on init
23
+ },
24
+ engines: {
25
+ active: ['claude'],
26
+ check_on_boot: false, // Re-check engine availability on each boot
27
+ claude: {
28
+ enabled: true,
29
+ path: 'auto',
30
+ model: 'sonnet'
31
+ },
32
+ codex: {
33
+ enabled: false,
34
+ path: null
35
+ }
36
+ },
37
+ workspaces: {
38
+ default: null, // Set during init - will use HOME if not configured
39
+ paths: []
40
+ },
41
+ termux: {
42
+ wake_lock: true,
43
+ notifications: true,
44
+ boot_start: false
45
+ }
46
+ };
47
+
48
+ /**
49
+ * Load configuration from file
50
+ */
51
+ function loadConfig() {
52
+ ensureDirectories();
53
+
54
+ if (!fs.existsSync(PATHS.CONFIG_FILE)) {
55
+ return null;
56
+ }
57
+
58
+ try {
59
+ const content = fs.readFileSync(PATHS.CONFIG_FILE, 'utf8');
60
+ return JSON.parse(content);
61
+ } catch (error) {
62
+ console.error('Error loading config:', error.message);
63
+ return null;
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Save configuration to file
69
+ */
70
+ function saveConfig(config) {
71
+ ensureDirectories();
72
+
73
+ try {
74
+ fs.writeFileSync(
75
+ PATHS.CONFIG_FILE,
76
+ JSON.stringify(config, null, 2),
77
+ 'utf8'
78
+ );
79
+ return true;
80
+ } catch (error) {
81
+ console.error('Error saving config:', error.message);
82
+ return false;
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Get configuration (load or create default)
88
+ */
89
+ function getConfig() {
90
+ const config = loadConfig();
91
+ if (config) {
92
+ return config;
93
+ }
94
+ return { ...DEFAULT_CONFIG };
95
+ }
96
+
97
+ /**
98
+ * Check if NexusCLI is initialized
99
+ */
100
+ function isInitialized() {
101
+ return fs.existsSync(PATHS.CONFIG_FILE);
102
+ }
103
+
104
+ /**
105
+ * Get a config value by dot-notation path
106
+ * e.g., 'server.port' -> config.server.port
107
+ */
108
+ function getConfigValue(key) {
109
+ const config = getConfig();
110
+ const parts = key.split('.');
111
+ let value = config;
112
+
113
+ for (const part of parts) {
114
+ if (value === undefined || value === null) {
115
+ return undefined;
116
+ }
117
+ value = value[part];
118
+ }
119
+
120
+ return value;
121
+ }
122
+
123
+ /**
124
+ * Set a config value by dot-notation path
125
+ */
126
+ function setConfigValue(key, value) {
127
+ const config = getConfig();
128
+ const parts = key.split('.');
129
+ let current = config;
130
+
131
+ for (let i = 0; i < parts.length - 1; i++) {
132
+ const part = parts[i];
133
+ if (current[part] === undefined) {
134
+ current[part] = {};
135
+ }
136
+ current = current[part];
137
+ }
138
+
139
+ // Parse value type
140
+ let parsedValue = value;
141
+ if (value === 'true') parsedValue = true;
142
+ else if (value === 'false') parsedValue = false;
143
+ else if (!isNaN(value) && value !== '') parsedValue = Number(value);
144
+
145
+ current[parts[parts.length - 1]] = parsedValue;
146
+
147
+ return saveConfig(config);
148
+ }
149
+
150
+ /**
151
+ * Get all config keys as flat list
152
+ */
153
+ function getConfigKeys(obj = null, prefix = '') {
154
+ if (!obj) {
155
+ obj = getConfig();
156
+ }
157
+
158
+ const keys = [];
159
+
160
+ for (const key of Object.keys(obj)) {
161
+ const fullKey = prefix ? `${prefix}.${key}` : key;
162
+
163
+ if (typeof obj[key] === 'object' && obj[key] !== null && !Array.isArray(obj[key])) {
164
+ keys.push(...getConfigKeys(obj[key], fullKey));
165
+ } else {
166
+ keys.push(fullKey);
167
+ }
168
+ }
169
+
170
+ return keys;
171
+ }
172
+
173
+ /**
174
+ * Initialize config with generated secrets
175
+ */
176
+ function initializeConfig(options = {}) {
177
+ const { v4: uuidv4 } = require('uuid');
178
+ const bcrypt = require('bcryptjs');
179
+
180
+ const config = { ...DEFAULT_CONFIG };
181
+
182
+ // Generate JWT secret
183
+ config.auth.jwt_secret = uuidv4() + '-' + uuidv4();
184
+
185
+ // Set user credentials
186
+ const username = options.username || 'tux';
187
+ const password = options.password || 'tux';
188
+
189
+ config.auth.user = username;
190
+ config.auth.pass_hash = bcrypt.hashSync(password, 10);
191
+
192
+ // Set workspaces
193
+ if (options.workspaces) {
194
+ config.workspaces.paths = options.workspaces;
195
+ }
196
+
197
+ // Set default workspace (explicit or first from list)
198
+ if (options.defaultWorkspace) {
199
+ config.workspaces.default = options.defaultWorkspace;
200
+ } else if (options.workspaces && options.workspaces.length > 0) {
201
+ config.workspaces.default = options.workspaces[0];
202
+ }
203
+
204
+ // Set port
205
+ if (options.port) {
206
+ config.server.port = options.port;
207
+ }
208
+
209
+ // Termux options
210
+ if (options.wakeLock !== undefined) {
211
+ config.termux.wake_lock = options.wakeLock;
212
+ }
213
+ if (options.notifications !== undefined) {
214
+ config.termux.notifications = options.notifications;
215
+ }
216
+ if (options.bootStart !== undefined) {
217
+ config.termux.boot_start = options.bootStart;
218
+ }
219
+
220
+ return saveConfig(config) ? config : null;
221
+ }
222
+
223
+ module.exports = {
224
+ DEFAULT_CONFIG,
225
+ loadConfig,
226
+ saveConfig,
227
+ getConfig,
228
+ isInitialized,
229
+ getConfigValue,
230
+ setConfigValue,
231
+ getConfigKeys,
232
+ initializeConfig
233
+ };
@@ -0,0 +1,20 @@
1
+ # NexusCLI Backend Configuration
2
+
3
+ # Port (NexusCLI Chaos Standard: 41800)
4
+ # 418 = ABRAHADABRA (Word of the Aeon, Crowley/Thelema)
5
+ # Represents the Great Work complete (Chaos Magic supreme formula)
6
+ # Range 41800-41899 (100 ports, UNIVERSAL compatibility, NO Elasticsearch conflict)
7
+ PORT=41800
8
+
9
+ # Database path (auto-detected for Linux/Termux)
10
+ # Linux: /var/lib/nexuscli/nexuscli.db
11
+ # Termux: $HOME/.nexuscli/nexuscli.db
12
+
13
+ # Node environment
14
+ NODE_ENV=production
15
+
16
+ # Workspace directory for CLI execution
17
+ WORKSPACE_DIR=/home/user/myproject
18
+
19
+ # Timeout for CLI commands (ms)
20
+ DEFAULT_TIMEOUT=30000