opensafari-mcp 0.1.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.
Files changed (235) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +341 -0
  3. package/dist/115.index.js +2 -0
  4. package/dist/115.index.js.map +1 -0
  5. package/dist/67.index.js +2 -0
  6. package/dist/67.index.js.map +1 -0
  7. package/dist/679.index.js +2 -0
  8. package/dist/679.index.js.map +1 -0
  9. package/dist/681.index.js +2 -0
  10. package/dist/681.index.js.map +1 -0
  11. package/dist/838.index.js +2 -0
  12. package/dist/838.index.js.map +1 -0
  13. package/dist/auth/index.d.ts +3 -0
  14. package/dist/auth/index.d.ts.map +1 -0
  15. package/dist/auth/manager.d.ts +33 -0
  16. package/dist/auth/manager.d.ts.map +1 -0
  17. package/dist/cli/148.index.js +3 -0
  18. package/dist/cli/148.index.js.map +1 -0
  19. package/dist/cli/473.index.js +3 -0
  20. package/dist/cli/473.index.js.map +1 -0
  21. package/dist/cli/622.index.js +3 -0
  22. package/dist/cli/622.index.js.map +1 -0
  23. package/dist/cli/712.index.js +3 -0
  24. package/dist/cli/712.index.js.map +1 -0
  25. package/dist/cli/844.index.js +3 -0
  26. package/dist/cli/844.index.js.map +1 -0
  27. package/dist/cli/index.d.ts +3 -0
  28. package/dist/cli/index.d.ts.map +1 -0
  29. package/dist/cli/index.js +3 -0
  30. package/dist/cli/index.js.map +1 -0
  31. package/dist/comparison/cross-viewport.d.ts +36 -0
  32. package/dist/comparison/cross-viewport.d.ts.map +1 -0
  33. package/dist/comparison/index.d.ts +4 -0
  34. package/dist/comparison/index.d.ts.map +1 -0
  35. package/dist/comparison/report.d.ts +10 -0
  36. package/dist/comparison/report.d.ts.map +1 -0
  37. package/dist/config/defaults.d.ts +33 -0
  38. package/dist/config/defaults.d.ts.map +1 -0
  39. package/dist/config/global.d.ts +25 -0
  40. package/dist/config/global.d.ts.map +1 -0
  41. package/dist/config/index.d.ts +5 -0
  42. package/dist/config/index.d.ts.map +1 -0
  43. package/dist/config/tool-tiers.d.ts +7 -0
  44. package/dist/config/tool-tiers.d.ts.map +1 -0
  45. package/dist/errors/codes.d.ts +20 -0
  46. package/dist/errors/codes.d.ts.map +1 -0
  47. package/dist/errors/index.d.ts +4 -0
  48. package/dist/errors/index.d.ts.map +1 -0
  49. package/dist/errors/timeout.d.ts +20 -0
  50. package/dist/errors/timeout.d.ts.map +1 -0
  51. package/dist/index.d.ts +25 -0
  52. package/dist/index.d.ts.map +1 -0
  53. package/dist/index.js +2 -0
  54. package/dist/index.js.map +1 -0
  55. package/dist/mcp-server.d.ts +41 -0
  56. package/dist/mcp-server.d.ts.map +1 -0
  57. package/dist/metrics/collector.d.ts +43 -0
  58. package/dist/metrics/collector.d.ts.map +1 -0
  59. package/dist/orchestration/index.d.ts +3 -0
  60. package/dist/orchestration/index.d.ts.map +1 -0
  61. package/dist/orchestration/workflow-engine.d.ts +90 -0
  62. package/dist/orchestration/workflow-engine.d.ts.map +1 -0
  63. package/dist/qa/audit.d.ts +42 -0
  64. package/dist/qa/audit.d.ts.map +1 -0
  65. package/dist/qa/detectors/auto-zoom.d.ts +4 -0
  66. package/dist/qa/detectors/auto-zoom.d.ts.map +1 -0
  67. package/dist/qa/detectors/dark-mode.d.ts +5 -0
  68. package/dist/qa/detectors/dark-mode.d.ts.map +1 -0
  69. package/dist/qa/detectors/fixed-stacking.d.ts +4 -0
  70. package/dist/qa/detectors/fixed-stacking.d.ts.map +1 -0
  71. package/dist/qa/detectors/horizontal-overflow.d.ts +4 -0
  72. package/dist/qa/detectors/horizontal-overflow.d.ts.map +1 -0
  73. package/dist/qa/detectors/hover-only.d.ts +4 -0
  74. package/dist/qa/detectors/hover-only.d.ts.map +1 -0
  75. package/dist/qa/detectors/index.d.ts +14 -0
  76. package/dist/qa/detectors/index.d.ts.map +1 -0
  77. package/dist/qa/detectors/input-type.d.ts +4 -0
  78. package/dist/qa/detectors/input-type.d.ts.map +1 -0
  79. package/dist/qa/detectors/keyboard-overlap.d.ts +4 -0
  80. package/dist/qa/detectors/keyboard-overlap.d.ts.map +1 -0
  81. package/dist/qa/detectors/orientation.d.ts +5 -0
  82. package/dist/qa/detectors/orientation.d.ts.map +1 -0
  83. package/dist/qa/detectors/pwa-meta.d.ts +4 -0
  84. package/dist/qa/detectors/pwa-meta.d.ts.map +1 -0
  85. package/dist/qa/detectors/safe-area.d.ts +4 -0
  86. package/dist/qa/detectors/safe-area.d.ts.map +1 -0
  87. package/dist/qa/detectors/scroll-lock.d.ts +4 -0
  88. package/dist/qa/detectors/scroll-lock.d.ts.map +1 -0
  89. package/dist/qa/detectors/touch-targets.d.ts +4 -0
  90. package/dist/qa/detectors/touch-targets.d.ts.map +1 -0
  91. package/dist/qa/detectors/vh100.d.ts +4 -0
  92. package/dist/qa/detectors/vh100.d.ts.map +1 -0
  93. package/dist/qa/history.d.ts +44 -0
  94. package/dist/qa/history.d.ts.map +1 -0
  95. package/dist/qa/index.d.ts +8 -0
  96. package/dist/qa/index.d.ts.map +1 -0
  97. package/dist/qa/report-markdown.d.ts +3 -0
  98. package/dist/qa/report-markdown.d.ts.map +1 -0
  99. package/dist/qa/types.d.ts +29 -0
  100. package/dist/qa/types.d.ts.map +1 -0
  101. package/dist/reliability/crash-watcher.d.ts +16 -0
  102. package/dist/reliability/crash-watcher.d.ts.map +1 -0
  103. package/dist/reliability/graceful-shutdown.d.ts +3 -0
  104. package/dist/reliability/graceful-shutdown.d.ts.map +1 -0
  105. package/dist/reliability/index.d.ts +4 -0
  106. package/dist/reliability/index.d.ts.map +1 -0
  107. package/dist/reliability/zombie-cleanup.d.ts +53 -0
  108. package/dist/reliability/zombie-cleanup.d.ts.map +1 -0
  109. package/dist/security/audit-logger.d.ts +2 -0
  110. package/dist/security/audit-logger.d.ts.map +1 -0
  111. package/dist/security/content-sanitizer.d.ts +32 -0
  112. package/dist/security/content-sanitizer.d.ts.map +1 -0
  113. package/dist/security/domain-guard.d.ts +16 -0
  114. package/dist/security/domain-guard.d.ts.map +1 -0
  115. package/dist/session-manager.d.ts +54 -0
  116. package/dist/session-manager.d.ts.map +1 -0
  117. package/dist/simulator/batch.d.ts +22 -0
  118. package/dist/simulator/batch.d.ts.map +1 -0
  119. package/dist/simulator/index.d.ts +15 -0
  120. package/dist/simulator/index.d.ts.map +1 -0
  121. package/dist/simulator/manager.d.ts +66 -0
  122. package/dist/simulator/manager.d.ts.map +1 -0
  123. package/dist/simulator/pool.d.ts +52 -0
  124. package/dist/simulator/pool.d.ts.map +1 -0
  125. package/dist/simulator/presets.d.ts +4 -0
  126. package/dist/simulator/presets.d.ts.map +1 -0
  127. package/dist/simulator/proxy.d.ts +78 -0
  128. package/dist/simulator/proxy.d.ts.map +1 -0
  129. package/dist/simulator/simctl.d.ts +12 -0
  130. package/dist/simulator/simctl.d.ts.map +1 -0
  131. package/dist/simulator/socket-finder.d.ts +22 -0
  132. package/dist/simulator/socket-finder.d.ts.map +1 -0
  133. package/dist/simulator/types.d.ts +21 -0
  134. package/dist/simulator/types.d.ts.map +1 -0
  135. package/dist/simulator/xcode-check.d.ts +15 -0
  136. package/dist/simulator/xcode-check.d.ts.map +1 -0
  137. package/dist/tools/appearance-toggle.d.ts +3 -0
  138. package/dist/tools/appearance-toggle.d.ts.map +1 -0
  139. package/dist/tools/auth.d.ts +3 -0
  140. package/dist/tools/auth.d.ts.map +1 -0
  141. package/dist/tools/batch-execute.d.ts +5 -0
  142. package/dist/tools/batch-execute.d.ts.map +1 -0
  143. package/dist/tools/batch-navigate.d.ts +5 -0
  144. package/dist/tools/batch-navigate.d.ts.map +1 -0
  145. package/dist/tools/batch-screenshot.d.ts +5 -0
  146. package/dist/tools/batch-screenshot.d.ts.map +1 -0
  147. package/dist/tools/click.d.ts +3 -0
  148. package/dist/tools/click.d.ts.map +1 -0
  149. package/dist/tools/cookies.d.ts +3 -0
  150. package/dist/tools/cookies.d.ts.map +1 -0
  151. package/dist/tools/cross-viewport-compare.d.ts +4 -0
  152. package/dist/tools/cross-viewport-compare.d.ts.map +1 -0
  153. package/dist/tools/device-boot.d.ts +3 -0
  154. package/dist/tools/device-boot.d.ts.map +1 -0
  155. package/dist/tools/device-list.d.ts +3 -0
  156. package/dist/tools/device-list.d.ts.map +1 -0
  157. package/dist/tools/device-rotate.d.ts +3 -0
  158. package/dist/tools/device-rotate.d.ts.map +1 -0
  159. package/dist/tools/device-shutdown.d.ts +3 -0
  160. package/dist/tools/device-shutdown.d.ts.map +1 -0
  161. package/dist/tools/dismiss-keyboard.d.ts +3 -0
  162. package/dist/tools/dismiss-keyboard.d.ts.map +1 -0
  163. package/dist/tools/index.d.ts +8 -0
  164. package/dist/tools/index.d.ts.map +1 -0
  165. package/dist/tools/inspect.d.ts +3 -0
  166. package/dist/tools/inspect.d.ts.map +1 -0
  167. package/dist/tools/javascript.d.ts +3 -0
  168. package/dist/tools/javascript.d.ts.map +1 -0
  169. package/dist/tools/long-press.d.ts +3 -0
  170. package/dist/tools/long-press.d.ts.map +1 -0
  171. package/dist/tools/navigate.d.ts +3 -0
  172. package/dist/tools/navigate.d.ts.map +1 -0
  173. package/dist/tools/orchestration-tools.d.ts +5 -0
  174. package/dist/tools/orchestration-tools.d.ts.map +1 -0
  175. package/dist/tools/press.d.ts +3 -0
  176. package/dist/tools/press.d.ts.map +1 -0
  177. package/dist/tools/qa-audit.d.ts +3 -0
  178. package/dist/tools/qa-audit.d.ts.map +1 -0
  179. package/dist/tools/qa-detectors.d.ts +3 -0
  180. package/dist/tools/qa-detectors.d.ts.map +1 -0
  181. package/dist/tools/query-dom.d.ts +3 -0
  182. package/dist/tools/query-dom.d.ts.map +1 -0
  183. package/dist/tools/read-page.d.ts +3 -0
  184. package/dist/tools/read-page.d.ts.map +1 -0
  185. package/dist/tools/screenshot.d.ts +3 -0
  186. package/dist/tools/screenshot.d.ts.map +1 -0
  187. package/dist/tools/scroll.d.ts +3 -0
  188. package/dist/tools/scroll.d.ts.map +1 -0
  189. package/dist/tools/select-option.d.ts +3 -0
  190. package/dist/tools/select-option.d.ts.map +1 -0
  191. package/dist/tools/swipe.d.ts +3 -0
  192. package/dist/tools/swipe.d.ts.map +1 -0
  193. package/dist/tools/type.d.ts +3 -0
  194. package/dist/tools/type.d.ts.map +1 -0
  195. package/dist/tools/wait-for.d.ts +3 -0
  196. package/dist/tools/wait-for.d.ts.map +1 -0
  197. package/dist/transports/http.d.ts +57 -0
  198. package/dist/transports/http.d.ts.map +1 -0
  199. package/dist/transports/index.d.ts +38 -0
  200. package/dist/transports/index.d.ts.map +1 -0
  201. package/dist/transports/stdio.d.ts +16 -0
  202. package/dist/transports/stdio.d.ts.map +1 -0
  203. package/dist/types/browser-backend.d.ts +84 -0
  204. package/dist/types/browser-backend.d.ts.map +1 -0
  205. package/dist/types/mcp.d.ts +63 -0
  206. package/dist/types/mcp.d.ts.map +1 -0
  207. package/dist/types/tool-manifest.d.ts +52 -0
  208. package/dist/types/tool-manifest.d.ts.map +1 -0
  209. package/dist/utils/format-age.d.ts +5 -0
  210. package/dist/utils/format-age.d.ts.map +1 -0
  211. package/dist/utils/format-error.d.ts +5 -0
  212. package/dist/utils/format-error.d.ts.map +1 -0
  213. package/dist/utils/logger.d.ts +10 -0
  214. package/dist/utils/logger.d.ts.map +1 -0
  215. package/dist/utils/rate-limiter.d.ts +72 -0
  216. package/dist/utils/rate-limiter.d.ts.map +1 -0
  217. package/dist/utils/request-queue.d.ts +37 -0
  218. package/dist/utils/request-queue.d.ts.map +1 -0
  219. package/dist/utils/schema-validator.d.ts +12 -0
  220. package/dist/utils/schema-validator.d.ts.map +1 -0
  221. package/dist/utils/url-utils.d.ts +5 -0
  222. package/dist/utils/url-utils.d.ts.map +1 -0
  223. package/dist/utils/with-timeout.d.ts +5 -0
  224. package/dist/utils/with-timeout.d.ts.map +1 -0
  225. package/dist/version.d.ts +5 -0
  226. package/dist/version.d.ts.map +1 -0
  227. package/dist/watchdog/event-loop-monitor.d.ts +86 -0
  228. package/dist/watchdog/event-loop-monitor.d.ts.map +1 -0
  229. package/dist/watchdog/simulator-monitor.d.ts +16 -0
  230. package/dist/watchdog/simulator-monitor.d.ts.map +1 -0
  231. package/dist/webkit/client.d.ts +106 -0
  232. package/dist/webkit/client.d.ts.map +1 -0
  233. package/dist/webkit/index.d.ts +3 -0
  234. package/dist/webkit/index.d.ts.map +1 -0
  235. package/package.json +84 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"838.index.js","mappings":"8HAUO,MAAMA,EACHC,GAAgC,KAChCC,eAAyF,KAEjG,SAAAC,CAAUC,GACRC,KAAKH,eAAiBE,CACxB,CAEA,IAAAE,CAAKC,GAEHC,QAAQC,OAAOC,MAAMC,KAAKC,UAAUL,GAAY,KAClD,CAEA,KAAAM,GACER,KAAKJ,GAAK,kBAAyB,CACjCa,MAAON,QAAQO,MAGfC,UAAU,IAGZX,KAAKJ,GAAGgB,GAAG,OAASC,IAClB,IAAKA,EAAKC,OAAQ,OAElB,IAAIC,EACJ,IACEA,EAAST,KAAKU,MAAMH,EACtB,CAAE,MAAOI,GACP,MAAMC,EAA6B,CACjCC,QAAS,MACTC,GAAI,EACJH,MAAO,CACLI,KAAM,IAAcC,YACpBC,QAASN,aAAiBO,MAAQP,EAAMM,QAAU,gBAItD,YADAvB,KAAKC,KAAKiB,EAEZ,CAEKlB,KAAKH,eAKVG,KAAKH,eAAekB,GACjBU,KAAMvB,IACDA,GACFF,KAAKC,KAAKC,KAGbwB,MAAOT,IACN,MACMC,EAA6B,CACjCC,QAAS,MACTC,GAHUL,EAAOK,IAA0B,EAI3CH,MAAO,CACLI,KAAM,IAAcM,eACpBJ,QAASN,aAAiBO,MAAQP,EAAMM,QAAU,mBAGtDvB,KAAKC,KAAKiB,KApBZU,QAAQX,MAAM,sEAwBlBjB,KAAKJ,GAAGgB,GAAG,QAAS,KAClBgB,QAAQX,MAAM,mDACdd,QAAQ0B,KAAK,IAEjB,CAEA,WAAMC,GACA9B,KAAKJ,KACPI,KAAKJ,GAAGkC,QACR9B,KAAKJ,GAAK,KAEd,E","sources":["webpack://opensafari-mcp/./src/transports/stdio.ts"],"sourcesContent":["/**\n * Stdio transport for MCP server.\n * Reads JSON-RPC messages from stdin (one per line), writes responses to stdout.\n * When stdin closes (EOF), the process exits — this is the expected stdio lifecycle.\n */\n\nimport * as readline from 'readline';\nimport { MCPResponse, MCPErrorCodes } from '../types/mcp';\nimport { MCPTransport } from './index';\n\nexport class StdioTransport implements MCPTransport {\n private rl: readline.Interface | null = null;\n private messageHandler: ((msg: Record<string, unknown>) => Promise<MCPResponse | null>) | null = null;\n\n onMessage(handler: (msg: Record<string, unknown>) => Promise<MCPResponse | null>): void {\n this.messageHandler = handler;\n }\n\n send(response: MCPResponse): void {\n // stdout is the MCP JSON-RPC channel in stdio mode\n process.stdout.write(JSON.stringify(response) + '\\n');\n }\n\n start(): void {\n this.rl = readline.createInterface({\n input: process.stdin,\n // Do NOT set output to process.stdout — stdout is the MCP JSON-RPC channel.\n // Setting it risks protocol corruption if readline writes internally (prompts, echoes).\n terminal: false,\n });\n\n this.rl.on('line', (line) => {\n if (!line.trim()) return;\n\n let parsed: Record<string, unknown>;\n try {\n parsed = JSON.parse(line) as Record<string, unknown>;\n } catch (error) {\n const errorResponse: MCPResponse = {\n jsonrpc: '2.0',\n id: 0,\n error: {\n code: MCPErrorCodes.PARSE_ERROR,\n message: error instanceof Error ? error.message : 'Parse error',\n },\n };\n this.send(errorResponse);\n return;\n }\n\n if (!this.messageHandler) {\n console.error('[StdioTransport] No message handler registered, dropping message');\n return;\n }\n\n this.messageHandler(parsed)\n .then((response) => {\n if (response) {\n this.send(response);\n }\n })\n .catch((error) => {\n const id = (parsed.id as string | number) ?? 0;\n const errorResponse: MCPResponse = {\n jsonrpc: '2.0',\n id,\n error: {\n code: MCPErrorCodes.INTERNAL_ERROR,\n message: error instanceof Error ? error.message : 'Internal error',\n },\n };\n this.send(errorResponse);\n });\n });\n\n this.rl.on('close', () => {\n console.error('[StdioTransport] stdin closed, shutting down...');\n process.exit(0);\n });\n }\n\n async close(): Promise<void> {\n if (this.rl) {\n this.rl.close();\n this.rl = null;\n }\n }\n}\n"],"names":["StdioTransport","rl","messageHandler","onMessage","handler","this","send","response","process","stdout","write","JSON","stringify","start","input","stdin","terminal","on","line","trim","parsed","parse","error","errorResponse","jsonrpc","id","code","PARSE_ERROR","message","Error","then","catch","INTERNAL_ERROR","console","exit","close"],"sourceRoot":""}
