tinker-agent 1.3.0 → 1.4.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/CHANGELOG.md +17 -1
- package/README.md +217 -72
- package/bin/tinker.js +75 -25
- package/package.json +8 -3
- package/src/agent/runtime-session.ts +34 -15
- package/src/cli/command-line.ts +291 -0
- package/src/cli/config.ts +131 -264
- package/src/cli/index.ts +33 -21
- package/src/cli/main.ts +213 -0
- package/src/cli/model-profiles.ts +143 -72
- package/src/cli/output.ts +113 -0
- package/src/cli/package-metadata.ts +36 -0
- package/src/cli/prompt-source.ts +229 -0
- package/src/cli/public-cli-contract.ts +69 -0
- package/src/cli/public-config-contract.ts +650 -0
- package/src/cli/run-runner.ts +17 -12
- package/src/cli/runner-dependencies.ts +100 -0
- package/src/cli/tui-runner.tsx +52 -49
- package/src/mcp/mcp-manager.ts +2 -19
- package/src/mcp/mcp-tool-executor.ts +3 -4
- package/src/model/model-context-profile.ts +0 -30
- package/src/tools/bash.ts +8 -25
- package/src/tools/grep.ts +9 -1
- package/src/tools/registry.ts +15 -1
- package/src/tools/ripgrep.ts +24 -27
- package/src/tools/web-fetch/index.ts +2 -15
- package/src/tui/app.tsx +3 -0
- package/src/tui/components/prompt-input.tsx +6 -3
- package/src/tui/slash-commands.ts +76 -24
- package/src/tui/workspace-file-search.ts +78 -71
package/src/tools/ripgrep.ts
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
|
-
import { rgPath } from "@vscode/ripgrep";
|
|
3
2
|
import { cancellationError, throwIfTurnCancelled } from "../agent/turn-cancellation";
|
|
3
|
+
import { DEFAULT_PUBLIC_TOOLING_CONFIG } from "../cli/public-config-contract";
|
|
4
4
|
|
|
5
5
|
export const RIPGREP_MISSING_ERROR =
|
|
6
6
|
"Tinker's bundled ripgrep executable is unavailable. Reinstall tinker-agent.";
|
|
7
7
|
|
|
8
|
-
const defaultTimeoutMs = 20_000;
|
|
9
|
-
const defaultMaxBufferBytes = 20_000_000;
|
|
10
|
-
|
|
11
8
|
export type RipgrepResult = {
|
|
12
9
|
ok: boolean;
|
|
13
10
|
lines: string[];
|
|
@@ -18,12 +15,13 @@ export type RipgrepResult = {
|
|
|
18
15
|
|
|
19
16
|
export type RipgrepOptions = {
|
|
20
17
|
signal: AbortSignal;
|
|
18
|
+
command?: string;
|
|
21
19
|
timeoutMs?: number;
|
|
22
20
|
maxBufferBytes?: number;
|
|
23
21
|
};
|
|
24
22
|
|
|
25
|
-
export function findRipgrepCommand(): string {
|
|
26
|
-
return
|
|
23
|
+
export function findRipgrepCommand(command?: string): string {
|
|
24
|
+
return command ?? DEFAULT_PUBLIC_TOOLING_CONFIG.ripgrepPath;
|
|
27
25
|
}
|
|
28
26
|
|
|
29
27
|
export async function ripGrep(
|
|
@@ -31,21 +29,28 @@ export async function ripGrep(
|
|
|
31
29
|
options: RipgrepOptions,
|
|
32
30
|
): Promise<RipgrepResult> {
|
|
33
31
|
throwIfTurnCancelled(options.signal);
|
|
34
|
-
const timeoutMs =
|
|
35
|
-
options.timeoutMs ??
|
|
36
|
-
parsePositiveInteger(process.env.TINKER_GREP_TIMEOUT_MS, defaultTimeoutMs);
|
|
32
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_PUBLIC_TOOLING_CONFIG.grepTimeoutMs;
|
|
37
33
|
const maxBufferBytes =
|
|
38
|
-
options.maxBufferBytes ??
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
34
|
+
options.maxBufferBytes ?? DEFAULT_PUBLIC_TOOLING_CONFIG.grepMaxBufferBytes;
|
|
35
|
+
const command = findRipgrepCommand(options.command);
|
|
36
|
+
|
|
37
|
+
const first = await runRipgrep(
|
|
38
|
+
command,
|
|
39
|
+
args,
|
|
40
|
+
timeoutMs,
|
|
41
|
+
maxBufferBytes,
|
|
42
|
+
options.signal,
|
|
43
|
+
);
|
|
45
44
|
if (first.retryWithSingleThread) {
|
|
46
45
|
throwIfTurnCancelled(options.signal);
|
|
47
46
|
return finalizeResult(
|
|
48
|
-
await runRipgrep(
|
|
47
|
+
await runRipgrep(
|
|
48
|
+
command,
|
|
49
|
+
["-j", "1", ...args],
|
|
50
|
+
timeoutMs,
|
|
51
|
+
maxBufferBytes,
|
|
52
|
+
options.signal,
|
|
53
|
+
),
|
|
49
54
|
);
|
|
50
55
|
}
|
|
51
56
|
|
|
@@ -62,6 +67,7 @@ type RipgrepAttempt = {
|
|
|
62
67
|
};
|
|
63
68
|
|
|
64
69
|
function runRipgrep(
|
|
70
|
+
command: string,
|
|
65
71
|
args: string[],
|
|
66
72
|
timeoutMs: number,
|
|
67
73
|
maxBufferBytes: number,
|
|
@@ -74,7 +80,7 @@ function runRipgrep(
|
|
|
74
80
|
}
|
|
75
81
|
|
|
76
82
|
execFile(
|
|
77
|
-
|
|
83
|
+
command,
|
|
78
84
|
args,
|
|
79
85
|
{ timeout: timeoutMs, maxBuffer: maxBufferBytes, signal },
|
|
80
86
|
(error, stdout, stderr) => {
|
|
@@ -210,12 +216,3 @@ function isEagainError(
|
|
|
210
216
|
stderr.includes("Resource temporarily unavailable")
|
|
211
217
|
);
|
|
212
218
|
}
|
|
213
|
-
|
|
214
|
-
function parsePositiveInteger(value: string | undefined, fallback: number): number {
|
|
215
|
-
if (value === undefined || value.trim() === "") {
|
|
216
|
-
return fallback;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
const parsed = Number(value);
|
|
220
|
-
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
|
|
221
|
-
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { cancellationError, throwIfTurnCancelled } from "../../agent/turn-cancellation";
|
|
2
|
+
import { DEFAULT_PUBLIC_TOOLING_CONFIG } from "../../cli/public-config-contract";
|
|
2
3
|
import { defineToolExecutor } from "../types";
|
|
3
4
|
import type { ToolExecutionContext, ToolExecutor, WebFetchRawResult } from "../types";
|
|
4
5
|
import type { WebFetchBackend, WebFetchBackendResult, WebFetchRoute } from "./backend";
|
|
@@ -25,7 +26,6 @@ export type WebFetchToolOptions = {
|
|
|
25
26
|
browserBackend?: WebFetchBackend | false;
|
|
26
27
|
};
|
|
27
28
|
|
|
28
|
-
export const WEB_FETCH_DEFAULT_REFINE_THRESHOLD = 2000;
|
|
29
29
|
export const WEB_FETCH_DEFAULT_CACHE_TTL_MS = 15 * 60 * 1000;
|
|
30
30
|
|
|
31
31
|
type CacheEntry = {
|
|
@@ -50,11 +50,7 @@ export function createWebFetchToolExecutor(
|
|
|
50
50
|
: (options.browserBackend ??
|
|
51
51
|
(isBrowserBackendAvailable() ? createBrowserWebFetchBackend() : undefined));
|
|
52
52
|
const refineThreshold =
|
|
53
|
-
options.refineThreshold ??
|
|
54
|
-
parsePositiveInteger(
|
|
55
|
-
process.env.TINKER_WEBFETCH_REFINE_THRESHOLD,
|
|
56
|
-
WEB_FETCH_DEFAULT_REFINE_THRESHOLD,
|
|
57
|
-
);
|
|
53
|
+
options.refineThreshold ?? DEFAULT_PUBLIC_TOOLING_CONFIG.webFetchRefineThreshold;
|
|
58
54
|
const cacheTtlMs = options.cacheTtlMs ?? WEB_FETCH_DEFAULT_CACHE_TTL_MS;
|
|
59
55
|
const cache = new Map<string, CacheEntry>();
|
|
60
56
|
|
|
@@ -284,15 +280,6 @@ function pruneCache(cache: Map<string, CacheEntry>): void {
|
|
|
284
280
|
}
|
|
285
281
|
}
|
|
286
282
|
|
|
287
|
-
function parsePositiveInteger(value: string | undefined, fallback: number): number {
|
|
288
|
-
if (value === undefined || value.trim() === "") {
|
|
289
|
-
return fallback;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
const parsed = Number(value);
|
|
293
|
-
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
283
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
297
284
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
298
285
|
}
|
package/src/tui/app.tsx
CHANGED
|
@@ -42,12 +42,14 @@ import type { SessionSummary } from "../session/session-catalog";
|
|
|
42
42
|
import type { ModelProfile, ModelProfiles } from "../cli/model-profiles";
|
|
43
43
|
import { loadViewFile, type ViewFile } from "./view-file";
|
|
44
44
|
import { writeClipboardText } from "./clipboard";
|
|
45
|
+
import type { WorkspaceFileLister } from "./workspace-file-search";
|
|
45
46
|
|
|
46
47
|
export type AppProps = {
|
|
47
48
|
sessionController: TuiSessionController;
|
|
48
49
|
readGitBranch?: (workspaceRoot: string) => Promise<string | undefined>;
|
|
49
50
|
history?: PromptHistory;
|
|
50
51
|
projectSlashCommands?: readonly ProjectSlashCommand[];
|
|
52
|
+
fileLister?: WorkspaceFileLister;
|
|
51
53
|
profiles?: ModelProfiles;
|
|
52
54
|
persistDefaultProfile?: (profileName: string) => Promise<void>;
|
|
53
55
|
readViewFile?: (workspaceRoot: string, filePath: string) => Promise<ViewFile>;
|
|
@@ -647,6 +649,7 @@ export function App(props: AppProps) {
|
|
|
647
649
|
isDisabled={isRunning || isSessionOperation || isCopying}
|
|
648
650
|
history={props.history}
|
|
649
651
|
commands={availableCommands}
|
|
652
|
+
fileLister={props.fileLister}
|
|
650
653
|
importImage={binding.importImage}
|
|
651
654
|
verifyImageAssets={binding.verifyImageAssets}
|
|
652
655
|
onSubmit={onSubmit}
|
|
@@ -222,7 +222,8 @@ export function PromptInput(props: PromptInputProps) {
|
|
|
222
222
|
if (mention === undefined || locked) {
|
|
223
223
|
return;
|
|
224
224
|
}
|
|
225
|
-
|
|
225
|
+
const importImage = props.importImage;
|
|
226
|
+
if (importImage === undefined) {
|
|
226
227
|
insertFilePath(filePath);
|
|
227
228
|
return;
|
|
228
229
|
}
|
|
@@ -236,8 +237,10 @@ export function PromptInput(props: PromptInputProps) {
|
|
|
236
237
|
phase: { kind: "attaching", operationId },
|
|
237
238
|
error: undefined,
|
|
238
239
|
}));
|
|
239
|
-
void
|
|
240
|
-
.
|
|
240
|
+
void Promise.resolve()
|
|
241
|
+
.then(() =>
|
|
242
|
+
importImage(filePath, controller.signal, captured.attachments.length + 1),
|
|
243
|
+
)
|
|
241
244
|
.then((imported) => {
|
|
242
245
|
setState((current) => {
|
|
243
246
|
if (
|
|
@@ -1,24 +1,66 @@
|
|
|
1
1
|
export type SlashCommand = {
|
|
2
|
-
name: string;
|
|
3
|
-
description: string;
|
|
2
|
+
readonly name: string;
|
|
3
|
+
readonly description: string;
|
|
4
|
+
readonly usage?: string;
|
|
4
5
|
};
|
|
5
6
|
|
|
6
|
-
export
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
export type BuiltInSlashCommand = SlashCommand & {
|
|
8
|
+
readonly usage: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const SLASH_COMMANDS: readonly BuiltInSlashCommand[] = [
|
|
12
|
+
{
|
|
13
|
+
name: "status",
|
|
14
|
+
usage: "/status",
|
|
15
|
+
description: "Show session and context details",
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
name: "skills",
|
|
19
|
+
usage: "/skills",
|
|
20
|
+
description: "Show available and active Agent Skills",
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
name: "mcp",
|
|
24
|
+
usage: "/mcp",
|
|
25
|
+
description: "Show MCP servers and runtime tools",
|
|
26
|
+
},
|
|
10
27
|
{
|
|
11
28
|
name: "compact",
|
|
29
|
+
usage: "/compact [retire]",
|
|
12
30
|
description: "Swap tool output or retire a cold history prefix",
|
|
13
31
|
},
|
|
14
|
-
{
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
{ name: "
|
|
20
|
-
{
|
|
21
|
-
|
|
32
|
+
{
|
|
33
|
+
name: "clear",
|
|
34
|
+
usage: "/clear",
|
|
35
|
+
description: "Start a new session and clear conversation",
|
|
36
|
+
},
|
|
37
|
+
{ name: "fork", usage: "/fork", description: "Clone the current session" },
|
|
38
|
+
{
|
|
39
|
+
name: "view",
|
|
40
|
+
usage: "/view <path>",
|
|
41
|
+
description: "View a local UTF-8 text file",
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: "copy",
|
|
45
|
+
usage: "/copy",
|
|
46
|
+
description: "Copy the last response as Markdown",
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: "model",
|
|
50
|
+
usage: "/model [profile-name]",
|
|
51
|
+
description: "Switch model profile (new session)",
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
name: "resume",
|
|
55
|
+
usage: "/resume [session-id]",
|
|
56
|
+
description: "Choose or resume a session",
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: "session",
|
|
60
|
+
usage: "/session delete <session-id> --confirm",
|
|
61
|
+
description: "Manage stored sessions",
|
|
62
|
+
},
|
|
63
|
+
{ name: "quit", usage: "/quit", description: "Exit the TUI" },
|
|
22
64
|
];
|
|
23
65
|
|
|
24
66
|
export type ParsedSlashCommand =
|
|
@@ -48,12 +90,12 @@ export class SlashCommandError extends Error {
|
|
|
48
90
|
export function parseSlashCommand(input: string): ParsedSlashCommand {
|
|
49
91
|
const trimmed = input.trim();
|
|
50
92
|
if (trimmed === "/view") {
|
|
51
|
-
throw
|
|
93
|
+
throw slashCommandUsageError("view");
|
|
52
94
|
}
|
|
53
95
|
if (trimmed.startsWith("/view ") || trimmed.startsWith("/view\t")) {
|
|
54
96
|
const filePath = trimmed.slice(5).trim();
|
|
55
97
|
if (filePath === "") {
|
|
56
|
-
throw
|
|
98
|
+
throw slashCommandUsageError("view");
|
|
57
99
|
}
|
|
58
100
|
return { type: "view", filePath };
|
|
59
101
|
}
|
|
@@ -70,7 +112,7 @@ export function parseSlashCommand(input: string): ParsedSlashCommand {
|
|
|
70
112
|
if (tokens.length === 1) {
|
|
71
113
|
return { type: "mcp" };
|
|
72
114
|
}
|
|
73
|
-
throw
|
|
115
|
+
throw slashCommandUsageError("mcp");
|
|
74
116
|
}
|
|
75
117
|
if (command === "/compact") {
|
|
76
118
|
if (tokens.length === 1) {
|
|
@@ -79,25 +121,25 @@ export function parseSlashCommand(input: string): ParsedSlashCommand {
|
|
|
79
121
|
if (tokens.length === 2 && tokens[1] === "retire") {
|
|
80
122
|
return { type: "compact_retire" };
|
|
81
123
|
}
|
|
82
|
-
throw
|
|
124
|
+
throw slashCommandUsageError("compact");
|
|
83
125
|
}
|
|
84
126
|
if (command === "/clear") {
|
|
85
127
|
if (tokens.length === 1) {
|
|
86
128
|
return { type: "clear" };
|
|
87
129
|
}
|
|
88
|
-
throw
|
|
130
|
+
throw slashCommandUsageError("clear");
|
|
89
131
|
}
|
|
90
132
|
if (command === "/fork") {
|
|
91
133
|
if (tokens.length === 1) {
|
|
92
134
|
return { type: "fork" };
|
|
93
135
|
}
|
|
94
|
-
throw
|
|
136
|
+
throw slashCommandUsageError("fork");
|
|
95
137
|
}
|
|
96
138
|
if (command === "/copy") {
|
|
97
139
|
if (tokens.length === 1) {
|
|
98
140
|
return { type: "copy" };
|
|
99
141
|
}
|
|
100
|
-
throw
|
|
142
|
+
throw slashCommandUsageError("copy");
|
|
101
143
|
}
|
|
102
144
|
if (command === "/quit" && tokens.length === 1) {
|
|
103
145
|
return { type: "quit" };
|
|
@@ -109,7 +151,7 @@ export function parseSlashCommand(input: string): ParsedSlashCommand {
|
|
|
109
151
|
if (tokens.length === 2) {
|
|
110
152
|
return { type: "model_switch", profileName: tokens[1] };
|
|
111
153
|
}
|
|
112
|
-
throw
|
|
154
|
+
throw slashCommandUsageError("model");
|
|
113
155
|
}
|
|
114
156
|
if (command === "/resume") {
|
|
115
157
|
if (tokens.length === 1) {
|
|
@@ -118,7 +160,7 @@ export function parseSlashCommand(input: string): ParsedSlashCommand {
|
|
|
118
160
|
if (tokens.length === 2) {
|
|
119
161
|
return { type: "resume", sessionId: parsePublicSessionId(tokens[1]) };
|
|
120
162
|
}
|
|
121
|
-
throw
|
|
163
|
+
throw slashCommandUsageError("resume");
|
|
122
164
|
}
|
|
123
165
|
if (command === "/session") {
|
|
124
166
|
if (tokens.length === 4 && tokens[1] === "delete" && tokens[3] === "--confirm") {
|
|
@@ -127,7 +169,7 @@ export function parseSlashCommand(input: string): ParsedSlashCommand {
|
|
|
127
169
|
sessionId: parsePublicSessionId(tokens[2]),
|
|
128
170
|
};
|
|
129
171
|
}
|
|
130
|
-
throw
|
|
172
|
+
throw slashCommandUsageError("session");
|
|
131
173
|
}
|
|
132
174
|
throw new SlashCommandError(`Unknown command: ${trimmed}`);
|
|
133
175
|
}
|
|
@@ -167,4 +209,14 @@ function parsePublicSessionId(value: string): SessionId {
|
|
|
167
209
|
throw new SlashCommandError(`Invalid session ID: ${value}`);
|
|
168
210
|
}
|
|
169
211
|
}
|
|
212
|
+
|
|
213
|
+
function slashCommandUsageError(
|
|
214
|
+
name: (typeof SLASH_COMMANDS)[number]["name"],
|
|
215
|
+
): SlashCommandError {
|
|
216
|
+
const command = SLASH_COMMANDS.find((candidate) => candidate.name === name);
|
|
217
|
+
if (command === undefined) {
|
|
218
|
+
throw new Error(`Missing built-in slash command declaration for ${name}.`);
|
|
219
|
+
}
|
|
220
|
+
return new SlashCommandError(`Usage: ${command.usage}`);
|
|
221
|
+
}
|
|
170
222
|
import { parseSessionId, type SessionId } from "../ids/runtime-id";
|
|
@@ -1,90 +1,97 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { RIPGREP_MISSING_ERROR, findRipgrepCommand } from "../tools/ripgrep";
|
|
3
|
-
|
|
4
|
-
const FILE_SEARCH_TIMEOUT_MS = 20_000;
|
|
5
|
-
const FILE_SEARCH_MAX_BUFFER_BYTES = 20_000_000;
|
|
3
|
+
import { DEFAULT_PUBLIC_TOOLING_CONFIG } from "../cli/public-config-contract";
|
|
6
4
|
|
|
7
5
|
export type WorkspaceFileLister = (
|
|
8
6
|
workspaceRoot: string,
|
|
9
7
|
signal: AbortSignal,
|
|
10
8
|
) => Promise<readonly string[]>;
|
|
11
9
|
|
|
12
|
-
export
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
10
|
+
export type WorkspaceFileListerOptions = {
|
|
11
|
+
readonly command?: string;
|
|
12
|
+
readonly timeoutMs?: number;
|
|
13
|
+
readonly maxBufferBytes?: number;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function createWorkspaceFileLister(
|
|
17
|
+
options: WorkspaceFileListerOptions = {},
|
|
18
|
+
): WorkspaceFileLister {
|
|
19
|
+
const command = findRipgrepCommand(options.command);
|
|
20
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_PUBLIC_TOOLING_CONFIG.grepTimeoutMs;
|
|
21
|
+
const maxBufferBytes =
|
|
22
|
+
options.maxBufferBytes ?? DEFAULT_PUBLIC_TOOLING_CONFIG.grepMaxBufferBytes;
|
|
23
|
+
|
|
24
|
+
return (workspaceRoot, signal) =>
|
|
25
|
+
new Promise((resolve, reject) => {
|
|
26
|
+
if (signal.aborted) {
|
|
27
|
+
reject(new Error("Workspace file search was cancelled."));
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
18
30
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
31
|
+
execFile(
|
|
32
|
+
command,
|
|
33
|
+
[
|
|
34
|
+
"--files",
|
|
35
|
+
"--hidden",
|
|
36
|
+
"--glob",
|
|
37
|
+
"!**/node_modules/**",
|
|
38
|
+
"--glob",
|
|
39
|
+
"!**/.git/**",
|
|
40
|
+
"--glob",
|
|
41
|
+
"!**/.tinker/**",
|
|
42
|
+
],
|
|
43
|
+
{
|
|
44
|
+
cwd: workspaceRoot,
|
|
45
|
+
encoding: "utf8",
|
|
46
|
+
maxBuffer: maxBufferBytes,
|
|
47
|
+
signal,
|
|
48
|
+
timeout: timeoutMs,
|
|
49
|
+
},
|
|
50
|
+
(error, stdout, stderr) => {
|
|
51
|
+
if (signal.aborted) {
|
|
52
|
+
reject(error ?? new Error("Workspace file search was cancelled."));
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
43
55
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
56
|
+
if (error === null) {
|
|
57
|
+
resolve(splitPaths(stdout));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
48
60
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
61
|
+
const execError = error as Error & {
|
|
62
|
+
code?: number | string;
|
|
63
|
+
killed?: boolean;
|
|
64
|
+
signal?: string | null;
|
|
65
|
+
};
|
|
54
66
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
67
|
+
if (execError.code === 1 && stderr.trim() === "") {
|
|
68
|
+
resolve(splitPaths(stdout));
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
59
71
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
72
|
+
if (execError.code === "ENOENT") {
|
|
73
|
+
reject(new Error(RIPGREP_MISSING_ERROR));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
64
76
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
),
|
|
70
|
-
);
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
77
|
+
if (execError.killed === true || typeof execError.signal === "string") {
|
|
78
|
+
reject(new Error(`Workspace file search timed out after ${timeoutMs}ms.`));
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
73
81
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
);
|
|
80
|
-
|
|
81
|
-
}
|
|
82
|
+
if (execError.message.includes("maxBuffer")) {
|
|
83
|
+
reject(new Error(`Workspace file list exceeded ${maxBufferBytes} bytes.`));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const detail = stderr.trim() === "" ? execError.message : stderr.trim();
|
|
88
|
+
reject(new Error(`Workspace file search failed: ${detail}`));
|
|
89
|
+
},
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
}
|
|
82
93
|
|
|
83
|
-
|
|
84
|
-
reject(new Error(`Workspace file search failed: ${detail}`));
|
|
85
|
-
},
|
|
86
|
-
);
|
|
87
|
-
});
|
|
94
|
+
export const listWorkspaceFiles = createWorkspaceFileLister();
|
|
88
95
|
|
|
89
96
|
function splitPaths(stdout: string): string[] {
|
|
90
97
|
return stdout
|