@wundr.io/cli 1.0.10 → 1.0.12

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 (269) hide show
  1. package/bin/wundr.js +8 -4
  2. package/package.json +23 -23
  3. package/src/ai/ai-service.ts +16 -17
  4. package/src/ai/claude-client.ts +16 -16
  5. package/src/ai/conversation-manager.ts +29 -29
  6. package/src/cli.ts +4 -4
  7. package/src/commands/ai.ts +246 -78
  8. package/src/commands/alignment.ts +74 -74
  9. package/src/commands/analyze-optimized.ts +111 -78
  10. package/src/commands/analyze.ts +14 -14
  11. package/src/commands/batch.ts +179 -42
  12. package/src/commands/chat.ts +37 -30
  13. package/src/commands/claude-init.ts +41 -45
  14. package/src/commands/claude-setup.ts +204 -119
  15. package/src/commands/computer-setup.ts +85 -43
  16. package/src/commands/create-command.ts +4 -4
  17. package/src/commands/create.ts +27 -27
  18. package/src/commands/dashboard.ts +24 -24
  19. package/src/commands/govern.ts +25 -25
  20. package/src/commands/governance.ts +34 -34
  21. package/src/commands/guardian.ts +56 -56
  22. package/src/commands/init.ts +25 -22
  23. package/src/commands/orchestrator.ts +68 -41
  24. package/src/commands/performance-optimizer.ts +34 -35
  25. package/src/commands/plugins.ts +27 -27
  26. package/src/commands/project-update.ts +175 -72
  27. package/src/commands/rag.ts +185 -78
  28. package/src/commands/session.ts +35 -35
  29. package/src/commands/setup.ts +40 -344
  30. package/src/commands/test-init.ts +3 -3
  31. package/src/commands/test.ts +4 -4
  32. package/src/commands/watch.ts +28 -29
  33. package/src/commands/worktree.ts +49 -49
  34. package/src/context/context-manager.ts +10 -10
  35. package/src/context/session-manager.ts +41 -41
  36. package/src/framework/command-interface.ts +520 -0
  37. package/src/framework/command-registry.ts +942 -0
  38. package/src/framework/completion-exporter.ts +383 -0
  39. package/src/framework/debug-logger.ts +519 -0
  40. package/src/framework/error-handler.ts +867 -0
  41. package/src/framework/help-generator.ts +540 -0
  42. package/src/framework/index.ts +169 -0
  43. package/src/framework/interactive-repl.ts +703 -0
  44. package/src/framework/output-formatter.ts +834 -0
  45. package/src/framework/progress-manager.ts +539 -0
  46. package/src/index.ts +4 -4
  47. package/src/interactive/interactive-mode.ts +16 -16
  48. package/src/lib/conflict-resolution.ts +799 -9
  49. package/src/lib/merge-strategy.ts +529 -7
  50. package/src/lib/safety-mechanisms.ts +422 -18
  51. package/src/lib/state-detection.ts +1015 -13
  52. package/src/nlp/command-mapper.ts +29 -29
  53. package/src/nlp/command-parser.ts +17 -17
  54. package/src/nlp/intent-classifier.ts +7 -7
  55. package/src/nlp/intent-parser.ts +54 -52
  56. package/src/plugins/plugin-manager.ts +61 -39
  57. package/src/tests/computer-setup-integration.test.ts +46 -15
  58. package/src/types/modules.d.ts +424 -1
  59. package/src/utils/backup-rollback-manager.ts +11 -8
  60. package/src/utils/config-manager.ts +3 -3
  61. package/src/utils/error-handler.ts +2 -2
  62. package/src/utils/logger.ts +22 -22
  63. package/templates/batch/ci-cd.yaml +7 -7
  64. package/test-suites/api/health.spec.ts +20 -23
  65. package/test-suites/helpers/test-config.ts +14 -13
  66. package/test-suites/ui/accessibility.spec.ts +27 -22
  67. package/test-suites/ui/smoke.spec.ts +26 -21
  68. package/LICENSE +0 -21
  69. package/dist/ai/ai-service.d.ts +0 -152
  70. package/dist/ai/ai-service.d.ts.map +0 -1
  71. package/dist/ai/ai-service.js +0 -430
  72. package/dist/ai/ai-service.js.map +0 -1
  73. package/dist/ai/claude-client.d.ts +0 -130
  74. package/dist/ai/claude-client.d.ts.map +0 -1
  75. package/dist/ai/claude-client.js +0 -340
  76. package/dist/ai/claude-client.js.map +0 -1
  77. package/dist/ai/conversation-manager.d.ts +0 -164
  78. package/dist/ai/conversation-manager.d.ts.map +0 -1
  79. package/dist/ai/conversation-manager.js +0 -614
  80. package/dist/ai/conversation-manager.js.map +0 -1
  81. package/dist/ai/index.d.ts +0 -5
  82. package/dist/ai/index.d.ts.map +0 -1
  83. package/dist/ai/index.js +0 -8
  84. package/dist/ai/index.js.map +0 -1
  85. package/dist/cli.d.ts +0 -36
  86. package/dist/cli.d.ts.map +0 -1
  87. package/dist/cli.js +0 -192
  88. package/dist/cli.js.map +0 -1
  89. package/dist/commands/ai.d.ts +0 -89
  90. package/dist/commands/ai.d.ts.map +0 -1
  91. package/dist/commands/ai.js +0 -799
  92. package/dist/commands/ai.js.map +0 -1
  93. package/dist/commands/alignment.d.ts +0 -78
  94. package/dist/commands/alignment.d.ts.map +0 -1
  95. package/dist/commands/alignment.js +0 -817
  96. package/dist/commands/alignment.js.map +0 -1
  97. package/dist/commands/analyze-optimized.d.ts +0 -14
  98. package/dist/commands/analyze-optimized.d.ts.map +0 -1
  99. package/dist/commands/analyze-optimized.js +0 -600
  100. package/dist/commands/analyze-optimized.js.map +0 -1
  101. package/dist/commands/analyze.d.ts +0 -65
  102. package/dist/commands/analyze.d.ts.map +0 -1
  103. package/dist/commands/analyze.js +0 -435
  104. package/dist/commands/analyze.js.map +0 -1
  105. package/dist/commands/batch.d.ts +0 -71
  106. package/dist/commands/batch.d.ts.map +0 -1
  107. package/dist/commands/batch.js +0 -738
  108. package/dist/commands/batch.js.map +0 -1
  109. package/dist/commands/chat.d.ts +0 -71
  110. package/dist/commands/chat.d.ts.map +0 -1
  111. package/dist/commands/chat.js +0 -674
  112. package/dist/commands/chat.js.map +0 -1
  113. package/dist/commands/claude-init.d.ts +0 -28
  114. package/dist/commands/claude-init.d.ts.map +0 -1
  115. package/dist/commands/claude-init.js +0 -591
  116. package/dist/commands/claude-init.js.map +0 -1
  117. package/dist/commands/claude-setup.d.ts +0 -119
  118. package/dist/commands/claude-setup.d.ts.map +0 -1
  119. package/dist/commands/claude-setup.js +0 -1073
  120. package/dist/commands/claude-setup.js.map +0 -1
  121. package/dist/commands/computer-setup-commands.d.ts +0 -53
  122. package/dist/commands/computer-setup-commands.d.ts.map +0 -1
  123. package/dist/commands/computer-setup-commands.js +0 -705
  124. package/dist/commands/computer-setup-commands.js.map +0 -1
  125. package/dist/commands/computer-setup.d.ts +0 -7
  126. package/dist/commands/computer-setup.d.ts.map +0 -1
  127. package/dist/commands/computer-setup.js +0 -849
  128. package/dist/commands/computer-setup.js.map +0 -1
  129. package/dist/commands/create-command.d.ts +0 -7
  130. package/dist/commands/create-command.d.ts.map +0 -1
  131. package/dist/commands/create-command.js +0 -158
  132. package/dist/commands/create-command.js.map +0 -1
  133. package/dist/commands/create.d.ts +0 -74
  134. package/dist/commands/create.d.ts.map +0 -1
  135. package/dist/commands/create.js +0 -556
  136. package/dist/commands/create.js.map +0 -1
  137. package/dist/commands/dashboard.d.ts +0 -91
  138. package/dist/commands/dashboard.d.ts.map +0 -1
  139. package/dist/commands/dashboard.js +0 -538
  140. package/dist/commands/dashboard.js.map +0 -1
  141. package/dist/commands/govern.d.ts +0 -70
  142. package/dist/commands/govern.d.ts.map +0 -1
  143. package/dist/commands/govern.js +0 -481
  144. package/dist/commands/govern.js.map +0 -1
  145. package/dist/commands/governance.d.ts +0 -17
  146. package/dist/commands/governance.d.ts.map +0 -1
  147. package/dist/commands/governance.js +0 -703
  148. package/dist/commands/governance.js.map +0 -1
  149. package/dist/commands/guardian.d.ts +0 -20
  150. package/dist/commands/guardian.d.ts.map +0 -1
  151. package/dist/commands/guardian.js +0 -597
  152. package/dist/commands/guardian.js.map +0 -1
  153. package/dist/commands/init.d.ts +0 -59
  154. package/dist/commands/init.d.ts.map +0 -1
  155. package/dist/commands/init.js +0 -650
  156. package/dist/commands/init.js.map +0 -1
  157. package/dist/commands/orchestrator.d.ts +0 -7
  158. package/dist/commands/orchestrator.d.ts.map +0 -1
  159. package/dist/commands/orchestrator.js +0 -571
  160. package/dist/commands/orchestrator.js.map +0 -1
  161. package/dist/commands/performance-optimizer.d.ts +0 -30
  162. package/dist/commands/performance-optimizer.d.ts.map +0 -1
  163. package/dist/commands/performance-optimizer.js +0 -650
  164. package/dist/commands/performance-optimizer.js.map +0 -1
  165. package/dist/commands/plugins.d.ts +0 -87
  166. package/dist/commands/plugins.d.ts.map +0 -1
  167. package/dist/commands/plugins.js +0 -685
  168. package/dist/commands/plugins.js.map +0 -1
  169. package/dist/commands/rag.d.ts +0 -7
  170. package/dist/commands/rag.d.ts.map +0 -1
  171. package/dist/commands/rag.js +0 -748
  172. package/dist/commands/rag.js.map +0 -1
  173. package/dist/commands/session.d.ts +0 -41
  174. package/dist/commands/session.d.ts.map +0 -1
  175. package/dist/commands/session.js +0 -441
  176. package/dist/commands/session.js.map +0 -1
  177. package/dist/commands/setup.d.ts +0 -29
  178. package/dist/commands/setup.d.ts.map +0 -1
  179. package/dist/commands/setup.js +0 -397
  180. package/dist/commands/setup.js.map +0 -1
  181. package/dist/commands/test-init.d.ts +0 -9
  182. package/dist/commands/test-init.d.ts.map +0 -1
  183. package/dist/commands/test-init.js +0 -222
  184. package/dist/commands/test-init.js.map +0 -1
  185. package/dist/commands/test.d.ts +0 -25
  186. package/dist/commands/test.d.ts.map +0 -1
  187. package/dist/commands/test.js +0 -217
  188. package/dist/commands/test.js.map +0 -1
  189. package/dist/commands/vp.d.ts +0 -7
  190. package/dist/commands/vp.d.ts.map +0 -1
  191. package/dist/commands/vp.js +0 -571
  192. package/dist/commands/vp.js.map +0 -1
  193. package/dist/commands/watch.d.ts +0 -76
  194. package/dist/commands/watch.d.ts.map +0 -1
  195. package/dist/commands/watch.js +0 -613
  196. package/dist/commands/watch.js.map +0 -1
  197. package/dist/commands/worktree.d.ts +0 -63
  198. package/dist/commands/worktree.d.ts.map +0 -1
  199. package/dist/commands/worktree.js +0 -774
  200. package/dist/commands/worktree.js.map +0 -1
  201. package/dist/context/context-manager.d.ts +0 -155
  202. package/dist/context/context-manager.d.ts.map +0 -1
  203. package/dist/context/context-manager.js +0 -383
  204. package/dist/context/context-manager.js.map +0 -1
  205. package/dist/context/index.d.ts +0 -3
  206. package/dist/context/index.d.ts.map +0 -1
  207. package/dist/context/index.js +0 -6
  208. package/dist/context/index.js.map +0 -1
  209. package/dist/context/session-manager.d.ts +0 -207
  210. package/dist/context/session-manager.d.ts.map +0 -1
  211. package/dist/context/session-manager.js +0 -686
  212. package/dist/context/session-manager.js.map +0 -1
  213. package/dist/index.d.ts +0 -8
  214. package/dist/index.d.ts.map +0 -1
  215. package/dist/index.js +0 -51
  216. package/dist/index.js.map +0 -1
  217. package/dist/interactive/interactive-mode.d.ts +0 -76
  218. package/dist/interactive/interactive-mode.d.ts.map +0 -1
  219. package/dist/interactive/interactive-mode.js +0 -732
  220. package/dist/interactive/interactive-mode.js.map +0 -1
  221. package/dist/nlp/command-mapper.d.ts +0 -174
  222. package/dist/nlp/command-mapper.d.ts.map +0 -1
  223. package/dist/nlp/command-mapper.js +0 -624
  224. package/dist/nlp/command-mapper.js.map +0 -1
  225. package/dist/nlp/command-parser.d.ts +0 -106
  226. package/dist/nlp/command-parser.d.ts.map +0 -1
  227. package/dist/nlp/command-parser.js +0 -417
  228. package/dist/nlp/command-parser.js.map +0 -1
  229. package/dist/nlp/index.d.ts +0 -5
  230. package/dist/nlp/index.d.ts.map +0 -1
  231. package/dist/nlp/index.js +0 -8
  232. package/dist/nlp/index.js.map +0 -1
  233. package/dist/nlp/intent-classifier.d.ts +0 -59
  234. package/dist/nlp/intent-classifier.d.ts.map +0 -1
  235. package/dist/nlp/intent-classifier.js +0 -384
  236. package/dist/nlp/intent-classifier.js.map +0 -1
  237. package/dist/nlp/intent-parser.d.ts +0 -152
  238. package/dist/nlp/intent-parser.d.ts.map +0 -1
  239. package/dist/nlp/intent-parser.js +0 -744
  240. package/dist/nlp/intent-parser.js.map +0 -1
  241. package/dist/plugins/plugin-manager.d.ts +0 -120
  242. package/dist/plugins/plugin-manager.d.ts.map +0 -1
  243. package/dist/plugins/plugin-manager.js +0 -595
  244. package/dist/plugins/plugin-manager.js.map +0 -1
  245. package/dist/types/index.d.ts +0 -224
  246. package/dist/types/index.d.ts.map +0 -1
  247. package/dist/types/index.js +0 -3
  248. package/dist/types/index.js.map +0 -1
  249. package/dist/utils/backup-rollback-manager.d.ts +0 -72
  250. package/dist/utils/backup-rollback-manager.d.ts.map +0 -1
  251. package/dist/utils/backup-rollback-manager.js +0 -289
  252. package/dist/utils/backup-rollback-manager.js.map +0 -1
  253. package/dist/utils/claude-config-installer.d.ts +0 -98
  254. package/dist/utils/claude-config-installer.d.ts.map +0 -1
  255. package/dist/utils/claude-config-installer.js +0 -678
  256. package/dist/utils/claude-config-installer.js.map +0 -1
  257. package/dist/utils/config-manager.d.ts +0 -73
  258. package/dist/utils/config-manager.d.ts.map +0 -1
  259. package/dist/utils/config-manager.js +0 -339
  260. package/dist/utils/config-manager.js.map +0 -1
  261. package/dist/utils/error-handler.d.ts +0 -46
  262. package/dist/utils/error-handler.d.ts.map +0 -1
  263. package/dist/utils/error-handler.js +0 -169
  264. package/dist/utils/error-handler.js.map +0 -1
  265. package/dist/utils/logger.d.ts +0 -25
  266. package/dist/utils/logger.d.ts.map +0 -1
  267. package/dist/utils/logger.js +0 -105
  268. package/dist/utils/logger.js.map +0 -1
  269. package/src/commands/computer-setup-commands.ts +0 -872
