winter-super-cli 2026.5.16 → 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 +119 -13
- package/src/tools/executor.test.js +40 -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) {
|
|
@@ -244,26 +245,53 @@ export class ToolExecutor {
|
|
|
244
245
|
const aliases = {
|
|
245
246
|
read: 'Read',
|
|
246
247
|
readfile: 'Read',
|
|
248
|
+
openfile: 'Read',
|
|
249
|
+
viewfile: 'Read',
|
|
250
|
+
cat: 'Read',
|
|
247
251
|
write: 'Write',
|
|
248
252
|
writefile: 'Write',
|
|
253
|
+
writetofile: 'Write',
|
|
254
|
+
createfile: 'Write',
|
|
255
|
+
savefile: 'Write',
|
|
249
256
|
edit: 'Edit',
|
|
250
257
|
editfile: 'Edit',
|
|
258
|
+
replaceinfile: 'Edit',
|
|
259
|
+
strreplace: 'Edit',
|
|
260
|
+
strreplaceeditor: 'Edit',
|
|
261
|
+
applydiff: 'Edit',
|
|
262
|
+
patch: 'Edit',
|
|
251
263
|
bash: 'Bash',
|
|
252
264
|
shell: 'Bash',
|
|
253
265
|
command: 'Bash',
|
|
266
|
+
commandexecutor: 'Bash',
|
|
267
|
+
executecommand: 'Bash',
|
|
268
|
+
runcommand: 'Bash',
|
|
269
|
+
terminal: 'Bash',
|
|
270
|
+
powershell: 'Bash',
|
|
254
271
|
glob: 'Glob',
|
|
272
|
+
listfiles: 'Glob',
|
|
273
|
+
ls: 'Glob',
|
|
274
|
+
findfiles: 'Glob',
|
|
255
275
|
grep: 'Grep',
|
|
256
276
|
search: 'Grep',
|
|
277
|
+
searchfiles: 'Grep',
|
|
278
|
+
searchtext: 'Grep',
|
|
279
|
+
rg: 'Grep',
|
|
257
280
|
lsp: 'LSP',
|
|
281
|
+
listcodedefinitionnames: 'LSP',
|
|
258
282
|
taskcreate: 'TaskCreate',
|
|
259
283
|
createtask: 'TaskCreate',
|
|
284
|
+
newtask: 'TaskCreate',
|
|
260
285
|
taskupdate: 'TaskUpdate',
|
|
261
286
|
updatetask: 'TaskUpdate',
|
|
262
287
|
tasklist: 'TaskList',
|
|
263
288
|
listtasks: 'TaskList',
|
|
289
|
+
plan: 'TaskList',
|
|
264
290
|
webfetch: 'WebFetch',
|
|
265
291
|
fetch: 'WebFetch',
|
|
292
|
+
fetchurl: 'WebFetch',
|
|
266
293
|
websearch: 'WebSearch',
|
|
294
|
+
searchweb: 'WebSearch',
|
|
267
295
|
browserdebug: 'BrowserDebug',
|
|
268
296
|
browser: 'BrowserDebug',
|
|
269
297
|
};
|
|
@@ -419,6 +447,9 @@ export class ToolExecutor {
|
|
|
419
447
|
timeout = parseInt(timeout, 10);
|
|
420
448
|
if (isNaN(timeout) || timeout < 0) timeout = 60000;
|
|
421
449
|
|
|
450
|
+
const heredocResult = await this.tryHandleHeredocWrite(command, cwd);
|
|
451
|
+
if (heredocResult) return heredocResult;
|
|
452
|
+
|
|
422
453
|
command = await this.translateWindowsCommand(command);
|
|
423
454
|
|
|
424
455
|
// Security check
|
|
@@ -430,7 +461,14 @@ export class ToolExecutor {
|
|
|
430
461
|
}
|
|
431
462
|
|
|
432
463
|
try {
|
|
433
|
-
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 });
|
|
434
472
|
return {
|
|
435
473
|
success: true,
|
|
436
474
|
stdout: stdout || '',
|
|
@@ -531,16 +569,17 @@ export class ToolExecutor {
|
|
|
531
569
|
if (process.platform !== 'win32') return command;
|
|
532
570
|
|
|
533
571
|
const trimmed = command.trim();
|
|
534
|
-
const
|
|
535
|
-
if (lsRecursive) {
|
|
536
|
-
const target = lsRecursive[1].trim();
|
|
537
|
-
return `powershell -NoProfile -Command "Get-ChildItem -LiteralPath ${this.quotePowerShellString(target)} -Recurse -Force | Select-Object -ExpandProperty FullName"`;
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
const ls = trimmed.match(/^ls\s+(.+)$/i);
|
|
572
|
+
const ls = trimmed.match(/^ls(?:\s+(.+))?$/i);
|
|
541
573
|
if (ls) {
|
|
542
|
-
const
|
|
543
|
-
|
|
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}`;
|
|
544
583
|
}
|
|
545
584
|
|
|
546
585
|
const cat = trimmed.match(/^cat\s+(.+)$/i);
|
|
@@ -550,15 +589,82 @@ export class ToolExecutor {
|
|
|
550
589
|
try {
|
|
551
590
|
const stat = await fs.stat(normalizedTarget);
|
|
552
591
|
if (stat.isDirectory()) {
|
|
553
|
-
return `
|
|
592
|
+
return `Get-ChildItem -LiteralPath ${this.quotePowerShellString(target)} -Force | Select-Object -ExpandProperty Name`;
|
|
554
593
|
}
|
|
555
594
|
} catch {}
|
|
556
|
-
return `
|
|
595
|
+
return `Get-Content -LiteralPath ${this.quotePowerShellString(target)}`;
|
|
557
596
|
}
|
|
558
597
|
|
|
559
598
|
return command;
|
|
560
599
|
}
|
|
561
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
|
+
|
|
562
668
|
quotePowerShellString(value) {
|
|
563
669
|
const unquoted = value.replace(/^['"]|['"]$/g, '');
|
|
564
670
|
return `'${unquoted.replace(/'/g, "''")}'`;
|
|
@@ -18,10 +18,50 @@ test('tool names accept common model aliases', () => {
|
|
|
18
18
|
const tools = new ToolExecutor({ projectPath: process.cwd() });
|
|
19
19
|
|
|
20
20
|
assert.equal(tools.normalizeToolName('read_file'), 'Read');
|
|
21
|
+
assert.equal(tools.normalizeToolName('write_to_file'), 'Write');
|
|
22
|
+
assert.equal(tools.normalizeToolName('replace_in_file'), 'Edit');
|
|
23
|
+
assert.equal(tools.normalizeToolName('command_executor'), 'Bash');
|
|
24
|
+
assert.equal(tools.normalizeToolName('execute_command'), 'Bash');
|
|
25
|
+
assert.equal(tools.normalizeToolName('list_files'), 'Glob');
|
|
26
|
+
assert.equal(tools.normalizeToolName('search_files'), 'Grep');
|
|
21
27
|
assert.equal(tools.normalizeToolName('shell'), 'Bash');
|
|
22
28
|
assert.equal(tools.normalizeToolName('web-search'), 'WebSearch');
|
|
23
29
|
});
|
|
24
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
|
+
|
|
25
65
|
test('Read lists directories instead of failing on directory paths', async () => {
|
|
26
66
|
const root = await mkdtemp(path.join(tmpdir(), 'winter-tools-'));
|
|
27
67
|
await mkdir(path.join(root, 'sub'));
|