@wundr.io/cli 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (213) hide show
  1. package/README.md +551 -0
  2. package/bin/wundr.js +39 -0
  3. package/dist/ai/ai-service.d.ts +152 -0
  4. package/dist/ai/ai-service.d.ts.map +1 -0
  5. package/dist/ai/ai-service.js +430 -0
  6. package/dist/ai/ai-service.js.map +1 -0
  7. package/dist/ai/claude-client.d.ts +130 -0
  8. package/dist/ai/claude-client.d.ts.map +1 -0
  9. package/dist/ai/claude-client.js +339 -0
  10. package/dist/ai/claude-client.js.map +1 -0
  11. package/dist/ai/conversation-manager.d.ts +164 -0
  12. package/dist/ai/conversation-manager.d.ts.map +1 -0
  13. package/dist/ai/conversation-manager.js +612 -0
  14. package/dist/ai/conversation-manager.js.map +1 -0
  15. package/dist/ai/index.d.ts +5 -0
  16. package/dist/ai/index.d.ts.map +1 -0
  17. package/dist/ai/index.js +8 -0
  18. package/dist/ai/index.js.map +1 -0
  19. package/dist/cli.d.ts +36 -0
  20. package/dist/cli.d.ts.map +1 -0
  21. package/dist/cli.js +173 -0
  22. package/dist/cli.js.map +1 -0
  23. package/dist/commands/ai.d.ts +89 -0
  24. package/dist/commands/ai.d.ts.map +1 -0
  25. package/dist/commands/ai.js +735 -0
  26. package/dist/commands/ai.js.map +1 -0
  27. package/dist/commands/analyze-optimized.d.ts +14 -0
  28. package/dist/commands/analyze-optimized.d.ts.map +1 -0
  29. package/dist/commands/analyze-optimized.js +437 -0
  30. package/dist/commands/analyze-optimized.js.map +1 -0
  31. package/dist/commands/analyze.d.ts +65 -0
  32. package/dist/commands/analyze.d.ts.map +1 -0
  33. package/dist/commands/analyze.js +435 -0
  34. package/dist/commands/analyze.js.map +1 -0
  35. package/dist/commands/batch.d.ts +71 -0
  36. package/dist/commands/batch.d.ts.map +1 -0
  37. package/dist/commands/batch.js +738 -0
  38. package/dist/commands/batch.js.map +1 -0
  39. package/dist/commands/chat.d.ts +71 -0
  40. package/dist/commands/chat.d.ts.map +1 -0
  41. package/dist/commands/chat.js +674 -0
  42. package/dist/commands/chat.js.map +1 -0
  43. package/dist/commands/claude-init.d.ts +28 -0
  44. package/dist/commands/claude-init.d.ts.map +1 -0
  45. package/dist/commands/claude-init.js +587 -0
  46. package/dist/commands/claude-init.js.map +1 -0
  47. package/dist/commands/claude-setup.d.ts +32 -0
  48. package/dist/commands/claude-setup.d.ts.map +1 -0
  49. package/dist/commands/claude-setup.js +570 -0
  50. package/dist/commands/claude-setup.js.map +1 -0
  51. package/dist/commands/computer-setup-commands.d.ts +39 -0
  52. package/dist/commands/computer-setup-commands.d.ts.map +1 -0
  53. package/dist/commands/computer-setup-commands.js +563 -0
  54. package/dist/commands/computer-setup-commands.js.map +1 -0
  55. package/dist/commands/computer-setup.d.ts +7 -0
  56. package/dist/commands/computer-setup.d.ts.map +1 -0
  57. package/dist/commands/computer-setup.js +481 -0
  58. package/dist/commands/computer-setup.js.map +1 -0
  59. package/dist/commands/create-command.d.ts +7 -0
  60. package/dist/commands/create-command.d.ts.map +1 -0
  61. package/dist/commands/create-command.js +158 -0
  62. package/dist/commands/create-command.js.map +1 -0
  63. package/dist/commands/create.d.ts +74 -0
  64. package/dist/commands/create.d.ts.map +1 -0
  65. package/dist/commands/create.js +556 -0
  66. package/dist/commands/create.js.map +1 -0
  67. package/dist/commands/dashboard.d.ts +91 -0
  68. package/dist/commands/dashboard.d.ts.map +1 -0
  69. package/dist/commands/dashboard.js +537 -0
  70. package/dist/commands/dashboard.js.map +1 -0
  71. package/dist/commands/govern.d.ts +70 -0
  72. package/dist/commands/govern.d.ts.map +1 -0
  73. package/dist/commands/govern.js +480 -0
  74. package/dist/commands/govern.js.map +1 -0
  75. package/dist/commands/init.d.ts +55 -0
  76. package/dist/commands/init.d.ts.map +1 -0
  77. package/dist/commands/init.js +584 -0
  78. package/dist/commands/init.js.map +1 -0
  79. package/dist/commands/performance-optimizer.d.ts +30 -0
  80. package/dist/commands/performance-optimizer.d.ts.map +1 -0
  81. package/dist/commands/performance-optimizer.js +649 -0
  82. package/dist/commands/performance-optimizer.js.map +1 -0
  83. package/dist/commands/plugins.d.ts +87 -0
  84. package/dist/commands/plugins.d.ts.map +1 -0
  85. package/dist/commands/plugins.js +685 -0
  86. package/dist/commands/plugins.js.map +1 -0
  87. package/dist/commands/setup.d.ts +29 -0
  88. package/dist/commands/setup.d.ts.map +1 -0
  89. package/dist/commands/setup.js +399 -0
  90. package/dist/commands/setup.js.map +1 -0
  91. package/dist/commands/test-init.d.ts +9 -0
  92. package/dist/commands/test-init.d.ts.map +1 -0
  93. package/dist/commands/test-init.js +222 -0
  94. package/dist/commands/test-init.js.map +1 -0
  95. package/dist/commands/test.d.ts +25 -0
  96. package/dist/commands/test.d.ts.map +1 -0
  97. package/dist/commands/test.js +217 -0
  98. package/dist/commands/test.js.map +1 -0
  99. package/dist/commands/watch.d.ts +76 -0
  100. package/dist/commands/watch.d.ts.map +1 -0
  101. package/dist/commands/watch.js +610 -0
  102. package/dist/commands/watch.js.map +1 -0
  103. package/dist/context/context-manager.d.ts +155 -0
  104. package/dist/context/context-manager.d.ts.map +1 -0
  105. package/dist/context/context-manager.js +383 -0
  106. package/dist/context/context-manager.js.map +1 -0
  107. package/dist/context/index.d.ts +3 -0
  108. package/dist/context/index.d.ts.map +1 -0
  109. package/dist/context/index.js +6 -0
  110. package/dist/context/index.js.map +1 -0
  111. package/dist/context/session-manager.d.ts +207 -0
  112. package/dist/context/session-manager.d.ts.map +1 -0
  113. package/dist/context/session-manager.js +682 -0
  114. package/dist/context/session-manager.js.map +1 -0
  115. package/dist/index.d.ts +8 -0
  116. package/dist/index.d.ts.map +1 -0
  117. package/dist/index.js +51 -0
  118. package/dist/index.js.map +1 -0
  119. package/dist/interactive/interactive-mode.d.ts +76 -0
  120. package/dist/interactive/interactive-mode.d.ts.map +1 -0
  121. package/dist/interactive/interactive-mode.js +730 -0
  122. package/dist/interactive/interactive-mode.js.map +1 -0
  123. package/dist/nlp/command-mapper.d.ts +174 -0
  124. package/dist/nlp/command-mapper.d.ts.map +1 -0
  125. package/dist/nlp/command-mapper.js +623 -0
  126. package/dist/nlp/command-mapper.js.map +1 -0
  127. package/dist/nlp/command-parser.d.ts +106 -0
  128. package/dist/nlp/command-parser.d.ts.map +1 -0
  129. package/dist/nlp/command-parser.js +416 -0
  130. package/dist/nlp/command-parser.js.map +1 -0
  131. package/dist/nlp/index.d.ts +5 -0
  132. package/dist/nlp/index.d.ts.map +1 -0
  133. package/dist/nlp/index.js +8 -0
  134. package/dist/nlp/index.js.map +1 -0
  135. package/dist/nlp/intent-classifier.d.ts +59 -0
  136. package/dist/nlp/intent-classifier.d.ts.map +1 -0
  137. package/dist/nlp/intent-classifier.js +384 -0
  138. package/dist/nlp/intent-classifier.js.map +1 -0
  139. package/dist/nlp/intent-parser.d.ts +152 -0
  140. package/dist/nlp/intent-parser.d.ts.map +1 -0
  141. package/dist/nlp/intent-parser.js +739 -0
  142. package/dist/nlp/intent-parser.js.map +1 -0
  143. package/dist/plugins/plugin-manager.d.ts +120 -0
  144. package/dist/plugins/plugin-manager.d.ts.map +1 -0
  145. package/dist/plugins/plugin-manager.js +595 -0
  146. package/dist/plugins/plugin-manager.js.map +1 -0
  147. package/dist/types/index.d.ts +224 -0
  148. package/dist/types/index.d.ts.map +1 -0
  149. package/dist/types/index.js +3 -0
  150. package/dist/types/index.js.map +1 -0
  151. package/dist/utils/config-manager.d.ts +73 -0
  152. package/dist/utils/config-manager.d.ts.map +1 -0
  153. package/dist/utils/config-manager.js +339 -0
  154. package/dist/utils/config-manager.js.map +1 -0
  155. package/dist/utils/error-handler.d.ts +46 -0
  156. package/dist/utils/error-handler.d.ts.map +1 -0
  157. package/dist/utils/error-handler.js +169 -0
  158. package/dist/utils/error-handler.js.map +1 -0
  159. package/dist/utils/logger.d.ts +25 -0
  160. package/dist/utils/logger.d.ts.map +1 -0
  161. package/dist/utils/logger.js +94 -0
  162. package/dist/utils/logger.js.map +1 -0
  163. package/package.json +119 -0
  164. package/src/ai/ai-service.ts +595 -0
  165. package/src/ai/claude-client.ts +490 -0
  166. package/src/ai/conversation-manager.ts +907 -0
  167. package/src/ai/index.ts +8 -0
  168. package/src/cli.ts +202 -0
  169. package/src/commands/ai.ts +995 -0
  170. package/src/commands/analyze-optimized.ts +641 -0
  171. package/src/commands/analyze.ts +576 -0
  172. package/src/commands/batch.ts +935 -0
  173. package/src/commands/chat.ts +876 -0
  174. package/src/commands/claude-init.ts +715 -0
  175. package/src/commands/claude-setup.ts +697 -0
  176. package/src/commands/computer-setup-commands.ts +709 -0
  177. package/src/commands/computer-setup.ts +565 -0
  178. package/src/commands/create-command.ts +175 -0
  179. package/src/commands/create.ts +727 -0
  180. package/src/commands/dashboard.ts +691 -0
  181. package/src/commands/govern.ts +635 -0
  182. package/src/commands/init.ts +677 -0
  183. package/src/commands/performance-optimizer.ts +864 -0
  184. package/src/commands/plugins.ts +848 -0
  185. package/src/commands/setup.ts +508 -0
  186. package/src/commands/test-init.ts +242 -0
  187. package/src/commands/test.ts +264 -0
  188. package/src/commands/watch.ts +755 -0
  189. package/src/context/context-manager.ts +546 -0
  190. package/src/context/index.ts +9 -0
  191. package/src/context/session-manager.ts +1019 -0
  192. package/src/index.ts +64 -0
  193. package/src/interactive/interactive-mode.ts +830 -0
  194. package/src/nlp/command-mapper.ts +885 -0
  195. package/src/nlp/command-parser.ts +564 -0
  196. package/src/nlp/index.ts +4 -0
  197. package/src/nlp/intent-classifier.ts +458 -0
  198. package/src/nlp/intent-parser.ts +1101 -0
  199. package/src/plugins/plugin-manager.ts +744 -0
  200. package/src/types/index.ts +252 -0
  201. package/src/types/modules.d.ts +56 -0
  202. package/src/utils/config-manager.ts +391 -0
  203. package/src/utils/error-handler.ts +192 -0
  204. package/src/utils/logger.ts +104 -0
  205. package/templates/batch/ci-cd.yaml +62 -0
  206. package/templates/component/{{fileName}}.test.tsx +17 -0
  207. package/templates/component/{{fileName}}.tsx +21 -0
  208. package/templates/service/{{fileName}}.ts +98 -0
  209. package/templates/wundr-test.config.js +0 -0
  210. package/test-suites/api/health.spec.ts +134 -0
  211. package/test-suites/helpers/test-config.ts +84 -0
  212. package/test-suites/ui/accessibility.spec.ts +102 -0
  213. package/test-suites/ui/smoke.spec.ts +92 -0
