getgloss 0.11.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 +1 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/server/daemon.js +915 -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-CtfqKZw2.js → syntax-CEpwlb09.js} +6 -6
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-BUtA99HE.css +0 -1
- package/dist/web/assets/index-DUOP7jxW.js +0 -269
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",
|
|
@@ -201,8 +201,44 @@ var DIFF_LINE_TYPES = ["context", "add", "delete"];
|
|
|
201
201
|
var DIFF_SCOPE_MODES = ["working", "branch", "explicit"];
|
|
202
202
|
var DIFF_FALLBACK_REASONS = ["working-tree-clean", "missing-branch-base"];
|
|
203
203
|
var DIFF_CONTEXT_MAX_LINES = 500;
|
|
204
|
+
var SOURCE_PEEK_MAX_BYTES = 35e4;
|
|
204
205
|
var REVIEW_SCOPE_MODES = ["all", "single", "range"];
|
|
205
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
|
+
];
|
|
206
242
|
|
|
207
243
|
// src/shared/validation.ts
|
|
208
244
|
function parseJson(raw, guard, label) {
|
|
@@ -222,7 +258,10 @@ function isClearReviewsRequest(value) {
|
|
|
222
258
|
return isRecord(value) && isOptionalNonNegativeInteger(value.olderThanDays) && isOptionalBoolean(value.dryRun);
|
|
223
259
|
}
|
|
224
260
|
function isOpenFileRequest(value) {
|
|
225
|
-
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);
|
|
226
265
|
}
|
|
227
266
|
function isCommitRangeDiffRequest(value) {
|
|
228
267
|
return isRecord(value) && isString(value.fromSha) && isString(value.toSha) && isOptionalString(value.turnId);
|
|
@@ -230,6 +269,9 @@ function isCommitRangeDiffRequest(value) {
|
|
|
230
269
|
function isDiffContextRequest(value) {
|
|
231
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);
|
|
232
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
|
+
}
|
|
233
275
|
function isSubmitReviewRequest(value) {
|
|
234
276
|
return isRecord(value) && isArrayOf(value.comments, isComment) && isOptional(value.reviewScope, isReviewScope);
|
|
235
277
|
}
|
|
@@ -333,6 +375,12 @@ function isReviewStatus(value) {
|
|
|
333
375
|
function isResolutionStatus(value) {
|
|
334
376
|
return isOneOf(value, RESOLUTION_STATUSES);
|
|
335
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
|
+
}
|
|
336
384
|
function isRecord(value) {
|
|
337
385
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
338
386
|
}
|
|
@@ -357,6 +405,12 @@ function isNumber(value) {
|
|
|
357
405
|
function isPositiveInteger(value) {
|
|
358
406
|
return isNumber(value) && Number.isInteger(value) && value > 0;
|
|
359
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
|
+
}
|
|
360
414
|
function isOptionalNumber(value) {
|
|
361
415
|
return value === void 0 || isNumber(value);
|
|
362
416
|
}
|
|
@@ -515,8 +569,8 @@ function createIdleScheduler(options) {
|
|
|
515
569
|
}
|
|
516
570
|
|
|
517
571
|
// src/server/index.ts
|
|
518
|
-
import { readFile as
|
|
519
|
-
import
|
|
572
|
+
import { readFile as readFile6, realpath, stat } from "fs/promises";
|
|
573
|
+
import path9 from "path";
|
|
520
574
|
import { fileURLToPath } from "url";
|
|
521
575
|
import { Hono } from "hono";
|
|
522
576
|
import { streamSSE } from "hono/streaming";
|
|
@@ -798,19 +852,706 @@ function isResolvableReviewStatus(status) {
|
|
|
798
852
|
}
|
|
799
853
|
|
|
800
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";
|
|
801
860
|
import open from "open";
|
|
802
|
-
|
|
803
|
-
|
|
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, "\\$&");
|
|
804
1545
|
}
|
|
805
1546
|
|
|
806
1547
|
// src/server/store.ts
|
|
807
1548
|
import { createHash } from "crypto";
|
|
808
|
-
import { readdir as readdir2, readFile as
|
|
809
|
-
import
|
|
1549
|
+
import { readdir as readdir2, readFile as readFile5 } from "fs/promises";
|
|
1550
|
+
import path8 from "path";
|
|
810
1551
|
import { ulid } from "ulid";
|
|
811
1552
|
|
|
812
1553
|
// src/shared/cleanup.ts
|
|
813
|
-
import { readdir, readFile as
|
|
1554
|
+
import { readdir, readFile as readFile4, rm as rm3 } from "fs/promises";
|
|
814
1555
|
var DEFAULT_REVIEW_RETENTION_DAYS = 30;
|
|
815
1556
|
var clearableStatuses = /* @__PURE__ */ new Set(["submitted", "resolved", "cancelled"]);
|
|
816
1557
|
var millisecondsPerDay = 24 * 60 * 60 * 1e3;
|
|
@@ -870,7 +1611,7 @@ function normalizeRetentionDays(value) {
|
|
|
870
1611
|
async function cleanupCandidate(reviewId, artifactDir, cutoff, skipped) {
|
|
871
1612
|
let raw;
|
|
872
1613
|
try {
|
|
873
|
-
raw = await
|
|
1614
|
+
raw = await readFile4(globalReviewMetaFile(reviewId), "utf8");
|
|
874
1615
|
} catch (error) {
|
|
875
1616
|
if (isFileNotFound(error)) {
|
|
876
1617
|
skipped.push({ reviewId, artifactDir, reason: "missing metadata" });
|
|
@@ -950,7 +1691,7 @@ async function persistedTurnCleanupState(reviewId, artifactDir, skipped) {
|
|
|
950
1691
|
async function readPersistedTurnMeta(reviewId, turnDirName, artifactDir, skipped) {
|
|
951
1692
|
let raw;
|
|
952
1693
|
try {
|
|
953
|
-
raw = await
|
|
1694
|
+
raw = await readFile4(globalReviewTurnMetaFile(reviewId, turnDirName), "utf8");
|
|
954
1695
|
} catch (error) {
|
|
955
1696
|
skipped.push({
|
|
956
1697
|
reviewId,
|
|
@@ -1477,7 +2218,7 @@ var ReviewStore = class {
|
|
|
1477
2218
|
const metaPath = globalReviewMetaFile(id);
|
|
1478
2219
|
let metaRaw;
|
|
1479
2220
|
try {
|
|
1480
|
-
metaRaw = await
|
|
2221
|
+
metaRaw = await readFile5(metaPath, "utf8");
|
|
1481
2222
|
} catch (error) {
|
|
1482
2223
|
if (isFileNotFound(error)) {
|
|
1483
2224
|
return this.loadReviewFromTurnsOnly(id);
|
|
@@ -1559,8 +2300,8 @@ var ReviewStore = class {
|
|
|
1559
2300
|
let diffRaw;
|
|
1560
2301
|
try {
|
|
1561
2302
|
[metaRaw, diffRaw] = await Promise.all([
|
|
1562
|
-
|
|
1563
|
-
|
|
2303
|
+
readFile5(metaPath, "utf8"),
|
|
2304
|
+
readFile5(diffPath, "utf8")
|
|
1564
2305
|
]);
|
|
1565
2306
|
} catch (error) {
|
|
1566
2307
|
if (isFileNotFound(error)) {
|
|
@@ -1590,7 +2331,7 @@ var ReviewStore = class {
|
|
|
1590
2331
|
const diffPath = globalReviewDiffFile(id);
|
|
1591
2332
|
let diffRaw;
|
|
1592
2333
|
try {
|
|
1593
|
-
diffRaw = await
|
|
2334
|
+
diffRaw = await readFile5(diffPath, "utf8");
|
|
1594
2335
|
} catch (error) {
|
|
1595
2336
|
if (isFileNotFound(error)) {
|
|
1596
2337
|
return null;
|
|
@@ -1770,9 +2511,9 @@ function reconcileTurn(meta, diff, feedback, resolution) {
|
|
|
1770
2511
|
status,
|
|
1771
2512
|
submittedAt: feedback?.timestamp ?? meta.submittedAt,
|
|
1772
2513
|
resolvedAt: status === "resolved" ? resolution?.resolvedAt ?? meta.resolvedAt : void 0,
|
|
1773
|
-
feedbackPath: feedback ? meta.feedbackPath ??
|
|
1774
|
-
markdownPath: feedback ? meta.markdownPath ??
|
|
1775
|
-
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,
|
|
1776
2517
|
diff,
|
|
1777
2518
|
...feedback ? { feedback } : {},
|
|
1778
2519
|
...resolution ? { resolution } : {}
|
|
@@ -1805,7 +2546,7 @@ function requiredPath(value, label) {
|
|
|
1805
2546
|
async function readOptionalJsonFile(filePath, guard, label) {
|
|
1806
2547
|
let raw;
|
|
1807
2548
|
try {
|
|
1808
|
-
raw = await
|
|
2549
|
+
raw = await readFile5(filePath, "utf8");
|
|
1809
2550
|
} catch (error) {
|
|
1810
2551
|
if (isFileNotFound(error)) {
|
|
1811
2552
|
return void 0;
|
|
@@ -1850,6 +2591,10 @@ function createApp(origin2, options = {}) {
|
|
|
1850
2591
|
};
|
|
1851
2592
|
return c.json(response);
|
|
1852
2593
|
});
|
|
2594
|
+
app.get("/api/open-targets", async (c) => {
|
|
2595
|
+
const response = { targets: await availableOpenFileTargets() };
|
|
2596
|
+
return c.json(response);
|
|
2597
|
+
});
|
|
1853
2598
|
app.get("/api/reviews", async (c) => {
|
|
1854
2599
|
const response = { reviews: await reviewStore.list() };
|
|
1855
2600
|
return c.json(response);
|
|
@@ -2076,7 +2821,7 @@ function createApp(origin2, options = {}) {
|
|
|
2076
2821
|
return c.json({ error: "turn not found" }, 404);
|
|
2077
2822
|
}
|
|
2078
2823
|
const diffPayload = turn?.diff ?? existing.diff;
|
|
2079
|
-
const repoRoot =
|
|
2824
|
+
const repoRoot = path9.resolve(diffPayload.cwd);
|
|
2080
2825
|
const pathError = validateContextPath(repoRoot, body.filePath, "filePath") ?? (body.oldPath ? validateContextPath(repoRoot, body.oldPath, "oldPath") : null);
|
|
2081
2826
|
if (pathError) {
|
|
2082
2827
|
return c.json({ error: pathError }, 400);
|
|
@@ -2111,6 +2856,129 @@ function createApp(origin2, options = {}) {
|
|
|
2111
2856
|
return c.json({ error: `context is unavailable: ${formatError(error)}` }, 409);
|
|
2112
2857
|
}
|
|
2113
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
|
+
});
|
|
2114
2982
|
app.post("/api/reviews/:id/files/open", async (c) => {
|
|
2115
2983
|
const id = c.req.param("id");
|
|
2116
2984
|
const existing = await reviewStore.get(id);
|
|
@@ -2121,12 +2989,12 @@ function createApp(origin2, options = {}) {
|
|
|
2121
2989
|
if (!parsed.ok) {
|
|
2122
2990
|
return parsed.response;
|
|
2123
2991
|
}
|
|
2124
|
-
const { filePath, turnId } = parsed.body;
|
|
2125
|
-
if (!filePath || filePath.includes("\0") ||
|
|
2992
|
+
const { filePath, scope = "review", target, turnId } = parsed.body;
|
|
2993
|
+
if (!filePath || filePath.includes("\0") || path9.isAbsolute(filePath)) {
|
|
2126
2994
|
return c.json({ error: "filePath must be a repo-relative path" }, 400);
|
|
2127
2995
|
}
|
|
2128
|
-
const repoRoot =
|
|
2129
|
-
const requestedAbsolutePath =
|
|
2996
|
+
const repoRoot = path9.resolve(existing.diff.cwd);
|
|
2997
|
+
const requestedAbsolutePath = path9.resolve(repoRoot, filePath);
|
|
2130
2998
|
if (!isPathWithin(repoRoot, requestedAbsolutePath)) {
|
|
2131
2999
|
return c.json({ error: "filePath must stay within the review cwd" }, 400);
|
|
2132
3000
|
}
|
|
@@ -2134,16 +3002,18 @@ function createApp(origin2, options = {}) {
|
|
|
2134
3002
|
if (turnId && !turn) {
|
|
2135
3003
|
return c.json({ error: "turn not found" }, 404);
|
|
2136
3004
|
}
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
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
|
+
}
|
|
2147
3017
|
}
|
|
2148
3018
|
let realRepoRoot;
|
|
2149
3019
|
let realFilePath;
|
|
@@ -2166,7 +3036,7 @@ function createApp(origin2, options = {}) {
|
|
|
2166
3036
|
return c.json({ error: "path is not a file" }, 409);
|
|
2167
3037
|
}
|
|
2168
3038
|
try {
|
|
2169
|
-
await openLocalPath(realFilePath);
|
|
3039
|
+
target ? await openLocalPath(realFilePath, target) : await openLocalPath(realFilePath);
|
|
2170
3040
|
} catch (error) {
|
|
2171
3041
|
return c.json({ error: `could not open file: ${formatError(error)}` }, 500);
|
|
2172
3042
|
}
|
|
@@ -2242,13 +3112,13 @@ function createApp(origin2, options = {}) {
|
|
|
2242
3112
|
}
|
|
2243
3113
|
async function serveAsset(c) {
|
|
2244
3114
|
const requestPath = new URL(c.req.url).pathname.replace(/^\/assets\//, "");
|
|
2245
|
-
const normalized =
|
|
2246
|
-
const assetPath =
|
|
3115
|
+
const normalized = path9.normalize(requestPath).replace(/^(\.\.(\/|\\|$))+/, "");
|
|
3116
|
+
const assetPath = path9.join(webRoot, "assets", normalized);
|
|
2247
3117
|
try {
|
|
2248
|
-
const body = await
|
|
3118
|
+
const body = await readFile6(assetPath);
|
|
2249
3119
|
return new Response(body, {
|
|
2250
3120
|
headers: {
|
|
2251
|
-
"content-type": mimeTypes[
|
|
3121
|
+
"content-type": mimeTypes[path9.extname(assetPath)] ?? "application/octet-stream"
|
|
2252
3122
|
}
|
|
2253
3123
|
});
|
|
2254
3124
|
} catch (error) {
|
|
@@ -2260,7 +3130,7 @@ async function serveAsset(c) {
|
|
|
2260
3130
|
}
|
|
2261
3131
|
async function serveIndex() {
|
|
2262
3132
|
try {
|
|
2263
|
-
const body = await
|
|
3133
|
+
const body = await readFile6(path9.join(webRoot, "index.html"));
|
|
2264
3134
|
return new Response(body, {
|
|
2265
3135
|
headers: { "content-type": "text/html; charset=utf-8" }
|
|
2266
3136
|
});
|
|
@@ -2274,7 +3144,7 @@ async function serveIndex() {
|
|
|
2274
3144
|
function serveRootFile(fileName, contentType) {
|
|
2275
3145
|
return async () => {
|
|
2276
3146
|
try {
|
|
2277
|
-
const body = await
|
|
3147
|
+
const body = await readFile6(path9.join(webRoot, fileName));
|
|
2278
3148
|
return new Response(body, {
|
|
2279
3149
|
headers: { "content-type": contentType }
|
|
2280
3150
|
});
|
|
@@ -2306,8 +3176,8 @@ async function readJsonBody(c, guard, label) {
|
|
|
2306
3176
|
}
|
|
2307
3177
|
}
|
|
2308
3178
|
function isPathWithin(parentPath, childPath) {
|
|
2309
|
-
const relative =
|
|
2310
|
-
return relative === "" || !relative.startsWith("..") && !
|
|
3179
|
+
const relative = path9.relative(parentPath, childPath);
|
|
3180
|
+
return relative === "" || !relative.startsWith("..") && !path9.isAbsolute(relative);
|
|
2311
3181
|
}
|
|
2312
3182
|
async function resolveContextSource(diffPayload, source) {
|
|
2313
3183
|
if (source.mode === "turn") {
|
|
@@ -2355,10 +3225,10 @@ async function resolveContextSource(diffPayload, source) {
|
|
|
2355
3225
|
};
|
|
2356
3226
|
}
|
|
2357
3227
|
function validateContextPath(repoRoot, filePath, label) {
|
|
2358
|
-
if (!filePath || filePath.includes("\0") ||
|
|
3228
|
+
if (!filePath || filePath.includes("\0") || path9.isAbsolute(filePath)) {
|
|
2359
3229
|
return `${label} must be a repo-relative path`;
|
|
2360
3230
|
}
|
|
2361
|
-
const requestedAbsolutePath =
|
|
3231
|
+
const requestedAbsolutePath = path9.resolve(repoRoot, filePath);
|
|
2362
3232
|
if (!isPathWithin(repoRoot, requestedAbsolutePath)) {
|
|
2363
3233
|
return `${label} must stay within the review cwd`;
|
|
2364
3234
|
}
|