@wundr.io/cli 1.0.11 → 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.
- package/bin/wundr.js +8 -4
- package/package.json +23 -23
- package/src/ai/ai-service.ts +16 -17
- package/src/ai/claude-client.ts +16 -16
- package/src/ai/conversation-manager.ts +29 -29
- package/src/cli.ts +4 -4
- package/src/commands/ai.ts +246 -78
- package/src/commands/alignment.ts +74 -74
- package/src/commands/analyze-optimized.ts +111 -78
- package/src/commands/analyze.ts +14 -14
- package/src/commands/batch.ts +179 -42
- package/src/commands/chat.ts +37 -30
- package/src/commands/claude-init.ts +41 -45
- package/src/commands/claude-setup.ts +204 -119
- package/src/commands/computer-setup.ts +85 -43
- package/src/commands/create-command.ts +4 -4
- package/src/commands/create.ts +27 -27
- package/src/commands/dashboard.ts +24 -24
- package/src/commands/govern.ts +25 -25
- package/src/commands/governance.ts +34 -34
- package/src/commands/guardian.ts +56 -56
- package/src/commands/init.ts +25 -22
- package/src/commands/orchestrator.ts +68 -41
- package/src/commands/performance-optimizer.ts +34 -35
- package/src/commands/plugins.ts +27 -27
- package/src/commands/project-update.ts +175 -72
- package/src/commands/rag.ts +185 -78
- package/src/commands/session.ts +35 -35
- package/src/commands/setup.ts +40 -344
- package/src/commands/test-init.ts +3 -3
- package/src/commands/test.ts +4 -4
- package/src/commands/watch.ts +28 -29
- package/src/commands/worktree.ts +49 -49
- package/src/context/context-manager.ts +10 -10
- package/src/context/session-manager.ts +41 -41
- package/src/framework/command-interface.ts +520 -0
- package/src/framework/command-registry.ts +942 -0
- package/src/framework/completion-exporter.ts +383 -0
- package/src/framework/debug-logger.ts +519 -0
- package/src/framework/error-handler.ts +867 -0
- package/src/framework/help-generator.ts +540 -0
- package/src/framework/index.ts +169 -0
- package/src/framework/interactive-repl.ts +703 -0
- package/src/framework/output-formatter.ts +834 -0
- package/src/framework/progress-manager.ts +539 -0
- package/src/index.ts +4 -4
- package/src/interactive/interactive-mode.ts +16 -16
- package/src/lib/conflict-resolution.ts +799 -9
- package/src/lib/merge-strategy.ts +529 -7
- package/src/lib/safety-mechanisms.ts +422 -18
- package/src/lib/state-detection.ts +1015 -13
- package/src/nlp/command-mapper.ts +29 -29
- package/src/nlp/command-parser.ts +17 -17
- package/src/nlp/intent-classifier.ts +7 -7
- package/src/nlp/intent-parser.ts +54 -52
- package/src/plugins/plugin-manager.ts +61 -39
- package/src/tests/computer-setup-integration.test.ts +46 -15
- package/src/types/modules.d.ts +424 -1
- package/src/utils/backup-rollback-manager.ts +11 -8
- package/src/utils/config-manager.ts +3 -3
- package/src/utils/error-handler.ts +2 -2
- package/src/utils/logger.ts +22 -22
- package/templates/batch/ci-cd.yaml +7 -7
- package/test-suites/api/health.spec.ts +20 -23
- package/test-suites/helpers/test-config.ts +14 -13
- package/test-suites/ui/accessibility.spec.ts +27 -22
- package/test-suites/ui/smoke.spec.ts +26 -21
- package/LICENSE +0 -21
- package/dist/ai/ai-service.d.ts +0 -152
- package/dist/ai/ai-service.d.ts.map +0 -1
- package/dist/ai/ai-service.js +0 -430
- package/dist/ai/ai-service.js.map +0 -1
- package/dist/ai/claude-client.d.ts +0 -130
- package/dist/ai/claude-client.d.ts.map +0 -1
- package/dist/ai/claude-client.js +0 -340
- package/dist/ai/claude-client.js.map +0 -1
- package/dist/ai/conversation-manager.d.ts +0 -164
- package/dist/ai/conversation-manager.d.ts.map +0 -1
- package/dist/ai/conversation-manager.js +0 -614
- package/dist/ai/conversation-manager.js.map +0 -1
- package/dist/ai/index.d.ts +0 -5
- package/dist/ai/index.d.ts.map +0 -1
- package/dist/ai/index.js +0 -8
- package/dist/ai/index.js.map +0 -1
- package/dist/cli.d.ts +0 -36
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js +0 -192
- package/dist/cli.js.map +0 -1
- package/dist/commands/ai.d.ts +0 -89
- package/dist/commands/ai.d.ts.map +0 -1
- package/dist/commands/ai.js +0 -799
- package/dist/commands/ai.js.map +0 -1
- package/dist/commands/alignment.d.ts +0 -78
- package/dist/commands/alignment.d.ts.map +0 -1
- package/dist/commands/alignment.js +0 -817
- package/dist/commands/alignment.js.map +0 -1
- package/dist/commands/analyze-optimized.d.ts +0 -14
- package/dist/commands/analyze-optimized.d.ts.map +0 -1
- package/dist/commands/analyze-optimized.js +0 -600
- package/dist/commands/analyze-optimized.js.map +0 -1
- package/dist/commands/analyze.d.ts +0 -65
- package/dist/commands/analyze.d.ts.map +0 -1
- package/dist/commands/analyze.js +0 -435
- package/dist/commands/analyze.js.map +0 -1
- package/dist/commands/batch.d.ts +0 -71
- package/dist/commands/batch.d.ts.map +0 -1
- package/dist/commands/batch.js +0 -738
- package/dist/commands/batch.js.map +0 -1
- package/dist/commands/chat.d.ts +0 -71
- package/dist/commands/chat.d.ts.map +0 -1
- package/dist/commands/chat.js +0 -674
- package/dist/commands/chat.js.map +0 -1
- package/dist/commands/claude-init.d.ts +0 -28
- package/dist/commands/claude-init.d.ts.map +0 -1
- package/dist/commands/claude-init.js +0 -591
- package/dist/commands/claude-init.js.map +0 -1
- package/dist/commands/claude-setup.d.ts +0 -119
- package/dist/commands/claude-setup.d.ts.map +0 -1
- package/dist/commands/claude-setup.js +0 -1073
- package/dist/commands/claude-setup.js.map +0 -1
- package/dist/commands/computer-setup-commands.d.ts +0 -53
- package/dist/commands/computer-setup-commands.d.ts.map +0 -1
- package/dist/commands/computer-setup-commands.js +0 -705
- package/dist/commands/computer-setup-commands.js.map +0 -1
- package/dist/commands/computer-setup.d.ts +0 -7
- package/dist/commands/computer-setup.d.ts.map +0 -1
- package/dist/commands/computer-setup.js +0 -849
- package/dist/commands/computer-setup.js.map +0 -1
- package/dist/commands/create-command.d.ts +0 -7
- package/dist/commands/create-command.d.ts.map +0 -1
- package/dist/commands/create-command.js +0 -158
- package/dist/commands/create-command.js.map +0 -1
- package/dist/commands/create.d.ts +0 -74
- package/dist/commands/create.d.ts.map +0 -1
- package/dist/commands/create.js +0 -556
- package/dist/commands/create.js.map +0 -1
- package/dist/commands/dashboard.d.ts +0 -91
- package/dist/commands/dashboard.d.ts.map +0 -1
- package/dist/commands/dashboard.js +0 -538
- package/dist/commands/dashboard.js.map +0 -1
- package/dist/commands/govern.d.ts +0 -70
- package/dist/commands/govern.d.ts.map +0 -1
- package/dist/commands/govern.js +0 -481
- package/dist/commands/govern.js.map +0 -1
- package/dist/commands/governance.d.ts +0 -17
- package/dist/commands/governance.d.ts.map +0 -1
- package/dist/commands/governance.js +0 -703
- package/dist/commands/governance.js.map +0 -1
- package/dist/commands/guardian.d.ts +0 -20
- package/dist/commands/guardian.d.ts.map +0 -1
- package/dist/commands/guardian.js +0 -597
- package/dist/commands/guardian.js.map +0 -1
- package/dist/commands/init.d.ts +0 -59
- package/dist/commands/init.d.ts.map +0 -1
- package/dist/commands/init.js +0 -650
- package/dist/commands/init.js.map +0 -1
- package/dist/commands/orchestrator.d.ts +0 -7
- package/dist/commands/orchestrator.d.ts.map +0 -1
- package/dist/commands/orchestrator.js +0 -571
- package/dist/commands/orchestrator.js.map +0 -1
- package/dist/commands/performance-optimizer.d.ts +0 -30
- package/dist/commands/performance-optimizer.d.ts.map +0 -1
- package/dist/commands/performance-optimizer.js +0 -650
- package/dist/commands/performance-optimizer.js.map +0 -1
- package/dist/commands/plugins.d.ts +0 -87
- package/dist/commands/plugins.d.ts.map +0 -1
- package/dist/commands/plugins.js +0 -685
- package/dist/commands/plugins.js.map +0 -1
- package/dist/commands/rag.d.ts +0 -7
- package/dist/commands/rag.d.ts.map +0 -1
- package/dist/commands/rag.js +0 -748
- package/dist/commands/rag.js.map +0 -1
- package/dist/commands/session.d.ts +0 -41
- package/dist/commands/session.d.ts.map +0 -1
- package/dist/commands/session.js +0 -441
- package/dist/commands/session.js.map +0 -1
- package/dist/commands/setup.d.ts +0 -29
- package/dist/commands/setup.d.ts.map +0 -1
- package/dist/commands/setup.js +0 -397
- package/dist/commands/setup.js.map +0 -1
- package/dist/commands/test-init.d.ts +0 -9
- package/dist/commands/test-init.d.ts.map +0 -1
- package/dist/commands/test-init.js +0 -222
- package/dist/commands/test-init.js.map +0 -1
- package/dist/commands/test.d.ts +0 -25
- package/dist/commands/test.d.ts.map +0 -1
- package/dist/commands/test.js +0 -217
- package/dist/commands/test.js.map +0 -1
- package/dist/commands/vp.d.ts +0 -7
- package/dist/commands/vp.d.ts.map +0 -1
- package/dist/commands/vp.js +0 -571
- package/dist/commands/vp.js.map +0 -1
- package/dist/commands/watch.d.ts +0 -76
- package/dist/commands/watch.d.ts.map +0 -1
- package/dist/commands/watch.js +0 -613
- package/dist/commands/watch.js.map +0 -1
- package/dist/commands/worktree.d.ts +0 -63
- package/dist/commands/worktree.d.ts.map +0 -1
- package/dist/commands/worktree.js +0 -774
- package/dist/commands/worktree.js.map +0 -1
- package/dist/context/context-manager.d.ts +0 -155
- package/dist/context/context-manager.d.ts.map +0 -1
- package/dist/context/context-manager.js +0 -383
- package/dist/context/context-manager.js.map +0 -1
- package/dist/context/index.d.ts +0 -3
- package/dist/context/index.d.ts.map +0 -1
- package/dist/context/index.js +0 -6
- package/dist/context/index.js.map +0 -1
- package/dist/context/session-manager.d.ts +0 -207
- package/dist/context/session-manager.d.ts.map +0 -1
- package/dist/context/session-manager.js +0 -686
- package/dist/context/session-manager.js.map +0 -1
- package/dist/index.d.ts +0 -8
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -51
- package/dist/index.js.map +0 -1
- package/dist/interactive/interactive-mode.d.ts +0 -76
- package/dist/interactive/interactive-mode.d.ts.map +0 -1
- package/dist/interactive/interactive-mode.js +0 -732
- package/dist/interactive/interactive-mode.js.map +0 -1
- package/dist/nlp/command-mapper.d.ts +0 -174
- package/dist/nlp/command-mapper.d.ts.map +0 -1
- package/dist/nlp/command-mapper.js +0 -624
- package/dist/nlp/command-mapper.js.map +0 -1
- package/dist/nlp/command-parser.d.ts +0 -106
- package/dist/nlp/command-parser.d.ts.map +0 -1
- package/dist/nlp/command-parser.js +0 -417
- package/dist/nlp/command-parser.js.map +0 -1
- package/dist/nlp/index.d.ts +0 -5
- package/dist/nlp/index.d.ts.map +0 -1
- package/dist/nlp/index.js +0 -8
- package/dist/nlp/index.js.map +0 -1
- package/dist/nlp/intent-classifier.d.ts +0 -59
- package/dist/nlp/intent-classifier.d.ts.map +0 -1
- package/dist/nlp/intent-classifier.js +0 -384
- package/dist/nlp/intent-classifier.js.map +0 -1
- package/dist/nlp/intent-parser.d.ts +0 -152
- package/dist/nlp/intent-parser.d.ts.map +0 -1
- package/dist/nlp/intent-parser.js +0 -744
- package/dist/nlp/intent-parser.js.map +0 -1
- package/dist/plugins/plugin-manager.d.ts +0 -120
- package/dist/plugins/plugin-manager.d.ts.map +0 -1
- package/dist/plugins/plugin-manager.js +0 -595
- package/dist/plugins/plugin-manager.js.map +0 -1
- package/dist/types/index.d.ts +0 -224
- package/dist/types/index.d.ts.map +0 -1
- package/dist/types/index.js +0 -3
- package/dist/types/index.js.map +0 -1
- package/dist/utils/backup-rollback-manager.d.ts +0 -72
- package/dist/utils/backup-rollback-manager.d.ts.map +0 -1
- package/dist/utils/backup-rollback-manager.js +0 -289
- package/dist/utils/backup-rollback-manager.js.map +0 -1
- package/dist/utils/claude-config-installer.d.ts +0 -98
- package/dist/utils/claude-config-installer.d.ts.map +0 -1
- package/dist/utils/claude-config-installer.js +0 -678
- package/dist/utils/claude-config-installer.js.map +0 -1
- package/dist/utils/config-manager.d.ts +0 -73
- package/dist/utils/config-manager.d.ts.map +0 -1
- package/dist/utils/config-manager.js +0 -339
- package/dist/utils/config-manager.js.map +0 -1
- package/dist/utils/error-handler.d.ts +0 -46
- package/dist/utils/error-handler.d.ts.map +0 -1
- package/dist/utils/error-handler.js +0 -169
- package/dist/utils/error-handler.js.map +0 -1
- package/dist/utils/logger.d.ts +0 -25
- package/dist/utils/logger.d.ts.map +0 -1
- package/dist/utils/logger.js +0 -105
- package/dist/utils/logger.js.map +0 -1
- 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
|
+
}
|