@asterxsk/kiln 0.1.0 → 0.2.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/LICENSE +21 -21
- package/README.md +170 -170
- package/agent/AGENTS.md +67 -67
- package/agent/README.md +5 -5
- package/agent/extensions/AGENTS.md +68 -68
- package/agent/extensions/ask-user/index.ts +418 -418
- package/agent/extensions/ask-user/package-lock.json +769 -769
- package/agent/extensions/ask-user/package.json +19 -19
- package/agent/extensions/ask-user/prompt.ts +45 -45
- package/agent/extensions/ask-user/tsconfig.json +7 -7
- package/agent/extensions/background-terminals/docs/implementation-guide.md +942 -942
- package/agent/extensions/background-terminals/index.ts +627 -627
- package/agent/extensions/background-terminals/manager.test.ts +735 -735
- package/agent/extensions/background-terminals/output.test.ts +109 -109
- package/agent/extensions/background-terminals/package-lock.json +769 -769
- package/agent/extensions/background-terminals/package.json +17 -17
- package/agent/extensions/background-terminals/prompt.test.ts +125 -125
- package/agent/extensions/background-terminals/ps.test.ts +82 -82
- package/agent/extensions/background-terminals/result-delivery.test.ts +44 -44
- package/agent/extensions/background-terminals/src/domain.ts +87 -87
- package/agent/extensions/background-terminals/src/manager.ts +907 -907
- package/agent/extensions/background-terminals/src/output.ts +84 -84
- package/agent/extensions/background-terminals/src/prompt.ts +142 -142
- package/agent/extensions/background-terminals/src/result-delivery.ts +27 -27
- package/agent/extensions/background-terminals/src/runtime.ts +36 -36
- package/agent/extensions/background-terminals/src/ui/output-view.ts +79 -79
- package/agent/extensions/background-terminals/src/ui/ps.ts +621 -621
- package/agent/extensions/background-terminals/tsconfig.json +7 -7
- package/agent/extensions/file-search/index.spec.ts +443 -443
- package/agent/extensions/file-search/index.ts +459 -459
- package/agent/extensions/file-search/package-lock.json +2253 -2253
- package/agent/extensions/file-search/package.json +23 -23
- package/agent/extensions/file-search/src/args.ts +122 -122
- package/agent/extensions/file-search/src/binaries.ts +422 -422
- package/agent/extensions/file-search/src/output.ts +126 -126
- package/agent/extensions/file-search/src/process.ts +146 -146
- package/agent/extensions/file-search/src/prompt.ts +52 -52
- package/agent/extensions/file-search/tsconfig.json +7 -7
- package/agent/extensions/modelconf/PLAN.md +915 -915
- package/agent/extensions/modelconf/index.ts +296 -296
- package/agent/extensions/modelconf/src/ui/ModelConfView.ts +1101 -1101
- package/agent/extensions/pi-web-access/CHANGELOG.md +690 -690
- package/agent/extensions/pi-web-access/LICENSE +21 -21
- package/agent/extensions/pi-web-access/README.md +470 -470
- package/agent/extensions/pi-web-access/SECURITY.md +5 -5
- package/agent/extensions/pi-web-access/activity.ts +101 -101
- package/agent/extensions/pi-web-access/auth-fetch.ts +148 -148
- package/agent/extensions/pi-web-access/brightdata-unlocker.ts +272 -272
- package/agent/extensions/pi-web-access/chrome-cookies.ts +669 -669
- package/agent/extensions/pi-web-access/content-find.ts +139 -139
- package/agent/extensions/pi-web-access/credential-source.ts +191 -191
- package/agent/extensions/pi-web-access/data-uri-sanitize.ts +406 -406
- package/agent/extensions/pi-web-access/datalab-pdf-extract.ts +568 -568
- package/agent/extensions/pi-web-access/declared-web-links.ts +173 -173
- package/agent/extensions/pi-web-access/evidence/CONTRACT-EVIDENCE.md +496 -496
- package/agent/extensions/pi-web-access/evidence/contract-probe.mjs +140 -140
- package/agent/extensions/pi-web-access/exa.ts +526 -526
- package/agent/extensions/pi-web-access/extract.ts +1196 -1196
- package/agent/extensions/pi-web-access/feature-config.ts +29 -29
- package/agent/extensions/pi-web-access/fetch-params.ts +111 -111
- package/agent/extensions/pi-web-access/gemini-adc.ts +298 -298
- package/agent/extensions/pi-web-access/gemini-api.ts +353 -353
- package/agent/extensions/pi-web-access/gemini-pdf-extract.ts +108 -108
- package/agent/extensions/pi-web-access/gemini-url-context.ts +128 -128
- package/agent/extensions/pi-web-access/gemini-web-config.ts +101 -101
- package/agent/extensions/pi-web-access/gemini-web.ts +487 -487
- package/agent/extensions/pi-web-access/github-api.ts +197 -197
- package/agent/extensions/pi-web-access/github-extract.ts +746 -746
- package/agent/extensions/pi-web-access/github-issue-pr.ts +700 -700
- package/agent/extensions/pi-web-access/index.ts +1737 -1737
- package/agent/extensions/pi-web-access/package-lock.json +5808 -5808
- package/agent/extensions/pi-web-access/package.json +64 -64
- package/agent/extensions/pi-web-access/page-query.ts +96 -96
- package/agent/extensions/pi-web-access/pdf-extract.ts +409 -409
- package/agent/extensions/pi-web-access/promise-try.d.ts +7 -7
- package/agent/extensions/pi-web-access/query-rewrite.ts +51 -51
- package/agent/extensions/pi-web-access/render-search-error.ts +170 -170
- package/agent/extensions/pi-web-access/rsc-extract.ts +338 -338
- package/agent/extensions/pi-web-access/source-check.ts +282 -282
- package/agent/extensions/pi-web-access/ssrf-protection.ts +526 -526
- package/agent/extensions/pi-web-access/storage.ts +521 -521
- package/agent/extensions/pi-web-access/summary-model-scope.ts +125 -125
- package/agent/extensions/pi-web-access/test/auth-fetch.test.mjs +208 -208
- package/agent/extensions/pi-web-access/test/brightdata-unlocker.test.mjs +840 -840
- package/agent/extensions/pi-web-access/test/chrome-cookie-extraction.test.mjs +441 -441
- package/agent/extensions/pi-web-access/test/config-path.test.mjs +283 -283
- package/agent/extensions/pi-web-access/test/content-find.test.mjs +25 -25
- package/agent/extensions/pi-web-access/test/credential-source.test.mjs +118 -118
- package/agent/extensions/pi-web-access/test/data-uri-sanitize.test.mjs +210 -210
- package/agent/extensions/pi-web-access/test/datalab-pdf-extract.test.mjs +552 -552
- package/agent/extensions/pi-web-access/test/declared-web-links.test.mjs +212 -212
- package/agent/extensions/pi-web-access/test/fetch-answer-storage.test.mjs +40 -40
- package/agent/extensions/pi-web-access/test/fetch-cache-storage.test.mjs +334 -334
- package/agent/extensions/pi-web-access/test/fetch-content-domain-policy.test.mjs +95 -95
- package/agent/extensions/pi-web-access/test/fetch-modes.test.mjs +53 -53
- package/agent/extensions/pi-web-access/test/fetch-not-found-guidance.test.mjs +92 -92
- package/agent/extensions/pi-web-access/test/fetch-params.test.mjs +86 -86
- package/agent/extensions/pi-web-access/test/fetch-render-call.test.mjs +34 -34
- package/agent/extensions/pi-web-access/test/fetch-routing.test.mjs +173 -173
- package/agent/extensions/pi-web-access/test/gemini-adc-auth.test.mjs +257 -257
- package/agent/extensions/pi-web-access/test/gemini-api-transport.test.mjs +170 -170
- package/agent/extensions/pi-web-access/test/gemini-pdf-extract.test.mjs +133 -133
- package/agent/extensions/pi-web-access/test/gemini-web-cookie-opt-in.test.mjs +178 -178
- package/agent/extensions/pi-web-access/test/gemini-web-header-overflow.test.mjs +148 -148
- package/agent/extensions/pi-web-access/test/get-search-content.test.mjs +223 -223
- package/agent/extensions/pi-web-access/test/github-extract.test.mjs +378 -378
- package/agent/extensions/pi-web-access/test/github-issue-pr.test.mjs +565 -565
- package/agent/extensions/pi-web-access/test/inline-content-config.test.mjs +99 -99
- package/agent/extensions/pi-web-access/test/lazy-extract-load.test.mjs +118 -118
- package/agent/extensions/pi-web-access/test/local-video-oversize.test.mjs +52 -52
- package/agent/extensions/pi-web-access/test/package-typebox-dependency.test.mjs +50 -50
- package/agent/extensions/pi-web-access/test/page-query.test.mjs +51 -51
- package/agent/extensions/pi-web-access/test/pdf-config.test.mjs +140 -140
- package/agent/extensions/pi-web-access/test/pdf-extract.test.mjs +500 -500
- package/agent/extensions/pi-web-access/test/proxy-transport.test.mjs +286 -286
- package/agent/extensions/pi-web-access/test/query-rewrite.test.mjs +52 -52
- package/agent/extensions/pi-web-access/test/rsc-fallback.test.mjs +102 -102
- package/agent/extensions/pi-web-access/test/search-error-render.test.mjs +152 -152
- package/agent/extensions/pi-web-access/test/search-providers.test.mjs +274 -274
- package/agent/extensions/pi-web-access/test/source-check.test.mjs +179 -179
- package/agent/extensions/pi-web-access/test/ssrf-allow-ranges-config.test.mjs +205 -205
- package/agent/extensions/pi-web-access/test/ssrf-protection.test.mjs +456 -456
- package/agent/extensions/pi-web-access/test/tool-registration-config.test.mjs +182 -182
- package/agent/extensions/pi-web-access/test/youtube-extract-errors.test.mjs +64 -64
- package/agent/extensions/pi-web-access/tsconfig.json +11 -11
- package/agent/extensions/pi-web-access/utils.ts +451 -451
- package/agent/extensions/pi-web-access/video-extract.ts +392 -392
- package/agent/extensions/pi-web-access/youtube-extract.ts +328 -328
- package/agent/extensions/shared/activity-status.ts +31 -31
- package/agent/extensions/shared/child-session.test.ts +270 -270
- package/agent/extensions/shared/child-session.ts +148 -148
- package/agent/extensions/shared/context-utilization.test.ts +48 -48
- package/agent/extensions/shared/context-utilization.ts +47 -47
- package/agent/extensions/shared/dashboard-state.ts +99 -99
- package/agent/extensions/shared/tool-call-timeout.test.ts +117 -117
- package/agent/extensions/shared/tool-call-timeout.ts +104 -104
- package/agent/extensions/subagents/by-the-way.test.ts +29 -29
- package/agent/extensions/subagents/claude.test.ts +119 -119
- package/agent/extensions/subagents/codex.test.ts +102 -102
- package/agent/extensions/subagents/context-usage.test.ts +107 -107
- package/agent/extensions/subagents/docs/design-plan.md +568 -568
- package/agent/extensions/subagents/docs/effect-v4-extension-guide.md +354 -354
- package/agent/extensions/subagents/docs/effect-v4-notes.md +571 -571
- package/agent/extensions/subagents/index.ts +779 -779
- package/agent/extensions/subagents/manager.test.ts +276 -276
- package/agent/extensions/subagents/package-lock.json +2244 -2244
- package/agent/extensions/subagents/package.json +19 -19
- package/agent/extensions/subagents/result-delivery.test.ts +27 -27
- package/agent/extensions/subagents/src/backend.ts +73 -73
- package/agent/extensions/subagents/src/backends/claude.ts +701 -701
- package/agent/extensions/subagents/src/backends/codex.ts +1060 -1060
- package/agent/extensions/subagents/src/backends/pi.ts +575 -575
- package/agent/extensions/subagents/src/backends/stub.ts +300 -300
- package/agent/extensions/subagents/src/by-the-way.ts +21 -21
- package/agent/extensions/subagents/src/domain.ts +253 -253
- package/agent/extensions/subagents/src/format.ts +74 -74
- package/agent/extensions/subagents/src/manager.ts +736 -736
- package/agent/extensions/subagents/src/prompt.ts +92 -92
- package/agent/extensions/subagents/src/result-delivery.ts +20 -20
- package/agent/extensions/subagents/src/runtime.ts +53 -53
- package/agent/extensions/subagents/src/ui/takeover.ts +583 -583
- package/agent/extensions/subagents/src/ui/transcript.ts +201 -201
- package/agent/extensions/subagents/takeover.test.ts +29 -29
- package/agent/extensions/subagents/tsconfig.json +7 -7
- package/agent/extensions/todo/AGENTS.md +38 -38
- package/agent/extensions/todo/LICENSE +21 -21
- package/agent/extensions/todo/config.ts +55 -55
- package/agent/extensions/todo/index.ts +151 -151
- package/agent/extensions/todo/locales/de.json +17 -17
- package/agent/extensions/todo/locales/en.json +15 -15
- package/agent/extensions/todo/locales/es.json +17 -17
- package/agent/extensions/todo/locales/fr.json +17 -17
- package/agent/extensions/todo/locales/pt-BR.json +17 -17
- package/agent/extensions/todo/locales/pt.json +17 -17
- package/agent/extensions/todo/locales/ru.json +17 -17
- package/agent/extensions/todo/locales/uk.json +17 -17
- package/agent/extensions/todo/locales/zh.json +17 -17
- package/agent/extensions/todo/package-lock.json +3358 -3358
- package/agent/extensions/todo/package.json +67 -67
- package/agent/extensions/todo/state/i18n-bridge.ts +64 -64
- package/agent/extensions/todo/state/invariants.ts +20 -20
- package/agent/extensions/todo/state/replay.ts +38 -38
- package/agent/extensions/todo/state/selectors.ts +107 -107
- package/agent/extensions/todo/state/state-reducer.ts +326 -326
- package/agent/extensions/todo/state/state.ts +18 -18
- package/agent/extensions/todo/state/store.ts +82 -82
- package/agent/extensions/todo/state/task-graph.ts +57 -57
- package/agent/extensions/todo/todo-overlay.ts +200 -200
- package/agent/extensions/todo/todo.ts +155 -155
- package/agent/extensions/todo/tool/response-envelope.ts +109 -109
- package/agent/extensions/todo/tool/types.ts +206 -206
- package/agent/extensions/todo/view/format.ts +177 -177
- package/agent/install.ps1 +637 -527
- package/agent/install.sh +620 -511
- package/agent/keybindings.json +7 -7
- package/bin/kiln.js +124 -11
- package/package.json +8 -2
- package/agent/extensions/taste/index.ts +0 -443
- package/agent/extensions/taste/install.ps1 +0 -23
- package/agent/extensions/taste/install.sh +0 -21
- /package/agent/extensions/{status line → statusline}/index.ts +0 -0
- /package/agent/extensions/{status line → statusline}/install.ps1 +0 -0
- /package/agent/extensions/{status line → statusline}/install.sh +0 -0
|
@@ -1,443 +1,443 @@
|
|
|
1
|
-
import * as NodeServices from "@effect/platform-node/NodeServices";
|
|
2
|
-
import { assert, it } from "@effect/vitest";
|
|
3
|
-
import { homedir } from "node:os";
|
|
4
|
-
import { dirname, join } from "node:path";
|
|
5
|
-
import { Effect, FileSystem } from "effect";
|
|
6
|
-
import { HttpClientRequest, HttpClientResponse } from "effect/unstable/http";
|
|
7
|
-
import {
|
|
8
|
-
buildFdArgs,
|
|
9
|
-
buildRgArgs,
|
|
10
|
-
FD_DEFAULT_LIMIT,
|
|
11
|
-
normalizeSearchPath,
|
|
12
|
-
} from "./src/args.ts";
|
|
13
|
-
import {
|
|
14
|
-
FD_INTEL_DARWIN_VERSION,
|
|
15
|
-
InstallError,
|
|
16
|
-
readBoundedResponse,
|
|
17
|
-
releaseAsset,
|
|
18
|
-
resolveBinary,
|
|
19
|
-
TOOL_SPECS,
|
|
20
|
-
UnsupportedPlatformError,
|
|
21
|
-
type BinaryEnv,
|
|
22
|
-
type ReleaseAsset,
|
|
23
|
-
type ResolvedBinary,
|
|
24
|
-
} from "./src/binaries.ts";
|
|
25
|
-
import { formatCapturedOutput, formatOutput } from "./src/output.ts";
|
|
26
|
-
import { executeSearchProcess } from "./src/process.ts";
|
|
27
|
-
import { installNotifications, makeBinaryInitializers } from "./index.ts";
|
|
28
|
-
|
|
29
|
-
// --- argument construction -------------------------------------------------
|
|
30
|
-
|
|
31
|
-
it("fd args: defaults list everything with the default limit", () => {
|
|
32
|
-
assert.deepEqual(buildFdArgs({}), [
|
|
33
|
-
"--color=never",
|
|
34
|
-
"--max-results",
|
|
35
|
-
String(FD_DEFAULT_LIMIT),
|
|
36
|
-
"--",
|
|
37
|
-
"",
|
|
38
|
-
]);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it("fd args: all options are translated and pattern stays behind --", () => {
|
|
42
|
-
const args = buildFdArgs({
|
|
43
|
-
pattern: "-rf",
|
|
44
|
-
path: "@src",
|
|
45
|
-
type: "file",
|
|
46
|
-
extension: ".ts",
|
|
47
|
-
glob: true,
|
|
48
|
-
hidden: true,
|
|
49
|
-
max_depth: 3,
|
|
50
|
-
limit: 50,
|
|
51
|
-
});
|
|
52
|
-
assert.deepEqual(args, [
|
|
53
|
-
"--color=never",
|
|
54
|
-
"--hidden",
|
|
55
|
-
"--glob",
|
|
56
|
-
"--type",
|
|
57
|
-
"f",
|
|
58
|
-
"--extension",
|
|
59
|
-
"ts",
|
|
60
|
-
"--max-depth",
|
|
61
|
-
"3",
|
|
62
|
-
"--max-results",
|
|
63
|
-
"50",
|
|
64
|
-
"--",
|
|
65
|
-
"-rf",
|
|
66
|
-
"src",
|
|
67
|
-
]);
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it("fd args: out-of-range values are clamped", () => {
|
|
71
|
-
const args = buildFdArgs({ max_depth: 500, limit: 1_000_000 });
|
|
72
|
-
assert.deepEqual(args, [
|
|
73
|
-
"--color=never",
|
|
74
|
-
"--max-depth",
|
|
75
|
-
"64",
|
|
76
|
-
"--max-results",
|
|
77
|
-
"10000",
|
|
78
|
-
"--",
|
|
79
|
-
"",
|
|
80
|
-
]);
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it("rg args: defaults use smart-case and safe separators", () => {
|
|
84
|
-
assert.deepEqual(buildRgArgs({ pattern: "--help" }), [
|
|
85
|
-
"--line-number",
|
|
86
|
-
"--color=never",
|
|
87
|
-
"--no-heading",
|
|
88
|
-
"--with-filename",
|
|
89
|
-
"--smart-case",
|
|
90
|
-
"--max-count",
|
|
91
|
-
"100",
|
|
92
|
-
"--",
|
|
93
|
-
"--help",
|
|
94
|
-
]);
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it("rg args: all options are translated", () => {
|
|
98
|
-
const args = buildRgArgs({
|
|
99
|
-
pattern: "TODO",
|
|
100
|
-
path: "@lib",
|
|
101
|
-
glob: "*.ts",
|
|
102
|
-
file_type: "ts",
|
|
103
|
-
case_sensitive: true,
|
|
104
|
-
fixed_strings: true,
|
|
105
|
-
hidden: true,
|
|
106
|
-
context: 2,
|
|
107
|
-
limit: 10,
|
|
108
|
-
});
|
|
109
|
-
assert.deepEqual(args, [
|
|
110
|
-
"--line-number",
|
|
111
|
-
"--color=never",
|
|
112
|
-
"--no-heading",
|
|
113
|
-
"--with-filename",
|
|
114
|
-
"--case-sensitive",
|
|
115
|
-
"--fixed-strings",
|
|
116
|
-
"--hidden",
|
|
117
|
-
"--context",
|
|
118
|
-
"2",
|
|
119
|
-
"--glob",
|
|
120
|
-
"*.ts",
|
|
121
|
-
"--type",
|
|
122
|
-
"ts",
|
|
123
|
-
"--max-count",
|
|
124
|
-
"10",
|
|
125
|
-
"--",
|
|
126
|
-
"TODO",
|
|
127
|
-
"lib",
|
|
128
|
-
]);
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
it("rg args: case_sensitive false forces ignore-case", () => {
|
|
132
|
-
const args = buildRgArgs({ pattern: "x", case_sensitive: false });
|
|
133
|
-
assert.isTrue(args.includes("--ignore-case"));
|
|
134
|
-
assert.isFalse(args.includes("--smart-case"));
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
it("path normalization strips leading @ and expands ~", () => {
|
|
138
|
-
assert.equal(normalizeSearchPath("@src/lib"), "src/lib");
|
|
139
|
-
assert.equal(normalizeSearchPath("~"), homedir());
|
|
140
|
-
assert.equal(normalizeSearchPath("~/projects"), join(homedir(), "projects"));
|
|
141
|
-
assert.equal(normalizeSearchPath(" plain "), "plain");
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
// --- binary resolution -----------------------------------------------------
|
|
145
|
-
|
|
146
|
-
function makeEnv(options: {
|
|
147
|
-
available?: string[];
|
|
148
|
-
installShouldFail?: boolean;
|
|
149
|
-
}): BinaryEnv & { installs: ReleaseAsset[]; probes: string[] } {
|
|
150
|
-
const installs: ReleaseAsset[] = [];
|
|
151
|
-
const probes: string[] = [];
|
|
152
|
-
const installed = new Set<string>();
|
|
153
|
-
return {
|
|
154
|
-
installs,
|
|
155
|
-
probes,
|
|
156
|
-
probe: (command) =>
|
|
157
|
-
Effect.sync(() => {
|
|
158
|
-
probes.push(command);
|
|
159
|
-
return (
|
|
160
|
-
(options.available ?? []).includes(command) || installed.has(command)
|
|
161
|
-
);
|
|
162
|
-
}),
|
|
163
|
-
install: (asset, destination) => {
|
|
164
|
-
if (options.installShouldFail) {
|
|
165
|
-
return Effect.fail(new InstallError({ message: "network down" }));
|
|
166
|
-
}
|
|
167
|
-
return Effect.sync(() => {
|
|
168
|
-
installs.push(asset);
|
|
169
|
-
installed.add(destination);
|
|
170
|
-
});
|
|
171
|
-
},
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
const darwinArm = { os: "darwin", arch: "arm64" } as const;
|
|
176
|
-
|
|
177
|
-
it.effect("binary resolution: system fd wins and nothing is installed", () =>
|
|
178
|
-
Effect.gen(function* () {
|
|
179
|
-
const env = makeEnv({ available: ["fd"] });
|
|
180
|
-
const resolved = yield* resolveBinary(
|
|
181
|
-
TOOL_SPECS.fd,
|
|
182
|
-
"/repo/bin",
|
|
183
|
-
darwinArm,
|
|
184
|
-
env,
|
|
185
|
-
);
|
|
186
|
-
|
|
187
|
-
assert.deepEqual(resolved, {
|
|
188
|
-
tool: "fd",
|
|
189
|
-
command: "fd",
|
|
190
|
-
source: "system",
|
|
191
|
-
});
|
|
192
|
-
assert.equal(env.installs.length, 0);
|
|
193
|
-
}),
|
|
194
|
-
);
|
|
195
|
-
|
|
196
|
-
it.effect("binary resolution: fdfind is accepted as a system fd", () =>
|
|
197
|
-
Effect.gen(function* () {
|
|
198
|
-
const env = makeEnv({ available: ["fdfind"] });
|
|
199
|
-
const resolved = yield* resolveBinary(
|
|
200
|
-
TOOL_SPECS.fd,
|
|
201
|
-
"/repo/bin",
|
|
202
|
-
darwinArm,
|
|
203
|
-
env,
|
|
204
|
-
);
|
|
205
|
-
|
|
206
|
-
assert.deepEqual(resolved, {
|
|
207
|
-
tool: "fd",
|
|
208
|
-
command: "fdfind",
|
|
209
|
-
source: "system",
|
|
210
|
-
});
|
|
211
|
-
assert.equal(env.installs.length, 0);
|
|
212
|
-
}),
|
|
213
|
-
);
|
|
214
|
-
|
|
215
|
-
it.effect("binary resolution: existing bin fallback is used silently", () =>
|
|
216
|
-
Effect.gen(function* () {
|
|
217
|
-
const env = makeEnv({ available: ["/repo/bin/rg"] });
|
|
218
|
-
const resolved = yield* resolveBinary(
|
|
219
|
-
TOOL_SPECS.rg,
|
|
220
|
-
"/repo/bin",
|
|
221
|
-
darwinArm,
|
|
222
|
-
env,
|
|
223
|
-
);
|
|
224
|
-
|
|
225
|
-
assert.deepEqual(resolved, {
|
|
226
|
-
tool: "rg",
|
|
227
|
-
command: "/repo/bin/rg",
|
|
228
|
-
source: "bundled",
|
|
229
|
-
});
|
|
230
|
-
assert.equal(env.installs.length, 0);
|
|
231
|
-
}),
|
|
232
|
-
);
|
|
233
|
-
|
|
234
|
-
it.effect(
|
|
235
|
-
"binary resolution: missing everywhere triggers exactly one install",
|
|
236
|
-
() =>
|
|
237
|
-
Effect.gen(function* () {
|
|
238
|
-
const env = makeEnv({ available: [] });
|
|
239
|
-
const resolved = yield* resolveBinary(
|
|
240
|
-
TOOL_SPECS.rg,
|
|
241
|
-
"/repo/bin",
|
|
242
|
-
darwinArm,
|
|
243
|
-
env,
|
|
244
|
-
);
|
|
245
|
-
|
|
246
|
-
assert.equal(resolved.source, "installed");
|
|
247
|
-
assert.equal(resolved.command, "/repo/bin/rg");
|
|
248
|
-
assert.equal(env.installs.length, 1);
|
|
249
|
-
assert.match(
|
|
250
|
-
env.installs[0].url,
|
|
251
|
-
/^https:\/\/github\.com\/BurntSushi\/ripgrep\//,
|
|
252
|
-
);
|
|
253
|
-
}),
|
|
254
|
-
);
|
|
255
|
-
|
|
256
|
-
it.effect("binary resolution: install failure surfaces a typed error", () =>
|
|
257
|
-
Effect.gen(function* () {
|
|
258
|
-
const env = makeEnv({ available: [], installShouldFail: true });
|
|
259
|
-
const error = yield* Effect.flip(
|
|
260
|
-
resolveBinary(TOOL_SPECS.fd, "/repo/bin", darwinArm, env),
|
|
261
|
-
);
|
|
262
|
-
|
|
263
|
-
assert.instanceOf(error, InstallError);
|
|
264
|
-
assert.equal(error.message, "network down");
|
|
265
|
-
}),
|
|
266
|
-
);
|
|
267
|
-
|
|
268
|
-
it.effect(
|
|
269
|
-
"binary resolution: unsupported platform fails without installing",
|
|
270
|
-
() =>
|
|
271
|
-
Effect.gen(function* () {
|
|
272
|
-
const env = makeEnv({ available: [] });
|
|
273
|
-
const error = yield* Effect.flip(
|
|
274
|
-
resolveBinary(
|
|
275
|
-
TOOL_SPECS.fd,
|
|
276
|
-
"/repo/bin",
|
|
277
|
-
{ os: "linux", arch: "s390x" },
|
|
278
|
-
env,
|
|
279
|
-
),
|
|
280
|
-
);
|
|
281
|
-
|
|
282
|
-
assert.instanceOf(error, UnsupportedPlatformError);
|
|
283
|
-
assert.equal(env.installs.length, 0);
|
|
284
|
-
}),
|
|
285
|
-
);
|
|
286
|
-
|
|
287
|
-
it.effect("binary resolution: one failed tool does not disable the other", () =>
|
|
288
|
-
Effect.gen(function* () {
|
|
289
|
-
const env = makeEnv({
|
|
290
|
-
available: ["rg"],
|
|
291
|
-
installShouldFail: true,
|
|
292
|
-
});
|
|
293
|
-
const initializers = makeBinaryInitializers("/repo/bin", darwinArm, env);
|
|
294
|
-
|
|
295
|
-
const fdError = yield* Effect.flip(initializers.fd);
|
|
296
|
-
const rg = yield* initializers.rg;
|
|
297
|
-
|
|
298
|
-
assert.instanceOf(fdError, InstallError);
|
|
299
|
-
assert.deepEqual(rg, { tool: "rg", command: "rg", source: "system" });
|
|
300
|
-
}),
|
|
301
|
-
);
|
|
302
|
-
|
|
303
|
-
it("release assets cover macOS and Linux on arm64 and x64 over HTTPS", () => {
|
|
304
|
-
for (const os of ["darwin", "linux"] as const) {
|
|
305
|
-
for (const arch of ["arm64", "x64"] as const) {
|
|
306
|
-
for (const tool of ["fd", "rg"] as const) {
|
|
307
|
-
const asset = releaseAsset(tool, { os, arch });
|
|
308
|
-
assert.isDefined(asset, `${tool} ${os}/${arch}`);
|
|
309
|
-
assert.match(asset.url, /^https:\/\//);
|
|
310
|
-
assert.isTrue(asset.url.endsWith(asset.fileName));
|
|
311
|
-
assert.match(asset.sha256, /^[a-f0-9]{64}$/);
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
});
|
|
316
|
-
|
|
317
|
-
it("linux assets use statically linked musl builds", () => {
|
|
318
|
-
const asset = releaseAsset("fd", { os: "linux", arch: "x64" });
|
|
319
|
-
assert.isTrue(asset?.url.includes("unknown-linux-musl"));
|
|
320
|
-
});
|
|
321
|
-
|
|
322
|
-
it("Intel macOS uses the latest fd release that publishes that target", () => {
|
|
323
|
-
const asset = releaseAsset("fd", { os: "darwin", arch: "x64" });
|
|
324
|
-
assert.equal(asset?.version, FD_INTEL_DARWIN_VERSION);
|
|
325
|
-
});
|
|
326
|
-
|
|
327
|
-
it.effect(
|
|
328
|
-
"bounded downloads reject oversized declared and streamed bodies",
|
|
329
|
-
() =>
|
|
330
|
-
Effect.gen(function* () {
|
|
331
|
-
const request = HttpClientRequest.get(
|
|
332
|
-
"https://example.com/archive.tar.gz",
|
|
333
|
-
);
|
|
334
|
-
const declared = HttpClientResponse.fromWeb(
|
|
335
|
-
request,
|
|
336
|
-
new Response("small", {
|
|
337
|
-
headers: { "content-length": "100" },
|
|
338
|
-
}),
|
|
339
|
-
);
|
|
340
|
-
const declaredError = yield* Effect.flip(
|
|
341
|
-
readBoundedResponse(declared, 10),
|
|
342
|
-
);
|
|
343
|
-
assert.match(declaredError.message, /size limit/);
|
|
344
|
-
|
|
345
|
-
const streamed = HttpClientResponse.fromWeb(
|
|
346
|
-
request,
|
|
347
|
-
new Response("this body is too large"),
|
|
348
|
-
);
|
|
349
|
-
const streamedError = yield* Effect.flip(
|
|
350
|
-
readBoundedResponse(streamed, 5),
|
|
351
|
-
);
|
|
352
|
-
assert.match(streamedError.message, /size limit/);
|
|
353
|
-
}),
|
|
354
|
-
);
|
|
355
|
-
|
|
356
|
-
// --- notification policy ----------------------------------------------------
|
|
357
|
-
|
|
358
|
-
it("notifications: only fresh installs notify", () => {
|
|
359
|
-
const system: ResolvedBinary = {
|
|
360
|
-
tool: "fd",
|
|
361
|
-
command: "fd",
|
|
362
|
-
source: "system",
|
|
363
|
-
};
|
|
364
|
-
const bundled: ResolvedBinary = {
|
|
365
|
-
tool: "rg",
|
|
366
|
-
command: "/repo/bin/rg",
|
|
367
|
-
source: "bundled",
|
|
368
|
-
};
|
|
369
|
-
const installed: ResolvedBinary = {
|
|
370
|
-
tool: "rg",
|
|
371
|
-
command: "/repo/bin/rg",
|
|
372
|
-
source: "installed",
|
|
373
|
-
version: "15.2.0",
|
|
374
|
-
};
|
|
375
|
-
|
|
376
|
-
assert.deepEqual(installNotifications([system, bundled]), []);
|
|
377
|
-
const messages = installNotifications([system, installed]);
|
|
378
|
-
assert.equal(messages.length, 1);
|
|
379
|
-
assert.match(messages[0], /downloaded rg 15\.2\.0/);
|
|
380
|
-
});
|
|
381
|
-
|
|
382
|
-
// --- output truncation -------------------------------------------------------
|
|
383
|
-
|
|
384
|
-
it.effect("process output is streamed to a complete spill file", () =>
|
|
385
|
-
Effect.gen(function* () {
|
|
386
|
-
const result = yield* executeSearchProcess({
|
|
387
|
-
command: process.execPath,
|
|
388
|
-
args: ["-e", 'process.stdout.write("line\\n".repeat(3000))'],
|
|
389
|
-
cwd: process.cwd(),
|
|
390
|
-
tempPrefix: "pi-search-test-",
|
|
391
|
-
});
|
|
392
|
-
const formatted = formatCapturedOutput(result.output);
|
|
393
|
-
|
|
394
|
-
assert.equal(result.code, 0);
|
|
395
|
-
assert.isTrue(formatted.truncated);
|
|
396
|
-
assert.equal(formatted.lineCount, 3000);
|
|
397
|
-
assert.match(formatted.text, /2000 of 3000 lines/);
|
|
398
|
-
assert.isDefined(formatted.fullOutputPath);
|
|
399
|
-
|
|
400
|
-
const fs = yield* FileSystem.FileSystem;
|
|
401
|
-
const fullOutput = yield* fs.readFileString(formatted.fullOutputPath);
|
|
402
|
-
assert.equal(fullOutput, "line\n".repeat(3000));
|
|
403
|
-
yield* fs.remove(dirname(formatted.fullOutputPath), {
|
|
404
|
-
recursive: true,
|
|
405
|
-
force: true,
|
|
406
|
-
});
|
|
407
|
-
}).pipe(Effect.provide(NodeServices.layer)),
|
|
408
|
-
);
|
|
409
|
-
|
|
410
|
-
it("output: small results pass through untouched", async () => {
|
|
411
|
-
const formatted = await formatOutput("a.ts\nb.ts\n", {
|
|
412
|
-
tempPrefix: "pi-fd-",
|
|
413
|
-
persistFullOutput: () => Promise.reject(new Error("should not persist")),
|
|
414
|
-
});
|
|
415
|
-
assert.equal(formatted.text, "a.ts\nb.ts");
|
|
416
|
-
assert.equal(formatted.lineCount, 2);
|
|
417
|
-
assert.isFalse(formatted.truncated);
|
|
418
|
-
assert.isUndefined(formatted.fullOutputPath);
|
|
419
|
-
});
|
|
420
|
-
|
|
421
|
-
it("output: oversized results are truncated and persisted", async () => {
|
|
422
|
-
const bigOutput = Array.from({ length: 3000 }, (_, i) => `file-${i}.ts`).join(
|
|
423
|
-
"\n",
|
|
424
|
-
);
|
|
425
|
-
let persisted: string | undefined;
|
|
426
|
-
const formatted = await formatOutput(bigOutput, {
|
|
427
|
-
tempPrefix: "pi-fd-",
|
|
428
|
-
persistFullOutput: async (full) => {
|
|
429
|
-
persisted = full;
|
|
430
|
-
return "/tmp/fake/output.txt";
|
|
431
|
-
},
|
|
432
|
-
});
|
|
433
|
-
assert.isTrue(formatted.truncated);
|
|
434
|
-
assert.equal(formatted.fullOutputPath, "/tmp/fake/output.txt");
|
|
435
|
-
assert.equal(persisted, bigOutput);
|
|
436
|
-
assert.match(formatted.text, /\[Output truncated: 2000 of 3000 lines/);
|
|
437
|
-
assert.match(
|
|
438
|
-
formatted.text,
|
|
439
|
-
/Full output saved to: \/tmp\/fake\/output\.txt\]/,
|
|
440
|
-
);
|
|
441
|
-
const shownLines = formatted.text.split("\n");
|
|
442
|
-
assert.equal(shownLines[0], "file-0.ts");
|
|
443
|
-
});
|
|
1
|
+
import * as NodeServices from "@effect/platform-node/NodeServices";
|
|
2
|
+
import { assert, it } from "@effect/vitest";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { Effect, FileSystem } from "effect";
|
|
6
|
+
import { HttpClientRequest, HttpClientResponse } from "effect/unstable/http";
|
|
7
|
+
import {
|
|
8
|
+
buildFdArgs,
|
|
9
|
+
buildRgArgs,
|
|
10
|
+
FD_DEFAULT_LIMIT,
|
|
11
|
+
normalizeSearchPath,
|
|
12
|
+
} from "./src/args.ts";
|
|
13
|
+
import {
|
|
14
|
+
FD_INTEL_DARWIN_VERSION,
|
|
15
|
+
InstallError,
|
|
16
|
+
readBoundedResponse,
|
|
17
|
+
releaseAsset,
|
|
18
|
+
resolveBinary,
|
|
19
|
+
TOOL_SPECS,
|
|
20
|
+
UnsupportedPlatformError,
|
|
21
|
+
type BinaryEnv,
|
|
22
|
+
type ReleaseAsset,
|
|
23
|
+
type ResolvedBinary,
|
|
24
|
+
} from "./src/binaries.ts";
|
|
25
|
+
import { formatCapturedOutput, formatOutput } from "./src/output.ts";
|
|
26
|
+
import { executeSearchProcess } from "./src/process.ts";
|
|
27
|
+
import { installNotifications, makeBinaryInitializers } from "./index.ts";
|
|
28
|
+
|
|
29
|
+
// --- argument construction -------------------------------------------------
|
|
30
|
+
|
|
31
|
+
it("fd args: defaults list everything with the default limit", () => {
|
|
32
|
+
assert.deepEqual(buildFdArgs({}), [
|
|
33
|
+
"--color=never",
|
|
34
|
+
"--max-results",
|
|
35
|
+
String(FD_DEFAULT_LIMIT),
|
|
36
|
+
"--",
|
|
37
|
+
"",
|
|
38
|
+
]);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("fd args: all options are translated and pattern stays behind --", () => {
|
|
42
|
+
const args = buildFdArgs({
|
|
43
|
+
pattern: "-rf",
|
|
44
|
+
path: "@src",
|
|
45
|
+
type: "file",
|
|
46
|
+
extension: ".ts",
|
|
47
|
+
glob: true,
|
|
48
|
+
hidden: true,
|
|
49
|
+
max_depth: 3,
|
|
50
|
+
limit: 50,
|
|
51
|
+
});
|
|
52
|
+
assert.deepEqual(args, [
|
|
53
|
+
"--color=never",
|
|
54
|
+
"--hidden",
|
|
55
|
+
"--glob",
|
|
56
|
+
"--type",
|
|
57
|
+
"f",
|
|
58
|
+
"--extension",
|
|
59
|
+
"ts",
|
|
60
|
+
"--max-depth",
|
|
61
|
+
"3",
|
|
62
|
+
"--max-results",
|
|
63
|
+
"50",
|
|
64
|
+
"--",
|
|
65
|
+
"-rf",
|
|
66
|
+
"src",
|
|
67
|
+
]);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("fd args: out-of-range values are clamped", () => {
|
|
71
|
+
const args = buildFdArgs({ max_depth: 500, limit: 1_000_000 });
|
|
72
|
+
assert.deepEqual(args, [
|
|
73
|
+
"--color=never",
|
|
74
|
+
"--max-depth",
|
|
75
|
+
"64",
|
|
76
|
+
"--max-results",
|
|
77
|
+
"10000",
|
|
78
|
+
"--",
|
|
79
|
+
"",
|
|
80
|
+
]);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("rg args: defaults use smart-case and safe separators", () => {
|
|
84
|
+
assert.deepEqual(buildRgArgs({ pattern: "--help" }), [
|
|
85
|
+
"--line-number",
|
|
86
|
+
"--color=never",
|
|
87
|
+
"--no-heading",
|
|
88
|
+
"--with-filename",
|
|
89
|
+
"--smart-case",
|
|
90
|
+
"--max-count",
|
|
91
|
+
"100",
|
|
92
|
+
"--",
|
|
93
|
+
"--help",
|
|
94
|
+
]);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("rg args: all options are translated", () => {
|
|
98
|
+
const args = buildRgArgs({
|
|
99
|
+
pattern: "TODO",
|
|
100
|
+
path: "@lib",
|
|
101
|
+
glob: "*.ts",
|
|
102
|
+
file_type: "ts",
|
|
103
|
+
case_sensitive: true,
|
|
104
|
+
fixed_strings: true,
|
|
105
|
+
hidden: true,
|
|
106
|
+
context: 2,
|
|
107
|
+
limit: 10,
|
|
108
|
+
});
|
|
109
|
+
assert.deepEqual(args, [
|
|
110
|
+
"--line-number",
|
|
111
|
+
"--color=never",
|
|
112
|
+
"--no-heading",
|
|
113
|
+
"--with-filename",
|
|
114
|
+
"--case-sensitive",
|
|
115
|
+
"--fixed-strings",
|
|
116
|
+
"--hidden",
|
|
117
|
+
"--context",
|
|
118
|
+
"2",
|
|
119
|
+
"--glob",
|
|
120
|
+
"*.ts",
|
|
121
|
+
"--type",
|
|
122
|
+
"ts",
|
|
123
|
+
"--max-count",
|
|
124
|
+
"10",
|
|
125
|
+
"--",
|
|
126
|
+
"TODO",
|
|
127
|
+
"lib",
|
|
128
|
+
]);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("rg args: case_sensitive false forces ignore-case", () => {
|
|
132
|
+
const args = buildRgArgs({ pattern: "x", case_sensitive: false });
|
|
133
|
+
assert.isTrue(args.includes("--ignore-case"));
|
|
134
|
+
assert.isFalse(args.includes("--smart-case"));
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("path normalization strips leading @ and expands ~", () => {
|
|
138
|
+
assert.equal(normalizeSearchPath("@src/lib"), "src/lib");
|
|
139
|
+
assert.equal(normalizeSearchPath("~"), homedir());
|
|
140
|
+
assert.equal(normalizeSearchPath("~/projects"), join(homedir(), "projects"));
|
|
141
|
+
assert.equal(normalizeSearchPath(" plain "), "plain");
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// --- binary resolution -----------------------------------------------------
|
|
145
|
+
|
|
146
|
+
function makeEnv(options: {
|
|
147
|
+
available?: string[];
|
|
148
|
+
installShouldFail?: boolean;
|
|
149
|
+
}): BinaryEnv & { installs: ReleaseAsset[]; probes: string[] } {
|
|
150
|
+
const installs: ReleaseAsset[] = [];
|
|
151
|
+
const probes: string[] = [];
|
|
152
|
+
const installed = new Set<string>();
|
|
153
|
+
return {
|
|
154
|
+
installs,
|
|
155
|
+
probes,
|
|
156
|
+
probe: (command) =>
|
|
157
|
+
Effect.sync(() => {
|
|
158
|
+
probes.push(command);
|
|
159
|
+
return (
|
|
160
|
+
(options.available ?? []).includes(command) || installed.has(command)
|
|
161
|
+
);
|
|
162
|
+
}),
|
|
163
|
+
install: (asset, destination) => {
|
|
164
|
+
if (options.installShouldFail) {
|
|
165
|
+
return Effect.fail(new InstallError({ message: "network down" }));
|
|
166
|
+
}
|
|
167
|
+
return Effect.sync(() => {
|
|
168
|
+
installs.push(asset);
|
|
169
|
+
installed.add(destination);
|
|
170
|
+
});
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const darwinArm = { os: "darwin", arch: "arm64" } as const;
|
|
176
|
+
|
|
177
|
+
it.effect("binary resolution: system fd wins and nothing is installed", () =>
|
|
178
|
+
Effect.gen(function* () {
|
|
179
|
+
const env = makeEnv({ available: ["fd"] });
|
|
180
|
+
const resolved = yield* resolveBinary(
|
|
181
|
+
TOOL_SPECS.fd,
|
|
182
|
+
"/repo/bin",
|
|
183
|
+
darwinArm,
|
|
184
|
+
env,
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
assert.deepEqual(resolved, {
|
|
188
|
+
tool: "fd",
|
|
189
|
+
command: "fd",
|
|
190
|
+
source: "system",
|
|
191
|
+
});
|
|
192
|
+
assert.equal(env.installs.length, 0);
|
|
193
|
+
}),
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
it.effect("binary resolution: fdfind is accepted as a system fd", () =>
|
|
197
|
+
Effect.gen(function* () {
|
|
198
|
+
const env = makeEnv({ available: ["fdfind"] });
|
|
199
|
+
const resolved = yield* resolveBinary(
|
|
200
|
+
TOOL_SPECS.fd,
|
|
201
|
+
"/repo/bin",
|
|
202
|
+
darwinArm,
|
|
203
|
+
env,
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
assert.deepEqual(resolved, {
|
|
207
|
+
tool: "fd",
|
|
208
|
+
command: "fdfind",
|
|
209
|
+
source: "system",
|
|
210
|
+
});
|
|
211
|
+
assert.equal(env.installs.length, 0);
|
|
212
|
+
}),
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
it.effect("binary resolution: existing bin fallback is used silently", () =>
|
|
216
|
+
Effect.gen(function* () {
|
|
217
|
+
const env = makeEnv({ available: ["/repo/bin/rg"] });
|
|
218
|
+
const resolved = yield* resolveBinary(
|
|
219
|
+
TOOL_SPECS.rg,
|
|
220
|
+
"/repo/bin",
|
|
221
|
+
darwinArm,
|
|
222
|
+
env,
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
assert.deepEqual(resolved, {
|
|
226
|
+
tool: "rg",
|
|
227
|
+
command: "/repo/bin/rg",
|
|
228
|
+
source: "bundled",
|
|
229
|
+
});
|
|
230
|
+
assert.equal(env.installs.length, 0);
|
|
231
|
+
}),
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
it.effect(
|
|
235
|
+
"binary resolution: missing everywhere triggers exactly one install",
|
|
236
|
+
() =>
|
|
237
|
+
Effect.gen(function* () {
|
|
238
|
+
const env = makeEnv({ available: [] });
|
|
239
|
+
const resolved = yield* resolveBinary(
|
|
240
|
+
TOOL_SPECS.rg,
|
|
241
|
+
"/repo/bin",
|
|
242
|
+
darwinArm,
|
|
243
|
+
env,
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
assert.equal(resolved.source, "installed");
|
|
247
|
+
assert.equal(resolved.command, "/repo/bin/rg");
|
|
248
|
+
assert.equal(env.installs.length, 1);
|
|
249
|
+
assert.match(
|
|
250
|
+
env.installs[0].url,
|
|
251
|
+
/^https:\/\/github\.com\/BurntSushi\/ripgrep\//,
|
|
252
|
+
);
|
|
253
|
+
}),
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
it.effect("binary resolution: install failure surfaces a typed error", () =>
|
|
257
|
+
Effect.gen(function* () {
|
|
258
|
+
const env = makeEnv({ available: [], installShouldFail: true });
|
|
259
|
+
const error = yield* Effect.flip(
|
|
260
|
+
resolveBinary(TOOL_SPECS.fd, "/repo/bin", darwinArm, env),
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
assert.instanceOf(error, InstallError);
|
|
264
|
+
assert.equal(error.message, "network down");
|
|
265
|
+
}),
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
it.effect(
|
|
269
|
+
"binary resolution: unsupported platform fails without installing",
|
|
270
|
+
() =>
|
|
271
|
+
Effect.gen(function* () {
|
|
272
|
+
const env = makeEnv({ available: [] });
|
|
273
|
+
const error = yield* Effect.flip(
|
|
274
|
+
resolveBinary(
|
|
275
|
+
TOOL_SPECS.fd,
|
|
276
|
+
"/repo/bin",
|
|
277
|
+
{ os: "linux", arch: "s390x" },
|
|
278
|
+
env,
|
|
279
|
+
),
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
assert.instanceOf(error, UnsupportedPlatformError);
|
|
283
|
+
assert.equal(env.installs.length, 0);
|
|
284
|
+
}),
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
it.effect("binary resolution: one failed tool does not disable the other", () =>
|
|
288
|
+
Effect.gen(function* () {
|
|
289
|
+
const env = makeEnv({
|
|
290
|
+
available: ["rg"],
|
|
291
|
+
installShouldFail: true,
|
|
292
|
+
});
|
|
293
|
+
const initializers = makeBinaryInitializers("/repo/bin", darwinArm, env);
|
|
294
|
+
|
|
295
|
+
const fdError = yield* Effect.flip(initializers.fd);
|
|
296
|
+
const rg = yield* initializers.rg;
|
|
297
|
+
|
|
298
|
+
assert.instanceOf(fdError, InstallError);
|
|
299
|
+
assert.deepEqual(rg, { tool: "rg", command: "rg", source: "system" });
|
|
300
|
+
}),
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
it("release assets cover macOS and Linux on arm64 and x64 over HTTPS", () => {
|
|
304
|
+
for (const os of ["darwin", "linux"] as const) {
|
|
305
|
+
for (const arch of ["arm64", "x64"] as const) {
|
|
306
|
+
for (const tool of ["fd", "rg"] as const) {
|
|
307
|
+
const asset = releaseAsset(tool, { os, arch });
|
|
308
|
+
assert.isDefined(asset, `${tool} ${os}/${arch}`);
|
|
309
|
+
assert.match(asset.url, /^https:\/\//);
|
|
310
|
+
assert.isTrue(asset.url.endsWith(asset.fileName));
|
|
311
|
+
assert.match(asset.sha256, /^[a-f0-9]{64}$/);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it("linux assets use statically linked musl builds", () => {
|
|
318
|
+
const asset = releaseAsset("fd", { os: "linux", arch: "x64" });
|
|
319
|
+
assert.isTrue(asset?.url.includes("unknown-linux-musl"));
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it("Intel macOS uses the latest fd release that publishes that target", () => {
|
|
323
|
+
const asset = releaseAsset("fd", { os: "darwin", arch: "x64" });
|
|
324
|
+
assert.equal(asset?.version, FD_INTEL_DARWIN_VERSION);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it.effect(
|
|
328
|
+
"bounded downloads reject oversized declared and streamed bodies",
|
|
329
|
+
() =>
|
|
330
|
+
Effect.gen(function* () {
|
|
331
|
+
const request = HttpClientRequest.get(
|
|
332
|
+
"https://example.com/archive.tar.gz",
|
|
333
|
+
);
|
|
334
|
+
const declared = HttpClientResponse.fromWeb(
|
|
335
|
+
request,
|
|
336
|
+
new Response("small", {
|
|
337
|
+
headers: { "content-length": "100" },
|
|
338
|
+
}),
|
|
339
|
+
);
|
|
340
|
+
const declaredError = yield* Effect.flip(
|
|
341
|
+
readBoundedResponse(declared, 10),
|
|
342
|
+
);
|
|
343
|
+
assert.match(declaredError.message, /size limit/);
|
|
344
|
+
|
|
345
|
+
const streamed = HttpClientResponse.fromWeb(
|
|
346
|
+
request,
|
|
347
|
+
new Response("this body is too large"),
|
|
348
|
+
);
|
|
349
|
+
const streamedError = yield* Effect.flip(
|
|
350
|
+
readBoundedResponse(streamed, 5),
|
|
351
|
+
);
|
|
352
|
+
assert.match(streamedError.message, /size limit/);
|
|
353
|
+
}),
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
// --- notification policy ----------------------------------------------------
|
|
357
|
+
|
|
358
|
+
it("notifications: only fresh installs notify", () => {
|
|
359
|
+
const system: ResolvedBinary = {
|
|
360
|
+
tool: "fd",
|
|
361
|
+
command: "fd",
|
|
362
|
+
source: "system",
|
|
363
|
+
};
|
|
364
|
+
const bundled: ResolvedBinary = {
|
|
365
|
+
tool: "rg",
|
|
366
|
+
command: "/repo/bin/rg",
|
|
367
|
+
source: "bundled",
|
|
368
|
+
};
|
|
369
|
+
const installed: ResolvedBinary = {
|
|
370
|
+
tool: "rg",
|
|
371
|
+
command: "/repo/bin/rg",
|
|
372
|
+
source: "installed",
|
|
373
|
+
version: "15.2.0",
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
assert.deepEqual(installNotifications([system, bundled]), []);
|
|
377
|
+
const messages = installNotifications([system, installed]);
|
|
378
|
+
assert.equal(messages.length, 1);
|
|
379
|
+
assert.match(messages[0], /downloaded rg 15\.2\.0/);
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
// --- output truncation -------------------------------------------------------
|
|
383
|
+
|
|
384
|
+
it.effect("process output is streamed to a complete spill file", () =>
|
|
385
|
+
Effect.gen(function* () {
|
|
386
|
+
const result = yield* executeSearchProcess({
|
|
387
|
+
command: process.execPath,
|
|
388
|
+
args: ["-e", 'process.stdout.write("line\\n".repeat(3000))'],
|
|
389
|
+
cwd: process.cwd(),
|
|
390
|
+
tempPrefix: "pi-search-test-",
|
|
391
|
+
});
|
|
392
|
+
const formatted = formatCapturedOutput(result.output);
|
|
393
|
+
|
|
394
|
+
assert.equal(result.code, 0);
|
|
395
|
+
assert.isTrue(formatted.truncated);
|
|
396
|
+
assert.equal(formatted.lineCount, 3000);
|
|
397
|
+
assert.match(formatted.text, /2000 of 3000 lines/);
|
|
398
|
+
assert.isDefined(formatted.fullOutputPath);
|
|
399
|
+
|
|
400
|
+
const fs = yield* FileSystem.FileSystem;
|
|
401
|
+
const fullOutput = yield* fs.readFileString(formatted.fullOutputPath);
|
|
402
|
+
assert.equal(fullOutput, "line\n".repeat(3000));
|
|
403
|
+
yield* fs.remove(dirname(formatted.fullOutputPath), {
|
|
404
|
+
recursive: true,
|
|
405
|
+
force: true,
|
|
406
|
+
});
|
|
407
|
+
}).pipe(Effect.provide(NodeServices.layer)),
|
|
408
|
+
);
|
|
409
|
+
|
|
410
|
+
it("output: small results pass through untouched", async () => {
|
|
411
|
+
const formatted = await formatOutput("a.ts\nb.ts\n", {
|
|
412
|
+
tempPrefix: "pi-fd-",
|
|
413
|
+
persistFullOutput: () => Promise.reject(new Error("should not persist")),
|
|
414
|
+
});
|
|
415
|
+
assert.equal(formatted.text, "a.ts\nb.ts");
|
|
416
|
+
assert.equal(formatted.lineCount, 2);
|
|
417
|
+
assert.isFalse(formatted.truncated);
|
|
418
|
+
assert.isUndefined(formatted.fullOutputPath);
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
it("output: oversized results are truncated and persisted", async () => {
|
|
422
|
+
const bigOutput = Array.from({ length: 3000 }, (_, i) => `file-${i}.ts`).join(
|
|
423
|
+
"\n",
|
|
424
|
+
);
|
|
425
|
+
let persisted: string | undefined;
|
|
426
|
+
const formatted = await formatOutput(bigOutput, {
|
|
427
|
+
tempPrefix: "pi-fd-",
|
|
428
|
+
persistFullOutput: async (full) => {
|
|
429
|
+
persisted = full;
|
|
430
|
+
return "/tmp/fake/output.txt";
|
|
431
|
+
},
|
|
432
|
+
});
|
|
433
|
+
assert.isTrue(formatted.truncated);
|
|
434
|
+
assert.equal(formatted.fullOutputPath, "/tmp/fake/output.txt");
|
|
435
|
+
assert.equal(persisted, bigOutput);
|
|
436
|
+
assert.match(formatted.text, /\[Output truncated: 2000 of 3000 lines/);
|
|
437
|
+
assert.match(
|
|
438
|
+
formatted.text,
|
|
439
|
+
/Full output saved to: \/tmp\/fake\/output\.txt\]/,
|
|
440
|
+
);
|
|
441
|
+
const shownLines = formatted.text.split("\n");
|
|
442
|
+
assert.equal(shownLines[0], "file-0.ts");
|
|
443
|
+
});
|