@@ -0,0 +1,703 @@
1
+ /**
2
+ * Interactive REPL - Read-Eval-Print Loop for the CLI.
3
+ *
4
+ * Provides:
5
+ * - Interactive command entry with readline
6
+ * - Command history with persistence
7
+ * - Tab completion for commands and options
8
+ * - Command aliases and shortcuts
9
+ * - Session context preservation
10
+ * - Graceful exit handling
11
+ *
12
+ * @module framework/interactive-repl
13
+ */
14
+
15
+ import * as fs from 'fs';
16
+ import * as os from 'os';
17
+ import * as path from 'path';
18
+ import * as readline from 'readline';
19
+
20
+ import chalk from 'chalk';
21
+
22
+ import type {
23
+ CommandContext,
24
+ CommandDefinition,
25
+ GlobalOptions,
26
+ } from './command-interface';
27
+ import type { CommandRegistry } from './command-registry';
28
+
29
+ // ---------------------------------------------------------------------------
30
+ // Types
31
+ // ---------------------------------------------------------------------------
32
+
33
+ /**
34
+ * Configuration for the REPL.
35
+ */
36
+ export interface ReplOptions {
37
+ /** Prompt string. Defaults to 'wundr> '. */
38
+ prompt?: string;
39
+
40
+ /** Path for persisting command history. */
41
+ historyFile?: string;
42
+
43
+ /** Maximum number of history entries. Defaults to 500. */
44
+ maxHistory?: number;
45
+
46
+ /** Whether to enable tab completion. Defaults to true. */
47
+ tabCompletion?: boolean;
48
+
49
+ /** Custom aliases mapping short forms to full commands. */
50
+ aliases?: Record<string, string>;
51
+
52
+ /** Welcome message shown on start. */
53
+ welcomeMessage?: string;
54
+
55
+ /** Factory to create a CommandContext for each command invocation. */
56
+ contextFactory?: (globalOpts: GlobalOptions) => CommandContext;
57
+ }
58
+
59
+ /**
60
+ * Command history entry.
61
+ */
62
+ export interface HistoryEntry {
63
+ command: string;
64
+ timestamp: Date;
65
+ success: boolean;
66
+ duration?: number;
67
+ }
68
+
69
+ // ---------------------------------------------------------------------------
70
+ // Default Aliases
71
+ // ---------------------------------------------------------------------------
72
+
73
+ const DEFAULT_ALIASES: Record<string, string> = {
74
+ s: 'status',
75
+ q: 'quit',
76
+ h: 'help',
77
+ '?': 'help',
78
+ ls: 'list',
79
+ ll: 'list --verbose',
80
+ cls: 'clear',
81
+ };
82
+
83
+ // ---------------------------------------------------------------------------
84
+ // Interactive REPL
85
+ // ---------------------------------------------------------------------------
86
+
87
+ export class InteractiveRepl {
88
+ private rl: readline.Interface | null = null;
89
+ private running: boolean = false;
90
+ private history: HistoryEntry[] = [];
91
+ private prompt: string;
92
+ private historyFile: string;
93
+ private maxHistory: number;
94
+ private aliases: Record<string, string>;
95
+ private tabCompletion: boolean;
96
+ private contextFactory?: (globalOpts: GlobalOptions) => CommandContext;
97
+
98
+ constructor(
99
+ private registry: CommandRegistry,
100
+ options: ReplOptions = {}
101
+ ) {
102
+ this.prompt = options.prompt ?? chalk.cyan('wundr') + chalk.gray('> ');
103
+ this.historyFile =
104
+ options.historyFile ?? path.join(os.homedir(), '.wundr_history');
105
+ this.maxHistory = options.maxHistory ?? 500;
106
+ this.tabCompletion = options.tabCompletion ?? true;
107
+ this.aliases = { ...DEFAULT_ALIASES, ...(options.aliases ?? {}) };
108
+ this.contextFactory = options.contextFactory;
109
+ }
110
+
111
+ // -------------------------------------------------------------------------
112
+ // Lifecycle
113
+ // -------------------------------------------------------------------------
114
+
115
+ /**
116
+ * Start the REPL loop.
117
+ */
118
+ async start(welcomeMessage?: string): Promise<void> {
119
+ if (this.running) return;
120
+ this.running = true;
121
+
122
+ // Load history
123
+ this.loadHistory();
124
+
125
+ // Create readline interface
126
+ this.rl = readline.createInterface({
127
+ input: process.stdin,
128
+ output: process.stderr, // Use stderr so stdout stays clean for piping
129
+ prompt: this.prompt,
130
+ terminal: process.stdin.isTTY === true,
131
+ completer: this.tabCompletion ? this.completer.bind(this) : undefined,
132
+ history: this.history.map(h => h.command),
133
+ historySize: this.maxHistory,
134
+ });
135
+
136
+ // Welcome message
137
+ if (welcomeMessage) {
138
+ process.stderr.write(welcomeMessage + '\n\n');
139
+ } else {
140
+ process.stderr.write(
141
+ chalk.cyan(
142
+ 'Interactive mode. Type "help" for commands, "quit" to exit.\n\n'
143
+ )
144
+ );
145
+ }
146
+
147
+ // Handle lines
148
+ this.rl.on('line', async (line: string) => {
149
+ const trimmed = line.trim();
150
+
151
+ if (!trimmed) {
152
+ this.rl?.prompt();
153
+ return;
154
+ }
155
+
156
+ await this.handleLine(trimmed);
157
+ this.rl?.prompt();
158
+ });
159
+
160
+ // Handle close
161
+ this.rl.on('close', () => {
162
+ this.stop();
163
+ });
164
+
165
+ // Handle SIGINT (Ctrl+C)
166
+ this.rl.on('SIGINT', () => {
167
+ process.stderr.write('\n(To exit, type "quit" or press Ctrl+D)\n');
168
+ this.rl?.prompt();
169
+ });
170
+
171
+ this.rl.prompt();
172
+
173
+ // Keep the process alive
174
+ return new Promise<void>(resolve => {
175
+ this.rl?.on('close', () => {
176
+ resolve();
177
+ });
178
+ });
179
+ }
180
+
181
+ /**
182
+ * Stop the REPL.
183
+ */
184
+ stop(): void {
185
+ if (!this.running) return;
186
+ this.running = false;
187
+
188
+ this.saveHistory();
189
+
190
+ if (this.rl) {
191
+ this.rl.close();
192
+ this.rl = null;
193
+ }
194
+
195
+ process.stderr.write(chalk.gray('\nGoodbye.\n'));
196
+ }
197
+
198
+ // -------------------------------------------------------------------------
199
+ // Command Handling
200
+ // -------------------------------------------------------------------------
201
+
202
+ /**
203
+ * Process a single input line.
204
+ */
205
+ private async handleLine(input: string): Promise<void> {
206
+ const startTime = Date.now();
207
+ let success = true;
208
+
209
+ try {
210
+ // Check built-in commands
211
+ if (this.handleBuiltinCommand(input)) {
212
+ this.recordHistory(input, true, Date.now() - startTime);
213
+ return;
214
+ }
215
+
216
+ // Resolve aliases
217
+ const resolved = this.resolveAlias(input);
218
+
219
+ // Parse into command + args
220
+ const parts = this.parseInput(resolved);
221
+ if (parts.length === 0) return;
222
+
223
+ const commandName = parts[0]!;
224
+ const args = parts.slice(1);
225
+
226
+ // Look up command
227
+ const command = this.findCommand(commandName);
228
+ if (!command) {
229
+ process.stderr.write(chalk.red(`Unknown command: ${commandName}\n`));
230
+ const suggestions = this.suggestCommands(commandName);
231
+ if (suggestions.length > 0) {
232
+ process.stderr.write(
233
+ chalk.yellow(`Did you mean: ${suggestions.join(', ')}?\n`)
234
+ );
235
+ }
236
+ success = false;
237
+ this.recordHistory(input, false, Date.now() - startTime);
238
+ return;
239
+ }
240
+
241
+ // Execute
242
+ await this.executeCommand(command, args);
243
+ } catch (error) {
244
+ success = false;
245
+ const message = error instanceof Error ? error.message : String(error);
246
+ process.stderr.write(chalk.red(`Error: ${message}\n`));
247
+ }
248
+
249
+ this.recordHistory(input, success, Date.now() - startTime);
250
+ }
251
+
252
+ /**
253
+ * Handle built-in REPL commands.
254
+ * Returns true if the input was a built-in command.
255
+ */
256
+ private handleBuiltinCommand(input: string): boolean {
257
+ const lower = input.toLowerCase();
258
+
259
+ switch (lower) {
260
+ case 'quit':
261
+ case 'exit':
262
+ this.stop();
263
+ return true;
264
+
265
+ case 'clear':
266
+ case 'cls':
267
+ process.stderr.write('\x1b[2J\x1b[H');
268
+ return true;
269
+
270
+ case 'help':
271
+ case '?':
272
+ this.showHelp();
273
+ return true;
274
+
275
+ case 'history':
276
+ this.showHistory();
277
+ return true;
278
+
279
+ case 'aliases':
280
+ this.showAliases();
281
+ return true;
282
+
283
+ default:
284
+ return false;
285
+ }
286
+ }
287
+
288
+ /**
289
+ * Execute a resolved command.
290
+ */
291
+ private async executeCommand(
292
+ command: CommandDefinition,
293
+ rawArgs: string[]
294
+ ): Promise<void> {
295
+ // Build args and options from raw input
296
+ const args: Record<string, unknown> = {};
297
+ const options: Record<string, unknown> = {};
298
+
299
+ let argIndex = 0;
300
+ for (let i = 0; i < rawArgs.length; i++) {
301
+ const token = rawArgs[i]!;
302
+
303
+ if (token.startsWith('--')) {
304
+ // Long option
305
+ const eqPos = token.indexOf('=');
306
+ if (eqPos !== -1) {
307
+ const key = this.camelCase(token.substring(2, eqPos));
308
+ options[key] = token.substring(eqPos + 1);
309
+ } else {
310
+ const key = this.camelCase(token.substring(2));
311
+ const next = rawArgs[i + 1];
312
+ if (next && !next.startsWith('-')) {
313
+ options[key] = next;
314
+ i++;
315
+ } else {
316
+ options[key] = true;
317
+ }
318
+ }
319
+ } else if (token.startsWith('-') && token.length === 2) {
320
+ // Short option
321
+ const key = token.substring(1);
322
+ const next = rawArgs[i + 1];
323
+ if (next && !next.startsWith('-')) {
324
+ options[key] = next;
325
+ i++;
326
+ } else {
327
+ options[key] = true;
328
+ }
329
+ } else {
330
+ // Positional arg
331
+ if (command.arguments && command.arguments[argIndex]) {
332
+ args[command.arguments[argIndex]!.name] = token;
333
+ argIndex++;
334
+ }
335
+ }
336
+ }
337
+
338
+ // Create context
339
+ const globalOpts: GlobalOptions = {
340
+ verbose: !!options['verbose'],
341
+ quiet: !!options['quiet'],
342
+ json: !!options['json'],
343
+ noColor: false,
344
+ dryRun: !!options['dryRun'],
345
+ };
346
+
347
+ if (this.contextFactory) {
348
+ const context = this.contextFactory(globalOpts);
349
+ const result = await command.execute(args, options, context);
350
+
351
+ if (result.message) {
352
+ process.stdout.write(result.message + '\n');
353
+ }
354
+
355
+ if (result.exitCode !== 0) {
356
+ process.stderr.write(
357
+ chalk.red(`Command exited with code ${result.exitCode}\n`)
358
+ );
359
+ }
360
+ } else {
361
+ process.stderr.write(
362
+ chalk.yellow('No context factory configured. Command not executed.\n')
363
+ );
364
+ }
365
+ }
366
+
367
+ // -------------------------------------------------------------------------
368
+ // Tab Completion
369
+ // -------------------------------------------------------------------------
370
+
371
+ /**
372
+ * Readline completer function.
373
+ */
374
+ private completer(line: string): [string[], string] {
375
+ const parts = line.split(/\s+/);
376
+ const current = parts[parts.length - 1] ?? '';
377
+
378
+ let completions: string[];
379
+
380
+ if (parts.length <= 1) {
381
+ // Complete command names
382
+ const allNames = [
383
+ ...this.registry.getCompletionWords(),
384
+ ...Object.keys(this.aliases),
385
+ 'help',
386
+ 'quit',
387
+ 'exit',
388
+ 'clear',
389
+ 'history',
390
+ 'aliases',
391
+ ];
392
+ completions = allNames.filter(name => name.startsWith(current));
393
+ } else {
394
+ // Complete options for the current command
395
+ const commandName = parts[0]!;
396
+ const command = this.findCommand(commandName);
397
+
398
+ if (command && current.startsWith('-')) {
399
+ const optFlags: string[] = [];
400
+ if (command.options) {
401
+ for (const opt of command.options) {
402
+ const match = opt.flags.match(/--([a-z-]+)/);
403
+ if (match) optFlags.push(`--${match[1]}`);
404
+ }
405
+ }
406
+ // Add global options
407
+ optFlags.push('--verbose', '--quiet', '--json', '--dry-run', '--help');
408
+ completions = optFlags.filter(f => f.startsWith(current));
409
+ } else if (command?.subcommands && !current.startsWith('-')) {
410
+ completions = command.subcommands
411
+ .map(s => s.name)
412
+ .filter(n => n.startsWith(current));
413
+ } else {
414
+ completions = [];
415
+ }
416
+ }
417
+
418
+ return [completions, current];
419
+ }
420
+
421
+ // -------------------------------------------------------------------------
422
+ // History
423
+ // -------------------------------------------------------------------------
424
+
425
+ /**
426
+ * Record a command in history.
427
+ */
428
+ private recordHistory(
429
+ command: string,
430
+ success: boolean,
431
+ duration: number
432
+ ): void {
433
+ this.history.push({
434
+ command,
435
+ timestamp: new Date(),
436
+ success,
437
+ duration,
438
+ });
439
+
440
+ // Trim to max
441
+ if (this.history.length > this.maxHistory) {
442
+ this.history = this.history.slice(-this.maxHistory);
443
+ }
444
+ }
445
+
446
+ /**
447
+ * Load history from file.
448
+ */
449
+ private loadHistory(): void {
450
+ try {
451
+ if (fs.existsSync(this.historyFile)) {
452
+ const content = fs.readFileSync(this.historyFile, 'utf-8');
453
+ const lines = content.trim().split('\n').filter(Boolean);
454
+ this.history = lines.map(line => ({
455
+ command: line,
456
+ timestamp: new Date(),
457
+ success: true,
458
+ }));
459
+ }
460
+ } catch {
461
+ // Silently ignore history load errors
462
+ }
463
+ }
464
+
465
+ /**
466
+ * Save history to file.
467
+ */
468
+ private saveHistory(): void {
469
+ try {
470
+ const dir = path.dirname(this.historyFile);
471
+ if (!fs.existsSync(dir)) {
472
+ fs.mkdirSync(dir, { recursive: true });
473
+ }
474
+
475
+ const content = this.history
476
+ .slice(-this.maxHistory)
477
+ .map(h => h.command)
478
+ .join('\n');
479
+ fs.writeFileSync(this.historyFile, content + '\n');
480
+ } catch {
481
+ // Silently ignore history save errors
482
+ }
483
+ }
484
+
485
+ /**
486
+ * Show command history.
487
+ */
488
+ private showHistory(): void {
489
+ const recent = this.history.slice(-20);
490
+ if (recent.length === 0) {
491
+ process.stderr.write(chalk.gray('No command history.\n'));
492
+ return;
493
+ }
494
+
495
+ process.stderr.write(chalk.white.bold('Recent commands:\n'));
496
+ for (let i = 0; i < recent.length; i++) {
497
+ const entry = recent[i]!;
498
+ const icon = entry.success ? chalk.green('+') : chalk.red('x');
499
+ const duration = entry.duration
500
+ ? chalk.gray(` (${entry.duration}ms)`)
501
+ : '';
502
+ process.stderr.write(` ${icon} ${entry.command}${duration}\n`);
503
+ }
504
+ }
505
+
506
+ // -------------------------------------------------------------------------
507
+ // Aliases
508
+ // -------------------------------------------------------------------------
509
+
510
+ /**
511
+ * Resolve an alias to the full command.
512
+ */
513
+ private resolveAlias(input: string): string {
514
+ const parts = input.split(/\s+/);
515
+ const first = parts[0]!;
516
+
517
+ if (this.aliases[first]) {
518
+ const expanded = this.aliases[first]!;
519
+ return [expanded, ...parts.slice(1)].join(' ');
520
+ }
521
+
522
+ return input;
523
+ }
524
+
525
+ /**
526
+ * Register a new alias.
527
+ */
528
+ addAlias(alias: string, command: string): void {
529
+ this.aliases[alias] = command;
530
+ }
531
+
532
+ /**
533
+ * Get all registered aliases.
534
+ */
535
+ getAliases(): Readonly<Record<string, string>> {
536
+ return this.aliases;
537
+ }
538
+
539
+ /**
540
+ * Show registered aliases.
541
+ */
542
+ private showAliases(): void {
543
+ process.stderr.write(chalk.white.bold('Aliases:\n'));
544
+ for (const [alias, command] of Object.entries(this.aliases)) {
545
+ process.stderr.write(
546
+ ` ${chalk.green(alias.padEnd(10))} -> ${command}\n`
547
+ );
548
+ }
549
+ }
550
+
551
+ // -------------------------------------------------------------------------
552
+ // Help
553
+ // -------------------------------------------------------------------------
554
+
555
+ /**
556
+ * Show REPL help.
557
+ */
558
+ private showHelp(): void {
559
+ process.stderr.write('\n');
560
+ process.stderr.write(chalk.white.bold('Built-in commands:\n'));
561
+ process.stderr.write(
562
+ ` ${chalk.green('help')} Show this help message\n`
563
+ );
564
+ process.stderr.write(
565
+ ` ${chalk.green('history')} Show command history\n`
566
+ );
567
+ process.stderr.write(
568
+ ` ${chalk.green('aliases')} Show command aliases\n`
569
+ );
570
+ process.stderr.write(` ${chalk.green('clear')} Clear the screen\n`);
571
+ process.stderr.write(
572
+ ` ${chalk.green('quit')} Exit interactive mode\n`
573
+ );
574
+ process.stderr.write('\n');
575
+
576
+ process.stderr.write(chalk.white.bold('Available commands:\n'));
577
+ const commands = this.registry
578
+ .list()
579
+ .filter(c => !c.hidden && !c.name.includes(':'));
580
+ for (const cmd of commands.slice(0, 15)) {
581
+ const aliases = cmd.aliases
582
+ ? chalk.gray(` (${cmd.aliases.join(', ')})`)
583
+ : '';
584
+ process.stderr.write(
585
+ ` ${chalk.green(cmd.name.padEnd(20))} ${cmd.description}${aliases}\n`
586
+ );
587
+ }
588
+
589
+ if (commands.length > 15) {
590
+ process.stderr.write(
591
+ chalk.gray(` ... and ${commands.length - 15} more commands\n`)
592
+ );
593
+ }
594
+ process.stderr.write('\n');
595
+ }
596
+
597
+ // -------------------------------------------------------------------------
598
+ // Private Helpers
599
+ // -------------------------------------------------------------------------
600
+
601
+ /**
602
+ * Find a command by name or alias.
603
+ */
604
+ private findCommand(name: string): CommandDefinition | undefined {
605
+ // Direct lookup
606
+ const direct = this.registry.get(name);
607
+ if (direct) return direct;
608
+
609
+ // Search by alias
610
+ for (const cmd of this.registry.list()) {
611
+ if (cmd.aliases && cmd.aliases.includes(name)) {
612
+ return cmd;
613
+ }
614
+ }
615
+
616
+ return undefined;
617
+ }
618
+
619
+ /**
620
+ * Suggest similar command names.
621
+ */
622
+ private suggestCommands(input: string): string[] {
623
+ const allNames = this.registry.names();
624
+ return allNames
625
+ .filter(name => {
626
+ // Simple edit-distance-like check
627
+ if (name.startsWith(input.substring(0, 2))) return true;
628
+ if (input.startsWith(name.substring(0, 2))) return true;
629
+ return this.levenshtein(input, name) <= 2;
630
+ })
631
+ .slice(0, 3);
632
+ }
633
+
634
+ /**
635
+ * Parse input respecting quoted strings.
636
+ */
637
+ private parseInput(input: string): string[] {
638
+ const tokens: string[] = [];
639
+ let current = '';
640
+ let inQuote: string | null = null;
641
+
642
+ for (let i = 0; i < input.length; i++) {
643
+ const char = input[i]!;
644
+
645
+ if (inQuote) {
646
+ if (char === inQuote) {
647
+ inQuote = null;
648
+ } else {
649
+ current += char;
650
+ }
651
+ } else if (char === '"' || char === "'") {
652
+ inQuote = char;
653
+ } else if (char === ' ' || char === '\t') {
654
+ if (current) {
655
+ tokens.push(current);
656
+ current = '';
657
+ }
658
+ } else {
659
+ current += char;
660
+ }
661
+ }
662
+
663
+ if (current) tokens.push(current);
664
+ return tokens;
665
+ }
666
+
667
+ /**
668
+ * Convert kebab-case to camelCase.
669
+ */
670
+ private camelCase(str: string): string {
671
+ return str.replace(/-([a-z])/g, (_, char) => char.toUpperCase());
672
+ }
673
+
674
+ /**
675
+ * Simple Levenshtein distance.
676
+ */
677
+ private levenshtein(a: string, b: string): number {
678
+ if (a.length === 0) return b.length;
679
+ if (b.length === 0) return a.length;
680
+
681
+ const matrix: number[][] = [];
682
+
683
+ for (let i = 0; i <= b.length; i++) {
684
+ matrix[i] = [i];
685
+ }
686
+ for (let j = 0; j <= a.length; j++) {
687
+ matrix[0]![j] = j;
688
+ }
689
+
690
+ for (let i = 1; i <= b.length; i++) {
691
+ for (let j = 1; j <= a.length; j++) {
692
+ const cost = a[j - 1] === b[i - 1] ? 0 : 1;
693
+ matrix[i]![j] = Math.min(
694
+ matrix[i - 1]![j]! + 1,
695
+ matrix[i]![j - 1]! + 1,
696
+ matrix[i - 1]![j - 1]! + cost
697
+ );
698
+ }
699
+ }
700
+
701
+ return matrix[b.length]![a.length]!;
702
+ }
703
+ }