@skyramp/mcp 0.3.8 → 0.4.0-rc.1
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/build/commands/commandLibrary.d.ts +1 -1
- package/build/commands/commandLibrary.js +3 -3
- package/build/commands/recommendTestsAndExecuteCommand.d.ts +1 -1
- package/build/commands/recommendTestsAndExecuteCommand.js +35 -20
- package/build/commands/testThisEndpointCommand.js +35 -19
- package/build/index.js +9 -3
- package/build/playwright/blueprintDigest.d.ts +15 -0
- package/build/playwright/blueprintDigest.js +152 -0
- package/build/playwright/blueprintDigestStore.d.ts +31 -0
- package/build/playwright/blueprintDigestStore.js +117 -0
- package/build/playwright/registerPlaywrightTools.js +60 -12
- package/build/playwright/traceRecordingPrompt.js +8 -7
- package/build/prompts/enhance-assertions/sharedAssertionRules.js +9 -8
- package/build/prompts/enhance-assertions/uiAssertionsPrompt.js +24 -2
- package/build/prompts/promptAssets.d.ts +20 -0
- package/build/prompts/promptAssets.js +55 -0
- package/build/prompts/sut-setup/modes/dockerComposePrompt.js +19 -5
- package/build/prompts/test-maintenance/actionsInstructions.d.ts +4 -0
- package/build/prompts/test-maintenance/actionsInstructions.js +14 -2
- package/build/prompts/test-maintenance/drift-analysis-prompt.d.ts +0 -10
- package/build/prompts/test-maintenance/drift-analysis-prompt.js +2 -11
- package/build/prompts/test-maintenance/uiDriftAnalysisSections.js +8 -4
- package/build/prompts/test-recommendation/diffExecutionPlan.d.ts +5 -22
- package/build/prompts/test-recommendation/diffExecutionPlan.js +37 -465
- package/build/prompts/test-recommendation/recommendationSections.d.ts +7 -17
- package/build/prompts/test-recommendation/recommendationSections.js +67 -309
- package/build/prompts/test-recommendation/recommendationShared.d.ts +19 -47
- package/build/prompts/test-recommendation/recommendationShared.js +49 -155
- package/build/prompts/test-recommendation/registerRecommendTestsPrompt.d.ts +0 -5
- package/build/prompts/test-recommendation/registerRecommendTestsPrompt.js +10 -153
- package/build/prompts/test-recommendation/test-recommendation-prompt.d.ts +2 -29
- package/build/prompts/test-recommendation/test-recommendation-prompt.js +32 -457
- package/build/prompts/testbot/planDeclarations.d.ts +6 -0
- package/build/prompts/testbot/planDeclarations.js +9 -0
- package/build/prompts/testbot/testbot-prompts.d.ts +8 -0
- package/build/prompts/testbot/testbot-prompts.js +256 -381
- package/build/recommendation/answers.d.ts +35 -0
- package/build/recommendation/answers.js +96 -0
- package/build/recommendation/registerPlan.d.ts +49 -0
- package/build/recommendation/registerPlan.js +117 -0
- package/build/recommendation/runVerifiers.d.ts +10 -0
- package/build/recommendation/runVerifiers.js +49 -0
- package/build/recommendation/subjectStep.d.ts +42 -0
- package/build/recommendation/subjectStep.js +86 -0
- package/build/recommendation/types.d.ts +163 -0
- package/build/recommendation/types.js +20 -0
- package/build/recommendation/verifierContracts.d.ts +382 -0
- package/build/recommendation/verifierContracts.js +263 -0
- package/build/recommendation/verifiers/changedFile.d.ts +2 -0
- package/build/recommendation/verifiers/changedFile.js +82 -0
- package/build/recommendation/verifiers/citedPath.d.ts +12 -0
- package/build/recommendation/verifiers/citedPath.js +35 -0
- package/build/recommendation/verifiers/coverage.d.ts +7 -0
- package/build/recommendation/verifiers/coverage.js +617 -0
- package/build/recommendation/verifiers/deliveredMatchesPlan.d.ts +11 -0
- package/build/recommendation/verifiers/deliveredMatchesPlan.js +33 -0
- package/build/recommendation/verifiers/endpointGrounded.d.ts +17 -0
- package/build/recommendation/verifiers/endpointGrounded.js +128 -0
- package/build/recommendation/verifiers/existingCoverage.d.ts +6 -0
- package/build/recommendation/verifiers/existingCoverage.js +51 -0
- package/build/recommendation/verifiers/expectedOutcome.d.ts +31 -0
- package/build/recommendation/verifiers/expectedOutcome.js +105 -0
- package/build/recommendation/verifiers/removedElementGuarded.d.ts +2 -0
- package/build/recommendation/verifiers/removedElementGuarded.js +57 -0
- package/build/recommendation/verifiers/reportedCategory.d.ts +26 -0
- package/build/recommendation/verifiers/reportedCategory.js +84 -0
- package/build/recommendation/verifiers/screenRoute.d.ts +10 -0
- package/build/recommendation/verifiers/screenRoute.js +118 -0
- package/build/recommendation/verifiers/statedDifference.d.ts +6 -0
- package/build/recommendation/verifiers/statedDifference.js +140 -0
- package/build/recommendation/verifiers/uiElementGrounded.d.ts +7 -0
- package/build/recommendation/verifiers/uiElementGrounded.js +318 -0
- package/build/resources/analysisResources.js +1 -114
- package/build/resources/testbotResource.js +23 -13
- package/build/services/ModularizationService.js +2 -1
- package/build/services/TestDiscoveryService.d.ts +3 -72
- package/build/services/TestDiscoveryService.js +10 -303
- package/build/services/containerEnv.d.ts +1 -1
- package/build/services/containerEnv.js +12 -0
- package/build/skills/fixTestImportErrorsSkill.d.ts +13 -0
- package/build/skills/fixTestImportErrorsSkill.js +20 -0
- package/build/toolNames.d.ts +1 -0
- package/build/toolNames.js +1 -0
- package/build/tools/code-refactor/enhanceAssertionsTool.js +3 -3
- package/build/tools/code-refactor/modularizationTool.js +2 -1
- package/build/tools/executeSkyrampTestTool.d.ts +80 -0
- package/build/tools/executeSkyrampTestTool.js +246 -19
- package/build/tools/generate-tests/generateBatchScenarioRestTool.js +6 -0
- package/build/tools/generate-tests/generateContractRestTool.js +3 -3
- package/build/tools/generate-tests/planGuard.d.ts +2 -2
- package/build/tools/generate-tests/planGuard.js +78 -18
- package/build/tools/one-click/oneClickTool.d.ts +0 -1
- package/build/tools/one-click/oneClickTool.js +0 -5
- package/build/tools/submitReportTool.d.ts +48 -42
- package/build/tools/submitReportTool.js +576 -193
- package/build/tools/test-management/actionsTool.js +72 -4
- package/build/tools/test-management/analyzeChangesTool.d.ts +144 -48
- package/build/tools/test-management/analyzeChangesTool.js +212 -1219
- package/build/tools/test-management/analyzeTestHealthTool.js +13 -24
- package/build/tools/test-management/index.d.ts +1 -0
- package/build/tools/test-management/index.js +1 -0
- package/build/tools/test-management/registerTestPlanTool.d.ts +795 -172
- package/build/tools/test-management/registerTestPlanTool.js +609 -542
- package/build/tools/test-management/resolveScreenTool.d.ts +75 -0
- package/build/tools/test-management/resolveScreenTool.js +289 -0
- package/build/types/BlueprintDigest.d.ts +34 -0
- package/build/types/BlueprintDigest.js +1 -0
- package/build/types/RepositoryAnalysis.d.ts +20 -1559
- package/build/types/RepositoryAnalysis.js +2 -58
- package/build/types/StepMethod.d.ts +40 -0
- package/build/types/StepMethod.js +77 -0
- package/build/types/TestAnalysis.d.ts +12 -0
- package/build/types/TestExecution.d.ts +4 -0
- package/build/types/TestRecommendation.d.ts +24 -24
- package/build/types/TestRecommendation.js +91 -89
- package/build/types/TestbotPromptOptions.d.ts +0 -4
- package/build/types/TestbotReport.d.ts +64 -2
- package/build/utils/AnalysisStateManager.d.ts +79 -113
- package/build/utils/AnalysisStateManager.js +147 -57
- package/build/utils/assertion-verify/api-shared-lints.js +1 -1
- package/build/utils/assertion-verify/metrics.js +85 -36
- package/build/utils/assertion-verify/ui-lints.d.ts +0 -5
- package/build/utils/assertion-verify/ui-lints.js +32 -0
- package/build/utils/branchDiff.d.ts +63 -31
- package/build/utils/branchDiff.js +242 -94
- package/build/utils/containedPath.d.ts +18 -0
- package/build/utils/containedPath.js +73 -0
- package/build/utils/dartRouteExtractor.d.ts +18 -34
- package/build/utils/dartRouteExtractor.js +101 -173
- package/build/utils/featureFlags.d.ts +12 -0
- package/build/utils/featureFlags.js +14 -0
- package/build/utils/frontendSelectors.d.ts +48 -27
- package/build/utils/frontendSelectors.js +241 -80
- package/build/utils/pathMatching.d.ts +2 -4
- package/build/utils/pathMatching.js +2 -4
- package/build/utils/planMatchKeys.d.ts +38 -47
- package/build/utils/planMatchKeys.js +143 -81
- package/build/utils/rebaselineSnapshots.d.ts +24 -0
- package/build/utils/rebaselineSnapshots.js +65 -0
- package/build/utils/removedUiElements.d.ts +22 -0
- package/build/utils/removedUiElements.js +106 -0
- package/build/utils/reportVerification.d.ts +2 -6
- package/build/utils/reportVerification.js +61 -2
- package/build/utils/screenRoutes.d.ts +66 -0
- package/build/utils/screenRoutes.js +727 -0
- package/build/utils/sourceRouteExtractor.js +320 -112
- package/build/utils/testFileClassification.d.ts +11 -2
- package/build/utils/testFileClassification.js +44 -2
- package/build/utils/testFixtures.d.ts +5 -0
- package/build/utils/testFixtures.js +13 -0
- package/build/utils/utils.d.ts +0 -1
- package/build/utils/utils.js +0 -11
- package/build/utils/versions.d.ts +3 -3
- package/build/utils/versions.js +1 -1
- package/build/workspace/workspace.d.ts +12 -12
- package/node_modules/playwright/lib/mcp/skyramp/assertHiddenTool.js +56 -0
- package/node_modules/playwright/lib/mcp/skyramp/assertTool.js +2 -1
- package/node_modules/playwright/lib/mcp/skyramp/loadTraceTool.js +10 -0
- package/node_modules/playwright/lib/mcp/skyramp/skyRampImport.js +4 -1
- package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +160 -1
- package/node_modules/playwright/lib/mcp/test/skyRampExport.js +4 -2
- package/node_modules/playwright/node_modules/playwright-core/lib/server/codegen/skyramp/jsonlReader.js +1 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder/recorderSignalProcessor.js +2 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder.js +5 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.-Id052Lr.js → index.B7KbSQcC.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +1 -1
- package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
- package/node_modules/playwright/node_modules/playwright-core/src/server/codegen/skyramp/jsonlReader.ts +1 -1
- package/node_modules/playwright/node_modules/playwright-core/src/server/recorder/recorderSignalProcessor.ts +7 -0
- package/node_modules/playwright/node_modules/playwright-core/src/server/recorder.ts +6 -1
- package/node_modules/playwright/package.json +1 -1
- package/package.json +4 -3
- package/plugin/.claude-plugin/plugin.json +8 -0
- package/plugin/plugin.json +6 -0
- package/plugin/prompts/declaring-a-plan.md +20 -0
- package/plugin/prompts/generate-tests/context-fetching.md +4 -0
- package/plugin/prompts/generate-tests/execution-plan.md +63 -0
- package/plugin/prompts/generate-tests/generation.md +108 -0
- package/plugin/prompts/generate-tests/path-parameters.md +1 -0
- package/plugin/prompts/generate-tests/reasoning-protocol.md +17 -0
- package/plugin/prompts/generate-tests/tool-workflow-variants.md +61 -0
- package/plugin/prompts/generate-tests/tool-workflows.md +65 -0
- package/plugin/prompts/plan-tests.md +42 -0
- package/plugin/prompts/testbot-task1.md +82 -0
- package/plugin/skills/fix-test-import-errors/SKILL.md +98 -0
- package/build/prompts/test-recommendation/analysisOutputPrompt.d.ts +0 -84
- package/build/prompts/test-recommendation/analysisOutputPrompt.js +0 -369
- package/build/prompts/test-recommendation/fullRepoCatalog.d.ts +0 -7
- package/build/prompts/test-recommendation/fullRepoCatalog.js +0 -283
- package/build/prompts/test-recommendation/scopeAssessment.d.ts +0 -81
- package/build/prompts/test-recommendation/scopeAssessment.js +0 -359
- package/build/recommendation/budgeters/diversityBalancedBudgeter.d.ts +0 -7
- package/build/recommendation/budgeters/diversityBalancedBudgeter.js +0 -105
- package/build/recommendation/budgeters/fixedNBudgeter.d.ts +0 -7
- package/build/recommendation/budgeters/fixedNBudgeter.js +0 -11
- package/build/recommendation/budgeters/shared.d.ts +0 -32
- package/build/recommendation/budgeters/shared.js +0 -246
- package/build/recommendation/discriminators.d.ts +0 -37
- package/build/recommendation/discriminators.js +0 -379
- package/build/recommendation/diversity.d.ts +0 -47
- package/build/recommendation/diversity.js +0 -101
- package/build/recommendation/planRanker.d.ts +0 -65
- package/build/recommendation/planRanker.js +0 -83
- package/build/recommendation/testFixtures.d.ts +0 -25
- package/build/recommendation/testFixtures.js +0 -45
- package/build/types/FrontendIntegration.d.ts +0 -28
- package/build/types/FrontendIntegration.js +0 -22
- package/build/types/Recommendation.d.ts +0 -146
- package/build/types/Recommendation.js +0 -74
- package/build/utils/changedRoutes.d.ts +0 -29
- package/build/utils/changedRoutes.js +0 -87
- package/build/utils/frontendIntegration.d.ts +0 -9
- package/build/utils/frontendIntegration.js +0 -243
- package/build/utils/importerHop.d.ts +0 -135
- package/build/utils/importerHop.js +0 -489
- package/build/utils/pathAffinityClassification.d.ts +0 -49
- package/build/utils/pathAffinityClassification.js +0 -180
- package/build/utils/pythonMountPrefixes.d.ts +0 -25
- package/build/utils/pythonMountPrefixes.js +0 -347
- package/build/utils/repoScanner.d.ts +0 -34
- package/build/utils/repoScanner.js +0 -300
- package/build/utils/routeParsers.d.ts +0 -95
- package/build/utils/routeParsers.js +0 -951
- package/build/utils/scenarioDrafting.d.ts +0 -92
- package/build/utils/scenarioDrafting.js +0 -951
- package/build/utils/subjectEndpoints.d.ts +0 -19
- package/build/utils/subjectEndpoints.js +0 -98
- package/build/utils/uiPageEnumerator.d.ts +0 -172
- package/build/utils/uiPageEnumerator.js +0 -474
|
@@ -10,35 +10,67 @@ export interface BranchDiffData {
|
|
|
10
10
|
/** Files deleted in this branch (+++ /dev/null in diff header). Relative paths. */
|
|
11
11
|
deletedFiles: string[];
|
|
12
12
|
}
|
|
13
|
-
/**
|
|
14
|
-
*
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
*
|
|
32
|
-
*/
|
|
13
|
+
/** Every path in a `diff --git` header, always the `b/` form so a rename
|
|
14
|
+
* returns the new name. */
|
|
15
|
+
/** One `diff --git` block, read once. */
|
|
16
|
+
export interface ChangedFileEntry {
|
|
17
|
+
/** The `b/` path, so a rename reports the new name. */
|
|
18
|
+
path: string;
|
|
19
|
+
/** `--- /dev/null`. A newly added BINARY file has no such line: git renders
|
|
20
|
+
* `Binary files /dev/null and b/… differ` instead. */
|
|
21
|
+
isNew: boolean;
|
|
22
|
+
/** `+++ /dev/null`, or a deleted binary's `Binary files a/… and /dev/null`. */
|
|
23
|
+
isDeleted: boolean;
|
|
24
|
+
/** A pure rename and a mode-only change carry no hunk. */
|
|
25
|
+
hasHunks: boolean;
|
|
26
|
+
/** A rename's `a/` path. The block is keyed by the NEW path, so without this
|
|
27
|
+
* "was this path removed" answers a false no. */
|
|
28
|
+
renamedFrom?: string;
|
|
29
|
+
}
|
|
30
|
+
/** Every `diff --git` block, in order. Per BLOCK, never per path: the text
|
|
31
|
+
* concatenates every repository, so one repository's deletion of a path would
|
|
32
|
+
* hide another's real change to it. */
|
|
33
|
+
export declare function parseDiffEntries(rawDiff: string): ChangedFileEntry[];
|
|
34
|
+
/** Each changed path's own slice, so a check can ask whether a line is in THIS
|
|
35
|
+
* file. Two repositories changing one path share a key, which errs toward
|
|
36
|
+
* saying a file holds a line it does not. */
|
|
37
|
+
export declare function diffBlocksByPath(rawDiff: string): Map<string, string>;
|
|
38
|
+
/** Git quotes a path only when it has to. An unquoted path is returned as it
|
|
39
|
+
* stands, because decoding escapes in one would corrupt a legal name that
|
|
40
|
+
* really contains a backslash. */
|
|
41
|
+
export declare function unquoteIfQuoted(value: string): string;
|
|
42
|
+
/** Undo git's C-style quoting. A non-ASCII character arrives as one `\NNN` escape
|
|
43
|
+
* per UTF-8 BYTE, so the bytes are decoded once at the end. */
|
|
44
|
+
export declare function unquoteGitPath(value: string): string;
|
|
45
|
+
/** Exported so `frontendSurface` drops a deleted file without a second walk. */
|
|
46
|
+
export declare function parseNewAndDeletedFiles(rawDiff: string): {
|
|
47
|
+
newFiles: string[];
|
|
48
|
+
deletedFiles: string[];
|
|
49
|
+
};
|
|
50
|
+
/** Reject an option-like or ambiguous ref, so git reads it as a revision name
|
|
51
|
+
* and never as a command-line flag. */
|
|
33
52
|
export declare function normalizeProvidedBaseRef(providedBaseBranch?: string): string | undefined;
|
|
34
|
-
/**
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
53
|
+
/** One changed path, read from `--name-status` rather than from diff text. */
|
|
54
|
+
export interface ChangedFileName {
|
|
55
|
+
/** Repository-relative path in git's own spelling, unquoted. A rename reports
|
|
56
|
+
* the new path; the old one arrives as a separate deleted entry. */
|
|
57
|
+
path: string;
|
|
58
|
+
/** Removed, but still a changed surface: a test can assert its route is gone. */
|
|
59
|
+
deleted: boolean;
|
|
60
|
+
}
|
|
61
|
+
/** Parse `git diff --name-status`. Exported for its own tests.
|
|
62
|
+
* A rename and a copy carry TWO paths: the new one is the changed surface, and
|
|
63
|
+
* a rename also removes the old, while a copy leaves the original. Git quotes a
|
|
64
|
+
* non-ASCII path here as it does in a diff header. */
|
|
65
|
+
export declare function parseNameStatus(raw: string): ChangedFileName[];
|
|
66
|
+
/** The ONE derivation of `{path, deleted}` from a names-only diff, so no caller
|
|
67
|
+
* re-derives the join. */
|
|
68
|
+
export declare function changedFileNames(diff: BranchDiffData): ChangedFileName[];
|
|
69
|
+
/** Compute the diff between a base reference and the current state.
|
|
70
|
+
* `providedBaseBranch` takes a branch, tag or SHA; omitted, the remote default
|
|
71
|
+
* is detected. `includeUncommitted` diffs the working tree (local dev); false
|
|
72
|
+
* compares two commits through the three-dot form, which is the CI shape. */
|
|
73
|
+
export declare function computeBranchDiff(repositoryPath: string, providedBaseBranch?: string, includeUncommitted?: boolean,
|
|
74
|
+
/** Names only: no diff text is computed, and `diffContent`/`diffStat` come back
|
|
75
|
+
* empty. The plan checks read which files changed, never what changed in them. */
|
|
76
|
+
namesOnly?: boolean): Promise<BranchDiffData>;
|
|
@@ -1,75 +1,141 @@
|
|
|
1
1
|
import { simpleGit } from "simple-git";
|
|
2
2
|
import { logger } from "./logger.js";
|
|
3
|
-
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
* (post-change) path. Exported so other file->resource-affinity callers
|
|
19
|
-
* (e.g. `pathAffinityClassification.ts`'s fallback, SKYR-3857) can derive the
|
|
20
|
-
* same per-file diff hunks this module uses, without re-implementing the
|
|
21
|
-
* diff-header walk. Lives here rather than in `importerHop.ts`, its original
|
|
22
|
-
* home, so callers that `importerHop.ts` itself imports can reach it without
|
|
23
|
-
* an import cycle.
|
|
24
|
-
*/
|
|
25
|
-
export function sliceAddedLinesByFile(diffContent) {
|
|
26
|
-
const sections = new Map();
|
|
27
|
-
let currentFile = null;
|
|
28
|
-
for (const line of diffContent.split("\n")) {
|
|
29
|
-
const headerMatch = /^diff --git a\/.+ b\/(.+)$/.exec(line);
|
|
30
|
-
if (headerMatch) {
|
|
31
|
-
currentFile = headerMatch[1];
|
|
3
|
+
/** Every `diff --git` block, in order. Per BLOCK, never per path: the text
|
|
4
|
+
* concatenates every repository, so one repository's deletion of a path would
|
|
5
|
+
* hide another's real change to it. */
|
|
6
|
+
export function parseDiffEntries(rawDiff) {
|
|
7
|
+
const entries = [];
|
|
8
|
+
for (const line of rawDiff.split("\n")) {
|
|
9
|
+
if (line.startsWith("diff --git ")) {
|
|
10
|
+
const path = parseDiffHeaderPath(line);
|
|
11
|
+
// An unreadable header still ENDS the previous block, or its `/dev/null`
|
|
12
|
+
// and `@@` lines are credited to the entry before it.
|
|
13
|
+
if (path === undefined) {
|
|
14
|
+
entries.push({ path: "", isNew: false, isDeleted: false, hasHunks: false });
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
entries.push({ path, isNew: false, isDeleted: false, hasHunks: false });
|
|
32
18
|
continue;
|
|
33
19
|
}
|
|
34
|
-
|
|
20
|
+
const current = entries[entries.length - 1];
|
|
21
|
+
if (!current)
|
|
35
22
|
continue;
|
|
36
|
-
if (line
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
23
|
+
if (line === "--- /dev/null")
|
|
24
|
+
current.isNew = true;
|
|
25
|
+
else if (line === "+++ /dev/null")
|
|
26
|
+
current.isDeleted = true;
|
|
27
|
+
else if (line.startsWith("@@ "))
|
|
28
|
+
current.hasHunks = true;
|
|
29
|
+
// A binary block has no `--- `/`+++ ` lines; git folds both into this one.
|
|
30
|
+
else if (line.startsWith("Binary files /dev/null and "))
|
|
31
|
+
current.isNew = true;
|
|
32
|
+
else if (line.startsWith("Binary files ") && line.endsWith(" and /dev/null differ"))
|
|
33
|
+
current.isDeleted = true;
|
|
34
|
+
// Unquoted like the new-path side, or one rename's two halves disagree.
|
|
35
|
+
else if (line.startsWith("rename from "))
|
|
36
|
+
current.renamedFrom = unquoteIfQuoted(line.slice("rename from ".length));
|
|
37
|
+
}
|
|
38
|
+
return entries;
|
|
39
|
+
}
|
|
40
|
+
/** Each changed path's own slice, so a check can ask whether a line is in THIS
|
|
41
|
+
* file. Two repositories changing one path share a key, which errs toward
|
|
42
|
+
* saying a file holds a line it does not. */
|
|
43
|
+
export function diffBlocksByPath(rawDiff) {
|
|
44
|
+
const blocks = new Map();
|
|
45
|
+
let current;
|
|
46
|
+
for (const line of rawDiff.split("\n")) {
|
|
47
|
+
// ANY `diff ` header ends a block, not just `diff --git`: a combined diff
|
|
48
|
+
// writes `diff --cc`, and its lines would otherwise append to the file before.
|
|
49
|
+
if (line.startsWith("diff ")) {
|
|
50
|
+
const path = line.startsWith("diff --git ") ? parseDiffHeaderPath(line) : undefined;
|
|
51
|
+
if (path === undefined) {
|
|
52
|
+
current = undefined;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
current = blocks.get(path) ?? [];
|
|
56
|
+
blocks.set(path, current);
|
|
40
57
|
}
|
|
58
|
+
current?.push(line);
|
|
41
59
|
}
|
|
42
|
-
return
|
|
60
|
+
return new Map([...blocks].map(([path, lines]) => [path, lines.join("\n")]));
|
|
43
61
|
}
|
|
44
|
-
/**
|
|
45
|
-
|
|
46
|
-
|
|
62
|
+
/** The `b/` path of a header, unquoted. Git QUOTES a non-ASCII path and
|
|
63
|
+
* `core.quotePath` defaults to true, so a header can read
|
|
64
|
+
* `diff --git "a/src/caf\303\251.tsx" "b/…"`. */
|
|
65
|
+
function parseDiffHeaderPath(line) {
|
|
66
|
+
const rest = line.slice("diff --git ".length);
|
|
67
|
+
// Git quotes each side INDEPENDENTLY: a rename of a non-ASCII name emits one
|
|
68
|
+
// quoted side and one bare, which a single whole-line pattern reads neither of.
|
|
69
|
+
const quoted = /^"a\/((?:[^"\\]|\\.)*)" "b\/((?:[^"\\]|\\.)*)"$/.exec(rest);
|
|
70
|
+
if (quoted)
|
|
71
|
+
return unquoteGitPath(quoted[2]);
|
|
72
|
+
const quotedNew = /^a\/.+ "b\/((?:[^"\\]|\\.)*)"$/.exec(rest);
|
|
73
|
+
if (quotedNew)
|
|
74
|
+
return unquoteGitPath(quotedNew[1]);
|
|
75
|
+
const quotedOld = /^"a\/(?:[^"\\]|\\.)*" b\/(.+)$/.exec(rest);
|
|
76
|
+
if (quotedOld)
|
|
77
|
+
return quotedOld[1];
|
|
78
|
+
const plain = /^a\/.+ b\/(.+)$/.exec(rest);
|
|
79
|
+
if (plain)
|
|
80
|
+
return plain[1];
|
|
81
|
+
// `diff.noprefix` drops `a/` and `b/`; `diff.mnemonicPrefix` makes them
|
|
82
|
+
// `c/ w/ i/ o/`. Both are developer-local, so only the local-dev path sees them.
|
|
83
|
+
const mnemonic = /^[a-z]\/(.+) [a-z]\/(.+)$/.exec(rest);
|
|
84
|
+
if (mnemonic)
|
|
85
|
+
return mnemonic[2];
|
|
86
|
+
const noPrefix = /^(\S+) (\S+)$/.exec(rest);
|
|
87
|
+
return noPrefix ? noPrefix[2] : undefined;
|
|
88
|
+
}
|
|
89
|
+
/** Git quotes a path only when it has to. An unquoted path is returned as it
|
|
90
|
+
* stands, because decoding escapes in one would corrupt a legal name that
|
|
91
|
+
* really contains a backslash. */
|
|
92
|
+
export function unquoteIfQuoted(value) {
|
|
93
|
+
const trimmed = value.trim();
|
|
94
|
+
return trimmed.startsWith('"') && trimmed.endsWith('"') ? unquoteGitPath(trimmed) : trimmed;
|
|
47
95
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
if (
|
|
56
|
-
|
|
96
|
+
/** Undo git's C-style quoting. A non-ASCII character arrives as one `\NNN` escape
|
|
97
|
+
* per UTF-8 BYTE, so the bytes are decoded once at the end. */
|
|
98
|
+
export function unquoteGitPath(value) {
|
|
99
|
+
const trimmed = value.startsWith('"') && value.endsWith('"') ? value.slice(1, -1) : value;
|
|
100
|
+
const simple = { n: "\n", t: "\t", r: "\r", '"': '"', "\\": "\\" };
|
|
101
|
+
const bytes = [];
|
|
102
|
+
for (let i = 0; i < trimmed.length; i++) {
|
|
103
|
+
if (trimmed[i] !== "\\") {
|
|
104
|
+
bytes.push(...Buffer.from(trimmed[i], "utf-8"));
|
|
57
105
|
continue;
|
|
58
106
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
107
|
+
const next = trimmed[++i];
|
|
108
|
+
if (next !== undefined && /[0-7]/.test(next) && /^[0-7]{3}$/.test(trimmed.slice(i, i + 3))) {
|
|
109
|
+
bytes.push(parseInt(trimmed.slice(i, i + 3), 8));
|
|
110
|
+
i += 2;
|
|
111
|
+
continue;
|
|
64
112
|
}
|
|
113
|
+
bytes.push(...Buffer.from(next === undefined ? "\\" : (simple[next] ?? next), "utf-8"));
|
|
65
114
|
}
|
|
66
|
-
return
|
|
115
|
+
return Buffer.from(bytes).toString("utf-8");
|
|
116
|
+
}
|
|
117
|
+
/** Parse diff headers to find newly created and deleted files. */
|
|
118
|
+
function isShaLikeRef(ref) {
|
|
119
|
+
return /^[0-9a-f]{7,40}$/i.test(ref);
|
|
67
120
|
}
|
|
68
|
-
/**
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
121
|
+
/** Exported so `frontendSurface` drops a deleted file without a second walk. */
|
|
122
|
+
export function parseNewAndDeletedFiles(rawDiff) {
|
|
123
|
+
const entries = parseDiffEntries(rawDiff);
|
|
124
|
+
const renamedFromPaths = entries
|
|
125
|
+
.map((entry) => entry.renamedFrom)
|
|
126
|
+
.filter((path) => path !== undefined);
|
|
127
|
+
// An unreadable header yields `""`, which would read as a file nobody added.
|
|
128
|
+
return {
|
|
129
|
+
newFiles: entries.filter((entry) => entry.isNew && entry.path).map((entry) => entry.path),
|
|
130
|
+
// Keyed by the NEW path, so the OLD one is added here: it moved, not died.
|
|
131
|
+
deletedFiles: [
|
|
132
|
+
...entries.filter((entry) => entry.isDeleted && entry.path).map((entry) => entry.path),
|
|
133
|
+
...renamedFromPaths,
|
|
134
|
+
].filter(Boolean),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
/** Reject an option-like or ambiguous ref, so git reads it as a revision name
|
|
138
|
+
* and never as a command-line flag. */
|
|
73
139
|
export function normalizeProvidedBaseRef(providedBaseBranch) {
|
|
74
140
|
if (providedBaseBranch === undefined)
|
|
75
141
|
return undefined;
|
|
@@ -87,21 +153,53 @@ export function normalizeProvidedBaseRef(providedBaseBranch) {
|
|
|
87
153
|
}
|
|
88
154
|
return trimmed;
|
|
89
155
|
}
|
|
156
|
+
/** The paths `--name-status` marks `A`. A rename's new path is NOT one: git
|
|
157
|
+
* reports it as `R`, and `parseNameStatus` already lists it as changed. */
|
|
158
|
+
function addedPaths(raw) {
|
|
159
|
+
const added = [];
|
|
160
|
+
for (const line of (typeof raw === "string" ? raw : "").split("\n")) {
|
|
161
|
+
const parts = line.split("\t");
|
|
162
|
+
if ((parts[0] ?? "").trim().toUpperCase() !== "A")
|
|
163
|
+
continue;
|
|
164
|
+
const path = unquoteIfQuoted(parts[1] ?? "");
|
|
165
|
+
if (path)
|
|
166
|
+
added.push(path);
|
|
167
|
+
}
|
|
168
|
+
return added;
|
|
169
|
+
}
|
|
170
|
+
/** `--name-status` for one ref, split into the three lists `BranchDiffData` carries.
|
|
171
|
+
* Undefined when the ref does not resolve; an EMPTY listing is a result, not a
|
|
172
|
+
* failure. */
|
|
173
|
+
async function tryNameStatus(git, ref, includeUncommitted) {
|
|
174
|
+
try {
|
|
175
|
+
const raw = await git.diff([includeUncommitted ? ref : `${ref}...HEAD`, "--name-status"]);
|
|
176
|
+
const files = parseNameStatus(raw);
|
|
177
|
+
return {
|
|
178
|
+
changedFiles: files.filter((file) => !file.deleted).map((file) => file.path),
|
|
179
|
+
// `--name-status` marks an added path `A`; the header parser never runs here.
|
|
180
|
+
newFiles: addedPaths(raw),
|
|
181
|
+
deletedFiles: files.filter((file) => file.deleted).map((file) => file.path),
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
return undefined;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
90
188
|
async function tryDiff(git, ref, includeUncommitted = false) {
|
|
91
189
|
try {
|
|
92
|
-
//
|
|
93
|
-
// working tree so uncommitted/unstaged local changes are captured.
|
|
190
|
+
// `includeUncommitted` diffs the ref against the working tree.
|
|
94
191
|
const diffRange = includeUncommitted ? ref : `${ref}...HEAD`;
|
|
95
192
|
const changedFilesRaw = await git.diff([diffRange, "--name-only"]);
|
|
96
193
|
const changedFiles = changedFilesRaw
|
|
97
194
|
.split("\n")
|
|
98
195
|
.map((f) => f.trim())
|
|
99
|
-
.filter((f) => f.length > 0)
|
|
196
|
+
.filter((f) => f.length > 0)
|
|
197
|
+
// `--name-only` quotes a non-ASCII path too.
|
|
198
|
+
.map((f) => unquoteGitPath(f));
|
|
100
199
|
const diffStat = await git.diff([diffRange, "--stat"]);
|
|
101
200
|
const fullDiff = await git.diff([diffRange]);
|
|
102
201
|
const { newFiles, deletedFiles } = parseNewAndDeletedFiles(fullDiff);
|
|
103
|
-
// git diff
|
|
104
|
-
// and new so endpoint classification treats newly created local files as new.
|
|
202
|
+
// git diff omits untracked files; list them as both changed and new.
|
|
105
203
|
if (includeUncommitted) {
|
|
106
204
|
const statusResult = await git.status();
|
|
107
205
|
const untrackedFiles = statusResult.not_added ?? [];
|
|
@@ -135,31 +233,11 @@ async function tryDiff(git, ref, includeUncommitted = false) {
|
|
|
135
233
|
return undefined;
|
|
136
234
|
}
|
|
137
235
|
}
|
|
138
|
-
/**
|
|
139
|
-
*
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
* When omitted, the remote default branch is auto-detected.
|
|
144
|
-
* @param includeUncommitted When true, diffs against the working tree so
|
|
145
|
-
* uncommitted/unstaged changes are included. Defaults to false (CI mode
|
|
146
|
-
* that only considers committed changes via the three-dot form).
|
|
147
|
-
*/
|
|
148
|
-
export async function computeBranchDiff(repositoryPath, providedBaseBranch, includeUncommitted = false) {
|
|
149
|
-
const git = simpleGit(repositoryPath);
|
|
150
|
-
const isRepo = await git.checkIsRepo();
|
|
151
|
-
if (!isRepo) {
|
|
152
|
-
throw new Error(`"${repositoryPath}" is not a Git repository.`);
|
|
153
|
-
}
|
|
154
|
-
const branchInfo = await git.branch();
|
|
155
|
-
const currentBranch = branchInfo.current || "HEAD";
|
|
156
|
-
// Build candidate base refs in priority order:
|
|
157
|
-
// 1. Provided value used directly (supports SHAs, local branches, tags)
|
|
158
|
-
// 2. Provided value with origin/ prefix (remote branch)
|
|
159
|
-
// 3. Remote default branch auto-detected from available remotes
|
|
160
|
-
// 4. FETCH_HEAD — present in base_commit repos (git fetch --depth 1 origin <sha>)
|
|
161
|
-
// where no remote tracking branch is created
|
|
162
|
-
// 5. HEAD~1 — last resort for repos with no remote at all
|
|
236
|
+
/** The base refs to try, in priority order. One ladder for both modes, or they
|
|
237
|
+
* would answer about different bases. */
|
|
238
|
+
async function baseRefCandidates(git, providedBaseBranch, includeUncommitted) {
|
|
239
|
+
// In order: the value as given, then with `origin/`, the detected remote
|
|
240
|
+
// default, FETCH_HEAD (a base_commit clone has no tracking branch), HEAD~1.
|
|
163
241
|
const candidates = [];
|
|
164
242
|
const normalizedBaseBranch = normalizeProvidedBaseRef(providedBaseBranch);
|
|
165
243
|
if (normalizedBaseBranch) {
|
|
@@ -168,20 +246,17 @@ export async function computeBranchDiff(repositoryPath, providedBaseBranch, incl
|
|
|
168
246
|
!isShaLikeRef(normalizedBaseBranch) &&
|
|
169
247
|
!/[~^:@]/.test(normalizedBaseBranch);
|
|
170
248
|
if (includeUncommitted && looksLikeSimpleBranch) {
|
|
171
|
-
//
|
|
172
|
-
// a developer's local main may be ahead of origin/main.
|
|
249
|
+
// A developer's local main may be ahead of origin/main.
|
|
173
250
|
candidates.push(`origin/${normalizedBaseBranch}`);
|
|
174
251
|
candidates.push(normalizedBaseBranch);
|
|
175
252
|
}
|
|
176
253
|
else {
|
|
177
|
-
// Try the raw value first — handles commit SHAs, tags, and explicit local refs.
|
|
178
254
|
candidates.push(normalizedBaseBranch);
|
|
179
255
|
if (!isRemoteQualified) {
|
|
180
256
|
candidates.push(`origin/${normalizedBaseBranch}`);
|
|
181
257
|
}
|
|
182
258
|
}
|
|
183
259
|
}
|
|
184
|
-
// Auto-detect remote default branch
|
|
185
260
|
try {
|
|
186
261
|
const remoteBranches = await git.branch(["-r"]);
|
|
187
262
|
if (remoteBranches.all.some((b) => b.endsWith("/main"))) {
|
|
@@ -196,10 +271,83 @@ export async function computeBranchDiff(repositoryPath, providedBaseBranch, incl
|
|
|
196
271
|
catch {
|
|
197
272
|
logger.debug("Could not list remote branches");
|
|
198
273
|
}
|
|
199
|
-
// FETCH_HEAD covers base_commit repos (appsmith, etc.)
|
|
200
274
|
candidates.push("FETCH_HEAD");
|
|
201
|
-
// Final fallback
|
|
202
275
|
candidates.push("HEAD~1");
|
|
276
|
+
return candidates;
|
|
277
|
+
}
|
|
278
|
+
/** Parse `git diff --name-status`. Exported for its own tests.
|
|
279
|
+
* A rename and a copy carry TWO paths: the new one is the changed surface, and
|
|
280
|
+
* a rename also removes the old, while a copy leaves the original. Git quotes a
|
|
281
|
+
* non-ASCII path here as it does in a diff header. */
|
|
282
|
+
export function parseNameStatus(raw) {
|
|
283
|
+
const files = [];
|
|
284
|
+
const add = (value, deleted) => {
|
|
285
|
+
const path = unquoteIfQuoted(value ?? "");
|
|
286
|
+
if (path)
|
|
287
|
+
files.push({ path, deleted });
|
|
288
|
+
};
|
|
289
|
+
for (const line of (typeof raw === "string" ? raw : "").split("\n")) {
|
|
290
|
+
if (!line.trim())
|
|
291
|
+
continue;
|
|
292
|
+
const parts = line.split("\t");
|
|
293
|
+
// A similarity score follows the letter on a rename or a copy: `R100`, `C085`.
|
|
294
|
+
const status = (parts[0] ?? "").trim().toUpperCase();
|
|
295
|
+
if (!status)
|
|
296
|
+
continue;
|
|
297
|
+
if ((status.startsWith("R") || status.startsWith("C")) && parts[2]) {
|
|
298
|
+
add(parts[2], false);
|
|
299
|
+
if (status.startsWith("R"))
|
|
300
|
+
add(parts[1], true);
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
// A rename or copy with no second path is malformed; read its one path as an
|
|
304
|
+
// ordinary change rather than dropping the file.
|
|
305
|
+
add(parts[1], status === "D");
|
|
306
|
+
}
|
|
307
|
+
return files;
|
|
308
|
+
}
|
|
309
|
+
/** The ONE derivation of `{path, deleted}` from a names-only diff, so no caller
|
|
310
|
+
* re-derives the join. */
|
|
311
|
+
export function changedFileNames(diff) {
|
|
312
|
+
return [
|
|
313
|
+
...(diff.changedFiles ?? []).map((path) => ({ path, deleted: false })),
|
|
314
|
+
...(diff.deletedFiles ?? []).map((path) => ({ path, deleted: true })),
|
|
315
|
+
];
|
|
316
|
+
}
|
|
317
|
+
/** Compute the diff between a base reference and the current state.
|
|
318
|
+
* `providedBaseBranch` takes a branch, tag or SHA; omitted, the remote default
|
|
319
|
+
* is detected. `includeUncommitted` diffs the working tree (local dev); false
|
|
320
|
+
* compares two commits through the three-dot form, which is the CI shape. */
|
|
321
|
+
export async function computeBranchDiff(repositoryPath, providedBaseBranch, includeUncommitted = false,
|
|
322
|
+
/** Names only: no diff text is computed, and `diffContent`/`diffStat` come back
|
|
323
|
+
* empty. The plan checks read which files changed, never what changed in them. */
|
|
324
|
+
namesOnly = false) {
|
|
325
|
+
const git = simpleGit(repositoryPath);
|
|
326
|
+
const isRepo = await git.checkIsRepo();
|
|
327
|
+
if (!isRepo) {
|
|
328
|
+
throw new Error(`"${repositoryPath}" is not a Git repository.`);
|
|
329
|
+
}
|
|
330
|
+
const branchInfo = await git.branch();
|
|
331
|
+
const currentBranch = branchInfo.current || "HEAD";
|
|
332
|
+
const candidates = await baseRefCandidates(git, providedBaseBranch, includeUncommitted);
|
|
333
|
+
if (namesOnly) {
|
|
334
|
+
for (const ref of candidates) {
|
|
335
|
+
const result = await tryNameStatus(git, ref, includeUncommitted);
|
|
336
|
+
// The FIRST ref that resolves wins, even when it lists nothing: walking past
|
|
337
|
+
// an empty listing reached `HEAD~1` and reported the previous commit's files
|
|
338
|
+
// as this pull request's.
|
|
339
|
+
if (result) {
|
|
340
|
+
logger.info("Changed-file names computed", {
|
|
341
|
+
currentBranch,
|
|
342
|
+
baseBranch: ref,
|
|
343
|
+
changedFiles: result.changedFiles.length,
|
|
344
|
+
});
|
|
345
|
+
return { currentBranch, baseBranch: ref, diffContent: "", diffStat: "", ...result };
|
|
346
|
+
}
|
|
347
|
+
logger.debug(`Name-status against ${ref} failed, trying next candidate`);
|
|
348
|
+
}
|
|
349
|
+
throw new Error(`Could not list changed file names: all candidates failed (${candidates.join(", ")})`);
|
|
350
|
+
}
|
|
203
351
|
for (const ref of candidates) {
|
|
204
352
|
const result = await tryDiff(git, ref, includeUncommitted);
|
|
205
353
|
if (result) {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Resolve and read a path an AGENT wrote, without leaving the checkout. ONE
|
|
2
|
+
* implementation for every such reader: an escape closed in one and left open in
|
|
3
|
+
* another is the same hole. */
|
|
4
|
+
/** `candidate` resolved against `root`, or undefined when the real target is not
|
|
5
|
+
* inside `root`. `root` must already be a real path, or a symlinked checkout fails
|
|
6
|
+
* its own containment test. */
|
|
7
|
+
export declare function resolveWithinRoot(root: string, candidate: string): string | undefined;
|
|
8
|
+
/** Whether `cited` names a real FILE inside `root`. */
|
|
9
|
+
export declare function fileExistsWithinRoot(root: string, cited: string): boolean;
|
|
10
|
+
/** The text of a file inside `root`, or undefined for anything else — outside the
|
|
11
|
+
* checkout, absent, not a regular file, or past the cap. A caller cannot tell those
|
|
12
|
+
* apart ON PURPOSE. `size` is 0 for a FIFO, so the byte cap is not enough:
|
|
13
|
+
* `readFile` on one blocks until something writes and the tool never returns. */
|
|
14
|
+
export declare function readFileWithinRoot(root: string, candidate: string, maxBytes: number): string | undefined;
|
|
15
|
+
/** The real paths of a run's checkouts, deduped. Every containment test compares
|
|
16
|
+
* real paths, so a root has to be one: on macOS `os.tmpdir()` is a symlink, and
|
|
17
|
+
* a raw root failed its own containment test for files really inside it. */
|
|
18
|
+
export declare function realRootsOf(roots: Array<string | undefined>): string[];
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
/** Resolve and read a path an AGENT wrote, without leaving the checkout. ONE
|
|
4
|
+
* implementation for every such reader: an escape closed in one and left open in
|
|
5
|
+
* another is the same hole. */
|
|
6
|
+
/** `candidate` resolved against `root`, or undefined when the real target is not
|
|
7
|
+
* inside `root`. `root` must already be a real path, or a symlinked checkout fails
|
|
8
|
+
* its own containment test. */
|
|
9
|
+
export function resolveWithinRoot(root, candidate) {
|
|
10
|
+
try {
|
|
11
|
+
// `path.resolve` returns an absolute candidate unchanged and a
|
|
12
|
+
// drive-relative one as written, so neither is silently joined to the root;
|
|
13
|
+
// the containment test below is what rejects them.
|
|
14
|
+
const real = fs.realpathSync(path.resolve(root, candidate));
|
|
15
|
+
if (real !== root && !real.startsWith(root + path.sep))
|
|
16
|
+
return undefined;
|
|
17
|
+
return real;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/** Whether `cited` names a real FILE inside `root`. */
|
|
24
|
+
export function fileExistsWithinRoot(root, cited) {
|
|
25
|
+
const real = resolveWithinRoot(root, cited);
|
|
26
|
+
if (!real)
|
|
27
|
+
return false;
|
|
28
|
+
try {
|
|
29
|
+
// A directory is not a file that defines a route, and `realpathSync` has
|
|
30
|
+
// already followed every symlink, so this is the real target's kind.
|
|
31
|
+
return fs.statSync(real).isFile();
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** The text of a file inside `root`, or undefined for anything else — outside the
|
|
38
|
+
* checkout, absent, not a regular file, or past the cap. A caller cannot tell those
|
|
39
|
+
* apart ON PURPOSE. `size` is 0 for a FIFO, so the byte cap is not enough:
|
|
40
|
+
* `readFile` on one blocks until something writes and the tool never returns. */
|
|
41
|
+
export function readFileWithinRoot(root, candidate, maxBytes) {
|
|
42
|
+
const real = resolveWithinRoot(root, candidate);
|
|
43
|
+
if (!real)
|
|
44
|
+
return undefined;
|
|
45
|
+
try {
|
|
46
|
+
const stat = fs.statSync(real);
|
|
47
|
+
if (!stat.isFile() || stat.size > maxBytes)
|
|
48
|
+
return undefined;
|
|
49
|
+
return fs.readFileSync(real, "utf-8");
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/** The real paths of a run's checkouts, deduped. Every containment test compares
|
|
56
|
+
* real paths, so a root has to be one: on macOS `os.tmpdir()` is a symlink, and
|
|
57
|
+
* a raw root failed its own containment test for files really inside it. */
|
|
58
|
+
export function realRootsOf(roots) {
|
|
59
|
+
const real = [];
|
|
60
|
+
for (const root of roots) {
|
|
61
|
+
if (!root || root === "unknown")
|
|
62
|
+
continue;
|
|
63
|
+
try {
|
|
64
|
+
const resolved = fs.realpathSync(root);
|
|
65
|
+
if (!real.includes(resolved))
|
|
66
|
+
real.push(resolved);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
// A checkout path that no longer resolves holds no readable file.
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return real;
|
|
73
|
+
}
|