@skyramp/mcp 0.1.8 → 0.2.0-rc.2
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/index.js +4 -2
- package/build/playwright/registerPlaywrightTools.js +12 -0
- package/build/playwright/traceRecordingPrompt.js +15 -0
- package/build/prompts/code-reuse.js +106 -7
- package/build/prompts/pom-aware-code-reuse.js +106 -7
- package/build/prompts/startTraceCollectionPrompts.js +37 -15
- package/build/prompts/test-maintenance/drift-analysis-prompt.js +26 -31
- package/build/prompts/test-maintenance/drift-analysis-prompt.test.js +40 -1
- package/build/prompts/test-maintenance/driftAnalysisSections.js +90 -86
- package/build/prompts/test-recommendation/analysisOutputPrompt.js +286 -163
- package/build/prompts/test-recommendation/analysisOutputPrompt.test.js +154 -45
- package/build/prompts/test-recommendation/diffExecutionPlan.js +246 -117
- package/build/prompts/test-recommendation/promptPlan.js +290 -0
- package/build/prompts/test-recommendation/promptPlan.test.js +336 -0
- package/build/prompts/test-recommendation/recommendationSections.js +4 -3
- package/build/prompts/test-recommendation/recommendationShared.js +23 -1
- package/build/prompts/test-recommendation/scopeAssessment.js +65 -14
- package/build/prompts/test-recommendation/scopeAssessment.test.js +93 -2
- package/build/prompts/test-recommendation/test-recommendation-prompt.js +36 -12
- package/build/prompts/test-recommendation/test-recommendation-prompt.test.js +316 -1
- package/build/prompts/testbot/testbot-prompts.js +73 -13
- package/build/prompts/testbot/testbot-prompts.test.js +114 -1
- package/build/resources/testbotResource.js +1 -1
- package/build/services/ScenarioGenerationService.integration.test.js +158 -0
- package/build/services/ScenarioGenerationService.js +47 -4
- package/build/services/ScenarioGenerationService.test.js +158 -22
- package/build/services/TestExecutionService.js +73 -15
- package/build/services/TestExecutionService.test.js +105 -0
- package/build/services/TestGenerationService.js +11 -1
- package/build/tools/executeSkyrampTestTool.js +1 -10
- package/build/tools/generate-tests/generateBatchScenarioRestTool.js +16 -4
- package/build/tools/generate-tests/generateIntegrationRestTool.js +2 -0
- package/build/tools/generate-tests/generateUIRestTool.js +2 -0
- package/build/tools/test-management/actionsTool.js +152 -63
- package/build/tools/test-management/analyzeChangesTool.js +178 -64
- package/build/tools/test-management/analyzeChangesTool.test.js +103 -16
- package/build/tools/test-management/analyzeTestHealthTool.js +30 -81
- package/build/tools/test-management/index.js +1 -0
- package/build/tools/test-management/uiAnalyzeChangesTool.js +149 -0
- package/build/tools/test-management/uiAnalyzeChangesTool.test.js +100 -0
- package/build/tools/trace/resolveSaveStoragePath.js +16 -0
- package/build/tools/trace/resolveSaveStoragePath.test.js +17 -0
- package/build/tools/trace/resolveSessionPaths.js +39 -0
- package/build/tools/trace/resolveSessionPaths.test.js +103 -0
- package/build/tools/trace/sessionState.js +14 -0
- package/build/tools/trace/sessionState.test.js +17 -0
- package/build/tools/trace/startTraceCollectionTool.js +84 -14
- package/build/tools/trace/stopTraceCollectionTool.js +9 -2
- package/build/types/TestAnalysis.js +50 -0
- package/build/types/TestRecommendation.js +6 -58
- package/build/types/TestTypes.js +1 -1
- package/build/utils/AnalysisStateManager.js +22 -11
- package/build/utils/branchDiff.js +11 -2
- package/build/utils/docker.test.js +1 -1
- package/build/utils/gitStaging.js +52 -3
- package/build/utils/gitStaging.test.js +19 -1
- package/build/utils/repoScanner.js +18 -10
- package/build/utils/repoScanner.test.js +92 -0
- package/build/utils/routeParsers.js +180 -25
- package/build/utils/routeParsers.test.js +180 -1
- package/build/utils/scenarioDrafting.js +220 -17
- package/build/utils/scenarioDrafting.test.js +182 -9
- package/build/utils/sourceRouteExtractor.js +806 -0
- package/build/utils/sourceRouteExtractor.test.js +565 -0
- package/build/utils/uiPageEnumerator.js +319 -0
- package/build/utils/uiPageEnumerator.test.js +422 -0
- package/build/utils/utils.js +27 -0
- package/build/utils/versions.js +1 -1
- package/build/utils/workspaceAuth.js +33 -4
- package/node_modules/playwright/ThirdPartyNotices.txt +6 -6
- package/node_modules/playwright/lib/dom-analyzer/analyze.js +111 -0
- package/node_modules/playwright/lib/dom-analyzer/blueprint.js +1210 -0
- package/node_modules/playwright/lib/dom-analyzer/blueprint.test.js +396 -0
- package/node_modules/playwright/lib/dom-analyzer/blueprintCache.js +57 -0
- package/node_modules/playwright/lib/dom-analyzer/blueprintCache.test.js +57 -0
- package/node_modules/playwright/lib/dom-analyzer/blueprintDiff.js +254 -0
- package/node_modules/playwright/lib/dom-analyzer/blueprintDiff.test.js +304 -0
- package/node_modules/playwright/lib/dom-analyzer/crawler.js +384 -0
- package/node_modules/playwright/lib/dom-analyzer/curatedWidgets.js +73 -0
- package/node_modules/playwright/lib/dom-analyzer/dynamicId.js +43 -0
- package/node_modules/playwright/lib/dom-analyzer/dynamicId.test.js +85 -0
- package/node_modules/playwright/lib/dom-analyzer/fingerprint.js +90 -0
- package/node_modules/playwright/lib/dom-analyzer/fingerprint.test.js +231 -0
- package/node_modules/playwright/lib/dom-analyzer/fingerprintAblation.fixtures.js +145 -0
- package/node_modules/playwright/lib/dom-analyzer/fingerprintAblation.test.js +41 -0
- package/node_modules/playwright/lib/dom-analyzer/graph.js +36 -0
- package/node_modules/playwright/lib/dom-analyzer/liveFingerprints.js +43 -0
- package/node_modules/playwright/lib/dom-analyzer/logicalNameResolver.js +72 -0
- package/node_modules/playwright/lib/dom-analyzer/logicalNameResolver.test.js +182 -0
- package/node_modules/playwright/lib/dom-analyzer/possibleAssertions.js +150 -0
- package/node_modules/playwright/lib/dom-analyzer/possibleAssertions.test.js +470 -0
- package/node_modules/playwright/lib/dom-analyzer/sectionGrouper.js +169 -0
- package/node_modules/playwright/lib/dom-analyzer/sectionGrouper.test.js +269 -0
- package/node_modules/playwright/lib/dom-analyzer/serialization.js +75 -0
- package/node_modules/playwright/lib/dom-analyzer/slug.js +30 -0
- package/node_modules/playwright/lib/dom-analyzer/slug.test.js +84 -0
- package/node_modules/playwright/lib/dom-analyzer/widgetContract.js +127 -0
- package/node_modules/playwright/lib/dom-analyzer/widgetContract.test.js +212 -0
- package/node_modules/playwright/lib/mcp/browser/browserContextFactory.js +3 -1
- package/node_modules/playwright/lib/mcp/browser/config.js +1 -1
- package/node_modules/playwright/lib/mcp/browser/context.js +17 -1
- package/node_modules/playwright/lib/mcp/browser/tab.js +38 -0
- package/node_modules/playwright/lib/mcp/browser/tools/domAnalyzer.js +261 -0
- package/node_modules/playwright/lib/mcp/browser/tools/keyboard.js +3 -3
- package/node_modules/playwright/lib/mcp/browser/tools/pageBlueprint.js +146 -0
- package/node_modules/playwright/lib/mcp/browser/tools/pageBlueprint.test.js +140 -0
- package/node_modules/playwright/lib/mcp/browser/tools/sitemap.js +226 -0
- package/node_modules/playwright/lib/mcp/browser/tools/snapshot.js +2 -2
- package/node_modules/playwright/lib/mcp/browser/tools/widgetContract.js +168 -0
- package/node_modules/playwright/lib/mcp/browser/tools.js +6 -0
- package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +52 -12
- package/node_modules/playwright/lib/mcp/test/skyRampExport.js +64 -13
- package/node_modules/playwright/package.json +1 -1
- package/node_modules/playwright/skyramp-playwright-1.58.2-skyramp.8.9.3.tgz +0 -0
- package/node_modules/playwright/skyramp-playwright-1.58.2-skyramp.8.9.4.tgz +0 -0
- package/node_modules/playwright/skyramp-playwright-1.58.2-skyramp.8.9.5.tgz +0 -0
- package/node_modules/playwright/skyramp-playwright-1.58.2-skyramp.8.9.6.tgz +0 -0
- package/package.json +3 -3
- package/build/services/TestHealthService.js +0 -694
- package/build/services/TestHealthService.test.js +0 -241
- package/build/types/TestDriftAnalysis.js +0 -1
- package/build/types/TestHealth.js +0 -4
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Programmatic UI page enumeration — translates "which frontend files changed"
|
|
3
|
+
* into "which URLs should the agent capture blueprints for."
|
|
4
|
+
*
|
|
5
|
+
* Replaces the prose-driven Task 0 strategy ladder in the testbot prompt with
|
|
6
|
+
* deterministic code so the candidate page list lives in the stateFile and is
|
|
7
|
+
* reusable across analyze_changes consumers (testbot, analyze_test_health,
|
|
8
|
+
* future blueprint-grounded recommenders).
|
|
9
|
+
*
|
|
10
|
+
* Strategies, tried in order:
|
|
11
|
+
*
|
|
12
|
+
* Strategy 1 — Framework route grep
|
|
13
|
+
* Translate file-system-routed component paths directly to URLs. Covers
|
|
14
|
+
* Next.js (app/ and pages/), Nuxt / Vue Router file-based, and SvelteKit.
|
|
15
|
+
* Gated on detection of a framework config file in the repo.
|
|
16
|
+
*
|
|
17
|
+
* Strategy 2 — Source-grounded routes
|
|
18
|
+
* Walks router/App files with the TS Compiler API for code-defined
|
|
19
|
+
* route declarations like `<Route path="/cart" element={<Cart/>}>`.
|
|
20
|
+
* Matches each route's component file OR declaration file against the
|
|
21
|
+
* changed-file list (so router rewrites surface their routes even when
|
|
22
|
+
* the component itself is unchanged). Handles React Router string-literal
|
|
23
|
+
* paths, cross-file constant resolution, Vue Router object literals,
|
|
24
|
+
* and Directus `defineModule` aggregation. See `sourceRouteExtractor.ts`.
|
|
25
|
+
*
|
|
26
|
+
* Strategy 3 — Root fallback
|
|
27
|
+
* Read the workspace's frontend baseUrl and treat it as the single
|
|
28
|
+
* candidate page. Always available when a frontend service is configured.
|
|
29
|
+
*/
|
|
30
|
+
import * as fs from "fs";
|
|
31
|
+
import * as path from "path";
|
|
32
|
+
import { extractSourceRoutes } from "./sourceRouteExtractor.js";
|
|
33
|
+
import { readWorkspaceConfigRaw } from "./workspaceAuth.js";
|
|
34
|
+
// ── Strategy 1: framework route grep ────────────────────────────────────────
|
|
35
|
+
/**
|
|
36
|
+
* File-system → URL mapping for filesystem-routed frameworks.
|
|
37
|
+
*
|
|
38
|
+
* Returns the URL path (no host) when the file is a route entrypoint, or
|
|
39
|
+
* null when the file isn't a route in any recognized convention.
|
|
40
|
+
*
|
|
41
|
+
* The `null` cases include component files used by routes (e.g. shared
|
|
42
|
+
* widgets in `components/`), API handlers (e.g. Next.js `pages/api/*`),
|
|
43
|
+
* and arbitrary `.ts`/`.js` modules under frontend directories. These
|
|
44
|
+
* are picked up by strategy 2 (import-graph walk, future) or by strategy
|
|
45
|
+
* 3 (root fallback).
|
|
46
|
+
*/
|
|
47
|
+
export function frameworkFileToUrlPath(filePath) {
|
|
48
|
+
// Normalize to forward slashes; works on Windows too.
|
|
49
|
+
const normalized = filePath.replace(/\\/g, "/");
|
|
50
|
+
// Next.js App Router root: app/page.{tsx,jsx} → /
|
|
51
|
+
if (/(?:^|\/)app\/page\.(tsx|jsx|ts|js)$/.test(normalized)) {
|
|
52
|
+
return "/";
|
|
53
|
+
}
|
|
54
|
+
// Next.js App Router nested: app/foo/page.tsx → /foo
|
|
55
|
+
// app/(group)/foo/page.tsx → /foo (route groups stripped in URL)
|
|
56
|
+
// app/foo/[id]/page.tsx → /foo/:id (Next.js bracket → colon)
|
|
57
|
+
const nextAppMatch = normalized.match(/(?:^|\/)app\/(.+?)\/page\.(tsx|jsx|ts|js)$/);
|
|
58
|
+
if (nextAppMatch) {
|
|
59
|
+
return "/" + stripRouteGroups(nextAppMatch[1]).replace(/\[(\.\.\.)?(\w+)\]/g, ":$2");
|
|
60
|
+
}
|
|
61
|
+
// Next.js Pages Router: pages/foo.tsx → /foo, pages/index.tsx → /
|
|
62
|
+
// Excludes pages/api/* (those are API handlers, not UI pages).
|
|
63
|
+
const nextPagesMatch = normalized.match(/(?:^|\/)pages\/(?!api\/)(.+?)\.(tsx|jsx|ts|js)$/);
|
|
64
|
+
if (nextPagesMatch) {
|
|
65
|
+
let urlPath = nextPagesMatch[1];
|
|
66
|
+
// /index → /, /foo/index → /foo
|
|
67
|
+
urlPath = urlPath.replace(/(?:^|\/)index$/, "");
|
|
68
|
+
urlPath = urlPath.replace(/\[(\.\.\.)?(\w+)\]/g, ":$2");
|
|
69
|
+
return "/" + urlPath;
|
|
70
|
+
}
|
|
71
|
+
// Nuxt / Vue Router file-based: pages/foo.vue → /foo
|
|
72
|
+
// Vue _id.vue → :id (Nuxt 2) or [id].vue → :id (Nuxt 3)
|
|
73
|
+
const nuxtMatch = normalized.match(/(?:^|\/)pages\/(.+?)\.vue$/);
|
|
74
|
+
if (nuxtMatch) {
|
|
75
|
+
let urlPath = nuxtMatch[1];
|
|
76
|
+
urlPath = urlPath.replace(/(?:^|\/)index$/, "");
|
|
77
|
+
urlPath = urlPath.replace(/\[(\.\.\.)?(\w+)\]/g, ":$2");
|
|
78
|
+
urlPath = urlPath.replace(/_(\w+)/g, ":$1");
|
|
79
|
+
return "/" + urlPath;
|
|
80
|
+
}
|
|
81
|
+
// SvelteKit: routes/foo/+page.svelte → /foo, routes/+page.svelte → /
|
|
82
|
+
const svelteMatch = normalized.match(/(?:^|\/)routes\/(.*?)\+page\.svelte$/);
|
|
83
|
+
if (svelteMatch) {
|
|
84
|
+
let urlPath = svelteMatch[1].replace(/\/$/, "");
|
|
85
|
+
urlPath = urlPath.replace(/\[(\.\.\.)?(\w+)\]/g, ":$2");
|
|
86
|
+
return "/" + urlPath;
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Drop Next.js App Router route-group segments — `(marketing)/foo` → `foo`.
|
|
92
|
+
* Route groups don't appear in the URL.
|
|
93
|
+
*/
|
|
94
|
+
function stripRouteGroups(routePath) {
|
|
95
|
+
return routePath
|
|
96
|
+
.split("/")
|
|
97
|
+
.filter((seg) => !/^\(.+\)$/.test(seg))
|
|
98
|
+
.join("/");
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Detect whether the repository is configured for filesystem-based routing.
|
|
102
|
+
*
|
|
103
|
+
* Strategy 1 maps file paths to URLs based on conventions (Next.js Pages
|
|
104
|
+
* Router, Nuxt, SvelteKit). Without a positive signal that the framework
|
|
105
|
+
* is actually configured, a repo that just *happens* to have a `pages/`
|
|
106
|
+
* directory (e.g. React with conventional layout, Vue with route-config
|
|
107
|
+
* code) produces false-positive URLs. This guard suppresses strategy 1
|
|
108
|
+
* for those repos so they fall through to root-fallback.
|
|
109
|
+
*
|
|
110
|
+
* Looks for framework config files in the repo root, common subdirectories,
|
|
111
|
+
* and the `frontendFiles[]`-derived candidate frontend roots.
|
|
112
|
+
*/
|
|
113
|
+
export function detectsFilesystemRouting(repositoryPath) {
|
|
114
|
+
const FRAMEWORK_CONFIG_FILES = [
|
|
115
|
+
// Next.js
|
|
116
|
+
"next.config.js", "next.config.mjs", "next.config.ts",
|
|
117
|
+
// Nuxt
|
|
118
|
+
"nuxt.config.ts", "nuxt.config.js",
|
|
119
|
+
// SvelteKit
|
|
120
|
+
"svelte.config.js", "svelte.config.ts",
|
|
121
|
+
];
|
|
122
|
+
// Search the repo root and one level deep into common app subdirectories.
|
|
123
|
+
// Most monorepos place the frontend at apps/web, app/, frontend/, web/, etc.
|
|
124
|
+
// Going more than one level adds I/O without meaningful coverage gain.
|
|
125
|
+
const ROOTS_TO_CHECK = [".", "frontend", "web", "app", "client", "apps/web"];
|
|
126
|
+
for (const root of ROOTS_TO_CHECK) {
|
|
127
|
+
for (const cfg of FRAMEWORK_CONFIG_FILES) {
|
|
128
|
+
const fullPath = path.join(repositoryPath, root, cfg);
|
|
129
|
+
try {
|
|
130
|
+
if (fs.existsSync(fullPath))
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
// ignore — fs errors don't change the answer
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Strategy 1 implementation. Filters frontend files through the framework
|
|
142
|
+
* route mapper; collects the URL paths that mapped successfully.
|
|
143
|
+
*
|
|
144
|
+
* Suppressed when the repo doesn't have a recognized filesystem-routing
|
|
145
|
+
* framework config — see `detectsFilesystemRouting`. Without that guard,
|
|
146
|
+
* repos with conventional `pages/` directories but code-defined routes
|
|
147
|
+
* (e.g. React with React Router) produce false-positive URLs.
|
|
148
|
+
*/
|
|
149
|
+
export function findCandidatePagesByFrameworkRoute(frontendFiles, baseUrl, repositoryPath) {
|
|
150
|
+
// When repositoryPath is provided, gate on framework detection. When
|
|
151
|
+
// omitted (e.g. older callers, unit tests), preserve original behavior.
|
|
152
|
+
if (repositoryPath !== undefined && !detectsFilesystemRouting(repositoryPath)) {
|
|
153
|
+
return [];
|
|
154
|
+
}
|
|
155
|
+
const byUrl = new Map();
|
|
156
|
+
for (const file of frontendFiles) {
|
|
157
|
+
const urlPath = frameworkFileToUrlPath(file);
|
|
158
|
+
if (urlPath === null)
|
|
159
|
+
continue;
|
|
160
|
+
const normalizedBase = baseUrl.replace(/\/$/, "");
|
|
161
|
+
const fullUrl = normalizedBase + urlPath;
|
|
162
|
+
const existing = byUrl.get(fullUrl);
|
|
163
|
+
if (existing) {
|
|
164
|
+
// Same URL surfaced by multiple files — track all sources.
|
|
165
|
+
if (!existing.sourcedFrom.includes(file)) {
|
|
166
|
+
existing.sourcedFrom.push(file);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
byUrl.set(fullUrl, {
|
|
171
|
+
url: fullUrl,
|
|
172
|
+
sourcedFrom: [file],
|
|
173
|
+
strategy: "framework-route-grep",
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return Array.from(byUrl.values());
|
|
178
|
+
}
|
|
179
|
+
// ── Strategy 2: source-grounded routes ──────────────────────────────────────
|
|
180
|
+
/**
|
|
181
|
+
* Strategy 2 implementation. Walks router/App files with the TS Compiler API
|
|
182
|
+
* to find route declarations (`<Route path="/cart" element={<Cart/>}>`),
|
|
183
|
+
* matches each declaration's component file against the changed-file list,
|
|
184
|
+
* and emits one CandidateUiPage per matching route.
|
|
185
|
+
*
|
|
186
|
+
* Slice 3a covers string-literal `path` attributes only — `path={CONST}` and
|
|
187
|
+
* Vue Router object literals are deferred to slice 3b.
|
|
188
|
+
*
|
|
189
|
+
* Returns empty when no route declarations match the changed files (which is
|
|
190
|
+
* also the right answer for SPAs without client-side routing — they fall
|
|
191
|
+
* through to root-fallback).
|
|
192
|
+
*/
|
|
193
|
+
export function findCandidatePagesBySourceRoute(frontendFiles, baseUrl, repositoryPath) {
|
|
194
|
+
const sourceRoutes = extractSourceRoutes(repositoryPath);
|
|
195
|
+
if (sourceRoutes.length === 0)
|
|
196
|
+
return [];
|
|
197
|
+
// Normalize changed files to absolute paths once.
|
|
198
|
+
const changedAbs = new Set(frontendFiles.map((f) => path.resolve(repositoryPath, f)));
|
|
199
|
+
const normalizedBase = baseUrl.replace(/\/$/, "");
|
|
200
|
+
const byUrl = new Map();
|
|
201
|
+
for (const route of sourceRoutes) {
|
|
202
|
+
// A route surfaces as a candidate when *either* its component file or its
|
|
203
|
+
// declaring file (router.ts, modules/<x>/index.ts) appears in the diff.
|
|
204
|
+
// Component-only matching misses route renames in the declaration file
|
|
205
|
+
// (the declaration changed; the component is unchanged).
|
|
206
|
+
const matchedBy = (route.componentFile && changedAbs.has(route.componentFile))
|
|
207
|
+
? route.componentFile
|
|
208
|
+
: (route.declaredIn && changedAbs.has(route.declaredIn))
|
|
209
|
+
? route.declaredIn
|
|
210
|
+
: null;
|
|
211
|
+
if (matchedBy === null)
|
|
212
|
+
continue;
|
|
213
|
+
const fullUrl = normalizedBase + (route.path.startsWith("/") ? route.path : "/" + route.path);
|
|
214
|
+
const sourceFile = path.relative(repositoryPath, matchedBy);
|
|
215
|
+
const existing = byUrl.get(fullUrl);
|
|
216
|
+
if (existing) {
|
|
217
|
+
if (!existing.sourcedFrom.includes(sourceFile)) {
|
|
218
|
+
existing.sourcedFrom.push(sourceFile);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
byUrl.set(fullUrl, {
|
|
223
|
+
url: fullUrl,
|
|
224
|
+
sourcedFrom: [sourceFile],
|
|
225
|
+
strategy: "source-grounded-routes",
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return Array.from(byUrl.values());
|
|
230
|
+
}
|
|
231
|
+
// ── Strategy 3: root fallback ───────────────────────────────────────────────
|
|
232
|
+
/**
|
|
233
|
+
* Strategy 3: when strategies 1+2 yield nothing, fall back to the workspace's
|
|
234
|
+
* frontend baseUrl as the single candidate page. The agent's downstream
|
|
235
|
+
* explore-and-discover step can then surface gated UI from the root.
|
|
236
|
+
*
|
|
237
|
+
* Returns null when the workspace has no recognizable frontend service.
|
|
238
|
+
*/
|
|
239
|
+
export async function findRootFallbackPage(repositoryPath) {
|
|
240
|
+
const baseUrl = await pickFrontendBaseUrl(repositoryPath);
|
|
241
|
+
if (!baseUrl)
|
|
242
|
+
return null;
|
|
243
|
+
return {
|
|
244
|
+
url: baseUrl,
|
|
245
|
+
sourcedFrom: [],
|
|
246
|
+
strategy: "root-fallback",
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Resolves the frontend service's baseUrl from the workspace config.
|
|
251
|
+
*
|
|
252
|
+
* Heuristics, in order:
|
|
253
|
+
* 1. Service whose `serviceName` ends with `-frontend` or contains `frontend`
|
|
254
|
+
* 2. Service whose `language` is `typescript` / `javascript` AND whose
|
|
255
|
+
* `framework` is one of the common UI frameworks (react, vue, svelte,
|
|
256
|
+
* next, nuxt, etc.)
|
|
257
|
+
* 3. First service with a defined `api.baseUrl`
|
|
258
|
+
*
|
|
259
|
+
* Returns the baseUrl string, or null when no service has a usable URL.
|
|
260
|
+
*
|
|
261
|
+
* Exported (vs private) so analyze_changes and future tools can reuse the
|
|
262
|
+
* same selection without re-implementing it.
|
|
263
|
+
*/
|
|
264
|
+
export async function pickFrontendBaseUrl(repositoryPath) {
|
|
265
|
+
const config = await readWorkspaceConfigRaw(repositoryPath);
|
|
266
|
+
if (!config?.services || !Array.isArray(config.services))
|
|
267
|
+
return null;
|
|
268
|
+
const services = config.services;
|
|
269
|
+
// Heuristic 1: serviceName signals frontend.
|
|
270
|
+
const byName = services.find((s) => typeof s?.serviceName === "string" &&
|
|
271
|
+
/(?:^|[-_])frontend(?:[-_]|$)|(?:^|[-_])ui(?:[-_]|$)|(?:^|[-_])app(?:[-_]|$)|(?:^|[-_])web(?:[-_]|$)/.test(s.serviceName.toLowerCase()) &&
|
|
272
|
+
s?.api?.baseUrl);
|
|
273
|
+
if (byName)
|
|
274
|
+
return byName.api.baseUrl;
|
|
275
|
+
// Heuristic 2: language + framework signal frontend.
|
|
276
|
+
const FRONTEND_FRAMEWORKS = new Set([
|
|
277
|
+
"react", "vue", "svelte", "next", "nextjs", "nuxt", "sveltekit",
|
|
278
|
+
"angular", "solid", "qwik", "remix", "astro",
|
|
279
|
+
]);
|
|
280
|
+
const byFramework = services.find((s) => {
|
|
281
|
+
const lang = (s?.language ?? "").toLowerCase();
|
|
282
|
+
const fw = (s?.framework ?? "").toLowerCase();
|
|
283
|
+
return ((lang === "typescript" || lang === "javascript") &&
|
|
284
|
+
FRONTEND_FRAMEWORKS.has(fw) &&
|
|
285
|
+
s?.api?.baseUrl);
|
|
286
|
+
});
|
|
287
|
+
if (byFramework)
|
|
288
|
+
return byFramework.api.baseUrl;
|
|
289
|
+
// Heuristic 3: first service with a baseUrl.
|
|
290
|
+
const anyBase = services.find((s) => s?.api?.baseUrl);
|
|
291
|
+
return anyBase?.api?.baseUrl ?? null;
|
|
292
|
+
}
|
|
293
|
+
// ── Composer ────────────────────────────────────────────────────────────────
|
|
294
|
+
/**
|
|
295
|
+
* Run the strategy ladder in order: framework route grep (1) → source-grounded
|
|
296
|
+
* routes (2) → root fallback (3). Each strategy is consulted only if the
|
|
297
|
+
* previous one produced no candidates. Returns whatever the ladder produces
|
|
298
|
+
* (possibly empty if the workspace has no frontend baseUrl AND none of the
|
|
299
|
+
* earlier strategies matched).
|
|
300
|
+
*/
|
|
301
|
+
export async function enumerateCandidateUiPages(repositoryPath, frontendFiles) {
|
|
302
|
+
if (frontendFiles.length === 0)
|
|
303
|
+
return [];
|
|
304
|
+
const baseUrl = await pickFrontendBaseUrl(repositoryPath);
|
|
305
|
+
// Strategy 1 needs a baseUrl to construct full URLs. Without one, the
|
|
306
|
+
// fallback path in strategy 3 also won't fire — return empty and let the
|
|
307
|
+
// caller decide what to do (typically: persist nothing, agent does its
|
|
308
|
+
// own enumeration via the existing prompt fallback).
|
|
309
|
+
if (!baseUrl)
|
|
310
|
+
return [];
|
|
311
|
+
const fromRoutes = findCandidatePagesByFrameworkRoute(frontendFiles, baseUrl, repositoryPath);
|
|
312
|
+
if (fromRoutes.length > 0)
|
|
313
|
+
return fromRoutes;
|
|
314
|
+
const fromSource = findCandidatePagesBySourceRoute(frontendFiles, baseUrl, repositoryPath);
|
|
315
|
+
if (fromSource.length > 0)
|
|
316
|
+
return fromSource;
|
|
317
|
+
const fallback = await findRootFallbackPage(repositoryPath);
|
|
318
|
+
return fallback ? [fallback] : [];
|
|
319
|
+
}
|