@skyramp/mcp 0.3.0-rc.1 → 0.3.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.
Files changed (136) hide show
  1. package/build/prompts/test-maintenance/actionsInstructions.js +4 -2
  2. package/build/prompts/test-maintenance/drift-analysis-prompt.d.ts +9 -0
  3. package/build/prompts/test-maintenance/drift-analysis-prompt.js +8 -7
  4. package/build/prompts/test-maintenance/driftAnalysisSections.js +4 -5
  5. package/build/prompts/test-maintenance/driftAnalysisShared.d.ts +11 -0
  6. package/build/prompts/test-maintenance/driftAnalysisShared.js +22 -6
  7. package/build/prompts/test-maintenance/uiDriftAnalysisSections.js +23 -31
  8. package/build/prompts/test-recommendation/diffExecutionPlan.d.ts +7 -1
  9. package/build/prompts/test-recommendation/diffExecutionPlan.js +20 -6
  10. package/build/prompts/test-recommendation/scopeAssessment.d.ts +10 -1
  11. package/build/prompts/test-recommendation/scopeAssessment.js +27 -2
  12. package/build/prompts/test-recommendation/scopeAssessment.test.js +35 -0
  13. package/build/prompts/test-recommendation/test-recommendation-prompt.js +5 -1
  14. package/build/prompts/test-recommendation/test-recommendation-prompt.test.js +61 -0
  15. package/build/prompts/testbot/testbot-prompts.js +7 -5
  16. package/build/prompts/testbot/testbot-prompts.test.js +1 -1
  17. package/build/services/AnalyticsService.js +37 -2
  18. package/build/services/TestDiscoveryService.d.ts +38 -4
  19. package/build/services/TestDiscoveryService.js +187 -16
  20. package/build/services/TestDiscoveryService.test.js +232 -10
  21. package/build/services/TestExecutionService.d.ts +12 -1
  22. package/build/services/TestExecutionService.js +20 -41
  23. package/build/services/TestGenerationService.d.ts +8 -0
  24. package/build/services/TestGenerationService.js +62 -17
  25. package/build/services/TestGenerationService.test.js +67 -1
  26. package/build/tools/auth/loginTool.js +1 -1
  27. package/build/tools/auth/logoutTool.js +1 -1
  28. package/build/tools/code-refactor/codeReuseTool.js +1 -1
  29. package/build/tools/code-refactor/modularizationTool.js +1 -1
  30. package/build/tools/executeSkyrampTestTool.js +30 -1
  31. package/build/tools/fixErrorTool.js +1 -1
  32. package/build/tools/one-click/oneClickTool.js +1 -1
  33. package/build/tools/test-management/actionsTool.js +76 -41
  34. package/build/tools/test-management/actionsTool.test.js +104 -38
  35. package/build/tools/test-management/analyzeChangesTool.js +102 -4
  36. package/build/tools/test-management/analyzeChangesTool.test.js +3 -1
  37. package/build/tools/test-management/analyzeTestHealthTool.js +9 -1
  38. package/build/tools/test-management/analyzeTestHealthTool.test.js +1 -1
  39. package/build/tools/trace/startTraceCollectionTool.js +1 -1
  40. package/build/tools/trace/stopTraceCollectionTool.js +1 -1
  41. package/build/tools/workspace/initScanWorkspaceTool.js +1 -1
  42. package/build/tools/workspace/initializeWorkspaceTool.js +1 -1
  43. package/build/tools/workspace/initializeWorkspaceTool.test.js +1 -1
  44. package/build/types/RepositoryAnalysis.d.ts +6 -6
  45. package/build/types/TestAnalysis.d.ts +13 -0
  46. package/build/utils/dartRouteExtractor.js +8 -30
  47. package/build/utils/docker.test.js +1 -1
  48. package/build/utils/fileWalk.d.ts +71 -0
  49. package/build/utils/fileWalk.js +79 -0
  50. package/build/utils/fileWalk.test.d.ts +1 -0
  51. package/build/utils/fileWalk.test.js +252 -0
  52. package/build/utils/frontendIntegration.js +20 -36
  53. package/build/utils/frontendSelectors.d.ts +34 -0
  54. package/build/utils/frontendSelectors.js +124 -0
  55. package/build/utils/frontendSelectors.test.d.ts +1 -0
  56. package/build/utils/frontendSelectors.test.js +118 -0
  57. package/build/utils/importerHop.d.ts +18 -0
  58. package/build/utils/importerHop.js +48 -3
  59. package/build/utils/importerHop.test.js +71 -1
  60. package/build/utils/pythonMountPrefixes.d.ts +25 -0
  61. package/build/utils/pythonMountPrefixes.js +347 -0
  62. package/build/utils/pythonMountPrefixes.test.d.ts +1 -0
  63. package/build/utils/pythonMountPrefixes.test.js +113 -0
  64. package/build/utils/repoScanner.js +30 -23
  65. package/build/utils/repoScanner.test.js +98 -0
  66. package/build/utils/routeParsers.d.ts +7 -1
  67. package/build/utils/routeParsers.js +32 -18
  68. package/build/utils/sourceRouteExtractor.js +17 -40
  69. package/build/utils/trace-parser.js +7 -19
  70. package/build/utils/versions.d.ts +3 -3
  71. package/build/utils/versions.js +1 -1
  72. package/build/utils/walkerCharacterization.test.d.ts +1 -0
  73. package/build/utils/walkerCharacterization.test.js +233 -0
  74. package/node_modules/playwright/lib/common/config.js +1 -1
  75. package/node_modules/playwright/lib/common/configLoader.js +1 -1
  76. package/node_modules/playwright/lib/common/fixtures.js +1 -1
  77. package/node_modules/playwright/lib/common/testType.js +1 -1
  78. package/node_modules/playwright/lib/index.js +2 -2
  79. package/node_modules/playwright/lib/isomorphic/testTree.js +1 -1
  80. package/node_modules/playwright/lib/matchers/expect.js +1 -1
  81. package/node_modules/playwright/lib/mcp/browser/tab.js +1 -1
  82. package/node_modules/playwright/lib/mcp/browser/tools/navigate.js +1 -1
  83. package/node_modules/playwright/lib/mcp/browser/tools/tracing.js +1 -1
  84. package/node_modules/playwright/lib/mcp/skyramp/loadTraceTool.js +7 -0
  85. package/node_modules/playwright/lib/mcp/skyramp/skyRampImport.js +2 -1
  86. package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +3 -1
  87. package/node_modules/playwright/lib/reporters/base.js +2 -2
  88. package/node_modules/playwright/lib/reporters/list.js +1 -1
  89. package/node_modules/playwright/lib/reporters/teleEmitter.js +1 -1
  90. package/node_modules/playwright/lib/worker/testInfo.js +2 -2
  91. package/node_modules/playwright/node_modules/playwright-core/ThirdPartyNotices.txt +23 -126
  92. package/node_modules/playwright/node_modules/playwright-core/lib/generated/injectedScriptSource.js +1 -1
  93. package/node_modules/playwright/node_modules/playwright-core/lib/generated/pollingRecorderSource.js +1 -1
  94. package/node_modules/playwright/node_modules/playwright-core/lib/server/codegen/skyramp/jsonlReader.js +2 -1
  95. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{codeMirrorModule-Cqp3cwEJ.js → codeMirrorModule-aszq5EdG.js} +1 -1
  96. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{defaultSettingsView-CYf9adZh.js → defaultSettingsView-BxS7Jm4s.js} +94 -94
  97. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.d4gkVSou.js → index.D4JTTy4R.js} +1 -1
  98. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +2 -2
  99. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{uiMode.u4_8VnCV.js → uiMode.DaRMQKOI.js} +1 -1
  100. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.html +2 -2
  101. package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
  102. package/node_modules/playwright/node_modules/playwright-core/src/generated/injectedScriptSource.ts +1 -1
  103. package/node_modules/playwright/node_modules/playwright-core/src/generated/pollingRecorderSource.ts +1 -1
  104. package/node_modules/playwright/node_modules/playwright-core/src/server/codegen/skyramp/jsonlReader.ts +1 -1
  105. package/node_modules/playwright/package.json +1 -1
  106. package/package.json +3 -3
  107. package/node_modules/playwright/._ThirdPartyNotices.txt +0 -0
  108. package/node_modules/playwright/._cli.js +0 -0
  109. package/node_modules/playwright/._index.d.ts +0 -0
  110. package/node_modules/playwright/._index.js +0 -0
  111. package/node_modules/playwright/._index.mjs +0 -0
  112. package/node_modules/playwright/._jsx-runtime.js +0 -0
  113. package/node_modules/playwright/._jsx-runtime.mjs +0 -0
  114. package/node_modules/playwright/._lib +0 -0
  115. package/node_modules/playwright/._node_modules +0 -0
  116. package/node_modules/playwright/._package.json +0 -0
  117. package/node_modules/playwright/._test.d.ts +0 -0
  118. package/node_modules/playwright/._test.js +0 -0
  119. package/node_modules/playwright/._test.mjs +0 -0
  120. package/node_modules/playwright/._types +0 -0
  121. package/node_modules/playwright/._vitest.config.ts +0 -0
  122. package/node_modules/playwright/node_modules/playwright-core/._.DS_Store +0 -0
  123. package/node_modules/playwright/node_modules/playwright-core/._.npmignore +0 -0
  124. package/node_modules/playwright/node_modules/playwright-core/._README.md +0 -0
  125. package/node_modules/playwright/node_modules/playwright-core/._ThirdPartyNotices.txt +0 -0
  126. package/node_modules/playwright/node_modules/playwright-core/._bin +0 -0
  127. package/node_modules/playwright/node_modules/playwright-core/._browsers.json +0 -0
  128. package/node_modules/playwright/node_modules/playwright-core/._bundles +0 -0
  129. package/node_modules/playwright/node_modules/playwright-core/._cli.js +0 -0
  130. package/node_modules/playwright/node_modules/playwright-core/._index.d.ts +0 -0
  131. package/node_modules/playwright/node_modules/playwright-core/._index.js +0 -0
  132. package/node_modules/playwright/node_modules/playwright-core/._index.mjs +0 -0
  133. package/node_modules/playwright/node_modules/playwright-core/._lib +0 -0
  134. package/node_modules/playwright/node_modules/playwright-core/._package.json +0 -0
  135. package/node_modules/playwright/node_modules/playwright-core/._src +0 -0
  136. package/node_modules/playwright/node_modules/playwright-core/._types +0 -0