@@ -0,0 +1,935 @@
1
+ import { Command } from 'commander';
2
+ import fs from 'fs-extra';
3
+ import path from 'path';
4
+ import chalk from 'chalk';
5
+ import YAML from 'yaml';
6
+ import { Listr } from 'listr2';
7
+ import { ConfigManager } from '../utils/config-manager';
8
+ import { PluginManager } from '../plugins/plugin-manager';
9
+ import { logger } from '../utils/logger';
10
+ import { errorHandler } from '../utils/error-handler';
11
+ import { BatchJob, BatchCommand } from '../types';
12
+
13
+ /**
14
+ * Batch commands for YAML automation and batch processing
15
+ */
16
+ export class BatchCommands {
17
+ private runningJobs: Map<string, any> = new Map();
18
+
19
+ constructor(
20
+ private program: Command,
21
+ private configManager: ConfigManager,
22
+ private pluginManager: PluginManager
23
+ ) {
24
+ this.registerCommands();
25
+ }
26
+
27
+ private registerCommands(): void {
28
+ const batchCmd = this.program
29
+ .command('batch')
30
+ .description('batch processing and automation with YAML');
31
+
32
+ // Run batch job
33
+ batchCmd
34
+ .command('run <file>')
35
+ .description('run batch job from YAML file')
36
+ .option('--dry-run', 'show what would be executed without running')
37
+ .option('--parallel', 'run commands in parallel where possible')
38
+ .option('--continue-on-error', 'continue execution on command failures')
39
+ .option(
40
+ '--vars <vars>',
41
+ 'variables to pass to batch job (JSON or key=value)'
42
+ )
43
+ .option('--timeout <ms>', 'global timeout for batch job')
44
+ .action(async (file, options) => {
45
+ await this.runBatchJob(file, options);
46
+ });
47
+
48
+ // Create batch job
49
+ batchCmd
50
+ .command('create <name>')
51
+ .description('create a new batch job')
52
+ .option('--template <template>', 'batch job template')
53
+ .option('--commands <commands>', 'commands to include (comma-separated)')
54
+ .option('--interactive', 'create job interactively')
55
+ .action(async (name, options) => {
56
+ await this.createBatchJob(name, options);
57
+ });
58
+
59
+ // List batch jobs
60
+ batchCmd
61
+ .command('list')
62
+ .alias('ls')
63
+ .description('list available batch jobs')
64
+ .option('--path <path>', 'custom batch jobs directory')
65
+ .action(async options => {
66
+ await this.listBatchJobs(options);
67
+ });
68
+
69
+ // Validate batch job
70
+ batchCmd
71
+ .command('validate <file>')
72
+ .description('validate batch job YAML file')
73
+ .action(async file => {
74
+ await this.validateBatchJob(file);
75
+ });
76
+
77
+ // Stop running job
78
+ batchCmd
79
+ .command('stop <jobId>')
80
+ .description('stop a running batch job')
81
+ .action(async jobId => {
82
+ await this.stopBatchJob(jobId);
83
+ });
84
+
85
+ // Show job status
86
+ batchCmd
87
+ .command('status [jobId]')
88
+ .description('show batch job status')
89
+ .action(async jobId => {
90
+ await this.showJobStatus(jobId);
91
+ });
92
+
93
+ // Schedule batch job
94
+ batchCmd
95
+ .command('schedule <file>')
96
+ .description('schedule batch job execution')
97
+ .option('--cron <expression>', 'cron expression for scheduling')
98
+ .option('--interval <ms>', 'interval in milliseconds')
99
+ .option('--once', 'run once after delay')
100
+ .action(async (file, options) => {
101
+ await this.scheduleBatchJob(file, options);
102
+ });
103
+
104
+ // Export batch job
105
+ batchCmd
106
+ .command('export <file>')
107
+ .description('export batch job to different formats')
108
+ .option(
109
+ '--format <format>',
110
+ 'export format (json, shell, dockerfile)',
111
+ 'json'
112
+ )
113
+ .option('--output <path>', 'output file path')
114
+ .action(async (file, options) => {
115
+ await this.exportBatchJob(file, options);
116
+ });
117
+
118
+ // Import batch job
119
+ batchCmd
120
+ .command('import <file>')
121
+ .description('import batch job from different formats')
122
+ .option(
123
+ '--format <format>',
124
+ 'source format (json, shell, package-scripts)'
125
+ )
126
+ .option('--name <name>', 'batch job name')
127
+ .action(async (file, options) => {
128
+ await this.importBatchJob(file, options);
129
+ });
130
+
131
+ // Template management
132
+ batchCmd.command('template').description('manage batch job templates');
133
+
134
+ batchCmd
135
+ .command('template list')
136
+ .description('list available templates')
137
+ .action(async () => {
138
+ await this.listTemplates();
139
+ });
140
+
141
+ batchCmd
142
+ .command('template create <name>')
143
+ .description('create a new template')
144
+ .option('--from <file>', 'create template from existing batch job')
145
+ .action(async (name, options) => {
146
+ await this.createTemplate(name, options);
147
+ });
148
+ }
149
+
150
+ /**
151
+ * Run batch job from YAML file
152
+ */
153
+ private async runBatchJob(file: string, options: any): Promise<void> {
154
+ try {
155
+ logger.info(`Running batch job: ${chalk.cyan(file)}`);
156
+
157
+ // Load and validate batch job
158
+ const job = await this.loadBatchJob(file);
159
+ await this.validateJobStructure(job);
160
+
161
+ // Process variables
162
+ const processedJob = await this.processJobVariables(job, options.vars);
163
+
164
+ if (options.dryRun) {
165
+ await this.showDryRun(processedJob);
166
+ return;
167
+ }
168
+
169
+ // Create job ID and track execution
170
+ const jobId = `job-${Date.now()}`;
171
+ this.runningJobs.set(jobId, {
172
+ file,
173
+ job: processedJob,
174
+ startTime: Date.now(),
175
+ status: 'running',
176
+ });
177
+
178
+ try {
179
+ await this.executeBatchJob(processedJob, jobId, options);
180
+ logger.success(`Batch job completed: ${file}`);
181
+ } finally {
182
+ this.runningJobs.delete(jobId);
183
+ }
184
+ } catch (error) {
185
+ throw errorHandler.createError(
186
+ 'WUNDR_BATCH_RUN_FAILED',
187
+ 'Failed to run batch job',
188
+ { file, options },
189
+ true
190
+ );
191
+ }
192
+ }
193
+
194
+ /**
195
+ * Create a new batch job
196
+ */
197
+ private async createBatchJob(name: string, options: any): Promise<void> {
198
+ try {
199
+ logger.info(`Creating batch job: ${chalk.cyan(name)}`);
200
+
201
+ let job: BatchJob;
202
+
203
+ if (options.interactive) {
204
+ job = await this.createInteractiveBatchJob(name);
205
+ } else if (options.template) {
206
+ job = await this.createJobFromTemplate(name, options.template);
207
+ } else {
208
+ job = this.createBasicBatchJob(name, options);
209
+ }
210
+
211
+ const jobPath = path.join(
212
+ process.cwd(),
213
+ '.wundr',
214
+ 'batch',
215
+ `${name}.yaml`
216
+ );
217
+ await fs.ensureDir(path.dirname(jobPath));
218
+ await fs.writeFile(jobPath, YAML.stringify(job));
219
+
220
+ logger.success(`Batch job created: ${jobPath}`);
221
+ } catch (error) {
222
+ throw errorHandler.createError(
223
+ 'WUNDR_BATCH_CREATE_FAILED',
224
+ 'Failed to create batch job',
225
+ { name, options },
226
+ true
227
+ );
228
+ }
229
+ }
230
+
231
+ /**
232
+ * List available batch jobs
233
+ */
234
+ private async listBatchJobs(options: any): Promise<void> {
235
+ try {
236
+ const batchDir =
237
+ options.path || path.join(process.cwd(), '.wundr', 'batch');
238
+
239
+ if (!(await fs.pathExists(batchDir))) {
240
+ logger.info('No batch jobs directory found');
241
+ return;
242
+ }
243
+
244
+ const files = await fs.readdir(batchDir);
245
+ const yamlFiles = files.filter(
246
+ f => f.endsWith('.yaml') || f.endsWith('.yml')
247
+ );
248
+
249
+ if (yamlFiles.length === 0) {
250
+ logger.info('No batch jobs found');
251
+ return;
252
+ }
253
+
254
+ logger.info(`Batch jobs (${yamlFiles.length}):`);
255
+
256
+ const jobs: Array<{
257
+ Name: string;
258
+ Description: string;
259
+ Commands: number | string;
260
+ Parallel: string;
261
+ }> = [];
262
+ for (const file of yamlFiles) {
263
+ const filePath = path.join(batchDir, file);
264
+ try {
265
+ const job = await this.loadBatchJob(filePath);
266
+ jobs.push({
267
+ Name: path.basename(file, path.extname(file)),
268
+ Description: job.description || 'No description',
269
+ Commands: job.commands.length,
270
+ Parallel: job.parallel ? '✓' : '✗',
271
+ });
272
+ } catch (error) {
273
+ jobs.push({
274
+ Name: path.basename(file, path.extname(file)),
275
+ Description: 'Invalid YAML',
276
+ Commands: 'N/A',
277
+ Parallel: 'N/A',
278
+ });
279
+ }
280
+ }
281
+
282
+ console.table(jobs);
283
+ } catch (error) {
284
+ throw errorHandler.createError(
285
+ 'WUNDR_BATCH_LIST_FAILED',
286
+ 'Failed to list batch jobs',
287
+ { options },
288
+ true
289
+ );
290
+ }
291
+ }
292
+
293
+ /**
294
+ * Validate batch job YAML file
295
+ */
296
+ private async validateBatchJob(file: string): Promise<void> {
297
+ try {
298
+ logger.info(`Validating batch job: ${chalk.cyan(file)}`);
299
+
300
+ const job = await this.loadBatchJob(file);
301
+ const validation = await this.validateJobStructure(job);
302
+
303
+ if (validation.valid) {
304
+ logger.success('Batch job is valid ✓');
305
+
306
+ // Show job summary
307
+ console.log(chalk.blue('\nJob Summary:'));
308
+ console.log(`Name: ${job.name}`);
309
+ console.log(`Description: ${job.description || 'None'}`);
310
+ console.log(`Commands: ${job.commands.length}`);
311
+ console.log(`Parallel execution: ${job.parallel ? 'Yes' : 'No'}`);
312
+ console.log(`Continue on error: ${job.continueOnError ? 'Yes' : 'No'}`);
313
+ } else {
314
+ logger.error('Batch job validation failed:');
315
+ validation.errors.forEach(error => {
316
+ logger.error(` - ${error}`);
317
+ });
318
+ process.exit(1);
319
+ }
320
+ } catch (error) {
321
+ throw errorHandler.createError(
322
+ 'WUNDR_BATCH_VALIDATE_FAILED',
323
+ 'Failed to validate batch job',
324
+ { file },
325
+ true
326
+ );
327
+ }
328
+ }
329
+
330
+ /**
331
+ * Stop a running batch job
332
+ */
333
+ private async stopBatchJob(jobId: string): Promise<void> {
334
+ try {
335
+ const job = this.runningJobs.get(jobId);
336
+
337
+ if (!job) {
338
+ logger.warn(`Job not found: ${jobId}`);
339
+ return;
340
+ }
341
+
342
+ // Stop the job (implementation would depend on how jobs are executed)
343
+ job.status = 'stopped';
344
+ this.runningJobs.delete(jobId);
345
+
346
+ logger.success(`Batch job stopped: ${jobId}`);
347
+ } catch (error) {
348
+ throw errorHandler.createError(
349
+ 'WUNDR_BATCH_STOP_FAILED',
350
+ 'Failed to stop batch job',
351
+ { jobId },
352
+ true
353
+ );
354
+ }
355
+ }
356
+
357
+ /**
358
+ * Show job status
359
+ */
360
+ private async showJobStatus(jobId?: string): Promise<void> {
361
+ try {
362
+ if (jobId) {
363
+ const job = this.runningJobs.get(jobId);
364
+ if (!job) {
365
+ logger.warn(`Job not found: ${jobId}`);
366
+ return;
367
+ }
368
+
369
+ console.log(chalk.blue(`\nJob Status: ${jobId}`));
370
+ console.log(`File: ${job.file}`);
371
+ console.log(`Status: ${job.status}`);
372
+ console.log(`Started: ${new Date(job.startTime).toLocaleString()}`);
373
+ console.log(`Duration: ${Date.now() - job.startTime}ms`);
374
+ } else {
375
+ if (this.runningJobs.size === 0) {
376
+ logger.info('No running batch jobs');
377
+ return;
378
+ }
379
+
380
+ console.log(chalk.blue(`\nRunning Jobs (${this.runningJobs.size}):`));
381
+ const jobData = Array.from(this.runningJobs.entries()).map(
382
+ ([id, job]) => ({
383
+ ID: id,
384
+ File: path.basename(job.file),
385
+ Status: job.status,
386
+ Duration: `${Date.now() - job.startTime}ms`,
387
+ })
388
+ );
389
+
390
+ console.table(jobData);
391
+ }
392
+ } catch (error) {
393
+ throw errorHandler.createError(
394
+ 'WUNDR_BATCH_STATUS_FAILED',
395
+ 'Failed to show job status',
396
+ { jobId },
397
+ true
398
+ );
399
+ }
400
+ }
401
+
402
+ /**
403
+ * Schedule batch job execution
404
+ */
405
+ private async scheduleBatchJob(file: string, options: any): Promise<void> {
406
+ try {
407
+ logger.info(`Scheduling batch job: ${chalk.cyan(file)}`);
408
+
409
+ // Validate the job first
410
+ await this.loadBatchJob(file);
411
+
412
+ if (options.cron) {
413
+ logger.info(`Scheduled with cron: ${options.cron}`);
414
+ // Implementation would use a cron library
415
+ } else if (options.interval) {
416
+ logger.info(`Scheduled with interval: ${options.interval}ms`);
417
+ // Implementation would use setInterval
418
+ } else if (options.once) {
419
+ logger.info('Scheduled to run once');
420
+ // Implementation would use setTimeout
421
+ } else {
422
+ throw new Error('No scheduling option provided');
423
+ }
424
+
425
+ logger.success('Batch job scheduled successfully');
426
+ } catch (error) {
427
+ throw errorHandler.createError(
428
+ 'WUNDR_BATCH_SCHEDULE_FAILED',
429
+ 'Failed to schedule batch job',
430
+ { file, options },
431
+ true
432
+ );
433
+ }
434
+ }
435
+
436
+ /**
437
+ * Export batch job to different formats
438
+ */
439
+ private async exportBatchJob(file: string, options: any): Promise<void> {
440
+ try {
441
+ logger.info(`Exporting batch job: ${chalk.cyan(file)}`);
442
+
443
+ const job = await this.loadBatchJob(file);
444
+ let exportedContent: string;
445
+
446
+ switch (options.format) {
447
+ case 'json':
448
+ exportedContent = JSON.stringify(job, null, 2);
449
+ break;
450
+ case 'shell':
451
+ exportedContent = this.convertToShellScript(job);
452
+ break;
453
+ case 'dockerfile':
454
+ exportedContent = this.convertToDockerfile(job);
455
+ break;
456
+ default:
457
+ throw new Error(`Unsupported export format: ${options.format}`);
458
+ }
459
+
460
+ const outputPath =
461
+ options.output ||
462
+ `${path.basename(file, path.extname(file))}.${options.format}`;
463
+ await fs.writeFile(outputPath, exportedContent);
464
+
465
+ logger.success(`Batch job exported: ${outputPath}`);
466
+ } catch (error) {
467
+ throw errorHandler.createError(
468
+ 'WUNDR_BATCH_EXPORT_FAILED',
469
+ 'Failed to export batch job',
470
+ { file, options },
471
+ true
472
+ );
473
+ }
474
+ }
475
+
476
+ /**
477
+ * Import batch job from different formats
478
+ */
479
+ private async importBatchJob(file: string, options: any): Promise<void> {
480
+ try {
481
+ logger.info(`Importing batch job: ${chalk.cyan(file)}`);
482
+
483
+ let job: BatchJob;
484
+
485
+ switch (options.format) {
486
+ case 'json':
487
+ job = await this.importFromJSON(file, options.name);
488
+ break;
489
+ case 'shell':
490
+ job = await this.importFromShell(file, options.name);
491
+ break;
492
+ case 'package-scripts':
493
+ job = await this.importFromPackageScripts(file, options.name);
494
+ break;
495
+ default:
496
+ throw new Error(`Unsupported import format: ${options.format}`);
497
+ }
498
+
499
+ const jobName = options.name || path.basename(file, path.extname(file));
500
+ const jobPath = path.join(
501
+ process.cwd(),
502
+ '.wundr',
503
+ 'batch',
504
+ `${jobName}.yaml`
505
+ );
506
+
507
+ await fs.ensureDir(path.dirname(jobPath));
508
+ await fs.writeFile(jobPath, YAML.stringify(job));
509
+
510
+ logger.success(`Batch job imported: ${jobPath}`);
511
+ } catch (error) {
512
+ throw errorHandler.createError(
513
+ 'WUNDR_BATCH_IMPORT_FAILED',
514
+ 'Failed to import batch job',
515
+ { file, options },
516
+ true
517
+ );
518
+ }
519
+ }
520
+
521
+ /**
522
+ * Helper methods for batch job operations
523
+ */
524
+ private async loadBatchJob(file: string): Promise<BatchJob> {
525
+ if (!(await fs.pathExists(file))) {
526
+ throw new Error(`Batch job file not found: ${file}`);
527
+ }
528
+
529
+ const content = await fs.readFile(file, 'utf8');
530
+ const ext = path.extname(file).toLowerCase();
531
+
532
+ if (ext === '.yaml' || ext === '.yml') {
533
+ return YAML.parse(content);
534
+ } else if (ext === '.json') {
535
+ return JSON.parse(content);
536
+ } else {
537
+ throw new Error(`Unsupported file format: ${ext}`);
538
+ }
539
+ }
540
+
541
+ private async validateJobStructure(
542
+ job: BatchJob
543
+ ): Promise<{ valid: boolean; errors: string[] }> {
544
+ const errors: string[] = [];
545
+
546
+ if (!job.name) {
547
+ errors.push('Job name is required');
548
+ }
549
+
550
+ if (!job.commands || job.commands.length === 0) {
551
+ errors.push('At least one command is required');
552
+ } else {
553
+ job.commands.forEach((cmd, index) => {
554
+ if (!cmd.command) {
555
+ errors.push(`Command ${index + 1} is missing command property`);
556
+ }
557
+ });
558
+ }
559
+
560
+ return { valid: errors.length === 0, errors };
561
+ }
562
+
563
+ private async processJobVariables(
564
+ job: BatchJob,
565
+ vars?: string
566
+ ): Promise<BatchJob> {
567
+ let variables: Record<string, any> = {};
568
+
569
+ if (vars) {
570
+ try {
571
+ // Try parsing as JSON first
572
+ variables = JSON.parse(vars);
573
+ } catch {
574
+ // Parse as key=value pairs
575
+ vars.split(',').forEach(pair => {
576
+ const [key, value] = pair.split('=');
577
+ if (key && value) {
578
+ variables[key.trim()] = value.trim();
579
+ }
580
+ });
581
+ }
582
+ }
583
+
584
+ // Replace variables in job
585
+ const processedJob = JSON.parse(JSON.stringify(job));
586
+ const jobString = JSON.stringify(processedJob);
587
+ let processedString = jobString;
588
+
589
+ Object.entries(variables).forEach(([key, value]) => {
590
+ const placeholder = new RegExp(`\\{\\{${key}\\}\\}`, 'g');
591
+ processedString = processedString.replace(placeholder, String(value));
592
+ });
593
+
594
+ return JSON.parse(processedString);
595
+ }
596
+
597
+ private async showDryRun(job: BatchJob): Promise<void> {
598
+ console.log(chalk.blue('\nDry Run - Commands to be executed:'));
599
+ console.log(chalk.gray('='.repeat(50)));
600
+
601
+ job.commands.forEach((cmd, index) => {
602
+ console.log(`${index + 1}. ${chalk.cyan(cmd.command)}`);
603
+ if (cmd.args) {
604
+ console.log(` Args: ${cmd.args.join(' ')}`);
605
+ }
606
+ if (cmd.condition) {
607
+ console.log(` Condition: ${cmd.condition}`);
608
+ }
609
+ if (cmd.retry) {
610
+ console.log(` Retry: ${cmd.retry} times`);
611
+ }
612
+ if (cmd.timeout) {
613
+ console.log(` Timeout: ${cmd.timeout}ms`);
614
+ }
615
+ console.log();
616
+ });
617
+
618
+ console.log(chalk.gray('='.repeat(50)));
619
+ console.log(`Total commands: ${job.commands.length}`);
620
+ console.log(`Parallel execution: ${job.parallel ? 'Yes' : 'No'}`);
621
+ console.log(`Continue on error: ${job.continueOnError ? 'Yes' : 'No'}`);
622
+ }
623
+
624
+ private async executeBatchJob(
625
+ job: BatchJob,
626
+ _jobId: string,
627
+ _options: any
628
+ ): Promise<void> {
629
+ const tasks = job.commands.map((cmd, _index) => ({
630
+ title: cmd.command,
631
+ task: async () => {
632
+ await this.executeCommand(cmd, _options);
633
+ },
634
+ retry: cmd.retry || 0,
635
+ timeout: cmd.timeout,
636
+ }));
637
+
638
+ const listr = new Listr(tasks, {
639
+ concurrent: job.parallel || _options.parallel,
640
+ exitOnError: !(job.continueOnError || _options.continueOnError),
641
+ rendererOptions: {
642
+ showSubtasks: true,
643
+ },
644
+ });
645
+
646
+ await listr.run();
647
+ }
648
+
649
+ private async executeCommand(
650
+ cmd: BatchCommand,
651
+ _options: any
652
+ ): Promise<void> {
653
+ // Check condition if specified
654
+ if (cmd.condition && !(await this.evaluateCondition(cmd.condition))) {
655
+ logger.debug(`Skipping command due to condition: ${cmd.condition}`);
656
+ return;
657
+ }
658
+
659
+ const { spawn } = await import('child_process');
660
+ const [command, ...args] = cmd.command.split(' ');
661
+ const finalArgs = cmd.args ? [...args, ...cmd.args] : args;
662
+
663
+ return new Promise((resolve, reject) => {
664
+ const child = spawn(command ?? 'echo', finalArgs, {
665
+ stdio: ['ignore', 'pipe', 'pipe'],
666
+ shell: true,
667
+ });
668
+
669
+ let output = '';
670
+ let error = '';
671
+
672
+ child.stdout?.on('data', (data: Buffer) => {
673
+ output += data.toString();
674
+ });
675
+
676
+ child.stderr?.on('data', (data: Buffer) => {
677
+ error += data.toString();
678
+ });
679
+
680
+ child.on('exit', (code: number | null) => {
681
+ if (code === 0) {
682
+ resolve();
683
+ } else {
684
+ reject(new Error(`Command failed with exit code ${code}: ${error}`));
685
+ }
686
+ });
687
+
688
+ // Handle timeout
689
+ if (cmd.timeout) {
690
+ setTimeout(() => {
691
+ child.kill();
692
+ reject(new Error(`Command timed out after ${cmd.timeout}ms`));
693
+ }, cmd.timeout);
694
+ }
695
+ });
696
+ }
697
+
698
+ private async evaluateCondition(condition: string): Promise<boolean> {
699
+ // Simple condition evaluation
700
+ // In a real implementation, this would be more sophisticated
701
+ switch (condition) {
702
+ case 'always':
703
+ return true;
704
+ case 'never':
705
+ return false;
706
+ default:
707
+ // Could evaluate file existence, environment variables, etc.
708
+ return true;
709
+ }
710
+ }
711
+
712
+ private async createInteractiveBatchJob(name: string): Promise<BatchJob> {
713
+ const inquirer = await import('inquirer');
714
+
715
+ const answers = await inquirer.default.prompt([
716
+ {
717
+ type: 'input',
718
+ name: 'description',
719
+ message: 'Job description:',
720
+ },
721
+ {
722
+ type: 'confirm',
723
+ name: 'parallel',
724
+ message: 'Run commands in parallel?',
725
+ default: false,
726
+ },
727
+ {
728
+ type: 'confirm',
729
+ name: 'continueOnError',
730
+ message: 'Continue on command failure?',
731
+ default: false,
732
+ },
733
+ ]);
734
+
735
+ const commands: BatchCommand[] = [];
736
+ let addMore = true;
737
+
738
+ while (addMore) {
739
+ const cmdAnswers = await inquirer.default.prompt([
740
+ {
741
+ type: 'input',
742
+ name: 'command',
743
+ message: 'Command:',
744
+ validate: input => input.length > 0 || 'Command is required',
745
+ },
746
+ {
747
+ type: 'input',
748
+ name: 'condition',
749
+ message: 'Condition (optional):',
750
+ },
751
+ {
752
+ type: 'number',
753
+ name: 'retry',
754
+ message: 'Retry count:',
755
+ default: 0,
756
+ },
757
+ {
758
+ type: 'confirm',
759
+ name: 'addMore',
760
+ message: 'Add another command?',
761
+ default: true,
762
+ },
763
+ ]);
764
+
765
+ commands.push({
766
+ command: cmdAnswers.command,
767
+ condition: cmdAnswers.condition || undefined,
768
+ retry: cmdAnswers.retry || undefined,
769
+ });
770
+
771
+ addMore = cmdAnswers.addMore;
772
+ }
773
+
774
+ return {
775
+ name,
776
+ description: answers.description,
777
+ commands,
778
+ parallel: answers.parallel,
779
+ continueOnError: answers.continueOnError,
780
+ };
781
+ }
782
+
783
+ private createBasicBatchJob(name: string, options: any): BatchJob {
784
+ const commands = options.commands
785
+ ? options.commands
786
+ .split(',')
787
+ .map((cmd: string) => ({ command: cmd.trim() }))
788
+ : [];
789
+
790
+ return {
791
+ name,
792
+ description: `Batch job: ${name}`,
793
+ commands,
794
+ parallel: false,
795
+ continueOnError: false,
796
+ };
797
+ }
798
+
799
+ private async createJobFromTemplate(
800
+ name: string,
801
+ template: string
802
+ ): Promise<BatchJob> {
803
+ // Load template and create job
804
+ const templatePath = path.join(
805
+ __dirname,
806
+ '../../templates/batch',
807
+ `${template}.yaml`
808
+ );
809
+ if (await fs.pathExists(templatePath)) {
810
+ const templateJob = await this.loadBatchJob(templatePath);
811
+ return { ...templateJob, name };
812
+ }
813
+
814
+ throw new Error(`Template not found: ${template}`);
815
+ }
816
+
817
+ // Format conversion methods
818
+ private convertToShellScript(job: BatchJob): string {
819
+ let script = `#!/bin/bash\n# Generated from batch job: ${job.name}\n\n`;
820
+
821
+ if (job.description) {
822
+ script += `# ${job.description}\n\n`;
823
+ }
824
+
825
+ script += 'set -e\n\n'; // Exit on error unless continueOnError is true
826
+
827
+ job.commands.forEach(cmd => {
828
+ script += `echo "Executing: ${cmd.command}"\n`;
829
+ script += `${cmd.command}\n\n`;
830
+ });
831
+
832
+ return script;
833
+ }
834
+
835
+ private convertToDockerfile(job: BatchJob): string {
836
+ let dockerfile = `# Generated from batch job: ${job.name}\n`;
837
+ dockerfile += `FROM node:18-alpine\n\n`;
838
+
839
+ if (job.description) {
840
+ dockerfile += `# ${job.description}\n`;
841
+ }
842
+
843
+ dockerfile += `WORKDIR /app\n`;
844
+ dockerfile += `COPY . .\n\n`;
845
+
846
+ job.commands.forEach(cmd => {
847
+ dockerfile += `RUN ${cmd.command}\n`;
848
+ });
849
+
850
+ return dockerfile;
851
+ }
852
+
853
+ private async importFromJSON(file: string, name?: string): Promise<BatchJob> {
854
+ const content = await fs.readJson(file);
855
+ return { ...content, name: name || content.name };
856
+ }
857
+
858
+ private async importFromShell(
859
+ file: string,
860
+ name?: string
861
+ ): Promise<BatchJob> {
862
+ const content = await fs.readFile(file, 'utf8');
863
+ const commands = content
864
+ .split('\n')
865
+ .filter(line => line.trim() && !line.startsWith('#'))
866
+ .map(line => ({ command: line.trim() }));
867
+
868
+ return {
869
+ name: name || path.basename(file, path.extname(file)),
870
+ description: `Imported from shell script: ${file}`,
871
+ commands,
872
+ };
873
+ }
874
+
875
+ private async importFromPackageScripts(
876
+ file: string,
877
+ name?: string
878
+ ): Promise<BatchJob> {
879
+ const packageJson = await fs.readJson(file);
880
+ const scripts = packageJson.scripts || {};
881
+
882
+ const commands = Object.entries(scripts).map(([script, command]) => ({
883
+ command: `npm run ${script}`,
884
+ args: [],
885
+ condition: undefined,
886
+ }));
887
+
888
+ return {
889
+ name: name || 'package-scripts',
890
+ description: `Imported from package.json scripts`,
891
+ commands,
892
+ };
893
+ }
894
+
895
+ private async listTemplates(): Promise<void> {
896
+ const templatesDir = path.join(__dirname, '../../templates/batch');
897
+ if (await fs.pathExists(templatesDir)) {
898
+ const templates = await fs.readdir(templatesDir);
899
+ const yamlTemplates = templates.filter(
900
+ t => t.endsWith('.yaml') || t.endsWith('.yml')
901
+ );
902
+
903
+ if (yamlTemplates.length > 0) {
904
+ logger.info('Available templates:');
905
+ yamlTemplates.forEach(template => {
906
+ console.log(` - ${path.basename(template, path.extname(template))}`);
907
+ });
908
+ } else {
909
+ logger.info('No templates available');
910
+ }
911
+ } else {
912
+ logger.info('No templates directory found');
913
+ }
914
+ }
915
+
916
+ private async createTemplate(name: string, options: any): Promise<void> {
917
+ logger.info(`Creating template: ${name}`);
918
+
919
+ if (options.from) {
920
+ const job = await this.loadBatchJob(options.from);
921
+ const templatePath = path.join(
922
+ __dirname,
923
+ '../../templates/batch',
924
+ `${name}.yaml`
925
+ );
926
+
927
+ await fs.ensureDir(path.dirname(templatePath));
928
+ await fs.writeFile(templatePath, YAML.stringify(job));
929
+
930
+ logger.success(`Template created: ${templatePath}`);
931
+ } else {
932
+ logger.info('Template creation from scratch not yet implemented');
933
+ }
934
+ }
935
+ }