@crystalplatforms/adm 0.2.3

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 (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +210 -0
  3. package/bin/adm +26 -0
  4. package/bin/adm-tui +13 -0
  5. package/package.json +62 -0
  6. package/scripts/completion/adm.bash +81 -0
  7. package/scripts/completion/adm.zsh +89 -0
  8. package/scripts/install.js +8 -0
  9. package/src/ai/knowledge.js +61 -0
  10. package/src/commands/connect.js +76 -0
  11. package/src/commands/dotfiles.js +72 -0
  12. package/src/commands/issue.js +80 -0
  13. package/src/commands/pr.js +55 -0
  14. package/src/commands/theme.js +103 -0
  15. package/src/commands/uninstall.js +72 -0
  16. package/src/commands/upgrade.js +114 -0
  17. package/src/config/index.js +32 -0
  18. package/src/data/extensions.json +358 -0
  19. package/src/installer/git-config.js +34 -0
  20. package/src/installer/node-installer.js +50 -0
  21. package/src/installer/package-manager-installer.js +31 -0
  22. package/src/installer/ssh-setup.js +31 -0
  23. package/src/installer/system-packages.js +49 -0
  24. package/src/installers/index.js +66 -0
  25. package/src/integrations/ai-backend.js +60 -0
  26. package/src/integrations/ai-providers/registry.js +261 -0
  27. package/src/integrations/github.js +107 -0
  28. package/src/integrations/gitlab.js +121 -0
  29. package/src/plugins/context.js +64 -0
  30. package/src/plugins/loader.js +50 -0
  31. package/src/plugins/runner.js +25 -0
  32. package/src/setup/catalog.js +13 -0
  33. package/src/setup/installer.js +192 -0
  34. package/src/setup/wizard.js +220 -0
  35. package/src/shell/detector.js +10 -0
  36. package/src/tui/app-state.js +374 -0
  37. package/src/tui/app.js +442 -0
  38. package/src/tui/autocomplete.js +57 -0
  39. package/src/tui/commands/registry.js +823 -0
  40. package/src/tui/components/SetupScreen.js +292 -0
  41. package/src/ui/animations.js +24 -0
  42. package/src/ui/spinner.js +34 -0
  43. package/src/ui/theme/cyberpunk.js +14 -0
  44. package/src/ui/theme/dark.js +14 -0
  45. package/src/ui/theme/forest.js +14 -0
  46. package/src/ui/theme/light.js +14 -0
  47. package/src/ui/theme/monokai.js +14 -0
  48. package/src/ui/theme/nord.js +14 -0
  49. package/src/ui/theme.js +49 -0
  50. package/src/utils/command-history.js +43 -0
  51. package/src/utils/dotfiles-sync.js +89 -0
  52. package/src/utils/error-handler.js +69 -0
  53. package/src/utils/file-conflict-handler.js +30 -0
  54. package/src/utils/fuzzy-search.js +20 -0
  55. package/src/utils/github-error-handler.js +26 -0
  56. package/src/utils/keychain.js +97 -0
  57. package/src/utils/open-url.js +15 -0
  58. package/src/utils/terminal-detection.js +26 -0
  59. package/src/utils/token-encryption.js +96 -0
@@ -0,0 +1,823 @@
1
+ const chalk = require('chalk');
2
+ const { listThemes, getTheme } = require('../../ui/theme');
3
+ const { readConfig, writeConfig } = require('../../config');
4
+ const { listProviders, getProvider } = require('../../integrations/ai-providers/registry');
5
+
6
+ const BUILTIN_COMMANDS = [
7
+ { name: 'help', description: 'Show command reference' },
8
+ { name: 'exit', description: 'Exit ADM' },
9
+ { name: 'clear', description: 'Clear message history' },
10
+ { name: 'theme', description: 'List or switch themes' },
11
+ { name: 'github', description: 'GitHub operations: status, pr, issue, commit', subcommands: ['status', 'pr', 'issue', 'commit'] },
12
+ { name: 'gitlab', description: 'GitLab operations: status, mr, issue, commit', subcommands: ['status', 'mr', 'issue', 'commit'] },
13
+ { name: 'ai', description: 'Toggle AI mode or ask a question' },
14
+ { name: 'model', description: 'Show or switch AI provider', subcommands: ['<provider-id>'] },
15
+ { name: 'download', description: 'Launch extension setup wizard' },
16
+ { name: 'feedback', description: 'Open feedback page in browser' },
17
+ { name: 'connect', description: 'Connect to GitHub or GitLab', subcommands: ['github', 'gitlab', 'list', 'disconnect'] },
18
+ { name: 'dotfiles', description: 'Sync dotfiles from repo', subcommands: ['sync'] },
19
+ { name: 'uninstall', description: 'Remove ADM CLI config' },
20
+ { name: 'upgrade', description: 'Check for and install updates' },
21
+ ];
22
+
23
+ function createRegistry(context = {}) {
24
+ const commands = new Map();
25
+ for (const cmd of BUILTIN_COMMANDS) {
26
+ commands.set(cmd.name, cmd);
27
+ }
28
+
29
+ async function dispatch(input) {
30
+ const trimmed = input.trim();
31
+ if (trimmed === '') {
32
+ return { output: '', shouldExit: false, shouldClear: false };
33
+ }
34
+
35
+ const stripped = trimmed.startsWith('/') ? trimmed.slice(1) : trimmed;
36
+ const parts = stripped.split(/\s+/);
37
+ const cmdName = parts[0];
38
+ const args = parts.slice(1).join(' ');
39
+
40
+ const cmd = commands.get(cmdName);
41
+ if (!cmd) {
42
+ // Redirect old commands to /github
43
+ if (['status', 'pr', 'issue', 'commit'].includes(cmdName)) {
44
+ return {
45
+ output: chalk.yellow(`/${cmdName} has moved. Use /github ${cmdName} instead.`),
46
+ shouldExit: false,
47
+ shouldClear: false,
48
+ };
49
+ }
50
+ // Try plugin fallback before reporting unknown command
51
+ const pluginResult = await dispatchPlugin(cmdName, args, context);
52
+ if (pluginResult) return pluginResult;
53
+
54
+ const suggestion = findClosestMatch(cmdName, commands);
55
+ let msg = chalk.red(`Unknown command: /${cmdName}.`);
56
+ if (suggestion) {
57
+ msg += chalk.yellow(` Did you mean /${suggestion}?`);
58
+ } else {
59
+ msg += chalk.gray(' Type /help for available commands.');
60
+ }
61
+ return { output: msg, shouldExit: false, shouldClear: false };
62
+ }
63
+
64
+ // Built-in handlers
65
+ if (cmdName === 'exit') {
66
+ return { output: chalk.gray('Goodbye!'), shouldExit: true, shouldClear: false };
67
+ }
68
+ if (cmdName === 'clear') {
69
+ return { output: chalk.green('Cleared.'), shouldExit: false, shouldClear: true };
70
+ }
71
+ if (cmdName === 'help') {
72
+ return dispatchHelp(commands);
73
+ }
74
+ if (cmdName === 'theme') {
75
+ return await dispatchTheme(args, context);
76
+ }
77
+ if (cmdName === 'github') {
78
+ return await dispatchGithub(args, context);
79
+ }
80
+ if (cmdName === 'gitlab') {
81
+ return await dispatchGitlab(args, context);
82
+ }
83
+ if (cmdName === 'ai') {
84
+ return dispatchAi(args, context);
85
+ }
86
+ if (cmdName === 'model') {
87
+ return await dispatchModel(args, context);
88
+ }
89
+ if (cmdName === 'download') {
90
+ return dispatchDownload(args);
91
+ }
92
+ if (cmdName === 'feedback') {
93
+ return dispatchFeedback(args, context);
94
+ }
95
+ if (cmdName === 'connect') {
96
+ return await dispatchConnect(args, context);
97
+ }
98
+ if (cmdName === 'dotfiles') {
99
+ return await dispatchDotfiles(args, context);
100
+ }
101
+ if (cmdName === 'uninstall') {
102
+ return await dispatchUninstall(context);
103
+ }
104
+ if (cmdName === 'upgrade') {
105
+ return await dispatchUpgrade(context);
106
+ }
107
+
108
+ return { output: `/${cmdName} not yet implemented`, shouldExit: false, shouldClear: false };
109
+ }
110
+
111
+ function autocomplete(partial) {
112
+ const p = partial.trim();
113
+ if (p === '') return [...commands.keys()];
114
+ return [...commands.keys()].filter(name => name.startsWith(p));
115
+ }
116
+
117
+ function getSubcommands(cmdName) {
118
+ const cmd = commands.get(cmdName);
119
+ return cmd ? cmd.subcommands : undefined;
120
+ }
121
+
122
+ function getCommandInfo(name) {
123
+ return commands.get(name) || null;
124
+ }
125
+
126
+ return { dispatch, autocomplete, commands, getSubcommands, getCommandInfo };
127
+ }
128
+
129
+ function dispatchHelp(commands) {
130
+ const lines = [chalk.bold('Available commands:\n')];
131
+ for (const [, cmd] of commands) {
132
+ lines.push(` ${chalk.green(cmd.name.padEnd(12))} ${cmd.description}`);
133
+ }
134
+ lines.push(`\nType ${chalk.bold('/help')} for commands. ${chalk.bold('/exit')} to quit.`);
135
+ return { output: lines.join('\n'), shouldExit: false, shouldClear: false };
136
+ }
137
+
138
+ async function dispatchTheme(args, context) {
139
+ if (!args) {
140
+ const themes = listThemes();
141
+ const lines = [chalk.bold('Available themes:\n')];
142
+ for (const t of themes) {
143
+ const marker = context.theme && context.theme.current === t.name ? ' ← active' : '';
144
+ lines.push(` ${chalk.cyan(t.name)}${marker}`);
145
+ }
146
+ lines.push(`\nType ${chalk.bold('/theme <name>')} to switch.`);
147
+ return { output: lines.join('\n'), shouldExit: false, shouldClear: false };
148
+ }
149
+
150
+ try {
151
+ getTheme(args); // throws if not found
152
+ if (context.theme) context.theme.current = args;
153
+ // Persist theme selection to config
154
+ const config = await readConfig();
155
+ config.theme = args;
156
+ await writeConfig(config);
157
+ return {
158
+ output: chalk.green(`Theme switched to ${chalk.bold(args)}.`),
159
+ shouldExit: false,
160
+ shouldClear: false,
161
+ };
162
+ } catch {
163
+ return {
164
+ output: chalk.red(`Unknown theme: ${args}. Type /theme to see available themes.`),
165
+ shouldExit: false,
166
+ shouldClear: false,
167
+ };
168
+ }
169
+ }
170
+
171
+ function dispatchStatus(context) {
172
+ const exec = context.execSync || require('child_process').execSync;
173
+ try {
174
+ const branch = exec('git rev-parse --abbrev-ref HEAD', { encoding: 'utf8' }).trim();
175
+ const porcelain = exec('git status --porcelain', { encoding: 'utf8' }).trim();
176
+
177
+ const lines = [`${chalk.bold('Branch:')} ${chalk.cyan(branch)}`];
178
+
179
+ if (porcelain === '') {
180
+ lines.push(chalk.green(' Working tree clean'));
181
+ } else {
182
+ const files = porcelain.split('\n').filter(Boolean);
183
+ lines.push(chalk.bold(` Modified (${files.length}):`));
184
+ for (const f of files) {
185
+ const status = f.substring(0, 2).trim();
186
+ const path = f.substring(3);
187
+ const color = status === '??' ? chalk.yellow : chalk.red;
188
+ lines.push(` ${color(status.padEnd(2))} ${path}`);
189
+ }
190
+ }
191
+
192
+ return { output: lines.join('\n'), shouldExit: false, shouldClear: false };
193
+ } catch {
194
+ return { output: chalk.yellow('Not in a git repo.'), shouldExit: false, shouldClear: false };
195
+ }
196
+ }
197
+
198
+ function getPlaceholderText(input, getSubcommandsFn) {
199
+ if (!input || !input.startsWith('/')) return null;
200
+ const match = input.match(/^\/(\w+) $/);
201
+ if (!match) return null;
202
+ const subs = getSubcommandsFn(match[1]);
203
+ if (!subs) return null;
204
+ return `<${subs.join(', ')}>`;
205
+ }
206
+
207
+ module.exports = { createRegistry, getPlaceholderText };
208
+
209
+ async function dispatchAi(args, context) {
210
+ if (!args) {
211
+ return { shouldToggleAI: true, shouldExit: false, shouldClear: false };
212
+ }
213
+
214
+ try {
215
+ const config = await readConfig();
216
+ const providerId = config.aiProvider || 'glm';
217
+ const apiKey = config[`ai.${providerId}Key`] || process.env.GLM_API_KEY;
218
+
219
+ if (!apiKey && providerId !== 'ollama') {
220
+ return { output: chalk.yellow(`AI not configured. No API key for ${providerId}. Use /model ${providerId} to set one.`), shouldExit: false, shouldClear: false };
221
+ }
222
+
223
+ const { queryWithProvider } = require('../../integrations/ai-providers/registry');
224
+ const response = await queryWithProvider(providerId, args, { apiKey });
225
+
226
+ const prefix = providerId === 'glm' ? 'GLM' : providerId;
227
+ return { output: `${prefix}: ${response}`, shouldToggleAI: true, shouldExit: false, shouldClear: false };
228
+ } catch (err) {
229
+ return { output: chalk.red(`AI error: ${err.message}`), shouldToggleAI: true, shouldExit: false, shouldClear: false };
230
+ }
231
+ }
232
+
233
+ async function dispatchModel(args, context) {
234
+ const config = await readConfig();
235
+ const currentProvider = config.aiProvider || 'glm';
236
+
237
+ if (!args || args === 'list') {
238
+ if (!args) {
239
+ const current = getProvider(currentProvider);
240
+ if (current) {
241
+ const lines = [chalk.bold(`Current provider: ${chalk.cyan(current.name)} (${currentProvider})`), ''];
242
+ const providers = listProviders();
243
+ for (const p of providers) {
244
+ const marker = p.id === currentProvider ? chalk.green(' ← active') : '';
245
+ lines.push(` ${chalk.cyan(p.id.padEnd(12))} ${p.name}${marker}`);
246
+ }
247
+ lines.push(`\nType ${chalk.bold('/model <id>')} to switch. ${chalk.bold('/model list')} for details.`);
248
+ return { output: lines.join('\n'), shouldExit: false, shouldClear: false };
249
+ }
250
+ }
251
+
252
+ const providers = listProviders();
253
+ const lines = [chalk.bold('Available AI providers:\n')];
254
+ for (const p of providers) {
255
+ const marker = p.id === currentProvider ? chalk.green(' ← active') : '';
256
+ const auth = p.requiresAuth ? chalk.yellow('(requires API key)') : chalk.gray('(free/local)');
257
+ lines.push(` ${chalk.cyan(p.id.padEnd(12))} ${p.name.padEnd(18)} ${auth}${marker}`);
258
+ }
259
+ lines.push(`\nType ${chalk.bold('/model <id>')} to switch provider.`);
260
+ return { output: lines.join('\n'), shouldExit: false, shouldClear: false };
261
+ }
262
+
263
+ const provider = getProvider(args);
264
+ if (!provider) {
265
+ return {
266
+ output: chalk.red(`Unknown provider: ${args}. Type /model list to see available providers.`),
267
+ shouldExit: false,
268
+ shouldClear: false,
269
+ };
270
+ }
271
+
272
+ // If provider requires auth and no key is stored, prompt for inline token input
273
+ if (provider.requiresAuth) {
274
+ const configKey = `ai.${args}Key`;
275
+ if (!config[configKey]) {
276
+ return {
277
+ output: null,
278
+ shouldPromptModelToken: true,
279
+ modelProvider: args,
280
+ shouldExit: false,
281
+ shouldClear: false,
282
+ };
283
+ }
284
+ }
285
+
286
+ config.aiProvider = args;
287
+ await writeConfig(config);
288
+
289
+ const providerSpecific = [];
290
+ if (args === 'ollama') {
291
+ providerSpecific.push(chalk.gray('Using default URL: http://localhost:11434'));
292
+ providerSpecific.push(chalk.gray('Change with: /config set ai.ollamaUrl <url>'));
293
+ }
294
+
295
+ const lines = [chalk.green(`Provider switched to ${chalk.bold(provider.name)} (${args}).`)];
296
+ if (providerSpecific.length) lines.push(...providerSpecific);
297
+
298
+ return { output: lines.join('\n'), shouldExit: false, shouldClear: false };
299
+ }
300
+
301
+ function levenshtein(a, b) {
302
+ const m = a.length, n = b.length;
303
+ const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
304
+ for (let i = 0; i <= m; i++) dp[i][0] = i;
305
+ for (let j = 0; j <= n; j++) dp[0][j] = j;
306
+ for (let i = 1; i <= m; i++) {
307
+ for (let j = 1; j <= n; j++) {
308
+ dp[i][j] = a[i - 1] === b[j - 1]
309
+ ? dp[i - 1][j - 1]
310
+ : 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
311
+ }
312
+ }
313
+ return dp[m][n];
314
+ }
315
+
316
+ function findClosestMatch(input, commands, maxDist = 2) {
317
+ let best = null, bestDist = Infinity;
318
+ for (const [name] of commands) {
319
+ const d = levenshtein(input, name);
320
+ if (d < bestDist) { bestDist = d; best = name; }
321
+ }
322
+ return bestDist <= maxDist ? best : null;
323
+ }
324
+
325
+ function dispatchDownload(args) {
326
+ const isDryRun = args === '--dry-run';
327
+ return {
328
+ output: isDryRun ? chalk.cyan('Setup dry-run mode — showing planned actions') : null,
329
+ shouldShowSetup: true,
330
+ shouldExit: false,
331
+ shouldClear: false,
332
+ dryRun: isDryRun,
333
+ };
334
+ }
335
+
336
+ const FEEDBACK_URL = 'https://crystalgames.studio/#/contact';
337
+
338
+ function dispatchFeedback(args, context) {
339
+ return {
340
+ output: chalk.cyan('Otworzyć stronę w przeglądarce? (Y/n)'),
341
+ needsConfirm: true,
342
+ confirmMessage: 'Open feedback page',
343
+ async onConfirm(deps = {}) {
344
+ const openUrl = deps.openUrl || require('../../utils/open-url').openUrl;
345
+ await openUrl(FEEDBACK_URL);
346
+ return { output: chalk.green('Opened feedback page.'), shouldExit: false, shouldClear: false };
347
+ },
348
+ onCancel(deps = {}) {
349
+ return { output: chalk.gray('Cancelled.'), shouldExit: false, shouldClear: false };
350
+ },
351
+ shouldExit: false,
352
+ shouldClear: false,
353
+ };
354
+ }
355
+
356
+ // ─── /github ───────────────────────────────────────────────
357
+ async function dispatchGithub(args, context) {
358
+ const parts = (args || '').trim().split(/\s+/);
359
+ const subcommand = parts[0];
360
+
361
+ if (!subcommand) {
362
+ return {
363
+ output: null,
364
+ shouldExit: false,
365
+ shouldClear: false,
366
+ shouldStartGithub: true,
367
+ };
368
+ }
369
+
370
+ if (subcommand === 'status') {
371
+ return dispatchStatus(context);
372
+ }
373
+
374
+ if (subcommand === 'pr') {
375
+ const prArgs = parts.slice(1).join(' ');
376
+ return await dispatchPr(prArgs || 'list', context);
377
+ }
378
+
379
+ if (subcommand === 'issue') {
380
+ const issueArgs = parts.slice(1).join(' ');
381
+ return await dispatchIssue(issueArgs || 'list', context);
382
+ }
383
+
384
+ if (subcommand === 'commit') {
385
+ const commitArgs = parts.slice(1).join(' ');
386
+ return await dispatchCommit(commitArgs, context);
387
+ }
388
+
389
+ return { output: chalk.yellow(`Usage: /github <status|pr|issue|commit>`), shouldExit: false, shouldClear: false };
390
+ }
391
+
392
+ // ─── /gitlab ───────────────────────────────────────────────
393
+ async function dispatchGitlab(args, context) {
394
+ const parts = (args || '').trim().split(/\s+/);
395
+ const subcommand = parts[0];
396
+
397
+ if (!subcommand) {
398
+ return { output: chalk.yellow('Usage: /gitlab <status|mr|issue|commit>'), shouldExit: false, shouldClear: false };
399
+ }
400
+
401
+ if (subcommand === 'status') {
402
+ return dispatchStatus(context);
403
+ }
404
+
405
+ if (subcommand === 'mr') {
406
+ const mrArgs = parts.slice(1).join(' ');
407
+ return await dispatchGitlabMr(mrArgs || 'list', context);
408
+ }
409
+
410
+ if (subcommand === 'issue') {
411
+ const issueArgs = parts.slice(1).join(' ');
412
+ return await dispatchGitlabIssue(issueArgs || 'list', context);
413
+ }
414
+
415
+ if (subcommand === 'commit') {
416
+ const commitArgs = parts.slice(1).join(' ');
417
+ return await dispatchCommit(commitArgs, context);
418
+ }
419
+
420
+ return { output: chalk.yellow('Usage: /gitlab <status|mr|issue|commit>'), shouldExit: false, shouldClear: false };
421
+ }
422
+
423
+ // ─── /gitlab mr ────────────────────────────────────────────
424
+ async function dispatchGitlabMr(args, context) {
425
+ const parts = (args || '').trim().split(/\s+/);
426
+ const subcommand = parts[0];
427
+
428
+ if (!subcommand) {
429
+ return { output: chalk.yellow('Usage: /gitlab mr <list|draft|comment>'), shouldExit: false, shouldClear: false };
430
+ }
431
+
432
+ const gl = require('../../integrations/gitlab');
433
+
434
+ if (subcommand === 'list') {
435
+ try {
436
+ const mrs = await gl.listMRs();
437
+ if (mrs.length === 0) {
438
+ return { output: chalk.yellow('No open merge requests found.'), shouldExit: false, shouldClear: false };
439
+ }
440
+ const lines = mrs.map(mr =>
441
+ ` ${chalk.bold(`!${mr.iid}`)} ${mr.title}\n ${chalk.gray(mr.url)}`
442
+ );
443
+ return { output: lines.join('\n'), shouldExit: false, shouldClear: false };
444
+ } catch (err) {
445
+ return { output: chalk.red(err.message), shouldExit: false, shouldClear: false };
446
+ }
447
+ }
448
+
449
+ if (subcommand === 'draft') {
450
+ const title = parts.slice(1).join(' ');
451
+ if (!title) {
452
+ return { output: chalk.yellow('Usage: /gitlab mr draft <title>'), shouldExit: false, shouldClear: false };
453
+ }
454
+ try {
455
+ const result = await gl.createDraftMR(title);
456
+ return {
457
+ output: chalk.green(`Draft MR !${result.iid} created\n ${chalk.gray(result.url)}`),
458
+ shouldExit: false,
459
+ shouldClear: false,
460
+ };
461
+ } catch (err) {
462
+ return { output: chalk.red(err.message), shouldExit: false, shouldClear: false };
463
+ }
464
+ }
465
+
466
+ if (subcommand === 'comment') {
467
+ const mrIid = parts[1];
468
+ const message = parts.slice(2).join(' ');
469
+ if (!mrIid || !message) {
470
+ return { output: chalk.yellow('Usage: /gitlab mr comment <iid> <message>'), shouldExit: false, shouldClear: false };
471
+ }
472
+ try {
473
+ await gl.commentOnMR(mrIid, message);
474
+ return { output: chalk.green(`Comment added to MR !${mrIid}`), shouldExit: false, shouldClear: false };
475
+ } catch (err) {
476
+ return { output: chalk.red(err.message), shouldExit: false, shouldClear: false };
477
+ }
478
+ }
479
+
480
+ return { output: chalk.yellow(`Unknown MR subcommand: ${subcommand}. Type /gitlab mr <list|draft|comment>`), shouldExit: false, shouldClear: false };
481
+ }
482
+
483
+ // ─── /gitlab issue ─────────────────────────────────────────
484
+ async function dispatchGitlabIssue(args, context) {
485
+ const parts = (args || '').trim().split(/\s+/);
486
+ const subcommand = parts[0];
487
+
488
+ if (!subcommand || subcommand === 'list') {
489
+ const gl = require('../../integrations/gitlab');
490
+ try {
491
+ const issues = await gl.listIssues();
492
+ if (issues.length === 0) {
493
+ return { output: chalk.yellow('No issues found.'), shouldExit: false, shouldClear: false };
494
+ }
495
+ const lines = issues.map(i => ` ${chalk.bold(`#${i.iid}`)} ${i.title}\n ${chalk.gray(i.url)}`);
496
+ return { output: lines.join('\n'), shouldExit: false, shouldClear: false };
497
+ } catch (err) {
498
+ return { output: chalk.red(err.message), shouldExit: false, shouldClear: false };
499
+ }
500
+ }
501
+
502
+ return { output: chalk.yellow(`Unknown issue subcommand: ${subcommand}. Type /gitlab issue list`), shouldExit: false, shouldClear: false };
503
+ }
504
+ async function dispatchConnect(args, context) {
505
+ const parts = (args || '').trim().split(/\s+/);
506
+ const subcommand = parts[0];
507
+
508
+ if (!subcommand) {
509
+ return {
510
+ output: null,
511
+ shouldExit: false,
512
+ shouldClear: false,
513
+ shouldStartConnect: true,
514
+ };
515
+ }
516
+
517
+ if (subcommand === 'list') {
518
+ const { listStoredServices: listFn } = require('../../utils/keychain');
519
+ const services = await (context.listStoredServices || listFn)();
520
+ if (services.length === 0) {
521
+ return { output: chalk.yellow('No connected services.'), shouldExit: false, shouldClear: false };
522
+ }
523
+ const lines = services.map(s => ` ${chalk.green(s)}: connected`);
524
+ return { output: lines.join('\n'), shouldExit: false, shouldClear: false };
525
+ }
526
+
527
+ // Parse --token flag
528
+ const tokenFlagIdx = parts.indexOf('--token');
529
+ const token = tokenFlagIdx !== -1 && parts[tokenFlagIdx + 1] ? parts[tokenFlagIdx + 1] : null;
530
+
531
+ if (subcommand === 'github') {
532
+ if (!token) {
533
+ return {
534
+ output: chalk.yellow('Enter your GitHub PAT: /connect github --token <token>'),
535
+ shouldExit: false,
536
+ shouldClear: false,
537
+ shouldPromptToken: 'github',
538
+ };
539
+ }
540
+ try {
541
+ const gh = require('../../integrations/github');
542
+ const result = await gh.connect(token);
543
+ return {
544
+ output: chalk.green(`Connected to GitHub as ${chalk.bold(result.user.login)}`),
545
+ shouldExit: false,
546
+ shouldClear: false,
547
+ };
548
+ } catch (err) {
549
+ return { output: chalk.red(err.message), shouldExit: false, shouldClear: false };
550
+ }
551
+ }
552
+
553
+ if (subcommand === 'gitlab') {
554
+ if (!token) {
555
+ return {
556
+ output: chalk.yellow('Enter your GitLab access token: /connect gitlab --token <token>'),
557
+ shouldExit: false,
558
+ shouldClear: false,
559
+ shouldPromptToken: 'gitlab',
560
+ };
561
+ }
562
+ try {
563
+ const gl = require('../../integrations/gitlab');
564
+ const result = await gl.connect(token);
565
+ return {
566
+ output: chalk.green(`Connected to GitLab as ${chalk.bold(result.user.username)}`),
567
+ shouldExit: false,
568
+ shouldClear: false,
569
+ };
570
+ } catch (err) {
571
+ return { output: chalk.red(err.message), shouldExit: false, shouldClear: false };
572
+ }
573
+ }
574
+
575
+ if (subcommand === 'disconnect') {
576
+ const service = parts[1];
577
+ if (!service) {
578
+ return { output: chalk.yellow('Usage: /connect disconnect <github|gitlab>'), shouldExit: false, shouldClear: false };
579
+ }
580
+ try {
581
+ if (service === 'github') {
582
+ const gh = require('../../integrations/github');
583
+ await gh.disconnect();
584
+ } else if (service === 'gitlab') {
585
+ const gl = require('../../integrations/gitlab');
586
+ await gl.disconnect();
587
+ }
588
+ return { output: chalk.green(`${service} disconnected.`), shouldExit: false, shouldClear: false };
589
+ } catch (err) {
590
+ return { output: chalk.red(err.message), shouldExit: false, shouldClear: false };
591
+ }
592
+ }
593
+
594
+ return { output: chalk.yellow(`Unknown connect subcommand: ${subcommand}. Type /connect list, /connect github, /connect gitlab, or /connect disconnect <service>`), shouldExit: false, shouldClear: false };
595
+ }
596
+
597
+ // ─── /pr ───────────────────────────────────────────────────
598
+ async function dispatchPr(args, context) {
599
+ const parts = (args || '').trim().split(/\s+/);
600
+ const subcommand = parts[0];
601
+
602
+ if (!subcommand) {
603
+ return { output: chalk.yellow('Usage: /pr <list|draft|comment>'), shouldExit: false, shouldClear: false };
604
+ }
605
+
606
+ const gh = require('../../integrations/github');
607
+
608
+ if (subcommand === 'list') {
609
+ try {
610
+ const prs = await gh.listPRs();
611
+ if (prs.length === 0) {
612
+ return { output: chalk.yellow('No open pull requests found.'), shouldExit: false, shouldClear: false };
613
+ }
614
+ const lines = prs.map(pr =>
615
+ ` ${chalk.bold(`#${pr.number}`)} ${pr.title} ${chalk.gray(`(${pr.repo})`)} ${chalk.green(pr.state)}\n ${chalk.gray(pr.url)}`
616
+ );
617
+ return { output: lines.join('\n'), shouldExit: false, shouldClear: false };
618
+ } catch (err) {
619
+ return { output: chalk.red(err.message), shouldExit: false, shouldClear: false };
620
+ }
621
+ }
622
+
623
+ if (subcommand === 'draft') {
624
+ const title = parts.slice(1).join(' ');
625
+ if (!title) {
626
+ return { output: chalk.yellow('Usage: /pr draft <title>'), shouldExit: false, shouldClear: false };
627
+ }
628
+ try {
629
+ const result = await gh.createDraftPR(title);
630
+ return {
631
+ output: chalk.green(`Draft PR #${result.number} created\n ${chalk.gray(result.url)}`),
632
+ shouldExit: false,
633
+ shouldClear: false,
634
+ };
635
+ } catch (err) {
636
+ return { output: chalk.red(err.message), shouldExit: false, shouldClear: false };
637
+ }
638
+ }
639
+
640
+ if (subcommand === 'comment') {
641
+ const prNumber = parts[1];
642
+ const message = parts.slice(2).join(' ');
643
+ if (!prNumber || !message) {
644
+ return { output: chalk.yellow('Usage: /pr comment <number> <message>'), shouldExit: false, shouldClear: false };
645
+ }
646
+ try {
647
+ const result = await gh.commentOnPR(prNumber, message);
648
+ return {
649
+ output: chalk.green(`Comment added to PR #${prNumber}\n ${chalk.gray(result.url)}`),
650
+ shouldExit: false,
651
+ shouldClear: false,
652
+ };
653
+ } catch (err) {
654
+ return { output: chalk.red(err.message), shouldExit: false, shouldClear: false };
655
+ }
656
+ }
657
+
658
+ return { output: chalk.yellow(`Unknown PR subcommand: ${subcommand}. Type /pr list, /pr draft <title>, or /pr comment <pr> <msg>`), shouldExit: false, shouldClear: false };
659
+ }
660
+
661
+ // ─── /issue ────────────────────────────────────────────────
662
+ async function dispatchIssue(args, context) {
663
+ const parts = (args || '').trim().split(/\s+/);
664
+ const subcommand = parts[0];
665
+
666
+ if (!subcommand || subcommand === 'list') {
667
+ try {
668
+ const { listStoredServices: listFn } = require('../../utils/keychain');
669
+ const services = await (context.listStoredServices || listFn)();
670
+ const platforms = services.filter(s => ['github', 'gitlab'].includes(s));
671
+ if (platforms.length === 0) {
672
+ return { output: chalk.yellow('No connected platforms. Use /connect github or /connect gitlab.'), shouldExit: false, shouldClear: false };
673
+ }
674
+
675
+ const platform = platforms[0];
676
+
677
+ if (platform === 'gitlab') {
678
+ const gl = require('../../integrations/gitlab');
679
+ const issues = await gl.listIssues();
680
+ if (issues.length === 0) {
681
+ return { output: chalk.yellow('No issues found.'), shouldExit: false, shouldClear: false };
682
+ }
683
+ const lines = issues.map(i => ` ${chalk.bold(`#${i.iid}`)} ${i.title}\n ${chalk.gray(i.url)}`);
684
+ return { output: lines.join('\n'), shouldExit: false, shouldClear: false };
685
+ }
686
+
687
+ // GitHub
688
+ const gh = require('../../integrations/github');
689
+ const octokit = await gh.getClient();
690
+ const { data: user } = await octokit.users.getAuthenticated();
691
+ const { data } = await octokit.search.issuesAndPullRequests({
692
+ q: `author:${user.login} is:issue is:open`,
693
+ sort: 'updated',
694
+ order: 'desc',
695
+ per_page: 20,
696
+ });
697
+ const issues = data.items.map(issue => ({
698
+ iid: issue.number,
699
+ title: issue.title,
700
+ state: issue.state,
701
+ url: issue.html_url,
702
+ }));
703
+ if (issues.length === 0) {
704
+ return { output: chalk.yellow('No issues found.'), shouldExit: false, shouldClear: false };
705
+ }
706
+ const lines = issues.map(i => ` ${chalk.bold(`#${i.iid}`)} ${i.title}\n ${chalk.gray(i.url)}`);
707
+ return { output: lines.join('\n'), shouldExit: false, shouldClear: false };
708
+ } catch (err) {
709
+ return { output: chalk.red(err.message), shouldExit: false, shouldClear: false };
710
+ }
711
+ }
712
+
713
+ return { output: chalk.yellow(`Unknown issue subcommand: ${subcommand}. Type /issue list`), shouldExit: false, shouldClear: false };
714
+ }
715
+
716
+ // ─── /commit ───────────────────────────────────────────────
717
+ async function dispatchCommit(args, context) {
718
+ const parts = (args || '').trim().split(/\s+/);
719
+ const subcommand = parts[0];
720
+
721
+ if (!subcommand) {
722
+ return { output: chalk.yellow('Usage: /commit <suggest>'), shouldExit: false, shouldClear: false };
723
+ }
724
+
725
+ if (subcommand === 'suggest') {
726
+ const exec = context.execSync || require('child_process').execSync;
727
+ let diff;
728
+ try {
729
+ diff = exec('git diff --cached', { encoding: 'utf8' }).trim();
730
+ } catch {
731
+ return { output: chalk.yellow('Not in a git repo.'), shouldExit: false, shouldClear: false };
732
+ }
733
+
734
+ if (!diff) {
735
+ return { output: chalk.yellow('No staged changes. Use `git add` to stage files first.'), shouldExit: false, shouldClear: false };
736
+ }
737
+
738
+ const ai = context.ai;
739
+ if (!ai) {
740
+ return { output: chalk.yellow('AI not configured. Set GLM_API_KEY to use commit suggest.'), shouldExit: false, shouldClear: false };
741
+ }
742
+
743
+ try {
744
+ const prompt = `Based on this git diff, suggest a concise commit message (just the subject line, under 72 chars, using conventional commits format):\n\n${diff}`;
745
+ const message = await ai.query(prompt);
746
+ return { output: `${chalk.bold('Suggested commit message:')}\n ${message}`, shouldExit: false, shouldClear: false };
747
+ } catch (err) {
748
+ return { output: chalk.red(err.message), shouldExit: false, shouldClear: false };
749
+ }
750
+ }
751
+
752
+ return { output: chalk.yellow(`Unknown commit subcommand: ${subcommand}. Type /commit suggest`), shouldExit: false, shouldClear: false };
753
+ }
754
+
755
+ // ─── /dotfiles ─────────────────────────────────────────────
756
+ async function dispatchDotfiles(args, context) {
757
+ const parts = (args || '').trim().split(/\s+/);
758
+ const subcommand = parts[0];
759
+
760
+ if (!subcommand || subcommand === 'sync') {
761
+ try {
762
+ const { syncDotfilesCommand } = require('../../commands/dotfiles');
763
+ const result = await syncDotfilesCommand({ repo: parts[1] });
764
+ const total = result.symlinked.length + result.copied.length;
765
+ const lines = [chalk.green(`Synced ${total} files.`)];
766
+ if (result.skipped.length > 0) {
767
+ lines.push(chalk.yellow(`Skipped: ${result.skipped.join(', ')}`));
768
+ }
769
+ if (result.errors.length > 0) {
770
+ lines.push(chalk.red(`Errors: ${result.errors.map(e => e.file).join(', ')}`));
771
+ }
772
+ return { output: lines.join('\n'), shouldExit: false, shouldClear: false };
773
+ } catch (err) {
774
+ return { output: chalk.red(err.message), shouldExit: false, shouldClear: false };
775
+ }
776
+ }
777
+
778
+ return { output: chalk.yellow(`Unknown dotfiles subcommand: ${subcommand}. Type /dotfiles sync`), shouldExit: false, shouldClear: false };
779
+ }
780
+
781
+ // ─── /uninstall ────────────────────────────────────────────
782
+ async function dispatchUninstall(context) {
783
+ return {
784
+ output: chalk.yellow('This will remove ADM CLI and all its configuration. Continue? [y/N]'),
785
+ needsConfirm: true,
786
+ confirmMessage: 'This will remove ADM CLI and all its configuration.',
787
+ async onConfirm() {
788
+ try {
789
+ const { uninstall } = require('../../commands/uninstall');
790
+ await uninstall();
791
+ return { output: chalk.green('ADM CLI uninstalled.'), shouldExit: true, shouldClear: false };
792
+ } catch (err) {
793
+ return { output: chalk.red(`Uninstall failed: ${err.message}`), shouldExit: false, shouldClear: false };
794
+ }
795
+ },
796
+ onCancel() {
797
+ return { output: chalk.gray('Cancelled.'), shouldExit: false, shouldClear: false };
798
+ },
799
+ shouldExit: false,
800
+ shouldClear: false,
801
+ };
802
+ }
803
+
804
+ // ─── /upgrade ──────────────────────────────────────────────
805
+ async function dispatchUpgrade(context) {
806
+ const { upgrade } = require('../../commands/upgrade');
807
+ return await upgrade();
808
+ }
809
+
810
+ // ─── Plugin fallback ───────────────────────────────────────
811
+ async function dispatchPlugin(cmdName, args, context) {
812
+ try {
813
+ const { loadPlugins } = require('../../plugins/loader');
814
+ const plugins = loadPlugins();
815
+ const plugin = plugins.get(cmdName);
816
+ if (!plugin) return null;
817
+
818
+ const output = await plugin.execute(args, context);
819
+ return { output: typeof output === 'string' ? output : JSON.stringify(output), shouldExit: false, shouldClear: false };
820
+ } catch {
821
+ return null;
822
+ }
823
+ }