@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
package/src/commands/batch.ts
CHANGED
|
@@ -22,7 +22,7 @@ export class BatchCommands {
|
|
|
22
22
|
constructor(
|
|
23
23
|
private program: Command,
|
|
24
24
|
private configManager: ConfigManager,
|
|
25
|
-
private pluginManager: PluginManager
|
|
25
|
+
private pluginManager: PluginManager
|
|
26
26
|
) {
|
|
27
27
|
this.registerCommands();
|
|
28
28
|
}
|
|
@@ -41,7 +41,7 @@ export class BatchCommands {
|
|
|
41
41
|
.option('--continue-on-error', 'continue execution on command failures')
|
|
42
42
|
.option(
|
|
43
43
|
'--vars <vars>',
|
|
44
|
-
'variables to pass to batch job (JSON or key=value)'
|
|
44
|
+
'variables to pass to batch job (JSON or key=value)'
|
|
45
45
|
)
|
|
46
46
|
.option('--timeout <ms>', 'global timeout for batch job')
|
|
47
47
|
.action(async (file, options) => {
|
|
@@ -111,7 +111,7 @@ export class BatchCommands {
|
|
|
111
111
|
.option(
|
|
112
112
|
'--format <format>',
|
|
113
113
|
'export format (json, shell, dockerfile)',
|
|
114
|
-
'json'
|
|
114
|
+
'json'
|
|
115
115
|
)
|
|
116
116
|
.option('--output <path>', 'output file path')
|
|
117
117
|
.action(async (file, options) => {
|
|
@@ -124,7 +124,7 @@ export class BatchCommands {
|
|
|
124
124
|
.description('import batch job from different formats')
|
|
125
125
|
.option(
|
|
126
126
|
'--format <format>',
|
|
127
|
-
'source format (json, shell, package-scripts)'
|
|
127
|
+
'source format (json, shell, package-scripts)'
|
|
128
128
|
)
|
|
129
129
|
.option('--name <name>', 'batch job name')
|
|
130
130
|
.action(async (file, options) => {
|
|
@@ -189,7 +189,7 @@ export class BatchCommands {
|
|
|
189
189
|
'WUNDR_BATCH_RUN_FAILED',
|
|
190
190
|
'Failed to run batch job',
|
|
191
191
|
{ file, options },
|
|
192
|
-
true
|
|
192
|
+
true
|
|
193
193
|
);
|
|
194
194
|
}
|
|
195
195
|
}
|
|
@@ -215,7 +215,7 @@ export class BatchCommands {
|
|
|
215
215
|
process.cwd(),
|
|
216
216
|
'.wundr',
|
|
217
217
|
'batch',
|
|
218
|
-
`${name}.yaml
|
|
218
|
+
`${name}.yaml`
|
|
219
219
|
);
|
|
220
220
|
await fs.ensureDir(path.dirname(jobPath));
|
|
221
221
|
await fs.writeFile(jobPath, YAML.stringify(job));
|
|
@@ -226,7 +226,7 @@ export class BatchCommands {
|
|
|
226
226
|
'WUNDR_BATCH_CREATE_FAILED',
|
|
227
227
|
'Failed to create batch job',
|
|
228
228
|
{ name, options },
|
|
229
|
-
true
|
|
229
|
+
true
|
|
230
230
|
);
|
|
231
231
|
}
|
|
232
232
|
}
|
|
@@ -246,7 +246,7 @@ export class BatchCommands {
|
|
|
246
246
|
|
|
247
247
|
const files = await fs.readdir(batchDir);
|
|
248
248
|
const yamlFiles = files.filter(
|
|
249
|
-
f => f.endsWith('.yaml') || f.endsWith('.yml')
|
|
249
|
+
f => f.endsWith('.yaml') || f.endsWith('.yml')
|
|
250
250
|
);
|
|
251
251
|
|
|
252
252
|
if (yamlFiles.length === 0) {
|
|
@@ -288,7 +288,7 @@ export class BatchCommands {
|
|
|
288
288
|
'WUNDR_BATCH_LIST_FAILED',
|
|
289
289
|
'Failed to list batch jobs',
|
|
290
290
|
{ options },
|
|
291
|
-
true
|
|
291
|
+
true
|
|
292
292
|
);
|
|
293
293
|
}
|
|
294
294
|
}
|
|
@@ -325,7 +325,7 @@ export class BatchCommands {
|
|
|
325
325
|
'WUNDR_BATCH_VALIDATE_FAILED',
|
|
326
326
|
'Failed to validate batch job',
|
|
327
327
|
{ file },
|
|
328
|
-
true
|
|
328
|
+
true
|
|
329
329
|
);
|
|
330
330
|
}
|
|
331
331
|
}
|
|
@@ -352,7 +352,7 @@ export class BatchCommands {
|
|
|
352
352
|
'WUNDR_BATCH_STOP_FAILED',
|
|
353
353
|
'Failed to stop batch job',
|
|
354
354
|
{ jobId },
|
|
355
|
-
true
|
|
355
|
+
true
|
|
356
356
|
);
|
|
357
357
|
}
|
|
358
358
|
}
|
|
@@ -387,7 +387,7 @@ export class BatchCommands {
|
|
|
387
387
|
File: path.basename(job.file),
|
|
388
388
|
Status: job.status,
|
|
389
389
|
Duration: `${Date.now() - job.startTime}ms`,
|
|
390
|
-
})
|
|
390
|
+
})
|
|
391
391
|
);
|
|
392
392
|
|
|
393
393
|
console.table(jobData);
|
|
@@ -397,7 +397,7 @@ export class BatchCommands {
|
|
|
397
397
|
'WUNDR_BATCH_STATUS_FAILED',
|
|
398
398
|
'Failed to show job status',
|
|
399
399
|
{ jobId },
|
|
400
|
-
true
|
|
400
|
+
true
|
|
401
401
|
);
|
|
402
402
|
}
|
|
403
403
|
}
|
|
@@ -431,7 +431,7 @@ export class BatchCommands {
|
|
|
431
431
|
'WUNDR_BATCH_SCHEDULE_FAILED',
|
|
432
432
|
'Failed to schedule batch job',
|
|
433
433
|
{ file, options },
|
|
434
|
-
true
|
|
434
|
+
true
|
|
435
435
|
);
|
|
436
436
|
}
|
|
437
437
|
}
|
|
@@ -471,7 +471,7 @@ export class BatchCommands {
|
|
|
471
471
|
'WUNDR_BATCH_EXPORT_FAILED',
|
|
472
472
|
'Failed to export batch job',
|
|
473
473
|
{ file, options },
|
|
474
|
-
true
|
|
474
|
+
true
|
|
475
475
|
);
|
|
476
476
|
}
|
|
477
477
|
}
|
|
@@ -504,7 +504,7 @@ export class BatchCommands {
|
|
|
504
504
|
process.cwd(),
|
|
505
505
|
'.wundr',
|
|
506
506
|
'batch',
|
|
507
|
-
`${jobName}.yaml
|
|
507
|
+
`${jobName}.yaml`
|
|
508
508
|
);
|
|
509
509
|
|
|
510
510
|
await fs.ensureDir(path.dirname(jobPath));
|
|
@@ -516,7 +516,7 @@ export class BatchCommands {
|
|
|
516
516
|
'WUNDR_BATCH_IMPORT_FAILED',
|
|
517
517
|
'Failed to import batch job',
|
|
518
518
|
{ file, options },
|
|
519
|
-
true
|
|
519
|
+
true
|
|
520
520
|
);
|
|
521
521
|
}
|
|
522
522
|
}
|
|
@@ -542,7 +542,7 @@ export class BatchCommands {
|
|
|
542
542
|
}
|
|
543
543
|
|
|
544
544
|
private async validateJobStructure(
|
|
545
|
-
job: BatchJob
|
|
545
|
+
job: BatchJob
|
|
546
546
|
): Promise<{ valid: boolean; errors: string[] }> {
|
|
547
547
|
const errors: string[] = [];
|
|
548
548
|
|
|
@@ -563,9 +563,14 @@ export class BatchCommands {
|
|
|
563
563
|
return { valid: errors.length === 0, errors };
|
|
564
564
|
}
|
|
565
565
|
|
|
566
|
+
/**
|
|
567
|
+
* Regex for valid variable names in batch templates.
|
|
568
|
+
*/
|
|
569
|
+
private static readonly VALID_VARIABLE_NAME = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
570
|
+
|
|
566
571
|
private async processJobVariables(
|
|
567
572
|
job: BatchJob,
|
|
568
|
-
vars?: string
|
|
573
|
+
vars?: string
|
|
569
574
|
): Promise<BatchJob> {
|
|
570
575
|
let variables: Record<string, any> = {};
|
|
571
576
|
|
|
@@ -576,25 +581,58 @@ export class BatchCommands {
|
|
|
576
581
|
} catch {
|
|
577
582
|
// Parse as key=value pairs
|
|
578
583
|
vars.split(',').forEach(pair => {
|
|
579
|
-
const
|
|
580
|
-
if (
|
|
581
|
-
|
|
584
|
+
const eqIndex = pair.indexOf('=');
|
|
585
|
+
if (eqIndex > 0) {
|
|
586
|
+
const key = pair.slice(0, eqIndex).trim();
|
|
587
|
+
const value = pair.slice(eqIndex + 1).trim();
|
|
588
|
+
if (key) {
|
|
589
|
+
variables[key] = value;
|
|
590
|
+
}
|
|
582
591
|
}
|
|
583
592
|
});
|
|
584
593
|
}
|
|
585
594
|
}
|
|
586
595
|
|
|
587
|
-
//
|
|
588
|
-
const
|
|
589
|
-
|
|
590
|
-
|
|
596
|
+
// Validate variable names to prevent regex injection
|
|
597
|
+
for (const key of Object.keys(variables)) {
|
|
598
|
+
if (!BatchCommands.VALID_VARIABLE_NAME.test(key)) {
|
|
599
|
+
throw new Error(
|
|
600
|
+
`Invalid variable name "${key}": must match [a-zA-Z_][a-zA-Z0-9_]*`
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// SECURITY: Instead of replacing {{var}} in a JSON-stringified string
|
|
606
|
+
// (which allows JSON structure breakout via quotes in values), we walk
|
|
607
|
+
// the job structure and only replace in string leaf values. Values are
|
|
608
|
+
// JSON-escaped to prevent structure injection.
|
|
609
|
+
const processedJob: BatchJob = JSON.parse(JSON.stringify(job));
|
|
610
|
+
|
|
611
|
+
const replaceVarsInString = (str: string): string => {
|
|
612
|
+
let result = str;
|
|
613
|
+
for (const [key, value] of Object.entries(variables)) {
|
|
614
|
+
const placeholder = `{{${key}}}`;
|
|
615
|
+
// Only replace exact placeholder matches, not regex patterns
|
|
616
|
+
while (result.includes(placeholder)) {
|
|
617
|
+
result = result.replace(placeholder, String(value));
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
return result;
|
|
621
|
+
};
|
|
591
622
|
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
623
|
+
// Walk commands and replace variables in string fields only
|
|
624
|
+
processedJob.commands = processedJob.commands.map(cmd => ({
|
|
625
|
+
...cmd,
|
|
626
|
+
command: replaceVarsInString(cmd.command),
|
|
627
|
+
args: cmd.args?.map(arg => replaceVarsInString(arg)),
|
|
628
|
+
condition: cmd.condition ? replaceVarsInString(cmd.condition) : undefined,
|
|
629
|
+
}));
|
|
596
630
|
|
|
597
|
-
|
|
631
|
+
if (processedJob.description) {
|
|
632
|
+
processedJob.description = replaceVarsInString(processedJob.description);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
return processedJob;
|
|
598
636
|
}
|
|
599
637
|
|
|
600
638
|
private async showDryRun(job: BatchJob): Promise<void> {
|
|
@@ -627,7 +665,7 @@ export class BatchCommands {
|
|
|
627
665
|
private async executeBatchJob(
|
|
628
666
|
job: BatchJob,
|
|
629
667
|
_jobId: string,
|
|
630
|
-
_options: any
|
|
668
|
+
_options: any
|
|
631
669
|
): Promise<void> {
|
|
632
670
|
const tasks = job.commands.map((cmd, _index) => ({
|
|
633
671
|
title: cmd.command,
|
|
@@ -649,9 +687,94 @@ export class BatchCommands {
|
|
|
649
687
|
await listr.run();
|
|
650
688
|
}
|
|
651
689
|
|
|
690
|
+
/**
|
|
691
|
+
* Shell metacharacters that indicate injection attempts.
|
|
692
|
+
* These characters have special meaning in sh/bash and must not
|
|
693
|
+
* appear in arguments passed to spawn().
|
|
694
|
+
*/
|
|
695
|
+
private static readonly SHELL_METACHARACTERS = /[;&|`$(){}[\]<>!#~*?\n\r\\]/;
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* Tokenize a command string into [binary, ...args] without shell
|
|
699
|
+
* interpretation. Supports simple single/double quoting.
|
|
700
|
+
*/
|
|
701
|
+
private tokenizeCommand(command: string): string[] {
|
|
702
|
+
const tokens: string[] = [];
|
|
703
|
+
let current = '';
|
|
704
|
+
let inSingle = false;
|
|
705
|
+
let inDouble = false;
|
|
706
|
+
|
|
707
|
+
for (let i = 0; i < command.length; i++) {
|
|
708
|
+
const ch = command[i];
|
|
709
|
+
|
|
710
|
+
if (inSingle) {
|
|
711
|
+
if (ch === "'") {
|
|
712
|
+
inSingle = false;
|
|
713
|
+
} else {
|
|
714
|
+
current += ch;
|
|
715
|
+
}
|
|
716
|
+
continue;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
if (inDouble) {
|
|
720
|
+
if (ch === '"') {
|
|
721
|
+
inDouble = false;
|
|
722
|
+
} else {
|
|
723
|
+
current += ch;
|
|
724
|
+
}
|
|
725
|
+
continue;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
if (ch === "'") {
|
|
729
|
+
inSingle = true;
|
|
730
|
+
continue;
|
|
731
|
+
}
|
|
732
|
+
if (ch === '"') {
|
|
733
|
+
inDouble = true;
|
|
734
|
+
continue;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
if (ch === ' ' || ch === '\t') {
|
|
738
|
+
if (current.length > 0) {
|
|
739
|
+
tokens.push(current);
|
|
740
|
+
current = '';
|
|
741
|
+
}
|
|
742
|
+
continue;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
current += ch;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
if (current.length > 0) {
|
|
749
|
+
tokens.push(current);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
if (inSingle || inDouble) {
|
|
753
|
+
throw new Error(`Unterminated quote in command: ${command}`);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
return tokens;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
/**
|
|
760
|
+
* Validate that no argument contains shell metacharacters.
|
|
761
|
+
* This is a defense-in-depth measure: since we never use shell: true,
|
|
762
|
+
* metacharacters would be treated literally, but their presence strongly
|
|
763
|
+
* suggests a command injection attempt.
|
|
764
|
+
*/
|
|
765
|
+
private validateArgs(args: string[]): void {
|
|
766
|
+
for (const arg of args) {
|
|
767
|
+
if (BatchCommands.SHELL_METACHARACTERS.test(arg)) {
|
|
768
|
+
throw new Error(
|
|
769
|
+
`Argument contains shell metacharacters (possible injection): "${arg}"`
|
|
770
|
+
);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
652
775
|
private async executeCommand(
|
|
653
776
|
cmd: BatchCommand,
|
|
654
|
-
_options: any
|
|
777
|
+
_options: any
|
|
655
778
|
): Promise<void> {
|
|
656
779
|
// Check condition if specified
|
|
657
780
|
if (cmd.condition && !(await this.evaluateCondition(cmd.condition))) {
|
|
@@ -660,13 +783,27 @@ export class BatchCommands {
|
|
|
660
783
|
}
|
|
661
784
|
|
|
662
785
|
const { spawn } = await import('child_process');
|
|
663
|
-
|
|
664
|
-
|
|
786
|
+
|
|
787
|
+
// Tokenize the command string into binary + args without shell
|
|
788
|
+
// interpretation. This prevents command injection via shell metacharacters
|
|
789
|
+
// like ; | & ` $() etc.
|
|
790
|
+
const tokens = this.tokenizeCommand(cmd.command);
|
|
791
|
+
if (tokens.length === 0) {
|
|
792
|
+
throw new Error('Empty command');
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
const [command, ...parsedArgs] = tokens;
|
|
796
|
+
const finalArgs = cmd.args ? [...parsedArgs, ...cmd.args] : parsedArgs;
|
|
797
|
+
|
|
798
|
+
// Validate arguments for shell metacharacters as defense-in-depth
|
|
799
|
+
this.validateArgs(finalArgs);
|
|
665
800
|
|
|
666
801
|
return new Promise((resolve, reject) => {
|
|
802
|
+
// SECURITY: No shell: true. The command is executed directly via
|
|
803
|
+
// execvp(), so shell metacharacters in arguments are treated as
|
|
804
|
+
// literal characters rather than being interpreted by a shell.
|
|
667
805
|
const child = spawn(command ?? 'echo', finalArgs, {
|
|
668
806
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
669
|
-
shell: true,
|
|
670
807
|
});
|
|
671
808
|
|
|
672
809
|
let output = '';
|
|
@@ -801,13 +938,13 @@ export class BatchCommands {
|
|
|
801
938
|
|
|
802
939
|
private async createJobFromTemplate(
|
|
803
940
|
name: string,
|
|
804
|
-
template: string
|
|
941
|
+
template: string
|
|
805
942
|
): Promise<BatchJob> {
|
|
806
943
|
// Load template and create job
|
|
807
944
|
const templatePath = path.join(
|
|
808
945
|
__dirname,
|
|
809
946
|
'../../templates/batch',
|
|
810
|
-
`${template}.yaml
|
|
947
|
+
`${template}.yaml`
|
|
811
948
|
);
|
|
812
949
|
if (await fs.pathExists(templatePath)) {
|
|
813
950
|
const templateJob = await this.loadBatchJob(templatePath);
|
|
@@ -860,7 +997,7 @@ export class BatchCommands {
|
|
|
860
997
|
|
|
861
998
|
private async importFromShell(
|
|
862
999
|
file: string,
|
|
863
|
-
name?: string
|
|
1000
|
+
name?: string
|
|
864
1001
|
): Promise<BatchJob> {
|
|
865
1002
|
const content = await fs.readFile(file, 'utf8');
|
|
866
1003
|
const commands = content
|
|
@@ -877,7 +1014,7 @@ export class BatchCommands {
|
|
|
877
1014
|
|
|
878
1015
|
private async importFromPackageScripts(
|
|
879
1016
|
file: string,
|
|
880
|
-
name?: string
|
|
1017
|
+
name?: string
|
|
881
1018
|
): Promise<BatchJob> {
|
|
882
1019
|
const packageJson = await fs.readJson(file);
|
|
883
1020
|
const scripts = packageJson.scripts || {};
|
|
@@ -900,7 +1037,7 @@ export class BatchCommands {
|
|
|
900
1037
|
if (await fs.pathExists(templatesDir)) {
|
|
901
1038
|
const templates = await fs.readdir(templatesDir);
|
|
902
1039
|
const yamlTemplates = templates.filter(
|
|
903
|
-
t => t.endsWith('.yaml') || t.endsWith('.yml')
|
|
1040
|
+
t => t.endsWith('.yaml') || t.endsWith('.yml')
|
|
904
1041
|
);
|
|
905
1042
|
|
|
906
1043
|
if (yamlTemplates.length > 0) {
|
|
@@ -924,7 +1061,7 @@ export class BatchCommands {
|
|
|
924
1061
|
const templatePath = path.join(
|
|
925
1062
|
__dirname,
|
|
926
1063
|
'../../templates/batch',
|
|
927
|
-
`${name}.yaml
|
|
1064
|
+
`${name}.yaml`
|
|
928
1065
|
);
|
|
929
1066
|
|
|
930
1067
|
await fs.ensureDir(path.dirname(templatePath));
|
package/src/commands/chat.ts
CHANGED
|
@@ -4,6 +4,7 @@ import chalk from 'chalk';
|
|
|
4
4
|
import fs from 'fs-extra';
|
|
5
5
|
import inquirer from 'inquirer';
|
|
6
6
|
|
|
7
|
+
import { AIService } from '../ai/ai-service';
|
|
7
8
|
import { errorHandler } from '../utils/error-handler';
|
|
8
9
|
import { logger } from '../utils/logger';
|
|
9
10
|
|
|
@@ -17,12 +18,14 @@ import type { Command } from 'commander';
|
|
|
17
18
|
*/
|
|
18
19
|
export class ChatCommands {
|
|
19
20
|
private activeSessions: Map<string, ChatSession> = new Map();
|
|
21
|
+
private aiService: AIService;
|
|
20
22
|
|
|
21
23
|
constructor(
|
|
22
24
|
private program: Command,
|
|
23
25
|
private configManager: ConfigManager,
|
|
24
|
-
private pluginManager: PluginManager
|
|
26
|
+
private pluginManager: PluginManager
|
|
25
27
|
) {
|
|
28
|
+
this.aiService = new AIService(configManager);
|
|
26
29
|
this.registerCommands();
|
|
27
30
|
}
|
|
28
31
|
|
|
@@ -41,7 +44,7 @@ export class ChatCommands {
|
|
|
41
44
|
.option(
|
|
42
45
|
'--persona <persona>',
|
|
43
46
|
'AI persona (developer, architect, reviewer)',
|
|
44
|
-
'developer'
|
|
47
|
+
'developer'
|
|
45
48
|
)
|
|
46
49
|
.option('--session-name <name>', 'custom session name')
|
|
47
50
|
.action(async options => {
|
|
@@ -84,7 +87,7 @@ export class ChatCommands {
|
|
|
84
87
|
.option(
|
|
85
88
|
'--format <format>',
|
|
86
89
|
'export format (json, markdown, txt)',
|
|
87
|
-
'markdown'
|
|
90
|
+
'markdown'
|
|
88
91
|
)
|
|
89
92
|
.option('--output <path>', 'output file path')
|
|
90
93
|
.action(async (sessionId, options) => {
|
|
@@ -133,7 +136,7 @@ export class ChatCommands {
|
|
|
133
136
|
.option(
|
|
134
137
|
'--action <action>',
|
|
135
138
|
'action to perform (explain, review, improve)',
|
|
136
|
-
'explain'
|
|
139
|
+
'explain'
|
|
137
140
|
)
|
|
138
141
|
.option('--model <model>', 'AI model to use')
|
|
139
142
|
.action(async (file, options) => {
|
|
@@ -148,7 +151,7 @@ export class ChatCommands {
|
|
|
148
151
|
.option(
|
|
149
152
|
'--action <action>',
|
|
150
153
|
'action to perform (explain, review, improve)',
|
|
151
|
-
'explain'
|
|
154
|
+
'explain'
|
|
152
155
|
)
|
|
153
156
|
.action(async options => {
|
|
154
157
|
await this.chatWithCode(options);
|
|
@@ -189,7 +192,7 @@ export class ChatCommands {
|
|
|
189
192
|
console.log(chalk.gray(`Context: ${session.context}`));
|
|
190
193
|
}
|
|
191
194
|
console.log(
|
|
192
|
-
chalk.gray('Type "exit" to end the session, "help" for commands\n')
|
|
195
|
+
chalk.gray('Type "exit" to end the session, "help" for commands\n')
|
|
193
196
|
);
|
|
194
197
|
|
|
195
198
|
await this.runChatLoop(session);
|
|
@@ -198,7 +201,7 @@ export class ChatCommands {
|
|
|
198
201
|
'WUNDR_CHAT_START_FAILED',
|
|
199
202
|
'Failed to start chat session',
|
|
200
203
|
{ options },
|
|
201
|
-
true
|
|
204
|
+
true
|
|
202
205
|
);
|
|
203
206
|
}
|
|
204
207
|
}
|
|
@@ -221,7 +224,7 @@ export class ChatCommands {
|
|
|
221
224
|
console.log(chalk.gray(`Session ID: ${session.id}`));
|
|
222
225
|
console.log(chalk.gray(`Messages: ${session.history.length}`));
|
|
223
226
|
console.log(
|
|
224
|
-
chalk.gray(`Last updated: ${session.updated.toLocaleString()}\n`)
|
|
227
|
+
chalk.gray(`Last updated: ${session.updated.toLocaleString()}\n`)
|
|
225
228
|
);
|
|
226
229
|
|
|
227
230
|
// Show recent messages
|
|
@@ -244,7 +247,7 @@ export class ChatCommands {
|
|
|
244
247
|
'WUNDR_CHAT_RESUME_FAILED',
|
|
245
248
|
'Failed to resume chat session',
|
|
246
249
|
{ sessionId },
|
|
247
|
-
true
|
|
250
|
+
true
|
|
248
251
|
);
|
|
249
252
|
}
|
|
250
253
|
}
|
|
@@ -281,7 +284,7 @@ export class ChatCommands {
|
|
|
281
284
|
'WUNDR_CHAT_LIST_FAILED',
|
|
282
285
|
'Failed to list chat sessions',
|
|
283
286
|
{ options },
|
|
284
|
-
true
|
|
287
|
+
true
|
|
285
288
|
);
|
|
286
289
|
}
|
|
287
290
|
}
|
|
@@ -291,7 +294,7 @@ export class ChatCommands {
|
|
|
291
294
|
*/
|
|
292
295
|
private async askSingleQuestion(
|
|
293
296
|
message: string,
|
|
294
|
-
options: any
|
|
297
|
+
options: any
|
|
295
298
|
): Promise<void> {
|
|
296
299
|
try {
|
|
297
300
|
logger.debug('Processing single question...');
|
|
@@ -326,7 +329,7 @@ export class ChatCommands {
|
|
|
326
329
|
'WUNDR_CHAT_ASK_FAILED',
|
|
327
330
|
'Failed to process question',
|
|
328
331
|
{ message, options },
|
|
329
|
-
true
|
|
332
|
+
true
|
|
330
333
|
);
|
|
331
334
|
}
|
|
332
335
|
}
|
|
@@ -336,7 +339,7 @@ export class ChatCommands {
|
|
|
336
339
|
*/
|
|
337
340
|
private async exportChatSession(
|
|
338
341
|
sessionId: string,
|
|
339
|
-
options: any
|
|
342
|
+
options: any
|
|
340
343
|
): Promise<void> {
|
|
341
344
|
try {
|
|
342
345
|
logger.info(`Exporting chat session: ${sessionId}`);
|
|
@@ -372,7 +375,7 @@ export class ChatCommands {
|
|
|
372
375
|
'WUNDR_CHAT_EXPORT_FAILED',
|
|
373
376
|
'Failed to export chat session',
|
|
374
377
|
{ sessionId, options },
|
|
375
|
-
true
|
|
378
|
+
true
|
|
376
379
|
);
|
|
377
380
|
}
|
|
378
381
|
}
|
|
@@ -413,7 +416,7 @@ export class ChatCommands {
|
|
|
413
416
|
'WUNDR_CHAT_IMPORT_FAILED',
|
|
414
417
|
'Failed to import chat session',
|
|
415
418
|
{ file, options },
|
|
416
|
-
true
|
|
419
|
+
true
|
|
417
420
|
);
|
|
418
421
|
}
|
|
419
422
|
}
|
|
@@ -423,7 +426,7 @@ export class ChatCommands {
|
|
|
423
426
|
*/
|
|
424
427
|
private async deleteChatSession(
|
|
425
428
|
sessionId: string,
|
|
426
|
-
options: any
|
|
429
|
+
options: any
|
|
427
430
|
): Promise<void> {
|
|
428
431
|
try {
|
|
429
432
|
const session = await this.loadChatSession(sessionId);
|
|
@@ -456,7 +459,7 @@ export class ChatCommands {
|
|
|
456
459
|
'WUNDR_CHAT_DELETE_FAILED',
|
|
457
460
|
'Failed to delete chat session',
|
|
458
461
|
{ sessionId, options },
|
|
459
|
-
true
|
|
462
|
+
true
|
|
460
463
|
);
|
|
461
464
|
}
|
|
462
465
|
}
|
|
@@ -490,7 +493,7 @@ export class ChatCommands {
|
|
|
490
493
|
'WUNDR_CHAT_FILE_FAILED',
|
|
491
494
|
'Failed to chat with file',
|
|
492
495
|
{ file, options },
|
|
493
|
-
true
|
|
496
|
+
true
|
|
494
497
|
);
|
|
495
498
|
}
|
|
496
499
|
}
|
|
@@ -533,7 +536,7 @@ export class ChatCommands {
|
|
|
533
536
|
'WUNDR_CHAT_CODE_FAILED',
|
|
534
537
|
'Failed to chat with code',
|
|
535
538
|
{ options },
|
|
536
|
-
true
|
|
539
|
+
true
|
|
537
540
|
);
|
|
538
541
|
}
|
|
539
542
|
}
|
|
@@ -585,7 +588,7 @@ export class ChatCommands {
|
|
|
585
588
|
} catch (error) {
|
|
586
589
|
logger.error('Chat error:', error);
|
|
587
590
|
console.log(
|
|
588
|
-
chalk.red('Sorry, there was an error processing your message.\n')
|
|
591
|
+
chalk.red('Sorry, there was an error processing your message.\n')
|
|
589
592
|
);
|
|
590
593
|
}
|
|
591
594
|
}
|
|
@@ -598,7 +601,7 @@ export class ChatCommands {
|
|
|
598
601
|
|
|
599
602
|
private async sendMessage(
|
|
600
603
|
session: ChatSession,
|
|
601
|
-
message: string
|
|
604
|
+
message: string
|
|
602
605
|
): Promise<string> {
|
|
603
606
|
// Add user message to history
|
|
604
607
|
const userMessage: ChatMessage = {
|
|
@@ -625,9 +628,13 @@ export class ChatCommands {
|
|
|
625
628
|
}
|
|
626
629
|
|
|
627
630
|
private async callAI(session: ChatSession, message: string): Promise<string> {
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
+
if (!this.aiService.isReady()) {
|
|
632
|
+
throw new Error(
|
|
633
|
+
'AI service not configured. Set ANTHROPIC_API_KEY environment variable to enable chat.'
|
|
634
|
+
);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
return this.aiService.sendMessage(session.id, message);
|
|
631
638
|
}
|
|
632
639
|
|
|
633
640
|
private showChatHelp(): void {
|
|
@@ -644,7 +651,7 @@ export class ChatCommands {
|
|
|
644
651
|
|
|
645
652
|
private async handleChatCommand(
|
|
646
653
|
session: ChatSession,
|
|
647
|
-
command: string
|
|
654
|
+
command: string
|
|
648
655
|
): Promise<void> {
|
|
649
656
|
const [cmd, ...args] = command.slice(1).split(' ');
|
|
650
657
|
|
|
@@ -693,20 +700,20 @@ export class ChatCommands {
|
|
|
693
700
|
process.cwd(),
|
|
694
701
|
'.wundr',
|
|
695
702
|
'chat',
|
|
696
|
-
`${session.id}.json
|
|
703
|
+
`${session.id}.json`
|
|
697
704
|
);
|
|
698
705
|
await fs.ensureDir(path.dirname(sessionPath));
|
|
699
706
|
await fs.writeJson(sessionPath, session, { spaces: 2 });
|
|
700
707
|
}
|
|
701
708
|
|
|
702
709
|
private async loadChatSession(
|
|
703
|
-
sessionId: string
|
|
710
|
+
sessionId: string
|
|
704
711
|
): Promise<ChatSession | null> {
|
|
705
712
|
const sessionPath = path.join(
|
|
706
713
|
process.cwd(),
|
|
707
714
|
'.wundr',
|
|
708
715
|
'chat',
|
|
709
|
-
`${sessionId}.json
|
|
716
|
+
`${sessionId}.json`
|
|
710
717
|
);
|
|
711
718
|
if (await fs.pathExists(sessionPath)) {
|
|
712
719
|
const data = await fs.readJson(sessionPath);
|
|
@@ -751,7 +758,7 @@ export class ChatCommands {
|
|
|
751
758
|
process.cwd(),
|
|
752
759
|
'.wundr',
|
|
753
760
|
'chat',
|
|
754
|
-
`${sessionId}.json
|
|
761
|
+
`${sessionId}.json`
|
|
755
762
|
);
|
|
756
763
|
if (await fs.pathExists(sessionPath)) {
|
|
757
764
|
await fs.remove(sessionPath);
|
|
@@ -869,7 +876,7 @@ export class ChatCommands {
|
|
|
869
876
|
Object.entries(variables).forEach(([key, value]) => {
|
|
870
877
|
processedTemplate = processedTemplate.replace(
|
|
871
878
|
`{{${key}}}`,
|
|
872
|
-
String(value)
|
|
879
|
+
String(value)
|
|
873
880
|
);
|
|
874
881
|
});
|
|
875
882
|
}
|