@catheadowl/ukp 0.2.4 → 0.2.6
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 +15 -7
- package/docs/remote-deployment.md +101 -16
- package/package.json +1 -1
- package/src/capabilities/door-health.ts +480 -0
- package/src/capabilities/remote-client.ts +201 -21
- package/src/capabilities/rg.ts +147 -20
- package/src/capabilities/tls-identity.ts +3 -2
- package/src/cli.ts +7 -1
- package/src/commands/diagnose.ts +80 -2
- package/src/commands/guide-content/remote.txt +4 -2
- package/src/commands/guide-content/rg.txt +35 -0
- package/src/commands/guide.ts +11 -4
- package/src/commands/init.ts +27 -0
- package/src/commands/rg.ts +45 -14
- package/src/commands/serve-task.ts +334 -44
- package/src/commands/serve.ts +101 -19
- package/src/server.ts +113 -65
- package/src/spawn-relay.ts +57 -0
|
@@ -19,11 +19,12 @@ import {
|
|
|
19
19
|
type RemoteUrlParts,
|
|
20
20
|
} from "../registry.ts";
|
|
21
21
|
import { spkiPinOf } from "./tls-identity.ts";
|
|
22
|
+
import { toolRelayScript } from "../spawn-relay.ts";
|
|
22
23
|
import type { SearchEndpointOutcome } from "./search.ts";
|
|
23
24
|
import type { NavEnvelope } from "./nav.ts";
|
|
24
25
|
import type { ProposeResult, ProposeStatus } from "./propose.ts";
|
|
25
26
|
import { EXTERNAL_PROVIDER } from "../config/external-tool.ts";
|
|
26
|
-
import type { RgEndpointOutcome, RgMatch, RgCountEntry } from "./rg.ts";
|
|
27
|
+
import type { RgEndpointOutcome, RgMatch, RgCountEntry, RgFileEntry } from "./rg.ts";
|
|
27
28
|
|
|
28
29
|
/** Remote transport for the client side (ukp-remote wire v1, ADR-REM-002/003;
|
|
29
30
|
* ukp_remote W2). All calls fetch fresh (no cross-invocation cache), carry the
|
|
@@ -381,6 +382,71 @@ function wrongShellSignature(form: WakeShellForm, diagnostics: string): boolean
|
|
|
381
382
|
return /['"]cmd['"]|cmd: (?:command )?not found/i.test(diagnostics);
|
|
382
383
|
}
|
|
383
384
|
|
|
385
|
+
/** Wake failure classes from ssh's own diagnostics (ADR-REM-008 / D-093):
|
|
386
|
+
* OpenSSH's stderr wording is locale-stable (unlike cmd.exe's), so stable
|
|
387
|
+
* substrings classify the deterministic failures — verdicts a re-run against
|
|
388
|
+
* the same host reproduces, which is why none of them belongs in the retry
|
|
389
|
+
* ladder. One taxonomy, three matchers: these connect/auth signatures here,
|
|
390
|
+
* missing-ukp (127/9009 dictionary) and shell-mismatch keep their existing
|
|
391
|
+
* branches. Pure function by design — the signature table is unit-testable
|
|
392
|
+
* without spawning ssh. */
|
|
393
|
+
export type WakeFailureClass = "unreachable" | "refused" | "auth" | "hostkey";
|
|
394
|
+
|
|
395
|
+
export function classifyWakeFailure(diagnostics: string): WakeFailureClass | undefined {
|
|
396
|
+
if (/Connection timed out|No route to host|Could not resolve hostname/i.test(diagnostics)) return "unreachable";
|
|
397
|
+
if (/Connection refused/i.test(diagnostics)) return "refused";
|
|
398
|
+
if (/Host key verification failed/i.test(diagnostics)) return "hostkey";
|
|
399
|
+
// Auth failures always carry the method-list suffix ("Permission denied
|
|
400
|
+
// (publickey,password).") — ssh's own format. The bare phrase also appears
|
|
401
|
+
// in REMOTE-COMMAND exec errors (a found-but-not-executable ukp on a
|
|
402
|
+
// noexec mount), which reach these diagnostics under -tt and must keep
|
|
403
|
+
// falling through to the generic wording, not get the key-auth remedy
|
|
404
|
+
// (review P2).
|
|
405
|
+
if (/Permission denied \(/i.test(diagnostics)) return "auth";
|
|
406
|
+
return undefined;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/** A probe-stage timeout verdict is not trustworthy on its own (ADR-REM-008
|
|
410
|
+
* §3, implementation-time refinement): the probe's 5s race window is shorter
|
|
411
|
+
* than the full ConnectTimeout budget a slow-but-alive host may legitimately
|
|
412
|
+
* need — its wake would succeed in the ladder's window — so black-hole
|
|
413
|
+
* signatures defer to the ladder, which runs the full budget before the same
|
|
414
|
+
* classifier issues the verdict. Instant verdicts (refused, auth, hostkey,
|
|
415
|
+
* resolve and no-route failures) settle within one roundtrip and are trusted
|
|
416
|
+
* wherever they appear; a probe-local short ConnectTimeout would have
|
|
417
|
+
* misjudged every host slower than it. */
|
|
418
|
+
function wakeFailureDefersToLadder(failureClass: WakeFailureClass, diagnostics: string): boolean {
|
|
419
|
+
return failureClass === "unreachable"
|
|
420
|
+
&& /Connection timed out/i.test(diagnostics)
|
|
421
|
+
&& !/No route to host|Could not resolve hostname/i.test(diagnostics);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/** Classified wake-failure text (ADR-REM-008 §4): names the ssh TARGET (an
|
|
425
|
+
* origin-level fact under fan-out — never an endpoint label), quotes the ssh
|
|
426
|
+
* diagnostics as evidence, and gives the class its one remedy. Printable
|
|
427
|
+
* ASCII only — the frozen vocabulary rule for runtime strings. */
|
|
428
|
+
export function wakeFailureMessage(failureClass: WakeFailureClass, target: string, diagnostics: string): string {
|
|
429
|
+
const evidence = diagnostics.length > 0 ? `: ${diagnostics}` : "";
|
|
430
|
+
switch (failureClass) {
|
|
431
|
+
case "unreachable":
|
|
432
|
+
return `waking the ukp door on '${target}' failed - the host is unreachable${evidence}. The machine appears off, firewalled, or its address no longer resolves; verify it is powered on and reachable`;
|
|
433
|
+
case "refused":
|
|
434
|
+
return `waking the ukp door on '${target}' failed - the host is reachable but nothing is listening for ssh${evidence}. Start the ssh server on the host (on Windows: the optional OpenSSH Server feature) or fix the port`;
|
|
435
|
+
case "auth":
|
|
436
|
+
return `waking the ukp door on '${target}' failed - ssh rejected the credentials (BatchMode, no password prompt)${evidence}. Fix key auth: add the public key to the host's authorized_keys, or check the ssh config's IdentityFile`;
|
|
437
|
+
case "hostkey":
|
|
438
|
+
return `waking the ukp door on '${target}' failed - the host key was not accepted${evidence}. If the host was reinstalled this is expected: re-verify it (ssh-keygen -R), then retry`;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/** Not-ready wording (ADR-REM-008): the old text blamed the host alias and
|
|
443
|
+
* key auth — both, by the time this message is reached, have already
|
|
444
|
+
* succeeded (their failures classify earlier); what failed is the door
|
|
445
|
+
* behind a working connection. */
|
|
446
|
+
export function wakeNotReadyMessage(target: string, timeoutMs: number): string {
|
|
447
|
+
return `the ssh connection to '${target}' worked but the ukp door did not become ready within ${Math.round(timeoutMs / 1000)}s. Host prerequisite (W9): ukp on the remote PATH; a slow first start also lands here - retry once before investigating`;
|
|
448
|
+
}
|
|
449
|
+
|
|
384
450
|
/** Cross-invocation shell-family cache (ukp_list W1 / L-1, ADR 0026's
|
|
385
451
|
* implementation-time note): a host's shell family is a static fact, but
|
|
386
452
|
* learning it costs a full ssh roundtrip per origin per invocation — on
|
|
@@ -426,6 +492,60 @@ function persistShellFamily(cachePath: string | undefined, target: string, form:
|
|
|
426
492
|
}
|
|
427
493
|
}
|
|
428
494
|
|
|
495
|
+
/** Structural subset of Bun's Subprocess that the ssh paths consume (a Pick,
|
|
496
|
+
* not a hand-written interface — the ADR 0021 guard scans for CLI-shaped
|
|
497
|
+
* member declarations). Direct spawns return the Subprocess itself; the win32
|
|
498
|
+
* relay below returns a delegating wrapper because its kill semantics differ
|
|
499
|
+
* (tree-kill). */
|
|
500
|
+
type HostSshProcess = Pick<ReturnType<typeof Bun.spawn>, "pid" | "exitCode" | "stdout" | "stderr" | "kill">;
|
|
501
|
+
|
|
502
|
+
/** Win32 Defender tax (2026-09-22 finding, ukp_remote TODO
|
|
503
|
+
* 20260922-win32-ssh-spawn-tax): the DIRECT bun-to-ssh.exe child edge is held
|
|
504
|
+
* ~10s by behavioral inspection — measured across stdio modes, windowsHide,
|
|
505
|
+
* and absolute paths, while bash-to-ssh is 0.16s and ANY intermediary parent
|
|
506
|
+
* (cmd, node, powershell) escapes to 0.5-0.8s. Every ssh spawn in this file
|
|
507
|
+
* pays it once, so on win32 real-ssh spawns go through a powershell.exe
|
|
508
|
+
* relay. -EncodedCommand carries the script as one base64 token (no quoting
|
|
509
|
+
* stress from Bun), and the script splats a PS array so the pinned wake
|
|
510
|
+
* command's load-bearing quotes reach ssh byte-identical (verified E2E: door
|
|
511
|
+
* READY in ~1.4s). Test injection (explicit sshCommand) bypasses the relay —
|
|
512
|
+
* the fakes must not be wrapped. Killing the relay orphans the ssh child
|
|
513
|
+
* (tunnel + door outlive the killed handle — verified), so the wrapped kill
|
|
514
|
+
* reaps the tree via taskkill /T /F. */
|
|
515
|
+
function spawnHostSsh(options: { sshCommand?: readonly string[] }, argv: readonly string[]): HostSshProcess {
|
|
516
|
+
if (process.platform !== "win32" || options.sshCommand !== undefined) {
|
|
517
|
+
return Bun.spawn([...argv], { stdout: "pipe", stderr: "pipe", stdin: "ignore" });
|
|
518
|
+
}
|
|
519
|
+
const script = toolRelayScript(argv);
|
|
520
|
+
const encoded = Buffer.from(script, "utf16le").toString("base64");
|
|
521
|
+
const proc = Bun.spawn(["powershell", "-NoProfile", "-EncodedCommand", encoded], {
|
|
522
|
+
stdout: "pipe",
|
|
523
|
+
stderr: "pipe",
|
|
524
|
+
stdin: "ignore",
|
|
525
|
+
});
|
|
526
|
+
const { pid, stdout, stderr } = proc;
|
|
527
|
+
return {
|
|
528
|
+
pid,
|
|
529
|
+
stdout,
|
|
530
|
+
stderr,
|
|
531
|
+
get exitCode() {
|
|
532
|
+
return proc.exitCode;
|
|
533
|
+
},
|
|
534
|
+
kill() {
|
|
535
|
+
// taskkill also carries the Defender tax when spawned directly from
|
|
536
|
+
// bun (measured 10.1s direct vs 105ms through cmd — same finding as
|
|
537
|
+
// the ssh/rg edges); the PID is digits-only, so the cmd payload needs
|
|
538
|
+
// no quoting. /T /F takes the relay AND its ssh child down together
|
|
539
|
+
// (killing the relay alone orphans the tunnel).
|
|
540
|
+
Bun.spawnSync(["cmd", "/d", "/c", `taskkill /PID ${pid} /T /F`], {
|
|
541
|
+
stdout: "ignore",
|
|
542
|
+
stderr: "ignore",
|
|
543
|
+
stdin: "ignore",
|
|
544
|
+
});
|
|
545
|
+
},
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
|
|
429
549
|
/** One cheap, language-independent roundtrip to learn the host shell family
|
|
430
550
|
* BEFORE the first wake attempt (2026-09-20, liku 9.5 finding): cmd.exe
|
|
431
551
|
* expands `%OS%` to `Windows_NT`; a POSIX shell echoes the literal token.
|
|
@@ -436,21 +556,32 @@ function persistShellFamily(cachePath: string | undefined, target: string, form:
|
|
|
436
556
|
* build LOSES a quoted remote command under a pty (an interactive cmd
|
|
437
557
|
* swallows it and waits on stdin forever — the no-pty path executes it
|
|
438
558
|
* correctly). Defaults to POSIX (the common server shape) when the probe
|
|
439
|
-
* errors or times out; the ladder remains as the backstop.
|
|
559
|
+
* errors or times out; the ladder remains as the backstop.
|
|
560
|
+
*
|
|
561
|
+
* The probe doubles as the first failure classifier (ADR-REM-008 §3): its
|
|
562
|
+
* merged output — the `%OS%` answer on stdout, ssh's own diagnostics on
|
|
563
|
+
* stderr — feeds `classifyWakeFailure`, so instant verdicts (refused / auth
|
|
564
|
+
* / hostkey / resolve failures) classify here and the ladder is never
|
|
565
|
+
* entered for them. */
|
|
440
566
|
async function probeHostShellFamily(
|
|
441
567
|
options: { sshCommand?: readonly string[] },
|
|
442
568
|
target: string,
|
|
443
|
-
): Promise<WakeShellForm> {
|
|
444
|
-
const proc =
|
|
569
|
+
): Promise<{ form: WakeShellForm; diagnostics: string }> {
|
|
570
|
+
const proc = spawnHostSsh(
|
|
571
|
+
options,
|
|
445
572
|
[...(options.sshCommand ?? ["ssh"]), "-o", "BatchMode=yes", ...connectTimeoutOptions(), target, "echo %OS%"],
|
|
446
|
-
{ stdout: "pipe", stderr: "pipe", stdin: "ignore" },
|
|
447
573
|
);
|
|
448
|
-
|
|
449
|
-
|
|
574
|
+
// One merged read of BOTH streams, raced against the 5s cap: resolves when
|
|
575
|
+
// the process dies (a healthy probe answers and exits), else returns empty
|
|
576
|
+
// at the cap and the default form stands — a slow host is never punished.
|
|
577
|
+
// A generous keep (review P3): the tail-trimmed merge must not push the
|
|
578
|
+
// stdout %OS% answer out the front even behind unusual stderr volume.
|
|
579
|
+
const text = await Promise.race([
|
|
580
|
+
drainStderrTail([proc.stdout, proc.stderr], 8192),
|
|
450
581
|
Bun.sleep(5_000).then(() => ""),
|
|
451
582
|
]);
|
|
452
583
|
proc.kill();
|
|
453
|
-
return /Windows_NT/i.test(
|
|
584
|
+
return { form: /Windows_NT/i.test(text) ? "cmd" : "posix", diagnostics: text };
|
|
454
585
|
}
|
|
455
586
|
|
|
456
587
|
/** HTTP-level readiness probe through the tunnel (W9): a TCP accept on the
|
|
@@ -463,7 +594,7 @@ async function probeHostShellFamily(
|
|
|
463
594
|
async function probeWakeReady(
|
|
464
595
|
base: string,
|
|
465
596
|
timeoutMs: number,
|
|
466
|
-
proc:
|
|
597
|
+
proc: HostSshProcess,
|
|
467
598
|
): Promise<boolean> {
|
|
468
599
|
const deadline = Date.now() + timeoutMs;
|
|
469
600
|
for (;;) {
|
|
@@ -499,9 +630,12 @@ async function probeWakeReady(
|
|
|
499
630
|
* so diagnostics can arrive on either stream — both are drained and
|
|
500
631
|
* concatenated. Resolves when the streams end (i.e. after the process is
|
|
501
632
|
* dead) — only await once the proc has been killed or has exited. */
|
|
502
|
-
function drainStderrTail(
|
|
633
|
+
function drainStderrTail(
|
|
634
|
+
streams: readonly (ReadableStream<Uint8Array> | number | undefined)[],
|
|
635
|
+
keep = 2000,
|
|
636
|
+
): Promise<string> {
|
|
503
637
|
return Promise.all(
|
|
504
|
-
streams.map((stream) => new Response(stream).text().catch(() => "")),
|
|
638
|
+
streams.map((stream) => (typeof stream === "object" && stream !== null ? new Response(stream).text().catch(() => "") : "")),
|
|
505
639
|
).then((texts) => {
|
|
506
640
|
const text = texts.join("").trim();
|
|
507
641
|
return text.length > keep ? `...${text.slice(-keep)}` : text;
|
|
@@ -525,7 +659,7 @@ function drainStderrTail(streams: readonly ReadableStream<Uint8Array>[], keep =
|
|
|
525
659
|
async function openSshTunnel(
|
|
526
660
|
parts: { user?: string; host: string },
|
|
527
661
|
options: { sshCommand?: readonly string[]; registryPath?: string },
|
|
528
|
-
): Promise<{ proc:
|
|
662
|
+
): Promise<{ proc: HostSshProcess; base: string }> {
|
|
529
663
|
const target = parts.user !== undefined ? `${parts.user}@${parts.host}` : parts.host;
|
|
530
664
|
// Tier 1 first: a detached mux master per origin (no-op when one already
|
|
531
665
|
// holds the socket); the wake client below rides it when possible and
|
|
@@ -539,8 +673,25 @@ async function openSshTunnel(
|
|
|
539
673
|
// Shell form (2026-09-20): probed up front via `echo %OS%` (language-
|
|
540
674
|
// independent — see probeHostShellFamily), POSIX by default; the wrong-
|
|
541
675
|
// shell ladder below remains the backstop for a lying or timed-out probe.
|
|
542
|
-
|
|
543
|
-
|
|
676
|
+
// Cold cache: the probe doubles as the first failure classifier
|
|
677
|
+
// (ADR-REM-008 §3) — its instant verdicts (refused / auth / hostkey /
|
|
678
|
+
// resolve failures) reject the origin here, before any ladder attempt;
|
|
679
|
+
// black-hole timeouts defer to the ladder's full ConnectTimeout budget.
|
|
680
|
+
// Warm cache (the probe is skipped): the ladder's first attempt carries
|
|
681
|
+
// the same classifier, so the classified verdicts cost at most one
|
|
682
|
+
// ConnectTimeout either way.
|
|
683
|
+
let form: WakeShellForm;
|
|
684
|
+
const cachedForm = readCachedShellFamily(shellFamilyCachePath(options), target);
|
|
685
|
+
if (cachedForm !== undefined) {
|
|
686
|
+
form = cachedForm;
|
|
687
|
+
} else {
|
|
688
|
+
const probed = await probeHostShellFamily(options, target);
|
|
689
|
+
const probeClass = classifyWakeFailure(probed.diagnostics);
|
|
690
|
+
if (probeClass !== undefined && !wakeFailureDefersToLadder(probeClass, probed.diagnostics)) {
|
|
691
|
+
throw new RemoteTransportError(wakeFailureMessage(probeClass, target, probed.diagnostics));
|
|
692
|
+
}
|
|
693
|
+
form = probed.form;
|
|
694
|
+
}
|
|
544
695
|
const attempts = 3;
|
|
545
696
|
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
546
697
|
const localPort = await freePort();
|
|
@@ -564,7 +715,7 @@ async function openSshTunnel(
|
|
|
564
715
|
target,
|
|
565
716
|
wakeDoorCommand(remotePort, form),
|
|
566
717
|
];
|
|
567
|
-
const proc =
|
|
718
|
+
const proc = spawnHostSsh(options, argv);
|
|
568
719
|
const stderrTail = drainStderrTail([proc.stdout, proc.stderr]);
|
|
569
720
|
if (await probeWakeReady(`http://127.0.0.1:${localPort}`, WAKE_READY_TIMEOUT_MS, proc)) {
|
|
570
721
|
// Ground truth for the shell-family cache: the form that actually woke
|
|
@@ -579,6 +730,15 @@ async function openSshTunnel(
|
|
|
579
730
|
// fast crash as a slow timeout.
|
|
580
731
|
const diagnostics = (await stderrTail).trim();
|
|
581
732
|
const exited = proc.exitCode !== null;
|
|
733
|
+
// Deterministic connect/auth verdicts never consume the ladder's
|
|
734
|
+
// remaining attempts (ADR-REM-008 §2): re-running against the same host
|
|
735
|
+
// reproduces them, so attempt 1's verdict IS the origin's verdict. The
|
|
736
|
+
// ladder stays for what it was built for — port-collision binds and
|
|
737
|
+
// not-ready timeouts.
|
|
738
|
+
const failureClass = exited ? classifyWakeFailure(diagnostics) : undefined;
|
|
739
|
+
if (failureClass !== undefined) {
|
|
740
|
+
throw new RemoteTransportError(wakeFailureMessage(failureClass, target, diagnostics));
|
|
741
|
+
}
|
|
582
742
|
if (/ControlPath too long|muxserver_listen|unix_listener/i.test(diagnostics)) {
|
|
583
743
|
muxDisabled = true;
|
|
584
744
|
}
|
|
@@ -604,7 +764,7 @@ async function openSshTunnel(
|
|
|
604
764
|
throw new RemoteTransportError(
|
|
605
765
|
exited
|
|
606
766
|
? `waking the ukp door on '${target}' failed: ${diagnostics}`
|
|
607
|
-
:
|
|
767
|
+
: wakeNotReadyMessage(target, WAKE_READY_TIMEOUT_MS),
|
|
608
768
|
);
|
|
609
769
|
}
|
|
610
770
|
}
|
|
@@ -662,7 +822,7 @@ export interface RemoteTransportPool {
|
|
|
662
822
|
export function createTransportPool(
|
|
663
823
|
options: { sshCommand?: readonly string[]; registryPath?: string } = {},
|
|
664
824
|
): RemoteTransportPool {
|
|
665
|
-
const tunnels = new Map<string, Promise<{ proc:
|
|
825
|
+
const tunnels = new Map<string, Promise<{ proc: HostSshProcess; base: string }>>();
|
|
666
826
|
return {
|
|
667
827
|
async acquire(binding) {
|
|
668
828
|
if (binding.kind !== "remote" || binding.url === undefined) {
|
|
@@ -1168,10 +1328,23 @@ function remoteRgCounts(raw: unknown): RgCountEntry[] | undefined {
|
|
|
1168
1328
|
});
|
|
1169
1329
|
}
|
|
1170
1330
|
|
|
1331
|
+
function remoteRgFiles(raw: unknown): RgFileEntry[] | undefined {
|
|
1332
|
+
if (!Array.isArray(raw)) return undefined;
|
|
1333
|
+
return raw.flatMap((entry): RgFileEntry[] => {
|
|
1334
|
+
if (typeof entry !== "object" || entry === null) return [];
|
|
1335
|
+
const file = entry as Record<string, unknown>;
|
|
1336
|
+
if (typeof file.path !== "string") return [];
|
|
1337
|
+
return [{ path: file.path, ...(typeof file.ukp_uri === "string" ? { ukp_uri: file.ukp_uri } : {}) }];
|
|
1338
|
+
});
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1171
1341
|
const RG_WIRE_STATUSES = new Set(["succeeded", "no_matches", "skipped", "failed", "interrupted", "cancelled"]);
|
|
1172
1342
|
|
|
1173
1343
|
/** GET /v1/rg — single-endpoint outcome in the local RgEndpointOutcome
|
|
1174
|
-
* vocabulary (provider is rg's external tier regardless of transport).
|
|
1344
|
+
* vocabulary (provider is rg's external tier regardless of transport).
|
|
1345
|
+
* Files mode (ADR-RG-005) sends files=1 and omits query — an older serve
|
|
1346
|
+
* without files support answers its query-required usage error, which
|
|
1347
|
+
* lands here as a failed outcome carrying the server's message. */
|
|
1175
1348
|
export async function remoteRg(
|
|
1176
1349
|
binding: RegistryBinding,
|
|
1177
1350
|
transport: RemoteTransportHandle,
|
|
@@ -1179,16 +1352,19 @@ export async function remoteRg(
|
|
|
1179
1352
|
params: {
|
|
1180
1353
|
query: string;
|
|
1181
1354
|
limit: number;
|
|
1182
|
-
|
|
1355
|
+
globs?: readonly string[];
|
|
1183
1356
|
type?: string;
|
|
1184
1357
|
ignoreCase?: boolean;
|
|
1185
1358
|
count?: boolean;
|
|
1359
|
+
files?: boolean;
|
|
1186
1360
|
passthrough: readonly string[];
|
|
1187
1361
|
},
|
|
1188
1362
|
): Promise<RemoteRgExecution> {
|
|
1189
1363
|
const base = transport.base;
|
|
1190
|
-
const search = new URLSearchParams({
|
|
1191
|
-
if (params.
|
|
1364
|
+
const search = new URLSearchParams({ limit: String(params.limit) });
|
|
1365
|
+
if (params.files === true) search.set("files", "1");
|
|
1366
|
+
else search.set("query", params.query);
|
|
1367
|
+
for (const glob of params.globs ?? []) search.append("glob", glob);
|
|
1192
1368
|
if (params.type !== undefined) search.set("type", params.type);
|
|
1193
1369
|
if (params.ignoreCase === true) search.set("i", "1");
|
|
1194
1370
|
if (params.count === true) search.set("count", "1");
|
|
@@ -1223,6 +1399,9 @@ export async function remoteRg(
|
|
|
1223
1399
|
match.ukp_uri !== undefined ? { ...match, ukp_uri: reanchorRemoteUkpUri(match.ukp_uri, binding) } : match,
|
|
1224
1400
|
);
|
|
1225
1401
|
const counts = remoteRgCounts(entry.counts);
|
|
1402
|
+
const files = remoteRgFiles(entry.files)?.map((file) =>
|
|
1403
|
+
file.ukp_uri !== undefined ? { ...file, ukp_uri: reanchorRemoteUkpUri(file.ukp_uri, binding) } : file,
|
|
1404
|
+
);
|
|
1226
1405
|
const warnings = Array.isArray(record.warnings) ? record.warnings.filter((item): item is string => typeof item === "string") : [];
|
|
1227
1406
|
return {
|
|
1228
1407
|
outcome: {
|
|
@@ -1232,6 +1411,7 @@ export async function remoteRg(
|
|
|
1232
1411
|
...(typeof entry.message === "string" ? { message: entry.message } : {}),
|
|
1233
1412
|
...(matches !== undefined ? { matches } : {}),
|
|
1234
1413
|
...(counts !== undefined ? { counts } : {}),
|
|
1414
|
+
...(files !== undefined ? { files } : {}),
|
|
1235
1415
|
...(entry.truncated === true ? { truncated: true } : {}),
|
|
1236
1416
|
},
|
|
1237
1417
|
warnings,
|