@@ -0,0 +1,3 @@
1
+ export { AuthManager } from './manager';
2
+ export type { AuthProfile, ExpiryInfo } from './manager';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/auth/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AACxC,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC"}
@@ -0,0 +1,33 @@
1
+ import { BrowserBackend, Cookie } from '../types/browser-backend';
2
+ export interface AuthProfile {
3
+ site: string;
4
+ savedAt: string;
5
+ currentUrl: string;
6
+ cookies: Cookie[];
7
+ localStorage: Record<string, string>;
8
+ sessionStorage: Record<string, string>;
9
+ }
10
+ export interface ExpiryInfo {
11
+ totalCookies: number;
12
+ expiredCount: number;
13
+ expiringCount: number;
14
+ earliestExpiry: number;
15
+ isExpired: boolean;
16
+ isExpiring: boolean;
17
+ }
18
+ export declare class AuthManager {
19
+ private authDir;
20
+ constructor(authDir?: string);
21
+ save(site: string, client: BrowserBackend, filteredCookies?: Cookie[]): Promise<string>;
22
+ restore(site: string, client: BrowserBackend): Promise<void>;
23
+ list(): Promise<Array<{
24
+ site: string;
25
+ savedAt: string;
26
+ cookieCount: number;
27
+ }>>;
28
+ delete(site: string): Promise<void>;
29
+ checkExpiry(site: string): Promise<ExpiryInfo>;
30
+ loadProfile(site: string): Promise<AuthProfile>;
31
+ private sanitizeSite;
32
+ }
33
+ //# sourceMappingURL=manager.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manager.d.ts","sourceRoot":"","sources":["../../src/auth/manager.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,0BAA0B,CAAC;AAElE,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACrC,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACxC;AAED,MAAM,WAAW,UAAU;IACzB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,OAAO,CAAC;IACnB,UAAU,EAAE,OAAO,CAAC;CACrB;AAED,qBAAa,WAAW;IACtB,OAAO,CAAC,OAAO,CAAS;gBAEZ,OAAO,CAAC,EAAE,MAAM;IAItB,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,cAAc,EAAE,eAAe,CAAC,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC;IA2CvF,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC;IAyB5D,IAAI,IAAI,OAAO,CAAC,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAmB9E,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAKnC,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IAoB9C,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC;IAKrD,OAAO,CAAC,YAAY;CAGrB"}
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ "use strict";exports.id=148,exports.ids=[148],exports.modules={148(e,t,n){async function s(e){return e.evaluate("\n (function() {\n var inputs = document.querySelectorAll('input, select, textarea');\n var issues = [];\n inputs.forEach(function(el) {\n var style = window.getComputedStyle(el);\n var size = parseFloat(style.fontSize);\n if (size < 16) {\n var rect = el.getBoundingClientRect();\n issues.push({\n selector: el.id ? '#' + el.id : (el.name ? el.tagName.toLowerCase() + '[name=\"' + el.name + '\"]' : el.tagName.toLowerCase() + (el.className ? '.' + el.className.split(' ')[0] : '')),\n problem: 'font-size is ' + size + 'px (< 16px minimum)',\n fix: 'Set font-size to at least 16px to prevent iOS Safari auto-zoom on focus',\n });\n }\n });\n return { detector: 'auto_zoom', severity: issues.length > 0 ? 'high' : 'pass', issues: issues, passed: issues.length === 0, totalScanned: inputs.length, issueCount: issues.length };\n })()\n ")}async function i(e){return e.evaluate("\n (function() {\n var selectors = 'a, button, input, select, textarea, [onclick], [role=\"button\"], [role=\"link\"], [tabindex]:not([tabindex=\"-1\"])';\n var elements = document.querySelectorAll(selectors);\n var threshold = 44;\n var issues = [];\n elements.forEach(function(el) {\n var rect = el.getBoundingClientRect();\n if (rect.width === 0 || rect.height === 0) return;\n if (rect.width < threshold || rect.height < threshold) {\n issues.push({\n selector: el.id ? '#' + el.id : el.tagName.toLowerCase() + (el.className ? '.' + el.className.split(' ')[0] : ''),\n problem: 'Touch target is ' + Math.round(rect.width) + 'x' + Math.round(rect.height) + 'px (minimum: ' + threshold + 'x' + threshold + 'px)',\n fix: 'Increase element size to at least 44x44px or add padding',\n size: { width: Math.round(rect.width), height: Math.round(rect.height) },\n });\n }\n });\n return { detector: 'touch_targets', severity: issues.length > 0 ? 'high' : 'pass', issues: issues, passed: issues.length === 0, totalScanned: elements.length, issueCount: issues.length };\n })()\n ")}async function o(e){return e.evaluate("\n (function() {\n var issues = [];\n try {\n Array.from(document.styleSheets).forEach(function(sheet) {\n try {\n Array.from(sheet.cssRules || []).forEach(function(rule) {\n if (rule.selectorText && rule.selectorText.indexOf(':hover') !== -1) {\n var style = rule.style;\n if (style.display || style.visibility || style.opacity) {\n var baseSelector = rule.selectorText.replace(/:hover/g, '').trim();\n var el = document.querySelector(baseSelector);\n if (el) {\n issues.push({\n selector: baseSelector,\n problem: ':hover changes visibility — inaccessible on touch devices',\n fix: 'Add click/touch handler or use :focus-within as alternative',\n cssRule: rule.selectorText,\n });\n }\n }\n }\n });\n } catch(e) {}\n });\n } catch(e) {}\n return { detector: 'hover_only', severity: issues.length > 0 ? 'medium' : 'pass', issues: issues, passed: issues.length === 0, totalScanned: 0, issueCount: issues.length };\n })()\n ")}async function r(e){return e.evaluate("\n (function() {\n var issues = [];\n var inputs = document.querySelectorAll('input');\n inputs.forEach(function(el) {\n var type = el.getAttribute('type') || 'text';\n var inputMode = el.getAttribute('inputmode');\n var name = (el.name || el.id || '').toLowerCase();\n if (type === 'text' && !inputMode) {\n if (name.match(/phone|tel|zip|postal|code|pin|otp|cvv|cvc/)) {\n issues.push({ selector: el.id ? '#' + el.id : 'input[name=\"' + el.name + '\"]', problem: 'Likely numeric field using type=\"text\" without inputmode', fix: 'Add inputmode=\"numeric\" or inputmode=\"tel\"' });\n }\n if (name.match(/email/) && type !== 'email') {\n issues.push({ selector: el.id ? '#' + el.id : 'input[name=\"' + el.name + '\"]', problem: 'Email field using type=\"text\"', fix: 'Use type=\"email\" for email keyboard' });\n }\n }\n });\n return { detector: 'input_type', severity: issues.length > 0 ? 'medium' : 'pass', issues: issues, passed: issues.length === 0, totalScanned: inputs.length, issueCount: issues.length };\n })()\n ")}async function a(e){return e.evaluate("\n (function() {\n var issues = [];\n var viewport = document.querySelector('meta[name=\"viewport\"]');\n var hasViewportFitCover = viewport && viewport.content && viewport.content.indexOf('viewport-fit=cover') !== -1;\n if (!hasViewportFitCover) {\n return { detector: 'safe_area', severity: 'pass', issues: [], passed: true, totalScanned: 0, issueCount: 0, metadata: { note: 'viewport-fit=cover not set' } };\n }\n var all = document.querySelectorAll('*');\n all.forEach(function(el) {\n var style = window.getComputedStyle(el);\n if (style.position === 'fixed' || style.position === 'sticky') {\n var top = parseFloat(style.top);\n var bottom = parseFloat(style.bottom);\n if (top === 0 || bottom === 0) {\n issues.push({\n selector: el.id ? '#' + el.id : el.tagName.toLowerCase() + (el.className ? '.' + el.className.split(' ')[0] : ''),\n problem: 'Fixed element at ' + (top === 0 ? 'top' : 'bottom') + ' edge without safe-area-inset padding',\n fix: 'Add padding: env(safe-area-inset-' + (top === 0 ? 'top' : 'bottom') + ')',\n });\n }\n }\n });\n return { detector: 'safe_area', severity: issues.length > 0 ? 'high' : 'pass', issues: issues, passed: issues.length === 0, totalScanned: 1, issueCount: issues.length };\n })()\n ")}async function l(e){const t=await e.evaluate("\n (function() {\n return Array.from(document.querySelectorAll('*')).filter(function(el) {\n var s = window.getComputedStyle(el);\n return s.position === 'fixed' && parseFloat(s.bottom) < 50;\n }).map(function(el) {\n return {\n selector: el.id ? '#' + el.id : el.tagName.toLowerCase() + (el.className ? '.' + el.className.split(' ')[0] : ''),\n bottom: parseFloat(window.getComputedStyle(el).bottom),\n rect: { y: el.getBoundingClientRect().y, height: el.getBoundingClientRect().height }\n };\n });\n })()\n ");if(!t||0===t.length)return{detector:"keyboard_overlap",severity:"pass",issues:[],passed:!0,totalScanned:0,issueCount:0};const n=await e.evaluate("\n Array.from(document.querySelectorAll('input:not([type=\"hidden\"]), textarea, select')).slice(0, 5).map(function(el) {\n return el.id ? '#' + el.id : (el.name ? el.tagName.toLowerCase() + '[name=\"' + el.name + '\"]' : el.tagName.toLowerCase());\n })\n "),s=[];for(const i of n||[])try{await e.click(i),await new Promise(e=>setTimeout(e,500));const n=await e.evaluate("window.visualViewport ? window.visualViewport.height : window.innerHeight");for(const e of t)e.rect.y+e.rect.height>n&&s.push({selector:e.selector,problem:`Fixed bottom element hidden behind keyboard (element bottom: ${Math.round(e.rect.y+e.rect.height)}px, viewport with keyboard: ${Math.round(n)}px)`,fix:"Use visualViewport API to adjust position when keyboard appears",triggeredBy:i});await e.dismissKeyboard(),await new Promise(e=>setTimeout(e,300))}catch{}return{detector:"keyboard_overlap",severity:s.length>0?"critical":"pass",issues:s,passed:0===s.length,totalScanned:t.length*(n?.length??0),issueCount:s.length}}async function u(e){return e.evaluate("\n (function() {\n var docWidth = document.documentElement.scrollWidth;\n var vpWidth = window.innerWidth;\n if (docWidth <= vpWidth) {\n return { detector: 'horizontal_overflow', severity: 'pass', issues: [], passed: true, totalScanned: 1, issueCount: 0 };\n }\n var issues = [];\n function findCulprits(parent) {\n Array.from(parent.children).forEach(function(el) {\n var rect = el.getBoundingClientRect();\n if (rect.right > vpWidth + 1) {\n issues.push({\n selector: el.id ? '#' + el.id : el.tagName.toLowerCase() + (el.className ? '.' + el.className.split(' ')[0] : ''),\n problem: 'Element extends to ' + Math.round(rect.right) + 'px (viewport: ' + vpWidth + 'px)',\n fix: 'Add overflow-x: hidden or max-width: 100%',\n overflow: Math.round(rect.right - vpWidth) + 'px',\n });\n }\n });\n }\n findCulprits(document.body);\n return { detector: 'horizontal_overflow', severity: 'high', issues: issues.slice(0, 20), passed: false, totalScanned: 1, issueCount: issues.length };\n })()\n ")}async function c(e){return e.evaluate("\n (function() {\n var temp = document.createElement('div');\n temp.style.cssText = 'position:fixed;top:0;height:100vh;width:1px;pointer-events:none;visibility:hidden';\n document.body.appendChild(temp);\n var vh100 = temp.offsetHeight;\n document.body.removeChild(temp);\n var innerH = window.innerHeight;\n var diff = vh100 - innerH;\n if (Math.abs(diff) < 10) {\n return { detector: '100vh', severity: 'pass', issues: [], passed: true, totalScanned: 1, issueCount: 0, metadata: { vh100: vh100, innerHeight: innerH, difference: diff } };\n }\n var issues = [];\n issues.push({\n selector: 'viewport',\n problem: '100vh = ' + vh100 + 'px but visible viewport = ' + innerH + 'px (diff: ' + diff + 'px)',\n fix: 'Use 100dvh or calc(var(--vh, 1vh) * 100) with JS viewport listener',\n });\n return { detector: '100vh', severity: issues.length > 0 ? 'medium' : 'pass', issues: issues, passed: issues.length === 0, totalScanned: 1, issueCount: issues.length, metadata: { vh100: vh100, innerHeight: innerH, difference: diff } };\n })()\n ")}async function d(e){return e.evaluate("\n (function() {\n var fixedEls = Array.from(document.querySelectorAll('*')).filter(function(el) {\n var s = window.getComputedStyle(el);\n return s.position === 'fixed' || s.position === 'sticky';\n }).map(function(el) {\n return {\n selector: el.id ? '#' + el.id : el.tagName.toLowerCase() + (el.className ? '.' + el.className.split(' ')[0] : ''),\n rect: el.getBoundingClientRect(),\n zIndex: parseInt(window.getComputedStyle(el).zIndex) || 0,\n };\n });\n var issues = [];\n for (var i = 0; i < fixedEls.length; i++) {\n for (var j = i + 1; j < fixedEls.length; j++) {\n var a = fixedEls[i], b = fixedEls[j];\n var overlap = !(a.rect.right < b.rect.left || a.rect.left > b.rect.right || a.rect.bottom < b.rect.top || a.rect.top > b.rect.bottom);\n if (overlap && a.zIndex === b.zIndex) {\n issues.push({\n selector: a.selector + ' <-> ' + b.selector,\n problem: 'Overlapping fixed elements with same z-index (' + a.zIndex + ')',\n fix: 'Set distinct z-index values',\n });\n }\n }\n }\n return { detector: 'fixed_stacking', severity: issues.length > 0 ? 'medium' : 'pass', issues: issues, passed: issues.length === 0, totalScanned: fixedEls.length, issueCount: issues.length };\n })()\n ")}async function h(e){return e.evaluate("\n (function() {\n var issues = [];\n var bodyOverflow = document.body.style.overflow;\n var htmlOverflow = document.documentElement.style.overflow;\n var hasVisibleModal = document.querySelector('[role=\"dialog\"]:not([aria-hidden=\"true\"]), .modal:not(.hidden), [data-modal]:not([hidden])');\n if ((bodyOverflow === 'hidden' || htmlOverflow === 'hidden') && !hasVisibleModal) {\n issues.push({ selector: bodyOverflow === 'hidden' ? 'document.body' : 'document.documentElement', problem: 'overflow: hidden set but no visible modal found', fix: 'Ensure modal close handlers restore overflow' });\n }\n if (bodyOverflow === 'unset') {\n issues.push({ selector: 'document.body', problem: 'overflow set to \"unset\" — not a proper reset', fix: 'Use document.body.style.overflow = \"\" (empty string)' });\n }\n return { detector: 'scroll_lock', severity: issues.length > 0 ? 'high' : 'pass', issues: issues, passed: issues.length === 0, totalScanned: 2, issueCount: issues.length };\n })()\n ")}async function m(e,t,n){const s=await e.evaluate("\n (document.querySelector('meta[name=\"color-scheme\"]') || {}).content || 'not set'\n "),i=[];let o,r;if("not set"===s&&i.push({selector:"head",problem:'No <meta name="color-scheme"> tag — browser may apply forced dark mode',fix:'Add <meta name="color-scheme" content="light only"> or implement proper dark mode'}),t&&n)try{await t.setAppearance(n,"light"),await new Promise(e=>setTimeout(e,500)),o=(await e.screenshot()).toString("base64"),await t.setAppearance(n,"dark"),await new Promise(e=>setTimeout(e,500)),r=(await e.screenshot()).toString("base64"),await t.setAppearance(n,"light")}catch{}return{detector:"dark_mode",severity:i.length>0?"medium":"pass",issues:i,passed:0===i.length,totalScanned:1,issueCount:i.length,metadata:{colorScheme:s,...o?{lightScreenshot:o,darkScreenshot:r,note:"Compare screenshots visually"}:{}}}}async function p(e,t,n){const s=await e.evaluate("({\n scrollWidth: document.documentElement.scrollWidth,\n innerWidth: window.innerWidth,\n overflow: document.documentElement.scrollWidth > window.innerWidth\n })"),i=[];if(t&&n)try{await t.rotate(n),await new Promise(e=>setTimeout(e,1e3));const s=await e.evaluate("({\n scrollWidth: document.documentElement.scrollWidth,\n innerWidth: window.innerWidth,\n overflow: document.documentElement.scrollWidth > window.innerWidth\n })");s.overflow&&i.push({selector:"document.documentElement",problem:`Horizontal overflow in landscape (scrollWidth: ${s.scrollWidth}px, viewport: ${s.innerWidth}px)`,fix:"Ensure responsive layout handles landscape orientation"}),await t.rotate(n)}catch{}return{detector:"orientation",severity:i.length>0?"medium":"pass",issues:i,passed:0===i.length,totalScanned:1,issueCount:i.length,metadata:{portrait:s}}}async function f(e){return e.evaluate("\n (function() {\n var checks = [\n { name: 'viewport', selector: 'meta[name=\"viewport\"]', required: true, fix: 'Add <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">' },\n { name: 'theme-color', selector: 'meta[name=\"theme-color\"]', required: false, fix: 'Add <meta name=\"theme-color\" content=\"#yourColor\">' },\n { name: 'color-scheme', selector: 'meta[name=\"color-scheme\"]', required: false, fix: 'Add <meta name=\"color-scheme\" content=\"light only\">' },\n { name: 'apple-touch-icon', selector: 'link[rel=\"apple-touch-icon\"]', required: false, fix: 'Add <link rel=\"apple-touch-icon\" href=\"/icon-180.png\">' },\n { name: 'manifest', selector: 'link[rel=\"manifest\"]', required: false, fix: 'Add <link rel=\"manifest\" href=\"/manifest.json\">' },\n ];\n var issues = [];\n checks.forEach(function(check) {\n if (!document.querySelector(check.selector)) {\n issues.push({ selector: 'head', problem: 'Missing ' + check.name + (check.required ? ' (required)' : ' (recommended)'), fix: check.fix });\n }\n });\n return { detector: 'pwa_meta', severity: issues.some(function(i) { return i.problem.indexOf('required') !== -1; }) ? 'high' : issues.length > 0 ? 'low' : 'pass', issues: issues, passed: issues.length === 0, totalScanned: checks.length, issueCount: issues.length };\n })()\n ")}n.d(t,{QAAudit:()=>g});const v={critical:10,high:5,medium:2,low:1};class g{client;config;simulator;deviceId;deviceInfo;constructor(e,t={},n,s,i){this.client=e,this.config=t,this.simulator=n,this.deviceId=s,this.deviceInfo=i}async runFullAudit(e){e&&await this.client.navigate({url:e,waitUntil:"load"});const t=await this.client.evaluate("window.location.href"),n=Date.now(),v=await Promise.allSettled([s(this.client),i(this.client),o(this.client),r(this.client),a(this.client),u(this.client),c(this.client),d(this.client),h(this.client),f(this.client)]),g=[];try{g.push({status:"fulfilled",value:await l(this.client)})}catch(e){g.push({status:"rejected",reason:e})}try{g.push({status:"fulfilled",value:await m(this.client,this.simulator,this.deviceId)})}catch(e){g.push({status:"rejected",reason:e})}try{g.push({status:"fulfilled",value:await p(this.client,this.simulator,this.deviceId)})}catch(e){g.push({status:"rejected",reason:e})}const w=[...v,...g].map((e,t)=>"fulfilled"===e.status?function(e,t){const n=t.ignore?.filter(t=>t.detector===e.detector)??[];return n.length>0&&(e.issues=e.issues.filter(e=>!n.some(t=>e.selector.includes(t.selector))),e.issueCount=e.issues.length,e.passed=0===e.issueCount,e.passed&&(e.severity="pass")),e}(e.value,this.config):{detector:"unknown",severity:"error",issues:[],passed:!1,totalScanned:0,issueCount:0,error:e.reason instanceof Error?e.reason.message:String(e.reason)}),y=this.calculateScore(w),x=this.summarize(w);return{url:t,device:this.deviceInfo?.name??"unknown",viewport:{w:this.deviceInfo?.w??0,h:this.deviceInfo?.h??0},timestamp:(new Date).toISOString(),duration:Date.now()-n,score:y,summary:x,detectors:w}}calculateScore(e){let t=0;for(const n of e)n.severity&&"pass"!==n.severity&&"error"!==n.severity&&(t+=(v[n.severity]??0)*n.issueCount);return Math.max(0,100-t)}summarize(e){return{totalIssues:e.reduce((e,t)=>e+t.issueCount,0),critical:e.filter(e=>"critical"===e.severity).reduce((e,t)=>e+t.issueCount,0),high:e.filter(e=>"high"===e.severity).reduce((e,t)=>e+t.issueCount,0),medium:e.filter(e=>"medium"===e.severity).reduce((e,t)=>e+t.issueCount,0),low:e.filter(e=>"low"===e.severity).reduce((e,t)=>e+t.issueCount,0),passed:e.filter(e=>e.passed).length,failed:e.filter(e=>!e.passed).length,errors:e.filter(e=>"error"===e.severity).length}}}}};
3
+ //# sourceMappingURL=148.index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli/148.index.js","mappings":"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","sources":["webpack://opensafari-mcp/./src/qa/detectors/auto-zoom.ts","webpack://opensafari-mcp/./src/qa/detectors/touch-targets.ts","webpack://opensafari-mcp/./src/qa/detectors/hover-only.ts","webpack://opensafari-mcp/./src/qa/detectors/input-type.ts","webpack://opensafari-mcp/./src/qa/detectors/safe-area.ts","webpack://opensafari-mcp/./src/qa/detectors/keyboard-overlap.ts","webpack://opensafari-mcp/./src/qa/detectors/horizontal-overflow.ts","webpack://opensafari-mcp/./src/qa/detectors/vh100.ts","webpack://opensafari-mcp/./src/qa/detectors/fixed-stacking.ts","webpack://opensafari-mcp/./src/qa/detectors/scroll-lock.ts","webpack://opensafari-mcp/./src/qa/detectors/dark-mode.ts","webpack://opensafari-mcp/./src/qa/detectors/orientation.ts","webpack://opensafari-mcp/./src/qa/detectors/pwa-meta.ts","webpack://opensafari-mcp/./src/qa/audit.ts","webpack://opensafari-mcp/./src/qa/types.ts"],"sourcesContent":["import { BrowserBackend } from '../../types/browser-backend';\nimport { DetectorResult } from '../types';\n\nexport async function detectAutoZoom(client: BrowserBackend): Promise<DetectorResult> {\n return client.evaluate<DetectorResult>(`\n (function() {\n var inputs = document.querySelectorAll('input, select, textarea');\n var issues = [];\n inputs.forEach(function(el) {\n var style = window.getComputedStyle(el);\n var size = parseFloat(style.fontSize);\n if (size < 16) {\n var rect = el.getBoundingClientRect();\n issues.push({\n selector: el.id ? '#' + el.id : (el.name ? el.tagName.toLowerCase() + '[name=\"' + el.name + '\"]' : el.tagName.toLowerCase() + (el.className ? '.' + el.className.split(' ')[0] : '')),\n problem: 'font-size is ' + size + 'px (< 16px minimum)',\n fix: 'Set font-size to at least 16px to prevent iOS Safari auto-zoom on focus',\n });\n }\n });\n return { detector: 'auto_zoom', severity: issues.length > 0 ? 'high' : 'pass', issues: issues, passed: issues.length === 0, totalScanned: inputs.length, issueCount: issues.length };\n })()\n `);\n}\n","import { BrowserBackend } from '../../types/browser-backend';\nimport { DetectorResult } from '../types';\n\nexport async function detectTouchTargets(client: BrowserBackend): Promise<DetectorResult> {\n return client.evaluate<DetectorResult>(`\n (function() {\n var selectors = 'a, button, input, select, textarea, [onclick], [role=\"button\"], [role=\"link\"], [tabindex]:not([tabindex=\"-1\"])';\n var elements = document.querySelectorAll(selectors);\n var threshold = 44;\n var issues = [];\n elements.forEach(function(el) {\n var rect = el.getBoundingClientRect();\n if (rect.width === 0 || rect.height === 0) return;\n if (rect.width < threshold || rect.height < threshold) {\n issues.push({\n selector: el.id ? '#' + el.id : el.tagName.toLowerCase() + (el.className ? '.' + el.className.split(' ')[0] : ''),\n problem: 'Touch target is ' + Math.round(rect.width) + 'x' + Math.round(rect.height) + 'px (minimum: ' + threshold + 'x' + threshold + 'px)',\n fix: 'Increase element size to at least 44x44px or add padding',\n size: { width: Math.round(rect.width), height: Math.round(rect.height) },\n });\n }\n });\n return { detector: 'touch_targets', severity: issues.length > 0 ? 'high' : 'pass', issues: issues, passed: issues.length === 0, totalScanned: elements.length, issueCount: issues.length };\n })()\n `);\n}\n","import { BrowserBackend } from '../../types/browser-backend';\nimport { DetectorResult } from '../types';\n\nexport async function detectHoverOnly(client: BrowserBackend): Promise<DetectorResult> {\n return client.evaluate<DetectorResult>(`\n (function() {\n var issues = [];\n try {\n Array.from(document.styleSheets).forEach(function(sheet) {\n try {\n Array.from(sheet.cssRules || []).forEach(function(rule) {\n if (rule.selectorText && rule.selectorText.indexOf(':hover') !== -1) {\n var style = rule.style;\n if (style.display || style.visibility || style.opacity) {\n var baseSelector = rule.selectorText.replace(/:hover/g, '').trim();\n var el = document.querySelector(baseSelector);\n if (el) {\n issues.push({\n selector: baseSelector,\n problem: ':hover changes visibility — inaccessible on touch devices',\n fix: 'Add click/touch handler or use :focus-within as alternative',\n cssRule: rule.selectorText,\n });\n }\n }\n }\n });\n } catch(e) {}\n });\n } catch(e) {}\n return { detector: 'hover_only', severity: issues.length > 0 ? 'medium' : 'pass', issues: issues, passed: issues.length === 0, totalScanned: 0, issueCount: issues.length };\n })()\n `);\n}\n","import { BrowserBackend } from '../../types/browser-backend';\nimport { DetectorResult } from '../types';\n\nexport async function detectInputType(client: BrowserBackend): Promise<DetectorResult> {\n return client.evaluate<DetectorResult>(`\n (function() {\n var issues = [];\n var inputs = document.querySelectorAll('input');\n inputs.forEach(function(el) {\n var type = el.getAttribute('type') || 'text';\n var inputMode = el.getAttribute('inputmode');\n var name = (el.name || el.id || '').toLowerCase();\n if (type === 'text' && !inputMode) {\n if (name.match(/phone|tel|zip|postal|code|pin|otp|cvv|cvc/)) {\n issues.push({ selector: el.id ? '#' + el.id : 'input[name=\"' + el.name + '\"]', problem: 'Likely numeric field using type=\"text\" without inputmode', fix: 'Add inputmode=\"numeric\" or inputmode=\"tel\"' });\n }\n if (name.match(/email/) && type !== 'email') {\n issues.push({ selector: el.id ? '#' + el.id : 'input[name=\"' + el.name + '\"]', problem: 'Email field using type=\"text\"', fix: 'Use type=\"email\" for email keyboard' });\n }\n }\n });\n return { detector: 'input_type', severity: issues.length > 0 ? 'medium' : 'pass', issues: issues, passed: issues.length === 0, totalScanned: inputs.length, issueCount: issues.length };\n })()\n `);\n}\n","import { BrowserBackend } from '../../types/browser-backend';\nimport { DetectorResult } from '../types';\n\nexport async function detectSafeArea(client: BrowserBackend): Promise<DetectorResult> {\n return client.evaluate<DetectorResult>(`\n (function() {\n var issues = [];\n var viewport = document.querySelector('meta[name=\"viewport\"]');\n var hasViewportFitCover = viewport && viewport.content && viewport.content.indexOf('viewport-fit=cover') !== -1;\n if (!hasViewportFitCover) {\n return { detector: 'safe_area', severity: 'pass', issues: [], passed: true, totalScanned: 0, issueCount: 0, metadata: { note: 'viewport-fit=cover not set' } };\n }\n var all = document.querySelectorAll('*');\n all.forEach(function(el) {\n var style = window.getComputedStyle(el);\n if (style.position === 'fixed' || style.position === 'sticky') {\n var top = parseFloat(style.top);\n var bottom = parseFloat(style.bottom);\n if (top === 0 || bottom === 0) {\n issues.push({\n selector: el.id ? '#' + el.id : el.tagName.toLowerCase() + (el.className ? '.' + el.className.split(' ')[0] : ''),\n problem: 'Fixed element at ' + (top === 0 ? 'top' : 'bottom') + ' edge without safe-area-inset padding',\n fix: 'Add padding: env(safe-area-inset-' + (top === 0 ? 'top' : 'bottom') + ')',\n });\n }\n }\n });\n return { detector: 'safe_area', severity: issues.length > 0 ? 'high' : 'pass', issues: issues, passed: issues.length === 0, totalScanned: 1, issueCount: issues.length };\n })()\n `);\n}\n","import { BrowserBackend } from '../../types/browser-backend';\nimport { DetectorResult } from '../types';\n\nexport async function detectKeyboardOverlap(client: BrowserBackend): Promise<DetectorResult> {\n // Get fixed-bottom elements\n const fixedBottom = await client.evaluate<Array<{ selector: string; bottom: number; rect: { y: number; height: number } }>>(`\n (function() {\n return Array.from(document.querySelectorAll('*')).filter(function(el) {\n var s = window.getComputedStyle(el);\n return s.position === 'fixed' && parseFloat(s.bottom) < 50;\n }).map(function(el) {\n return {\n selector: el.id ? '#' + el.id : el.tagName.toLowerCase() + (el.className ? '.' + el.className.split(' ')[0] : ''),\n bottom: parseFloat(window.getComputedStyle(el).bottom),\n rect: { y: el.getBoundingClientRect().y, height: el.getBoundingClientRect().height }\n };\n });\n })()\n `);\n\n if (!fixedBottom || fixedBottom.length === 0) {\n return { detector: 'keyboard_overlap', severity: 'pass', issues: [], passed: true, totalScanned: 0, issueCount: 0 };\n }\n\n // Get inputs\n const inputs = await client.evaluate<string[]>(`\n Array.from(document.querySelectorAll('input:not([type=\"hidden\"]), textarea, select')).slice(0, 5).map(function(el) {\n return el.id ? '#' + el.id : (el.name ? el.tagName.toLowerCase() + '[name=\"' + el.name + '\"]' : el.tagName.toLowerCase());\n })\n `);\n\n const issues: Array<{ selector: string; problem: string; fix: string; triggeredBy?: string }> = [];\n\n for (const inputSelector of (inputs || [])) {\n try {\n await client.click(inputSelector);\n await new Promise(r => setTimeout(r, 500));\n\n const viewportWithKeyboard = await client.evaluate<number>('window.visualViewport ? window.visualViewport.height : window.innerHeight');\n\n for (const fixed of fixedBottom) {\n if (fixed.rect.y + fixed.rect.height > viewportWithKeyboard) {\n issues.push({\n selector: fixed.selector,\n problem: `Fixed bottom element hidden behind keyboard (element bottom: ${Math.round(fixed.rect.y + fixed.rect.height)}px, viewport with keyboard: ${Math.round(viewportWithKeyboard)}px)`,\n fix: 'Use visualViewport API to adjust position when keyboard appears',\n triggeredBy: inputSelector,\n });\n }\n }\n\n await client.dismissKeyboard();\n await new Promise(r => setTimeout(r, 300));\n } catch {\n // Input may not be focusable\n }\n }\n\n return {\n detector: 'keyboard_overlap',\n severity: issues.length > 0 ? 'critical' : 'pass',\n issues,\n passed: issues.length === 0,\n totalScanned: fixedBottom.length * (inputs?.length ?? 0),\n issueCount: issues.length,\n };\n}\n","import { BrowserBackend } from '../../types/browser-backend';\nimport { DetectorResult } from '../types';\n\nexport async function detectHorizontalOverflow(client: BrowserBackend): Promise<DetectorResult> {\n return client.evaluate<DetectorResult>(`\n (function() {\n var docWidth = document.documentElement.scrollWidth;\n var vpWidth = window.innerWidth;\n if (docWidth <= vpWidth) {\n return { detector: 'horizontal_overflow', severity: 'pass', issues: [], passed: true, totalScanned: 1, issueCount: 0 };\n }\n var issues = [];\n function findCulprits(parent) {\n Array.from(parent.children).forEach(function(el) {\n var rect = el.getBoundingClientRect();\n if (rect.right > vpWidth + 1) {\n issues.push({\n selector: el.id ? '#' + el.id : el.tagName.toLowerCase() + (el.className ? '.' + el.className.split(' ')[0] : ''),\n problem: 'Element extends to ' + Math.round(rect.right) + 'px (viewport: ' + vpWidth + 'px)',\n fix: 'Add overflow-x: hidden or max-width: 100%',\n overflow: Math.round(rect.right - vpWidth) + 'px',\n });\n }\n });\n }\n findCulprits(document.body);\n return { detector: 'horizontal_overflow', severity: 'high', issues: issues.slice(0, 20), passed: false, totalScanned: 1, issueCount: issues.length };\n })()\n `);\n}\n","import { BrowserBackend } from '../../types/browser-backend';\nimport { DetectorResult } from '../types';\n\nexport async function detect100vh(client: BrowserBackend): Promise<DetectorResult> {\n return client.evaluate<DetectorResult>(`\n (function() {\n var temp = document.createElement('div');\n temp.style.cssText = 'position:fixed;top:0;height:100vh;width:1px;pointer-events:none;visibility:hidden';\n document.body.appendChild(temp);\n var vh100 = temp.offsetHeight;\n document.body.removeChild(temp);\n var innerH = window.innerHeight;\n var diff = vh100 - innerH;\n if (Math.abs(diff) < 10) {\n return { detector: '100vh', severity: 'pass', issues: [], passed: true, totalScanned: 1, issueCount: 0, metadata: { vh100: vh100, innerHeight: innerH, difference: diff } };\n }\n var issues = [];\n issues.push({\n selector: 'viewport',\n problem: '100vh = ' + vh100 + 'px but visible viewport = ' + innerH + 'px (diff: ' + diff + 'px)',\n fix: 'Use 100dvh or calc(var(--vh, 1vh) * 100) with JS viewport listener',\n });\n return { detector: '100vh', severity: issues.length > 0 ? 'medium' : 'pass', issues: issues, passed: issues.length === 0, totalScanned: 1, issueCount: issues.length, metadata: { vh100: vh100, innerHeight: innerH, difference: diff } };\n })()\n `);\n}\n","import { BrowserBackend } from '../../types/browser-backend';\nimport { DetectorResult } from '../types';\n\nexport async function detectFixedStacking(client: BrowserBackend): Promise<DetectorResult> {\n return client.evaluate<DetectorResult>(`\n (function() {\n var fixedEls = Array.from(document.querySelectorAll('*')).filter(function(el) {\n var s = window.getComputedStyle(el);\n return s.position === 'fixed' || s.position === 'sticky';\n }).map(function(el) {\n return {\n selector: el.id ? '#' + el.id : el.tagName.toLowerCase() + (el.className ? '.' + el.className.split(' ')[0] : ''),\n rect: el.getBoundingClientRect(),\n zIndex: parseInt(window.getComputedStyle(el).zIndex) || 0,\n };\n });\n var issues = [];\n for (var i = 0; i < fixedEls.length; i++) {\n for (var j = i + 1; j < fixedEls.length; j++) {\n var a = fixedEls[i], b = fixedEls[j];\n var overlap = !(a.rect.right < b.rect.left || a.rect.left > b.rect.right || a.rect.bottom < b.rect.top || a.rect.top > b.rect.bottom);\n if (overlap && a.zIndex === b.zIndex) {\n issues.push({\n selector: a.selector + ' <-> ' + b.selector,\n problem: 'Overlapping fixed elements with same z-index (' + a.zIndex + ')',\n fix: 'Set distinct z-index values',\n });\n }\n }\n }\n return { detector: 'fixed_stacking', severity: issues.length > 0 ? 'medium' : 'pass', issues: issues, passed: issues.length === 0, totalScanned: fixedEls.length, issueCount: issues.length };\n })()\n `);\n}\n","import { BrowserBackend } from '../../types/browser-backend';\nimport { DetectorResult } from '../types';\n\nexport async function detectScrollLock(client: BrowserBackend): Promise<DetectorResult> {\n return client.evaluate<DetectorResult>(`\n (function() {\n var issues = [];\n var bodyOverflow = document.body.style.overflow;\n var htmlOverflow = document.documentElement.style.overflow;\n var hasVisibleModal = document.querySelector('[role=\"dialog\"]:not([aria-hidden=\"true\"]), .modal:not(.hidden), [data-modal]:not([hidden])');\n if ((bodyOverflow === 'hidden' || htmlOverflow === 'hidden') && !hasVisibleModal) {\n issues.push({ selector: bodyOverflow === 'hidden' ? 'document.body' : 'document.documentElement', problem: 'overflow: hidden set but no visible modal found', fix: 'Ensure modal close handlers restore overflow' });\n }\n if (bodyOverflow === 'unset') {\n issues.push({ selector: 'document.body', problem: 'overflow set to \"unset\" — not a proper reset', fix: 'Use document.body.style.overflow = \"\" (empty string)' });\n }\n return { detector: 'scroll_lock', severity: issues.length > 0 ? 'high' : 'pass', issues: issues, passed: issues.length === 0, totalScanned: 2, issueCount: issues.length };\n })()\n `);\n}\n","import { BrowserBackend } from '../../types/browser-backend';\nimport { DetectorResult } from '../types';\nimport { SimulatorManager } from '../../simulator/manager';\n\nexport async function detectDarkMode(client: BrowserBackend, simulator?: SimulatorManager, deviceId?: string): Promise<DetectorResult> {\n const colorScheme = await client.evaluate<string>(`\n (document.querySelector('meta[name=\"color-scheme\"]') || {}).content || 'not set'\n `);\n\n const issues: Array<{ selector: string; problem: string; fix: string }> = [];\n\n if (colorScheme === 'not set') {\n issues.push({\n selector: 'head',\n problem: 'No <meta name=\"color-scheme\"> tag — browser may apply forced dark mode',\n fix: 'Add <meta name=\"color-scheme\" content=\"light only\"> or implement proper dark mode',\n });\n }\n\n // Toggle dark mode if simulator available\n let lightScreenshot: string | undefined;\n let darkScreenshot: string | undefined;\n\n if (simulator && deviceId) {\n try {\n await simulator.setAppearance(deviceId, 'light');\n await new Promise(r => setTimeout(r, 500));\n const lightBuf = await client.screenshot();\n lightScreenshot = lightBuf.toString('base64');\n\n await simulator.setAppearance(deviceId, 'dark');\n await new Promise(r => setTimeout(r, 500));\n const darkBuf = await client.screenshot();\n darkScreenshot = darkBuf.toString('base64');\n\n await simulator.setAppearance(deviceId, 'light');\n } catch {\n // Simulator not available\n }\n }\n\n return {\n detector: 'dark_mode',\n severity: issues.length > 0 ? 'medium' : 'pass',\n issues,\n passed: issues.length === 0,\n totalScanned: 1,\n issueCount: issues.length,\n metadata: {\n colorScheme,\n ...(lightScreenshot ? { lightScreenshot, darkScreenshot, note: 'Compare screenshots visually' } : {}),\n },\n };\n}\n","import { BrowserBackend } from '../../types/browser-backend';\nimport { DetectorResult } from '../types';\nimport { SimulatorManager } from '../../simulator/manager';\n\nexport async function detectOrientation(client: BrowserBackend, simulator?: SimulatorManager, deviceId?: string): Promise<DetectorResult> {\n const portraitMeta = await client.evaluate<{ scrollWidth: number; innerWidth: number; overflow: boolean }>(`({\n scrollWidth: document.documentElement.scrollWidth,\n innerWidth: window.innerWidth,\n overflow: document.documentElement.scrollWidth > window.innerWidth\n })`);\n\n const issues: Array<{ selector: string; problem: string; fix: string }> = [];\n\n // Try rotation\n if (simulator && deviceId) {\n try {\n await simulator.rotate(deviceId);\n await new Promise(r => setTimeout(r, 1000));\n\n const landscapeMeta = await client.evaluate<{ scrollWidth: number; innerWidth: number; overflow: boolean }>(`({\n scrollWidth: document.documentElement.scrollWidth,\n innerWidth: window.innerWidth,\n overflow: document.documentElement.scrollWidth > window.innerWidth\n })`);\n\n if (landscapeMeta.overflow) {\n issues.push({\n selector: 'document.documentElement',\n problem: `Horizontal overflow in landscape (scrollWidth: ${landscapeMeta.scrollWidth}px, viewport: ${landscapeMeta.innerWidth}px)`,\n fix: 'Ensure responsive layout handles landscape orientation',\n });\n }\n\n await simulator.rotate(deviceId); // rotate back\n } catch {\n // Rotation not available\n }\n }\n\n return {\n detector: 'orientation',\n severity: issues.length > 0 ? 'medium' : 'pass',\n issues,\n passed: issues.length === 0,\n totalScanned: 1,\n issueCount: issues.length,\n metadata: { portrait: portraitMeta },\n };\n}\n","import { BrowserBackend } from '../../types/browser-backend';\nimport { DetectorResult } from '../types';\n\nexport async function detectPwaMeta(client: BrowserBackend): Promise<DetectorResult> {\n return client.evaluate<DetectorResult>(`\n (function() {\n var checks = [\n { name: 'viewport', selector: 'meta[name=\"viewport\"]', required: true, fix: 'Add <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">' },\n { name: 'theme-color', selector: 'meta[name=\"theme-color\"]', required: false, fix: 'Add <meta name=\"theme-color\" content=\"#yourColor\">' },\n { name: 'color-scheme', selector: 'meta[name=\"color-scheme\"]', required: false, fix: 'Add <meta name=\"color-scheme\" content=\"light only\">' },\n { name: 'apple-touch-icon', selector: 'link[rel=\"apple-touch-icon\"]', required: false, fix: 'Add <link rel=\"apple-touch-icon\" href=\"/icon-180.png\">' },\n { name: 'manifest', selector: 'link[rel=\"manifest\"]', required: false, fix: 'Add <link rel=\"manifest\" href=\"/manifest.json\">' },\n ];\n var issues = [];\n checks.forEach(function(check) {\n if (!document.querySelector(check.selector)) {\n issues.push({ selector: 'head', problem: 'Missing ' + check.name + (check.required ? ' (required)' : ' (recommended)'), fix: check.fix });\n }\n });\n return { detector: 'pwa_meta', severity: issues.some(function(i) { return i.problem.indexOf('required') !== -1; }) ? 'high' : issues.length > 0 ? 'low' : 'pass', issues: issues, passed: issues.length === 0, totalScanned: checks.length, issueCount: issues.length };\n })()\n `);\n}\n","import { BrowserBackend } from '../types/browser-backend';\nimport { DetectorResult, QAConfig, applyIgnoreRules } from './types';\nimport { detectAutoZoom } from './detectors/auto-zoom';\nimport { detectTouchTargets } from './detectors/touch-targets';\nimport { detectHoverOnly } from './detectors/hover-only';\nimport { detectInputType } from './detectors/input-type';\nimport { detectSafeArea } from './detectors/safe-area';\nimport { detectKeyboardOverlap } from './detectors/keyboard-overlap';\nimport { detectHorizontalOverflow } from './detectors/horizontal-overflow';\nimport { detect100vh } from './detectors/vh100';\nimport { detectFixedStacking } from './detectors/fixed-stacking';\nimport { detectScrollLock } from './detectors/scroll-lock';\nimport { detectDarkMode } from './detectors/dark-mode';\nimport { detectOrientation } from './detectors/orientation';\nimport { detectPwaMeta } from './detectors/pwa-meta';\nimport { SimulatorManager } from '../simulator/manager';\n\nexport interface AuditSummary {\n totalIssues: number;\n critical: number;\n high: number;\n medium: number;\n low: number;\n passed: number;\n failed: number;\n errors: number;\n}\n\nexport interface AuditReport {\n url: string;\n device: string;\n viewport: { w: number; h: number };\n timestamp: string;\n duration: number;\n score: number;\n summary: AuditSummary;\n detectors: DetectorResult[];\n}\n\nconst SEVERITY_WEIGHTS: Record<string, number> = {\n critical: 10,\n high: 5,\n medium: 2,\n low: 1,\n};\n\nexport class QAAudit {\n constructor(\n private client: BrowserBackend,\n private config: QAConfig = {},\n private simulator?: SimulatorManager,\n private deviceId?: string,\n private deviceInfo?: { name: string; w: number; h: number },\n ) {}\n\n async runFullAudit(url?: string): Promise<AuditReport> {\n if (url) await this.client.navigate({ url, waitUntil: 'load' });\n\n const currentUrl = await this.client.evaluate<string>('window.location.href');\n const startTime = Date.now();\n\n // Parallel: stateless detectors (10)\n const parallelResults = await Promise.allSettled([\n detectAutoZoom(this.client),\n detectTouchTargets(this.client),\n detectHoverOnly(this.client),\n detectInputType(this.client),\n detectSafeArea(this.client),\n detectHorizontalOverflow(this.client),\n detect100vh(this.client),\n detectFixedStacking(this.client),\n detectScrollLock(this.client),\n detectPwaMeta(this.client),\n ]);\n\n // Sequential: stateful detectors (3)\n const sequentialResults: PromiseSettledResult<DetectorResult>[] = [];\n try {\n sequentialResults.push({ status: 'fulfilled', value: await detectKeyboardOverlap(this.client) });\n } catch (e) {\n sequentialResults.push({ status: 'rejected', reason: e });\n }\n try {\n sequentialResults.push({ status: 'fulfilled', value: await detectDarkMode(this.client, this.simulator, this.deviceId) });\n } catch (e) {\n sequentialResults.push({ status: 'rejected', reason: e });\n }\n try {\n sequentialResults.push({ status: 'fulfilled', value: await detectOrientation(this.client, this.simulator, this.deviceId) });\n } catch (e) {\n sequentialResults.push({ status: 'rejected', reason: e });\n }\n\n // Combine + apply ignore rules\n const allSettled = [...parallelResults, ...sequentialResults];\n const allResults: DetectorResult[] = allSettled.map((r, _i) => {\n if (r.status === 'fulfilled') {\n return applyIgnoreRules(r.value, this.config);\n }\n return {\n detector: 'unknown',\n severity: 'error' as const,\n issues: [],\n passed: false,\n totalScanned: 0,\n issueCount: 0,\n error: r.reason instanceof Error ? r.reason.message : String(r.reason),\n };\n });\n\n const score = this.calculateScore(allResults);\n const summary = this.summarize(allResults);\n\n return {\n url: currentUrl,\n device: this.deviceInfo?.name ?? 'unknown',\n viewport: { w: this.deviceInfo?.w ?? 0, h: this.deviceInfo?.h ?? 0 },\n timestamp: new Date().toISOString(),\n duration: Date.now() - startTime,\n score,\n summary,\n detectors: allResults,\n };\n }\n\n private calculateScore(results: DetectorResult[]): number {\n let penalty = 0;\n for (const result of results) {\n if (result.severity && result.severity !== 'pass' && result.severity !== 'error') {\n penalty += (SEVERITY_WEIGHTS[result.severity] ?? 0) * result.issueCount;\n }\n }\n return Math.max(0, 100 - penalty);\n }\n\n private summarize(results: DetectorResult[]): AuditSummary {\n return {\n totalIssues: results.reduce((s, r) => s + r.issueCount, 0),\n critical: results.filter(r => r.severity === 'critical').reduce((s, r) => s + r.issueCount, 0),\n high: results.filter(r => r.severity === 'high').reduce((s, r) => s + r.issueCount, 0),\n medium: results.filter(r => r.severity === 'medium').reduce((s, r) => s + r.issueCount, 0),\n low: results.filter(r => r.severity === 'low').reduce((s, r) => s + r.issueCount, 0),\n passed: results.filter(r => r.passed).length,\n failed: results.filter(r => !r.passed).length,\n errors: results.filter(r => r.severity === 'error').length,\n };\n }\n}\n","export interface DetectorIssue {\n selector: string;\n element?: string;\n problem: string;\n fix: string;\n [key: string]: unknown;\n}\n\nexport interface DetectorResult {\n detector: string;\n severity: 'critical' | 'high' | 'medium' | 'low' | 'pass' | 'error';\n issues: DetectorIssue[];\n passed: boolean;\n totalScanned: number;\n issueCount: number;\n metadata?: Record<string, unknown>;\n error?: string;\n}\n\nexport interface QAConfig {\n thresholds?: {\n touchTargetMinSize?: number;\n inputMinFontSize?: number;\n };\n ignore?: Array<{\n detector: string;\n selector: string;\n }>;\n}\n\nexport function applyIgnoreRules(result: DetectorResult, config: QAConfig): DetectorResult {\n const ignores = config.ignore?.filter(r => r.detector === result.detector) ?? [];\n if (ignores.length > 0) {\n result.issues = result.issues.filter(issue =>\n !ignores.some(ign => issue.selector.includes(ign.selector))\n );\n result.issueCount = result.issues.length;\n result.passed = result.issueCount === 0;\n if (result.passed) result.severity = 'pass';\n }\n return result;\n}\n"],"names":["async","detectAutoZoom","client","evaluate","detectTouchTargets","detectHoverOnly","detectInputType","detectSafeArea","detectKeyboardOverlap","fixedBottom","length","detector","severity","issues","passed","totalScanned","issueCount","inputs","inputSelector","click","Promise","r","setTimeout","viewportWithKeyboard","fixed","rect","y","height","push","selector","problem","Math","round","fix","triggeredBy","dismissKeyboard","detectHorizontalOverflow","detect100vh","detectFixedStacking","detectScrollLock","detectDarkMode","simulator","deviceId","colorScheme","lightScreenshot","darkScreenshot","setAppearance","screenshot","toString","metadata","note","detectOrientation","portraitMeta","rotate","landscapeMeta","overflow","scrollWidth","innerWidth","portrait","detectPwaMeta","SEVERITY_WEIGHTS","critical","high","medium","low","QAAudit","config","deviceInfo","constructor","runFullAudit","url","this","navigate","waitUntil","currentUrl","startTime","Date","now","parallelResults","allSettled","sequentialResults","status","value","e","reason","allResults","map","_i","result","ignores","ignore","filter","issue","some","ign","includes","applyIgnoreRules","error","Error","message","String","score","calculateScore","summary","summarize","device","name","viewport","w","h","timestamp","toISOString","duration","detectors","results","penalty","max","totalIssues","reduce","s","failed","errors"],"sourceRoot":""}
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ "use strict";exports.id=473,exports.ids=[473],exports.modules={473(s,e,r){r.d(e,{StdioTransport:()=>o});var t=r(785),n=r(183);class o{rl=null;messageHandler=null;onMessage(s){this.messageHandler=s}send(s){process.stdout.write(JSON.stringify(s)+"\n")}start(){this.rl=t.createInterface({input:process.stdin,terminal:!1}),this.rl.on("line",s=>{if(!s.trim())return;let e;try{e=JSON.parse(s)}catch(s){const e={jsonrpc:"2.0",id:0,error:{code:n.D.PARSE_ERROR,message:s instanceof Error?s.message:"Parse error"}};return void this.send(e)}this.messageHandler?this.messageHandler(e).then(s=>{s&&this.send(s)}).catch(s=>{const r={jsonrpc:"2.0",id:e.id??0,error:{code:n.D.INTERNAL_ERROR,message:s instanceof Error?s.message:"Internal error"}};this.send(r)}):console.error("[StdioTransport] No message handler registered, dropping message")}),this.rl.on("close",()=>{console.error("[StdioTransport] stdin closed, shutting down..."),process.exit(0)})}async close(){this.rl&&(this.rl.close(),this.rl=null)}}}};
3
+ //# sourceMappingURL=473.index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli/473.index.js","mappings":";8HAUO,MAAMA,EACHC,GAAgC,KAChCC,eAAyF,KAEjG,SAAAC,CAAUC,GACRC,KAAKH,eAAiBE,CACxB,CAEA,IAAAE,CAAKC,GAEHC,QAAQC,OAAOC,MAAMC,KAAKC,UAAUL,GAAY,KAClD,CAEA,KAAAM,GACER,KAAKJ,GAAK,kBAAyB,CACjCa,MAAON,QAAQO,MAGfC,UAAU,IAGZX,KAAKJ,GAAGgB,GAAG,OAASC,IAClB,IAAKA,EAAKC,OAAQ,OAElB,IAAIC,EACJ,IACEA,EAAST,KAAKU,MAAMH,EACtB,CAAE,MAAOI,GACP,MAAMC,EAA6B,CACjCC,QAAS,MACTC,GAAI,EACJH,MAAO,CACLI,KAAM,IAAcC,YACpBC,QAASN,aAAiBO,MAAQP,EAAMM,QAAU,gBAItD,YADAvB,KAAKC,KAAKiB,EAEZ,CAEKlB,KAAKH,eAKVG,KAAKH,eAAekB,GACjBU,KAAMvB,IACDA,GACFF,KAAKC,KAAKC,KAGbwB,MAAOT,IACN,MACMC,EAA6B,CACjCC,QAAS,MACTC,GAHUL,EAAOK,IAA0B,EAI3CH,MAAO,CACLI,KAAM,IAAcM,eACpBJ,QAASN,aAAiBO,MAAQP,EAAMM,QAAU,mBAGtDvB,KAAKC,KAAKiB,KApBZU,QAAQX,MAAM,sEAwBlBjB,KAAKJ,GAAGgB,GAAG,QAAS,KAClBgB,QAAQX,MAAM,mDACdd,QAAQ0B,KAAK,IAEjB,CAEA,WAAMC,GACA9B,KAAKJ,KACPI,KAAKJ,GAAGkC,QACR9B,KAAKJ,GAAK,KAEd,E","sources":["webpack://opensafari-mcp/./src/transports/stdio.ts"],"sourcesContent":["/**\n * Stdio transport for MCP server.\n * Reads JSON-RPC messages from stdin (one per line), writes responses to stdout.\n * When stdin closes (EOF), the process exits — this is the expected stdio lifecycle.\n */\n\nimport * as readline from 'readline';\nimport { MCPResponse, MCPErrorCodes } from '../types/mcp';\nimport { MCPTransport } from './index';\n\nexport class StdioTransport implements MCPTransport {\n private rl: readline.Interface | null = null;\n private messageHandler: ((msg: Record<string, unknown>) => Promise<MCPResponse | null>) | null = null;\n\n onMessage(handler: (msg: Record<string, unknown>) => Promise<MCPResponse | null>): void {\n this.messageHandler = handler;\n }\n\n send(response: MCPResponse): void {\n // stdout is the MCP JSON-RPC channel in stdio mode\n process.stdout.write(JSON.stringify(response) + '\\n');\n }\n\n start(): void {\n this.rl = readline.createInterface({\n input: process.stdin,\n // Do NOT set output to process.stdout — stdout is the MCP JSON-RPC channel.\n // Setting it risks protocol corruption if readline writes internally (prompts, echoes).\n terminal: false,\n });\n\n this.rl.on('line', (line) => {\n if (!line.trim()) return;\n\n let parsed: Record<string, unknown>;\n try {\n parsed = JSON.parse(line) as Record<string, unknown>;\n } catch (error) {\n const errorResponse: MCPResponse = {\n jsonrpc: '2.0',\n id: 0,\n error: {\n code: MCPErrorCodes.PARSE_ERROR,\n message: error instanceof Error ? error.message : 'Parse error',\n },\n };\n this.send(errorResponse);\n return;\n }\n\n if (!this.messageHandler) {\n console.error('[StdioTransport] No message handler registered, dropping message');\n return;\n }\n\n this.messageHandler(parsed)\n .then((response) => {\n if (response) {\n this.send(response);\n }\n })\n .catch((error) => {\n const id = (parsed.id as string | number) ?? 0;\n const errorResponse: MCPResponse = {\n jsonrpc: '2.0',\n id,\n error: {\n code: MCPErrorCodes.INTERNAL_ERROR,\n message: error instanceof Error ? error.message : 'Internal error',\n },\n };\n this.send(errorResponse);\n });\n });\n\n this.rl.on('close', () => {\n console.error('[StdioTransport] stdin closed, shutting down...');\n process.exit(0);\n });\n }\n\n async close(): Promise<void> {\n if (this.rl) {\n this.rl.close();\n this.rl = null;\n }\n }\n}\n"],"names":["StdioTransport","rl","messageHandler","onMessage","handler","this","send","response","process","stdout","write","JSON","stringify","start","input","stdin","terminal","on","line","trim","parsed","parse","error","errorResponse","jsonrpc","id","code","PARSE_ERROR","message","Error","then","catch","INTERNAL_ERROR","console","exit","close"],"sourceRoot":""}
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ "use strict";exports.id=622,exports.ids=[622],exports.modules={622(e,s,t){t.d(s,{HTTPTransport:()=>i});var n=t(67),r=t(598),o=t(183);class i{server=null;messageHandler=null;port;sessions=new Set;sseConnections=[];sessionDeleteHandler=null;constructor(e){this.port=e}onSessionDelete(e){this.sessionDeleteHandler=e}onMessage(e){this.messageHandler=e}send(e){for(const s of this.sseConnections)try{s.res.write(`data: ${JSON.stringify(e)}\n\n`)}catch{}}start(){this.server=n.createServer((e,s)=>{this.handleHTTPRequest(e,s)}),this.server.listen(this.port,()=>{console.error(`[HTTPTransport] Listening on port ${this.port}`),console.error(`[HTTPTransport] MCP endpoint: http://localhost:${this.port}/mcp`)}),this.server.on("error",e=>{console.error("[HTTPTransport] Server error:",e)})}async close(){for(const e of this.sseConnections)try{e.res.end()}catch{}return this.sseConnections=[],new Promise(e=>{this.server?this.server.close(()=>{this.server=null,e()}):e()})}handleHTTPRequest(e,s){const t=new URL(e.url||"/",`http://localhost:${this.port}`).pathname;if(s.setHeader("Access-Control-Allow-Origin","*"),s.setHeader("Access-Control-Allow-Methods","GET, POST, DELETE, OPTIONS"),s.setHeader("Access-Control-Allow-Headers","Content-Type, Mcp-Session-Id"),s.setHeader("Access-Control-Expose-Headers","Mcp-Session-Id"),"OPTIONS"===e.method)return s.writeHead(204),void s.end();if("/health"!==t){if("/mcp"===t)switch(e.method){case"POST":return void this.handlePost(e,s);case"GET":return void this.handleSSE(e,s);case"DELETE":return void this.handleDelete(e,s);default:return s.writeHead(405,{"Content-Type":"application/json"}),void s.end(JSON.stringify({error:"Method not allowed"}))}s.writeHead(404,{"Content-Type":"application/json"}),s.end(JSON.stringify({error:"Not found"}))}else this.handleHealth(s)}handleHealth(e){e.writeHead(200,{"Content-Type":"application/json"}),e.end(JSON.stringify({status:"ok",transport:"http",activeSessions:this.sessions.size,sseConnections:this.sseConnections.length}))}handlePost(e,s){const t=[];let n=0;e.on("data",r=>{if(n+=r.length,n>10485760)return s.writeHead(413,{"Content-Type":"application/json"}),s.end(JSON.stringify({jsonrpc:"2.0",id:0,error:{code:o.D.INVALID_REQUEST,message:"Request body too large"}})),void e.destroy();t.push(r)}),e.on("end",async()=>{const n=Buffer.concat(t).toString("utf-8");if(!n.trim())return s.writeHead(400,{"Content-Type":"application/json"}),void s.end(JSON.stringify({jsonrpc:"2.0",id:0,error:{code:o.D.PARSE_ERROR,message:"Empty request body"}}));let i;try{i=JSON.parse(n)}catch(e){return s.writeHead(400,{"Content-Type":"application/json"}),void s.end(JSON.stringify({jsonrpc:"2.0",id:0,error:{code:o.D.PARSE_ERROR,message:e instanceof Error?e.message:"Parse error"}}))}let a=e.headers["mcp-session-id"];if(!this.messageHandler)return s.writeHead(500,{"Content-Type":"application/json"}),void s.end(JSON.stringify({jsonrpc:"2.0",id:0,error:{code:o.D.INTERNAL_ERROR,message:"No message handler registered"}}));if(Array.isArray(i)){const e=(await this.processBatch(i,a)).filter(e=>null!==e);return a&&s.setHeader("Mcp-Session-Id",a),void(0===e.length?(s.writeHead(202),s.end()):1===e.length?(s.writeHead(200,{"Content-Type":"application/json"}),s.end(JSON.stringify(e[0]))):(s.writeHead(200,{"Content-Type":"application/json"}),s.end(JSON.stringify(e))))}const d=i;"initialize"!==d.method||a||(a=r.randomUUID(),this.sessions.add(a));try{const e=await this.messageHandler(d);a&&s.setHeader("Mcp-Session-Id",a),null===e?(s.writeHead(202),s.end()):(s.writeHead(200,{"Content-Type":"application/json"}),s.end(JSON.stringify(e)))}catch(e){const t=d.id??0;s.writeHead(200,{"Content-Type":"application/json"}),s.end(JSON.stringify({jsonrpc:"2.0",id:t,error:{code:o.D.INTERNAL_ERROR,message:e instanceof Error?e.message:"Internal error"}}))}}),e.on("error",e=>{console.error("[HTTPTransport] Request read error:",e),s.headersSent||(s.writeHead(400,{"Content-Type":"application/json"}),s.end(JSON.stringify({jsonrpc:"2.0",id:0,error:{code:o.D.PARSE_ERROR,message:"Request read error"}})))})}handleSSE(e,s){const t=e.headers["mcp-session-id"]||"anonymous";s.writeHead(200,{"Content-Type":"text/event-stream","Cache-Control":"no-cache",Connection:"keep-alive"}),s.write(": keepalive\n\n");const n={res:s,sessionId:t};this.sseConnections.push(n),e.on("close",()=>{const e=this.sseConnections.indexOf(n);-1!==e&&this.sseConnections.splice(e,1),console.error(`[HTTPTransport] SSE client disconnected (session: ${t})`)})}handleDelete(e,s){const t=e.headers["mcp-session-id"];t&&this.sessions.has(t)?(this.sessions.delete(t),this.sessionDeleteHandler&&this.sessionDeleteHandler(t),this.sseConnections=this.sseConnections.filter(e=>{if(e.sessionId===t){try{e.res.end()}catch{}return!1}return!0}),s.writeHead(200,{"Content-Type":"application/json"}),s.end(JSON.stringify({status:"session terminated"}))):(s.writeHead(404,{"Content-Type":"application/json"}),s.end(JSON.stringify({error:"Session not found"})))}async processBatch(e,s){const t=this.messageHandler;s||e.some(e=>"object"==typeof e&&null!==e&&"initialize"===e.method)&&(s=r.randomUUID(),this.sessions.add(s));const n=e.map(async e=>{if("object"!=typeof e||null===e)return{jsonrpc:"2.0",id:0,error:{code:o.D.INVALID_REQUEST,message:"Invalid batch element: not an object"}};const s=e;try{return await t(s)}catch(e){return{jsonrpc:"2.0",id:s.id??0,error:{code:o.D.INTERNAL_ERROR,message:e instanceof Error?e.message:"Internal error"}}}});return Promise.all(n)}}}};
3
+ //# sourceMappingURL=622.index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli/622.index.js","mappings":"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","sources":["webpack://opensafari-mcp/./src/transports/http.ts"],"sourcesContent":["/**\n * Streamable HTTP transport for MCP server.\n *\n * Implements MCP Streamable HTTP transport (spec 2025-03-26):\n * - POST /mcp: receives JSON-RPC request/notification, returns JSON-RPC response\n * - GET /health: basic health check (separate from the self-healing health endpoint)\n * - DELETE /mcp: session termination\n *\n * Key difference from stdio: client disconnect does NOT kill the server.\n * The HTTP server continues to accept new connections.\n */\n\nimport * as http from 'node:http';\nimport * as crypto from 'node:crypto';\nimport { MCPResponse, MCPErrorCodes } from '../types/mcp';\nimport { MCPTransport } from './index';\n\n/** Maximum allowed HTTP request body size (10 MB) to prevent OOM from oversized requests */\nconst MAX_BODY_BYTES = 10 * 1024 * 1024;\n\n/** Active SSE connections for server-initiated notifications */\ninterface SSEConnection {\n res: http.ServerResponse;\n sessionId: string;\n}\n\nexport class HTTPTransport implements MCPTransport {\n private server: http.Server | null = null;\n private messageHandler: ((msg: Record<string, unknown>) => Promise<MCPResponse | null>) | null = null;\n private port: number;\n private sessions: Set<string> = new Set();\n private sseConnections: SSEConnection[] = [];\n private sessionDeleteHandler: ((sessionId: string) => void) | null = null;\n\n constructor(port: number) {\n this.port = port;\n }\n\n /**\n * Register a callback to be invoked whenever a session is deleted.\n * Used by MCPServer to clean up per-session state (e.g. rate-limiter buckets).\n */\n onSessionDelete(handler: (sessionId: string) => void): void {\n this.sessionDeleteHandler = handler;\n }\n\n onMessage(handler: (msg: Record<string, unknown>) => Promise<MCPResponse | null>): void {\n this.messageHandler = handler;\n }\n\n /**\n * Send a server-initiated notification to all connected SSE clients.\n * For HTTP, request-correlated responses are sent directly in handlePost.\n */\n send(response: MCPResponse): void {\n // Broadcast to all SSE connections\n for (const conn of this.sseConnections) {\n try {\n conn.res.write(`data: ${JSON.stringify(response)}\\n\\n`);\n } catch {\n // Connection may have been closed\n }\n }\n }\n\n start(): void {\n this.server = http.createServer((req, res) => {\n this.handleHTTPRequest(req, res);\n });\n\n this.server.listen(this.port, () => {\n console.error(`[HTTPTransport] Listening on port ${this.port}`);\n console.error(`[HTTPTransport] MCP endpoint: http://localhost:${this.port}/mcp`);\n });\n\n this.server.on('error', (err) => {\n console.error(`[HTTPTransport] Server error:`, err);\n });\n }\n\n async close(): Promise<void> {\n // Close all SSE connections\n for (const conn of this.sseConnections) {\n try {\n conn.res.end();\n } catch {\n // Already closed\n }\n }\n this.sseConnections = [];\n\n return new Promise((resolve) => {\n if (this.server) {\n this.server.close(() => {\n this.server = null;\n resolve();\n });\n } else {\n resolve();\n }\n });\n }\n\n private handleHTTPRequest(req: http.IncomingMessage, res: http.ServerResponse): void {\n const url = new URL(req.url || '/', `http://localhost:${this.port}`);\n const pathname = url.pathname;\n\n // CORS headers for all responses\n res.setHeader('Access-Control-Allow-Origin', '*');\n res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');\n res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Mcp-Session-Id');\n res.setHeader('Access-Control-Expose-Headers', 'Mcp-Session-Id');\n\n // Handle CORS preflight\n if (req.method === 'OPTIONS') {\n res.writeHead(204);\n res.end();\n return;\n }\n\n if (pathname === '/health') {\n this.handleHealth(res);\n return;\n }\n\n if (pathname === '/mcp') {\n switch (req.method) {\n case 'POST':\n this.handlePost(req, res);\n return;\n case 'GET':\n this.handleSSE(req, res);\n return;\n case 'DELETE':\n this.handleDelete(req, res);\n return;\n default:\n res.writeHead(405, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ error: 'Method not allowed' }));\n return;\n }\n }\n\n // Unknown path\n res.writeHead(404, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ error: 'Not found' }));\n }\n\n /**\n * GET /health - basic health check\n */\n private handleHealth(res: http.ServerResponse): void {\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({\n status: 'ok',\n transport: 'http',\n activeSessions: this.sessions.size,\n sseConnections: this.sseConnections.length,\n }));\n }\n\n /**\n * POST /mcp - handle JSON-RPC request or batch\n */\n private handlePost(req: http.IncomingMessage, res: http.ServerResponse): void {\n const chunks: Buffer[] = [];\n let bodyBytes = 0;\n\n req.on('data', (chunk: Buffer) => {\n bodyBytes += chunk.length;\n if (bodyBytes > MAX_BODY_BYTES) {\n res.writeHead(413, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({\n jsonrpc: '2.0',\n id: 0,\n error: { code: MCPErrorCodes.INVALID_REQUEST, message: 'Request body too large' },\n }));\n req.destroy();\n return;\n }\n chunks.push(chunk);\n });\n\n req.on('end', async () => {\n const body = Buffer.concat(chunks).toString('utf-8');\n\n if (!body.trim()) {\n res.writeHead(400, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({\n jsonrpc: '2.0',\n id: 0,\n error: { code: MCPErrorCodes.PARSE_ERROR, message: 'Empty request body' },\n }));\n return;\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(body);\n } catch (error) {\n res.writeHead(400, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({\n jsonrpc: '2.0',\n id: 0,\n error: {\n code: MCPErrorCodes.PARSE_ERROR,\n message: error instanceof Error ? error.message : 'Parse error',\n },\n }));\n return;\n }\n\n // Session tracking via Mcp-Session-Id header\n let sessionId = req.headers['mcp-session-id'] as string | undefined;\n\n if (!this.messageHandler) {\n res.writeHead(500, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({\n jsonrpc: '2.0',\n id: 0,\n error: { code: MCPErrorCodes.INTERNAL_ERROR, message: 'No message handler registered' },\n }));\n return;\n }\n\n // Handle JSON-RPC batch (array of requests)\n if (Array.isArray(parsed)) {\n const results = await this.processBatch(parsed, sessionId);\n // Filter out null results (notifications don't produce responses)\n const responses = results.filter((r): r is MCPResponse => r !== null);\n\n if (sessionId) {\n res.setHeader('Mcp-Session-Id', sessionId);\n }\n\n if (responses.length === 0) {\n // All were notifications — respond with 202 Accepted\n res.writeHead(202);\n res.end();\n } else if (responses.length === 1) {\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify(responses[0]));\n } else {\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify(responses));\n }\n return;\n }\n\n // Single request/notification\n const msg = parsed as Record<string, unknown>;\n\n // Check if this is an initialize request — assign session ID\n if (msg.method === 'initialize' && !sessionId) {\n sessionId = crypto.randomUUID();\n this.sessions.add(sessionId);\n }\n\n try {\n const response = await this.messageHandler(msg);\n\n if (sessionId) {\n res.setHeader('Mcp-Session-Id', sessionId);\n }\n\n if (response === null) {\n // Notification — no response body\n res.writeHead(202);\n res.end();\n } else {\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify(response));\n }\n } catch (error) {\n const id = (msg.id as string | number) ?? 0;\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({\n jsonrpc: '2.0',\n id,\n error: {\n code: MCPErrorCodes.INTERNAL_ERROR,\n message: error instanceof Error ? error.message : 'Internal error',\n },\n }));\n }\n });\n\n req.on('error', (err) => {\n console.error('[HTTPTransport] Request read error:', err);\n if (!res.headersSent) {\n res.writeHead(400, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({\n jsonrpc: '2.0',\n id: 0,\n error: { code: MCPErrorCodes.PARSE_ERROR, message: 'Request read error' },\n }));\n }\n });\n }\n\n /**\n * GET /mcp - Server-Sent Events for server-initiated notifications\n */\n private handleSSE(req: http.IncomingMessage, res: http.ServerResponse): void {\n const sessionId = req.headers['mcp-session-id'] as string || 'anonymous';\n\n res.writeHead(200, {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n 'Connection': 'keep-alive',\n });\n\n // Send initial keepalive\n res.write(': keepalive\\n\\n');\n\n const conn: SSEConnection = { res, sessionId };\n this.sseConnections.push(conn);\n\n // Clean up on disconnect\n req.on('close', () => {\n const idx = this.sseConnections.indexOf(conn);\n if (idx !== -1) {\n this.sseConnections.splice(idx, 1);\n }\n console.error(`[HTTPTransport] SSE client disconnected (session: ${sessionId})`);\n });\n }\n\n /**\n * DELETE /mcp - Session termination\n */\n private handleDelete(req: http.IncomingMessage, res: http.ServerResponse): void {\n const sessionId = req.headers['mcp-session-id'] as string | undefined;\n\n if (sessionId && this.sessions.has(sessionId)) {\n this.sessions.delete(sessionId);\n\n // Notify session-delete listeners (e.g. rate-limiter cleanup)\n if (this.sessionDeleteHandler) {\n this.sessionDeleteHandler(sessionId);\n }\n\n // Close any SSE connections for this session\n this.sseConnections = this.sseConnections.filter((conn) => {\n if (conn.sessionId === sessionId) {\n try {\n conn.res.end();\n } catch {\n // Already closed\n }\n return false;\n }\n return true;\n });\n\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ status: 'session terminated' }));\n } else {\n res.writeHead(404, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ error: 'Session not found' }));\n }\n }\n\n /**\n * Process a batch of JSON-RPC messages\n */\n private async processBatch(\n messages: unknown[],\n sessionId: string | undefined,\n ): Promise<(MCPResponse | null)[]> {\n const handler = this.messageHandler!;\n\n // Assign sessionId once before concurrent processing to avoid data race\n // when multiple initialize requests appear in the same batch.\n if (!sessionId) {\n const hasInitialize = messages.some(\n (msg) => typeof msg === 'object' && msg !== null && (msg as Record<string, unknown>).method === 'initialize',\n );\n if (hasInitialize) {\n sessionId = crypto.randomUUID();\n this.sessions.add(sessionId);\n }\n }\n\n const promises = messages.map(async (msg) => {\n if (typeof msg !== 'object' || msg === null) {\n return {\n jsonrpc: '2.0' as const,\n id: 0,\n error: {\n code: MCPErrorCodes.INVALID_REQUEST,\n message: 'Invalid batch element: not an object',\n },\n } as MCPResponse;\n }\n\n const record = msg as Record<string, unknown>;\n\n try {\n return await handler(record);\n } catch (error) {\n const id = (record.id as string | number) ?? 0;\n return {\n jsonrpc: '2.0' as const,\n id,\n error: {\n code: MCPErrorCodes.INTERNAL_ERROR,\n message: error instanceof Error ? error.message : 'Internal error',\n },\n } as MCPResponse;\n }\n });\n\n return Promise.all(promises);\n }\n}\n"],"names":["HTTPTransport","server","messageHandler","port","sessions","Set","sseConnections","sessionDeleteHandler","constructor","this","onSessionDelete","handler","onMessage","send","response","conn","res","write","JSON","stringify","start","req","handleHTTPRequest","listen","console","error","on","err","close","end","Promise","resolve","pathname","URL","url","setHeader","method","writeHead","handlePost","handleSSE","handleDelete","handleHealth","status","transport","activeSessions","size","length","chunks","bodyBytes","chunk","jsonrpc","id","code","INVALID_REQUEST","message","destroy","push","async","body","Buffer","concat","toString","trim","PARSE_ERROR","parsed","parse","Error","sessionId","headers","INTERNAL_ERROR","Array","isArray","responses","processBatch","filter","r","msg","add","headersSent","idx","indexOf","splice","has","delete","messages","some","promises","map","record","all"],"sourceRoot":""}
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ "use strict";exports.id=712,exports.ids=[712],exports.modules={712(s,e,t){function r(s){const e=[`## ${s.score>=90?"V":s.score>=70?"!":"X"} iOS QA Audit Report`,"",`**Score: ${s.score}/100** | Device: ${s.device} (${s.viewport.w}x${s.viewport.h}) | ${s.timestamp}`,`**URL:** ${s.url} | Duration: ${s.duration}ms`,"","| Severity | Count |","|----------|-------|",`| Critical | ${s.summary.critical} |`,`| High | ${s.summary.high} |`,`| Medium | ${s.summary.medium} |`,`| Low | ${s.summary.low} |`,`| Passed | ${s.summary.passed}/13 detectors |`],t=s.detectors.filter(s=>!s.passed);if(t.length>0){e.push("","### Issues Found","");for(const s of t){e.push(`#### [${s.severity.toUpperCase()}] ${s.detector} (${s.issueCount} issues)`);for(const t of s.issues.slice(0,5))e.push(`- \`${t.selector}\`: ${t.problem}`),e.push(` - **Fix:** ${t.fix}`);s.issues.length>5&&e.push(`- ... and ${s.issues.length-5} more`),e.push("")}}return e.join("\n")}t.d(e,{generateAuditMarkdown:()=>r})}};
3
+ //# sourceMappingURL=712.index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli/712.index.js","mappings":";0EAEO,SAASA,EAAsBC,GACpC,MACMC,EAAQ,CACZ,MAFYD,EAAOE,OAAS,GAAK,IAAMF,EAAOE,OAAS,GAAK,IAAM,0BAGlE,GACA,YAAYF,EAAOE,yBAAyBF,EAAOG,WAAWH,EAAOI,SAASC,KAAKL,EAAOI,SAASE,QAAQN,EAAOO,YAClH,YAAYP,EAAOQ,mBAAmBR,EAAOS,aAC7C,GACA,uBACA,uBACA,gBAAgBT,EAAOU,QAAQC,aAC/B,YAAYX,EAAOU,QAAQE,SAC3B,cAAcZ,EAAOU,QAAQG,WAC7B,WAAWb,EAAOU,QAAQI,QAC1B,cAAcd,EAAOU,QAAQK,yBAGzBC,EAAShB,EAAOiB,UAAUC,OAAOC,IAAMA,EAAEJ,QAC/C,GAAIC,EAAOI,OAAS,EAAG,CACrBnB,EAAMoB,KAAK,GAAI,mBAAoB,IACnC,IAAK,MAAMC,KAAON,EAAQ,CACxBf,EAAMoB,KAAK,SAASC,EAAIC,SAASC,kBAAkBF,EAAIG,aAAaH,EAAII,sBACxE,IAAK,MAAMC,KAASL,EAAIM,OAAOC,MAAM,EAAG,GACtC5B,EAAMoB,KAAK,OAAOM,EAAMG,eAAeH,EAAMI,WAC7C9B,EAAMoB,KAAK,gBAAgBM,EAAMK,OAE/BV,EAAIM,OAAOR,OAAS,GAAGnB,EAAMoB,KAAK,aAAaC,EAAIM,OAAOR,OAAS,UACvEnB,EAAMoB,KAAK,GACb,CACF,CAEA,OAAOpB,EAAMgC,KAAK,KACpB,C","sources":["webpack://opensafari-mcp/./src/qa/report-markdown.ts"],"sourcesContent":["import { AuditReport } from './audit';\n\nexport function generateAuditMarkdown(report: AuditReport): string {\n const emoji = report.score >= 90 ? 'V' : report.score >= 70 ? '!' : 'X';\n const lines = [\n `## ${emoji} iOS QA Audit Report`,\n '',\n `**Score: ${report.score}/100** | Device: ${report.device} (${report.viewport.w}x${report.viewport.h}) | ${report.timestamp}`,\n `**URL:** ${report.url} | Duration: ${report.duration}ms`,\n '',\n '| Severity | Count |',\n '|----------|-------|',\n `| Critical | ${report.summary.critical} |`,\n `| High | ${report.summary.high} |`,\n `| Medium | ${report.summary.medium} |`,\n `| Low | ${report.summary.low} |`,\n `| Passed | ${report.summary.passed}/13 detectors |`,\n ];\n\n const failed = report.detectors.filter(d => !d.passed);\n if (failed.length > 0) {\n lines.push('', '### Issues Found', '');\n for (const det of failed) {\n lines.push(`#### [${det.severity.toUpperCase()}] ${det.detector} (${det.issueCount} issues)`);\n for (const issue of det.issues.slice(0, 5)) {\n lines.push(`- \\`${issue.selector}\\`: ${issue.problem}`);\n lines.push(` - **Fix:** ${issue.fix}`);\n }\n if (det.issues.length > 5) lines.push(`- ... and ${det.issues.length - 5} more`);\n lines.push('');\n }\n }\n\n return lines.join('\\n');\n}\n"],"names":["generateAuditMarkdown","report","lines","score","device","viewport","w","h","timestamp","url","duration","summary","critical","high","medium","low","passed","failed","detectors","filter","d","length","push","det","severity","toUpperCase","detector","issueCount","issue","issues","slice","selector","problem","fix","join"],"sourceRoot":""}
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ "use strict";exports.id=844,exports.ids=[844],exports.modules={844(e,t,r){r.d(t,{QAHistory:()=>o});var i=r(943),s=r(928),n=r(857);class o{reportsDir;constructor(e){this.reportsDir=e??s.join(n.homedir(),".opensafari","reports")}async save(e){const t=s.join(this.reportsDir,this.sanitizeSite(e.url));await i.mkdir(t,{recursive:!0});const r=(new Date).toISOString().replace(/[:.]/g,"-")+".json",n=s.join(t,r);return await i.writeFile(n,JSON.stringify(e,null,2)),await this.rotate(t,30),n}async getLatest(e){const t=await this.listReportFiles(e);return 0===t.length?null:JSON.parse(await i.readFile(t[t.length-1],"utf-8"))}async getPrevious(e){const t=await this.listReportFiles(e);return t.length<2?null:JSON.parse(await i.readFile(t[t.length-2],"utf-8"))}async detectRegressions(e,t){const r=this.buildFingerprints(e),i=this.buildFingerprints(t),s=[...r.values()].filter(e=>!i.has(e.fingerprint)),n=[...i.values()].filter(e=>!r.has(e.fingerprint)),o=[...r.values()].filter(e=>i.has(e.fingerprint)),a=e.score-t.score,c=a>0?"improved":a<0?"regressed":"unchanged";return{currentScore:e.score,previousScore:t.score,scoreDelta:a,newIssues:s,fixedIssues:n,recurringIssues:o,summary:`Score ${c} ${t.score} -> ${e.score} (${a>=0?"+":""}${a}). ${n.length} fixed, ${s.length} new, ${o.length} recurring.`}}getExitCode(e,t){const r={failOnCritical:!0,failOnHigh:!1,...t};return r.failOnCritical&&e.summary.critical>0||r.failOnHigh&&e.summary.high>0||r.minScore&&e.score<r.minScore?1:0}buildFingerprints(e){const t=new Map;for(const r of e.detectors)for(const e of r.issues){const i=this.fingerprint(r.detector,e.selector);t.set(i,{detector:r.detector,selector:e.selector,problem:e.problem,fingerprint:i})}return t}fingerprint(e,t){const r=`${e}::${t}`;let i=0;for(let e=0;e<r.length;e++)i=(i<<5)-i+r.charCodeAt(e),i|=0;return i.toString(36)}sanitizeSite(e){try{return new URL(e).hostname.replace(/[^a-zA-Z0-9.-]/g,"_")}catch{return e.replace(/[^a-zA-Z0-9.-]/g,"_")}}async listReportFiles(e){const t=s.join(this.reportsDir,this.sanitizeSite(e));try{return(await i.readdir(t)).filter(e=>e.endsWith(".json")).sort().map(e=>s.join(t,e))}catch{return[]}}async rotate(e,t){try{const r=(await i.readdir(e)).filter(e=>e.endsWith(".json")).sort();if(r.length>t)for(const n of r.slice(0,r.length-t))await i.unlink(s.join(e,n)).catch(()=>{})}catch{}}}}};
3
+ //# sourceMappingURL=844.index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli/844.index.js","mappings":"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","sources":["webpack://opensafari-mcp/./src/qa/history.ts"],"sourcesContent":["import * as fs from 'fs/promises';\nimport * as path from 'path';\nimport * as os from 'os';\nimport { AuditReport } from './audit';\n\nexport interface RegressionReport {\n currentScore: number;\n previousScore: number;\n scoreDelta: number;\n newIssues: Array<{ detector: string; selector: string; problem: string; fingerprint: string }>;\n fixedIssues: Array<{ detector: string; selector: string; problem: string; fingerprint: string }>;\n recurringIssues: Array<{ detector: string; selector: string; problem: string; fingerprint: string }>;\n summary: string;\n}\n\nexport class QAHistory {\n private reportsDir: string;\n\n constructor(reportsDir?: string) {\n this.reportsDir = reportsDir ?? path.join(os.homedir(), '.opensafari', 'reports');\n }\n\n async save(report: AuditReport): Promise<string> {\n const siteDir = path.join(this.reportsDir, this.sanitizeSite(report.url));\n await fs.mkdir(siteDir, { recursive: true });\n const filename = new Date().toISOString().replace(/[:.]/g, '-') + '.json';\n const filePath = path.join(siteDir, filename);\n await fs.writeFile(filePath, JSON.stringify(report, null, 2));\n await this.rotate(siteDir, 30);\n return filePath;\n }\n\n async getLatest(url: string): Promise<AuditReport | null> {\n const files = await this.listReportFiles(url);\n if (files.length === 0) return null;\n return JSON.parse(await fs.readFile(files[files.length - 1], 'utf-8'));\n }\n\n async getPrevious(url: string): Promise<AuditReport | null> {\n const files = await this.listReportFiles(url);\n if (files.length < 2) return null;\n return JSON.parse(await fs.readFile(files[files.length - 2], 'utf-8'));\n }\n\n async detectRegressions(current: AuditReport, previous: AuditReport): Promise<RegressionReport> {\n const currentFP = this.buildFingerprints(current);\n const previousFP = this.buildFingerprints(previous);\n\n const newIssues = [...currentFP.values()].filter(i => !previousFP.has(i.fingerprint));\n const fixedIssues = [...previousFP.values()].filter(i => !currentFP.has(i.fingerprint));\n const recurringIssues = [...currentFP.values()].filter(i => previousFP.has(i.fingerprint));\n\n const scoreDelta = current.score - previous.score;\n const trend = scoreDelta > 0 ? 'improved' : scoreDelta < 0 ? 'regressed' : 'unchanged';\n\n return {\n currentScore: current.score,\n previousScore: previous.score,\n scoreDelta,\n newIssues,\n fixedIssues,\n recurringIssues,\n summary: `Score ${trend} ${previous.score} -> ${current.score} (${scoreDelta >= 0 ? '+' : ''}${scoreDelta}). ${fixedIssues.length} fixed, ${newIssues.length} new, ${recurringIssues.length} recurring.`,\n };\n }\n\n getExitCode(report: AuditReport, options?: { failOnCritical?: boolean; failOnHigh?: boolean; minScore?: number }): number {\n const opts = { failOnCritical: true, failOnHigh: false, ...options };\n if (opts.failOnCritical && report.summary.critical > 0) return 1;\n if (opts.failOnHigh && report.summary.high > 0) return 1;\n if (opts.minScore && report.score < opts.minScore) return 1;\n return 0;\n }\n\n private buildFingerprints(report: AuditReport): Map<string, { detector: string; selector: string; problem: string; fingerprint: string }> {\n const map = new Map<string, { detector: string; selector: string; problem: string; fingerprint: string }>();\n for (const det of report.detectors) {\n for (const issue of det.issues) {\n const fp = this.fingerprint(det.detector, issue.selector);\n map.set(fp, { detector: det.detector, selector: issue.selector, problem: issue.problem, fingerprint: fp });\n }\n }\n return map;\n }\n\n private fingerprint(detector: string, selector: string): string {\n const input = `${detector}::${selector}`;\n let hash = 0;\n for (let i = 0; i < input.length; i++) {\n hash = ((hash << 5) - hash) + input.charCodeAt(i);\n hash |= 0;\n }\n return hash.toString(36);\n }\n\n private sanitizeSite(url: string): string {\n try { return new URL(url).hostname.replace(/[^a-zA-Z0-9.-]/g, '_'); }\n catch { return url.replace(/[^a-zA-Z0-9.-]/g, '_'); }\n }\n\n private async listReportFiles(url: string): Promise<string[]> {\n const siteDir = path.join(this.reportsDir, this.sanitizeSite(url));\n try {\n const files = await fs.readdir(siteDir);\n return files.filter(f => f.endsWith('.json')).sort().map(f => path.join(siteDir, f));\n } catch { return []; }\n }\n\n private async rotate(dir: string, maxReports: number): Promise<void> {\n try {\n const files = await fs.readdir(dir);\n const sorted = files.filter(f => f.endsWith('.json')).sort();\n if (sorted.length > maxReports) {\n for (const f of sorted.slice(0, sorted.length - maxReports)) {\n await fs.unlink(path.join(dir, f)).catch(() => {});\n }\n }\n } catch { /* */ }\n }\n}\n"],"names":["QAHistory","reportsDir","constructor","this","save","report","siteDir","sanitizeSite","url","recursive","filename","Date","toISOString","replace","filePath","JSON","stringify","rotate","getLatest","files","listReportFiles","length","parse","getPrevious","detectRegressions","current","previous","currentFP","buildFingerprints","previousFP","newIssues","values","filter","i","has","fingerprint","fixedIssues","recurringIssues","scoreDelta","score","trend","currentScore","previousScore","summary","getExitCode","options","opts","failOnCritical","failOnHigh","critical","high","minScore","map","Map","det","detectors","issue","issues","fp","detector","selector","set","problem","input","hash","charCodeAt","toString","URL","hostname","f","endsWith","sort","dir","maxReports","sorted","slice","catch"],"sourceRoot":""}
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../cli/index.ts"],"names":[],"mappings":""}