keystone-cli 2.0.0 → 2.1.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 +43 -4
- package/package.json +4 -1
- package/src/cli.ts +1 -0
- package/src/commands/event.ts +9 -0
- package/src/commands/run.ts +17 -0
- package/src/db/dynamic-state-manager.ts +12 -9
- package/src/db/memory-db.test.ts +19 -1
- package/src/db/memory-db.ts +101 -22
- package/src/db/workflow-db.ts +181 -9
- package/src/expression/evaluator.ts +4 -1
- package/src/parser/config-schema.ts +6 -0
- package/src/parser/schema.ts +1 -0
- package/src/runner/__test__/llm-test-setup.ts +43 -11
- package/src/runner/durable-timers.test.ts +1 -1
- package/src/runner/executors/dynamic-executor.ts +125 -88
- package/src/runner/executors/engine-executor.ts +10 -39
- package/src/runner/executors/file-executor.ts +67 -0
- package/src/runner/executors/foreach-executor.ts +170 -17
- package/src/runner/executors/human-executor.ts +18 -0
- package/src/runner/executors/llm/stream-handler.ts +103 -0
- package/src/runner/executors/llm/tool-manager.ts +360 -0
- package/src/runner/executors/llm-executor.ts +288 -555
- package/src/runner/executors/memory-executor.ts +41 -34
- package/src/runner/executors/shell-executor.ts +96 -52
- package/src/runner/executors/subworkflow-executor.ts +16 -0
- package/src/runner/executors/types.ts +3 -1
- package/src/runner/executors/verification_fixes.test.ts +46 -0
- package/src/runner/join-scheduling.test.ts +2 -1
- package/src/runner/llm-adapter.integration.test.ts +10 -5
- package/src/runner/llm-adapter.ts +57 -18
- package/src/runner/llm-clarification.test.ts +4 -1
- package/src/runner/llm-executor.test.ts +21 -7
- package/src/runner/mcp-client.ts +36 -2
- package/src/runner/mcp-server.ts +65 -36
- package/src/runner/recovery-security.test.ts +5 -2
- package/src/runner/reflexion.test.ts +6 -3
- package/src/runner/services/context-builder.ts +13 -4
- package/src/runner/services/workflow-validator.ts +2 -1
- package/src/runner/standard-tools-ast.test.ts +4 -2
- package/src/runner/standard-tools-execution.test.ts +14 -1
- package/src/runner/standard-tools-integration.test.ts +6 -0
- package/src/runner/standard-tools.ts +13 -10
- package/src/runner/step-executor.ts +2 -2
- package/src/runner/tool-integration.test.ts +4 -1
- package/src/runner/workflow-runner.test.ts +23 -12
- package/src/runner/workflow-runner.ts +172 -79
- package/src/runner/workflow-state.ts +181 -111
- package/src/ui/dashboard.tsx +17 -3
- package/src/utils/config-loader.ts +4 -0
- package/src/utils/constants.ts +4 -0
- package/src/utils/context-injector.test.ts +27 -27
- package/src/utils/context-injector.ts +68 -26
- package/src/utils/process-sandbox.ts +138 -148
- package/src/utils/redactor.ts +39 -9
- package/src/utils/resource-loader.ts +24 -19
- package/src/utils/sandbox.ts +6 -0
- package/src/utils/stream-utils.ts +58 -0
|
@@ -208,7 +208,7 @@ function convertToExecutableStep(
|
|
|
208
208
|
export async function executeDynamicStep(
|
|
209
209
|
step: DynamicStep,
|
|
210
210
|
context: ExpressionContext,
|
|
211
|
-
executeStepFn: (step: Step, context: ExpressionContext) => Promise<StepResult>,
|
|
211
|
+
executeStepFn: (step: Step, context: ExpressionContext, depth?: number) => Promise<StepResult>,
|
|
212
212
|
logger: Logger,
|
|
213
213
|
options: StepExecutorOptions & {
|
|
214
214
|
stateManager?: DynamicStateManager;
|
|
@@ -216,9 +216,18 @@ export async function executeDynamicStep(
|
|
|
216
216
|
saveState?: (stepId: string, state: DynamicStepState) => Promise<void>;
|
|
217
217
|
executeLlmStep?: typeof executeLlmStep;
|
|
218
218
|
executeHumanStep?: typeof executeHumanStep;
|
|
219
|
+
depth?: number;
|
|
219
220
|
}
|
|
220
221
|
): Promise<StepResult> {
|
|
221
222
|
const { runId, db, abortSignal } = options;
|
|
223
|
+
const depth = options.depth || 0;
|
|
224
|
+
|
|
225
|
+
// Prevent infinite recursion in dynamic steps
|
|
226
|
+
// 10 is matching WorkflowRunner.MAX_RECURSION_DEPTH
|
|
227
|
+
// We should ideally import this constant, but for now we hardcode or add to LIMITS
|
|
228
|
+
if (depth > 10) {
|
|
229
|
+
throw new Error('Maximum workflow recursion depth (10) exceeded in dynamic step.');
|
|
230
|
+
}
|
|
222
231
|
const stateManager = options.stateManager || (db ? new DynamicStateManager(db) : null);
|
|
223
232
|
|
|
224
233
|
const { state, dbState } = await initializeState(step, runId, stateManager, options.loadState);
|
|
@@ -242,7 +251,7 @@ export async function executeDynamicStep(
|
|
|
242
251
|
stateManager,
|
|
243
252
|
executeStepFn,
|
|
244
253
|
logger,
|
|
245
|
-
options
|
|
254
|
+
{ ...options, depth }
|
|
246
255
|
);
|
|
247
256
|
}
|
|
248
257
|
|
|
@@ -259,7 +268,7 @@ export async function executeDynamicStep(
|
|
|
259
268
|
stateManager,
|
|
260
269
|
executeStepFn,
|
|
261
270
|
logger,
|
|
262
|
-
options
|
|
271
|
+
{ ...options, depth }
|
|
263
272
|
);
|
|
264
273
|
}
|
|
265
274
|
}
|
|
@@ -329,12 +338,13 @@ async function handlePlanningPhase(
|
|
|
329
338
|
state: DynamicStepState,
|
|
330
339
|
dbState: DynamicStepState | null,
|
|
331
340
|
stateManager: DynamicStateManager | null,
|
|
332
|
-
executeStepFn: (step: Step, context: ExpressionContext) => Promise<StepResult>,
|
|
341
|
+
executeStepFn: (step: Step, context: ExpressionContext, depth?: number) => Promise<StepResult>,
|
|
333
342
|
logger: Logger,
|
|
334
343
|
options: StepExecutorOptions & {
|
|
335
344
|
stateManager?: DynamicStateManager;
|
|
336
345
|
saveState?: (stepId: string, state: DynamicStepState) => Promise<void>;
|
|
337
346
|
executeLlmStep?: typeof executeLlmStep;
|
|
347
|
+
depth?: number;
|
|
338
348
|
}
|
|
339
349
|
) {
|
|
340
350
|
const { runId, emitEvent, workflowName, abortSignal, mcpManager, workflowDir } = options;
|
|
@@ -392,6 +402,17 @@ async function handlePlanningPhase(
|
|
|
392
402
|
}
|
|
393
403
|
|
|
394
404
|
state.generatedPlan = planResult.output as DynamicPlan;
|
|
405
|
+
// Early validation of plan structure and dependencies
|
|
406
|
+
try {
|
|
407
|
+
topologicalSort(state.generatedPlan.steps);
|
|
408
|
+
} catch (err) {
|
|
409
|
+
state.status = 'failed';
|
|
410
|
+
state.error = err instanceof Error ? err.message : String(err);
|
|
411
|
+
if (stateManager && dbState && dbState.id)
|
|
412
|
+
await stateManager.finish(dbState.id, 'failed', state.error);
|
|
413
|
+
throw new Error(state.error);
|
|
414
|
+
}
|
|
415
|
+
|
|
395
416
|
state.status = step.confirmPlan ? 'awaiting_confirmation' : 'executing';
|
|
396
417
|
|
|
397
418
|
logger.log(` 📋 Plan generated with ${state.generatedPlan.steps.length} steps:`);
|
|
@@ -449,6 +470,8 @@ async function handleConfirmationPhase(
|
|
|
449
470
|
try {
|
|
450
471
|
const modifiedPlan = JSON.parse(response) as DynamicPlan;
|
|
451
472
|
if (modifiedPlan.steps && Array.isArray(modifiedPlan.steps)) {
|
|
473
|
+
// Validate modified plan
|
|
474
|
+
topologicalSort(modifiedPlan.steps);
|
|
452
475
|
state.generatedPlan = modifiedPlan;
|
|
453
476
|
logger.log(' ✓ Using modified plan');
|
|
454
477
|
}
|
|
@@ -480,14 +503,15 @@ async function handleExecutionPhase(
|
|
|
480
503
|
state: DynamicStepState,
|
|
481
504
|
dbState: DynamicStepState | null,
|
|
482
505
|
stateManager: DynamicStateManager | null,
|
|
483
|
-
executeStepFn: (step: Step, context: ExpressionContext) => Promise<StepResult>,
|
|
506
|
+
executeStepFn: (step: Step, context: ExpressionContext, depth?: number) => Promise<StepResult>,
|
|
484
507
|
logger: Logger,
|
|
485
508
|
options: StepExecutorOptions & {
|
|
486
509
|
stateManager?: DynamicStateManager;
|
|
487
510
|
saveState?: (stepId: string, state: DynamicStepState) => Promise<void>;
|
|
511
|
+
depth?: number;
|
|
488
512
|
}
|
|
489
513
|
) {
|
|
490
|
-
const { abortSignal, runId, workflowName, emitEvent, saveState } = options;
|
|
514
|
+
const { abortSignal, runId, workflowName, emitEvent, saveState, depth = 0 } = options;
|
|
491
515
|
|
|
492
516
|
// Detect circular dependencies and validate plan
|
|
493
517
|
topologicalSort(state.generatedPlan.steps);
|
|
@@ -522,6 +546,9 @@ async function handleExecutionPhase(
|
|
|
522
546
|
1;
|
|
523
547
|
logger.log(` 🚀 Starting parallel execution (concurrency: ${maxConcurrency})`);
|
|
524
548
|
|
|
549
|
+
// Track running promises to avoid busy wait
|
|
550
|
+
const executionPromises = new Set<Promise<void>>();
|
|
551
|
+
|
|
525
552
|
while (completed.size + failed.size < state.generatedPlan.steps.length) {
|
|
526
553
|
if (abortSignal?.aborted) throw new Error('Dynamic step execution canceled');
|
|
527
554
|
|
|
@@ -551,94 +578,104 @@ async function handleExecutionPhase(
|
|
|
551
578
|
break;
|
|
552
579
|
}
|
|
553
580
|
|
|
581
|
+
// Helper to wrap execution with cleanup
|
|
582
|
+
const executeWrapper = async (genStep: GeneratedStep, i: number) => {
|
|
583
|
+
try {
|
|
584
|
+
logger.log(
|
|
585
|
+
` ⚡ [${i + 1}/${state.generatedPlan.steps.length}] Executing step: ${genStep.name}`
|
|
586
|
+
);
|
|
587
|
+
|
|
588
|
+
const executableStep = convertToExecutableStep(genStep, step.id, step.allowInsecure);
|
|
589
|
+
const stepContext = {
|
|
590
|
+
...dynamicContext,
|
|
591
|
+
steps: {
|
|
592
|
+
...(dynamicContext.steps || {}),
|
|
593
|
+
...Object.fromEntries(
|
|
594
|
+
Array.from(resultsMap.entries()).map(([id, res]) => [
|
|
595
|
+
`${step.id}_${id}`,
|
|
596
|
+
{ output: res.output },
|
|
597
|
+
])
|
|
598
|
+
),
|
|
599
|
+
},
|
|
600
|
+
} as ExpressionContext;
|
|
601
|
+
|
|
602
|
+
if (emitEvent && runId && workflowName) {
|
|
603
|
+
emitEvent({
|
|
604
|
+
type: 'step.start',
|
|
605
|
+
timestamp: new Date().toISOString(),
|
|
606
|
+
runId,
|
|
607
|
+
workflow: workflowName,
|
|
608
|
+
stepId: executableStep.id,
|
|
609
|
+
stepType: executableStep.type,
|
|
610
|
+
phase: 'main',
|
|
611
|
+
stepIndex: i + 1,
|
|
612
|
+
totalSteps: state.generatedPlan.steps.length,
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
const res = await executeStepFn(executableStep, stepContext, depth + 1);
|
|
617
|
+
resultsMap.set(genStep.id, res);
|
|
618
|
+
state.stepResults.set(genStep.id, res);
|
|
619
|
+
|
|
620
|
+
if (stateManager && dbState && dbState.id) {
|
|
621
|
+
await stateManager.completeStep(dbState.id, genStep.id, res);
|
|
622
|
+
await stateManager.updateProgress(dbState.id, completed.size + failed.size + 1);
|
|
623
|
+
} else if (saveState) {
|
|
624
|
+
await saveState(step.id, state);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
if (emitEvent && runId && workflowName) {
|
|
628
|
+
emitEvent({
|
|
629
|
+
type: 'step.end',
|
|
630
|
+
timestamp: new Date().toISOString(),
|
|
631
|
+
runId,
|
|
632
|
+
workflow: workflowName,
|
|
633
|
+
stepId: executableStep.id,
|
|
634
|
+
stepType: executableStep.type,
|
|
635
|
+
phase: 'main',
|
|
636
|
+
status: res.status as any,
|
|
637
|
+
stepIndex: i + 1,
|
|
638
|
+
totalSteps: state.generatedPlan.steps.length,
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
if (res.status === 'success') {
|
|
643
|
+
completed.add(genStep.id);
|
|
644
|
+
} else {
|
|
645
|
+
failed.add(genStep.id);
|
|
646
|
+
if (!(genStep.allowStepFailure ?? step.allowStepFailure ?? false)) {
|
|
647
|
+
state.status = 'failed';
|
|
648
|
+
state.error = `Step "${genStep.name}" failed: ${res.error}`;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
} catch (err) {
|
|
652
|
+
const failRes: StepResult = { status: 'failed', error: String(err), output: {} };
|
|
653
|
+
resultsMap.set(genStep.id, failRes);
|
|
654
|
+
state.stepResults.set(genStep.id, failRes);
|
|
655
|
+
failed.add(genStep.id);
|
|
656
|
+
state.status = 'failed';
|
|
657
|
+
state.error = `Step "${genStep.name}" crashed: ${err}`;
|
|
658
|
+
} finally {
|
|
659
|
+
running.delete(genStep.id);
|
|
660
|
+
}
|
|
661
|
+
};
|
|
662
|
+
|
|
554
663
|
if (ready.length > 0 && running.size < maxConcurrency) {
|
|
555
664
|
const toStart = ready.slice(0, maxConcurrency - running.size);
|
|
556
665
|
for (const genStep of toStart) {
|
|
557
666
|
running.add(genStep.id);
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
);
|
|
564
|
-
|
|
565
|
-
const executableStep = convertToExecutableStep(genStep, step.id, step.allowInsecure);
|
|
566
|
-
const stepContext = {
|
|
567
|
-
...dynamicContext,
|
|
568
|
-
steps: {
|
|
569
|
-
...(dynamicContext.steps || {}),
|
|
570
|
-
...Object.fromEntries(
|
|
571
|
-
Array.from(resultsMap.entries()).map(([id, res]) => [
|
|
572
|
-
`${step.id}_${id}`,
|
|
573
|
-
{ output: res.output },
|
|
574
|
-
])
|
|
575
|
-
),
|
|
576
|
-
},
|
|
577
|
-
} as ExpressionContext;
|
|
578
|
-
|
|
579
|
-
if (emitEvent && runId && workflowName) {
|
|
580
|
-
emitEvent({
|
|
581
|
-
type: 'step.start',
|
|
582
|
-
timestamp: new Date().toISOString(),
|
|
583
|
-
runId,
|
|
584
|
-
workflow: workflowName,
|
|
585
|
-
stepId: executableStep.id,
|
|
586
|
-
stepType: executableStep.type,
|
|
587
|
-
phase: 'main',
|
|
588
|
-
stepIndex: i + 1,
|
|
589
|
-
totalSteps: state.generatedPlan.steps.length,
|
|
590
|
-
});
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
const res = await executeStepFn(executableStep, stepContext);
|
|
594
|
-
resultsMap.set(genStep.id, res);
|
|
595
|
-
state.stepResults.set(genStep.id, res);
|
|
596
|
-
|
|
597
|
-
if (stateManager && dbState && dbState.id) {
|
|
598
|
-
await stateManager.completeStep(dbState.id, genStep.id, res);
|
|
599
|
-
await stateManager.updateProgress(dbState.id, completed.size + failed.size + 1);
|
|
600
|
-
} else if (saveState) {
|
|
601
|
-
await saveState(step.id, state);
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
if (emitEvent && runId && workflowName) {
|
|
605
|
-
emitEvent({
|
|
606
|
-
type: 'step.end',
|
|
607
|
-
timestamp: new Date().toISOString(),
|
|
608
|
-
runId,
|
|
609
|
-
workflow: workflowName,
|
|
610
|
-
stepId: executableStep.id,
|
|
611
|
-
stepType: executableStep.type,
|
|
612
|
-
phase: 'main',
|
|
613
|
-
status: res.status as any,
|
|
614
|
-
stepIndex: i + 1,
|
|
615
|
-
totalSteps: state.generatedPlan.steps.length,
|
|
616
|
-
});
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
if (res.status === 'success') {
|
|
620
|
-
completed.add(genStep.id);
|
|
621
|
-
} else {
|
|
622
|
-
failed.add(genStep.id);
|
|
623
|
-
if (!(genStep.allowStepFailure ?? step.allowStepFailure ?? false)) {
|
|
624
|
-
state.status = 'failed';
|
|
625
|
-
state.error = `Step "${genStep.name}" failed: ${res.error}`;
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
} catch (err) {
|
|
629
|
-
const failRes: StepResult = { status: 'failed', error: String(err), output: {} };
|
|
630
|
-
resultsMap.set(genStep.id, failRes);
|
|
631
|
-
state.stepResults.set(genStep.id, failRes);
|
|
632
|
-
failed.add(genStep.id);
|
|
633
|
-
state.status = 'failed';
|
|
634
|
-
state.error = `Step "${genStep.name}" crashed: ${err}`;
|
|
635
|
-
} finally {
|
|
636
|
-
running.delete(genStep.id);
|
|
637
|
-
}
|
|
638
|
-
})();
|
|
667
|
+
const i = state.generatedPlan.steps.indexOf(genStep);
|
|
668
|
+
const promise = executeWrapper(genStep, i).then(() => {
|
|
669
|
+
executionPromises.delete(promise);
|
|
670
|
+
});
|
|
671
|
+
executionPromises.add(promise);
|
|
639
672
|
}
|
|
640
673
|
}
|
|
641
|
-
|
|
674
|
+
|
|
675
|
+
// Wait for at least one task to complete before re-evaluating loop
|
|
676
|
+
if (executionPromises.size > 0) {
|
|
677
|
+
await Promise.race(executionPromises);
|
|
678
|
+
}
|
|
642
679
|
}
|
|
643
680
|
|
|
644
681
|
const allSatisfied = Array.from(resultsMap.entries()).every(
|
|
@@ -2,6 +2,7 @@ import { spawn } from 'node:child_process';
|
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import { existsSync, mkdirSync } from 'node:fs';
|
|
4
4
|
import * as path from 'node:path';
|
|
5
|
+
import { StringDecoder } from 'node:string_decoder';
|
|
5
6
|
import yaml from 'js-yaml';
|
|
6
7
|
import type { ExpressionContext } from '../../expression/evaluator';
|
|
7
8
|
import { ExpressionEvaluator } from '../../expression/evaluator';
|
|
@@ -52,44 +53,7 @@ class LRUCache<K, V> {
|
|
|
52
53
|
}
|
|
53
54
|
|
|
54
55
|
const VERSION_CACHE = new LRUCache<string, string>(LIMITS.VERSION_CACHE_MAX_SIZE);
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
function createOutputLimiter(maxBytes: number) {
|
|
58
|
-
let bytes = 0;
|
|
59
|
-
let text = '';
|
|
60
|
-
let truncated = false;
|
|
61
|
-
|
|
62
|
-
const append = (chunk: Buffer | string) => {
|
|
63
|
-
if (truncated || maxBytes <= 0) {
|
|
64
|
-
truncated = true;
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
67
|
-
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
68
|
-
const remaining = maxBytes - bytes;
|
|
69
|
-
if (remaining <= 0) {
|
|
70
|
-
truncated = true;
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
if (buffer.length <= remaining) {
|
|
74
|
-
text += buffer.toString();
|
|
75
|
-
bytes += buffer.length;
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
text += buffer.subarray(0, remaining).toString();
|
|
79
|
-
bytes = maxBytes;
|
|
80
|
-
truncated = true;
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
const finalize = () => (truncated ? `${text}${TRUNCATED_SUFFIX}` : text);
|
|
84
|
-
|
|
85
|
-
return {
|
|
86
|
-
append,
|
|
87
|
-
finalize,
|
|
88
|
-
get truncated() {
|
|
89
|
-
return truncated;
|
|
90
|
-
},
|
|
91
|
-
};
|
|
92
|
-
}
|
|
56
|
+
import { createOutputLimiter } from '../../utils/stream-utils';
|
|
93
57
|
|
|
94
58
|
export interface EngineExecutionResult {
|
|
95
59
|
stdout: string;
|
|
@@ -163,11 +127,17 @@ async function runCommand(
|
|
|
163
127
|
if (child.stdout) {
|
|
164
128
|
child.stdout.on('data', (chunk: Buffer) => {
|
|
165
129
|
stdoutLimiter.append(chunk);
|
|
130
|
+
if (stdoutLimiter.truncated) {
|
|
131
|
+
child.kill();
|
|
132
|
+
}
|
|
166
133
|
});
|
|
167
134
|
}
|
|
168
135
|
if (child.stderr) {
|
|
169
136
|
child.stderr.on('data', (chunk: Buffer) => {
|
|
170
137
|
stderrLimiter.append(chunk);
|
|
138
|
+
if (stderrLimiter.truncated) {
|
|
139
|
+
child.kill();
|
|
140
|
+
}
|
|
171
141
|
});
|
|
172
142
|
}
|
|
173
143
|
|
|
@@ -208,7 +178,8 @@ async function checkEngineVersion(
|
|
|
208
178
|
cwd: string,
|
|
209
179
|
abortSignal?: AbortSignal
|
|
210
180
|
): Promise<string> {
|
|
211
|
-
const
|
|
181
|
+
const pathEnv = env.PATH || env.Path || env.path || '';
|
|
182
|
+
const cacheKey = `${command}::${versionArgs.join(' ')}::${cwd}::${pathEnv}`;
|
|
212
183
|
const cached = VERSION_CACHE.get(cacheKey);
|
|
213
184
|
if (cached) return cached;
|
|
214
185
|
|
|
@@ -113,6 +113,73 @@ export function parseUnifiedDiff(patch: string): UnifiedDiff {
|
|
|
113
113
|
}
|
|
114
114
|
|
|
115
115
|
export function applyUnifiedDiff(content: string, patch: string, targetPath: string): string {
|
|
116
|
+
// Try using system `patch` command first as it's more robust
|
|
117
|
+
try {
|
|
118
|
+
const { spawnSync } = require('node:child_process');
|
|
119
|
+
|
|
120
|
+
// Check if patch is available (quick check)
|
|
121
|
+
// We assume standard unix `patch` or compatible.
|
|
122
|
+
// writing content to temp file and patch to temp file?
|
|
123
|
+
// actually, we can pipe to stdin.
|
|
124
|
+
// echo content | patch -o output
|
|
125
|
+
// But patch usually works on files.
|
|
126
|
+
|
|
127
|
+
// Since we are operating on in-memory strings (content), using `patch` binary requires tmp files.
|
|
128
|
+
// This might be slow for many small files.
|
|
129
|
+
// BUT the robustness is worth it.
|
|
130
|
+
|
|
131
|
+
const fs = require('node:fs');
|
|
132
|
+
const os = require('node:os');
|
|
133
|
+
const path = require('node:path');
|
|
134
|
+
|
|
135
|
+
// Create temp dir
|
|
136
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'keystone-patch-'));
|
|
137
|
+
const tmpSrc = path.join(tmpDir, 'source');
|
|
138
|
+
const tmpPatch = path.join(tmpDir, 'changes.patch');
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
fs.writeFileSync(tmpSrc, content);
|
|
142
|
+
fs.writeFileSync(tmpPatch, patch);
|
|
143
|
+
|
|
144
|
+
// Run patch: patch -p1 -i changes.patch -o output (if headers have paths)
|
|
145
|
+
// Or just patch tmpSrc < changes.patch?
|
|
146
|
+
// Unified diffs usually expect paths.
|
|
147
|
+
// If we force it...
|
|
148
|
+
// `patch` utility is tricky with paths.
|
|
149
|
+
// LLM generated diffs might have /dev/null or a/b paths.
|
|
150
|
+
|
|
151
|
+
// Let's try `git apply` if inside a git repo?
|
|
152
|
+
// No, we might not be in a git repo.
|
|
153
|
+
|
|
154
|
+
// Let's stick to the JS Custom Parser BUT make it more lenient/robust as per user request?
|
|
155
|
+
// User said: "rely on the system's patch or git apply"
|
|
156
|
+
|
|
157
|
+
// Let's try `patch -u -l --fuzz=2 -i patchfile srcfile -o outfile`
|
|
158
|
+
const result = spawnSync(
|
|
159
|
+
'patch',
|
|
160
|
+
['-u', '-l', '--fuzz=2', '-i', tmpPatch, tmpSrc, '-o', path.join(tmpDir, 'result')],
|
|
161
|
+
{
|
|
162
|
+
encoding: 'utf-8',
|
|
163
|
+
stdio: 'pipe',
|
|
164
|
+
}
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
if (result.status === 0 && fs.existsSync(path.join(tmpDir, 'result'))) {
|
|
168
|
+
return fs.readFileSync(path.join(tmpDir, 'result'), 'utf-8');
|
|
169
|
+
}
|
|
170
|
+
} catch (e) {
|
|
171
|
+
// ignore
|
|
172
|
+
} finally {
|
|
173
|
+
// cleanup
|
|
174
|
+
try {
|
|
175
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
176
|
+
} catch {}
|
|
177
|
+
}
|
|
178
|
+
} catch (e) {
|
|
179
|
+
// ignore
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Fallback to JS implementation
|
|
116
183
|
const diff = parseUnifiedDiff(patch);
|
|
117
184
|
assertDiffMatchesTarget(diff.newFile || diff.originalFile, targetPath);
|
|
118
185
|
|