@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,942 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command Registry - Auto-discovery, registration, and Commander.js integration.
|
|
3
|
+
*
|
|
4
|
+
* The registry is the central hub that:
|
|
5
|
+
* 1. Discovers CommandDefinition files from the commands/ directory
|
|
6
|
+
* 2. Validates definitions at registration time
|
|
7
|
+
* 3. Builds a Commander.js program from registered definitions
|
|
8
|
+
* 4. Provides lookup for shell completion generation
|
|
9
|
+
* 5. Supports legacy command wrapping for incremental migration
|
|
10
|
+
*
|
|
11
|
+
* @module framework/command-registry
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import * as fs from 'fs';
|
|
15
|
+
import * as path from 'path';
|
|
16
|
+
|
|
17
|
+
import chalk from 'chalk';
|
|
18
|
+
import { Command } from 'commander';
|
|
19
|
+
|
|
20
|
+
import type {
|
|
21
|
+
CommandDefinition,
|
|
22
|
+
CommandModule,
|
|
23
|
+
CommandHook,
|
|
24
|
+
CommandCategory,
|
|
25
|
+
CommandContext,
|
|
26
|
+
CommandResult,
|
|
27
|
+
ValidationResult,
|
|
28
|
+
GlobalOptions,
|
|
29
|
+
} from './command-interface';
|
|
30
|
+
import { validationOk } from './command-interface';
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Types
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Options for the command registry.
|
|
38
|
+
*/
|
|
39
|
+
export interface RegistryOptions {
|
|
40
|
+
/**
|
|
41
|
+
* Whether to enable strict mode.
|
|
42
|
+
* In strict mode, duplicate command names cause an error.
|
|
43
|
+
* In non-strict mode, the later registration wins with a warning.
|
|
44
|
+
*/
|
|
45
|
+
strict?: boolean;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Base directory for auto-discovery.
|
|
49
|
+
* Defaults to the `commands/` directory relative to the framework.
|
|
50
|
+
*/
|
|
51
|
+
commandsDir?: string;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* File pattern for auto-discovery.
|
|
55
|
+
* Defaults to files ending in `.command.ts` or `.command.js`.
|
|
56
|
+
* Set to `*` to discover all .ts/.js files.
|
|
57
|
+
*/
|
|
58
|
+
filePattern?: RegExp;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Metadata about a registered command for introspection.
|
|
63
|
+
*/
|
|
64
|
+
export interface RegisteredCommand {
|
|
65
|
+
definition: CommandDefinition;
|
|
66
|
+
hooks: CommandHook[];
|
|
67
|
+
registeredAt: Date;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
// Command Registry
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
export class CommandRegistry {
|
|
75
|
+
private commands: Map<string, RegisteredCommand> = new Map();
|
|
76
|
+
private globalHooks: CommandHook[] = [];
|
|
77
|
+
private options: Required<RegistryOptions>;
|
|
78
|
+
|
|
79
|
+
constructor(options: RegistryOptions = {}) {
|
|
80
|
+
this.options = {
|
|
81
|
+
strict: options.strict ?? false,
|
|
82
|
+
commandsDir:
|
|
83
|
+
options.commandsDir ?? path.join(__dirname, '..', 'commands'),
|
|
84
|
+
filePattern: options.filePattern ?? /\.(command)\.(ts|js)$/,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// -------------------------------------------------------------------------
|
|
89
|
+
// Registration
|
|
90
|
+
// -------------------------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Register a single command definition.
|
|
94
|
+
*
|
|
95
|
+
* @param definition - The command to register
|
|
96
|
+
* @param hooks - Optional lifecycle hooks for this command
|
|
97
|
+
* @throws Error in strict mode if command name is already registered
|
|
98
|
+
*/
|
|
99
|
+
register(definition: CommandDefinition, hooks: CommandHook[] = []): void {
|
|
100
|
+
const name = definition.name;
|
|
101
|
+
|
|
102
|
+
if (this.commands.has(name)) {
|
|
103
|
+
if (this.options.strict) {
|
|
104
|
+
throw new Error(
|
|
105
|
+
`Command "${name}" is already registered. ` +
|
|
106
|
+
`Disable strict mode or use a different name.`
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
// Non-strict: warn and overwrite
|
|
110
|
+
console.warn(
|
|
111
|
+
chalk.yellow(`[registry] Overwriting existing command: ${name}`)
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Validate the definition at registration time
|
|
116
|
+
this.validateDefinition(definition);
|
|
117
|
+
|
|
118
|
+
this.commands.set(name, {
|
|
119
|
+
definition,
|
|
120
|
+
hooks,
|
|
121
|
+
registeredAt: new Date(),
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// Recursively register subcommands
|
|
125
|
+
if (definition.subcommands) {
|
|
126
|
+
for (const sub of definition.subcommands) {
|
|
127
|
+
// Prefix subcommand names with parent for flat lookup
|
|
128
|
+
const qualifiedName = `${name}:${sub.name}`;
|
|
129
|
+
const qualifiedSub = { ...sub, name: qualifiedName };
|
|
130
|
+
this.register(qualifiedSub, hooks);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Register a command module (definition + hooks bundle).
|
|
137
|
+
*/
|
|
138
|
+
registerModule(mod: CommandModule): void {
|
|
139
|
+
this.register(mod.command, mod.hooks);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Register a global hook that runs for all commands.
|
|
144
|
+
*/
|
|
145
|
+
registerGlobalHook(hook: CommandHook): void {
|
|
146
|
+
this.globalHooks.push(hook);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Remove a registered command.
|
|
151
|
+
*/
|
|
152
|
+
unregister(name: string): boolean {
|
|
153
|
+
return this.commands.delete(name);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// -------------------------------------------------------------------------
|
|
157
|
+
// Auto-Discovery
|
|
158
|
+
// -------------------------------------------------------------------------
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Discover and register commands from a directory.
|
|
162
|
+
*
|
|
163
|
+
* Scans the directory for files matching the configured pattern.
|
|
164
|
+
* Each file should export either:
|
|
165
|
+
* - A `module` property conforming to `CommandModule`
|
|
166
|
+
* - A `command` property conforming to `CommandDefinition`
|
|
167
|
+
* - A default export conforming to `CommandDefinition`
|
|
168
|
+
*
|
|
169
|
+
* @param directory - Directory to scan. Defaults to configured commandsDir.
|
|
170
|
+
* @returns Number of commands discovered and registered.
|
|
171
|
+
*/
|
|
172
|
+
async discoverCommands(directory?: string): Promise<number> {
|
|
173
|
+
const dir = directory ?? this.options.commandsDir;
|
|
174
|
+
let count = 0;
|
|
175
|
+
|
|
176
|
+
if (!fs.existsSync(dir)) {
|
|
177
|
+
console.warn(
|
|
178
|
+
chalk.yellow(`[registry] Commands directory not found: ${dir}`)
|
|
179
|
+
);
|
|
180
|
+
return 0;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
184
|
+
|
|
185
|
+
for (const entry of entries) {
|
|
186
|
+
if (!entry.isFile()) {
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Check file pattern
|
|
191
|
+
if (!this.options.filePattern.test(entry.name)) {
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const filePath = path.join(dir, entry.name);
|
|
196
|
+
|
|
197
|
+
try {
|
|
198
|
+
const exported = await this.loadModule(filePath);
|
|
199
|
+
|
|
200
|
+
if (exported.module && typeof exported.module === 'object') {
|
|
201
|
+
// CommandModule export
|
|
202
|
+
const mod = exported.module as CommandModule;
|
|
203
|
+
if (
|
|
204
|
+
mod.command &&
|
|
205
|
+
mod.command.name &&
|
|
206
|
+
typeof mod.command.execute === 'function'
|
|
207
|
+
) {
|
|
208
|
+
this.registerModule(mod);
|
|
209
|
+
count++;
|
|
210
|
+
}
|
|
211
|
+
} else if (exported.command && typeof exported.command === 'object') {
|
|
212
|
+
// Direct CommandDefinition export
|
|
213
|
+
const def = exported.command as CommandDefinition;
|
|
214
|
+
if (def.name && typeof def.execute === 'function') {
|
|
215
|
+
this.register(def);
|
|
216
|
+
count++;
|
|
217
|
+
}
|
|
218
|
+
} else if (exported.default && typeof exported.default === 'object') {
|
|
219
|
+
// Default export
|
|
220
|
+
const def = exported.default as CommandDefinition;
|
|
221
|
+
if (def.name && typeof def.execute === 'function') {
|
|
222
|
+
this.register(def);
|
|
223
|
+
count++;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
} catch (error) {
|
|
227
|
+
console.warn(
|
|
228
|
+
chalk.yellow(
|
|
229
|
+
`[registry] Failed to load command from ${entry.name}: ` +
|
|
230
|
+
`${error instanceof Error ? error.message : String(error)}`
|
|
231
|
+
)
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return count;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// -------------------------------------------------------------------------
|
|
240
|
+
// Multi-Directory Discovery
|
|
241
|
+
// -------------------------------------------------------------------------
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Discover commands from multiple directories.
|
|
245
|
+
*
|
|
246
|
+
* @param directories - Array of directory paths to scan
|
|
247
|
+
* @returns Total number of commands discovered
|
|
248
|
+
*/
|
|
249
|
+
async discoverFromDirectories(directories: string[]): Promise<number> {
|
|
250
|
+
let total = 0;
|
|
251
|
+
for (const dir of directories) {
|
|
252
|
+
total += await this.discoverCommands(dir);
|
|
253
|
+
}
|
|
254
|
+
return total;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// -------------------------------------------------------------------------
|
|
258
|
+
// Lookup
|
|
259
|
+
// -------------------------------------------------------------------------
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Get a registered command by name.
|
|
263
|
+
*/
|
|
264
|
+
get(name: string): CommandDefinition | undefined {
|
|
265
|
+
return this.commands.get(name)?.definition;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Find a command by name or alias.
|
|
270
|
+
* Searches direct name first, then aliases.
|
|
271
|
+
*/
|
|
272
|
+
findByNameOrAlias(nameOrAlias: string): CommandDefinition | undefined {
|
|
273
|
+
// Direct name lookup
|
|
274
|
+
const direct = this.commands.get(nameOrAlias);
|
|
275
|
+
if (direct) return direct.definition;
|
|
276
|
+
|
|
277
|
+
// Search aliases
|
|
278
|
+
for (const registered of Array.from(this.commands.values())) {
|
|
279
|
+
const { definition } = registered;
|
|
280
|
+
if (definition.aliases && definition.aliases.includes(nameOrAlias)) {
|
|
281
|
+
return definition;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return undefined;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Check if a command is registered.
|
|
290
|
+
*/
|
|
291
|
+
has(name: string): boolean {
|
|
292
|
+
return this.commands.has(name);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* List all registered commands, optionally filtered by category.
|
|
297
|
+
*/
|
|
298
|
+
list(category?: CommandCategory): CommandDefinition[] {
|
|
299
|
+
const all = Array.from(this.commands.values()).map(r => r.definition);
|
|
300
|
+
|
|
301
|
+
if (category) {
|
|
302
|
+
return all.filter(cmd => cmd.category === category);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return all;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* List all registered command names.
|
|
310
|
+
*/
|
|
311
|
+
names(): string[] {
|
|
312
|
+
return Array.from(this.commands.keys());
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Get commands grouped by category.
|
|
317
|
+
*/
|
|
318
|
+
grouped(): Map<CommandCategory | 'uncategorized', CommandDefinition[]> {
|
|
319
|
+
const groups = new Map<
|
|
320
|
+
CommandCategory | 'uncategorized',
|
|
321
|
+
CommandDefinition[]
|
|
322
|
+
>();
|
|
323
|
+
|
|
324
|
+
for (const registered of Array.from(this.commands.values())) {
|
|
325
|
+
const cat = registered.definition.category ?? 'uncategorized';
|
|
326
|
+
const existing = groups.get(cat) ?? [];
|
|
327
|
+
existing.push(registered.definition);
|
|
328
|
+
groups.set(cat, existing);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return groups;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Get all command names for shell completion.
|
|
336
|
+
*/
|
|
337
|
+
getCompletionWords(): string[] {
|
|
338
|
+
const words: string[] = [];
|
|
339
|
+
|
|
340
|
+
for (const registered of Array.from(this.commands.values())) {
|
|
341
|
+
if (registered.definition.hidden) {
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
words.push(registered.definition.name);
|
|
346
|
+
|
|
347
|
+
if (registered.definition.aliases) {
|
|
348
|
+
words.push(...registered.definition.aliases);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return words.sort();
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// -------------------------------------------------------------------------
|
|
356
|
+
// Commander.js Integration
|
|
357
|
+
// -------------------------------------------------------------------------
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Build a Commander.js program from all registered commands.
|
|
361
|
+
*
|
|
362
|
+
* This is the bridge between the registry's CommandDefinition world
|
|
363
|
+
* and Commander.js's imperative API. Call this once after all commands
|
|
364
|
+
* are registered.
|
|
365
|
+
*
|
|
366
|
+
* @param program - The root Commander.js Command instance
|
|
367
|
+
* @param contextFactory - Factory that creates a CommandContext for each invocation
|
|
368
|
+
*/
|
|
369
|
+
buildProgram(
|
|
370
|
+
program: Command,
|
|
371
|
+
contextFactory: (globalOpts: GlobalOptions) => CommandContext
|
|
372
|
+
): void {
|
|
373
|
+
for (const registered of Array.from(this.commands.values())) {
|
|
374
|
+
const { definition, hooks } = registered;
|
|
375
|
+
|
|
376
|
+
// Skip qualified subcommand names (they are handled by their parent)
|
|
377
|
+
if (definition.name.includes(':')) {
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Check for legacy factory
|
|
382
|
+
const legacyFactory = (
|
|
383
|
+
definition as CommandDefinition & { _legacyFactory?: () => Command }
|
|
384
|
+
)._legacyFactory;
|
|
385
|
+
|
|
386
|
+
if (legacyFactory) {
|
|
387
|
+
// Legacy command: add the pre-built Commander.Command directly
|
|
388
|
+
program.addCommand(legacyFactory());
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Build a new Commander.Command from the definition
|
|
393
|
+
const cmd = this.buildCommand(definition, hooks, contextFactory);
|
|
394
|
+
program.addCommand(cmd);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Build a single Commander.Command from a CommandDefinition.
|
|
400
|
+
*/
|
|
401
|
+
private buildCommand(
|
|
402
|
+
definition: CommandDefinition,
|
|
403
|
+
hooks: CommandHook[],
|
|
404
|
+
contextFactory: (globalOpts: GlobalOptions) => CommandContext
|
|
405
|
+
): Command {
|
|
406
|
+
const cmd = new Command(definition.name);
|
|
407
|
+
cmd.description(definition.description);
|
|
408
|
+
|
|
409
|
+
// Aliases
|
|
410
|
+
if (definition.aliases) {
|
|
411
|
+
for (const alias of definition.aliases) {
|
|
412
|
+
cmd.alias(alias);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Hidden - Commander.js doesn't expose hideHelp, so we use the internal approach
|
|
417
|
+
if (definition.hidden) {
|
|
418
|
+
(cmd as unknown as { _hidden: boolean })._hidden = true;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Arguments
|
|
422
|
+
if (definition.arguments) {
|
|
423
|
+
for (const arg of definition.arguments) {
|
|
424
|
+
const spec = arg.required
|
|
425
|
+
? arg.variadic
|
|
426
|
+
? `<${arg.name}...>`
|
|
427
|
+
: `<${arg.name}>`
|
|
428
|
+
: arg.variadic
|
|
429
|
+
? `[${arg.name}...]`
|
|
430
|
+
: `[${arg.name}]`;
|
|
431
|
+
cmd.argument(spec, arg.description, arg.defaultValue);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Options
|
|
436
|
+
if (definition.options) {
|
|
437
|
+
for (const opt of definition.options) {
|
|
438
|
+
if (opt.required) {
|
|
439
|
+
cmd.requiredOption(
|
|
440
|
+
opt.flags,
|
|
441
|
+
opt.description,
|
|
442
|
+
opt.defaultValue as string | boolean | undefined
|
|
443
|
+
);
|
|
444
|
+
} else {
|
|
445
|
+
cmd.option(
|
|
446
|
+
opt.flags,
|
|
447
|
+
opt.description,
|
|
448
|
+
opt.defaultValue as string | boolean | undefined
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if (opt.choices) {
|
|
453
|
+
// Commander.js doesn't have built-in choices on options;
|
|
454
|
+
// we handle this in validation instead.
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Examples in help text
|
|
460
|
+
if (definition.examples && definition.examples.length > 0) {
|
|
461
|
+
const examplesText = definition.examples
|
|
462
|
+
.map(
|
|
463
|
+
ex => ` ${chalk.green(ex.command)} ${chalk.gray(ex.description)}`
|
|
464
|
+
)
|
|
465
|
+
.join('\n');
|
|
466
|
+
|
|
467
|
+
cmd.addHelpText(
|
|
468
|
+
'after',
|
|
469
|
+
`\n${chalk.gray('Examples:')}\n${examplesText}\n`
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// Subcommands
|
|
474
|
+
if (definition.subcommands) {
|
|
475
|
+
for (const sub of definition.subcommands) {
|
|
476
|
+
const subCmd = this.buildCommand(sub, hooks, contextFactory);
|
|
477
|
+
cmd.addCommand(subCmd);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Action handler
|
|
482
|
+
cmd.action(async (...actionArgs: unknown[]) => {
|
|
483
|
+
// Commander passes positional args first, then options object, then the Command
|
|
484
|
+
const commanderCmd = actionArgs[actionArgs.length - 1] as Command;
|
|
485
|
+
const options = actionArgs[actionArgs.length - 2] as Record<
|
|
486
|
+
string,
|
|
487
|
+
unknown
|
|
488
|
+
>;
|
|
489
|
+
|
|
490
|
+
// Build positional args map
|
|
491
|
+
const args: Record<string, unknown> = {};
|
|
492
|
+
if (definition.arguments) {
|
|
493
|
+
for (let i = 0; i < definition.arguments.length; i++) {
|
|
494
|
+
const argDef = definition.arguments[i];
|
|
495
|
+
if (argDef) {
|
|
496
|
+
args[argDef.name] = actionArgs[i];
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Merge environment variable defaults into options
|
|
502
|
+
if (definition.options) {
|
|
503
|
+
for (const opt of definition.options) {
|
|
504
|
+
if (opt.envVar) {
|
|
505
|
+
const optName = this.flagsToOptionName(opt.flags);
|
|
506
|
+
if (options[optName] === undefined && process.env[opt.envVar]) {
|
|
507
|
+
options[optName] = process.env[opt.envVar];
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// Create execution context
|
|
514
|
+
const rootOpts = commanderCmd.parent?.opts() ?? commanderCmd.opts();
|
|
515
|
+
const globalOpts: GlobalOptions = {
|
|
516
|
+
verbose: !!rootOpts['verbose'],
|
|
517
|
+
quiet: !!rootOpts['quiet'],
|
|
518
|
+
json: !!rootOpts['json'] || !!options['json'],
|
|
519
|
+
noColor: !!rootOpts['noColor'],
|
|
520
|
+
dryRun: !!rootOpts['dryRun'] || !!options['dryRun'],
|
|
521
|
+
config: rootOpts['config'] as string | undefined,
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
const context = contextFactory(globalOpts);
|
|
525
|
+
|
|
526
|
+
try {
|
|
527
|
+
// Run pre-validate hooks
|
|
528
|
+
const allHooks = [...this.globalHooks, ...hooks];
|
|
529
|
+
await this.runHooks('preValidate', allHooks, definition, context);
|
|
530
|
+
|
|
531
|
+
// Validate
|
|
532
|
+
let validation: ValidationResult = validationOk();
|
|
533
|
+
if (definition.validate) {
|
|
534
|
+
validation = await definition.validate(args, options, context);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// Validate option choices
|
|
538
|
+
if (definition.options) {
|
|
539
|
+
for (const opt of definition.options) {
|
|
540
|
+
if (opt.choices) {
|
|
541
|
+
const optName = this.flagsToOptionName(opt.flags);
|
|
542
|
+
const value = options[optName];
|
|
543
|
+
if (value !== undefined && !opt.choices.includes(String(value))) {
|
|
544
|
+
validation = {
|
|
545
|
+
valid: false,
|
|
546
|
+
errors: [
|
|
547
|
+
...validation.errors,
|
|
548
|
+
{
|
|
549
|
+
field: optName,
|
|
550
|
+
message: `Invalid value "${value}" for --${optName}. Allowed: ${opt.choices.join(', ')}`,
|
|
551
|
+
suggestion: `Use one of: ${opt.choices.join(', ')}`,
|
|
552
|
+
},
|
|
553
|
+
],
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// Validate option conflicts
|
|
561
|
+
if (definition.options) {
|
|
562
|
+
for (const opt of definition.options) {
|
|
563
|
+
if (opt.conflicts) {
|
|
564
|
+
const optName = this.flagsToOptionName(opt.flags);
|
|
565
|
+
if (options[optName] !== undefined) {
|
|
566
|
+
for (const conflictName of opt.conflicts) {
|
|
567
|
+
if (options[conflictName] !== undefined) {
|
|
568
|
+
validation = {
|
|
569
|
+
valid: false,
|
|
570
|
+
errors: [
|
|
571
|
+
...validation.errors,
|
|
572
|
+
{
|
|
573
|
+
field: optName,
|
|
574
|
+
message: `Option --${optName} conflicts with --${conflictName}`,
|
|
575
|
+
suggestion: `Use either --${optName} or --${conflictName}, not both`,
|
|
576
|
+
},
|
|
577
|
+
],
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// Run post-validate hooks
|
|
587
|
+
await this.runHooks('postValidate', allHooks, definition, context);
|
|
588
|
+
|
|
589
|
+
if (!validation.valid) {
|
|
590
|
+
for (const err of validation.errors) {
|
|
591
|
+
console.error(
|
|
592
|
+
chalk.red(`Validation error [${err.field}]: ${err.message}`)
|
|
593
|
+
);
|
|
594
|
+
if (err.suggestion) {
|
|
595
|
+
console.error(chalk.yellow(` Suggestion: ${err.suggestion}`));
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
process.exitCode = 1;
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// Run pre-execute hooks
|
|
603
|
+
await this.runHooks('preExecute', allHooks, definition, context);
|
|
604
|
+
|
|
605
|
+
// Execute
|
|
606
|
+
const result = await definition.execute(args, options, context);
|
|
607
|
+
|
|
608
|
+
// Run post-execute hooks
|
|
609
|
+
await this.runHooks(
|
|
610
|
+
'postExecute',
|
|
611
|
+
allHooks,
|
|
612
|
+
definition,
|
|
613
|
+
context,
|
|
614
|
+
result
|
|
615
|
+
);
|
|
616
|
+
|
|
617
|
+
// Handle result
|
|
618
|
+
this.handleResult(result, context);
|
|
619
|
+
} catch (error) {
|
|
620
|
+
// Attempt rollback
|
|
621
|
+
if (definition.rollback && error instanceof Error) {
|
|
622
|
+
try {
|
|
623
|
+
await definition.rollback(error, context);
|
|
624
|
+
} catch (rollbackError) {
|
|
625
|
+
context.logger.error(
|
|
626
|
+
`Rollback failed: ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// Format error output
|
|
632
|
+
if (error instanceof Error) {
|
|
633
|
+
if (globalOpts.verbose) {
|
|
634
|
+
console.error(chalk.red(`\nCommand "${definition.name}" failed:`));
|
|
635
|
+
console.error(chalk.red(error.message));
|
|
636
|
+
if (error.stack) {
|
|
637
|
+
console.error(chalk.gray(error.stack));
|
|
638
|
+
}
|
|
639
|
+
} else {
|
|
640
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
641
|
+
console.error(chalk.gray(`Run with --verbose for details`));
|
|
642
|
+
}
|
|
643
|
+
} else {
|
|
644
|
+
console.error(chalk.red(`Error: ${String(error)}`));
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
process.exitCode = 1;
|
|
648
|
+
}
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
return cmd;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// -------------------------------------------------------------------------
|
|
655
|
+
// Shell Completion Generation
|
|
656
|
+
// -------------------------------------------------------------------------
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* Generate a bash completion script.
|
|
660
|
+
*/
|
|
661
|
+
generateBashCompletion(programName: string = 'wundr'): string {
|
|
662
|
+
const commands = this.getCompletionWords();
|
|
663
|
+
|
|
664
|
+
return `
|
|
665
|
+
# Bash completion for ${programName}
|
|
666
|
+
# Generated by @wundr/cli framework
|
|
667
|
+
|
|
668
|
+
_${programName}_completions() {
|
|
669
|
+
local cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
670
|
+
local commands="${commands.join(' ')}"
|
|
671
|
+
local global_opts="--verbose --quiet --json --no-color --dry-run --config --help --version"
|
|
672
|
+
|
|
673
|
+
if [[ \${COMP_CWORD} -eq 1 ]]; then
|
|
674
|
+
COMPREPLY=( $(compgen -W "\${commands} \${global_opts}" -- "\${cur}") )
|
|
675
|
+
else
|
|
676
|
+
local cmd="\${COMP_WORDS[1]}"
|
|
677
|
+
case "\${cmd}" in
|
|
678
|
+
${this.generateBashCaseClauses()}
|
|
679
|
+
*)
|
|
680
|
+
COMPREPLY=( $(compgen -W "\${global_opts}" -- "\${cur}") )
|
|
681
|
+
;;
|
|
682
|
+
esac
|
|
683
|
+
fi
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
complete -F _${programName}_completions ${programName}
|
|
687
|
+
`.trim();
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* Generate a zsh completion script.
|
|
692
|
+
*/
|
|
693
|
+
generateZshCompletion(programName: string = 'wundr'): string {
|
|
694
|
+
const commands = this.list()
|
|
695
|
+
.filter(cmd => !cmd.hidden && !cmd.name.includes(':'))
|
|
696
|
+
.map(cmd => `'${cmd.name}:${cmd.description.replace(/'/g, '')}'`)
|
|
697
|
+
.join('\n ');
|
|
698
|
+
|
|
699
|
+
return `
|
|
700
|
+
#compdef ${programName}
|
|
701
|
+
# Zsh completion for ${programName}
|
|
702
|
+
# Generated by @wundr/cli framework
|
|
703
|
+
|
|
704
|
+
_${programName}() {
|
|
705
|
+
local -a commands
|
|
706
|
+
commands=(
|
|
707
|
+
${commands}
|
|
708
|
+
)
|
|
709
|
+
|
|
710
|
+
_arguments -C \\
|
|
711
|
+
'--verbose[Enable verbose logging]' \\
|
|
712
|
+
'--quiet[Suppress output]' \\
|
|
713
|
+
'--json[Output as JSON]' \\
|
|
714
|
+
'--no-color[Disable colored output]' \\
|
|
715
|
+
'--dry-run[Show what would be done]' \\
|
|
716
|
+
'--config[Specify config file]:file:_files' \\
|
|
717
|
+
'-h[Show help]' \\
|
|
718
|
+
'-v[Show version]' \\
|
|
719
|
+
'1:command:->command' \\
|
|
720
|
+
'*::arg:->args'
|
|
721
|
+
|
|
722
|
+
case $state in
|
|
723
|
+
command)
|
|
724
|
+
_describe -t commands 'command' commands
|
|
725
|
+
;;
|
|
726
|
+
args)
|
|
727
|
+
case $words[1] in
|
|
728
|
+
${this.generateZshCaseClauses()}
|
|
729
|
+
esac
|
|
730
|
+
;;
|
|
731
|
+
esac
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
_${programName}
|
|
735
|
+
`.trim();
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// -------------------------------------------------------------------------
|
|
739
|
+
// Private Helpers
|
|
740
|
+
// -------------------------------------------------------------------------
|
|
741
|
+
|
|
742
|
+
/**
|
|
743
|
+
* Load a module from a file path. Handles both ESM and CJS.
|
|
744
|
+
*/
|
|
745
|
+
private async loadModule(filePath: string): Promise<Record<string, unknown>> {
|
|
746
|
+
try {
|
|
747
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
748
|
+
return require(filePath);
|
|
749
|
+
} catch {
|
|
750
|
+
// Fall back to dynamic import for ESM
|
|
751
|
+
return await import(filePath);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
/**
|
|
756
|
+
* Validate a command definition at registration time.
|
|
757
|
+
*/
|
|
758
|
+
private validateDefinition(definition: CommandDefinition): void {
|
|
759
|
+
if (!definition.name || typeof definition.name !== 'string') {
|
|
760
|
+
throw new Error('Command definition must have a non-empty name');
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
if (!definition.description || typeof definition.description !== 'string') {
|
|
764
|
+
throw new Error(`Command "${definition.name}" must have a description`);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
if (typeof definition.execute !== 'function') {
|
|
768
|
+
throw new Error(
|
|
769
|
+
`Command "${definition.name}" must have an execute function`
|
|
770
|
+
);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// Validate arguments don't have duplicates
|
|
774
|
+
if (definition.arguments) {
|
|
775
|
+
const names = new Set<string>();
|
|
776
|
+
for (const arg of definition.arguments) {
|
|
777
|
+
if (names.has(arg.name)) {
|
|
778
|
+
throw new Error(
|
|
779
|
+
`Command "${definition.name}" has duplicate argument name: ${arg.name}`
|
|
780
|
+
);
|
|
781
|
+
}
|
|
782
|
+
names.add(arg.name);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
/**
|
|
788
|
+
* Run hooks for a specific phase.
|
|
789
|
+
*/
|
|
790
|
+
private async runHooks(
|
|
791
|
+
phase: CommandHook['phase'],
|
|
792
|
+
hooks: CommandHook[],
|
|
793
|
+
command: CommandDefinition,
|
|
794
|
+
context: CommandContext,
|
|
795
|
+
result?: CommandResult
|
|
796
|
+
): Promise<void> {
|
|
797
|
+
const matching = hooks.filter(h => {
|
|
798
|
+
if (h.phase !== phase) return false;
|
|
799
|
+
if (h.commands && !h.commands.includes(command.name)) return false;
|
|
800
|
+
return true;
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
for (const hook of matching) {
|
|
804
|
+
const shouldContinue = await hook.handler(command, context, result);
|
|
805
|
+
if (
|
|
806
|
+
shouldContinue === false &&
|
|
807
|
+
(phase === 'preValidate' || phase === 'preExecute')
|
|
808
|
+
) {
|
|
809
|
+
throw new Error(`Command "${command.name}" aborted by ${phase} hook`);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
/**
|
|
815
|
+
* Handle a CommandResult by formatting output appropriately.
|
|
816
|
+
*/
|
|
817
|
+
private handleResult(result: CommandResult, context: CommandContext): void {
|
|
818
|
+
if (result.exitCode !== 0) {
|
|
819
|
+
if (result.message) {
|
|
820
|
+
console.error(chalk.red(result.message));
|
|
821
|
+
}
|
|
822
|
+
process.exitCode = result.exitCode;
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// Warnings
|
|
827
|
+
if (result.warnings && result.warnings.length > 0) {
|
|
828
|
+
for (const warning of result.warnings) {
|
|
829
|
+
console.error(chalk.yellow(`Warning: ${warning}`));
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
// JSON mode: output data directly
|
|
834
|
+
if (context.globalOptions.json && result.data !== undefined) {
|
|
835
|
+
const output =
|
|
836
|
+
typeof result.data === 'string'
|
|
837
|
+
? result.data
|
|
838
|
+
: JSON.stringify(result.data, null, 2);
|
|
839
|
+
console.log(output);
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// Quiet mode: no message output
|
|
844
|
+
if (context.globalOptions.quiet) {
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// Normal mode: output message
|
|
849
|
+
if (result.message) {
|
|
850
|
+
console.log(result.message);
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
/**
|
|
855
|
+
* Convert Commander.js flag specification to a camelCase option name.
|
|
856
|
+
* e.g., '-p, --port <number>' -> 'port'
|
|
857
|
+
* e.g., '--dry-run' -> 'dryRun'
|
|
858
|
+
*/
|
|
859
|
+
private flagsToOptionName(flags: string): string {
|
|
860
|
+
const match = flags.match(/--([a-z-]+)/);
|
|
861
|
+
if (!match || !match[1]) return flags;
|
|
862
|
+
|
|
863
|
+
return match[1].replace(/-([a-z])/g, (_, char) => char.toUpperCase());
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
/**
|
|
867
|
+
* Generate bash case clauses for subcommand completion.
|
|
868
|
+
*/
|
|
869
|
+
private generateBashCaseClauses(): string {
|
|
870
|
+
const clauses: string[] = [];
|
|
871
|
+
|
|
872
|
+
for (const registered of Array.from(this.commands.values())) {
|
|
873
|
+
const { definition } = registered;
|
|
874
|
+
if (definition.hidden || definition.name.includes(':')) continue;
|
|
875
|
+
|
|
876
|
+
const subNames: string[] = [];
|
|
877
|
+
const optFlags: string[] = [];
|
|
878
|
+
|
|
879
|
+
if (definition.subcommands) {
|
|
880
|
+
for (const sub of definition.subcommands) {
|
|
881
|
+
subNames.push(sub.name);
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
if (definition.options) {
|
|
886
|
+
for (const opt of definition.options) {
|
|
887
|
+
const longMatch = opt.flags.match(/--[a-z-]+/);
|
|
888
|
+
if (longMatch) optFlags.push(longMatch[0]);
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
const words = [...subNames, ...optFlags].join(' ');
|
|
893
|
+
if (words) {
|
|
894
|
+
clauses.push(
|
|
895
|
+
` ${definition.name})\n COMPREPLY=( $(compgen -W "${words}" -- "\${cur}") )\n ;;`
|
|
896
|
+
);
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
return clauses.join('\n');
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
/**
|
|
904
|
+
* Generate zsh case clauses for subcommand completion.
|
|
905
|
+
*/
|
|
906
|
+
private generateZshCaseClauses(): string {
|
|
907
|
+
const clauses: string[] = [];
|
|
908
|
+
|
|
909
|
+
for (const registered of Array.from(this.commands.values())) {
|
|
910
|
+
const { definition } = registered;
|
|
911
|
+
if (definition.hidden || definition.name.includes(':')) continue;
|
|
912
|
+
|
|
913
|
+
const args: string[] = [];
|
|
914
|
+
|
|
915
|
+
if (definition.subcommands) {
|
|
916
|
+
const subs = definition.subcommands
|
|
917
|
+
.map(s => `'${s.name}:${s.description.replace(/'/g, '')}'`)
|
|
918
|
+
.join(' ');
|
|
919
|
+
args.push(`_values 'subcommand' ${subs}`);
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
if (definition.options) {
|
|
923
|
+
for (const opt of definition.options) {
|
|
924
|
+
const longMatch = opt.flags.match(/--([a-z-]+)/);
|
|
925
|
+
if (longMatch) {
|
|
926
|
+
args.push(
|
|
927
|
+
`'--${longMatch[1]}[${opt.description.replace(/'/g, '')}]'`
|
|
928
|
+
);
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
if (args.length > 0) {
|
|
934
|
+
clauses.push(
|
|
935
|
+
` ${definition.name})\n _arguments ${args.join(' \\\n ')}\n ;;`
|
|
936
|
+
);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
return clauses.join('\n');
|
|
941
|
+
}
|
|
942
|
+
}
|