@@ -1,29 +1,12 @@
1
1
  import * as fs from "fs";
2
2
  import * as path from "path";
3
3
  import { nextjsFileToApiPath, parseFileEndpoints, } from "./routeParsers.js";
4
+ import { resolvePythonMountPrefixes } from "./pythonMountPrefixes.js";
5
+ import { walkSourceFiles } from "./fileWalk.js";
4
6
  import { logger } from "./logger.js";
7
+ const GLOB_SKIP_DIRS = new Set(["node_modules", ".git", ".next", "dist", "build"]);
5
8
  function globRecursive(dir, extensions) {
6
- const results = [];
7
- let entries;
8
- try {
9
- entries = fs.readdirSync(dir, { withFileTypes: true });
10
- }
11
- catch (err) {
12
- logger.debug("globRecursive: skipping directory", { dir, error: err instanceof Error ? err.message : String(err) });
13
- return results;
14
- }
15
- for (const entry of entries) {
16
- const fullPath = path.join(dir, entry.name);
17
- if (entry.isDirectory()) {
18
- if (entry.name === "node_modules" || entry.name === ".git" || entry.name === ".next" || entry.name === "dist" || entry.name === "build")
19
- continue;
20
- results.push(...globRecursive(fullPath, extensions));
21
- }
22
- else if (extensions.some((ext) => entry.name.endsWith(ext))) {
23
- results.push(fullPath);
24
- }
25
- }
26
- return results;
9
+ return walkSourceFiles(dir, { extensions, skipDirs: GLOB_SKIP_DIRS });
27
10
  }
