winter-super-cli 2026.5.17 → 2026.5.18
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/package.json +1 -1
- package/src/tools/executor.js +93 -13
- package/src/tools/executor.test.js +35 -0
package/package.json
CHANGED
package/src/tools/executor.js
CHANGED
|
@@ -5,11 +5,12 @@
|
|
|
5
5
|
|
|
6
6
|
import { promises as fs } from 'fs';
|
|
7
7
|
import path from 'path';
|
|
8
|
-
import { exec } from 'child_process';
|
|
8
|
+
import { exec, execFile } from 'child_process';
|
|
9
9
|
import { promisify } from 'util';
|
|
10
10
|
import { diffLines } from 'diff';
|
|
11
11
|
|
|
12
12
|
const execAsync = promisify(exec);
|
|
13
|
+
const execFileAsync = promisify(execFile);
|
|
13
14
|
|
|
14
15
|
export class ToolExecutor {
|
|
15
16
|
constructor(repl) {
|
|
@@ -262,6 +263,7 @@ export class ToolExecutor {
|
|
|
262
263
|
bash: 'Bash',
|
|
263
264
|
shell: 'Bash',
|
|
264
265
|
command: 'Bash',
|
|
266
|
+
commandexecutor: 'Bash',
|
|
265
267
|
executecommand: 'Bash',
|
|
266
268
|
runcommand: 'Bash',
|
|
267
269
|
terminal: 'Bash',
|
|
@@ -445,6 +447,9 @@ export class ToolExecutor {
|
|
|
445
447
|
timeout = parseInt(timeout, 10);
|
|
446
448
|
if (isNaN(timeout) || timeout < 0) timeout = 60000;
|
|
447
449
|
|
|
450
|
+
const heredocResult = await this.tryHandleHeredocWrite(command, cwd);
|
|
451
|
+
if (heredocResult) return heredocResult;
|
|
452
|
+
|
|
448
453
|
command = await this.translateWindowsCommand(command);
|
|
449
454
|
|
|
450
455
|
// Security check
|
|
@@ -456,7 +461,14 @@ export class ToolExecutor {
|
|
|
456
461
|
}
|
|
457
462
|
|
|
458
463
|
try {
|
|
459
|
-
const { stdout, stderr } =
|
|
464
|
+
const { stdout, stderr } = process.platform === 'win32'
|
|
465
|
+
? await execFileAsync('powershell.exe', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', command], {
|
|
466
|
+
cwd,
|
|
467
|
+
timeout,
|
|
468
|
+
windowsHide: true,
|
|
469
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
470
|
+
})
|
|
471
|
+
: await execAsync(command, { cwd, timeout, shell: true, maxBuffer: 10 * 1024 * 1024 });
|
|
460
472
|
return {
|
|
461
473
|
success: true,
|
|
462
474
|
stdout: stdout || '',
|
|
@@ -557,16 +569,17 @@ export class ToolExecutor {
|
|
|
557
569
|
if (process.platform !== 'win32') return command;
|
|
558
570
|
|
|
559
571
|
const trimmed = command.trim();
|
|
560
|
-
const
|
|
561
|
-
if (lsRecursive) {
|
|
562
|
-
const target = lsRecursive[1].trim();
|
|
563
|
-
return `powershell -NoProfile -Command "Get-ChildItem -LiteralPath ${this.quotePowerShellString(target)} -Recurse -Force | Select-Object -ExpandProperty FullName"`;
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
const ls = trimmed.match(/^ls\s+(.+)$/i);
|
|
572
|
+
const ls = trimmed.match(/^ls(?:\s+(.+))?$/i);
|
|
567
573
|
if (ls) {
|
|
568
|
-
const
|
|
569
|
-
|
|
574
|
+
const parsed = this.parseLsArgs(ls[1] || '');
|
|
575
|
+
const target = parsed.target || '.';
|
|
576
|
+
const recurse = parsed.flags.has('r') || parsed.flags.has('recursive');
|
|
577
|
+
const long = parsed.flags.has('l') || parsed.flags.has('la') || parsed.flags.has('al');
|
|
578
|
+
const force = parsed.flags.has('a') || parsed.flags.has('force') || parsed.flags.has('la') || parsed.flags.has('al');
|
|
579
|
+
const fields = long
|
|
580
|
+
? 'Mode,Length,LastWriteTime,Name'
|
|
581
|
+
: 'Name';
|
|
582
|
+
return `Get-ChildItem -LiteralPath ${this.quotePowerShellString(target)}${recurse ? ' -Recurse' : ''}${force ? ' -Force' : ''} | Select-Object ${fields}`;
|
|
570
583
|
}
|
|
571
584
|
|
|
572
585
|
const cat = trimmed.match(/^cat\s+(.+)$/i);
|
|
@@ -576,15 +589,82 @@ export class ToolExecutor {
|
|
|
576
589
|
try {
|
|
577
590
|
const stat = await fs.stat(normalizedTarget);
|
|
578
591
|
if (stat.isDirectory()) {
|
|
579
|
-
return `
|
|
592
|
+
return `Get-ChildItem -LiteralPath ${this.quotePowerShellString(target)} -Force | Select-Object -ExpandProperty Name`;
|
|
580
593
|
}
|
|
581
594
|
} catch {}
|
|
582
|
-
return `
|
|
595
|
+
return `Get-Content -LiteralPath ${this.quotePowerShellString(target)}`;
|
|
583
596
|
}
|
|
584
597
|
|
|
585
598
|
return command;
|
|
586
599
|
}
|
|
587
600
|
|
|
601
|
+
async tryHandleHeredocWrite(command, cwd) {
|
|
602
|
+
const match = String(command || '').match(/^cat\s*>\s*(.+?)\s*<<\s*['"]?([A-Za-z0-9_-]+)['"]?\r?\n([\s\S]*?)\r?\n\2\s*$/);
|
|
603
|
+
if (!match) return null;
|
|
604
|
+
|
|
605
|
+
const [, rawTarget, , content] = match;
|
|
606
|
+
const target = this.resolveInputPath(rawTarget.trim(), cwd);
|
|
607
|
+
return await this.writeFile(target, content.endsWith('\n') ? content : `${content}\n`);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
parseLsArgs(rawArgs) {
|
|
611
|
+
const tokens = this.splitShellLike(rawArgs);
|
|
612
|
+
const flags = new Set();
|
|
613
|
+
const paths = [];
|
|
614
|
+
|
|
615
|
+
for (const token of tokens) {
|
|
616
|
+
if (token.startsWith('-') && token.length > 1) {
|
|
617
|
+
const flag = token.replace(/^-+/, '').toLowerCase();
|
|
618
|
+
flags.add(flag);
|
|
619
|
+
if (!flag.includes('=')) {
|
|
620
|
+
for (const ch of flag) flags.add(ch);
|
|
621
|
+
}
|
|
622
|
+
} else {
|
|
623
|
+
paths.push(token);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
return { flags, target: paths.join(' ') };
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
splitShellLike(text) {
|
|
631
|
+
const tokens = [];
|
|
632
|
+
let current = '';
|
|
633
|
+
let quote = null;
|
|
634
|
+
let escaped = false;
|
|
635
|
+
|
|
636
|
+
for (const ch of String(text || '')) {
|
|
637
|
+
if (escaped) {
|
|
638
|
+
current += ch;
|
|
639
|
+
escaped = false;
|
|
640
|
+
continue;
|
|
641
|
+
}
|
|
642
|
+
if (ch === '\\') {
|
|
643
|
+
escaped = true;
|
|
644
|
+
continue;
|
|
645
|
+
}
|
|
646
|
+
if ((ch === '"' || ch === "'") && !quote) {
|
|
647
|
+
quote = ch;
|
|
648
|
+
continue;
|
|
649
|
+
}
|
|
650
|
+
if (ch === quote) {
|
|
651
|
+
quote = null;
|
|
652
|
+
continue;
|
|
653
|
+
}
|
|
654
|
+
if (/\s/.test(ch) && !quote) {
|
|
655
|
+
if (current) {
|
|
656
|
+
tokens.push(current);
|
|
657
|
+
current = '';
|
|
658
|
+
}
|
|
659
|
+
continue;
|
|
660
|
+
}
|
|
661
|
+
current += ch;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
if (current) tokens.push(current);
|
|
665
|
+
return tokens;
|
|
666
|
+
}
|
|
667
|
+
|
|
588
668
|
quotePowerShellString(value) {
|
|
589
669
|
const unquoted = value.replace(/^['"]|['"]$/g, '');
|
|
590
670
|
return `'${unquoted.replace(/'/g, "''")}'`;
|
|
@@ -20,6 +20,7 @@ test('tool names accept common model aliases', () => {
|
|
|
20
20
|
assert.equal(tools.normalizeToolName('read_file'), 'Read');
|
|
21
21
|
assert.equal(tools.normalizeToolName('write_to_file'), 'Write');
|
|
22
22
|
assert.equal(tools.normalizeToolName('replace_in_file'), 'Edit');
|
|
23
|
+
assert.equal(tools.normalizeToolName('command_executor'), 'Bash');
|
|
23
24
|
assert.equal(tools.normalizeToolName('execute_command'), 'Bash');
|
|
24
25
|
assert.equal(tools.normalizeToolName('list_files'), 'Glob');
|
|
25
26
|
assert.equal(tools.normalizeToolName('search_files'), 'Grep');
|
|
@@ -27,6 +28,40 @@ test('tool names accept common model aliases', () => {
|
|
|
27
28
|
assert.equal(tools.normalizeToolName('web-search'), 'WebSearch');
|
|
28
29
|
});
|
|
29
30
|
|
|
31
|
+
test('Bash supports model-style heredoc file writes', async () => {
|
|
32
|
+
const root = await mkdtemp(path.join(tmpdir(), 'winter-heredoc-'));
|
|
33
|
+
const tools = new ToolExecutor({ projectPath: root });
|
|
34
|
+
const result = await tools.execute('command_executor', {
|
|
35
|
+
command: "cat > src/components/InstallSection.tsx << 'EOF'\nconst ok = true;\nEOF",
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
assert.equal(result.success, true);
|
|
39
|
+
assert.equal(result.path, path.join(root, 'src', 'components', 'InstallSection.tsx'));
|
|
40
|
+
|
|
41
|
+
const read = await tools.execute('Read', { file_path: 'src/components/InstallSection.tsx' });
|
|
42
|
+
assert.equal(read.content, 'const ok = true;\n');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('Windows Bash runs PowerShell commands and translates ls flags', async (t) => {
|
|
46
|
+
if (process.platform !== 'win32') {
|
|
47
|
+
t.skip('Windows-only shell behavior');
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const root = await mkdtemp(path.join(tmpdir(), 'winter-powershell-'));
|
|
52
|
+
await mkdir(path.join(root, 'src', 'components'), { recursive: true });
|
|
53
|
+
await writeFile(path.join(root, 'src', 'components', 'a.txt'), 'hello');
|
|
54
|
+
|
|
55
|
+
const tools = new ToolExecutor({ projectPath: root });
|
|
56
|
+
const ls = await tools.execute('Bash', { command: 'ls -la src/components/' });
|
|
57
|
+
assert.equal(ls.success, true);
|
|
58
|
+
assert.match(ls.stdout, /a\.txt/);
|
|
59
|
+
|
|
60
|
+
const ps = await tools.execute('Bash', { command: 'Get-ChildItem -Path src/components/ -Name' });
|
|
61
|
+
assert.equal(ps.success, true);
|
|
62
|
+
assert.match(ps.stdout, /a\.txt/);
|
|
63
|
+
});
|
|
64
|
+
|
|
30
65
|
test('Read lists directories instead of failing on directory paths', async () => {
|
|
31
66
|
const root = await mkdtemp(path.join(tmpdir(), 'winter-tools-'));
|
|
32
67
|
await mkdir(path.join(root, 'sub'));
|