@synapta/skills 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (353) hide show
  1. package/dist/index.js +11 -4
  2. package/package.json +3 -4
  3. package/skills/ATTRIBUTION.md +80 -0
  4. package/skills/accessibility-audit/SKILL.md +325 -0
  5. package/skills/accessibility-audit/reference/wcag-checklist.md +103 -0
  6. package/skills/apns-notifier/SKILL.md +86 -0
  7. package/skills/approval-policy-enforcer/SKILL.md +66 -0
  8. package/skills/apps-sdk-builder/LICENSE.txt +201 -0
  9. package/skills/apps-sdk-builder/SKILL.md +328 -0
  10. package/skills/apps-sdk-builder/agents/openai.yaml +13 -0
  11. package/skills/apps-sdk-builder/references/app-archetypes.md +132 -0
  12. package/skills/apps-sdk-builder/references/apps-sdk-docs-workflow.md +135 -0
  13. package/skills/apps-sdk-builder/references/interactive-state-sync-patterns.md +113 -0
  14. package/skills/apps-sdk-builder/references/repo-contract-and-validation.md +93 -0
  15. package/skills/apps-sdk-builder/references/search-fetch-standard.md +67 -0
  16. package/skills/apps-sdk-builder/references/upstream-example-workflow.md +79 -0
  17. package/skills/apps-sdk-builder/references/window-openai-patterns.md +79 -0
  18. package/skills/apps-sdk-builder/scripts/scaffold_node_ext_apps.mjs +606 -0
  19. package/skills/architecture-selector/SKILL.md +64 -0
  20. package/skills/backlog-planner/SKILL.md +68 -0
  21. package/skills/carplay-entitlement-checker/SKILL.md +82 -0
  22. package/skills/concept-discovery/SKILL.md +517 -0
  23. package/skills/concept-discovery/assets/sample-analysis.json +81 -0
  24. package/skills/concept-discovery/expected_outputs/sample-enum-dictionary.md +25 -0
  25. package/skills/concept-discovery/expected_outputs/sample-page-user-list.md +83 -0
  26. package/skills/concept-discovery/expected_outputs/sample-prd-readme.md +43 -0
  27. package/skills/concept-discovery/references/framework-patterns.md +228 -0
  28. package/skills/concept-discovery/references/prd-quality-checklist.md +65 -0
  29. package/skills/concept-discovery/scripts/codebase_analyzer.py +732 -0
  30. package/skills/concept-discovery/scripts/prd_scaffolder.py +435 -0
  31. package/skills/dast-zap/SKILL.md +453 -0
  32. package/skills/dast-zap/assets/.gitkeep +9 -0
  33. package/skills/dast-zap/assets/github_action.yml +207 -0
  34. package/skills/dast-zap/assets/gitlab_ci.yml +226 -0
  35. package/skills/dast-zap/assets/zap_automation.yaml +196 -0
  36. package/skills/dast-zap/assets/zap_context.xml +192 -0
  37. package/skills/dast-zap/references/EXAMPLE.md +40 -0
  38. package/skills/dast-zap/references/api_testing_guide.md +475 -0
  39. package/skills/dast-zap/references/authentication_guide.md +431 -0
  40. package/skills/dast-zap/references/false_positive_handling.md +427 -0
  41. package/skills/dast-zap/references/owasp_mapping.md +255 -0
  42. package/skills/dep-sbom-scan/SKILL.md +466 -0
  43. package/skills/deploy-cloudflare/SKILL.md +930 -0
  44. package/skills/deploy-docker/SKILL.md +55 -0
  45. package/skills/deploy-fly/SKILL.md +228 -0
  46. package/skills/deploy-k8s/SKILL.md +108 -0
  47. package/skills/deploy-k8s/assets/logo.png +0 -0
  48. package/skills/deploy-k8s/docs/README.md +29 -0
  49. package/skills/deploy-k8s/docs/SUMMARY.md +56 -0
  50. package/skills/deploy-k8s/docs/advanced/token-efficiency.md +61 -0
  51. package/skills/deploy-k8s/docs/architecture/multi-tenancy.md +96 -0
  52. package/skills/deploy-k8s/docs/architecture/storage-and-state.md +102 -0
  53. package/skills/deploy-k8s/docs/architecture/workload-patterns.md +87 -0
  54. package/skills/deploy-k8s/docs/book.json +16 -0
  55. package/skills/deploy-k8s/docs/community/changelog.md +34 -0
  56. package/skills/deploy-k8s/docs/community/contributing.md +67 -0
  57. package/skills/deploy-k8s/docs/core-concepts/failure-modes.md +153 -0
  58. package/skills/deploy-k8s/docs/core-concepts/philosophy.md +83 -0
  59. package/skills/deploy-k8s/docs/core-concepts/workflow.md +124 -0
  60. package/skills/deploy-k8s/docs/examples/bad-patterns.md +47 -0
  61. package/skills/deploy-k8s/docs/examples/do-dont-checklist.md +37 -0
  62. package/skills/deploy-k8s/docs/examples/good-patterns.md +49 -0
  63. package/skills/deploy-k8s/docs/failure-modes/api-drift.md +104 -0
  64. package/skills/deploy-k8s/docs/failure-modes/fragile-rollouts.md +99 -0
  65. package/skills/deploy-k8s/docs/failure-modes/insecure-workload-defaults.md +80 -0
  66. package/skills/deploy-k8s/docs/failure-modes/network-exposure.md +98 -0
  67. package/skills/deploy-k8s/docs/failure-modes/privilege-sprawl.md +91 -0
  68. package/skills/deploy-k8s/docs/failure-modes/resource-starvation.md +85 -0
  69. package/skills/deploy-k8s/docs/getting-started/installation.md +152 -0
  70. package/skills/deploy-k8s/docs/getting-started/quick-start.md +115 -0
  71. package/skills/deploy-k8s/docs/guides/helm-patterns.md +71 -0
  72. package/skills/deploy-k8s/docs/guides/kustomize-patterns.md +65 -0
  73. package/skills/deploy-k8s/docs/guides/observability.md +67 -0
  74. package/skills/deploy-k8s/docs/guides/security-hardening.md +59 -0
  75. package/skills/deploy-k8s/docs/guides/validation-and-policy.md +66 -0
  76. package/skills/deploy-k8s/docs/integrations/mcp-integration.md +52 -0
  77. package/skills/deploy-k8s/docs/package-lock.json +2892 -0
  78. package/skills/deploy-k8s/docs/package.json +13 -0
  79. package/skills/deploy-k8s/references/api-drift.md +298 -0
  80. package/skills/deploy-k8s/references/conditional/aks-patterns.md +70 -0
  81. package/skills/deploy-k8s/references/conditional/eks-patterns.md +79 -0
  82. package/skills/deploy-k8s/references/conditional/gitops-controllers.md +71 -0
  83. package/skills/deploy-k8s/references/conditional/gke-patterns.md +74 -0
  84. package/skills/deploy-k8s/references/conditional/observability-stacks.md +80 -0
  85. package/skills/deploy-k8s/references/conditional/openshift-patterns.md +67 -0
  86. package/skills/deploy-k8s/references/daemonset-operator-patterns.md +155 -0
  87. package/skills/deploy-k8s/references/deployment-patterns.md +146 -0
  88. package/skills/deploy-k8s/references/do-dont-patterns.md +87 -0
  89. package/skills/deploy-k8s/references/examples-bad.md +282 -0
  90. package/skills/deploy-k8s/references/examples-good.md +440 -0
  91. package/skills/deploy-k8s/references/fragile-rollouts.md +303 -0
  92. package/skills/deploy-k8s/references/helm-patterns.md +203 -0
  93. package/skills/deploy-k8s/references/insecure-workload-defaults.md +300 -0
  94. package/skills/deploy-k8s/references/job-patterns.md +120 -0
  95. package/skills/deploy-k8s/references/kustomize-patterns.md +239 -0
  96. package/skills/deploy-k8s/references/multi-tenancy.md +343 -0
  97. package/skills/deploy-k8s/references/network-exposure.md +481 -0
  98. package/skills/deploy-k8s/references/observability.md +302 -0
  99. package/skills/deploy-k8s/references/privilege-sprawl.md +273 -0
  100. package/skills/deploy-k8s/references/resource-starvation.md +374 -0
  101. package/skills/deploy-k8s/references/security-hardening.md +209 -0
  102. package/skills/deploy-k8s/references/stateful-patterns.md +130 -0
  103. package/skills/deploy-k8s/references/storage-and-state.md +330 -0
  104. package/skills/deploy-k8s/references/validation-and-policy.md +242 -0
  105. package/skills/deploy-railway/SKILL.md +235 -0
  106. package/skills/deploy-railway/references/analyze-db-mongo.md +84 -0
  107. package/skills/deploy-railway/references/analyze-db-mysql.md +254 -0
  108. package/skills/deploy-railway/references/analyze-db-postgres.md +479 -0
  109. package/skills/deploy-railway/references/analyze-db-redis.md +208 -0
  110. package/skills/deploy-railway/references/analyze-db.md +344 -0
  111. package/skills/deploy-railway/references/configure.md +309 -0
  112. package/skills/deploy-railway/references/deploy.md +195 -0
  113. package/skills/deploy-railway/references/operate.md +214 -0
  114. package/skills/deploy-railway/references/request.md +248 -0
  115. package/skills/deploy-railway/references/setup.md +312 -0
  116. package/skills/deploy-railway/scripts/analyze-mongo.py +1549 -0
  117. package/skills/deploy-railway/scripts/analyze-mysql.py +1195 -0
  118. package/skills/deploy-railway/scripts/analyze-postgres.py +3058 -0
  119. package/skills/deploy-railway/scripts/analyze-redis.py +1090 -0
  120. package/skills/deploy-railway/scripts/dal.py +671 -0
  121. package/skills/deploy-railway/scripts/enable-pg-stats.py +170 -0
  122. package/skills/deploy-railway/scripts/pg-extensions.py +370 -0
  123. package/skills/deploy-railway/scripts/railway-api.sh +52 -0
  124. package/skills/deploy-ssh/SKILL.md +91 -0
  125. package/skills/deploy-vercel/SKILL.md +304 -0
  126. package/skills/deploy-vercel/resources/deploy-codex.sh +301 -0
  127. package/skills/deploy-vercel/resources/deploy.sh +301 -0
  128. package/skills/docs-runbooks/SKILL.md +399 -0
  129. package/skills/drive-status-renderer/SKILL.md +62 -0
  130. package/skills/iac-scan/SKILL.md +680 -0
  131. package/skills/iac-scan/assets/.gitkeep +9 -0
  132. package/skills/iac-scan/assets/checkov_config.yaml +94 -0
  133. package/skills/iac-scan/assets/github_actions.yml +199 -0
  134. package/skills/iac-scan/assets/gitlab_ci.yml +218 -0
  135. package/skills/iac-scan/assets/pre_commit_config.yaml +92 -0
  136. package/skills/iac-scan/references/EXAMPLE.md +40 -0
  137. package/skills/iac-scan/references/compliance_mapping.md +237 -0
  138. package/skills/iac-scan/references/custom_policies.md +460 -0
  139. package/skills/iac-scan/references/suppression_guide.md +431 -0
  140. package/skills/incident-briefing/SKILL.md +66 -0
  141. package/skills/incident-triage/SKILL.md +481 -0
  142. package/{LICENSE → skills/mcp-builder/LICENSE.txt} +15 -14
  143. package/skills/mcp-builder/SKILL.md +244 -0
  144. package/skills/mcp-builder/reference/evaluation.md +602 -0
  145. package/skills/mcp-builder/reference/mcp_best_practices.md +249 -0
  146. package/skills/mcp-builder/reference/node_mcp_server.md +970 -0
  147. package/skills/mcp-builder/reference/python_mcp_server.md +719 -0
  148. package/skills/mcp-builder/scripts/connections.py +151 -0
  149. package/skills/mcp-builder/scripts/evaluation.py +373 -0
  150. package/skills/mcp-builder/scripts/example_evaluation.xml +22 -0
  151. package/skills/mcp-builder/scripts/requirements.txt +2 -0
  152. package/skills/mobile-pairing/SKILL.md +52 -0
  153. package/skills/ops-sre/SKILL.md +297 -0
  154. package/skills/playwright-qa/LICENSE.txt +201 -0
  155. package/skills/playwright-qa/NOTICE.txt +14 -0
  156. package/skills/playwright-qa/SKILL.md +156 -0
  157. package/skills/playwright-qa/agents/openai.yaml +6 -0
  158. package/skills/playwright-qa/assets/playwright-small.svg +3 -0
  159. package/skills/playwright-qa/assets/playwright.png +0 -0
  160. package/skills/playwright-qa/references/cli.md +116 -0
  161. package/skills/playwright-qa/references/workflows.md +95 -0
  162. package/skills/playwright-qa/scripts/playwright_cli.sh +25 -0
  163. package/skills/release-publish/SKILL.md +85 -0
  164. package/skills/repo-bootstrap/SKILL.md +92 -0
  165. package/skills/repo-bootstrap/assets/example-workflows/validate-agents.yml +89 -0
  166. package/skills/repo-bootstrap/assets/root-thin.md +141 -0
  167. package/skills/repo-bootstrap/assets/root-verbose.md +149 -0
  168. package/skills/repo-bootstrap/assets/scoped/backend-go.md +107 -0
  169. package/skills/repo-bootstrap/assets/scoped/backend-php.md +94 -0
  170. package/skills/repo-bootstrap/assets/scoped/backend-python.md +84 -0
  171. package/skills/repo-bootstrap/assets/scoped/backend-typescript.md +89 -0
  172. package/skills/repo-bootstrap/assets/scoped/claude-code-skill.md +101 -0
  173. package/skills/repo-bootstrap/assets/scoped/cli.md +83 -0
  174. package/skills/repo-bootstrap/assets/scoped/concourse.md +196 -0
  175. package/skills/repo-bootstrap/assets/scoped/ddev.md +68 -0
  176. package/skills/repo-bootstrap/assets/scoped/docker.md +160 -0
  177. package/skills/repo-bootstrap/assets/scoped/documentation.md +98 -0
  178. package/skills/repo-bootstrap/assets/scoped/examples.md +96 -0
  179. package/skills/repo-bootstrap/assets/scoped/frontend-typescript.md +88 -0
  180. package/skills/repo-bootstrap/assets/scoped/github-actions.md +174 -0
  181. package/skills/repo-bootstrap/assets/scoped/gitlab-ci.md +174 -0
  182. package/skills/repo-bootstrap/assets/scoped/oro-bundle.md +209 -0
  183. package/skills/repo-bootstrap/assets/scoped/oro-project.md +170 -0
  184. package/skills/repo-bootstrap/assets/scoped/python-modern.md +170 -0
  185. package/skills/repo-bootstrap/assets/scoped/resources.md +96 -0
  186. package/skills/repo-bootstrap/assets/scoped/skill-repo.md +139 -0
  187. package/skills/repo-bootstrap/assets/scoped/symfony.md +168 -0
  188. package/skills/repo-bootstrap/assets/scoped/testing.md +87 -0
  189. package/skills/repo-bootstrap/assets/scoped/typo3-docs.md +103 -0
  190. package/skills/repo-bootstrap/assets/scoped/typo3-extension.md +133 -0
  191. package/skills/repo-bootstrap/assets/scoped/typo3-project.md +137 -0
  192. package/skills/repo-bootstrap/assets/scoped/typo3-testing.md +80 -0
  193. package/skills/repo-bootstrap/checkpoints.yaml +279 -0
  194. package/skills/repo-bootstrap/evals/evals.json +385 -0
  195. package/skills/repo-bootstrap/references/ai-contribution-guidelines.md +63 -0
  196. package/skills/repo-bootstrap/references/ai-tool-compatibility.md +223 -0
  197. package/skills/repo-bootstrap/references/directory-coverage.md +82 -0
  198. package/skills/repo-bootstrap/references/examples/coding-agent-cli/AGENTS.md +70 -0
  199. package/skills/repo-bootstrap/references/examples/coding-agent-cli/go.mod +3 -0
  200. package/skills/repo-bootstrap/references/examples/coding-agent-cli/scripts-AGENTS.md +389 -0
  201. package/skills/repo-bootstrap/references/examples/express-api-ts/.env.example +13 -0
  202. package/skills/repo-bootstrap/references/examples/express-api-ts/AGENTS.md +91 -0
  203. package/skills/repo-bootstrap/references/examples/express-api-ts/package.json +33 -0
  204. package/skills/repo-bootstrap/references/examples/express-api-ts/pnpm-lock.yaml +3 -0
  205. package/skills/repo-bootstrap/references/examples/express-api-ts/src/AGENTS.md +91 -0
  206. package/skills/repo-bootstrap/references/examples/express-api-ts/src/config.ts +28 -0
  207. package/skills/repo-bootstrap/references/examples/express-api-ts/src/controllers/userController.ts +74 -0
  208. package/skills/repo-bootstrap/references/examples/express-api-ts/src/index.ts +26 -0
  209. package/skills/repo-bootstrap/references/examples/express-api-ts/src/middleware/errorHandler.ts +45 -0
  210. package/skills/repo-bootstrap/references/examples/express-api-ts/src/middleware/requestLogger.ts +18 -0
  211. package/skills/repo-bootstrap/references/examples/express-api-ts/src/routes/health.ts +18 -0
  212. package/skills/repo-bootstrap/references/examples/express-api-ts/src/routes/users.ts +13 -0
  213. package/skills/repo-bootstrap/references/examples/express-api-ts/src/utils/errors.ts +40 -0
  214. package/skills/repo-bootstrap/references/examples/express-api-ts/src/utils/logger.ts +14 -0
  215. package/skills/repo-bootstrap/references/examples/express-api-ts/tsconfig.json +24 -0
  216. package/skills/repo-bootstrap/references/examples/fastapi-app/.env.example +19 -0
  217. package/skills/repo-bootstrap/references/examples/fastapi-app/AGENTS.md +92 -0
  218. package/skills/repo-bootstrap/references/examples/fastapi-app/pyproject.toml +88 -0
  219. package/skills/repo-bootstrap/references/examples/fastapi-app/src/AGENTS.md +85 -0
  220. package/skills/repo-bootstrap/references/examples/fastapi-app/src/__init__.py +3 -0
  221. package/skills/repo-bootstrap/references/examples/fastapi-app/src/config.py +49 -0
  222. package/skills/repo-bootstrap/references/examples/fastapi-app/src/main.py +66 -0
  223. package/skills/repo-bootstrap/references/examples/fastapi-app/src/models/__init__.py +13 -0
  224. package/skills/repo-bootstrap/references/examples/fastapi-app/src/models/item.py +43 -0
  225. package/skills/repo-bootstrap/references/examples/fastapi-app/src/models/user.py +40 -0
  226. package/skills/repo-bootstrap/references/examples/fastapi-app/src/routes/__init__.py +5 -0
  227. package/skills/repo-bootstrap/references/examples/fastapi-app/src/routes/health.py +20 -0
  228. package/skills/repo-bootstrap/references/examples/fastapi-app/src/routes/items.py +61 -0
  229. package/skills/repo-bootstrap/references/examples/fastapi-app/src/routes/users.py +55 -0
  230. package/skills/repo-bootstrap/references/examples/fastapi-app/src/services/__init__.py +6 -0
  231. package/skills/repo-bootstrap/references/examples/fastapi-app/src/services/item_service.py +77 -0
  232. package/skills/repo-bootstrap/references/examples/fastapi-app/src/services/user_service.py +69 -0
  233. package/skills/repo-bootstrap/references/examples/fastapi-app/uv.lock +4 -0
  234. package/skills/repo-bootstrap/references/examples/go-api-with-react-admin/.scopes +3 -0
  235. package/skills/repo-bootstrap/references/examples/go-api-with-react-admin/AGENTS.md +86 -0
  236. package/skills/repo-bootstrap/references/examples/go-api-with-react-admin/admin/package.json +20 -0
  237. package/skills/repo-bootstrap/references/examples/go-api-with-react-admin/admin/src/App.tsx +5 -0
  238. package/skills/repo-bootstrap/references/examples/go-api-with-react-admin/cmd/api/main.go +7 -0
  239. package/skills/repo-bootstrap/references/examples/go-api-with-react-admin/go.mod +2 -0
  240. package/skills/repo-bootstrap/references/examples/go-api-with-react-admin/main.go +7 -0
  241. package/skills/repo-bootstrap/references/examples/go-with-internal-web-tsx/.scopes +3 -0
  242. package/skills/repo-bootstrap/references/examples/go-with-internal-web-tsx/AGENTS.md +89 -0
  243. package/skills/repo-bootstrap/references/examples/go-with-internal-web-tsx/go.mod +2 -0
  244. package/skills/repo-bootstrap/references/examples/go-with-internal-web-tsx/internal/web/AGENTS.md +90 -0
  245. package/skills/repo-bootstrap/references/examples/go-with-internal-web-tsx/internal/web/package.json +17 -0
  246. package/skills/repo-bootstrap/references/examples/go-with-internal-web-tsx/internal/web/src/App.tsx +1 -0
  247. package/skills/repo-bootstrap/references/examples/go-with-internal-web-tsx/internal/web/src/Button.tsx +1 -0
  248. package/skills/repo-bootstrap/references/examples/go-with-internal-web-tsx/internal/web/src/Footer.tsx +1 -0
  249. package/skills/repo-bootstrap/references/examples/go-with-internal-web-tsx/internal/web/src/Header.tsx +1 -0
  250. package/skills/repo-bootstrap/references/examples/go-with-internal-web-tsx/internal/web/src/Sidebar.tsx +1 -0
  251. package/skills/repo-bootstrap/references/examples/go-with-internal-web-tsx/main.go +7 -0
  252. package/skills/repo-bootstrap/references/examples/go-with-internal-web-tsx/package-lock.json +0 -0
  253. package/skills/repo-bootstrap/references/examples/go-with-internal-web-tsx/package.json +12 -0
  254. package/skills/repo-bootstrap/references/examples/ldap-selfservice/AGENTS.md +70 -0
  255. package/skills/repo-bootstrap/references/examples/ldap-selfservice/go.mod +3 -0
  256. package/skills/repo-bootstrap/references/examples/ldap-selfservice/internal-AGENTS.md +371 -0
  257. package/skills/repo-bootstrap/references/examples/ldap-selfservice/internal-web-AGENTS.md +448 -0
  258. package/skills/repo-bootstrap/references/examples/php-with-frontend/.scopes +3 -0
  259. package/skills/repo-bootstrap/references/examples/php-with-frontend/AGENTS.md +91 -0
  260. package/skills/repo-bootstrap/references/examples/php-with-frontend/composer.json +8 -0
  261. package/skills/repo-bootstrap/references/examples/php-with-frontend/package.json +15 -0
  262. package/skills/repo-bootstrap/references/examples/php-with-frontend/pnpm-lock.yaml +0 -0
  263. package/skills/repo-bootstrap/references/examples/php-with-frontend/src/Controller.php +3 -0
  264. package/skills/repo-bootstrap/references/examples/php-with-frontend/web/AGENTS.md +92 -0
  265. package/skills/repo-bootstrap/references/examples/php-with-frontend/web/package.json +26 -0
  266. package/skills/repo-bootstrap/references/examples/php-with-frontend/web/src/App.tsx +3 -0
  267. package/skills/repo-bootstrap/references/examples/php-with-frontend/web/src/Button.tsx +10 -0
  268. package/skills/repo-bootstrap/references/examples/php-with-frontend/web/src/Footer.tsx +9 -0
  269. package/skills/repo-bootstrap/references/examples/php-with-frontend/web/src/Header.tsx +9 -0
  270. package/skills/repo-bootstrap/references/examples/php-with-frontend/web/src/main.tsx +3 -0
  271. package/skills/repo-bootstrap/references/examples/php-with-frontend/web/tsconfig.json +13 -0
  272. package/skills/repo-bootstrap/references/examples/pnpm-workspace/AGENTS.md +75 -0
  273. package/skills/repo-bootstrap/references/examples/pnpm-workspace/package.json +7 -0
  274. package/skills/repo-bootstrap/references/examples/pnpm-workspace/packages/web/package.json +11 -0
  275. package/skills/repo-bootstrap/references/examples/pnpm-workspace/packages/web/src/index.ts +11 -0
  276. package/skills/repo-bootstrap/references/examples/pnpm-workspace/pnpm-lock.yaml +42 -0
  277. package/skills/repo-bootstrap/references/examples/pnpm-workspace/pnpm-workspace.yaml +2 -0
  278. package/skills/repo-bootstrap/references/examples/simple-ldap-go/AGENTS.md +70 -0
  279. package/skills/repo-bootstrap/references/examples/simple-ldap-go/examples-AGENTS.md +45 -0
  280. package/skills/repo-bootstrap/references/examples/simple-ldap-go/go.mod +3 -0
  281. package/skills/repo-bootstrap/references/examples/t3x-rte-ckeditor-image/AGENTS.md +70 -0
  282. package/skills/repo-bootstrap/references/examples/t3x-rte-ckeditor-image/Classes-AGENTS.md +392 -0
  283. package/skills/repo-bootstrap/references/examples/t3x-rte-ckeditor-image/composer.json +8 -0
  284. package/skills/repo-bootstrap/references/feedback-memory-schema.md +135 -0
  285. package/skills/repo-bootstrap/references/git-hooks-setup.md +79 -0
  286. package/skills/repo-bootstrap/references/output-structure.md +124 -0
  287. package/skills/repo-bootstrap/references/scripts-guide.md +175 -0
  288. package/skills/repo-bootstrap/references/verification-guide.md +137 -0
  289. package/skills/repo-bootstrap/scripts/analyze-git-history.sh +315 -0
  290. package/skills/repo-bootstrap/scripts/check-freshness.sh +230 -0
  291. package/skills/repo-bootstrap/scripts/detect-golden-samples.sh +161 -0
  292. package/skills/repo-bootstrap/scripts/detect-heuristics.sh +93 -0
  293. package/skills/repo-bootstrap/scripts/detect-project.sh +486 -0
  294. package/skills/repo-bootstrap/scripts/detect-scopes.sh +330 -0
  295. package/skills/repo-bootstrap/scripts/detect-utilities.sh +133 -0
  296. package/skills/repo-bootstrap/scripts/extract-adrs.sh +194 -0
  297. package/skills/repo-bootstrap/scripts/extract-agent-configs.sh +331 -0
  298. package/skills/repo-bootstrap/scripts/extract-architecture-rules.sh +522 -0
  299. package/skills/repo-bootstrap/scripts/extract-ci-commands.sh +385 -0
  300. package/skills/repo-bootstrap/scripts/extract-ci-rules.sh +384 -0
  301. package/skills/repo-bootstrap/scripts/extract-commands.sh +358 -0
  302. package/skills/repo-bootstrap/scripts/extract-documentation.sh +308 -0
  303. package/skills/repo-bootstrap/scripts/extract-github-rulesets.sh +96 -0
  304. package/skills/repo-bootstrap/scripts/extract-github-settings.sh +88 -0
  305. package/skills/repo-bootstrap/scripts/extract-ide-settings.sh +228 -0
  306. package/skills/repo-bootstrap/scripts/extract-platform-files.sh +290 -0
  307. package/skills/repo-bootstrap/scripts/extract-quality-configs.sh +442 -0
  308. package/skills/repo-bootstrap/scripts/generate-agents.sh +2424 -0
  309. package/skills/repo-bootstrap/scripts/generate-file-map.sh +153 -0
  310. package/skills/repo-bootstrap/scripts/lib/config-root.sh +211 -0
  311. package/skills/repo-bootstrap/scripts/lib/summary.sh +244 -0
  312. package/skills/repo-bootstrap/scripts/lib/template.sh +397 -0
  313. package/skills/repo-bootstrap/scripts/validate-structure.sh +324 -0
  314. package/skills/repo-bootstrap/scripts/verify-commands.sh +615 -0
  315. package/skills/repo-bootstrap/scripts/verify-content.sh +302 -0
  316. package/skills/schema-api-contracts/SKILL.md +56 -0
  317. package/skills/secret-hygiene/SKILL.md +511 -0
  318. package/skills/secret-hygiene/assets/.gitkeep +9 -0
  319. package/skills/secret-hygiene/assets/config-balanced.toml +81 -0
  320. package/skills/secret-hygiene/assets/config-custom.toml +178 -0
  321. package/skills/secret-hygiene/assets/config-strict.toml +48 -0
  322. package/skills/secret-hygiene/assets/github-action.yml +181 -0
  323. package/skills/secret-hygiene/assets/gitlab-ci.yml +257 -0
  324. package/skills/secret-hygiene/assets/precommit-config.yaml +70 -0
  325. package/skills/secret-hygiene/references/EXAMPLE.md +40 -0
  326. package/skills/secret-hygiene/references/compliance_mapping.md +538 -0
  327. package/skills/secret-hygiene/references/detection_rules.md +276 -0
  328. package/skills/secret-hygiene/references/false_positives.md +598 -0
  329. package/skills/secret-hygiene/references/remediation_guide.md +530 -0
  330. package/skills/stack-selector/SKILL.md +56 -0
  331. package/skills/telegram-control/SKILL.md +110 -0
  332. package/skills/telegram-control/references/architecture.md +184 -0
  333. package/skills/telegram-control/references/convex.md +173 -0
  334. package/skills/telegram-control/references/error_handling.md +212 -0
  335. package/skills/telegram-control/references/initial_setup.md +165 -0
  336. package/skills/telegram-control/references/telegram_api.md +156 -0
  337. package/skills/telegram-control/scripts/cancel_message.ts +53 -0
  338. package/skills/telegram-control/scripts/list_scheduled.ts +103 -0
  339. package/skills/telegram-control/scripts/logger.ts +121 -0
  340. package/skills/telegram-control/scripts/proxy-util.ts +11 -0
  341. package/skills/telegram-control/scripts/schedule_message.ts +216 -0
  342. package/skills/telegram-control/scripts/send_message.ts +115 -0
  343. package/skills/telegram-control/scripts/setup.ts +185 -0
  344. package/skills/telegram-control/scripts/types.ts +75 -0
  345. package/skills/telegram-control/scripts/view_history.ts +74 -0
  346. package/skills/test-strategy/SKILL.md +352 -0
  347. package/skills/threat-model/SKILL.md +303 -0
  348. package/skills/threat-model/examples/example-output.md +196 -0
  349. package/skills/threat-model/template.md +96 -0
  350. package/skills/ts-lint/SKILL.md +80 -0
  351. package/skills/ui-flow/SKILL.md +668 -0
  352. package/skills/voice-command-router/SKILL.md +51 -0
  353. package/skills/widget-live-activity-sync/SKILL.md +66 -0
