@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.
- package/README.md +551 -0
- package/bin/wundr.js +39 -0
- package/dist/ai/ai-service.d.ts +152 -0
- package/dist/ai/ai-service.d.ts.map +1 -0
- package/dist/ai/ai-service.js +430 -0
- package/dist/ai/ai-service.js.map +1 -0
- package/dist/ai/claude-client.d.ts +130 -0
- package/dist/ai/claude-client.d.ts.map +1 -0
- package/dist/ai/claude-client.js +339 -0
- package/dist/ai/claude-client.js.map +1 -0
- package/dist/ai/conversation-manager.d.ts +164 -0
- package/dist/ai/conversation-manager.d.ts.map +1 -0
- package/dist/ai/conversation-manager.js +612 -0
- package/dist/ai/conversation-manager.js.map +1 -0
- package/dist/ai/index.d.ts +5 -0
- package/dist/ai/index.d.ts.map +1 -0
- package/dist/ai/index.js +8 -0
- package/dist/ai/index.js.map +1 -0
- package/dist/cli.d.ts +36 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +173 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/ai.d.ts +89 -0
- package/dist/commands/ai.d.ts.map +1 -0
- package/dist/commands/ai.js +735 -0
- package/dist/commands/ai.js.map +1 -0
- package/dist/commands/analyze-optimized.d.ts +14 -0
- package/dist/commands/analyze-optimized.d.ts.map +1 -0
- package/dist/commands/analyze-optimized.js +437 -0
- package/dist/commands/analyze-optimized.js.map +1 -0
- package/dist/commands/analyze.d.ts +65 -0
- package/dist/commands/analyze.d.ts.map +1 -0
- package/dist/commands/analyze.js +435 -0
- package/dist/commands/analyze.js.map +1 -0
- package/dist/commands/batch.d.ts +71 -0
- package/dist/commands/batch.d.ts.map +1 -0
- package/dist/commands/batch.js +738 -0
- package/dist/commands/batch.js.map +1 -0
- package/dist/commands/chat.d.ts +71 -0
- package/dist/commands/chat.d.ts.map +1 -0
- package/dist/commands/chat.js +674 -0
- package/dist/commands/chat.js.map +1 -0
- package/dist/commands/claude-init.d.ts +28 -0
- package/dist/commands/claude-init.d.ts.map +1 -0
- package/dist/commands/claude-init.js +587 -0
- package/dist/commands/claude-init.js.map +1 -0
- package/dist/commands/claude-setup.d.ts +32 -0
- package/dist/commands/claude-setup.d.ts.map +1 -0
- package/dist/commands/claude-setup.js +570 -0
- package/dist/commands/claude-setup.js.map +1 -0
- package/dist/commands/computer-setup-commands.d.ts +39 -0
- package/dist/commands/computer-setup-commands.d.ts.map +1 -0
- package/dist/commands/computer-setup-commands.js +563 -0
- package/dist/commands/computer-setup-commands.js.map +1 -0
- package/dist/commands/computer-setup.d.ts +7 -0
- package/dist/commands/computer-setup.d.ts.map +1 -0
- package/dist/commands/computer-setup.js +481 -0
- package/dist/commands/computer-setup.js.map +1 -0
- package/dist/commands/create-command.d.ts +7 -0
- package/dist/commands/create-command.d.ts.map +1 -0
- package/dist/commands/create-command.js +158 -0
- package/dist/commands/create-command.js.map +1 -0
- package/dist/commands/create.d.ts +74 -0
- package/dist/commands/create.d.ts.map +1 -0
- package/dist/commands/create.js +556 -0
- package/dist/commands/create.js.map +1 -0
- package/dist/commands/dashboard.d.ts +91 -0
- package/dist/commands/dashboard.d.ts.map +1 -0
- package/dist/commands/dashboard.js +537 -0
- package/dist/commands/dashboard.js.map +1 -0
- package/dist/commands/govern.d.ts +70 -0
- package/dist/commands/govern.d.ts.map +1 -0
- package/dist/commands/govern.js +480 -0
- package/dist/commands/govern.js.map +1 -0
- package/dist/commands/init.d.ts +55 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +584 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/performance-optimizer.d.ts +30 -0
- package/dist/commands/performance-optimizer.d.ts.map +1 -0
- package/dist/commands/performance-optimizer.js +649 -0
- package/dist/commands/performance-optimizer.js.map +1 -0
- package/dist/commands/plugins.d.ts +87 -0
- package/dist/commands/plugins.d.ts.map +1 -0
- package/dist/commands/plugins.js +685 -0
- package/dist/commands/plugins.js.map +1 -0
- package/dist/commands/setup.d.ts +29 -0
- package/dist/commands/setup.d.ts.map +1 -0
- package/dist/commands/setup.js +399 -0
- package/dist/commands/setup.js.map +1 -0
- package/dist/commands/test-init.d.ts +9 -0
- package/dist/commands/test-init.d.ts.map +1 -0
- package/dist/commands/test-init.js +222 -0
- package/dist/commands/test-init.js.map +1 -0
- package/dist/commands/test.d.ts +25 -0
- package/dist/commands/test.d.ts.map +1 -0
- package/dist/commands/test.js +217 -0
- package/dist/commands/test.js.map +1 -0
- package/dist/commands/watch.d.ts +76 -0
- package/dist/commands/watch.d.ts.map +1 -0
- package/dist/commands/watch.js +610 -0
- package/dist/commands/watch.js.map +1 -0
- package/dist/context/context-manager.d.ts +155 -0
- package/dist/context/context-manager.d.ts.map +1 -0
- package/dist/context/context-manager.js +383 -0
- package/dist/context/context-manager.js.map +1 -0
- package/dist/context/index.d.ts +3 -0
- package/dist/context/index.d.ts.map +1 -0
- package/dist/context/index.js +6 -0
- package/dist/context/index.js.map +1 -0
- package/dist/context/session-manager.d.ts +207 -0
- package/dist/context/session-manager.d.ts.map +1 -0
- package/dist/context/session-manager.js +682 -0
- package/dist/context/session-manager.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +51 -0
- package/dist/index.js.map +1 -0
- package/dist/interactive/interactive-mode.d.ts +76 -0
- package/dist/interactive/interactive-mode.d.ts.map +1 -0
- package/dist/interactive/interactive-mode.js +730 -0
- package/dist/interactive/interactive-mode.js.map +1 -0
- package/dist/nlp/command-mapper.d.ts +174 -0
- package/dist/nlp/command-mapper.d.ts.map +1 -0
- package/dist/nlp/command-mapper.js +623 -0
- package/dist/nlp/command-mapper.js.map +1 -0
- package/dist/nlp/command-parser.d.ts +106 -0
- package/dist/nlp/command-parser.d.ts.map +1 -0
- package/dist/nlp/command-parser.js +416 -0
- package/dist/nlp/command-parser.js.map +1 -0
- package/dist/nlp/index.d.ts +5 -0
- package/dist/nlp/index.d.ts.map +1 -0
- package/dist/nlp/index.js +8 -0
- package/dist/nlp/index.js.map +1 -0
- package/dist/nlp/intent-classifier.d.ts +59 -0
- package/dist/nlp/intent-classifier.d.ts.map +1 -0
- package/dist/nlp/intent-classifier.js +384 -0
- package/dist/nlp/intent-classifier.js.map +1 -0
- package/dist/nlp/intent-parser.d.ts +152 -0
- package/dist/nlp/intent-parser.d.ts.map +1 -0
- package/dist/nlp/intent-parser.js +739 -0
- package/dist/nlp/intent-parser.js.map +1 -0
- package/dist/plugins/plugin-manager.d.ts +120 -0
- package/dist/plugins/plugin-manager.d.ts.map +1 -0
- package/dist/plugins/plugin-manager.js +595 -0
- package/dist/plugins/plugin-manager.js.map +1 -0
- package/dist/types/index.d.ts +224 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +3 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/config-manager.d.ts +73 -0
- package/dist/utils/config-manager.d.ts.map +1 -0
- package/dist/utils/config-manager.js +339 -0
- package/dist/utils/config-manager.js.map +1 -0
- package/dist/utils/error-handler.d.ts +46 -0
- package/dist/utils/error-handler.d.ts.map +1 -0
- package/dist/utils/error-handler.js +169 -0
- package/dist/utils/error-handler.js.map +1 -0
- package/dist/utils/logger.d.ts +25 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +94 -0
- package/dist/utils/logger.js.map +1 -0
- package/package.json +119 -0
- package/src/ai/ai-service.ts +595 -0
- package/src/ai/claude-client.ts +490 -0
- package/src/ai/conversation-manager.ts +907 -0
- package/src/ai/index.ts +8 -0
- package/src/cli.ts +202 -0
- package/src/commands/ai.ts +995 -0
- package/src/commands/analyze-optimized.ts +641 -0
- package/src/commands/analyze.ts +576 -0
- package/src/commands/batch.ts +935 -0
- package/src/commands/chat.ts +876 -0
- package/src/commands/claude-init.ts +715 -0
- package/src/commands/claude-setup.ts +697 -0
- package/src/commands/computer-setup-commands.ts +709 -0
- package/src/commands/computer-setup.ts +565 -0
- package/src/commands/create-command.ts +175 -0
- package/src/commands/create.ts +727 -0
- package/src/commands/dashboard.ts +691 -0
- package/src/commands/govern.ts +635 -0
- package/src/commands/init.ts +677 -0
- package/src/commands/performance-optimizer.ts +864 -0
- package/src/commands/plugins.ts +848 -0
- package/src/commands/setup.ts +508 -0
- package/src/commands/test-init.ts +242 -0
- package/src/commands/test.ts +264 -0
- package/src/commands/watch.ts +755 -0
- package/src/context/context-manager.ts +546 -0
- package/src/context/index.ts +9 -0
- package/src/context/session-manager.ts +1019 -0
- package/src/index.ts +64 -0
- package/src/interactive/interactive-mode.ts +830 -0
- package/src/nlp/command-mapper.ts +885 -0
- package/src/nlp/command-parser.ts +564 -0
- package/src/nlp/index.ts +4 -0
- package/src/nlp/intent-classifier.ts +458 -0
- package/src/nlp/intent-parser.ts +1101 -0
- package/src/plugins/plugin-manager.ts +744 -0
- package/src/types/index.ts +252 -0
- package/src/types/modules.d.ts +56 -0
- package/src/utils/config-manager.ts +391 -0
- package/src/utils/error-handler.ts +192 -0
- package/src/utils/logger.ts +104 -0
- package/templates/batch/ci-cd.yaml +62 -0
- package/templates/component/{{fileName}}.test.tsx +17 -0
- package/templates/component/{{fileName}}.tsx +21 -0
- package/templates/service/{{fileName}}.ts +98 -0
- package/templates/wundr-test.config.js +0 -0
- package/test-suites/api/health.spec.ts +134 -0
- package/test-suites/helpers/test-config.ts +84 -0
- package/test-suites/ui/accessibility.spec.ts +102 -0
- 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
|
+
}
|