28
11
  function safeReadFile(filePath) {
29
12
  try {
@@ -231,8 +214,28 @@ export function findCandidateRouteFiles(repositoryPath) {
231
214
  }
232
215
  return result;
233
216
  }
217
+ /**
218
+ * Cross-file FastAPI mount-prefix lookup, resolved lazily: `resolvePythonMountPrefixes`
219
+ * walks the whole repo's Python, so it's only invoked the first time a `.py` file is
220
+ * actually scanned — non-Python repos pay nothing. Returns undefined for non-`.py`
221
+ * files (they never carry Python mount prefixes).
222
+ */
223
+ function makeMountPrefixLookup(repositoryPath) {
224
+ let resolved = null;
225
+ return (relative) => {
226
+ if (!relative.endsWith(".py"))
227
+ return undefined;
228
+ if (resolved === null)
229
+ resolved = resolvePythonMountPrefixes(repositoryPath);
230
+ return resolved.get(relative.replace(/\\/g, "/"));
231
+ };
232
+ }
234
233
  export function scanAllRepoEndpoints(repositoryPath) {
234
+ // Normalize up-front so relative-path derivation (and mount-prefix lookups) don't
235
+ // depend on a caller-supplied trailing separator.
236
+ repositoryPath = path.resolve(repositoryPath);
235
237
  const endpointMap = new Map();
238
+ const mountPrefixFor = makeMountPrefixLookup(repositoryPath);
236
239
  const sourceFiles = globRecursive(repositoryPath, [".ts", ".tsx", ".js", ".jsx", ".py", ".java", ".kt", ".go", ".rb", ".php", ".rs", ".cs"]);
237
240
  for (const file of sourceFiles) {
238
241
  if (scanNextjsFile(file, repositoryPath, endpointMap))
@@ -244,7 +247,7 @@ export function scanAllRepoEndpoints(repositoryPath) {
244
247
  const content = safeReadFile(file);
245
248
  if (content === null)
246
249
  continue;
247
- for (const ep of parseFileEndpoints(content, relative)) {
250
+ for (const ep of parseFileEndpoints(content, relative, mountPrefixFor(relative))) {
248
251
  addEndpointToMap(endpointMap, ep.path, ep.method, file, repositoryPath, ep.line);
249
252
  }
250
253
  }
@@ -256,6 +259,7 @@ export function scanAllRepoEndpoints(repositoryPath) {
256
259
  }));
257
260
  }
258
261
  export function scanRelatedEndpoints(repositoryPath, changedFiles) {
262
+ repositoryPath = path.resolve(repositoryPath);
259
263
  const relatedDirs = new Set();
260
264
  for (const f of changedFiles) {
261
265
  const absDir = path.dirname(path.join(repositoryPath, f));
@@ -265,6 +269,9 @@ export function scanRelatedEndpoints(repositoryPath, changedFiles) {
265
269
  relatedDirs.add(parentDir);
266
270
  }
267
271
  const endpointMap = new Map();
272
+ // Mount prefixes are resolved over the whole repo (aggregators may live outside
273
+ // the related dirs) but lazily — only once a Python file is actually scanned.
274
+ const mountPrefixFor = makeMountPrefixLookup(repositoryPath);
268
275
  for (const dir of relatedDirs) {
269
276
  if (!fs.existsSync(dir))
270
277
  continue;
@@ -279,7 +286,7 @@ export function scanRelatedEndpoints(repositoryPath, changedFiles) {
279
286
  const fileContent = safeReadFile(file);
280
287
  if (fileContent === null)
281
288
  continue;
282
- for (const ep of parseFileEndpoints(fileContent, relative)) {
289
+ for (const ep of parseFileEndpoints(fileContent, relative, mountPrefixFor(relative))) {
283
290
  addEndpointToMap(endpointMap, ep.path, ep.method, file, repositoryPath, ep.line);
284
291
  }
285
292
  }
@@ -89,4 +89,102 @@ async def bulk_delete_deployments(): pass
89
89
  { path: "/bulk_delete", methods: ["POST"], sourceFile: "src/prefect/server/api/flows.py" },
90
90
  ]));
91
91
  });
