@giovannijecha/jecode 0.8.3 → 0.8.5
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 +11 -8
- package/assets/wordmark-steel.svg +3 -0
- package/dist/accounts.js +47 -10
- package/dist/atomic.js +24 -14
- package/dist/batch.js +49 -4
- package/dist/bounded-file.js +212 -0
- package/dist/commands.js +2 -0
- package/dist/config.js +8 -4
- package/dist/context/automatic.js +35 -0
- package/dist/context/compactor.js +32 -2
- package/dist/context/manual.js +20 -3
- package/dist/context/request-projection.js +130 -0
- package/dist/controller-request.js +33 -11
- package/dist/credential-commands.js +22 -6
- package/dist/credentials.js +56 -18
- package/dist/directory-anchor.js +91 -0
- package/dist/file-identity.js +12 -0
- package/dist/model-command.js +5 -4
- package/dist/openai-account-command.js +13 -2
- package/dist/process-lease.js +329 -0
- package/dist/provider-commands.js +53 -10
- package/dist/provider-errors.js +94 -3
- package/dist/provider-label.js +13 -0
- package/dist/providers/anthropic-stream.js +4 -1
- package/dist/providers/anthropic-wire.js +8 -3
- package/dist/providers/anthropic.js +39 -19
- package/dist/providers/catalog.js +4 -4
- package/dist/providers/failure.js +181 -0
- package/dist/providers/http.js +86 -57
- package/dist/providers/ollama-stream.js +5 -1
- package/dist/providers/ollama.js +31 -19
- package/dist/providers/openai-codex.js +67 -41
- package/dist/providers/openai-stream.js +69 -9
- package/dist/providers/openai.js +51 -24
- package/dist/providers/sse.js +89 -17
- package/dist/request-identity.js +32 -0
- package/dist/sessions/catalog.js +199 -0
- package/dist/sessions/lease.js +132 -49
- package/dist/sessions/runtime.js +15 -8
- package/dist/sessions/store.js +451 -183
- package/dist/settings.js +62 -10
- package/dist/stable-directory.js +148 -0
- package/dist/store-lock.js +68 -84
- package/dist/tools/args.js +2 -2
- package/dist/tools/fs.js +124 -102
- package/dist/tools/search.js +81 -107
- package/dist/tools/text-boundary.js +7 -33
- package/dist/tui/app-workflows.js +32 -4
- package/dist/tui/components/footer.js +1 -1
- package/dist/tui/feedback.js +4 -0
- package/dist/tui/session-view.js +7 -2
- package/dist/tui/workspace.js +21 -7
- package/dist/user-store.js +23 -31
- package/package.json +4 -4
- package/dist/tools/ripgrep.js +0 -230
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { handleCommand } from "../commands.js";
|
|
3
3
|
import { runTurn } from "../controller.js";
|
|
4
4
|
import { resolveContextPolicy } from "../context/capacity.js";
|
|
5
|
+
import { automaticCompactionGate, automaticCompactionKey, } from "../context/automatic.js";
|
|
5
6
|
import { compactContext } from "../context/compactor.js";
|
|
6
7
|
import { compactSession } from "../context/manual.js";
|
|
7
8
|
import { isContextOverflow } from "../context/policy.js";
|
|
@@ -10,6 +11,8 @@ import { steeringInbox } from "../steering.js";
|
|
|
10
11
|
import { saveTranscript } from "../transcript-export.js";
|
|
11
12
|
import { recordAuxiliaryUsage, recordRequestInput, recordUsage } from "../usage.js";
|
|
12
13
|
import { selectTimeline } from "../timeline.js";
|
|
14
|
+
import { toolSpecs } from "../tools/index.js";
|
|
15
|
+
import { resetRequestIdentity, requestIdentityForSession } from "../request-identity.js";
|
|
13
16
|
import { transition } from "./activity.js";
|
|
14
17
|
import { answerAt } from "./approve.js";
|
|
15
18
|
import * as edit from "./editor.js";
|
|
@@ -20,6 +23,7 @@ const WAITING = "Waiting";
|
|
|
20
23
|
export function appWorkflows(options) {
|
|
21
24
|
const { session, state, permissions, feedback } = options;
|
|
22
25
|
let activeSteering;
|
|
26
|
+
const automaticCompaction = automaticCompactionGate();
|
|
23
27
|
const choose = (picker) => new Promise((resolve) => {
|
|
24
28
|
state.open = { picker, settle: resolve };
|
|
25
29
|
options.render();
|
|
@@ -52,6 +56,8 @@ export function appWorkflows(options) {
|
|
|
52
56
|
},
|
|
53
57
|
reset: async () => {
|
|
54
58
|
await session.persistence?.reset();
|
|
59
|
+
resetRequestIdentity(session);
|
|
60
|
+
automaticCompaction.reset();
|
|
55
61
|
state.blocks.splice(0);
|
|
56
62
|
state.past.length = 0;
|
|
57
63
|
permissions.reset();
|
|
@@ -80,13 +86,16 @@ export function appWorkflows(options) {
|
|
|
80
86
|
if (session.conversation.activeNodeId !== state.committedNodeId) {
|
|
81
87
|
return "branch-pending";
|
|
82
88
|
}
|
|
83
|
-
|
|
89
|
+
const result = await compactSession(session, {
|
|
84
90
|
signal: activity.control.signal,
|
|
85
91
|
onStatus: (said) => {
|
|
86
92
|
status(said ?? activity.label);
|
|
87
93
|
options.render();
|
|
88
94
|
},
|
|
89
95
|
});
|
|
96
|
+
if (result === "compacted")
|
|
97
|
+
automaticCompaction.reset();
|
|
98
|
+
return result;
|
|
90
99
|
},
|
|
91
100
|
});
|
|
92
101
|
if (outcome === "exit")
|
|
@@ -131,6 +140,8 @@ export function appWorkflows(options) {
|
|
|
131
140
|
let nodeId;
|
|
132
141
|
let context;
|
|
133
142
|
const unpersistedSteering = [];
|
|
143
|
+
const turnTools = permissions.availableTools();
|
|
144
|
+
const specs = toolSpecs(turnTools);
|
|
134
145
|
let firstPolicy = true;
|
|
135
146
|
const policy = () => {
|
|
136
147
|
let visible = firstPolicy;
|
|
@@ -202,7 +213,12 @@ export function appWorkflows(options) {
|
|
|
202
213
|
(request.error === undefined || !isContextOverflow(request.error))) {
|
|
203
214
|
return undefined;
|
|
204
215
|
}
|
|
205
|
-
const force = request.reason === "overflow";
|
|
216
|
+
const force = request.reason === "overflow" || request.projectionSaturated;
|
|
217
|
+
const key = automaticCompactionKey(session.provider.id, session.model, nodeId ?? prospectiveNodeId, checkpoint.length);
|
|
218
|
+
const attempt = { key, reason: request.reason };
|
|
219
|
+
if (!automaticCompaction.allows(attempt))
|
|
220
|
+
return undefined;
|
|
221
|
+
let attempted = false;
|
|
206
222
|
const result = await compactContext({
|
|
207
223
|
provider: session.provider,
|
|
208
224
|
model: session.model,
|
|
@@ -216,7 +232,14 @@ export function appWorkflows(options) {
|
|
|
216
232
|
signal: activity.control.signal,
|
|
217
233
|
force,
|
|
218
234
|
policy: request.policy,
|
|
235
|
+
requestEnvelope: {
|
|
236
|
+
system: session.system,
|
|
237
|
+
tools: specs,
|
|
238
|
+
maxOutputTokens: session.config.maxTokens,
|
|
239
|
+
},
|
|
240
|
+
requestIdentity: requestIdentityForSession(session),
|
|
219
241
|
onBegin: () => {
|
|
242
|
+
attempted = true;
|
|
220
243
|
status("Compacting");
|
|
221
244
|
options.render();
|
|
222
245
|
},
|
|
@@ -225,8 +248,12 @@ export function appWorkflows(options) {
|
|
|
225
248
|
options.render();
|
|
226
249
|
},
|
|
227
250
|
});
|
|
228
|
-
if (result === undefined)
|
|
251
|
+
if (result === undefined) {
|
|
252
|
+
if (attempted && !activity.control.signal.aborted)
|
|
253
|
+
automaticCompaction.failed(attempt);
|
|
229
254
|
return undefined;
|
|
255
|
+
}
|
|
256
|
+
automaticCompaction.succeeded(attempt);
|
|
230
257
|
context = result.anchor;
|
|
231
258
|
if (result.usage !== undefined)
|
|
232
259
|
recordAuxiliaryUsage(session.usage, result.usage);
|
|
@@ -244,6 +271,7 @@ export function appWorkflows(options) {
|
|
|
244
271
|
reason: "budget",
|
|
245
272
|
policy: await policy(),
|
|
246
273
|
inputTokens: session.usage.lastInputTokens,
|
|
274
|
+
projectionSaturated: false,
|
|
247
275
|
});
|
|
248
276
|
if (compacted !== undefined)
|
|
249
277
|
await persist(checkpoint, settlement);
|
|
@@ -252,7 +280,7 @@ export function appWorkflows(options) {
|
|
|
252
280
|
let finishReason;
|
|
253
281
|
let failed;
|
|
254
282
|
try {
|
|
255
|
-
await runTurn(history, controllerOptions(session, policy,
|
|
283
|
+
await runTurn(history, controllerOptions(session, policy, turnTools, inbox), events, activity.control.signal, modelHistory);
|
|
256
284
|
}
|
|
257
285
|
catch (error) {
|
|
258
286
|
const interrupted = activity.control.signal.aborted;
|
|
@@ -10,7 +10,7 @@ export function renderFooter(info, status, width, pal) {
|
|
|
10
10
|
function identity(info, cols) {
|
|
11
11
|
if (cols <= 0)
|
|
12
12
|
return "";
|
|
13
|
-
const core = `${info.model || "no model"} · ${info.effort}`;
|
|
13
|
+
const core = `${info.provider || "Provider"} · ${info.model || "no model"} · ${info.effort}`;
|
|
14
14
|
if (textWidth(core) >= cols || info.workspace === "")
|
|
15
15
|
return elide(core, cols);
|
|
16
16
|
const divider = " · ";
|
package/dist/tui/feedback.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// Operational feedback belongs in the footer, not in the conversation.
|
|
2
|
+
import { CONVERSATION_LIMITS } from "../conversation.js";
|
|
2
3
|
import { providerLabel } from "../provider-label.js";
|
|
3
4
|
const INFO_MS = 2_200;
|
|
4
5
|
const WARN_MS = 4_200;
|
|
@@ -53,6 +54,9 @@ export function turnBlocker(session) {
|
|
|
53
54
|
if (session.persistence?.failure !== undefined) {
|
|
54
55
|
return { text: "session could not be saved · /new to retry", tone: "error" };
|
|
55
56
|
}
|
|
57
|
+
if (session.conversation.nodes.length >= CONVERSATION_LIMITS.nodes) {
|
|
58
|
+
return { text: "conversation reached its session limit · /new to continue", tone: "warn" };
|
|
59
|
+
}
|
|
56
60
|
const blocked = session.provider.blocked();
|
|
57
61
|
const auth = session.provider.auth;
|
|
58
62
|
const expected = auth.kind === "api-key"
|
package/dist/tui/session-view.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
// Small projections of the live session used by the shell and footer.
|
|
2
2
|
import { credentialSource } from "../credentials.js";
|
|
3
3
|
import { providerFailure } from "../provider-errors.js";
|
|
4
|
+
import { providerRouteLabel } from "../provider-label.js";
|
|
5
|
+
import { providerFailureDetails } from "../providers/failure.js";
|
|
6
|
+
import { requestIdentityForSession } from "../request-identity.js";
|
|
4
7
|
export function controllerOptions(session, contextPolicy, tools = session.tools, steering) {
|
|
5
8
|
return {
|
|
6
9
|
provider: session.provider,
|
|
@@ -10,6 +13,7 @@ export function controllerOptions(session, contextPolicy, tools = session.tools,
|
|
|
10
13
|
maxTokens: session.config.maxTokens,
|
|
11
14
|
contextPolicy,
|
|
12
15
|
effort: session.config.effort,
|
|
16
|
+
requestIdentity: requestIdentityForSession(session),
|
|
13
17
|
maxModelRequests: session.config.maxModelRequests,
|
|
14
18
|
toolContext: { root: session.config.root },
|
|
15
19
|
...(steering === undefined ? {} : { steering }),
|
|
@@ -18,6 +22,7 @@ export function controllerOptions(session, contextPolicy, tools = session.tools,
|
|
|
18
22
|
export function footerInfo(session, workspace = session.config.root) {
|
|
19
23
|
return {
|
|
20
24
|
workspace,
|
|
25
|
+
provider: providerRouteLabel(session.provider),
|
|
21
26
|
model: session.model || "no model",
|
|
22
27
|
effort: session.config.effort,
|
|
23
28
|
};
|
|
@@ -26,8 +31,8 @@ export function footerInfo(session, workspace = session.config.root) {
|
|
|
26
31
|
export function turnFailure(session, error, aborted) {
|
|
27
32
|
if (aborted)
|
|
28
33
|
return { kind: "notice", text: "[interrupted]", tone: "warn" };
|
|
29
|
-
let text = providerFailure(session.provider, error);
|
|
30
|
-
if (
|
|
34
|
+
let text = providerFailure(session.provider, error, true);
|
|
35
|
+
if (providerFailureDetails(session.provider.id, error).kind === "authentication") {
|
|
31
36
|
const auth = session.provider.auth;
|
|
32
37
|
if (auth.kind === "oauth") {
|
|
33
38
|
text += ` · reconnect ${auth.label} in /providers`;
|
package/dist/tui/workspace.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
// Compact workspace identity for the persistent footer.
|
|
2
|
-
import {
|
|
2
|
+
import { lstat } from "node:fs/promises";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import * as path from "node:path";
|
|
5
|
+
import { readBoundedText, stableFileExpectation } from "../bounded-file.js";
|
|
6
|
+
import { assertDirectoryAnchor, captureDirectDirectory, } from "../directory-anchor.js";
|
|
7
|
+
const MAX_GIT_POINTER_BYTES = 4_096;
|
|
5
8
|
export async function workspaceLabel(root) {
|
|
6
9
|
const shown = displayRoot(root);
|
|
7
10
|
const branch = await findBranch(root);
|
|
@@ -30,16 +33,24 @@ async function findBranch(root) {
|
|
|
30
33
|
async function branchAt(directory) {
|
|
31
34
|
const marker = path.join(directory, ".git");
|
|
32
35
|
try {
|
|
33
|
-
const info = await
|
|
34
|
-
if (info.
|
|
35
|
-
return
|
|
36
|
+
const info = await lstat(marker, { bigint: true });
|
|
37
|
+
if (info.isSymbolicLink())
|
|
38
|
+
return undefined;
|
|
39
|
+
if (info.isDirectory()) {
|
|
40
|
+
const gitDirectory = await captureDirectDirectory(marker, "Git directory");
|
|
41
|
+
return readHead(gitDirectory);
|
|
42
|
+
}
|
|
36
43
|
if (!info.isFile())
|
|
37
44
|
return undefined;
|
|
38
|
-
const pointer = await
|
|
45
|
+
const pointer = await readBoundedText(marker, MAX_GIT_POINTER_BYTES, {
|
|
46
|
+
label: "Git directory pointer",
|
|
47
|
+
expected: stableFileExpectation(info),
|
|
48
|
+
});
|
|
39
49
|
const match = /^gitdir:\s*(.+)\s*$/im.exec(pointer);
|
|
40
50
|
if (match === null)
|
|
41
51
|
return undefined;
|
|
42
|
-
|
|
52
|
+
const gitDirectory = await captureDirectDirectory(path.resolve(directory, match[1]), "Git directory");
|
|
53
|
+
return readHead(gitDirectory);
|
|
43
54
|
}
|
|
44
55
|
catch {
|
|
45
56
|
return undefined;
|
|
@@ -47,7 +58,10 @@ async function branchAt(directory) {
|
|
|
47
58
|
}
|
|
48
59
|
async function readHead(gitDirectory) {
|
|
49
60
|
try {
|
|
50
|
-
const head = (await
|
|
61
|
+
const head = (await readBoundedText(path.join(gitDirectory.path, "HEAD"), MAX_GIT_POINTER_BYTES, {
|
|
62
|
+
label: "Git HEAD",
|
|
63
|
+
validate: async () => assertDirectoryAnchor(gitDirectory),
|
|
64
|
+
})).trim();
|
|
51
65
|
const prefix = "ref: refs/heads/";
|
|
52
66
|
if (head.startsWith(prefix))
|
|
53
67
|
return head.slice(prefix.length);
|
package/dist/user-store.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Bounded synchronous reads for the tiny JSON stores under ~/.jecode.
|
|
2
2
|
import { Buffer } from "node:buffer";
|
|
3
|
-
import {
|
|
3
|
+
import { readBoundedFileSync } from "./bounded-file.js";
|
|
4
|
+
import { assertDirectoryAnchorSync } from "./directory-anchor.js";
|
|
4
5
|
export const USER_STORE_LIMITS = Object.freeze({
|
|
5
6
|
settingsBytes: 64 * 1_024,
|
|
6
7
|
credentialsBytes: 256 * 1_024,
|
|
@@ -13,38 +14,29 @@ export const USER_STORE_LIMITS = Object.freeze({
|
|
|
13
14
|
accountToken: 32_768,
|
|
14
15
|
accountLabel: 1_024,
|
|
15
16
|
});
|
|
16
|
-
export function readBoundedJsonSync(file, maxBytes) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
:
|
|
22
|
-
|
|
17
|
+
export function readBoundedJsonSync(file, maxBytes, directory) {
|
|
18
|
+
const validate = directory === undefined
|
|
19
|
+
? undefined
|
|
20
|
+
: () => assertDirectoryAnchorSync(directory);
|
|
21
|
+
return JSON.parse(readBoundedFileSync(file, maxBytes, {
|
|
22
|
+
label: "user store",
|
|
23
|
+
validate,
|
|
24
|
+
}).toString("utf8"));
|
|
25
|
+
}
|
|
26
|
+
export function readBoundedJsonForMutationSync(file, maxBytes, label, directory) {
|
|
23
27
|
try {
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
// the read allocation to follow the file's new size.
|
|
32
|
-
const capacity = Math.min(maxBytes + 1, Math.max(1, details.size + 1));
|
|
33
|
-
const bytes = Buffer.allocUnsafe(capacity);
|
|
34
|
-
let offset = 0;
|
|
35
|
-
while (offset < capacity) {
|
|
36
|
-
const count = readSync(descriptor, bytes, offset, capacity - offset, null);
|
|
37
|
-
if (count === 0)
|
|
38
|
-
break;
|
|
39
|
-
offset += count;
|
|
40
|
-
}
|
|
41
|
-
if (offset > maxBytes || offset > details.size) {
|
|
42
|
-
throw new Error("user store changed while it was being read");
|
|
43
|
-
}
|
|
44
|
-
return JSON.parse(bytes.toString("utf8", 0, offset));
|
|
28
|
+
const validate = directory === undefined
|
|
29
|
+
? undefined
|
|
30
|
+
: () => assertDirectoryAnchorSync(directory);
|
|
31
|
+
return JSON.parse(readBoundedFileSync(file, maxBytes, {
|
|
32
|
+
label: "user store",
|
|
33
|
+
validate,
|
|
34
|
+
}).toString("utf8"));
|
|
45
35
|
}
|
|
46
|
-
|
|
47
|
-
|
|
36
|
+
catch (error) {
|
|
37
|
+
if (error.code === "ENOENT")
|
|
38
|
+
return undefined;
|
|
39
|
+
throw new Error(`${label} is invalid, unsafe, or too large`, { cause: error });
|
|
48
40
|
}
|
|
49
41
|
}
|
|
50
42
|
export function assertStoreText(text, maxBytes) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@giovannijecha/jecode",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.5",
|
|
4
4
|
"description": "An owned coding agent with zero external runtime dependencies.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
"bin/",
|
|
27
27
|
"dist/",
|
|
28
28
|
"assets/jeco-256.png",
|
|
29
|
+
"assets/wordmark-steel.svg",
|
|
29
30
|
"LICENSE",
|
|
30
31
|
"README.md"
|
|
31
32
|
],
|
|
@@ -58,9 +59,8 @@
|
|
|
58
59
|
"check:install": "npm run build:release -- --quiet && node dev/check-installed-cli.ts",
|
|
59
60
|
"check": "npm run check:source-tree && npm run typecheck && npm run coverage && npm run check:package && npm run check:install"
|
|
60
61
|
},
|
|
61
|
-
"dependencies": {},
|
|
62
62
|
"devDependencies": {
|
|
63
|
-
"@types/node": "^26.4.
|
|
64
|
-
"typescript": "^
|
|
63
|
+
"@types/node": "^26.4.1",
|
|
64
|
+
"typescript": "^7.0.2"
|
|
65
65
|
}
|
|
66
66
|
}
|
package/dist/tools/ripgrep.js
DELETED
|
@@ -1,230 +0,0 @@
|
|
|
1
|
-
// Optional native acceleration for bounded literal search.
|
|
2
|
-
import { spawn } from "node:child_process";
|
|
3
|
-
import * as path from "node:path";
|
|
4
|
-
import { shellEnvironment } from "../credential-safety.js";
|
|
5
|
-
import { resolveExecutable } from "../executable.js";
|
|
6
|
-
const MAX_BATCH_FILES = 256;
|
|
7
|
-
const MAX_BATCH_BYTES = 16_000_000;
|
|
8
|
-
const MAX_COMMAND_CHARS = 12_000;
|
|
9
|
-
const MAX_QUERY_CHARS = 4_000;
|
|
10
|
-
const MAX_EVENT_CHARS = 8_000_000;
|
|
11
|
-
const MAX_STDERR_CHARS = 16_000;
|
|
12
|
-
/** Return undefined when ripgrep is unavailable or cannot preserve the contract. */
|
|
13
|
-
export async function trySearchWithRipgrep(options) {
|
|
14
|
-
if (options.files.length === 0)
|
|
15
|
-
return { matches: [], binaryPaths: [] };
|
|
16
|
-
if (options.query.length > MAX_QUERY_CHARS || options.query.includes("\0"))
|
|
17
|
-
return undefined;
|
|
18
|
-
const executable = resolveExecutable("rg", { rejectUnder: options.root });
|
|
19
|
-
if (executable === undefined)
|
|
20
|
-
return undefined;
|
|
21
|
-
const matches = [];
|
|
22
|
-
const binaryPaths = new Set();
|
|
23
|
-
try {
|
|
24
|
-
for (const batch of batches(options.files, options.query.length)) {
|
|
25
|
-
throwIfAborted(options.signal);
|
|
26
|
-
const searched = await searchBatch(executable, batch, options);
|
|
27
|
-
if (searched === undefined)
|
|
28
|
-
return undefined;
|
|
29
|
-
for (const match of searched.matches) {
|
|
30
|
-
if (matches.length >= options.limit)
|
|
31
|
-
break;
|
|
32
|
-
matches.push(match);
|
|
33
|
-
}
|
|
34
|
-
for (const file of searched.binaryPaths)
|
|
35
|
-
binaryPaths.add(file);
|
|
36
|
-
if (matches.length >= options.limit)
|
|
37
|
-
break;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
catch (error) {
|
|
41
|
-
if (options.signal?.aborted === true)
|
|
42
|
-
throw abortReason(options.signal);
|
|
43
|
-
return undefined;
|
|
44
|
-
}
|
|
45
|
-
return { matches, binaryPaths: [...binaryPaths] };
|
|
46
|
-
}
|
|
47
|
-
function batches(files, queryChars) {
|
|
48
|
-
const groups = [];
|
|
49
|
-
let group = [];
|
|
50
|
-
let bytes = 0;
|
|
51
|
-
let chars = queryChars;
|
|
52
|
-
for (const file of files) {
|
|
53
|
-
const nextChars = chars + file.path.length + 1;
|
|
54
|
-
if (group.length > 0 &&
|
|
55
|
-
(group.length >= MAX_BATCH_FILES ||
|
|
56
|
-
bytes + file.bytes > MAX_BATCH_BYTES ||
|
|
57
|
-
nextChars > MAX_COMMAND_CHARS)) {
|
|
58
|
-
groups.push(group);
|
|
59
|
-
group = [];
|
|
60
|
-
bytes = 0;
|
|
61
|
-
chars = queryChars;
|
|
62
|
-
}
|
|
63
|
-
group.push(file);
|
|
64
|
-
bytes += file.bytes;
|
|
65
|
-
chars += file.path.length + 1;
|
|
66
|
-
}
|
|
67
|
-
if (group.length > 0)
|
|
68
|
-
groups.push(group);
|
|
69
|
-
return groups;
|
|
70
|
-
}
|
|
71
|
-
function searchBatch(executable, files, options) {
|
|
72
|
-
throwIfAborted(options.signal);
|
|
73
|
-
const args = [
|
|
74
|
-
"--json",
|
|
75
|
-
"--no-config",
|
|
76
|
-
"--fixed-strings",
|
|
77
|
-
"--max-filesize",
|
|
78
|
-
"1000000",
|
|
79
|
-
"--max-count",
|
|
80
|
-
String(options.limit),
|
|
81
|
-
...(options.caseSensitive ? [] : ["--ignore-case"]),
|
|
82
|
-
"--",
|
|
83
|
-
options.query,
|
|
84
|
-
...files.map((file) => file.path),
|
|
85
|
-
];
|
|
86
|
-
return new Promise((resolve, reject) => {
|
|
87
|
-
let child;
|
|
88
|
-
try {
|
|
89
|
-
child = spawn(executable, args, {
|
|
90
|
-
cwd: path.dirname(executable),
|
|
91
|
-
env: shellEnvironment(),
|
|
92
|
-
windowsHide: true,
|
|
93
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
catch {
|
|
97
|
-
resolve(undefined);
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
const matches = [];
|
|
101
|
-
const binaryPaths = new Set();
|
|
102
|
-
let buffered = "";
|
|
103
|
-
let stderr = "";
|
|
104
|
-
let invalid = false;
|
|
105
|
-
let overLimit = false;
|
|
106
|
-
let settled = false;
|
|
107
|
-
const onAbort = () => child.kill();
|
|
108
|
-
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
109
|
-
if (options.signal?.aborted === true)
|
|
110
|
-
onAbort();
|
|
111
|
-
const finish = (value, error) => {
|
|
112
|
-
if (settled)
|
|
113
|
-
return;
|
|
114
|
-
settled = true;
|
|
115
|
-
options.signal?.removeEventListener("abort", onAbort);
|
|
116
|
-
if (error !== undefined)
|
|
117
|
-
reject(error);
|
|
118
|
-
else
|
|
119
|
-
resolve(value);
|
|
120
|
-
};
|
|
121
|
-
const consume = (line) => {
|
|
122
|
-
if (line === "" || invalid || overLimit)
|
|
123
|
-
return;
|
|
124
|
-
try {
|
|
125
|
-
const event = JSON.parse(line);
|
|
126
|
-
const parsed = ripgrepEvent(event);
|
|
127
|
-
if (parsed?.kind === "match") {
|
|
128
|
-
// `rg --max-count` is per file, not global. Once the raw stream
|
|
129
|
-
// exceeds the requested result count, stop the accelerator and let
|
|
130
|
-
// the portable scanner produce the exact bounded answer. Returning
|
|
131
|
-
// early here would lose the later binary-file end marker.
|
|
132
|
-
if (matches.length >= options.limit) {
|
|
133
|
-
overLimit = true;
|
|
134
|
-
child.kill();
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
matches.push(parsed.match);
|
|
138
|
-
}
|
|
139
|
-
if (parsed?.kind === "binary")
|
|
140
|
-
binaryPaths.add(parsed.path);
|
|
141
|
-
}
|
|
142
|
-
catch {
|
|
143
|
-
invalid = true;
|
|
144
|
-
child.kill();
|
|
145
|
-
}
|
|
146
|
-
};
|
|
147
|
-
child.stdout.setEncoding("utf8");
|
|
148
|
-
child.stderr.setEncoding("utf8");
|
|
149
|
-
child.stdout.on("data", (chunk) => {
|
|
150
|
-
buffered += chunk;
|
|
151
|
-
if (buffered.length > MAX_EVENT_CHARS) {
|
|
152
|
-
invalid = true;
|
|
153
|
-
child.kill();
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
|
-
let newline = buffered.indexOf("\n");
|
|
157
|
-
while (newline !== -1) {
|
|
158
|
-
consume(buffered.slice(0, newline));
|
|
159
|
-
buffered = buffered.slice(newline + 1);
|
|
160
|
-
newline = buffered.indexOf("\n");
|
|
161
|
-
}
|
|
162
|
-
});
|
|
163
|
-
child.stderr.on("data", (chunk) => {
|
|
164
|
-
stderr = `${stderr}${chunk}`.slice(-MAX_STDERR_CHARS);
|
|
165
|
-
});
|
|
166
|
-
child.on("error", (error) => {
|
|
167
|
-
if (options.signal?.aborted === true)
|
|
168
|
-
finish(undefined, abortReason(options.signal));
|
|
169
|
-
else if (error.code === "ENOENT")
|
|
170
|
-
finish(undefined);
|
|
171
|
-
else
|
|
172
|
-
finish(undefined);
|
|
173
|
-
});
|
|
174
|
-
child.on("close", (code) => {
|
|
175
|
-
if (options.signal?.aborted === true) {
|
|
176
|
-
finish(undefined, abortReason(options.signal));
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
|
-
consume(buffered);
|
|
180
|
-
if (invalid || overLimit || (code !== 0 && code !== 1) || stderr.trim() !== "") {
|
|
181
|
-
finish(undefined);
|
|
182
|
-
return;
|
|
183
|
-
}
|
|
184
|
-
const order = new Map(files.map((file, index) => [file.path, index]));
|
|
185
|
-
finish({
|
|
186
|
-
matches: matches
|
|
187
|
-
.filter((match) => !binaryPaths.has(match.path))
|
|
188
|
-
.sort((a, b) => ((order.get(a.path) ?? Number.MAX_SAFE_INTEGER) -
|
|
189
|
-
(order.get(b.path) ?? Number.MAX_SAFE_INTEGER) ||
|
|
190
|
-
a.line - b.line)),
|
|
191
|
-
binaryPaths: [...binaryPaths],
|
|
192
|
-
});
|
|
193
|
-
});
|
|
194
|
-
});
|
|
195
|
-
}
|
|
196
|
-
function ripgrepEvent(value) {
|
|
197
|
-
if (!record(value) || !record(value["data"]))
|
|
198
|
-
return undefined;
|
|
199
|
-
const data = value["data"];
|
|
200
|
-
const file = textField(data["path"]);
|
|
201
|
-
if (file === undefined)
|
|
202
|
-
return undefined;
|
|
203
|
-
if (value["type"] === "match") {
|
|
204
|
-
const line = data["line_number"];
|
|
205
|
-
const text = textField(data["lines"]);
|
|
206
|
-
if (typeof line !== "number" || !Number.isInteger(line) || text === undefined)
|
|
207
|
-
return undefined;
|
|
208
|
-
return {
|
|
209
|
-
kind: "match",
|
|
210
|
-
match: { path: file, line, text: text.replace(/\r?\n$/, "") },
|
|
211
|
-
};
|
|
212
|
-
}
|
|
213
|
-
if (value["type"] === "end" && typeof data["binary_offset"] === "number") {
|
|
214
|
-
return { kind: "binary", path: file };
|
|
215
|
-
}
|
|
216
|
-
return undefined;
|
|
217
|
-
}
|
|
218
|
-
function textField(value) {
|
|
219
|
-
return record(value) && typeof value["text"] === "string" ? value["text"] : undefined;
|
|
220
|
-
}
|
|
221
|
-
function record(value) {
|
|
222
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
223
|
-
}
|
|
224
|
-
function throwIfAborted(signal) {
|
|
225
|
-
if (signal?.aborted === true)
|
|
226
|
-
throw abortReason(signal);
|
|
227
|
-
}
|
|
228
|
-
function abortReason(signal) {
|
|
229
|
-
return signal.reason instanceof Error ? signal.reason : new Error("interrupted");
|
|
230
|
-
}
|