getgloss 0.10.0 → 0.12.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/dist/cli/index.js +2 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/server/daemon.js +916 -45
- package/dist/server/daemon.js.map +1 -1
- package/dist/web/assets/index-Bn84l3Zr.css +1 -0
- package/dist/web/assets/index-Cu9NghTt.js +306 -0
- package/dist/web/assets/{syntax-CRTtR0tw.js → syntax-CEpwlb09.js} +6 -6
- package/dist/web/index.html +2 -2
- package/package.json +2 -1
- package/dist/web/assets/index-BKRv97xI.js +0 -269
- package/dist/web/assets/index-Bp9ypCXu.css +0 -1
package/dist/server/daemon.js
CHANGED
|
@@ -10,7 +10,7 @@ import path from "path";
|
|
|
10
10
|
// package.json
|
|
11
11
|
var package_default = {
|
|
12
12
|
name: "getgloss",
|
|
13
|
-
version: "0.
|
|
13
|
+
version: "0.12.0",
|
|
14
14
|
description: "Local browser-based diff review for coding-agent loops.",
|
|
15
15
|
type: "module",
|
|
16
16
|
packageManager: "pnpm@10.33.2",
|
|
@@ -53,6 +53,7 @@ var package_default = {
|
|
|
53
53
|
react: "19.2.6",
|
|
54
54
|
"react-dom": "19.2.6",
|
|
55
55
|
shiki: "4.1.0",
|
|
56
|
+
"simple-icons": "16.23.0",
|
|
56
57
|
ulid: "3.0.2",
|
|
57
58
|
zustand: "5.0.13"
|
|
58
59
|
},
|
|
@@ -200,8 +201,44 @@ var DIFF_LINE_TYPES = ["context", "add", "delete"];
|
|
|
200
201
|
var DIFF_SCOPE_MODES = ["working", "branch", "explicit"];
|
|
201
202
|
var DIFF_FALLBACK_REASONS = ["working-tree-clean", "missing-branch-base"];
|
|
202
203
|
var DIFF_CONTEXT_MAX_LINES = 500;
|
|
204
|
+
var SOURCE_PEEK_MAX_BYTES = 35e4;
|
|
203
205
|
var REVIEW_SCOPE_MODES = ["all", "single", "range"];
|
|
204
206
|
var RESOLUTION_STATUSES = ["partial", "resolved"];
|
|
207
|
+
var OPEN_FILE_SCOPES = ["review", "repo"];
|
|
208
|
+
var OPEN_FILE_TARGETS = [
|
|
209
|
+
"default",
|
|
210
|
+
"vscode",
|
|
211
|
+
"vscode-insiders",
|
|
212
|
+
"vscodium",
|
|
213
|
+
"cursor",
|
|
214
|
+
"sublime",
|
|
215
|
+
"zed",
|
|
216
|
+
"windsurf",
|
|
217
|
+
"webstorm",
|
|
218
|
+
"intellij",
|
|
219
|
+
"pycharm",
|
|
220
|
+
"goland",
|
|
221
|
+
"phpstorm",
|
|
222
|
+
"rubymine",
|
|
223
|
+
"clion",
|
|
224
|
+
"datagrip",
|
|
225
|
+
"android-studio",
|
|
226
|
+
"fleet",
|
|
227
|
+
"neovide",
|
|
228
|
+
"macvim",
|
|
229
|
+
"emacs",
|
|
230
|
+
"lapce",
|
|
231
|
+
"textmate",
|
|
232
|
+
"bbedit",
|
|
233
|
+
"coteditor",
|
|
234
|
+
"nova",
|
|
235
|
+
"textedit",
|
|
236
|
+
"xcode",
|
|
237
|
+
"terminal",
|
|
238
|
+
"iterm2",
|
|
239
|
+
"ghostty",
|
|
240
|
+
"folder"
|
|
241
|
+
];
|
|
205
242
|
|
|
206
243
|
// src/shared/validation.ts
|
|
207
244
|
function parseJson(raw, guard, label) {
|
|
@@ -221,7 +258,10 @@ function isClearReviewsRequest(value) {
|
|
|
221
258
|
return isRecord(value) && isOptionalNonNegativeInteger(value.olderThanDays) && isOptionalBoolean(value.dryRun);
|
|
222
259
|
}
|
|
223
260
|
function isOpenFileRequest(value) {
|
|
224
|
-
return isRecord(value) && isString(value.filePath) && isOptionalString(value.turnId);
|
|
261
|
+
return isRecord(value) && isString(value.filePath) && isOptionalString(value.turnId) && isOptional(value.scope, isOpenFileScope) && isOptional(value.target, isOpenFileTarget);
|
|
262
|
+
}
|
|
263
|
+
function isFileContentRequest(value) {
|
|
264
|
+
return isRecord(value) && isString(value.filePath) && isOptionalString(value.turnId) && isOptional(value.scope, isOpenFileScope);
|
|
225
265
|
}
|
|
226
266
|
function isCommitRangeDiffRequest(value) {
|
|
227
267
|
return isRecord(value) && isString(value.fromSha) && isString(value.toSha) && isOptionalString(value.turnId);
|
|
@@ -229,6 +269,9 @@ function isCommitRangeDiffRequest(value) {
|
|
|
229
269
|
function isDiffContextRequest(value) {
|
|
230
270
|
return isRecord(value) && isString(value.filePath) && isNullableString(value.oldPath) && isOptionalString(value.turnId) && isDiffContextSource(value.source) && isPositiveInteger(value.oldStart) && isPositiveInteger(value.newStart) && isPositiveInteger(value.lineCount);
|
|
231
271
|
}
|
|
272
|
+
function isSourcePeekRequest(value) {
|
|
273
|
+
return isRecord(value) && isString(value.filePath) && isNullableString(value.oldPath) && isOptionalString(value.turnId) && isDiffContextSource(value.source) && isOneOf(value.side, SIDES) && isPositiveInteger(value.line) && isNonNegativeInteger(value.column) && isIdentifier(value.symbol);
|
|
274
|
+
}
|
|
232
275
|
function isSubmitReviewRequest(value) {
|
|
233
276
|
return isRecord(value) && isArrayOf(value.comments, isComment) && isOptional(value.reviewScope, isReviewScope);
|
|
234
277
|
}
|
|
@@ -332,6 +375,12 @@ function isReviewStatus(value) {
|
|
|
332
375
|
function isResolutionStatus(value) {
|
|
333
376
|
return isOneOf(value, RESOLUTION_STATUSES);
|
|
334
377
|
}
|
|
378
|
+
function isOpenFileScope(value) {
|
|
379
|
+
return isOneOf(value, OPEN_FILE_SCOPES);
|
|
380
|
+
}
|
|
381
|
+
function isOpenFileTarget(value) {
|
|
382
|
+
return isOneOf(value, OPEN_FILE_TARGETS);
|
|
383
|
+
}
|
|
335
384
|
function isRecord(value) {
|
|
336
385
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
337
386
|
}
|
|
@@ -356,6 +405,12 @@ function isNumber(value) {
|
|
|
356
405
|
function isPositiveInteger(value) {
|
|
357
406
|
return isNumber(value) && Number.isInteger(value) && value > 0;
|
|
358
407
|
}
|
|
408
|
+
function isNonNegativeInteger(value) {
|
|
409
|
+
return isNumber(value) && Number.isInteger(value) && value >= 0;
|
|
410
|
+
}
|
|
411
|
+
function isIdentifier(value) {
|
|
412
|
+
return isString(value) && /^[A-Za-z_$][\w$]*$/.test(value);
|
|
413
|
+
}
|
|
359
414
|
function isOptionalNumber(value) {
|
|
360
415
|
return value === void 0 || isNumber(value);
|
|
361
416
|
}
|
|
@@ -514,8 +569,8 @@ function createIdleScheduler(options) {
|
|
|
514
569
|
}
|
|
515
570
|
|
|
516
571
|
// src/server/index.ts
|
|
517
|
-
import { readFile as
|
|
518
|
-
import
|
|
572
|
+
import { readFile as readFile6, realpath, stat } from "fs/promises";
|
|
573
|
+
import path9 from "path";
|
|
519
574
|
import { fileURLToPath } from "url";
|
|
520
575
|
import { Hono } from "hono";
|
|
521
576
|
import { streamSSE } from "hono/streaming";
|
|
@@ -797,19 +852,706 @@ function isResolvableReviewStatus(status) {
|
|
|
797
852
|
}
|
|
798
853
|
|
|
799
854
|
// src/server/local-open.ts
|
|
855
|
+
import { execFile } from "child_process";
|
|
856
|
+
import { access } from "fs/promises";
|
|
857
|
+
import { homedir as homedir2 } from "os";
|
|
858
|
+
import path6 from "path";
|
|
859
|
+
import { promisify } from "util";
|
|
800
860
|
import open from "open";
|
|
801
|
-
|
|
802
|
-
|
|
861
|
+
var execFileAsync = promisify(execFile);
|
|
862
|
+
var applicationRoots = [
|
|
863
|
+
"/Applications",
|
|
864
|
+
path6.join(homedir2(), "Applications"),
|
|
865
|
+
"/System/Applications",
|
|
866
|
+
"/System/Applications/Utilities"
|
|
867
|
+
];
|
|
868
|
+
var openFileTargetDescriptors = [
|
|
869
|
+
{ appName: "Visual Studio Code", label: "VS Code", target: "vscode" },
|
|
870
|
+
{
|
|
871
|
+
appName: ["Visual Studio Code - Insiders", "Code - Insiders"],
|
|
872
|
+
label: "VS Code Insiders",
|
|
873
|
+
target: "vscode-insiders"
|
|
874
|
+
},
|
|
875
|
+
{ appName: "VSCodium", label: "VSCodium", target: "vscodium" },
|
|
876
|
+
{ appName: "Cursor", label: "Cursor", target: "cursor" },
|
|
877
|
+
{ appName: "Sublime Text", label: "Sublime Text", target: "sublime" },
|
|
878
|
+
{ appName: "Zed", label: "Zed", target: "zed" },
|
|
879
|
+
{ appName: "Windsurf", label: "Windsurf", target: "windsurf" },
|
|
880
|
+
{ appName: "WebStorm", label: "WebStorm", target: "webstorm" },
|
|
881
|
+
{
|
|
882
|
+
appName: ["IntelliJ IDEA", "IntelliJ IDEA Ultimate", "IntelliJ IDEA CE"],
|
|
883
|
+
label: "IntelliJ IDEA",
|
|
884
|
+
target: "intellij"
|
|
885
|
+
},
|
|
886
|
+
{ appName: ["PyCharm", "PyCharm CE"], label: "PyCharm", target: "pycharm" },
|
|
887
|
+
{ appName: "GoLand", label: "GoLand", target: "goland" },
|
|
888
|
+
{ appName: "PhpStorm", label: "PhpStorm", target: "phpstorm" },
|
|
889
|
+
{ appName: "RubyMine", label: "RubyMine", target: "rubymine" },
|
|
890
|
+
{ appName: "CLion", label: "CLion", target: "clion" },
|
|
891
|
+
{ appName: "DataGrip", label: "DataGrip", target: "datagrip" },
|
|
892
|
+
{ appName: "Android Studio", label: "Android Studio", target: "android-studio" },
|
|
893
|
+
{ appName: "Fleet", label: "Fleet", target: "fleet" },
|
|
894
|
+
{ appName: "Neovide", label: "Neovide", target: "neovide" },
|
|
895
|
+
{ appName: "MacVim", label: "MacVim", target: "macvim" },
|
|
896
|
+
{ appName: "Emacs", label: "Emacs", target: "emacs" },
|
|
897
|
+
{ appName: "Lapce", label: "Lapce", target: "lapce" },
|
|
898
|
+
{ appName: "TextMate", label: "TextMate", target: "textmate" },
|
|
899
|
+
{ appName: "BBEdit", label: "BBEdit", target: "bbedit" },
|
|
900
|
+
{ appName: "CotEditor", label: "CotEditor", target: "coteditor" },
|
|
901
|
+
{ appName: "Nova", label: "Nova", target: "nova" },
|
|
902
|
+
{ appName: "TextEdit", label: "TextEdit", target: "textedit" },
|
|
903
|
+
{ appName: "Terminal", label: "Terminal", opensFolder: true, target: "terminal" },
|
|
904
|
+
{ appName: ["iTerm", "iTerm2"], label: "iTerm2", opensFolder: true, target: "iterm2" },
|
|
905
|
+
{ appName: "Ghostty", label: "Ghostty", opensFolder: true, target: "ghostty" },
|
|
906
|
+
{ appName: "Xcode", label: "Xcode", target: "xcode" },
|
|
907
|
+
{ label: "Default app", target: "default" },
|
|
908
|
+
{ label: "Open in folder", opensFolder: true, target: "folder" }
|
|
909
|
+
];
|
|
910
|
+
var openFileTargetDescriptorByTarget = new Map(
|
|
911
|
+
openFileTargetDescriptors.map((descriptor) => [descriptor.target, descriptor])
|
|
912
|
+
);
|
|
913
|
+
async function availableOpenFileTargets() {
|
|
914
|
+
const availability = await Promise.all(
|
|
915
|
+
openFileTargetDescriptors.map(
|
|
916
|
+
async (descriptor) => descriptor.appName && !await resolveAppPath(descriptor.appName) ? null : descriptor
|
|
917
|
+
)
|
|
918
|
+
);
|
|
919
|
+
return availability.filter((descriptor) => descriptor !== null).map(({ label, target }) => ({ label, target }));
|
|
920
|
+
}
|
|
921
|
+
async function openLocalPath(filePath, target = "default") {
|
|
922
|
+
const descriptor = openFileTargetDescriptorByTarget.get(target);
|
|
923
|
+
const openPath = descriptor?.opensFolder ? path6.dirname(filePath) : filePath;
|
|
924
|
+
const appName = descriptor?.appName;
|
|
925
|
+
if (!appName) {
|
|
926
|
+
await open(openPath, { wait: false });
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
const appPath = await resolveAppPath(appName);
|
|
930
|
+
if (appPath) {
|
|
931
|
+
await execFileAsync("open", ["-a", appPath, openPath]);
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
await open(openPath, { app: { name: appName }, wait: false });
|
|
935
|
+
}
|
|
936
|
+
async function resolveAppPath(appName) {
|
|
937
|
+
if (process.platform !== "darwin") {
|
|
938
|
+
return null;
|
|
939
|
+
}
|
|
940
|
+
const appNames = Array.isArray(appName) ? appName : [appName];
|
|
941
|
+
for (const name of appNames) {
|
|
942
|
+
for (const root of applicationRoots) {
|
|
943
|
+
const appPath = path6.join(root, `${name}.app`);
|
|
944
|
+
try {
|
|
945
|
+
await access(appPath);
|
|
946
|
+
return appPath;
|
|
947
|
+
} catch {
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
return null;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
// src/server/source-peek.ts
|
|
955
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
956
|
+
import path7 from "path";
|
|
957
|
+
import { execa as execa2 } from "execa";
|
|
958
|
+
var MODULE_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".json"];
|
|
959
|
+
var PATH_CONFIG_FILES = ["tsconfig.json", "jsconfig.json"];
|
|
960
|
+
var IGNORED_SEARCH_PATHS = ["node_modules", "dist", "build", "coverage", ".git"];
|
|
961
|
+
var IDENTIFIER_PATTERN = "[A-Za-z_$][\\w$]*";
|
|
962
|
+
var SOURCE_PEEK_CONTEXT_LINES = 520;
|
|
963
|
+
var SOURCE_PEEK_TARGET_CONTEXT_LINES = 240;
|
|
964
|
+
async function resolveSourcePeek({
|
|
965
|
+
column,
|
|
966
|
+
line,
|
|
967
|
+
repoRoot,
|
|
968
|
+
sourceFilePath,
|
|
969
|
+
sourceRef,
|
|
970
|
+
symbol
|
|
971
|
+
}) {
|
|
972
|
+
const sourceContent = await readRepoText(repoRoot, sourceRef, sourceFilePath);
|
|
973
|
+
const sourceLines = splitFileLines2(sourceContent);
|
|
974
|
+
const reference = referenceTokenAt(sourceLines[line - 1] ?? "", column, symbol);
|
|
975
|
+
const imports = parseImportBindings(sourceContent);
|
|
976
|
+
const aliasConfig = await loadPathAliasConfig(repoRoot);
|
|
977
|
+
const importedCandidates = candidateImportsForReference(imports, reference);
|
|
978
|
+
for (const candidate of importedCandidates) {
|
|
979
|
+
const resolved = await resolveImportedTarget({
|
|
980
|
+
aliasConfig,
|
|
981
|
+
importedName: candidate.targetSymbol,
|
|
982
|
+
matchReason: "import",
|
|
983
|
+
moduleSpecifier: candidate.moduleSpecifier,
|
|
984
|
+
repoRoot,
|
|
985
|
+
sourceFilePath,
|
|
986
|
+
sourceRef,
|
|
987
|
+
symbol
|
|
988
|
+
});
|
|
989
|
+
if (resolved) {
|
|
990
|
+
return resolved;
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
const sameFileMatch = findDefinition(sourceContent, symbol);
|
|
994
|
+
if (sameFileMatch) {
|
|
995
|
+
return responseForMatch({
|
|
996
|
+
column: sameFileMatch.column,
|
|
997
|
+
content: sourceContent,
|
|
998
|
+
filePath: sourceFilePath,
|
|
999
|
+
line: sameFileMatch.line,
|
|
1000
|
+
matchReason: "same-file",
|
|
1001
|
+
symbol,
|
|
1002
|
+
targetSymbol: symbol
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
const searched = await searchRepoDefinition(repoRoot, sourceRef, symbol);
|
|
1006
|
+
if (searched) {
|
|
1007
|
+
return responseForMatch({
|
|
1008
|
+
...searched,
|
|
1009
|
+
matchReason: "repo-search",
|
|
1010
|
+
symbol,
|
|
1011
|
+
targetSymbol: symbol
|
|
1012
|
+
});
|
|
1013
|
+
}
|
|
1014
|
+
throw new Error(`No definition found for ${symbol}`);
|
|
1015
|
+
}
|
|
1016
|
+
async function resolveImportedTarget({
|
|
1017
|
+
aliasConfig,
|
|
1018
|
+
importedName,
|
|
1019
|
+
matchReason,
|
|
1020
|
+
moduleSpecifier,
|
|
1021
|
+
repoRoot,
|
|
1022
|
+
sourceFilePath,
|
|
1023
|
+
sourceRef,
|
|
1024
|
+
symbol
|
|
1025
|
+
}) {
|
|
1026
|
+
const moduleFile = await firstExistingModuleFile(
|
|
1027
|
+
repoRoot,
|
|
1028
|
+
sourceRef,
|
|
1029
|
+
moduleCandidates(repoRoot, aliasConfig, sourceFilePath, moduleSpecifier)
|
|
1030
|
+
);
|
|
1031
|
+
if (!moduleFile) {
|
|
1032
|
+
return null;
|
|
1033
|
+
}
|
|
1034
|
+
const targetMatch = findDefinition(moduleFile.content, importedName);
|
|
1035
|
+
if (targetMatch) {
|
|
1036
|
+
return responseForMatch({
|
|
1037
|
+
column: targetMatch.column,
|
|
1038
|
+
content: moduleFile.content,
|
|
1039
|
+
filePath: moduleFile.filePath,
|
|
1040
|
+
line: targetMatch.line,
|
|
1041
|
+
matchReason,
|
|
1042
|
+
symbol,
|
|
1043
|
+
targetSymbol: importedName
|
|
1044
|
+
});
|
|
1045
|
+
}
|
|
1046
|
+
const reExported = await resolveReExport({
|
|
1047
|
+
aliasConfig,
|
|
1048
|
+
content: moduleFile.content,
|
|
1049
|
+
depth: 0,
|
|
1050
|
+
repoRoot,
|
|
1051
|
+
sourceFilePath: moduleFile.filePath,
|
|
1052
|
+
sourceRef,
|
|
1053
|
+
symbol,
|
|
1054
|
+
targetSymbol: importedName
|
|
1055
|
+
});
|
|
1056
|
+
if (reExported) {
|
|
1057
|
+
return reExported;
|
|
1058
|
+
}
|
|
1059
|
+
if (importedName === "default") {
|
|
1060
|
+
const fallbackLine = firstMeaningfulLine(moduleFile.content);
|
|
1061
|
+
return responseForMatch({
|
|
1062
|
+
column: 0,
|
|
1063
|
+
content: moduleFile.content,
|
|
1064
|
+
filePath: moduleFile.filePath,
|
|
1065
|
+
line: fallbackLine,
|
|
1066
|
+
matchReason: "module",
|
|
1067
|
+
symbol,
|
|
1068
|
+
targetSymbol: importedName
|
|
1069
|
+
});
|
|
1070
|
+
}
|
|
1071
|
+
return null;
|
|
1072
|
+
}
|
|
1073
|
+
async function resolveReExport({
|
|
1074
|
+
aliasConfig,
|
|
1075
|
+
content,
|
|
1076
|
+
depth,
|
|
1077
|
+
repoRoot,
|
|
1078
|
+
sourceFilePath,
|
|
1079
|
+
sourceRef,
|
|
1080
|
+
symbol,
|
|
1081
|
+
targetSymbol
|
|
1082
|
+
}) {
|
|
1083
|
+
if (depth > 3) {
|
|
1084
|
+
return null;
|
|
1085
|
+
}
|
|
1086
|
+
for (const binding of parseReExportBindings(content)) {
|
|
1087
|
+
if (binding.exportedName !== "*" && binding.exportedName !== targetSymbol) {
|
|
1088
|
+
continue;
|
|
1089
|
+
}
|
|
1090
|
+
const nextTarget = binding.importedName === "*" ? targetSymbol : binding.importedName;
|
|
1091
|
+
const moduleFile = await firstExistingModuleFile(
|
|
1092
|
+
repoRoot,
|
|
1093
|
+
sourceRef,
|
|
1094
|
+
moduleCandidates(repoRoot, aliasConfig, sourceFilePath, binding.moduleSpecifier)
|
|
1095
|
+
);
|
|
1096
|
+
if (!moduleFile) {
|
|
1097
|
+
continue;
|
|
1098
|
+
}
|
|
1099
|
+
const match = findDefinition(moduleFile.content, nextTarget);
|
|
1100
|
+
if (match) {
|
|
1101
|
+
return responseForMatch({
|
|
1102
|
+
column: match.column,
|
|
1103
|
+
content: moduleFile.content,
|
|
1104
|
+
filePath: moduleFile.filePath,
|
|
1105
|
+
line: match.line,
|
|
1106
|
+
matchReason: "import",
|
|
1107
|
+
symbol,
|
|
1108
|
+
targetSymbol: nextTarget
|
|
1109
|
+
});
|
|
1110
|
+
}
|
|
1111
|
+
const nested = await resolveReExport({
|
|
1112
|
+
aliasConfig,
|
|
1113
|
+
content: moduleFile.content,
|
|
1114
|
+
depth: depth + 1,
|
|
1115
|
+
repoRoot,
|
|
1116
|
+
sourceFilePath: moduleFile.filePath,
|
|
1117
|
+
sourceRef,
|
|
1118
|
+
symbol,
|
|
1119
|
+
targetSymbol: nextTarget
|
|
1120
|
+
});
|
|
1121
|
+
if (nested) {
|
|
1122
|
+
return nested;
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
return null;
|
|
1126
|
+
}
|
|
1127
|
+
async function searchRepoDefinition(repoRoot, sourceRef, symbol) {
|
|
1128
|
+
const grepPattern = definitionGrepPattern(symbol);
|
|
1129
|
+
const args = ["grep", "-n", "-I", "-E", grepPattern];
|
|
1130
|
+
if (sourceRef) {
|
|
1131
|
+
args.push(sourceRef);
|
|
1132
|
+
}
|
|
1133
|
+
args.push("--", ".");
|
|
1134
|
+
for (const ignoredPath of IGNORED_SEARCH_PATHS) {
|
|
1135
|
+
args.push(`:!${ignoredPath}`);
|
|
1136
|
+
}
|
|
1137
|
+
let output = "";
|
|
1138
|
+
try {
|
|
1139
|
+
output = (await execa2("git", args, { cwd: repoRoot })).stdout;
|
|
1140
|
+
} catch (error) {
|
|
1141
|
+
if (isExitCode(error, 1)) {
|
|
1142
|
+
return null;
|
|
1143
|
+
}
|
|
1144
|
+
throw error;
|
|
1145
|
+
}
|
|
1146
|
+
for (const rawLine of output.split("\n")) {
|
|
1147
|
+
const parsed = parseGrepLine(rawLine, sourceRef);
|
|
1148
|
+
if (!parsed) {
|
|
1149
|
+
continue;
|
|
1150
|
+
}
|
|
1151
|
+
const content = await readRepoText(repoRoot, sourceRef, parsed.filePath).catch(() => null);
|
|
1152
|
+
if (!content) {
|
|
1153
|
+
continue;
|
|
1154
|
+
}
|
|
1155
|
+
const match = findDefinition(content, symbol);
|
|
1156
|
+
if (!match) {
|
|
1157
|
+
continue;
|
|
1158
|
+
}
|
|
1159
|
+
return {
|
|
1160
|
+
column: match.column,
|
|
1161
|
+
content,
|
|
1162
|
+
filePath: parsed.filePath,
|
|
1163
|
+
line: match.line
|
|
1164
|
+
};
|
|
1165
|
+
}
|
|
1166
|
+
return null;
|
|
1167
|
+
}
|
|
1168
|
+
async function readRepoText(repoRoot, ref, filePath) {
|
|
1169
|
+
if (ref === null) {
|
|
1170
|
+
return readFile3(path7.resolve(repoRoot, filePath), "utf8");
|
|
1171
|
+
}
|
|
1172
|
+
return (await execa2("git", ["show", `${ref}:${filePath}`], { cwd: repoRoot })).stdout;
|
|
1173
|
+
}
|
|
1174
|
+
function responseForMatch({
|
|
1175
|
+
column,
|
|
1176
|
+
content,
|
|
1177
|
+
filePath,
|
|
1178
|
+
line,
|
|
1179
|
+
matchReason,
|
|
1180
|
+
symbol,
|
|
1181
|
+
targetSymbol
|
|
1182
|
+
}) {
|
|
1183
|
+
const limited = limitContentAroundLine(content, line);
|
|
1184
|
+
return {
|
|
1185
|
+
symbol,
|
|
1186
|
+
targetSymbol,
|
|
1187
|
+
filePath,
|
|
1188
|
+
startLine: limited.startLine,
|
|
1189
|
+
line,
|
|
1190
|
+
column,
|
|
1191
|
+
language: languageForPath(filePath),
|
|
1192
|
+
content: limited.content,
|
|
1193
|
+
truncated: limited.truncated,
|
|
1194
|
+
matchReason
|
|
1195
|
+
};
|
|
1196
|
+
}
|
|
1197
|
+
function limitContentAroundLine(content, line) {
|
|
1198
|
+
if (Buffer.byteLength(content, "utf8") <= SOURCE_PEEK_MAX_BYTES) {
|
|
1199
|
+
return { content, startLine: 1, truncated: false };
|
|
1200
|
+
}
|
|
1201
|
+
const lines = splitFileLines2(content);
|
|
1202
|
+
const targetIndex = Math.max(0, line - 1);
|
|
1203
|
+
const preferredStartIndex = Math.max(0, targetIndex - SOURCE_PEEK_TARGET_CONTEXT_LINES);
|
|
1204
|
+
const preferredEndIndex = Math.min(lines.length, preferredStartIndex + SOURCE_PEEK_CONTEXT_LINES);
|
|
1205
|
+
let startIndex = preferredStartIndex;
|
|
1206
|
+
let endIndex = preferredEndIndex;
|
|
1207
|
+
let limitedContent = lines.slice(startIndex, endIndex).join("\n");
|
|
1208
|
+
while (Buffer.byteLength(limitedContent, "utf8") > SOURCE_PEEK_MAX_BYTES && endIndex - startIndex > 1) {
|
|
1209
|
+
if (targetIndex - startIndex > endIndex - targetIndex - 1) {
|
|
1210
|
+
startIndex += 1;
|
|
1211
|
+
} else {
|
|
1212
|
+
endIndex -= 1;
|
|
1213
|
+
}
|
|
1214
|
+
limitedContent = lines.slice(startIndex, endIndex).join("\n");
|
|
1215
|
+
}
|
|
1216
|
+
if (Buffer.byteLength(limitedContent, "utf8") > SOURCE_PEEK_MAX_BYTES) {
|
|
1217
|
+
limitedContent = truncateUtf8(limitedContent, SOURCE_PEEK_MAX_BYTES);
|
|
1218
|
+
}
|
|
1219
|
+
return {
|
|
1220
|
+
content: limitedContent,
|
|
1221
|
+
startLine: startIndex + 1,
|
|
1222
|
+
truncated: true
|
|
1223
|
+
};
|
|
1224
|
+
}
|
|
1225
|
+
function truncateUtf8(value, maxBytes) {
|
|
1226
|
+
const buffer = Buffer.from(value, "utf8");
|
|
1227
|
+
if (buffer.byteLength <= maxBytes) {
|
|
1228
|
+
return value;
|
|
1229
|
+
}
|
|
1230
|
+
return buffer.subarray(0, maxBytes).toString("utf8").replace(/\uFFFD$/, "");
|
|
1231
|
+
}
|
|
1232
|
+
function candidateImportsForReference(imports, reference) {
|
|
1233
|
+
if (reference.namespace) {
|
|
1234
|
+
return imports.filter(
|
|
1235
|
+
(binding) => binding.kind === "namespace" && binding.localName === reference.namespace
|
|
1236
|
+
).map((binding) => ({
|
|
1237
|
+
moduleSpecifier: binding.moduleSpecifier,
|
|
1238
|
+
targetSymbol: reference.symbol
|
|
1239
|
+
}));
|
|
1240
|
+
}
|
|
1241
|
+
return imports.filter((binding) => binding.localName === reference.symbol).map((binding) => ({
|
|
1242
|
+
moduleSpecifier: binding.moduleSpecifier,
|
|
1243
|
+
targetSymbol: binding.importedName
|
|
1244
|
+
}));
|
|
1245
|
+
}
|
|
1246
|
+
function referenceTokenAt(line, column, symbol) {
|
|
1247
|
+
let start = Math.min(Math.max(column, 0), line.length);
|
|
1248
|
+
if (line.slice(start, start + symbol.length) !== symbol) {
|
|
1249
|
+
const beforeOrAt = line.lastIndexOf(symbol, start);
|
|
1250
|
+
start = beforeOrAt >= 0 ? beforeOrAt : line.indexOf(symbol);
|
|
1251
|
+
}
|
|
1252
|
+
if (start < 0) {
|
|
1253
|
+
return { namespace: null, symbol };
|
|
1254
|
+
}
|
|
1255
|
+
const namespaceMatch = line.slice(0, start).match(/([A-Za-z_$][\w$]*)\s*\.\s*$/);
|
|
1256
|
+
return {
|
|
1257
|
+
namespace: namespaceMatch?.[1] ?? null,
|
|
1258
|
+
symbol
|
|
1259
|
+
};
|
|
1260
|
+
}
|
|
1261
|
+
function parseImportBindings(content) {
|
|
1262
|
+
const bindings = [];
|
|
1263
|
+
const importRegex = /^\s*import\s+(?:type\s+)?([^;]*?)\s+from\s+['"]([^'"]+)['"];?/gm;
|
|
1264
|
+
let match = importRegex.exec(content);
|
|
1265
|
+
while (match !== null) {
|
|
1266
|
+
const body = match[1]?.trim() ?? "";
|
|
1267
|
+
const moduleSpecifier = match[2] ?? "";
|
|
1268
|
+
for (const part of splitImportParts(body)) {
|
|
1269
|
+
if (part.startsWith("{") && part.endsWith("}")) {
|
|
1270
|
+
bindings.push(
|
|
1271
|
+
...parseNamedBindings(part).map((binding) => ({
|
|
1272
|
+
...binding,
|
|
1273
|
+
kind: "named",
|
|
1274
|
+
moduleSpecifier
|
|
1275
|
+
}))
|
|
1276
|
+
);
|
|
1277
|
+
continue;
|
|
1278
|
+
}
|
|
1279
|
+
const namespaceMatch = part.match(/^\*\s+as\s+([A-Za-z_$][\w$]*)$/);
|
|
1280
|
+
if (namespaceMatch?.[1]) {
|
|
1281
|
+
bindings.push({
|
|
1282
|
+
importedName: "*",
|
|
1283
|
+
kind: "namespace",
|
|
1284
|
+
localName: namespaceMatch[1],
|
|
1285
|
+
moduleSpecifier
|
|
1286
|
+
});
|
|
1287
|
+
continue;
|
|
1288
|
+
}
|
|
1289
|
+
if (new RegExp(`^${IDENTIFIER_PATTERN}$`).test(part)) {
|
|
1290
|
+
bindings.push({
|
|
1291
|
+
importedName: "default",
|
|
1292
|
+
kind: "default",
|
|
1293
|
+
localName: part,
|
|
1294
|
+
moduleSpecifier
|
|
1295
|
+
});
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
match = importRegex.exec(content);
|
|
1299
|
+
}
|
|
1300
|
+
return bindings;
|
|
1301
|
+
}
|
|
1302
|
+
function parseReExportBindings(content) {
|
|
1303
|
+
const bindings = [];
|
|
1304
|
+
const exportRegex = /^\s*export\s+(?:type\s+)?(\*|\{[\s\S]*?\})\s+from\s+['"]([^'"]+)['"];?/gm;
|
|
1305
|
+
let match = exportRegex.exec(content);
|
|
1306
|
+
while (match !== null) {
|
|
1307
|
+
const body = match[1] ?? "";
|
|
1308
|
+
const moduleSpecifier = match[2] ?? "";
|
|
1309
|
+
if (body === "*") {
|
|
1310
|
+
bindings.push({ exportedName: "*", importedName: "*", moduleSpecifier });
|
|
1311
|
+
match = exportRegex.exec(content);
|
|
1312
|
+
continue;
|
|
1313
|
+
}
|
|
1314
|
+
bindings.push(
|
|
1315
|
+
...parseNamedBindings(body).map((binding) => ({
|
|
1316
|
+
exportedName: binding.localName,
|
|
1317
|
+
importedName: binding.importedName,
|
|
1318
|
+
moduleSpecifier
|
|
1319
|
+
}))
|
|
1320
|
+
);
|
|
1321
|
+
match = exportRegex.exec(content);
|
|
1322
|
+
}
|
|
1323
|
+
return bindings;
|
|
1324
|
+
}
|
|
1325
|
+
function parseNamedBindings(body) {
|
|
1326
|
+
return body.slice(1, -1).split(",").map((part) => part.trim().replace(/^type\s+/, "")).filter(Boolean).map((part) => {
|
|
1327
|
+
const aliasMatch = part.match(/^([A-Za-z_$][\w$]*)\s+as\s+([A-Za-z_$][\w$]*)$/);
|
|
1328
|
+
if (aliasMatch?.[1] && aliasMatch[2]) {
|
|
1329
|
+
return { importedName: aliasMatch[1], localName: aliasMatch[2] };
|
|
1330
|
+
}
|
|
1331
|
+
return new RegExp(`^${IDENTIFIER_PATTERN}$`).test(part) ? { importedName: part, localName: part } : null;
|
|
1332
|
+
}).filter((binding) => Boolean(binding));
|
|
1333
|
+
}
|
|
1334
|
+
function splitImportParts(body) {
|
|
1335
|
+
const parts = [];
|
|
1336
|
+
let depth = 0;
|
|
1337
|
+
let start = 0;
|
|
1338
|
+
for (let index = 0; index < body.length; index += 1) {
|
|
1339
|
+
const char = body[index];
|
|
1340
|
+
if (char === "{") {
|
|
1341
|
+
depth += 1;
|
|
1342
|
+
} else if (char === "}") {
|
|
1343
|
+
depth = Math.max(0, depth - 1);
|
|
1344
|
+
} else if (char === "," && depth === 0) {
|
|
1345
|
+
parts.push(body.slice(start, index).trim());
|
|
1346
|
+
start = index + 1;
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
parts.push(body.slice(start).trim());
|
|
1350
|
+
return parts.filter(Boolean);
|
|
1351
|
+
}
|
|
1352
|
+
async function loadPathAliasConfig(repoRoot) {
|
|
1353
|
+
for (const configFile of PATH_CONFIG_FILES) {
|
|
1354
|
+
const rawConfig = await readFile3(path7.join(repoRoot, configFile), "utf8").catch(() => null);
|
|
1355
|
+
if (!rawConfig) {
|
|
1356
|
+
continue;
|
|
1357
|
+
}
|
|
1358
|
+
const parsed = parseJsonConfig(rawConfig);
|
|
1359
|
+
if (!parsed) {
|
|
1360
|
+
continue;
|
|
1361
|
+
}
|
|
1362
|
+
const compilerOptions = asRecord(parsed.compilerOptions);
|
|
1363
|
+
const baseUrlValue = compilerOptions ? compilerOptions.baseUrl : null;
|
|
1364
|
+
const baseUrl = typeof baseUrlValue === "string" ? path7.resolve(repoRoot, baseUrlValue) : repoRoot;
|
|
1365
|
+
const paths = asRecord(compilerOptions?.paths);
|
|
1366
|
+
const aliases = paths ? Object.entries(paths).flatMap(
|
|
1367
|
+
([pattern, replacements]) => Array.isArray(replacements) ? [
|
|
1368
|
+
{
|
|
1369
|
+
pattern,
|
|
1370
|
+
replacements: replacements.filter(
|
|
1371
|
+
(replacement) => typeof replacement === "string"
|
|
1372
|
+
)
|
|
1373
|
+
}
|
|
1374
|
+
] : []
|
|
1375
|
+
) : [];
|
|
1376
|
+
return { aliases, baseUrl };
|
|
1377
|
+
}
|
|
1378
|
+
return { aliases: [], baseUrl: null };
|
|
1379
|
+
}
|
|
1380
|
+
function parseJsonConfig(rawConfig) {
|
|
1381
|
+
try {
|
|
1382
|
+
return JSON.parse(stripJsonComments(rawConfig).replace(/,\s*([}\]])/g, "$1"));
|
|
1383
|
+
} catch {
|
|
1384
|
+
return null;
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
function stripJsonComments(input) {
|
|
1388
|
+
return input.replace(/\/\*[\s\S]*?\*\//g, "").replace(/^\s*\/\/.*$/gm, "");
|
|
1389
|
+
}
|
|
1390
|
+
function moduleCandidates(repoRoot, aliasConfig, sourceFilePath, moduleSpecifier) {
|
|
1391
|
+
const roots = [];
|
|
1392
|
+
if (moduleSpecifier.startsWith(".")) {
|
|
1393
|
+
roots.push(path7.resolve(repoRoot, path7.dirname(sourceFilePath), moduleSpecifier));
|
|
1394
|
+
} else {
|
|
1395
|
+
roots.push(...aliasCandidateRoots(repoRoot, aliasConfig, moduleSpecifier));
|
|
1396
|
+
if (aliasConfig.baseUrl) {
|
|
1397
|
+
roots.push(path7.resolve(aliasConfig.baseUrl, moduleSpecifier));
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
const candidates = /* @__PURE__ */ new Set();
|
|
1401
|
+
for (const root of roots) {
|
|
1402
|
+
for (const candidate of expandModuleRoot(root)) {
|
|
1403
|
+
const relativePath = repoRelativePath(repoRoot, candidate);
|
|
1404
|
+
if (relativePath) {
|
|
1405
|
+
candidates.add(relativePath);
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
return [...candidates];
|
|
1410
|
+
}
|
|
1411
|
+
function aliasCandidateRoots(repoRoot, aliasConfig, moduleSpecifier) {
|
|
1412
|
+
const roots = [];
|
|
1413
|
+
for (const alias of aliasConfig.aliases) {
|
|
1414
|
+
const wildcardIndex = alias.pattern.indexOf("*");
|
|
1415
|
+
const matched = wildcardIndex >= 0 ? matchWildcardPattern(alias.pattern, wildcardIndex, moduleSpecifier) : alias.pattern === moduleSpecifier ? "" : null;
|
|
1416
|
+
if (matched === null) {
|
|
1417
|
+
continue;
|
|
1418
|
+
}
|
|
1419
|
+
for (const replacement of alias.replacements) {
|
|
1420
|
+
roots.push(
|
|
1421
|
+
path7.resolve(
|
|
1422
|
+
aliasConfig.baseUrl ?? repoRoot,
|
|
1423
|
+
wildcardIndex >= 0 ? replacement.replace("*", matched) : replacement
|
|
1424
|
+
)
|
|
1425
|
+
);
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
return roots;
|
|
1429
|
+
}
|
|
1430
|
+
function matchWildcardPattern(pattern, wildcardIndex, value) {
|
|
1431
|
+
const prefix = pattern.slice(0, wildcardIndex);
|
|
1432
|
+
const suffix = pattern.slice(wildcardIndex + 1);
|
|
1433
|
+
if (!value.startsWith(prefix) || !value.endsWith(suffix)) {
|
|
1434
|
+
return null;
|
|
1435
|
+
}
|
|
1436
|
+
return value.slice(prefix.length, value.length - suffix.length);
|
|
1437
|
+
}
|
|
1438
|
+
function expandModuleRoot(root) {
|
|
1439
|
+
const candidates = [];
|
|
1440
|
+
if (path7.extname(root)) {
|
|
1441
|
+
candidates.push(root);
|
|
1442
|
+
} else {
|
|
1443
|
+
for (const extension of MODULE_EXTENSIONS) {
|
|
1444
|
+
candidates.push(`${root}${extension}`);
|
|
1445
|
+
}
|
|
1446
|
+
for (const extension of MODULE_EXTENSIONS) {
|
|
1447
|
+
candidates.push(path7.join(root, `index${extension}`));
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
return candidates;
|
|
1451
|
+
}
|
|
1452
|
+
async function firstExistingModuleFile(repoRoot, sourceRef, candidates) {
|
|
1453
|
+
for (const filePath of candidates) {
|
|
1454
|
+
const content = await readRepoText(repoRoot, sourceRef, filePath).catch(() => null);
|
|
1455
|
+
if (content !== null) {
|
|
1456
|
+
return { content, filePath };
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
return null;
|
|
1460
|
+
}
|
|
1461
|
+
function findDefinition(content, symbol) {
|
|
1462
|
+
const lines = splitFileLines2(content);
|
|
1463
|
+
const patterns = definitionPatterns(symbol);
|
|
1464
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
1465
|
+
const line = lines[index] ?? "";
|
|
1466
|
+
if (!patterns.some((pattern) => pattern.test(line))) {
|
|
1467
|
+
continue;
|
|
1468
|
+
}
|
|
1469
|
+
return {
|
|
1470
|
+
column: Math.max(0, line.indexOf(symbol === "default" ? "default" : symbol)),
|
|
1471
|
+
line: index + 1
|
|
1472
|
+
};
|
|
1473
|
+
}
|
|
1474
|
+
return null;
|
|
1475
|
+
}
|
|
1476
|
+
function definitionPatterns(symbol) {
|
|
1477
|
+
if (symbol === "default") {
|
|
1478
|
+
return [/^\s*export\s+default\b/];
|
|
1479
|
+
}
|
|
1480
|
+
const escaped = escapeRegex(symbol);
|
|
1481
|
+
return [
|
|
1482
|
+
new RegExp(`^\\s*(?:export\\s+)?(?:declare\\s+)?(?:async\\s+)?function\\s+${escaped}\\b`),
|
|
1483
|
+
new RegExp(`^\\s*(?:export\\s+)?(?:declare\\s+)?(?:const|let|var)\\s+${escaped}\\b`),
|
|
1484
|
+
new RegExp(
|
|
1485
|
+
`^\\s*(?:export\\s+)?(?:declare\\s+)?(?:class|interface|type|enum)\\s+${escaped}\\b`
|
|
1486
|
+
),
|
|
1487
|
+
new RegExp(`^\\s*export\\s+default\\s+(?:async\\s+)?function\\s+${escaped}\\b`),
|
|
1488
|
+
new RegExp(`^\\s*export\\s+default\\s+class\\s+${escaped}\\b`)
|
|
1489
|
+
];
|
|
1490
|
+
}
|
|
1491
|
+
function definitionGrepPattern(symbol) {
|
|
1492
|
+
const escaped = escapeExtendedGrep(symbol);
|
|
1493
|
+
return [
|
|
1494
|
+
`^[[:space:]]*(export[[:space:]]+)?(declare[[:space:]]+)?(async[[:space:]]+)?function[[:space:]]+${escaped}([^[:alnum:]_$]|$)`,
|
|
1495
|
+
`^[[:space:]]*(export[[:space:]]+)?(declare[[:space:]]+)?(const|let|var)[[:space:]]+${escaped}([^[:alnum:]_$]|$)`,
|
|
1496
|
+
`^[[:space:]]*(export[[:space:]]+)?(declare[[:space:]]+)?(class|interface|type|enum)[[:space:]]+${escaped}([^[:alnum:]_$]|$)`
|
|
1497
|
+
].join("|");
|
|
1498
|
+
}
|
|
1499
|
+
function firstMeaningfulLine(content) {
|
|
1500
|
+
const lines = splitFileLines2(content);
|
|
1501
|
+
const index = lines.findIndex((line) => line.trim() !== "");
|
|
1502
|
+
return index >= 0 ? index + 1 : 1;
|
|
1503
|
+
}
|
|
1504
|
+
function parseGrepLine(rawLine, sourceRef) {
|
|
1505
|
+
const line = sourceRef ? rawLine.slice(sourceRef.length + 1) : rawLine;
|
|
1506
|
+
const firstColon = line.indexOf(":");
|
|
1507
|
+
const secondColon = line.indexOf(":", firstColon + 1);
|
|
1508
|
+
if (firstColon < 0 || secondColon < 0) {
|
|
1509
|
+
return null;
|
|
1510
|
+
}
|
|
1511
|
+
const lineNumber = Number(line.slice(firstColon + 1, secondColon));
|
|
1512
|
+
if (!Number.isFinite(lineNumber)) {
|
|
1513
|
+
return null;
|
|
1514
|
+
}
|
|
1515
|
+
return {
|
|
1516
|
+
filePath: line.slice(0, firstColon),
|
|
1517
|
+
line: lineNumber
|
|
1518
|
+
};
|
|
1519
|
+
}
|
|
1520
|
+
function repoRelativePath(repoRoot, absolutePath) {
|
|
1521
|
+
const relativePath = path7.relative(repoRoot, absolutePath);
|
|
1522
|
+
if (!relativePath || relativePath.startsWith("..") || path7.isAbsolute(relativePath)) {
|
|
1523
|
+
return null;
|
|
1524
|
+
}
|
|
1525
|
+
return relativePath.split(path7.sep).join("/");
|
|
1526
|
+
}
|
|
1527
|
+
function splitFileLines2(contents) {
|
|
1528
|
+
const lines = contents.replace(/\r\n/g, "\n").split("\n");
|
|
1529
|
+
if (lines.at(-1) === "") {
|
|
1530
|
+
lines.pop();
|
|
1531
|
+
}
|
|
1532
|
+
return lines;
|
|
1533
|
+
}
|
|
1534
|
+
function asRecord(value) {
|
|
1535
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
|
|
1536
|
+
}
|
|
1537
|
+
function isExitCode(error, exitCode) {
|
|
1538
|
+
return typeof error === "object" && error !== null && "exitCode" in error ? error.exitCode === exitCode : false;
|
|
1539
|
+
}
|
|
1540
|
+
function escapeRegex(value) {
|
|
1541
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1542
|
+
}
|
|
1543
|
+
function escapeExtendedGrep(value) {
|
|
1544
|
+
return value.replace(/[.[*^$()+?{}|\\]/g, "\\$&");
|
|
803
1545
|
}
|
|
804
1546
|
|
|
805
1547
|
// src/server/store.ts
|
|
806
1548
|
import { createHash } from "crypto";
|
|
807
|
-
import { readdir as readdir2, readFile as
|
|
808
|
-
import
|
|
1549
|
+
import { readdir as readdir2, readFile as readFile5 } from "fs/promises";
|
|
1550
|
+
import path8 from "path";
|
|
809
1551
|
import { ulid } from "ulid";
|
|
810
1552
|
|
|
811
1553
|
// src/shared/cleanup.ts
|
|
812
|
-
import { readdir, readFile as
|
|
1554
|
+
import { readdir, readFile as readFile4, rm as rm3 } from "fs/promises";
|
|
813
1555
|
var DEFAULT_REVIEW_RETENTION_DAYS = 30;
|
|
814
1556
|
var clearableStatuses = /* @__PURE__ */ new Set(["submitted", "resolved", "cancelled"]);
|
|
815
1557
|
var millisecondsPerDay = 24 * 60 * 60 * 1e3;
|
|
@@ -869,7 +1611,7 @@ function normalizeRetentionDays(value) {
|
|
|
869
1611
|
async function cleanupCandidate(reviewId, artifactDir, cutoff, skipped) {
|
|
870
1612
|
let raw;
|
|
871
1613
|
try {
|
|
872
|
-
raw = await
|
|
1614
|
+
raw = await readFile4(globalReviewMetaFile(reviewId), "utf8");
|
|
873
1615
|
} catch (error) {
|
|
874
1616
|
if (isFileNotFound(error)) {
|
|
875
1617
|
skipped.push({ reviewId, artifactDir, reason: "missing metadata" });
|
|
@@ -949,7 +1691,7 @@ async function persistedTurnCleanupState(reviewId, artifactDir, skipped) {
|
|
|
949
1691
|
async function readPersistedTurnMeta(reviewId, turnDirName, artifactDir, skipped) {
|
|
950
1692
|
let raw;
|
|
951
1693
|
try {
|
|
952
|
-
raw = await
|
|
1694
|
+
raw = await readFile4(globalReviewTurnMetaFile(reviewId, turnDirName), "utf8");
|
|
953
1695
|
} catch (error) {
|
|
954
1696
|
skipped.push({
|
|
955
1697
|
reviewId,
|
|
@@ -1476,7 +2218,7 @@ var ReviewStore = class {
|
|
|
1476
2218
|
const metaPath = globalReviewMetaFile(id);
|
|
1477
2219
|
let metaRaw;
|
|
1478
2220
|
try {
|
|
1479
|
-
metaRaw = await
|
|
2221
|
+
metaRaw = await readFile5(metaPath, "utf8");
|
|
1480
2222
|
} catch (error) {
|
|
1481
2223
|
if (isFileNotFound(error)) {
|
|
1482
2224
|
return this.loadReviewFromTurnsOnly(id);
|
|
@@ -1558,8 +2300,8 @@ var ReviewStore = class {
|
|
|
1558
2300
|
let diffRaw;
|
|
1559
2301
|
try {
|
|
1560
2302
|
[metaRaw, diffRaw] = await Promise.all([
|
|
1561
|
-
|
|
1562
|
-
|
|
2303
|
+
readFile5(metaPath, "utf8"),
|
|
2304
|
+
readFile5(diffPath, "utf8")
|
|
1563
2305
|
]);
|
|
1564
2306
|
} catch (error) {
|
|
1565
2307
|
if (isFileNotFound(error)) {
|
|
@@ -1589,7 +2331,7 @@ var ReviewStore = class {
|
|
|
1589
2331
|
const diffPath = globalReviewDiffFile(id);
|
|
1590
2332
|
let diffRaw;
|
|
1591
2333
|
try {
|
|
1592
|
-
diffRaw = await
|
|
2334
|
+
diffRaw = await readFile5(diffPath, "utf8");
|
|
1593
2335
|
} catch (error) {
|
|
1594
2336
|
if (isFileNotFound(error)) {
|
|
1595
2337
|
return null;
|
|
@@ -1769,9 +2511,9 @@ function reconcileTurn(meta, diff, feedback, resolution) {
|
|
|
1769
2511
|
status,
|
|
1770
2512
|
submittedAt: feedback?.timestamp ?? meta.submittedAt,
|
|
1771
2513
|
resolvedAt: status === "resolved" ? resolution?.resolvedAt ?? meta.resolvedAt : void 0,
|
|
1772
|
-
feedbackPath: feedback ? meta.feedbackPath ??
|
|
1773
|
-
markdownPath: feedback ? meta.markdownPath ??
|
|
1774
|
-
resolvedPath: resolution ? meta.resolvedPath ??
|
|
2514
|
+
feedbackPath: feedback ? meta.feedbackPath ?? path8.join(meta.artifactDir, "feedback.json") : void 0,
|
|
2515
|
+
markdownPath: feedback ? meta.markdownPath ?? path8.join(meta.artifactDir, "feedback.md") : void 0,
|
|
2516
|
+
resolvedPath: resolution ? meta.resolvedPath ?? path8.join(meta.artifactDir, "resolved.json") : void 0,
|
|
1775
2517
|
diff,
|
|
1776
2518
|
...feedback ? { feedback } : {},
|
|
1777
2519
|
...resolution ? { resolution } : {}
|
|
@@ -1804,7 +2546,7 @@ function requiredPath(value, label) {
|
|
|
1804
2546
|
async function readOptionalJsonFile(filePath, guard, label) {
|
|
1805
2547
|
let raw;
|
|
1806
2548
|
try {
|
|
1807
|
-
raw = await
|
|
2549
|
+
raw = await readFile5(filePath, "utf8");
|
|
1808
2550
|
} catch (error) {
|
|
1809
2551
|
if (isFileNotFound(error)) {
|
|
1810
2552
|
return void 0;
|
|
@@ -1849,6 +2591,10 @@ function createApp(origin2, options = {}) {
|
|
|
1849
2591
|
};
|
|
1850
2592
|
return c.json(response);
|
|
1851
2593
|
});
|
|
2594
|
+
app.get("/api/open-targets", async (c) => {
|
|
2595
|
+
const response = { targets: await availableOpenFileTargets() };
|
|
2596
|
+
return c.json(response);
|
|
2597
|
+
});
|
|
1852
2598
|
app.get("/api/reviews", async (c) => {
|
|
1853
2599
|
const response = { reviews: await reviewStore.list() };
|
|
1854
2600
|
return c.json(response);
|
|
@@ -2075,7 +2821,7 @@ function createApp(origin2, options = {}) {
|
|
|
2075
2821
|
return c.json({ error: "turn not found" }, 404);
|
|
2076
2822
|
}
|
|
2077
2823
|
const diffPayload = turn?.diff ?? existing.diff;
|
|
2078
|
-
const repoRoot =
|
|
2824
|
+
const repoRoot = path9.resolve(diffPayload.cwd);
|
|
2079
2825
|
const pathError = validateContextPath(repoRoot, body.filePath, "filePath") ?? (body.oldPath ? validateContextPath(repoRoot, body.oldPath, "oldPath") : null);
|
|
2080
2826
|
if (pathError) {
|
|
2081
2827
|
return c.json({ error: pathError }, 400);
|
|
@@ -2110,6 +2856,129 @@ function createApp(origin2, options = {}) {
|
|
|
2110
2856
|
return c.json({ error: `context is unavailable: ${formatError(error)}` }, 409);
|
|
2111
2857
|
}
|
|
2112
2858
|
});
|
|
2859
|
+
app.post("/api/reviews/:id/source-peek", async (c) => {
|
|
2860
|
+
const id = c.req.param("id");
|
|
2861
|
+
const existing = await reviewStore.get(id);
|
|
2862
|
+
if (!existing) {
|
|
2863
|
+
return c.json({ error: "review not found" }, 404);
|
|
2864
|
+
}
|
|
2865
|
+
const parsed = await readJsonBody(c, isSourcePeekRequest, "source peek request");
|
|
2866
|
+
if (!parsed.ok) {
|
|
2867
|
+
return parsed.response;
|
|
2868
|
+
}
|
|
2869
|
+
const body = parsed.body;
|
|
2870
|
+
const turn = body.turnId ? await reviewStore.getTurn(id, body.turnId) : null;
|
|
2871
|
+
if (body.turnId && !turn) {
|
|
2872
|
+
return c.json({ error: "turn not found" }, 404);
|
|
2873
|
+
}
|
|
2874
|
+
const diffPayload = turn?.diff ?? existing.diff;
|
|
2875
|
+
const repoRoot = path9.resolve(diffPayload.cwd);
|
|
2876
|
+
const pathError = validateContextPath(repoRoot, body.filePath, "filePath") ?? (body.oldPath ? validateContextPath(repoRoot, body.oldPath, "oldPath") : null);
|
|
2877
|
+
if (pathError) {
|
|
2878
|
+
return c.json({ error: pathError }, 400);
|
|
2879
|
+
}
|
|
2880
|
+
const source = await resolveContextSource(diffPayload, body.source);
|
|
2881
|
+
if (!source.ok) {
|
|
2882
|
+
return c.json({ error: source.error }, source.status);
|
|
2883
|
+
}
|
|
2884
|
+
const reviewFile = source.files.find((file) => file.path === body.filePath);
|
|
2885
|
+
if (!reviewFile) {
|
|
2886
|
+
return c.json({ error: "file is not part of this review context" }, 404);
|
|
2887
|
+
}
|
|
2888
|
+
if ((reviewFile.oldPath ?? null) !== body.oldPath) {
|
|
2889
|
+
return c.json({ error: "oldPath does not match the reviewed file" }, 400);
|
|
2890
|
+
}
|
|
2891
|
+
if (reviewFile.isBinary) {
|
|
2892
|
+
return c.json({ error: "binary file source peek is not available" }, 409);
|
|
2893
|
+
}
|
|
2894
|
+
if (body.side === "L" && reviewFile.isNew) {
|
|
2895
|
+
return c.json({ error: "new files do not have an old-side source" }, 409);
|
|
2896
|
+
}
|
|
2897
|
+
if (body.side === "R" && reviewFile.isDeleted) {
|
|
2898
|
+
return c.json({ error: "deleted files do not have a new-side source" }, 409);
|
|
2899
|
+
}
|
|
2900
|
+
const sourceFilePath = body.side === "L" ? body.oldPath ?? body.filePath : body.filePath;
|
|
2901
|
+
const sourceRef = body.side === "L" ? source.oldRef : source.newRef;
|
|
2902
|
+
try {
|
|
2903
|
+
return c.json(
|
|
2904
|
+
await resolveSourcePeek({
|
|
2905
|
+
repoRoot,
|
|
2906
|
+
sourceFilePath,
|
|
2907
|
+
sourceRef,
|
|
2908
|
+
symbol: body.symbol,
|
|
2909
|
+
line: body.line,
|
|
2910
|
+
column: body.column
|
|
2911
|
+
})
|
|
2912
|
+
);
|
|
2913
|
+
} catch (error) {
|
|
2914
|
+
return c.json({ error: `source peek unavailable: ${formatError(error)}` }, 404);
|
|
2915
|
+
}
|
|
2916
|
+
});
|
|
2917
|
+
app.post("/api/reviews/:id/files/content", async (c) => {
|
|
2918
|
+
const id = c.req.param("id");
|
|
2919
|
+
const existing = await reviewStore.get(id);
|
|
2920
|
+
if (!existing) {
|
|
2921
|
+
return c.json({ error: "review not found" }, 404);
|
|
2922
|
+
}
|
|
2923
|
+
const parsed = await readJsonBody(c, isFileContentRequest, "file content request");
|
|
2924
|
+
if (!parsed.ok) {
|
|
2925
|
+
return parsed.response;
|
|
2926
|
+
}
|
|
2927
|
+
const { filePath, scope = "review", turnId } = parsed.body;
|
|
2928
|
+
if (!filePath || filePath.includes("\0") || path9.isAbsolute(filePath)) {
|
|
2929
|
+
return c.json({ error: "filePath must be a repo-relative path" }, 400);
|
|
2930
|
+
}
|
|
2931
|
+
const repoRoot = path9.resolve(existing.diff.cwd);
|
|
2932
|
+
const requestedAbsolutePath = path9.resolve(repoRoot, filePath);
|
|
2933
|
+
if (!isPathWithin(repoRoot, requestedAbsolutePath)) {
|
|
2934
|
+
return c.json({ error: "filePath must stay within the review cwd" }, 400);
|
|
2935
|
+
}
|
|
2936
|
+
const turn = turnId ? await reviewStore.getTurn(id, turnId) : null;
|
|
2937
|
+
if (turnId && !turn) {
|
|
2938
|
+
return c.json({ error: "turn not found" }, 404);
|
|
2939
|
+
}
|
|
2940
|
+
if (scope === "review") {
|
|
2941
|
+
const diffPayload = turn?.diff ?? existing.diff;
|
|
2942
|
+
const reviewFiles = [
|
|
2943
|
+
...diffPayload.files,
|
|
2944
|
+
...(diffPayload.commitDiffs ?? []).flatMap((commitDiff) => commitDiff.files)
|
|
2945
|
+
].filter((file) => file.path === filePath);
|
|
2946
|
+
if (reviewFiles.length === 0) {
|
|
2947
|
+
return c.json({ error: "file is not part of this review" }, 404);
|
|
2948
|
+
}
|
|
2949
|
+
if (reviewFiles.every((file) => file.isDeleted)) {
|
|
2950
|
+
return c.json({ error: "deleted files cannot be copied" }, 409);
|
|
2951
|
+
}
|
|
2952
|
+
if (reviewFiles.some((file) => file.isBinary)) {
|
|
2953
|
+
return c.json({ error: "binary file contents cannot be copied" }, 409);
|
|
2954
|
+
}
|
|
2955
|
+
}
|
|
2956
|
+
let realRepoRoot;
|
|
2957
|
+
let realFilePath;
|
|
2958
|
+
try {
|
|
2959
|
+
[realRepoRoot, realFilePath] = await Promise.all([
|
|
2960
|
+
realpath(repoRoot),
|
|
2961
|
+
realpath(requestedAbsolutePath)
|
|
2962
|
+
]);
|
|
2963
|
+
} catch (error) {
|
|
2964
|
+
if (isFileNotFound(error)) {
|
|
2965
|
+
return c.json({ error: "file no longer exists on disk" }, 404);
|
|
2966
|
+
}
|
|
2967
|
+
throw error;
|
|
2968
|
+
}
|
|
2969
|
+
if (!isPathWithin(realRepoRoot, realFilePath)) {
|
|
2970
|
+
return c.json({ error: "filePath must stay within the review cwd" }, 400);
|
|
2971
|
+
}
|
|
2972
|
+
const fileStats = await stat(realFilePath);
|
|
2973
|
+
if (!fileStats.isFile()) {
|
|
2974
|
+
return c.json({ error: "path is not a file" }, 409);
|
|
2975
|
+
}
|
|
2976
|
+
const response = {
|
|
2977
|
+
content: await readFile6(realFilePath, "utf8"),
|
|
2978
|
+
filePath
|
|
2979
|
+
};
|
|
2980
|
+
return c.json(response);
|
|
2981
|
+
});
|
|
2113
2982
|
app.post("/api/reviews/:id/files/open", async (c) => {
|
|
2114
2983
|
const id = c.req.param("id");
|
|
2115
2984
|
const existing = await reviewStore.get(id);
|
|
@@ -2120,12 +2989,12 @@ function createApp(origin2, options = {}) {
|
|
|
2120
2989
|
if (!parsed.ok) {
|
|
2121
2990
|
return parsed.response;
|
|
2122
2991
|
}
|
|
2123
|
-
const { filePath, turnId } = parsed.body;
|
|
2124
|
-
if (!filePath || filePath.includes("\0") ||
|
|
2992
|
+
const { filePath, scope = "review", target, turnId } = parsed.body;
|
|
2993
|
+
if (!filePath || filePath.includes("\0") || path9.isAbsolute(filePath)) {
|
|
2125
2994
|
return c.json({ error: "filePath must be a repo-relative path" }, 400);
|
|
2126
2995
|
}
|
|
2127
|
-
const repoRoot =
|
|
2128
|
-
const requestedAbsolutePath =
|
|
2996
|
+
const repoRoot = path9.resolve(existing.diff.cwd);
|
|
2997
|
+
const requestedAbsolutePath = path9.resolve(repoRoot, filePath);
|
|
2129
2998
|
if (!isPathWithin(repoRoot, requestedAbsolutePath)) {
|
|
2130
2999
|
return c.json({ error: "filePath must stay within the review cwd" }, 400);
|
|
2131
3000
|
}
|
|
@@ -2133,16 +3002,18 @@ function createApp(origin2, options = {}) {
|
|
|
2133
3002
|
if (turnId && !turn) {
|
|
2134
3003
|
return c.json({ error: "turn not found" }, 404);
|
|
2135
3004
|
}
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
3005
|
+
if (scope === "review") {
|
|
3006
|
+
const diffPayload = turn?.diff ?? existing.diff;
|
|
3007
|
+
const reviewFiles = [
|
|
3008
|
+
...diffPayload.files,
|
|
3009
|
+
...(diffPayload.commitDiffs ?? []).flatMap((commitDiff) => commitDiff.files)
|
|
3010
|
+
].filter((file) => file.path === filePath);
|
|
3011
|
+
if (reviewFiles.length === 0) {
|
|
3012
|
+
return c.json({ error: "file is not part of this review" }, 404);
|
|
3013
|
+
}
|
|
3014
|
+
if (reviewFiles.every((file) => file.isDeleted)) {
|
|
3015
|
+
return c.json({ error: "deleted files cannot be opened locally" }, 409);
|
|
3016
|
+
}
|
|
2146
3017
|
}
|
|
2147
3018
|
let realRepoRoot;
|
|
2148
3019
|
let realFilePath;
|
|
@@ -2165,7 +3036,7 @@ function createApp(origin2, options = {}) {
|
|
|
2165
3036
|
return c.json({ error: "path is not a file" }, 409);
|
|
2166
3037
|
}
|
|
2167
3038
|
try {
|
|
2168
|
-
await openLocalPath(realFilePath);
|
|
3039
|
+
target ? await openLocalPath(realFilePath, target) : await openLocalPath(realFilePath);
|
|
2169
3040
|
} catch (error) {
|
|
2170
3041
|
return c.json({ error: `could not open file: ${formatError(error)}` }, 500);
|
|
2171
3042
|
}
|
|
@@ -2241,13 +3112,13 @@ function createApp(origin2, options = {}) {
|
|
|
2241
3112
|
}
|
|
2242
3113
|
async function serveAsset(c) {
|
|
2243
3114
|
const requestPath = new URL(c.req.url).pathname.replace(/^\/assets\//, "");
|
|
2244
|
-
const normalized =
|
|
2245
|
-
const assetPath =
|
|
3115
|
+
const normalized = path9.normalize(requestPath).replace(/^(\.\.(\/|\\|$))+/, "");
|
|
3116
|
+
const assetPath = path9.join(webRoot, "assets", normalized);
|
|
2246
3117
|
try {
|
|
2247
|
-
const body = await
|
|
3118
|
+
const body = await readFile6(assetPath);
|
|
2248
3119
|
return new Response(body, {
|
|
2249
3120
|
headers: {
|
|
2250
|
-
"content-type": mimeTypes[
|
|
3121
|
+
"content-type": mimeTypes[path9.extname(assetPath)] ?? "application/octet-stream"
|
|
2251
3122
|
}
|
|
2252
3123
|
});
|
|
2253
3124
|
} catch (error) {
|
|
@@ -2259,7 +3130,7 @@ async function serveAsset(c) {
|
|
|
2259
3130
|
}
|
|
2260
3131
|
async function serveIndex() {
|
|
2261
3132
|
try {
|
|
2262
|
-
const body = await
|
|
3133
|
+
const body = await readFile6(path9.join(webRoot, "index.html"));
|
|
2263
3134
|
return new Response(body, {
|
|
2264
3135
|
headers: { "content-type": "text/html; charset=utf-8" }
|
|
2265
3136
|
});
|
|
@@ -2273,7 +3144,7 @@ async function serveIndex() {
|
|
|
2273
3144
|
function serveRootFile(fileName, contentType) {
|
|
2274
3145
|
return async () => {
|
|
2275
3146
|
try {
|
|
2276
|
-
const body = await
|
|
3147
|
+
const body = await readFile6(path9.join(webRoot, fileName));
|
|
2277
3148
|
return new Response(body, {
|
|
2278
3149
|
headers: { "content-type": contentType }
|
|
2279
3150
|
});
|
|
@@ -2305,8 +3176,8 @@ async function readJsonBody(c, guard, label) {
|
|
|
2305
3176
|
}
|
|
2306
3177
|
}
|
|
2307
3178
|
function isPathWithin(parentPath, childPath) {
|
|
2308
|
-
const relative =
|
|
2309
|
-
return relative === "" || !relative.startsWith("..") && !
|
|
3179
|
+
const relative = path9.relative(parentPath, childPath);
|
|
3180
|
+
return relative === "" || !relative.startsWith("..") && !path9.isAbsolute(relative);
|
|
2310
3181
|
}
|
|
2311
3182
|
async function resolveContextSource(diffPayload, source) {
|
|
2312
3183
|
if (source.mode === "turn") {
|
|
@@ -2354,10 +3225,10 @@ async function resolveContextSource(diffPayload, source) {
|
|
|
2354
3225
|
};
|
|
2355
3226
|
}
|
|
2356
3227
|
function validateContextPath(repoRoot, filePath, label) {
|
|
2357
|
-
if (!filePath || filePath.includes("\0") ||
|
|
3228
|
+
if (!filePath || filePath.includes("\0") || path9.isAbsolute(filePath)) {
|
|
2358
3229
|
return `${label} must be a repo-relative path`;
|
|
2359
3230
|
}
|
|
2360
|
-
const requestedAbsolutePath =
|
|
3231
|
+
const requestedAbsolutePath = path9.resolve(repoRoot, filePath);
|
|
2361
3232
|
if (!isPathWithin(repoRoot, requestedAbsolutePath)) {
|
|
2362
3233
|
return `${label} must stay within the review cwd`;
|
|
2363
3234
|
}
|