92
+ // FastAPI composes a route's full URL from prefixes applied at include_router()
93
+ // mount points that live in *other* files — the single-file parser can't see
94
+ // them. Mealie mounts its whole API under a top-level APIRouter(prefix="/api"),
95
+ // so a leaf router's own prefix ("/recipes") must be composed with the ancestor
96
+ // "/api" or generated tests hit the un-prefixed URL and 405.
97
+ it("composes cross-file include_router mount prefixes into FastAPI route paths", () => {
98
+ const repo = makeTempRepo();
99
+ const routesDir = path.join(repo, "mealie/routes");
100
+ const recipeDir = path.join(routesDir, "recipe");
101
+ fs.mkdirSync(recipeDir, { recursive: true });
102
+ // Top-level app: mounts the API router with no extra prefix.
103
+ fs.writeFileSync(path.join(repo, "mealie/app.py"), `
104
+ from fastapi import FastAPI
105
+ from mealie.routes import router
106
+
107
+ app = FastAPI()
108
+ app.include_router(router)
109
+ `);
110
+ // API aggregator: APIRouter(prefix="/api") includes each resource router.
111
+ fs.writeFileSync(path.join(routesDir, "__init__.py"), `
112
+ from fastapi import APIRouter
113
+ from . import recipe
114
+
115
+ router = APIRouter(prefix="/api")
116
+ router.include_router(recipe.router)
117
+ `);
118
+ // Recipe aggregator: no own prefix; mounts the CRUD router directly and the
119
+ // comments router under a "/recipes" mount prefix drawn from a local const.
120
+ fs.writeFileSync(path.join(recipeDir, "__init__.py"), `
121
+ from fastapi import APIRouter
122
+ from . import recipe_crud_routes, comments
123
+
124
+ prefix = "/recipes"
125
+
126
+ router = APIRouter()
127
+ router.include_router(recipe_crud_routes.router, tags=["Recipe: CRUD"])
128
+ router.include_router(comments.router, prefix=prefix, tags=["Recipe: Comments"])
129
+ `);
130
+ // Leaf CRUD router declared via a custom APIRouter *subclass* with its own
131
+ // "/recipes" prefix.
132
+ fs.writeFileSync(path.join(recipeDir, "recipe_crud_routes.py"), `
133
+ from mealie.routes._base.routers import UserAPIRouter
134
+
135
+ router = UserAPIRouter(prefix="/recipes")
136
+
137
+ @router.get("")
138
+ async def get_all(): pass
139
+
140
+ @router.get("/{slug}")
141
+ async def get_one(): pass
142
+ `);
143
+ // Comments router: no own prefix — its "/recipes" comes only from the mount.
144
+ fs.writeFileSync(path.join(recipeDir, "comments.py"), `
145
+ from fastapi import APIRouter
146
+
147
+ router = APIRouter()
148
+
149
+ @router.get("/{slug}/comments")
150
+ async def get_comments(): pass
151
+ `);
152
+ const endpoints = scanAllRepoEndpoints(repo);
153
+ // CRUD: /api (aggregator) + /recipes (leaf subclass own prefix)
154
+ expect(endpoints).toEqual(expect.arrayContaining([
155
+ expect.objectContaining({ path: "/api/recipes", methods: ["GET"], sourceFile: "mealie/routes/recipe/recipe_crud_routes.py" }),
156
+ expect.objectContaining({ path: "/api/recipes/{slug}", methods: ["GET"], sourceFile: "mealie/routes/recipe/recipe_crud_routes.py" }),
157
+ ]));
158
+ // Comments: /api (aggregator) + /recipes (mount prefix from a local const) + own path
159
+ expect(endpoints).toEqual(expect.arrayContaining([
160
+ expect.objectContaining({ path: "/api/recipes/{slug}/comments", methods: ["GET"], sourceFile: "mealie/routes/recipe/comments.py" }),
161
+ ]));
162
+ // The un-prefixed path must NOT appear (the 405 bug).
163
+ expect(endpoints).not.toContainEqual(expect.objectContaining({ path: "/recipes" }));
164
+ });
165
+ it("composes an app-level include_router(prefix=...) mount", () => {
166
+ const repo = makeTempRepo();
167
+ const apiDir = path.join(repo, "src/api");
168
+ fs.mkdirSync(apiDir, { recursive: true });
169
+ // Prefix applied at the app mount point rather than on the APIRouter itself.
170
+ fs.writeFileSync(path.join(repo, "src/main.py"), `
171
+ from fastapi import FastAPI
172
+ from src.api.users import router as users_router
173
+
174
+ app = FastAPI()
175
+ app.include_router(users_router, prefix="/api/v1")
176
+ `);
177
+ fs.writeFileSync(path.join(apiDir, "users.py"), `
178
+ from fastapi import APIRouter
179
+
180
+ router = APIRouter(prefix="/users")
181
+
182
+ @router.get("/{id}")
183
+ async def get_user(): pass
184
+ `);
185
+ const endpoints = scanAllRepoEndpoints(repo);
186
+ expect(endpoints).toEqual(expect.arrayContaining([
187
+ expect.objectContaining({ path: "/api/v1/users/{id}", methods: ["GET"], sourceFile: "src/api/users.py" }),
188
+ ]));
189
+ });
92
190
  });
