winter-super-cli 2026.6.6 → 2026.6.8
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/winter.js +1 -0
- package/package.json +1 -1
- package/src/agent/runtime.js +10 -16
- package/src/ai/model-capabilities.js +15 -0
- package/src/ai/prompts/system-prompt.js +26 -4
- package/src/ai/providers.js +132 -55
- package/src/ai/small-model-amplifier.js +2 -2
- package/src/cli/commands.js +12 -0
- package/src/cli/context-loader.js +1 -1
- package/src/cli/diff-view.js +88 -32
- package/src/cli/input-controller.js +77 -44
- package/src/cli/markdown-format.js +12 -7
- package/src/cli/prompt-builder.js +20 -11
- package/src/cli/repl-commands.js +3 -0
- package/src/cli/repl.js +299 -358
- package/src/cli/slash-commands.js +1 -0
- package/src/cli/snowflake-logo.js +64 -86
- package/src/cli/terminal-manager.js +74 -0
- package/src/cli/terminal-ui.js +139 -86
- package/src/cli/tool-runtime.js +8 -3
- package/src/cli/tui.js +195 -0
- package/src/context/token-juice.js +37 -10
- package/src/tools/executor.js +78 -3
package/src/cli/tui.js
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { terminalWidth, visibleWidth, wrapText } from './terminal-ui.js';
|
|
2
|
+
|
|
3
|
+
const WINTER_LOGO = [
|
|
4
|
+
' __ __ _____ _ _ _______ ______ _____ ',
|
|
5
|
+
' \\ \\ / /|_ _| \\ | |__ __| ____| __ \\ ',
|
|
6
|
+
' \\ \\ /\\ / / | | | \\| | | | | |__ | |__) |',
|
|
7
|
+
' \\ V V / | | | . | | | | __| | _ / ',
|
|
8
|
+
' \\_/\\_/ _| |_| |\\ | | | | |____| | \\ \\ ',
|
|
9
|
+
' |_____|_| \\_| |_| |______|_| \\_\\',
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
function basenameFromPath(filePath, fallback = 'project') {
|
|
13
|
+
const parts = String(filePath || '').split(/[\\/]/).filter(Boolean);
|
|
14
|
+
return parts[parts.length - 1] || fallback;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function buildTuiSnapshot(repl = {}) {
|
|
18
|
+
const provider = repl.ai?.getActiveProvider?.() || 'provider';
|
|
19
|
+
const model = repl.ai?.providers?.[provider]?.model || 'model';
|
|
20
|
+
return {
|
|
21
|
+
provider,
|
|
22
|
+
model,
|
|
23
|
+
projectPath: repl.projectPath || process.cwd(),
|
|
24
|
+
sessionShort: String(repl.session?.getSessionId?.() || 'session').slice(0, 8),
|
|
25
|
+
projectName: basenameFromPath(repl.projectPath || process.cwd()),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function renderInputPanel(snapshot, {
|
|
30
|
+
colors,
|
|
31
|
+
width = terminalWidth(66, 124),
|
|
32
|
+
} = {}) {
|
|
33
|
+
const c = colors || {};
|
|
34
|
+
const panelWidth = Math.max(64, width - 2);
|
|
35
|
+
const innerWidth = Math.max(20, panelWidth - 4);
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
top: `${c.dim}┌${'─'.repeat(innerWidth + 2)}┐${c.reset}`,
|
|
39
|
+
status: '',
|
|
40
|
+
hint: '',
|
|
41
|
+
prompt: `${c.bright}${c.green}│${c.reset} `,
|
|
42
|
+
bottom: `${c.dim}└${'─'.repeat(innerWidth + 2)}┘${c.reset}`,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function renderLandingTui(snapshot, { colors } = {}) {
|
|
47
|
+
const c = colors || {};
|
|
48
|
+
const W = Math.max(60, Math.min(process.stdout.columns || 80, 140));
|
|
49
|
+
|
|
50
|
+
const bright = c.bright || '\x1b[1m';
|
|
51
|
+
const green = c.brightGreen || c.green || '\x1b[92m';
|
|
52
|
+
const white = c.white || '\x1b[37m';
|
|
53
|
+
const dim = c.dim || '\x1b[2m';
|
|
54
|
+
const reset = c.reset || '\x1b[0m';
|
|
55
|
+
const bgBlue = '\x1b[48;5;236m';
|
|
56
|
+
|
|
57
|
+
const logoLines = WINTER_LOGO.map(line => `${bright}${green}${line}${reset}`);
|
|
58
|
+
|
|
59
|
+
const leftStatus = ` ${snapshot.provider} · ${snapshot.model} `;
|
|
60
|
+
const rightStatus = ` ESC×2 exit · /help `;
|
|
61
|
+
const padding = Math.max(0, W - leftStatus.length - rightStatus.length);
|
|
62
|
+
const statusBar = `${bgBlue}${white}${leftStatus}${' '.repeat(padding)}${rightStatus}${reset}`;
|
|
63
|
+
|
|
64
|
+
const dock = renderInputPanel(snapshot, { colors });
|
|
65
|
+
|
|
66
|
+
return [
|
|
67
|
+
...logoLines,
|
|
68
|
+
'',
|
|
69
|
+
`${white}Winter will run commands on your behalf to help you build.${reset}`,
|
|
70
|
+
'',
|
|
71
|
+
`${white}Directory${reset} ${dim}${snapshot.projectPath}${reset}`,
|
|
72
|
+
'',
|
|
73
|
+
statusBar,
|
|
74
|
+
dock.top
|
|
75
|
+
].join('\n');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function renderStatusPanel(snapshot, { colors, title = 'Status' } = {}) {
|
|
79
|
+
const c = colors || {};
|
|
80
|
+
const bgBlue = '\x1b[48;5;236m';
|
|
81
|
+
const header = `${bgBlue}${c.white} ${title.toUpperCase()} ${c.reset}`;
|
|
82
|
+
|
|
83
|
+
return [
|
|
84
|
+
'',
|
|
85
|
+
header,
|
|
86
|
+
`${c.dim}Project :${c.reset} ${snapshot.projectName} (${snapshot.projectPath})`,
|
|
87
|
+
`${c.dim}Model :${c.reset} ${snapshot.provider}/${snapshot.model} (${snapshot.modelTier})`,
|
|
88
|
+
`${c.dim}Session :${c.reset} ${snapshot.sessionShort} | State: ${snapshot.statusText}`,
|
|
89
|
+
`${c.dim}Codebase:${c.reset} ${snapshot.codebaseFiles} files, ${snapshot.codebaseChunks} chunks`,
|
|
90
|
+
`${c.dim}Activity:${c.reset} ${snapshot.toolSummary || 'idle'}`,
|
|
91
|
+
''
|
|
92
|
+
].join('\n');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function renderStartupTui(snapshot, opts) { return renderLandingTui(snapshot, opts); }
|
|
96
|
+
export function renderConversationStartup(snapshot, opts) { return renderLandingTui(snapshot, opts); }
|
|
97
|
+
export function renderShellTui(snapshot, opts) { return renderLandingTui(snapshot, opts); }
|
|
98
|
+
|
|
99
|
+
export function renderCommandCenter({ colors, width = 80 } = {}) {
|
|
100
|
+
const c = colors || {};
|
|
101
|
+
const bgBlue = '\x1b[48;5;236m';
|
|
102
|
+
const header = `${bgBlue}${c.white} COMMAND CENTER ${c.reset}`;
|
|
103
|
+
|
|
104
|
+
return [
|
|
105
|
+
'',
|
|
106
|
+
header,
|
|
107
|
+
`${c.brightGreen}B${c.reset} ${c.bright}${c.cyan}Build ${c.reset} ${c.dim}/auto /debug /tdd /swe /composer${c.reset}`,
|
|
108
|
+
`${c.brightGreen}I${c.reset} ${c.bright}${c.cyan}Inspect${c.reset} ${c.dim}/read /grep /glob /search /context${c.reset}`,
|
|
109
|
+
`${c.brightGreen}M${c.reset} ${c.bright}${c.cyan}Model ${c.reset} ${c.dim}/provider /providers /model /models /scorecard${c.reset}`,
|
|
110
|
+
`${c.brightGreen}K${c.reset} ${c.bright}${c.cyan}Memory ${c.reset} ${c.dim}/remember /memories /memory-vault /compress${c.reset}`,
|
|
111
|
+
`${c.brightGreen}V${c.reset} ${c.bright}${c.cyan}Visual ${c.reset} ${c.dim}/image /paste ^V img /designs /page-agent${c.reset}`,
|
|
112
|
+
`${c.brightGreen}S${c.reset} ${c.bright}${c.cyan}System ${c.reset} ${c.dim}/doctor full /stats /permissions /mcp /help${c.reset}`,
|
|
113
|
+
''
|
|
114
|
+
].join('\n');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function renderSplitPanel({ title, left = [], right = [], colors, width = 80 } = {}) {
|
|
118
|
+
const c = colors || {};
|
|
119
|
+
const bgBlue = '\x1b[48;5;236m';
|
|
120
|
+
const header = `${bgBlue}${c.white} ${(title || 'Info').toUpperCase()} ${c.reset}`;
|
|
121
|
+
|
|
122
|
+
const innerWidth = Math.max(56, width - 4);
|
|
123
|
+
const leftWidth = Math.floor(innerWidth * 0.5);
|
|
124
|
+
|
|
125
|
+
const rows = [];
|
|
126
|
+
const count = Math.max(left.length, right.length);
|
|
127
|
+
for (let i = 0; i < count; i++) {
|
|
128
|
+
const lText = String(left[i] || '');
|
|
129
|
+
const rText = String(right[i] || '');
|
|
130
|
+
const lPlain = lText.replace(/\x1b\[[0-9;]*m/g, '');
|
|
131
|
+
const lPad = Math.max(0, leftWidth - lPlain.length);
|
|
132
|
+
rows.push(`${lText}${' '.repeat(lPad)} ${rText}`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return [
|
|
136
|
+
'',
|
|
137
|
+
header,
|
|
138
|
+
...rows,
|
|
139
|
+
''
|
|
140
|
+
].join('\n');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function renderHistoryPanel(entries = [], { colors, title = 'Recent Session' } = {}) {
|
|
144
|
+
const c = colors || {};
|
|
145
|
+
const bgBlue = '\x1b[48;5;236m';
|
|
146
|
+
const header = `${bgBlue}${c.white} ${title.toUpperCase()} ${c.reset}`;
|
|
147
|
+
|
|
148
|
+
const body = entries.length > 0
|
|
149
|
+
? entries.map(entry => {
|
|
150
|
+
const role = entry.role === 'assistant' ? 'Winter' : entry.role === 'user' ? 'You' : entry.role || 'event';
|
|
151
|
+
const roleColor = entry.role === 'assistant' ? c.cyan : c.green;
|
|
152
|
+
return `${c.bright}${roleColor}${role}${c.reset}: ${entry.content || entry.text || ''}`;
|
|
153
|
+
})
|
|
154
|
+
: [`${c.dim}No previous messages.${c.reset}`];
|
|
155
|
+
|
|
156
|
+
return [
|
|
157
|
+
'',
|
|
158
|
+
header,
|
|
159
|
+
...body,
|
|
160
|
+
''
|
|
161
|
+
].join('\n');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function renderAssistantPanel({ content = '', footer = '', colors, title = 'Assistant' } = {}) {
|
|
165
|
+
const c = colors || {};
|
|
166
|
+
const bgBlue = '\x1b[48;5;236m';
|
|
167
|
+
const header = `${bgBlue}${c.white} ${title.toUpperCase()} ${c.reset}`;
|
|
168
|
+
|
|
169
|
+
const parts = [];
|
|
170
|
+
if (title) parts.push('', header);
|
|
171
|
+
if (content) parts.push(content);
|
|
172
|
+
if (footer) parts.push(`${c.dim}${footer}${c.reset}`);
|
|
173
|
+
return parts.join('\n') + '\n';
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function renderToolPanel({ toolName = 'Tool', summary = '', success = true, colors } = {}) {
|
|
177
|
+
const c = colors || {};
|
|
178
|
+
const status = success ? `${c.brightGreen}✓${c.reset}` : `${c.red}✖${c.reset}`;
|
|
179
|
+
|
|
180
|
+
if (!summary.includes('\n')) {
|
|
181
|
+
return `${status} ${c.bright}${c.cyan}${toolName}${c.reset} ${c.dim}· ${summary}${c.reset}`;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const lines = summary.split('\n');
|
|
185
|
+
const firstLine = lines.shift();
|
|
186
|
+
|
|
187
|
+
const formattedRest = lines.map(line => {
|
|
188
|
+
if (line.startsWith('+')) return ` ${c.green}${line}${c.reset}`;
|
|
189
|
+
if (line.startsWith('-')) return ` ${c.red}${line}${c.reset}`;
|
|
190
|
+
if (line.startsWith('@@')) return ` ${c.cyan}${line}${c.reset}`;
|
|
191
|
+
return ` ${c.dim}${line}${c.reset}`;
|
|
192
|
+
}).join('\n');
|
|
193
|
+
|
|
194
|
+
return `${status} ${c.bright}${c.cyan}${toolName}${c.reset} ${c.dim}· ${firstLine}${c.reset}\n${formattedRest}`;
|
|
195
|
+
}
|
|
@@ -63,26 +63,52 @@ export function serializeToolResultToMarkdown(toolName, result = {}) {
|
|
|
63
63
|
export function extractKeyLines(text = '', limit = 18) {
|
|
64
64
|
const patterns = /(error|failed|exception|warning|todo|fixme|export |import |class |function |const |let |var |def |interface |type |=>)/i;
|
|
65
65
|
const selected = [];
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
const lines = String(text).split(/\r?\n/);
|
|
67
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
68
|
+
const line = lines[index];
|
|
69
|
+
if (patterns.test(line)) selected.push(`L${index + 1}: ${line.trim()}`);
|
|
68
70
|
if (selected.length >= limit) break;
|
|
69
71
|
}
|
|
70
72
|
return selected;
|
|
71
73
|
}
|
|
72
74
|
|
|
73
|
-
|
|
75
|
+
function buildExcerpts(text = '', maxChars = 700) {
|
|
76
|
+
const value = String(text || '').trim();
|
|
77
|
+
if (!value) return [];
|
|
78
|
+
if (value.length <= maxChars) return [value];
|
|
79
|
+
const head = value.slice(0, Math.floor(maxChars * 0.58)).trimEnd();
|
|
80
|
+
const tail = value.slice(-Math.floor(maxChars * 0.32)).trimStart();
|
|
81
|
+
return [
|
|
82
|
+
`${head}\n[TokenJuice middle omitted from inline preview; full text is in memory]\n${tail}`,
|
|
83
|
+
];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function buildChunkMap(saved) {
|
|
87
|
+
if (!saved?.files?.length) return [];
|
|
88
|
+
return saved.files.map(file => {
|
|
89
|
+
const label = `part ${file.part}/${saved.parts}`;
|
|
90
|
+
return `- ${label}: ${file.wikiLink} (~${file.tokensEst} tokens)`;
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function buildCompressedPreview(toolName, result, markdown, savedOrLinks = []) {
|
|
95
|
+
const links = Array.isArray(savedOrLinks) ? savedOrLinks : (savedOrLinks?.links || []);
|
|
74
96
|
const source = [result?.error, result?.content, result?.stdout, result?.stderr, result?.diff, result?.message]
|
|
75
97
|
.filter(Boolean)
|
|
76
98
|
.join('\n');
|
|
77
99
|
const keyLines = extractKeyLines(source);
|
|
78
|
-
const
|
|
100
|
+
const excerpts = buildExcerpts(source || markdown);
|
|
101
|
+
const chunkMap = Array.isArray(savedOrLinks) ? [] : buildChunkMap(savedOrLinks);
|
|
79
102
|
const lines = [
|
|
80
|
-
`TokenJuice
|
|
81
|
-
links.length ? `Full
|
|
82
|
-
|
|
103
|
+
`TokenJuice losslessly stored a large ${toolName || 'tool'} result before model context.`,
|
|
104
|
+
links.length ? `Full detail chunks: ${links.join(', ')}` : '',
|
|
105
|
+
chunkMap.length ? 'Chunk map for exact detail retrieval:' : '',
|
|
106
|
+
...chunkMap,
|
|
107
|
+
'Instruction: use Read on the memory file behind a chunk link when exact omitted lines are needed.',
|
|
108
|
+
keyLines.length ? 'Key lines with original line numbers:' : '',
|
|
83
109
|
...keyLines.map(line => `- ${line}`),
|
|
84
|
-
|
|
85
|
-
|
|
110
|
+
excerpts.length ? 'Representative excerpts:' : '',
|
|
111
|
+
...excerpts.map(excerpt => compactText(excerpt, 900)),
|
|
86
112
|
].filter(Boolean);
|
|
87
113
|
return lines.join('\n');
|
|
88
114
|
}
|
|
@@ -129,7 +155,7 @@ export class TokenJuice {
|
|
|
129
155
|
source: result.path || result.url || result.command || '',
|
|
130
156
|
},
|
|
131
157
|
});
|
|
132
|
-
const preview = buildCompressedPreview(toolName, result, markdown, saved
|
|
158
|
+
const preview = buildCompressedPreview(toolName, result, markdown, saved);
|
|
133
159
|
const compressed = {
|
|
134
160
|
...basePromptResult,
|
|
135
161
|
content: preview,
|
|
@@ -143,6 +169,7 @@ export class TokenJuice {
|
|
|
143
169
|
memoryRoot: saved.rootDir,
|
|
144
170
|
memoryLinks: saved.links,
|
|
145
171
|
memoryFiles: saved.files.map(file => file.relativePath),
|
|
172
|
+
detailRetrieval: 'Read the listed memoryFiles for exact omitted content.',
|
|
146
173
|
parts: saved.parts,
|
|
147
174
|
},
|
|
148
175
|
};
|
package/src/tools/executor.js
CHANGED
|
@@ -471,7 +471,7 @@ export class ToolExecutor {
|
|
|
471
471
|
const cwd = context.cwd || this.projectPath;
|
|
472
472
|
const resolvedPath = (p) => this.resolveInputPath(p, cwd);
|
|
473
473
|
|
|
474
|
-
const preflight = this.preflightValidateToolArgs(toolName, input, { cwd });
|
|
474
|
+
const preflight = await this.preflightValidateToolArgs(toolName, input, { cwd });
|
|
475
475
|
if (preflight?.success === false) {
|
|
476
476
|
return preflight;
|
|
477
477
|
}
|
|
@@ -568,7 +568,7 @@ export class ToolExecutor {
|
|
|
568
568
|
}
|
|
569
569
|
}
|
|
570
570
|
|
|
571
|
-
preflightValidateToolArgs(toolName, input, { cwd } = {}) {
|
|
571
|
+
async preflightValidateToolArgs(toolName, input, { cwd } = {}) {
|
|
572
572
|
const args = (input && typeof input === 'object' && !Array.isArray(input)) ? input : {};
|
|
573
573
|
const pick = (...keys) => {
|
|
574
574
|
for (const key of keys) {
|
|
@@ -619,6 +619,16 @@ export class ToolExecutor {
|
|
|
619
619
|
if (!cmd) {
|
|
620
620
|
return { success: false, error: 'command is required', recovery: 'Example: Bash {"command":"npm test"}' };
|
|
621
621
|
}
|
|
622
|
+
const missingScript = await this.findMissingNpmScript(cmd, cwd);
|
|
623
|
+
if (missingScript) {
|
|
624
|
+
return {
|
|
625
|
+
success: false,
|
|
626
|
+
error: `npm script "${missingScript.script}" does not exist in package.json`,
|
|
627
|
+
recovery: missingScript.available.length
|
|
628
|
+
? `Use an existing script: ${missingScript.available.map(name => `npm run ${name}`).join(', ')}`
|
|
629
|
+
: 'No package scripts are defined. Inspect package.json before running npm scripts.',
|
|
630
|
+
};
|
|
631
|
+
}
|
|
622
632
|
const next = { command: cmd };
|
|
623
633
|
if (typeof args.timeout !== 'undefined') next.timeout = args.timeout;
|
|
624
634
|
if (typeof args.shell !== 'undefined') next.shell = args.shell;
|
|
@@ -706,6 +716,21 @@ export class ToolExecutor {
|
|
|
706
716
|
return { success: true };
|
|
707
717
|
}
|
|
708
718
|
|
|
719
|
+
async findMissingNpmScript(command, cwd) {
|
|
720
|
+
const match = String(command || '').trim().match(/^(?:npm|pnpm|yarn|bun)\s+run\s+([A-Za-z0-9:_-]+)(?:\s|$)/i);
|
|
721
|
+
if (!match) return null;
|
|
722
|
+
const script = match[1];
|
|
723
|
+
try {
|
|
724
|
+
const packageJsonPath = path.join(cwd || this.projectPath, 'package.json');
|
|
725
|
+
const pkg = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
|
|
726
|
+
const scripts = pkg.scripts && typeof pkg.scripts === 'object' ? pkg.scripts : {};
|
|
727
|
+
if (Object.prototype.hasOwnProperty.call(scripts, script)) return null;
|
|
728
|
+
return { script, available: Object.keys(scripts).sort() };
|
|
729
|
+
} catch {
|
|
730
|
+
return null;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
709
734
|
redactToolInput(input) {
|
|
710
735
|
if (!input || typeof input !== 'object') return input;
|
|
711
736
|
const secretPattern = /(api[-_]?key|auth[-_]?token|access[-_]?token|refresh[-_]?token|secret|password)/i;
|
|
@@ -1012,10 +1037,60 @@ export class ToolExecutor {
|
|
|
1012
1037
|
size: content.length
|
|
1013
1038
|
};
|
|
1014
1039
|
} catch (error) {
|
|
1015
|
-
|
|
1040
|
+
if (error?.code === 'ENOENT') {
|
|
1041
|
+
const suggestions = await this.findNearbyPathSuggestions(filePath);
|
|
1042
|
+
return {
|
|
1043
|
+
success: false,
|
|
1044
|
+
error: `File not found: ${filePath}`,
|
|
1045
|
+
code: 'ENOENT',
|
|
1046
|
+
path: filePath,
|
|
1047
|
+
suggestions,
|
|
1048
|
+
recovery: [
|
|
1049
|
+
'Do not retry the same missing path.',
|
|
1050
|
+
'Use Glob or Grep to discover the real file path before reading again.',
|
|
1051
|
+
suggestions.length ? `Nearby candidates: ${suggestions.join(', ')}` : `Search from project root: ${this.projectPath}`,
|
|
1052
|
+
].join(' '),
|
|
1053
|
+
};
|
|
1054
|
+
}
|
|
1055
|
+
return { success: false, error: error.message, code: error?.code, path: filePath };
|
|
1016
1056
|
}
|
|
1017
1057
|
}
|
|
1018
1058
|
|
|
1059
|
+
async findNearbyPathSuggestions(filePath, limit = 8) {
|
|
1060
|
+
if (!filePath) return [];
|
|
1061
|
+
const targetName = path.basename(filePath).toLowerCase();
|
|
1062
|
+
const targetStem = targetName.replace(/\.[^.]+$/, '');
|
|
1063
|
+
if (!targetName) return [];
|
|
1064
|
+
|
|
1065
|
+
const candidates = [];
|
|
1066
|
+
const ignored = new Set(['node_modules', '.git', 'dist', 'build', '.winter', '.claude', 'VSCode-win32-x64', 'vscode-main']);
|
|
1067
|
+
const walk = async (dir, depth = 0) => {
|
|
1068
|
+
if (depth > 5 || candidates.length >= limit * 4) return;
|
|
1069
|
+
let entries = [];
|
|
1070
|
+
try {
|
|
1071
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
1072
|
+
} catch {
|
|
1073
|
+
return;
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
for (const entry of entries) {
|
|
1077
|
+
if (ignored.has(entry.name)) continue;
|
|
1078
|
+
const fullPath = path.join(dir, entry.name);
|
|
1079
|
+
if (entry.isDirectory()) {
|
|
1080
|
+
await walk(fullPath, depth + 1);
|
|
1081
|
+
continue;
|
|
1082
|
+
}
|
|
1083
|
+
const name = entry.name.toLowerCase();
|
|
1084
|
+
if (name === targetName || name.includes(targetStem) || targetStem.includes(name.replace(/\.[^.]+$/, ''))) {
|
|
1085
|
+
candidates.push(path.relative(this.projectPath, fullPath) || fullPath);
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
};
|
|
1089
|
+
|
|
1090
|
+
await walk(this.projectPath);
|
|
1091
|
+
return [...new Set(candidates)].slice(0, limit);
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1019
1094
|
async backupFile(filePath) {
|
|
1020
1095
|
try {
|
|
1021
1096
|
const fsMod = await import('fs/promises');
|