@@ -0,0 +1,1195 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ MySQL analysis for Railway deployments.
4
+
5
+ Produces a comprehensive report covering:
6
+ - Deployment status & resource metrics (CPU, memory, disk)
7
+ - Connection overview
8
+ - Query throughput & efficiency
9
+ - InnoDB buffer pool & row operations
10
+ - Lock contention
11
+ - Top queries (from performance_schema)
12
+ - Table sizes
13
+ - Active processes
14
+ - Recommendations
15
+
16
+ Usage:
17
+ analyze-mysql.py --service <name>
18
+ analyze-mysql.py --service <name> --json
19
+ analyze-mysql.py --service <name> --step ssh-test
20
+ """
21
+
22
+ import argparse
23
+ import json
24
+ import os
25
+ import re
26
+ import subprocess
27
+ import sys
28
+ from concurrent.futures import ThreadPoolExecutor
29
+ from dataclasses import asdict, dataclass, field
30
+ from datetime import datetime, timezone
31
+ from typing import Any, Dict, List, Optional, Tuple
32
+
33
+ import dal
34
+ from dal import (
35
+ LOG_LINES_DEFAULT, ProgressTimer, RailwayContext,
36
+ _init_context, progress, run_railway_command, run_ssh_query,
37
+ get_railway_status, get_deployment_status,
38
+ get_all_metrics_from_api, _analyze_window, _build_metrics_history,
39
+ get_recent_logs,
40
+ _safe_int, _safe_float, _format_uptime, _trend_indicator,
41
+ )
42
+
43
+
44
+ # ---------------------------------------------------------------------------
45
+ # Result container
46
+ # ---------------------------------------------------------------------------
47
+
48
+ @dataclass
49
+ class MySQLAnalysisResult:
50
+ """Container for MySQL analysis results."""
51
+ service: str
52
+ db_type: str
53
+ timestamp: str
54
+ deployment_status: str = "UNKNOWN"
55
+
56
+ # Resource metrics from Railway API
57
+ disk_usage: Optional[Dict[str, Any]] = None
58
+ cpu_memory: Optional[Dict[str, Any]] = None
59
+ metrics_history: Optional[Dict[str, Any]] = None
60
+
61
+ # MySQL data
62
+ overview: Optional[Dict[str, Any]] = None
63
+ query_throughput: Optional[Dict[str, Any]] = None
64
+ innodb_row_ops: Optional[Dict[str, Any]] = None
65
+ query_efficiency: Optional[Dict[str, Any]] = None
66
+ innodb_buffer_pool: Optional[Dict[str, Any]] = None
67
+ innodb_io: Optional[Dict[str, Any]] = None
68
+ network: Optional[Dict[str, Any]] = None
69
+ locks: Optional[Dict[str, Any]] = None
70
+ table_cache: Optional[Dict[str, Any]] = None
71
+ top_queries: List[Dict[str, Any]] = field(default_factory=list)
72
+ top_queries_status: Optional[str] = None
73
+ tables: List[Dict[str, Any]] = field(default_factory=list)
74
+ active_processes: List[Dict[str, Any]] = field(default_factory=list)
75
+
76
+ # Logs
77
+ recent_logs: List[str] = field(default_factory=list)
78
+ recent_errors: List[str] = field(default_factory=list)
79
+
80
+ # Metadata
81
+ collection_status: Dict[str, Dict[str, Any]] = field(default_factory=dict)
82
+ errors: List[str] = field(default_factory=list)
83
+ recommendations: List[Dict[str, str]] = field(default_factory=list)
84
+
85
+
86
+ # ---------------------------------------------------------------------------
87
+ # MySQL-specific helpers
88
+ # ---------------------------------------------------------------------------
89
+
90
+ def run_mysql_query(service: str, query: str, timeout: int = 30) -> Tuple[int, str]:
91
+ """Run a MySQL query via SSH and return (returncode, output).
92
+
93
+ Uses -B (batch) mode which produces tab-separated output with headers.
94
+ Filters out the mysql CLI password warning.
95
+ """
96
+ import base64
97
+ query = " ".join(query.split())
98
+ # Base64-encode the query to avoid all shell quoting issues
99
+ # (single quotes in SQL IN clauses break bash -c '...' wrapping)
100
+ encoded = base64.b64encode(query.encode()).decode()
101
+ command = (
102
+ f'''bash +H -c 'echo {encoded} | base64 -d | MYSQL_PWD="$MYSQLPASSWORD" mysql -h localhost -P 3306 '''
103
+ f'''-u "$MYSQLUSER" -D "$MYSQLDATABASE" --default-character-set=utf8mb4 '''
104
+ f'''-B' '''
105
+ )
106
+ code, stdout, stderr = run_ssh_query(service, command, timeout)
107
+ # Filter out the password warning from stdout (mysql sometimes writes it there)
108
+ lines = []
109
+ for line in stdout.split("\n"):
110
+ if "Using a password on the command line" in line:
111
+ continue
112
+ lines.append(line)
113
+ stdout = "\n".join(lines)
114
+ if code != 0:
115
+ # Also filter warning from stderr
116
+ stderr_clean = "\n".join(
117
+ l for l in stderr.split("\n")
118
+ if "Using a password on the command line" not in l
119
+ )
120
+ return code, stderr_clean or stdout
121
+ return 0, stdout
122
+
123
+
124
+ def parse_mysql_batch(output: str) -> List[Dict[str, str]]:
125
+ """Parse MySQL -B (batch/tab-separated) output into list of dicts.
126
+
127
+ First line is column headers, subsequent lines are values.
128
+ """
129
+ lines = [l for l in output.strip().split("\n") if l.strip()]
130
+ if len(lines) < 1:
131
+ return []
132
+ headers = lines[0].split("\t")
133
+ rows = []
134
+ for line in lines[1:]:
135
+ values = line.split("\t")
136
+ if len(values) == len(headers):
137
+ rows.append(dict(zip(headers, values)))
138
+ return rows
139
+
140
+
141
+ def parse_mysql_kv(output: str) -> Dict[str, str]:
142
+ """Parse MySQL SHOW output (Variable_name / Value pairs) into a dict."""
143
+ rows = parse_mysql_batch(output)
144
+ result: Dict[str, str] = {}
145
+ for row in rows:
146
+ name = row.get("Variable_name", "")
147
+ value = row.get("Value", "")
148
+ if name:
149
+ result[name] = value
150
+ return result
151
+
152
+
153
+ # ---------------------------------------------------------------------------
154
+ # Railway context / status / metrics helpers
155
+ # ---------------------------------------------------------------------------
156
+
157
+ # ---------------------------------------------------------------------------
158
+ # MySQL queries
159
+ # ---------------------------------------------------------------------------
160
+
161
+ QUERY_GLOBAL_STATUS = """SHOW GLOBAL STATUS WHERE Variable_name IN ('Threads_connected','Threads_running','Max_used_connections','Questions','Slow_queries','Com_select','Com_insert','Com_update','Com_delete','Innodb_buffer_pool_read_requests','Innodb_buffer_pool_reads','Innodb_buffer_pool_pages_data','Innodb_buffer_pool_pages_free','Innodb_buffer_pool_pages_dirty','Innodb_row_lock_waits','Innodb_row_lock_time','Uptime','Bytes_received','Bytes_sent','Connections','Aborted_clients','Aborted_connects','Innodb_rows_read','Innodb_rows_inserted','Innodb_rows_updated','Innodb_rows_deleted','Innodb_data_reads','Innodb_data_writes','Innodb_buffer_pool_bytes_data','Innodb_buffer_pool_bytes_dirty','Created_tmp_disk_tables','Created_tmp_tables','Handler_read_rnd_next','Handler_read_first','Handler_read_key','Select_full_join','Select_range','Sort_merge_passes','Table_locks_waited','Table_locks_immediate','Open_tables','Opened_tables')"""
162
+
163
+ QUERY_VARIABLES = """SHOW VARIABLES WHERE Variable_name IN ('max_connections','innodb_buffer_pool_size','long_query_time','version','table_open_cache','performance_schema')"""
164
+
165
+ QUERY_TABLE_SIZES = """SELECT TABLE_NAME, TABLE_ROWS, DATA_LENGTH, INDEX_LENGTH, DATA_LENGTH + INDEX_LENGTH AS TOTAL_SIZE FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() ORDER BY TOTAL_SIZE DESC LIMIT 15"""
166
+
167
+ QUERY_PROCESSLIST = """SHOW PROCESSLIST"""
168
+
169
+ QUERY_TOP_QUERIES = """SELECT DIGEST, LEFT(DIGEST_TEXT, 200) AS DIGEST_TEXT, COUNT_STAR, ROUND(SUM_TIMER_WAIT/1000000000, 2) AS TOTAL_LATENCY_MS, ROUND(AVG_TIMER_WAIT/1000000000, 2) AS AVG_LATENCY_MS, SUM_ROWS_EXAMINED, SUM_ROWS_SENT, SUM_CREATED_TMP_DISK_TABLES, SUM_NO_INDEX_USED FROM performance_schema.events_statements_summary_by_digest WHERE DIGEST IS NOT NULL AND COUNT_STAR > 0 ORDER BY SUM_TIMER_WAIT DESC LIMIT 15"""
170
+
171
+
172
+ # ---------------------------------------------------------------------------
173
+ # MySQL data collection
174
+ # ---------------------------------------------------------------------------
175
+
176
+ def collect_mysql_data(service: str, timeout: int = 30) -> Dict[str, Any]:
177
+ """Collect all MySQL metrics via SSH.
178
+
179
+ Batches queries into two SSH calls for efficiency:
180
+ 1. SHOW GLOBAL STATUS + SHOW VARIABLES
181
+ 2. Table sizes + processlist + top queries (performance_schema)
182
+
183
+ Returns a dict with raw parsed data keyed by section.
184
+ """
185
+ data: Dict[str, Any] = {
186
+ "global_status": {},
187
+ "variables": {},
188
+ "tables": [],
189
+ "processlist": [],
190
+ "top_queries": [],
191
+ "errors": [],
192
+ }
193
+
194
+ # --- Batch 1: SHOW GLOBAL STATUS; SHOW VARIABLES ---
195
+ batch1_query = QUERY_GLOBAL_STATUS + "; " + QUERY_VARIABLES
196
+ code, output = run_mysql_query(service, batch1_query, timeout=timeout)
197
+ if code != 0:
198
+ data["errors"].append(f"Batch 1 (status/variables) failed: {output}")
199
+ else:
200
+ # MySQL concatenates the two result sets; split them by detecting
201
+ # a second header line starting with Variable_name
202
+ sections = _split_mysql_resultsets(output, "Variable_name")
203
+ if len(sections) >= 1:
204
+ data["global_status"] = parse_mysql_kv(sections[0])
205
+ if len(sections) >= 2:
206
+ data["variables"] = parse_mysql_kv(sections[1])
207
+
208
+ # --- Batch 2: tables + processlist + top queries ---
209
+ batch2_query = QUERY_TABLE_SIZES + "; " + QUERY_PROCESSLIST + "; " + QUERY_TOP_QUERIES
210
+ code, output = run_mysql_query(service, batch2_query, timeout=timeout)
211
+ if code != 0:
212
+ # Top queries may fail if performance_schema is off; try without
213
+ batch2_fallback = QUERY_TABLE_SIZES + "; " + QUERY_PROCESSLIST
214
+ code2, output2 = run_mysql_query(service, batch2_fallback, timeout=timeout)
215
+ if code2 != 0:
216
+ data["errors"].append(f"Batch 2 (tables/processlist) failed: {output}")
217
+ else:
218
+ sections = _split_mysql_resultsets_multi(output2, [
219
+ "TABLE_NAME",
220
+ "Id",
221
+ ])
222
+ if len(sections) >= 1:
223
+ data["tables"] = parse_mysql_batch(sections[0])
224
+ if len(sections) >= 2:
225
+ data["processlist"] = parse_mysql_batch(sections[1])
226
+ else:
227
+ sections = _split_mysql_resultsets_multi(output, [
228
+ "TABLE_NAME",
229
+ "Id",
230
+ "DIGEST",
231
+ ])
232
+ if len(sections) >= 1:
233
+ data["tables"] = parse_mysql_batch(sections[0])
234
+ if len(sections) >= 2:
235
+ data["processlist"] = parse_mysql_batch(sections[1])
236
+ if len(sections) >= 3:
237
+ data["top_queries"] = parse_mysql_batch(sections[2])
238
+
239
+ return data
240
+
241
+
242
+ def _split_mysql_resultsets(output: str, header_key: str) -> List[str]:
243
+ """Split concatenated MySQL batch output into sections by header line."""
244
+ lines = output.strip().split("\n")
245
+ sections: List[List[str]] = []
246
+ current: List[str] = []
247
+
248
+ for line in lines:
249
+ if line.startswith(header_key + "\t") or line.strip() == header_key:
250
+ if current:
251
+ sections.append("\n".join(current))
252
+ current = [line]
253
+ else:
254
+ current.append(line)
255
+ if current:
256
+ sections.append("\n".join(current))
257
+
258
+ return sections
259
+
260
+
261
+ def _split_mysql_resultsets_multi(output: str, header_keys: List[str]) -> List[str]:
262
+ """Split concatenated MySQL batch output into sections by multiple different header keys."""
263
+ lines = output.strip().split("\n")
264
+ sections: List[List[str]] = []
265
+ current: List[str] = []
266
+ expected_idx = 0
267
+
268
+ for line in lines:
269
+ # Check if this line starts a new result set
270
+ matched = False
271
+ if expected_idx < len(header_keys):
272
+ key = header_keys[expected_idx]
273
+ if line.startswith(key + "\t") or line.strip() == key:
274
+ if current:
275
+ sections.append("\n".join(current))
276
+ current = [line]
277
+ expected_idx += 1
278
+ matched = True
279
+ # Also check remaining headers in case we skipped one
280
+ if not matched:
281
+ for idx in range(expected_idx, len(header_keys)):
282
+ key = header_keys[idx]
283
+ if line.startswith(key + "\t") or line.strip() == key:
284
+ if current:
285
+ sections.append("\n".join(current))
286
+ current = [line]
287
+ expected_idx = idx + 1
288
+ matched = True
289
+ break
290
+ if not matched:
291
+ current.append(line)
292
+
293
+ if current:
294
+ sections.append("\n".join(current))
295
+
296
+ return sections
297
+
298
+
299
+ # ---------------------------------------------------------------------------
300
+ # Parse collected data into result
301
+ # ---------------------------------------------------------------------------
302
+
303
+ def parse_mysql_data(data: Dict[str, Any], result: MySQLAnalysisResult) -> None:
304
+ """Transform raw MySQL data into structured result sections."""
305
+ gs = data.get("global_status", {})
306
+ vs = data.get("variables", {})
307
+
308
+ # --- Overview ---
309
+ version = vs.get("version", "unknown")
310
+ uptime_sec = _safe_int(gs.get("Uptime"))
311
+ threads_connected = _safe_int(gs.get("Threads_connected"))
312
+ threads_running = _safe_int(gs.get("Threads_running"))
313
+ max_used_connections = _safe_int(gs.get("Max_used_connections"))
314
+ max_connections = _safe_int(vs.get("max_connections"), 1)
315
+ aborted_clients = _safe_int(gs.get("Aborted_clients"))
316
+ aborted_connects = _safe_int(gs.get("Aborted_connects"))
317
+ connection_usage_pct = round(max_used_connections / max_connections * 100, 1) if max_connections > 0 else 0
318
+
319
+ result.overview = {
320
+ "version": version,
321
+ "uptime_seconds": uptime_sec,
322
+ "uptime_human": _format_uptime(uptime_sec),
323
+ "threads_connected": threads_connected,
324
+ "threads_running": threads_running,
325
+ "max_used_connections": max_used_connections,
326
+ "max_connections": max_connections,
327
+ "connection_usage_percent": connection_usage_pct,
328
+ "aborted_clients": aborted_clients,
329
+ "aborted_connects": aborted_connects,
330
+ }
331
+
332
+ # --- Query Throughput ---
333
+ questions = _safe_int(gs.get("Questions"))
334
+ slow_queries = _safe_int(gs.get("Slow_queries"))
335
+ long_query_time = vs.get("long_query_time", "10")
336
+ com_select = _safe_int(gs.get("Com_select"))
337
+ com_insert = _safe_int(gs.get("Com_insert"))
338
+ com_update = _safe_int(gs.get("Com_update"))
339
+ com_delete = _safe_int(gs.get("Com_delete"))
340
+
341
+ result.query_throughput = {
342
+ "questions": questions,
343
+ "slow_queries": slow_queries,
344
+ "long_query_time": long_query_time,
345
+ "com_select": com_select,
346
+ "com_insert": com_insert,
347
+ "com_update": com_update,
348
+ "com_delete": com_delete,
349
+ }
350
+
351
+ # --- InnoDB Row Operations ---
352
+ result.innodb_row_ops = {
353
+ "rows_read": _safe_int(gs.get("Innodb_rows_read")),
354
+ "rows_inserted": _safe_int(gs.get("Innodb_rows_inserted")),
355
+ "rows_updated": _safe_int(gs.get("Innodb_rows_updated")),
356
+ "rows_deleted": _safe_int(gs.get("Innodb_rows_deleted")),
357
+ }
358
+
359
+ # --- Query Efficiency ---
360
+ created_tmp_disk = _safe_int(gs.get("Created_tmp_disk_tables"))
361
+ created_tmp = _safe_int(gs.get("Created_tmp_tables"))
362
+ tmp_disk_pct = round(created_tmp_disk / created_tmp * 100, 1) if created_tmp > 0 else 0
363
+ handler_rnd_next = _safe_int(gs.get("Handler_read_rnd_next"))
364
+ handler_first = _safe_int(gs.get("Handler_read_first"))
365
+ handler_key = _safe_int(gs.get("Handler_read_key"))
366
+ scan_total = handler_rnd_next + handler_first + handler_key
367
+ table_scan_pct = round((handler_rnd_next + handler_first) / scan_total * 100, 1) if scan_total > 0 else 0
368
+ select_full_join = _safe_int(gs.get("Select_full_join"))
369
+ select_range = _safe_int(gs.get("Select_range"))
370
+ sort_merge_passes = _safe_int(gs.get("Sort_merge_passes"))
371
+
372
+ result.query_efficiency = {
373
+ "created_tmp_disk_tables": created_tmp_disk,
374
+ "created_tmp_tables": created_tmp,
375
+ "tmp_disk_table_percent": tmp_disk_pct,
376
+ "handler_read_rnd_next": handler_rnd_next,
377
+ "handler_read_first": handler_first,
378
+ "handler_read_key": handler_key,
379
+ "table_scan_percent": table_scan_pct,
380
+ "select_full_join": select_full_join,
381
+ "select_range": select_range,
382
+ "sort_merge_passes": sort_merge_passes,
383
+ }
384
+
385
+ # --- InnoDB Buffer Pool ---
386
+ read_requests = _safe_int(gs.get("Innodb_buffer_pool_read_requests"))
387
+ reads = _safe_int(gs.get("Innodb_buffer_pool_reads"))
388
+ hit_ratio = round((read_requests - reads) / read_requests * 100, 2) if read_requests > 0 else 0
389
+ pool_size = _safe_int(vs.get("innodb_buffer_pool_size"))
390
+ bytes_data = _safe_int(gs.get("Innodb_buffer_pool_bytes_data"))
391
+ bytes_dirty = _safe_int(gs.get("Innodb_buffer_pool_bytes_dirty"))
392
+ usage_pct = round(bytes_data / pool_size * 100, 1) if pool_size > 0 else 0
393
+ pages_free = _safe_int(gs.get("Innodb_buffer_pool_pages_free"))
394
+ pages_data = _safe_int(gs.get("Innodb_buffer_pool_pages_data"))
395
+ pages_dirty = _safe_int(gs.get("Innodb_buffer_pool_pages_dirty"))
396
+
397
+ result.innodb_buffer_pool = {
398
+ "hit_ratio": hit_ratio,
399
+ "read_requests": read_requests,
400
+ "reads": reads,
401
+ "buffer_pool_size": pool_size,
402
+ "bytes_data": bytes_data,
403
+ "bytes_dirty": bytes_dirty,
404
+ "usage_percent": usage_pct,
405
+ "pages_data": pages_data,
406
+ "pages_free": pages_free,
407
+ "pages_dirty": pages_dirty,
408
+ }
409
+
410
+ # --- InnoDB I/O ---
411
+ result.innodb_io = {
412
+ "data_reads": _safe_int(gs.get("Innodb_data_reads")),
413
+ "data_writes": _safe_int(gs.get("Innodb_data_writes")),
414
+ }
415
+
416
+ # --- Network ---
417
+ result.network = {
418
+ "bytes_received": _safe_int(gs.get("Bytes_received")),
419
+ "bytes_sent": _safe_int(gs.get("Bytes_sent")),
420
+ }
421
+
422
+ # --- Locks ---
423
+ row_lock_waits = _safe_int(gs.get("Innodb_row_lock_waits"))
424
+ row_lock_time = _safe_int(gs.get("Innodb_row_lock_time"))
425
+ table_locks_waited = _safe_int(gs.get("Table_locks_waited"))
426
+ table_locks_immediate = _safe_int(gs.get("Table_locks_immediate"))
427
+ lock_total = table_locks_waited + table_locks_immediate
428
+ table_lock_contention = round(table_locks_waited / lock_total * 100, 2) if lock_total > 0 else 0
429
+
430
+ result.locks = {
431
+ "row_lock_waits": row_lock_waits,
432
+ "row_lock_time": row_lock_time,
433
+ "table_locks_waited": table_locks_waited,
434
+ "table_locks_immediate": table_locks_immediate,
435
+ "table_lock_contention": table_lock_contention,
436
+ }
437
+
438
+ # --- Table Cache ---
439
+ open_tables = _safe_int(gs.get("Open_tables"))
440
+ opened_tables = _safe_int(gs.get("Opened_tables"))
441
+ table_open_cache = _safe_int(vs.get("table_open_cache"))
442
+ cache_utilization_pct = round(open_tables / table_open_cache * 100, 1) if table_open_cache > 0 else 0
443
+ opens_per_sec = round(opened_tables / uptime_sec, 2) if uptime_sec > 0 else 0
444
+
445
+ result.table_cache = {
446
+ "open_tables": open_tables,
447
+ "opened_tables": opened_tables,
448
+ "table_open_cache": table_open_cache,
449
+ "cache_utilization_percent": cache_utilization_pct,
450
+ "opens_per_second": opens_per_sec,
451
+ }
452
+
453
+ # --- Top Queries ---
454
+ for row in data.get("top_queries", []):
455
+ result.top_queries.append({
456
+ "digest": row.get("DIGEST", ""),
457
+ "digest_text": row.get("DIGEST_TEXT", ""),
458
+ "count_star": _safe_int(row.get("COUNT_STAR")),
459
+ "total_latency_ms": _safe_float(row.get("TOTAL_LATENCY_MS")),
460
+ "avg_latency_ms": _safe_float(row.get("AVG_LATENCY_MS")),
461
+ "rows_examined": _safe_int(row.get("SUM_ROWS_EXAMINED")),
462
+ "rows_sent": _safe_int(row.get("SUM_ROWS_SENT")),
463
+ "tmp_disk_tables": _safe_int(row.get("SUM_CREATED_TMP_DISK_TABLES")),
464
+ "no_index_used": _safe_int(row.get("SUM_NO_INDEX_USED")),
465
+ })
466
+
467
+ if result.top_queries:
468
+ result.top_queries_status = "ok"
469
+ else:
470
+ perf_schema = vs.get("performance_schema", "").upper()
471
+ if perf_schema == "OFF":
472
+ result.top_queries_status = "performance_schema_disabled"
473
+ elif perf_schema == "ON":
474
+ result.top_queries_status = "no_queries_recorded"
475
+ else:
476
+ result.top_queries_status = "unknown"
477
+
478
+ # --- Tables ---
479
+ for row in data.get("tables", []):
480
+ result.tables.append({
481
+ "name": row.get("TABLE_NAME", ""),
482
+ "rows": _safe_int(row.get("TABLE_ROWS")),
483
+ "data_length": _safe_int(row.get("DATA_LENGTH")),
484
+ "index_length": _safe_int(row.get("INDEX_LENGTH")),
485
+ "total_size": _safe_int(row.get("TOTAL_SIZE")),
486
+ })
487
+
488
+ # --- Active Processes ---
489
+ for row in data.get("processlist", []):
490
+ result.active_processes.append({
491
+ "id": row.get("Id", ""),
492
+ "user": row.get("User", ""),
493
+ "db": row.get("db", ""),
494
+ "command": row.get("Command", ""),
495
+ "time": _safe_int(row.get("Time")),
496
+ "state": row.get("State", ""),
497
+ "info": row.get("Info", ""),
498
+ })
499
+
500
+
501
+ # ---------------------------------------------------------------------------
502
+ # Formatting helpers
503
+ # ---------------------------------------------------------------------------
504
+
505
+ def _format_count(n: int) -> str:
506
+ """Format large numbers with K/M/G suffix."""
507
+ if n >= 1_000_000_000:
508
+ return f"{n / 1_000_000_000:.1f}G"
509
+ if n >= 1_000_000:
510
+ return f"{n / 1_000_000:.1f}M"
511
+ if n >= 1_000:
512
+ return f"{n / 1_000:.1f}K"
513
+ return str(n)
514
+
515
+
516
+ def _format_bytes(b: int) -> str:
517
+ """Format bytes to human-readable."""
518
+ if b >= 1_073_741_824:
519
+ return f"{b / 1_073_741_824:.1f} GB"
520
+ if b >= 1_048_576:
521
+ return f"{b / 1_048_576:.1f} MB"
522
+ if b >= 1_024:
523
+ return f"{b / 1_024:.1f} KB"
524
+ return f"{b} B"
525
+
526
+
527
+ def _status_ok_warn_crit(value: float, warn_threshold: float, crit_threshold: float) -> str:
528
+ if value >= crit_threshold:
529
+ return "CRITICAL"
530
+ if value >= warn_threshold:
531
+ return "WARN"
532
+ return "OK"
533
+
534
+
535
+ # ---------------------------------------------------------------------------
536
+ # Recommendations
537
+ # ---------------------------------------------------------------------------
538
+
539
+ def generate_recommendations(result: MySQLAnalysisResult) -> List[Dict[str, str]]:
540
+ recs: List[Dict[str, str]] = []
541
+
542
+ # Collection failures — surface critical issues when SSH/introspection failed
543
+ if result.collection_status:
544
+ failed = {k: v for k, v in result.collection_status.items()
545
+ if v.get("status") in ("failed", "error")}
546
+ ssh_sources = {"mysql_query"}
547
+ ssh_failed = {k: v for k, v in failed.items() if k in ssh_sources}
548
+ if ssh_failed:
549
+ sources = ", ".join(ssh_failed.keys())
550
+ errors = "; ".join(v.get("error", "unknown") for v in ssh_failed.values())
551
+ recs.append({
552
+ "severity": "critical",
553
+ "category": "collection",
554
+ "message": f"SSH introspection failed — unable to collect {sources}. "
555
+ f"Error: {errors}. "
556
+ f"Analysis is incomplete: InnoDB buffer pool, query throughput, "
557
+ f"locks, and tuning parameters could not be evaluated.",
558
+ })
559
+
560
+ def rec(severity: str, message: str):
561
+ recs.append({"severity": severity, "message": message})
562
+
563
+ ov = result.overview or {}
564
+ qt = result.query_throughput or {}
565
+ qe = result.query_efficiency or {}
566
+ bp = result.innodb_buffer_pool or {}
567
+ lk = result.locks or {}
568
+ tc = result.table_cache or {}
569
+
570
+ # Connection usage
571
+ conn_pct = ov.get("connection_usage_percent", 0)
572
+ max_conn = ov.get("max_connections", 0)
573
+ if conn_pct >= 90:
574
+ rec("critical", f"Connection usage critical at {conn_pct}%. Approaching max_connections ({max_conn}).")
575
+ elif conn_pct >= 70:
576
+ rec("warning", f"Connection usage at {conn_pct}%. Consider increasing max_connections.")
577
+
578
+ # Buffer pool hit ratio
579
+ hit_ratio = bp.get("hit_ratio", 100)
580
+ if hit_ratio < 95:
581
+ rec("critical", f"Buffer pool hit ratio at {hit_ratio}%. Increase innodb_buffer_pool_size.")
582
+ elif hit_ratio < 99:
583
+ rec("warning", f"Buffer pool hit ratio at {hit_ratio}% -- room for improvement with more RAM.")
584
+
585
+ # Buffer pool usage
586
+ bp_usage = bp.get("usage_percent", 0)
587
+ if bp_usage > 95:
588
+ rec("warning", f"Buffer pool {bp_usage}% full. Data pages may be evicted under load.")
589
+
590
+ # Temp tables to disk
591
+ tmp_pct = qe.get("tmp_disk_table_percent", 0)
592
+ created_disk = qe.get("created_tmp_disk_tables", 0)
593
+ created_total = qe.get("created_tmp_tables", 0)
594
+ if tmp_pct > 25:
595
+ rec("warning", f"{tmp_pct}% of temp tables going to disk. Increase tmp_table_size/max_heap_table_size or optimize queries.")
596
+ elif tmp_pct > 10:
597
+ rec("info", f"Temp tables to disk at {tmp_pct}%. Watch for queries creating large temporary results.")
598
+
599
+ # Table scan ratio
600
+ scan_pct = qe.get("table_scan_percent", 0)
601
+ if scan_pct > 75:
602
+ rec("critical", f"Table scan ratio at {scan_pct}%. Most reads are full scans -- add indexes.")
603
+ elif scan_pct > 50:
604
+ rec("warning", f"Table scan ratio at {scan_pct}%. Consider indexing frequently queried columns.")
605
+
606
+ # Full joins
607
+ full_joins = qe.get("select_full_join", 0)
608
+ if full_joins > 100:
609
+ rec("warning", f"{_format_count(full_joins)} full joins detected. These scan entire tables -- add indexes to join columns.")
610
+
611
+ # Sort merge passes
612
+ sort_passes = qe.get("sort_merge_passes", 0)
613
+ if sort_passes > 0:
614
+ rec("info", f"Sort merge passes ({_format_count(sort_passes)}). Increase sort_buffer_size or optimize queries.")
615
+
616
+ # Row lock waits
617
+ row_lock_waits = lk.get("row_lock_waits", 0)
618
+ if row_lock_waits > 1000:
619
+ rec("warning", f"InnoDB row lock waits ({_format_count(row_lock_waits)}). Check for lock contention in concurrent writes.")
620
+ elif row_lock_waits > 0:
621
+ rec("info", f"InnoDB row lock waits ({_format_count(row_lock_waits)}). Check for lock contention in concurrent writes.")
622
+
623
+ # Table lock contention
624
+ tl_contention = lk.get("table_lock_contention", 0)
625
+ if tl_contention > 5:
626
+ rec("warning", f"Table lock contention at {tl_contention}%. May indicate MyISAM tables -- convert to InnoDB.")
627
+
628
+ # Slow queries
629
+ slow = qt.get("slow_queries", 0)
630
+ threshold = qt.get("long_query_time", "10")
631
+ if slow > 0:
632
+ rec("info", f"{_format_count(slow)} slow queries (threshold: {threshold}s). Review with performance_schema or slow query log.")
633
+
634
+ # Aborted clients
635
+ aborted_clients = ov.get("aborted_clients", 0)
636
+ if aborted_clients > 0:
637
+ rec("info", f"{_format_count(aborted_clients)} aborted clients. Applications may not be closing connections properly.")
638
+
639
+ # Aborted connects
640
+ aborted_connects = ov.get("aborted_connects", 0)
641
+ if aborted_connects > 0:
642
+ rec("info", f"{_format_count(aborted_connects)} aborted connection attempts. Check authentication issues or connection limits.")
643
+
644
+ # No index used in top queries
645
+ if result.top_queries:
646
+ no_index_count = sum(1 for q in result.top_queries if q.get("no_index_used", 0) > 0)
647
+ if no_index_count > 0:
648
+ rec("warning", f"Top queries using no index ({no_index_count} of {len(result.top_queries)}). Missing indexes are likely impacting performance.")
649
+
650
+ # Table cache
651
+ tc = result.table_cache or {}
652
+ opens_per_sec = tc.get("opens_per_second", 0)
653
+ cache_util = tc.get("cache_utilization_percent", 0)
654
+ if cache_util >= 95:
655
+ rec("warning", f"Table cache {cache_util}% full ({tc.get('open_tables')}/{tc.get('table_open_cache')}). Increase table_open_cache.")
656
+ if opens_per_sec > 5:
657
+ rec("warning", f"Table opens at {opens_per_sec}/sec — cache may be undersized. Increase table_open_cache.")
658
+
659
+ # Top queries diagnostic
660
+ if not result.top_queries:
661
+ if result.top_queries_status == "performance_schema_disabled":
662
+ pass # Off by default on Railway; overhead (~400MB+) is too high to recommend casually
663
+ elif result.top_queries_status == "no_queries_recorded":
664
+ rec("info", "performance_schema is ON but no queries recorded. Database may be idle or recently restarted.")
665
+
666
+ return recs
667
+
668
+
669
+ # ---------------------------------------------------------------------------
670
+ # Report formatter
671
+ # ---------------------------------------------------------------------------
672
+
673
+ def format_report(result: MySQLAnalysisResult) -> str:
674
+ lines: List[str] = []
675
+
676
+ def heading(title: str, level: int = 2):
677
+ prefix = "#" * level
678
+ lines.append(f"\n{prefix} {title}")
679
+
680
+ def table_row(*cells: str):
681
+ lines.append("| " + " | ".join(cells) + " |")
682
+
683
+ def table_sep(ncols: int):
684
+ lines.append("| " + " | ".join(["---"] * ncols) + " |")
685
+
686
+ # Title
687
+ lines.append(f"# MySQL Analysis: {result.service}")
688
+ lines.append(f"Timestamp: {result.timestamp}")
689
+ lines.append(f"Deployment: {result.deployment_status}")
690
+
691
+ # --- Resource Overview (from Railway API) ---
692
+ if result.cpu_memory or result.disk_usage:
693
+ heading("Resource Overview")
694
+ table_row("Metric", "Value")
695
+ table_sep(2)
696
+ if result.cpu_memory:
697
+ cm = result.cpu_memory
698
+ if "cpu_percent" in cm:
699
+ trend = _trend_indicator(result.metrics_history, "cpu")
700
+ lines.append(f"| CPU | {cm['cpu_percent']}% vCPU{trend} |")
701
+ if "memory_gb" in cm:
702
+ trend = _trend_indicator(result.metrics_history, "memory")
703
+ mem_str = f"{cm['memory_gb']} GB"
704
+ if "memory_limit_gb" in cm and cm["memory_limit_gb"] > 0:
705
+ pct = round(cm["memory_gb"] / cm["memory_limit_gb"] * 100, 1)
706
+ mem_str += f" / {cm['memory_limit_gb']} GB ({pct}%)"
707
+ lines.append(f"| Memory | {mem_str}{trend} |")
708
+ if result.disk_usage:
709
+ trend = _trend_indicator(result.metrics_history, "disk")
710
+ lines.append(f"| Disk | {result.disk_usage.get('used', 'N/A')}{trend} |")
711
+
712
+ # --- Overview ---
713
+ ov = result.overview
714
+ if ov:
715
+ heading("Overview")
716
+ table_row("Metric", "Value", "Status")
717
+ table_sep(3)
718
+ table_row("Version", str(ov["version"]), "")
719
+ table_row("Uptime", ov["uptime_human"], "")
720
+ conn_status = _status_ok_warn_crit(ov["connection_usage_percent"], 70, 90)
721
+ table_row(
722
+ "Connections",
723
+ f"{ov['connection_usage_percent']}% ({ov['max_used_connections']}/{ov['max_connections']})",
724
+ conn_status,
725
+ )
726
+ table_row("Threads Running", str(ov["threads_running"]), "")
727
+ aborted_c_status = "WARN" if ov["aborted_clients"] > 0 else "OK"
728
+ table_row("Aborted Clients", _format_count(ov["aborted_clients"]), aborted_c_status)
729
+ aborted_conn_status = "WARN" if ov["aborted_connects"] > 0 else "OK"
730
+ table_row("Aborted Connects", _format_count(ov["aborted_connects"]), aborted_conn_status)
731
+
732
+ # --- Query Throughput ---
733
+ qt = result.query_throughput
734
+ if qt:
735
+ heading("Query Throughput")
736
+ table_row("Metric", "Value", "Status")
737
+ table_sep(3)
738
+ table_row("Total Queries", _format_count(qt["questions"]), "")
739
+ slow_status = "WARN" if qt["slow_queries"] > 0 else "OK"
740
+ table_row("Slow Queries", f"{_format_count(qt['slow_queries'])} (> {qt['long_query_time']}s threshold)", slow_status)
741
+ table_row("SELECT", _format_count(qt["com_select"]), "")
742
+ table_row("INSERT", _format_count(qt["com_insert"]), "")
743
+ table_row("UPDATE", _format_count(qt["com_update"]), "")
744
+ table_row("DELETE", _format_count(qt["com_delete"]), "")
745
+
746
+ # --- InnoDB Row Operations ---
747
+ ro = result.innodb_row_ops
748
+ if ro:
749
+ heading("InnoDB Row Operations")
750
+ table_row("Operation", "Count")
751
+ table_sep(2)
752
+ table_row("Rows Read", _format_count(ro["rows_read"]))
753
+ table_row("Rows Inserted", _format_count(ro["rows_inserted"]))
754
+ table_row("Rows Updated", _format_count(ro["rows_updated"]))
755
+ table_row("Rows Deleted", _format_count(ro["rows_deleted"]))
756
+
757
+ # --- Query Efficiency ---
758
+ qe = result.query_efficiency
759
+ if qe:
760
+ heading("Query Efficiency")
761
+ table_row("Metric", "Value", "Status")
762
+ table_sep(3)
763
+ tmp_status = _status_ok_warn_crit(qe["tmp_disk_table_percent"], 10, 25)
764
+ table_row(
765
+ "Temp Tables to Disk",
766
+ f"{qe['tmp_disk_table_percent']}% ({_format_count(qe['created_tmp_disk_tables'])}/{_format_count(qe['created_tmp_tables'])})",
767
+ tmp_status,
768
+ )
769
+ scan_status = _status_ok_warn_crit(qe["table_scan_percent"], 50, 75)
770
+ table_row("Table Scan Ratio", f"{qe['table_scan_percent']}%", scan_status)
771
+ fj_status = "WARN" if qe["select_full_join"] > 100 else "OK"
772
+ table_row("Full Joins", _format_count(qe["select_full_join"]), fj_status)
773
+ table_row("Sort Merge Passes", _format_count(qe["sort_merge_passes"]), "")
774
+
775
+ # --- InnoDB Buffer Pool ---
776
+ bp = result.innodb_buffer_pool
777
+ if bp:
778
+ heading("InnoDB Buffer Pool")
779
+ table_row("Metric", "Value", "Status")
780
+ table_sep(3)
781
+ hr_status = _status_ok_warn_crit(100 - bp["hit_ratio"], 1, 5) # inverted: lower hit = worse
782
+ table_row("Cache Hit Ratio", f"{bp['hit_ratio']}%", hr_status)
783
+ usage_status = _status_ok_warn_crit(bp["usage_percent"], 90, 95)
784
+ table_row(
785
+ "Pool Usage",
786
+ f"{bp['usage_percent']}% ({_format_bytes(bp['bytes_data'])}/{_format_bytes(bp['buffer_pool_size'])})",
787
+ usage_status,
788
+ )
789
+ table_row("Dirty Pages", _format_bytes(bp["bytes_dirty"]), "")
790
+ table_row("Free Pages", _format_count(bp["pages_free"]), "")
791
+
792
+ # --- Network ---
793
+ nw = result.network
794
+ if nw:
795
+ heading("Network")
796
+ table_row("Metric", "Value")
797
+ table_sep(2)
798
+ table_row("Bytes Received", _format_bytes(nw["bytes_received"]))
799
+ table_row("Bytes Sent", _format_bytes(nw["bytes_sent"]))
800
+
801
+ # --- Locks ---
802
+ lk = result.locks
803
+ if lk:
804
+ heading("Locks")
805
+ table_row("Metric", "Value", "Status")
806
+ table_sep(3)
807
+ table_row("Row Lock Waits", _format_count(lk["row_lock_waits"]), "")
808
+ table_row("Row Lock Time (ms)", _format_count(lk["row_lock_time"]), "")
809
+ tl_status = _status_ok_warn_crit(lk["table_lock_contention"], 1, 5)
810
+ table_row("Table Lock Contention", f"{lk['table_lock_contention']}%", tl_status)
811
+
812
+ # --- Table Cache ---
813
+ tc = result.table_cache
814
+ if tc:
815
+ heading("Table Cache")
816
+ table_row("Metric", "Value")
817
+ table_sep(2)
818
+ table_row("Open Tables", f"{_format_count(tc['open_tables'])} / {_format_count(tc.get('table_open_cache', 0))}")
819
+ table_row("Cache Utilization", f"{tc.get('cache_utilization_percent', 0)}%")
820
+ table_row("Table Opens/sec", f"{tc.get('opens_per_second', 0)}")
821
+
822
+ # --- Top Queries ---
823
+ if result.top_queries:
824
+ heading("Top Queries (by total latency)")
825
+ table_row("Query", "Calls", "Avg Latency", "Total Latency", "Rows Examined", "Rows Sent")
826
+ table_sep(6)
827
+ for q in result.top_queries[:15]:
828
+ digest = q["digest_text"][:60] + "..." if len(q["digest_text"]) > 60 else q["digest_text"]
829
+ # Escape pipe chars in query text
830
+ digest = digest.replace("|", "\\|")
831
+ table_row(
832
+ digest,
833
+ _format_count(q["count_star"]),
834
+ f"{q['avg_latency_ms']:.1f}ms",
835
+ f"{q['total_latency_ms']:.1f}ms",
836
+ _format_count(q["rows_examined"]),
837
+ _format_count(q["rows_sent"]),
838
+ )
839
+ else:
840
+ heading("Top Queries (by total latency)")
841
+ if result.top_queries_status == "performance_schema_disabled":
842
+ lines.append("performance_schema is disabled — no query-level data available.")
843
+ lines.append("Note: enabling it requires ~400MB+ additional memory; only advisable on larger instances.")
844
+ elif result.top_queries_status == "no_queries_recorded":
845
+ lines.append("No queries recorded. Database may be idle or recently restarted.")
846
+ else:
847
+ lines.append("No query data available.")
848
+ lines.append("")
849
+
850
+ # --- Tables ---
851
+ if result.tables:
852
+ heading("Tables (by size)")
853
+ table_row("Table", "Rows", "Data", "Indexes", "Total")
854
+ table_sep(5)
855
+ for t in result.tables[:15]:
856
+ table_row(
857
+ t["name"],
858
+ _format_count(t["rows"]),
859
+ _format_bytes(t["data_length"]),
860
+ _format_bytes(t["index_length"]),
861
+ _format_bytes(t["total_size"]),
862
+ )
863
+
864
+ # --- Active Queries ---
865
+ if result.active_processes:
866
+ heading("Active Queries")
867
+ # Filter out system processes
868
+ user_procs = [p for p in result.active_processes if p["command"] != "Daemon"]
869
+ if user_procs:
870
+ table_row("User", "Database", "Command", "Time (s)", "Query")
871
+ table_sep(5)
872
+ for p in user_procs[:20]:
873
+ info = (p["info"] or "")[:80]
874
+ info = info.replace("|", "\\|")
875
+ table_row(
876
+ p["user"],
877
+ p["db"] or "",
878
+ p["command"],
879
+ str(p["time"]),
880
+ info,
881
+ )
882
+ else:
883
+ lines.append("\nNo active user queries.")
884
+
885
+ # --- Infrastructure Metrics ---
886
+ if result.metrics_history:
887
+ windows = result.metrics_history.get("windows", {})
888
+ for window_label, window_data in windows.items():
889
+ mh = window_data.get("metrics", {})
890
+ if not mh:
891
+ continue
892
+ lines.append(f"## Infrastructure Metrics ({window_label})")
893
+ lines.append("| Metric | Current | Min | Max | Avg | Trend |")
894
+ lines.append("|--------|---------|-----|-----|-----|-------|")
895
+ for key in ["cpu", "memory", "disk", "network_rx", "network_tx"]:
896
+ if key in mh:
897
+ entry = mh[key]
898
+ trend = entry.get("trend", {})
899
+ trend_str = trend.get("direction", "N/A")
900
+ change = trend.get("change_pct", 0)
901
+ if change != 0:
902
+ trend_str += f" ({change:+.1f}%)"
903
+ lines.append(
904
+ f"| {key.replace('_', ' ').title()} "
905
+ f"| {entry['current']}{entry['unit']} "
906
+ f"| {entry['min']}{entry['unit']} "
907
+ f"| {entry['max']}{entry['unit']} "
908
+ f"| {entry['avg']}{entry['unit']} "
909
+ f"| {trend_str} |"
910
+ )
911
+ lines.append("")
912
+
913
+ # --- Collection Errors ---
914
+ if result.errors:
915
+ heading("Collection Errors")
916
+ for err in result.errors:
917
+ lines.append(f"- {err}")
918
+
919
+ # --- Recommendations ---
920
+ heading("Recommendations")
921
+ if result.recommendations:
922
+ for r in result.recommendations:
923
+ severity = r["severity"].upper()
924
+ lines.append(f"- **[{severity}]** {r['message']}")
925
+ else:
926
+ lines.append("No issues detected.")
927
+
928
+ return "\n".join(lines) + "\n"
929
+
930
+
931
+ # ---------------------------------------------------------------------------
932
+ # Main analysis function
933
+ # ---------------------------------------------------------------------------
934
+
935
+ def analyze_mysql(service: str, timeout: int = 60, quiet: bool = False,
936
+ skip_logs: bool = False, metrics_hours: int = 168,
937
+ project_id: Optional[str] = None,
938
+ environment_id: Optional[str] = None,
939
+ service_id: Optional[str] = None) -> MySQLAnalysisResult:
940
+ """Run complete MySQL analysis."""
941
+ if not quiet:
942
+ print(f"Analyzing mysql database: {service}", file=sys.stderr)
943
+
944
+ result = MySQLAnalysisResult(
945
+ service=service,
946
+ db_type="mysql",
947
+ timestamp=datetime.now(timezone.utc).isoformat(),
948
+ )
949
+
950
+ # === CONTEXT ===
951
+ if not quiet:
952
+ print(" [0/5] Getting Railway context...", file=sys.stderr, flush=True)
953
+ dal._progress_timer.start()
954
+
955
+ if environment_id and service_id:
956
+ dal._ctx = RailwayContext(project_id=project_id, environment_id=environment_id, service_id=service_id)
957
+ if not quiet:
958
+ print(f" using explicit IDs (env={environment_id[:8]}..., svc={service_id[:8]}...)", file=sys.stderr, flush=True)
959
+ else:
960
+ railway_status = get_railway_status()
961
+ if railway_status:
962
+ dal._ctx = RailwayContext(
963
+ project_id=railway_status.get("projectId"),
964
+ environment_id=railway_status.get("environmentId"),
965
+ service_id=railway_status.get("serviceId"),
966
+ )
967
+ environment_id = dal._ctx.environment_id
968
+ service_id = dal._ctx.service_id
969
+
970
+ # === DEPLOYMENT STATUS ===
971
+ progress(1, 5, "Fetching deployment status...", quiet)
972
+ result.deployment_status = get_deployment_status(service, service_id=service_id)
973
+
974
+ # === SSH PRE-CHECK ===
975
+ progress(2, 5, "Testing SSH connectivity...", quiet)
976
+ ssh_available = False
977
+ ssh_stderr = ""
978
+ ssh_attempts = [30, 60, 90]
979
+ for attempt, attempt_timeout in enumerate(ssh_attempts, 1):
980
+ ssh_code, ssh_stdout, ssh_stderr = run_ssh_query(service, "echo ok", timeout=attempt_timeout)
981
+ if ssh_code == 0 and "ok" in ssh_stdout:
982
+ ssh_available = True
983
+ if not quiet:
984
+ for line in ssh_stderr.splitlines():
985
+ if line.startswith("Using SSH key:"):
986
+ print(f" {line}", file=sys.stderr, flush=True)
987
+ break
988
+ break
989
+ if not quiet:
990
+ remaining = len(ssh_attempts) - attempt
991
+ if remaining > 0:
992
+ print(f" SSH attempt {attempt}/{len(ssh_attempts)} failed ({ssh_stderr or 'no response'}), retrying with {ssh_attempts[attempt]}s timeout...", file=sys.stderr, flush=True)
993
+ else:
994
+ print(f" SSH attempt {attempt}/{len(ssh_attempts)} failed ({ssh_stderr or 'no response'}), giving up", file=sys.stderr, flush=True)
995
+
996
+ # === PARALLEL EXECUTION ===
997
+ progress(3, 5, "Running analysis (metrics, queries, logs in parallel)...", quiet)
998
+
999
+ def task_metrics():
1000
+ if environment_id and service_id:
1001
+ return get_all_metrics_from_api(environment_id, service_id, hours=metrics_hours)
1002
+ return None
1003
+
1004
+ def task_mysql_queries():
1005
+ if not ssh_available:
1006
+ return {"errors": [f"SSH not available: {ssh_stderr or 'connection failed'}"]}
1007
+ data = collect_mysql_data(service, timeout=timeout)
1008
+ if data.get("errors") and not data.get("global_status"):
1009
+ # Retry once
1010
+ data = collect_mysql_data(service, timeout=timeout)
1011
+ return data
1012
+
1013
+ def task_logs():
1014
+ if skip_logs:
1015
+ return []
1016
+ return get_recent_logs(service, lines=LOG_LINES_DEFAULT,
1017
+ environment_id=environment_id,
1018
+ service_id=service_id)
1019
+
1020
+ with ThreadPoolExecutor(max_workers=3) as executor:
1021
+ future_metrics = executor.submit(task_metrics)
1022
+ future_mysql = executor.submit(task_mysql_queries)
1023
+ future_logs = executor.submit(task_logs)
1024
+
1025
+ metrics_result = future_metrics.result()
1026
+ mysql_data = future_mysql.result()
1027
+ logs_result = future_logs.result()
1028
+
1029
+ # Process metrics
1030
+ if metrics_result:
1031
+ result.disk_usage = metrics_result.get("disk_usage")
1032
+ result.cpu_memory = metrics_result.get("cpu_memory")
1033
+ result.metrics_history = metrics_result.get("metrics_history")
1034
+ result.collection_status["metrics_api"] = {"status": "success"}
1035
+ else:
1036
+ result.collection_status["metrics_api"] = {
1037
+ "status": "error",
1038
+ "error": "Metrics API returned no data",
1039
+ }
1040
+
1041
+ # Process MySQL data
1042
+ progress(4, 5, "Processing results...", quiet)
1043
+ mysql_errors = mysql_data.get("errors", [])
1044
+ if mysql_data.get("global_status"):
1045
+ parse_mysql_data(mysql_data, result)
1046
+ result.collection_status["mysql_query"] = {"status": "success"}
1047
+ if mysql_errors:
1048
+ result.collection_status["mysql_query"]["warnings"] = mysql_errors
1049
+ else:
1050
+ error_msg = "; ".join(mysql_errors) if mysql_errors else "No data returned"
1051
+ if not ssh_available:
1052
+ error_msg = f"SSH failed after {len(ssh_attempts)} attempts: {ssh_stderr or 'connection failed'}"
1053
+ result.errors.append(f"MySQL data collection failed: {error_msg}")
1054
+ result.collection_status["mysql_query"] = {
1055
+ "status": "error",
1056
+ "error": error_msg,
1057
+ }
1058
+
1059
+ # Process logs
1060
+ if skip_logs:
1061
+ result.collection_status["logs_api"] = {"status": "skipped", "reason": "skip_logs flag set"}
1062
+ elif logs_result:
1063
+ result.recent_logs = logs_result
1064
+ result.collection_status["logs_api"] = {"status": "success", "lines": len(logs_result)}
1065
+ result.recent_errors = [
1066
+ line for line in result.recent_logs
1067
+ if "ERROR" in line.upper() or "FATAL" in line.upper()
1068
+ ][:100]
1069
+ else:
1070
+ result.recent_logs = []
1071
+ result.collection_status["logs_api"] = {
1072
+ "status": "error",
1073
+ "error": "Logs API returned no data",
1074
+ }
1075
+
1076
+ # === RECOMMENDATIONS ===
1077
+ progress(5, 5, "Generating recommendations...", quiet)
1078
+ result.recommendations = generate_recommendations(result)
1079
+
1080
+ if not quiet:
1081
+ total = dal._progress_timer.total_elapsed()
1082
+ print(f"Done.{total}", file=sys.stderr)
1083
+
1084
+ return result
1085
+
1086
+
1087
+ # ---------------------------------------------------------------------------
1088
+ # Single-step debugging
1089
+ # ---------------------------------------------------------------------------
1090
+
1091
+ def run_single_step(args) -> int:
1092
+ """Run a single collection step for debugging."""
1093
+ service = args.service
1094
+ _init_context(args)
1095
+ environment_id = dal._ctx.environment_id
1096
+ service_id = dal._ctx.service_id
1097
+
1098
+ if args.step == "ssh-test":
1099
+ print(f"Testing SSH to service: {service}", file=sys.stderr)
1100
+ code, stdout, stderr = run_ssh_query(service, "echo ok", timeout=45)
1101
+ print(f"Exit code: {code}")
1102
+ print(f"Stdout: {stdout.strip()}")
1103
+ if stderr:
1104
+ print(f"Stderr: {stderr.strip()}")
1105
+ return 0 if (code == 0 and "ok" in stdout) else 1
1106
+
1107
+ elif args.step == "query":
1108
+ print(f"Running MySQL queries on: {service}", file=sys.stderr)
1109
+ data = collect_mysql_data(service, timeout=args.timeout)
1110
+ print(json.dumps(data, indent=2, default=str))
1111
+ return 0 if data.get("global_status") else 1
1112
+
1113
+ elif args.step == "logs":
1114
+ print(f"Fetching logs for: {service}", file=sys.stderr)
1115
+ logs = get_recent_logs(service, lines=LOG_LINES_DEFAULT,
1116
+ environment_id=environment_id,
1117
+ service_id=service_id)
1118
+ print(f"Lines fetched: {len(logs)}")
1119
+ for line in logs:
1120
+ print(line)
1121
+ return 0
1122
+
1123
+ elif args.step == "metrics":
1124
+ print(f"Fetching metrics for: {service}", file=sys.stderr)
1125
+ if environment_id and service_id:
1126
+ metrics = get_all_metrics_from_api(environment_id, service_id)
1127
+ if metrics:
1128
+ print(json.dumps(metrics, indent=2))
1129
+ else:
1130
+ print("Metrics API returned no data")
1131
+ return 1
1132
+ else:
1133
+ print("Missing environment_id or service_id from railway config")
1134
+ return 1
1135
+ return 0
1136
+
1137
+ return 1
1138
+
1139
+
1140
+ # ---------------------------------------------------------------------------
1141
+ # Main entry point
1142
+ # ---------------------------------------------------------------------------
1143
+
1144
+ def main():
1145
+ parser = argparse.ArgumentParser(
1146
+ description="MySQL analysis for Railway services.",
1147
+ formatter_class=argparse.RawDescriptionHelpFormatter,
1148
+ )
1149
+
1150
+ parser.add_argument("--service", required=True, help="Service name")
1151
+ parser.add_argument("--json", action="store_true",
1152
+ help="Output as JSON")
1153
+ parser.add_argument("--timeout", type=int, default=60,
1154
+ help="Timeout in seconds for SSH queries (default: 60)")
1155
+ parser.add_argument("--quiet", "-q", action="store_true",
1156
+ help="Suppress progress messages")
1157
+ parser.add_argument("--skip-logs", action="store_true",
1158
+ help="Skip log fetching for faster analysis")
1159
+ parser.add_argument("--metrics-hours", type=int, default=168,
1160
+ help="Hours of metrics history to fetch (default: 168, max: 168)")
1161
+ parser.add_argument("--step", choices=["ssh-test", "query", "logs", "metrics"],
1162
+ help="Run a single collection step for debugging")
1163
+ parser.add_argument("--project-id", help="Project ID (bypasses railway link)")
1164
+ parser.add_argument("--environment-id", help="Environment ID (bypasses railway link)")
1165
+ parser.add_argument("--service-id", help="Service ID (bypasses railway link)")
1166
+
1167
+ args = parser.parse_args()
1168
+
1169
+ # Single-step debugging mode
1170
+ if args.step:
1171
+ return run_single_step(args)
1172
+
1173
+ # Run analysis
1174
+ result = analyze_mysql(
1175
+ args.service,
1176
+ timeout=args.timeout,
1177
+ quiet=args.quiet,
1178
+ skip_logs=args.skip_logs,
1179
+ metrics_hours=min(args.metrics_hours, 168),
1180
+ project_id=args.project_id,
1181
+ environment_id=args.environment_id,
1182
+ service_id=args.service_id,
1183
+ )
1184
+
1185
+ # Output
1186
+ if args.json:
1187
+ print(json.dumps(asdict(result), indent=2))
1188
+ else:
1189
+ print(format_report(result))
1190
+
1191
+ return 0
1192
+
1193
+
1194
+ if __name__ == "__main__":
1195
+ sys.exit(main())