@@ -27,8 +27,14 @@ export declare function parseRouteLine(line: string, sourceFile: string): Parsed
27
27
  *
28
28
  * Falls back to pure parseRouteLine for all other frameworks (Express inline, Gin,
29
29
  * Rails, Laravel, Actix, Hapi, Go stdlib, etc.), which remain unaffected.
30
+ *
31
+ * `mountPrefixes` optionally supplies, per router variable, the **ancestor**
32
+ * prefix that router inherits from cross-file `include_router(...)` mount points
33
+ * (see `resolvePythonMountPrefixes`). It is composed *ahead* of the router's own
34
+ * in-file prefix, so a `router = APIRouter(prefix="/recipes")` mounted under an
35
+ * aggregator's `APIRouter(prefix="/api")` yields `/api/recipes`, not `/recipes`.
30
36
  */
31
- export declare function parseFileEndpoints(content: string, sourceFile: string): ParsedDiffEndpoint[];
37
+ export declare function parseFileEndpoints(content: string, sourceFile: string, mountPrefixes?: Map<string, string>): ParsedDiffEndpoint[];
32
38
  export declare const SKIP_PATH_SEGMENTS: Set<string>;
33
39
  /**
34
40
  * True for version/prefix path segments — "v1".."v99" and the literal "api".
@@ -1,3 +1,4 @@
1
+ import { joinRoutePrefix } from "./pythonMountPrefixes.js";
1
2
  import { logger } from "./logger.js";
2
3
  export function nextjsFileToApiPath(filePath) {
3
4
  const pagesMatch = filePath.match(/(?:^|\/)pages\/(api\/.+)\.[jt]sx?$/);
@@ -183,13 +184,29 @@ export function parseRouteLine(line, sourceFile) {
183
184
  *
184
185
  * Falls back to pure parseRouteLine for all other frameworks (Express inline, Gin,
185
186
  * Rails, Laravel, Actix, Hapi, Go stdlib, etc.), which remain unaffected.
187
+ *
188
+ * `mountPrefixes` optionally supplies, per router variable, the **ancestor**
189
+ * prefix that router inherits from cross-file `include_router(...)` mount points
190
+ * (see `resolvePythonMountPrefixes`). It is composed *ahead* of the router's own
191
+ * in-file prefix, so a `router = APIRouter(prefix="/recipes")` mounted under an
192
+ * aggregator's `APIRouter(prefix="/api")` yields `/api/recipes`, not `/recipes`.
186
193
  */
187
- export function parseFileEndpoints(content, sourceFile) {
194
+ export function parseFileEndpoints(content, sourceFile, mountPrefixes) {
188
195
  const results = [];
189
196
  let classPrefix = ""; // active prefix for the current class body
190
197
  let pendingPrefix = ""; // set by class-level annotation; activated on class declaration
191
198
  // FastAPI APIRouter / Flask Blueprint: variable name → prefix
192
199
  const routerPrefixes = new Map();
200
+ // Effective prefix for a router var = its cross-file ancestor mount prefix
201
+ // composed with its own in-file APIRouter(prefix=…). Returns undefined when
202
+ // the var has neither, so callers leave the path untouched.
203
+ const effectivePrefix = (varName) => {
204
+ const own = routerPrefixes.get(varName);
205
+ const mount = mountPrefixes?.get(varName);
206
+ if (own === undefined && mount === undefined)
207
+ return undefined;
208
+ return joinRoutePrefix(mount ?? "", own ?? "");
209
+ };
193
210
  let pendingRouterVar = ""; // variable name being accumulated in a multi-line declaration
194
211
  let pendingPythonDecorator = null;
195
212
  let pendingExpressMethod = ""; // HTTP method from a multi-line Express route (e.g. "POST" from `router.post(\n`)
@@ -254,15 +271,16 @@ export function parseFileEndpoints(content, sourceFile) {
254
271
  }
255
272
  continue;
256
273
  }
257
- // Single-line: varName = APIRouter(prefix="/path") or APIRouter(tags=..., prefix="/path")
258
- const singleM = trimmed.match(/^(\w+)\s*=\s*APIRouter\s*\([^)]*prefix\s*=\s*["']([^"'?#]+)["']/) ||
274
+ // Single-line: varName = APIRouter(prefix="/path") or APIRouter(tags=..., prefix="/path").
275
+ // `\w*APIRouter` also matches custom subclasses (e.g. Mealie's UserAPIRouter).
276
+ const singleM = trimmed.match(/^(\w+)\s*=\s*\w*APIRouter\s*\([^)]*prefix\s*=\s*["']([^"'?#]+)["']/) ||
259
277
  trimmed.match(/^(\w+)\s*=\s*Blueprint\s*\([^)]*url_prefix\s*=\s*["']([^"'?#]+)["']/);
260
278
  if (singleM) {
261
279
  routerPrefixes.set(singleM[1], singleM[2]);
262
280
  continue;
263
281
  }
264
282
  // Multi-line open: varName = APIRouter( or Blueprint( without closing ) on this line
265
- if (/^(\w+)\s*=\s*(?:APIRouter|Blueprint)\s*\(/.test(trimmed) && !trimmed.includes(")")) {
283
+ if (/^(\w+)\s*=\s*(?:\w*APIRouter|Blueprint)\s*\(/.test(trimmed) && !trimmed.includes(")")) {
266
284
  const openM = trimmed.match(/^(\w+)/);
267
285
  if (openM)
268
286
  pendingRouterVar = openM[1];
@@ -278,15 +296,13 @@ export function parseFileEndpoints(content, sourceFile) {
278
296
  const pathMatch = trimmed.match(/^(?:path\s*=\s*)?["']([^"'?#]*)/) ||
279
297
  trimmed.match(/^value\s*=\s*["']([^"'?#]*)/);
280
298
  if (pathMatch) {
281
- const routerPrefix = routerPrefixes.get(pendingPythonDecorator.routerVar);
282
- const prefix = routerPrefix?.replace(/\/$/, "");
283
- const suffix = pathMatch[1].replace(/^\//, "");
284
- const path = prefix
285
- ? (suffix ? `${prefix}/${suffix}` : prefix)
299
+ const eff = effectivePrefix(pendingPythonDecorator.routerVar);
300
+ const composedPath = eff !== undefined
301
+ ? joinRoutePrefix(eff, pathMatch[1])
286
302
  : pathMatch[1];
287
303
  results.push({
288
304
  method: pendingPythonDecorator.method,
289
- path,
305
+ path: composedPath,
290
306
  sourceFile,
291
307
  line: pendingPythonDecorator.startLine,
292
308
  });
@@ -448,17 +464,15 @@ export function parseFileEndpoints(content, sourceFile) {
448
464
  ep.path = `${prefix}/${suffix}`;
449
465
  }
450
466
  }
451
- else if (routerPrefixes.size > 0) {
452
- // Router-variable prefix (FastAPI, Flask Blueprint)
467
+ else if (routerPrefixes.size > 0 || (mountPrefixes && mountPrefixes.size > 0)) {
468
+ // Router-variable prefix (FastAPI, Flask Blueprint) — own prefix composed
469
+ // with any cross-file ancestor mount prefix.
453
470
  const varM = trimmed.match(/@(\w+)\.(get|post|put|patch|delete|head|options)\s*\(/i)
454
471
  || trimmed.match(/@(\w+)\.route\s*\(/i);
455
472
  if (varM) {
456
- const routerPrefix = routerPrefixes.get(varM[1]);
457
- if (routerPrefix) {
458
- const prefix = routerPrefix.replace(/\/$/, "");
459
- const suffix = ep.path.replace(/^\//, "");
460
- ep.path = suffix ? `${prefix}/${suffix}` : prefix;
461
- }
473
+ const eff = effectivePrefix(varM[1]);
474
+ if (eff !== undefined)
475
+ ep.path = joinRoutePrefix(eff, ep.path);
462
476
  }
463
477
  }
464
478
  ep.line = lineNo;
@@ -30,6 +30,7 @@
30
30
  import * as fs from "fs";
31
31
  import * as path from "path";
32
32
  import * as ts from "typescript";
33
+ import { collectFiles } from "./fileWalk.js";
33
34
  function makeParseCache() {
34
35
  return { files: new Map(), tsconfigBaseUrl: new Map() };
35
36
  }
@@ -123,49 +124,25 @@ function parseFile(filePath, cache) {
123
124
  }
124
125
  // ── File discovery ────────────────────────────────────────────────────────
125
126
  function findRouterFiles(repoPath) {
126
- const matches = [];
127
127
  const targets = new Set(ROUTER_FILE_GLOBS.map((g) => g.toLowerCase()));
128
128
  const indexExtensions = new Set([".ts", ".tsx", ".js", ".jsx"]);
129
- function walk(dir, depth) {
130
- if (depth > MAX_WALK_DEPTH)
131
- return;
132
- if (matches.length >= MAX_CANDIDATE_FILES)
133
- return;
134
- let entries;
135
- try {
136
- entries = fs.readdirSync(dir, { withFileTypes: true });
137
- }
138
- catch {
139
- return;
140
- }
141
- for (const entry of entries) {
142
- if (matches.length >= MAX_CANDIDATE_FILES)
143
- return;
144
- const full = path.join(dir, entry.name);
145
- if (entry.isDirectory()) {
146
- if (SKIP_DIRS.has(entry.name) || entry.name.startsWith("."))
147
- continue;
148
- walk(full, depth + 1);
149
- }
150
- else if (entry.isFile()) {
151
- const lower = entry.name.toLowerCase();
152
- if (targets.has(lower)) {
153
- matches.push(full);
154
- continue;
155
- }
156
- // Directus-style modules: `…/modules/<name>/index.{ts,tsx,js,jsx}`
157
- // hosts a `defineModule({ routes: [...] })` call.
158
- if (lower.startsWith("index.")) {
159
- const ext = path.extname(lower);
160
- if (indexExtensions.has(ext) && full.includes(`${path.sep}modules${path.sep}`)) {
161
- matches.push(full);
162
- }
163
- }
129
+ return collectFiles(repoPath, {
130
+ match: (entry, full) => {
131
+ const lower = entry.name.toLowerCase();
132
+ if (targets.has(lower))
133
+ return true;
134
+ // Directus-style modules: `…/modules/<name>/index.{ts,tsx,js,jsx}`
135
+ // hosts a `defineModule({ routes: [...] })` call.
136
+ if (lower.startsWith("index.")) {
137
+ const ext = path.extname(lower);
138
+ return indexExtensions.has(ext) && full.includes(`${path.sep}modules${path.sep}`);
164
139
  }
165
- }
166
- }
167
- walk(repoPath, 0);
168
- return matches;
140
+ return false;
141
+ },
142
+ limit: MAX_CANDIDATE_FILES,
143
+ maxDepth: MAX_WALK_DEPTH,
144
+ shouldSkipDir: (entry) => SKIP_DIRS.has(entry.name) || entry.name.startsWith("."),
145
+ });
169
146
  }
170
147
  // ── Per-file extraction ────────────────────────────────────────────────────
171
148
  function extractRoutesFromFile(filePath, cache) {
@@ -5,6 +5,7 @@
5
5
  import * as fs from "fs";
6
6
  import * as path from "path";
7
7
  import { KNOWN_AUTH_HEADERS } from "./workspaceAuth.js";
8
+ import { walkDir } from "./fileWalk.js";
8
9
  const FLOW_GAP_MS = 30_000;
9
10
  // Redact-only headers: scrub these from traces but don't treat them as extractable auth headers.
10
11
  // set-cookie carries session tokens; auth-token / client-secret / access-token are generic secrets.
@@ -168,27 +169,14 @@ const SKIP_DIRS = new Set(["node_modules", ".git", "dist", "build", ".next", ".n
168
169
  /** Known test-artifact directories where testbot-generated traces are written. */
169
170
  const TRACE_SCAN_DIRS = [".skyramp", "tests", "test", "e2e", "playwright"];
170
171
  /**
171
- * Recursively scan a directory for files matching a predicate, up to maxDepth levels.
172
+ * Recursively scan a directory for files matching a predicate, up to maxDepth
173
+ * levels below `dir` (maxDepth 0 = only files directly in `dir`).
172
174
  */
173
175
  function scanDir(dir, predicate, maxDepth, results) {
174
- if (maxDepth < 0)
175
- return;
176
- let entries;
177
- try {
178
- entries = fs.readdirSync(dir, { withFileTypes: true });
179
- }
180
- catch {
181
- return;
182
- }
183
- for (const entry of entries) {
184
- if (entry.isDirectory()) {
185
- if (!SKIP_DIRS.has(entry.name)) {
186
- scanDir(path.join(dir, entry.name), predicate, maxDepth - 1, results);
187
- }
188
- }
189
- else if (entry.isFile() && predicate(entry.name)) {
190
- results.push(path.join(dir, entry.name));
191
- }
176
+ const opts = { maxDepth, shouldSkipDir: (entry) => SKIP_DIRS.has(entry.name) };
177
+ for (const [entry, full] of walkDir(dir, opts)) {
178
+ if (predicate(entry.name))
179
+ results.push(full);
192
180
  }
193
181
  }
194
182
  /**
@@ -1,3 +1,3 @@
1
- export declare const SKYRAMP_IMAGE_VERSION = "v1.3.33";
2
- export declare const EXECUTOR_DOCKER_IMAGE = "skyramp/executor:v1.3.33";
3
- export declare const WORKER_DOCKER_IMAGE = "skyramp/worker:v1.3.33";
1
+ export declare const SKYRAMP_IMAGE_VERSION = "v1.3.34";
2
+ export declare const EXECUTOR_DOCKER_IMAGE = "skyramp/executor:v1.3.34";
3
+ export declare const WORKER_DOCKER_IMAGE = "skyramp/worker:v1.3.34";
@@ -1,3 +1,3 @@
1
- export const SKYRAMP_IMAGE_VERSION = "v1.3.33";
1
+ export const SKYRAMP_IMAGE_VERSION = "v1.3.34";
2
2
  export const EXECUTOR_DOCKER_IMAGE = `skyramp/executor:${SKYRAMP_IMAGE_VERSION}`;
3
3
  export const WORKER_DOCKER_IMAGE = `skyramp/worker:${SKYRAMP_IMAGE_VERSION}`;
@@ -0,0 +1 @@
1
+ export {};