@synapta/skills 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (354) 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-deepener/SKILL.md +86 -0
  23. package/skills/concept-discovery/SKILL.md +517 -0
  24. package/skills/concept-discovery/assets/sample-analysis.json +81 -0
  25. package/skills/concept-discovery/expected_outputs/sample-enum-dictionary.md +25 -0
  26. package/skills/concept-discovery/expected_outputs/sample-page-user-list.md +83 -0
  27. package/skills/concept-discovery/expected_outputs/sample-prd-readme.md +43 -0
  28. package/skills/concept-discovery/references/framework-patterns.md +228 -0
  29. package/skills/concept-discovery/references/prd-quality-checklist.md +65 -0
  30. package/skills/concept-discovery/scripts/codebase_analyzer.py +732 -0
  31. package/skills/concept-discovery/scripts/prd_scaffolder.py +435 -0
  32. package/skills/dast-zap/SKILL.md +453 -0
  33. package/skills/dast-zap/assets/.gitkeep +9 -0
  34. package/skills/dast-zap/assets/github_action.yml +207 -0
  35. package/skills/dast-zap/assets/gitlab_ci.yml +226 -0
  36. package/skills/dast-zap/assets/zap_automation.yaml +196 -0
  37. package/skills/dast-zap/assets/zap_context.xml +192 -0
  38. package/skills/dast-zap/references/EXAMPLE.md +40 -0
  39. package/skills/dast-zap/references/api_testing_guide.md +475 -0
  40. package/skills/dast-zap/references/authentication_guide.md +431 -0
  41. package/skills/dast-zap/references/false_positive_handling.md +427 -0
  42. package/skills/dast-zap/references/owasp_mapping.md +255 -0
  43. package/skills/dep-sbom-scan/SKILL.md +466 -0
  44. package/skills/deploy-cloudflare/SKILL.md +930 -0
  45. package/skills/deploy-docker/SKILL.md +55 -0
  46. package/skills/deploy-fly/SKILL.md +228 -0
  47. package/skills/deploy-k8s/SKILL.md +108 -0
  48. package/skills/deploy-k8s/assets/logo.png +0 -0
  49. package/skills/deploy-k8s/docs/README.md +29 -0
  50. package/skills/deploy-k8s/docs/SUMMARY.md +56 -0
  51. package/skills/deploy-k8s/docs/advanced/token-efficiency.md +61 -0
  52. package/skills/deploy-k8s/docs/architecture/multi-tenancy.md +96 -0
  53. package/skills/deploy-k8s/docs/architecture/storage-and-state.md +102 -0
  54. package/skills/deploy-k8s/docs/architecture/workload-patterns.md +87 -0
  55. package/skills/deploy-k8s/docs/book.json +16 -0
  56. package/skills/deploy-k8s/docs/community/changelog.md +34 -0
  57. package/skills/deploy-k8s/docs/community/contributing.md +67 -0
  58. package/skills/deploy-k8s/docs/core-concepts/failure-modes.md +153 -0
  59. package/skills/deploy-k8s/docs/core-concepts/philosophy.md +83 -0
  60. package/skills/deploy-k8s/docs/core-concepts/workflow.md +124 -0
  61. package/skills/deploy-k8s/docs/examples/bad-patterns.md +47 -0
  62. package/skills/deploy-k8s/docs/examples/do-dont-checklist.md +37 -0
  63. package/skills/deploy-k8s/docs/examples/good-patterns.md +49 -0
  64. package/skills/deploy-k8s/docs/failure-modes/api-drift.md +104 -0
  65. package/skills/deploy-k8s/docs/failure-modes/fragile-rollouts.md +99 -0
  66. package/skills/deploy-k8s/docs/failure-modes/insecure-workload-defaults.md +80 -0
  67. package/skills/deploy-k8s/docs/failure-modes/network-exposure.md +98 -0
  68. package/skills/deploy-k8s/docs/failure-modes/privilege-sprawl.md +91 -0
  69. package/skills/deploy-k8s/docs/failure-modes/resource-starvation.md +85 -0
  70. package/skills/deploy-k8s/docs/getting-started/installation.md +152 -0
  71. package/skills/deploy-k8s/docs/getting-started/quick-start.md +115 -0
  72. package/skills/deploy-k8s/docs/guides/helm-patterns.md +71 -0
  73. package/skills/deploy-k8s/docs/guides/kustomize-patterns.md +65 -0
  74. package/skills/deploy-k8s/docs/guides/observability.md +67 -0
  75. package/skills/deploy-k8s/docs/guides/security-hardening.md +59 -0
  76. package/skills/deploy-k8s/docs/guides/validation-and-policy.md +66 -0
  77. package/skills/deploy-k8s/docs/integrations/mcp-integration.md +52 -0
  78. package/skills/deploy-k8s/docs/package-lock.json +2892 -0
  79. package/skills/deploy-k8s/docs/package.json +13 -0
  80. package/skills/deploy-k8s/references/api-drift.md +298 -0
  81. package/skills/deploy-k8s/references/conditional/aks-patterns.md +70 -0
  82. package/skills/deploy-k8s/references/conditional/eks-patterns.md +79 -0
  83. package/skills/deploy-k8s/references/conditional/gitops-controllers.md +71 -0
  84. package/skills/deploy-k8s/references/conditional/gke-patterns.md +74 -0
  85. package/skills/deploy-k8s/references/conditional/observability-stacks.md +80 -0
  86. package/skills/deploy-k8s/references/conditional/openshift-patterns.md +67 -0
  87. package/skills/deploy-k8s/references/daemonset-operator-patterns.md +155 -0
  88. package/skills/deploy-k8s/references/deployment-patterns.md +146 -0
  89. package/skills/deploy-k8s/references/do-dont-patterns.md +87 -0
  90. package/skills/deploy-k8s/references/examples-bad.md +282 -0
  91. package/skills/deploy-k8s/references/examples-good.md +440 -0
  92. package/skills/deploy-k8s/references/fragile-rollouts.md +303 -0
  93. package/skills/deploy-k8s/references/helm-patterns.md +203 -0
  94. package/skills/deploy-k8s/references/insecure-workload-defaults.md +300 -0
  95. package/skills/deploy-k8s/references/job-patterns.md +120 -0
  96. package/skills/deploy-k8s/references/kustomize-patterns.md +239 -0
  97. package/skills/deploy-k8s/references/multi-tenancy.md +343 -0
  98. package/skills/deploy-k8s/references/network-exposure.md +481 -0
  99. package/skills/deploy-k8s/references/observability.md +302 -0
  100. package/skills/deploy-k8s/references/privilege-sprawl.md +273 -0
  101. package/skills/deploy-k8s/references/resource-starvation.md +374 -0
  102. package/skills/deploy-k8s/references/security-hardening.md +209 -0
  103. package/skills/deploy-k8s/references/stateful-patterns.md +130 -0
  104. package/skills/deploy-k8s/references/storage-and-state.md +330 -0
  105. package/skills/deploy-k8s/references/validation-and-policy.md +242 -0
  106. package/skills/deploy-railway/SKILL.md +235 -0
  107. package/skills/deploy-railway/references/analyze-db-mongo.md +84 -0
  108. package/skills/deploy-railway/references/analyze-db-mysql.md +254 -0
  109. package/skills/deploy-railway/references/analyze-db-postgres.md +479 -0
  110. package/skills/deploy-railway/references/analyze-db-redis.md +208 -0
  111. package/skills/deploy-railway/references/analyze-db.md +344 -0
  112. package/skills/deploy-railway/references/configure.md +309 -0
  113. package/skills/deploy-railway/references/deploy.md +195 -0
  114. package/skills/deploy-railway/references/operate.md +214 -0
  115. package/skills/deploy-railway/references/request.md +248 -0
  116. package/skills/deploy-railway/references/setup.md +312 -0
  117. package/skills/deploy-railway/scripts/analyze-mongo.py +1549 -0
  118. package/skills/deploy-railway/scripts/analyze-mysql.py +1195 -0
  119. package/skills/deploy-railway/scripts/analyze-postgres.py +3058 -0
  120. package/skills/deploy-railway/scripts/analyze-redis.py +1090 -0
  121. package/skills/deploy-railway/scripts/dal.py +671 -0
  122. package/skills/deploy-railway/scripts/enable-pg-stats.py +170 -0
  123. package/skills/deploy-railway/scripts/pg-extensions.py +370 -0
  124. package/skills/deploy-railway/scripts/railway-api.sh +52 -0
  125. package/skills/deploy-ssh/SKILL.md +91 -0
  126. package/skills/deploy-vercel/SKILL.md +304 -0
  127. package/skills/deploy-vercel/resources/deploy-codex.sh +301 -0
  128. package/skills/deploy-vercel/resources/deploy.sh +301 -0
  129. package/skills/docs-runbooks/SKILL.md +399 -0
  130. package/skills/drive-status-renderer/SKILL.md +62 -0
  131. package/skills/iac-scan/SKILL.md +680 -0
  132. package/skills/iac-scan/assets/.gitkeep +9 -0
  133. package/skills/iac-scan/assets/checkov_config.yaml +94 -0
  134. package/skills/iac-scan/assets/github_actions.yml +199 -0
  135. package/skills/iac-scan/assets/gitlab_ci.yml +218 -0
  136. package/skills/iac-scan/assets/pre_commit_config.yaml +92 -0
  137. package/skills/iac-scan/references/EXAMPLE.md +40 -0
  138. package/skills/iac-scan/references/compliance_mapping.md +237 -0
  139. package/skills/iac-scan/references/custom_policies.md +460 -0
  140. package/skills/iac-scan/references/suppression_guide.md +431 -0
  141. package/skills/incident-briefing/SKILL.md +66 -0
  142. package/skills/incident-triage/SKILL.md +481 -0
  143. package/{LICENSE → skills/mcp-builder/LICENSE.txt} +15 -14
  144. package/skills/mcp-builder/SKILL.md +244 -0
  145. package/skills/mcp-builder/reference/evaluation.md +602 -0
  146. package/skills/mcp-builder/reference/mcp_best_practices.md +249 -0
  147. package/skills/mcp-builder/reference/node_mcp_server.md +970 -0
  148. package/skills/mcp-builder/reference/python_mcp_server.md +719 -0
  149. package/skills/mcp-builder/scripts/connections.py +151 -0
  150. package/skills/mcp-builder/scripts/evaluation.py +373 -0
  151. package/skills/mcp-builder/scripts/example_evaluation.xml +22 -0
  152. package/skills/mcp-builder/scripts/requirements.txt +2 -0
  153. package/skills/mobile-pairing/SKILL.md +52 -0
  154. package/skills/ops-sre/SKILL.md +297 -0
  155. package/skills/playwright-qa/LICENSE.txt +201 -0
  156. package/skills/playwright-qa/NOTICE.txt +14 -0
  157. package/skills/playwright-qa/SKILL.md +156 -0
  158. package/skills/playwright-qa/agents/openai.yaml +6 -0
  159. package/skills/playwright-qa/assets/playwright-small.svg +3 -0
  160. package/skills/playwright-qa/assets/playwright.png +0 -0
  161. package/skills/playwright-qa/references/cli.md +116 -0
  162. package/skills/playwright-qa/references/workflows.md +95 -0
  163. package/skills/playwright-qa/scripts/playwright_cli.sh +25 -0
  164. package/skills/release-publish/SKILL.md +85 -0
  165. package/skills/repo-bootstrap/SKILL.md +92 -0
  166. package/skills/repo-bootstrap/assets/example-workflows/validate-agents.yml +89 -0
  167. package/skills/repo-bootstrap/assets/root-thin.md +141 -0
  168. package/skills/repo-bootstrap/assets/root-verbose.md +149 -0
  169. package/skills/repo-bootstrap/assets/scoped/backend-go.md +107 -0
  170. package/skills/repo-bootstrap/assets/scoped/backend-php.md +94 -0
  171. package/skills/repo-bootstrap/assets/scoped/backend-python.md +84 -0
  172. package/skills/repo-bootstrap/assets/scoped/backend-typescript.md +89 -0
  173. package/skills/repo-bootstrap/assets/scoped/claude-code-skill.md +101 -0
  174. package/skills/repo-bootstrap/assets/scoped/cli.md +83 -0
  175. package/skills/repo-bootstrap/assets/scoped/concourse.md +196 -0
  176. package/skills/repo-bootstrap/assets/scoped/ddev.md +68 -0
  177. package/skills/repo-bootstrap/assets/scoped/docker.md +160 -0
  178. package/skills/repo-bootstrap/assets/scoped/documentation.md +98 -0
  179. package/skills/repo-bootstrap/assets/scoped/examples.md +96 -0
  180. package/skills/repo-bootstrap/assets/scoped/frontend-typescript.md +88 -0
  181. package/skills/repo-bootstrap/assets/scoped/github-actions.md +174 -0
  182. package/skills/repo-bootstrap/assets/scoped/gitlab-ci.md +174 -0
  183. package/skills/repo-bootstrap/assets/scoped/oro-bundle.md +209 -0
  184. package/skills/repo-bootstrap/assets/scoped/oro-project.md +170 -0
  185. package/skills/repo-bootstrap/assets/scoped/python-modern.md +170 -0
  186. package/skills/repo-bootstrap/assets/scoped/resources.md +96 -0
  187. package/skills/repo-bootstrap/assets/scoped/skill-repo.md +139 -0
  188. package/skills/repo-bootstrap/assets/scoped/symfony.md +168 -0
  189. package/skills/repo-bootstrap/assets/scoped/testing.md +87 -0
  190. package/skills/repo-bootstrap/assets/scoped/typo3-docs.md +103 -0
  191. package/skills/repo-bootstrap/assets/scoped/typo3-extension.md +133 -0
  192. package/skills/repo-bootstrap/assets/scoped/typo3-project.md +137 -0
  193. package/skills/repo-bootstrap/assets/scoped/typo3-testing.md +80 -0
  194. package/skills/repo-bootstrap/checkpoints.yaml +279 -0
  195. package/skills/repo-bootstrap/evals/evals.json +385 -0
  196. package/skills/repo-bootstrap/references/ai-contribution-guidelines.md +63 -0
  197. package/skills/repo-bootstrap/references/ai-tool-compatibility.md +223 -0
  198. package/skills/repo-bootstrap/references/directory-coverage.md +82 -0
  199. package/skills/repo-bootstrap/references/examples/coding-agent-cli/AGENTS.md +70 -0
  200. package/skills/repo-bootstrap/references/examples/coding-agent-cli/go.mod +3 -0
  201. package/skills/repo-bootstrap/references/examples/coding-agent-cli/scripts-AGENTS.md +389 -0
  202. package/skills/repo-bootstrap/references/examples/express-api-ts/.env.example +13 -0
  203. package/skills/repo-bootstrap/references/examples/express-api-ts/AGENTS.md +91 -0
  204. package/skills/repo-bootstrap/references/examples/express-api-ts/package.json +33 -0
  205. package/skills/repo-bootstrap/references/examples/express-api-ts/pnpm-lock.yaml +3 -0
  206. package/skills/repo-bootstrap/references/examples/express-api-ts/src/AGENTS.md +91 -0
  207. package/skills/repo-bootstrap/references/examples/express-api-ts/src/config.ts +28 -0
  208. package/skills/repo-bootstrap/references/examples/express-api-ts/src/controllers/userController.ts +74 -0
  209. package/skills/repo-bootstrap/references/examples/express-api-ts/src/index.ts +26 -0
  210. package/skills/repo-bootstrap/references/examples/express-api-ts/src/middleware/errorHandler.ts +45 -0
  211. package/skills/repo-bootstrap/references/examples/express-api-ts/src/middleware/requestLogger.ts +18 -0
  212. package/skills/repo-bootstrap/references/examples/express-api-ts/src/routes/health.ts +18 -0
  213. package/skills/repo-bootstrap/references/examples/express-api-ts/src/routes/users.ts +13 -0
  214. package/skills/repo-bootstrap/references/examples/express-api-ts/src/utils/errors.ts +40 -0
  215. package/skills/repo-bootstrap/references/examples/express-api-ts/src/utils/logger.ts +14 -0
  216. package/skills/repo-bootstrap/references/examples/express-api-ts/tsconfig.json +24 -0
  217. package/skills/repo-bootstrap/references/examples/fastapi-app/.env.example +19 -0
  218. package/skills/repo-bootstrap/references/examples/fastapi-app/AGENTS.md +92 -0
  219. package/skills/repo-bootstrap/references/examples/fastapi-app/pyproject.toml +88 -0
  220. package/skills/repo-bootstrap/references/examples/fastapi-app/src/AGENTS.md +85 -0
  221. package/skills/repo-bootstrap/references/examples/fastapi-app/src/__init__.py +3 -0
  222. package/skills/repo-bootstrap/references/examples/fastapi-app/src/config.py +49 -0
  223. package/skills/repo-bootstrap/references/examples/fastapi-app/src/main.py +66 -0
  224. package/skills/repo-bootstrap/references/examples/fastapi-app/src/models/__init__.py +13 -0
  225. package/skills/repo-bootstrap/references/examples/fastapi-app/src/models/item.py +43 -0
  226. package/skills/repo-bootstrap/references/examples/fastapi-app/src/models/user.py +40 -0
  227. package/skills/repo-bootstrap/references/examples/fastapi-app/src/routes/__init__.py +5 -0
  228. package/skills/repo-bootstrap/references/examples/fastapi-app/src/routes/health.py +20 -0
  229. package/skills/repo-bootstrap/references/examples/fastapi-app/src/routes/items.py +61 -0
  230. package/skills/repo-bootstrap/references/examples/fastapi-app/src/routes/users.py +55 -0
  231. package/skills/repo-bootstrap/references/examples/fastapi-app/src/services/__init__.py +6 -0
  232. package/skills/repo-bootstrap/references/examples/fastapi-app/src/services/item_service.py +77 -0
  233. package/skills/repo-bootstrap/references/examples/fastapi-app/src/services/user_service.py +69 -0
  234. package/skills/repo-bootstrap/references/examples/fastapi-app/uv.lock +4 -0
  235. package/skills/repo-bootstrap/references/examples/go-api-with-react-admin/.scopes +3 -0
  236. package/skills/repo-bootstrap/references/examples/go-api-with-react-admin/AGENTS.md +86 -0
  237. package/skills/repo-bootstrap/references/examples/go-api-with-react-admin/admin/package.json +20 -0
  238. package/skills/repo-bootstrap/references/examples/go-api-with-react-admin/admin/src/App.tsx +5 -0
  239. package/skills/repo-bootstrap/references/examples/go-api-with-react-admin/cmd/api/main.go +7 -0
  240. package/skills/repo-bootstrap/references/examples/go-api-with-react-admin/go.mod +2 -0
  241. package/skills/repo-bootstrap/references/examples/go-api-with-react-admin/main.go +7 -0
  242. package/skills/repo-bootstrap/references/examples/go-with-internal-web-tsx/.scopes +3 -0
  243. package/skills/repo-bootstrap/references/examples/go-with-internal-web-tsx/AGENTS.md +89 -0
  244. package/skills/repo-bootstrap/references/examples/go-with-internal-web-tsx/go.mod +2 -0
  245. package/skills/repo-bootstrap/references/examples/go-with-internal-web-tsx/internal/web/AGENTS.md +90 -0
  246. package/skills/repo-bootstrap/references/examples/go-with-internal-web-tsx/internal/web/package.json +17 -0
  247. package/skills/repo-bootstrap/references/examples/go-with-internal-web-tsx/internal/web/src/App.tsx +1 -0
  248. package/skills/repo-bootstrap/references/examples/go-with-internal-web-tsx/internal/web/src/Button.tsx +1 -0
  249. package/skills/repo-bootstrap/references/examples/go-with-internal-web-tsx/internal/web/src/Footer.tsx +1 -0
  250. package/skills/repo-bootstrap/references/examples/go-with-internal-web-tsx/internal/web/src/Header.tsx +1 -0
  251. package/skills/repo-bootstrap/references/examples/go-with-internal-web-tsx/internal/web/src/Sidebar.tsx +1 -0
  252. package/skills/repo-bootstrap/references/examples/go-with-internal-web-tsx/main.go +7 -0
  253. package/skills/repo-bootstrap/references/examples/go-with-internal-web-tsx/package-lock.json +0 -0
  254. package/skills/repo-bootstrap/references/examples/go-with-internal-web-tsx/package.json +12 -0
  255. package/skills/repo-bootstrap/references/examples/ldap-selfservice/AGENTS.md +70 -0
  256. package/skills/repo-bootstrap/references/examples/ldap-selfservice/go.mod +3 -0
  257. package/skills/repo-bootstrap/references/examples/ldap-selfservice/internal-AGENTS.md +371 -0
  258. package/skills/repo-bootstrap/references/examples/ldap-selfservice/internal-web-AGENTS.md +448 -0
  259. package/skills/repo-bootstrap/references/examples/php-with-frontend/.scopes +3 -0
  260. package/skills/repo-bootstrap/references/examples/php-with-frontend/AGENTS.md +91 -0
  261. package/skills/repo-bootstrap/references/examples/php-with-frontend/composer.json +8 -0
  262. package/skills/repo-bootstrap/references/examples/php-with-frontend/package.json +15 -0
  263. package/skills/repo-bootstrap/references/examples/php-with-frontend/pnpm-lock.yaml +0 -0
  264. package/skills/repo-bootstrap/references/examples/php-with-frontend/src/Controller.php +3 -0
  265. package/skills/repo-bootstrap/references/examples/php-with-frontend/web/AGENTS.md +92 -0
  266. package/skills/repo-bootstrap/references/examples/php-with-frontend/web/package.json +26 -0
  267. package/skills/repo-bootstrap/references/examples/php-with-frontend/web/src/App.tsx +3 -0
  268. package/skills/repo-bootstrap/references/examples/php-with-frontend/web/src/Button.tsx +10 -0
  269. package/skills/repo-bootstrap/references/examples/php-with-frontend/web/src/Footer.tsx +9 -0
  270. package/skills/repo-bootstrap/references/examples/php-with-frontend/web/src/Header.tsx +9 -0
  271. package/skills/repo-bootstrap/references/examples/php-with-frontend/web/src/main.tsx +3 -0
  272. package/skills/repo-bootstrap/references/examples/php-with-frontend/web/tsconfig.json +13 -0
  273. package/skills/repo-bootstrap/references/examples/pnpm-workspace/AGENTS.md +75 -0
  274. package/skills/repo-bootstrap/references/examples/pnpm-workspace/package.json +7 -0
  275. package/skills/repo-bootstrap/references/examples/pnpm-workspace/packages/web/package.json +11 -0
  276. package/skills/repo-bootstrap/references/examples/pnpm-workspace/packages/web/src/index.ts +11 -0
  277. package/skills/repo-bootstrap/references/examples/pnpm-workspace/pnpm-lock.yaml +42 -0
  278. package/skills/repo-bootstrap/references/examples/pnpm-workspace/pnpm-workspace.yaml +2 -0
  279. package/skills/repo-bootstrap/references/examples/simple-ldap-go/AGENTS.md +70 -0
  280. package/skills/repo-bootstrap/references/examples/simple-ldap-go/examples-AGENTS.md +45 -0
  281. package/skills/repo-bootstrap/references/examples/simple-ldap-go/go.mod +3 -0
  282. package/skills/repo-bootstrap/references/examples/t3x-rte-ckeditor-image/AGENTS.md +70 -0
  283. package/skills/repo-bootstrap/references/examples/t3x-rte-ckeditor-image/Classes-AGENTS.md +392 -0
  284. package/skills/repo-bootstrap/references/examples/t3x-rte-ckeditor-image/composer.json +8 -0
  285. package/skills/repo-bootstrap/references/feedback-memory-schema.md +135 -0
  286. package/skills/repo-bootstrap/references/git-hooks-setup.md +79 -0
  287. package/skills/repo-bootstrap/references/output-structure.md +124 -0
  288. package/skills/repo-bootstrap/references/scripts-guide.md +175 -0
  289. package/skills/repo-bootstrap/references/verification-guide.md +137 -0
  290. package/skills/repo-bootstrap/scripts/analyze-git-history.sh +315 -0
  291. package/skills/repo-bootstrap/scripts/check-freshness.sh +230 -0
  292. package/skills/repo-bootstrap/scripts/detect-golden-samples.sh +161 -0
  293. package/skills/repo-bootstrap/scripts/detect-heuristics.sh +93 -0
  294. package/skills/repo-bootstrap/scripts/detect-project.sh +486 -0
  295. package/skills/repo-bootstrap/scripts/detect-scopes.sh +330 -0
  296. package/skills/repo-bootstrap/scripts/detect-utilities.sh +133 -0
  297. package/skills/repo-bootstrap/scripts/extract-adrs.sh +194 -0
  298. package/skills/repo-bootstrap/scripts/extract-agent-configs.sh +331 -0
  299. package/skills/repo-bootstrap/scripts/extract-architecture-rules.sh +522 -0
  300. package/skills/repo-bootstrap/scripts/extract-ci-commands.sh +385 -0
  301. package/skills/repo-bootstrap/scripts/extract-ci-rules.sh +384 -0
  302. package/skills/repo-bootstrap/scripts/extract-commands.sh +358 -0
  303. package/skills/repo-bootstrap/scripts/extract-documentation.sh +308 -0
  304. package/skills/repo-bootstrap/scripts/extract-github-rulesets.sh +96 -0
  305. package/skills/repo-bootstrap/scripts/extract-github-settings.sh +88 -0
  306. package/skills/repo-bootstrap/scripts/extract-ide-settings.sh +228 -0
  307. package/skills/repo-bootstrap/scripts/extract-platform-files.sh +290 -0
  308. package/skills/repo-bootstrap/scripts/extract-quality-configs.sh +442 -0
  309. package/skills/repo-bootstrap/scripts/generate-agents.sh +2424 -0
  310. package/skills/repo-bootstrap/scripts/generate-file-map.sh +153 -0
  311. package/skills/repo-bootstrap/scripts/lib/config-root.sh +211 -0
  312. package/skills/repo-bootstrap/scripts/lib/summary.sh +244 -0
  313. package/skills/repo-bootstrap/scripts/lib/template.sh +397 -0
  314. package/skills/repo-bootstrap/scripts/validate-structure.sh +324 -0
  315. package/skills/repo-bootstrap/scripts/verify-commands.sh +615 -0
  316. package/skills/repo-bootstrap/scripts/verify-content.sh +302 -0
  317. package/skills/schema-api-contracts/SKILL.md +56 -0
  318. package/skills/secret-hygiene/SKILL.md +511 -0
  319. package/skills/secret-hygiene/assets/.gitkeep +9 -0
  320. package/skills/secret-hygiene/assets/config-balanced.toml +81 -0
  321. package/skills/secret-hygiene/assets/config-custom.toml +178 -0
  322. package/skills/secret-hygiene/assets/config-strict.toml +48 -0
  323. package/skills/secret-hygiene/assets/github-action.yml +181 -0
  324. package/skills/secret-hygiene/assets/gitlab-ci.yml +257 -0
  325. package/skills/secret-hygiene/assets/precommit-config.yaml +70 -0
  326. package/skills/secret-hygiene/references/EXAMPLE.md +40 -0
  327. package/skills/secret-hygiene/references/compliance_mapping.md +538 -0
  328. package/skills/secret-hygiene/references/detection_rules.md +276 -0
  329. package/skills/secret-hygiene/references/false_positives.md +598 -0
  330. package/skills/secret-hygiene/references/remediation_guide.md +530 -0
  331. package/skills/stack-selector/SKILL.md +56 -0
  332. package/skills/telegram-control/SKILL.md +110 -0
  333. package/skills/telegram-control/references/architecture.md +184 -0
  334. package/skills/telegram-control/references/convex.md +173 -0
  335. package/skills/telegram-control/references/error_handling.md +212 -0
  336. package/skills/telegram-control/references/initial_setup.md +165 -0
  337. package/skills/telegram-control/references/telegram_api.md +156 -0
  338. package/skills/telegram-control/scripts/cancel_message.ts +53 -0
  339. package/skills/telegram-control/scripts/list_scheduled.ts +103 -0
  340. package/skills/telegram-control/scripts/logger.ts +121 -0
  341. package/skills/telegram-control/scripts/proxy-util.ts +11 -0
  342. package/skills/telegram-control/scripts/schedule_message.ts +216 -0
  343. package/skills/telegram-control/scripts/send_message.ts +115 -0
  344. package/skills/telegram-control/scripts/setup.ts +185 -0
  345. package/skills/telegram-control/scripts/types.ts +75 -0
  346. package/skills/telegram-control/scripts/view_history.ts +74 -0
  347. package/skills/test-strategy/SKILL.md +352 -0
  348. package/skills/threat-model/SKILL.md +303 -0
  349. package/skills/threat-model/examples/example-output.md +196 -0
  350. package/skills/threat-model/template.md +96 -0
  351. package/skills/ts-lint/SKILL.md +80 -0
  352. package/skills/ui-flow/SKILL.md +668 -0
  353. package/skills/voice-command-router/SKILL.md +51 -0
  354. package/skills/widget-live-activity-sync/SKILL.md +66 -0
@@ -0,0 +1,1090 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Redis analysis for Railway deployments.
4
+
5
+ Produces a comprehensive report covering:
6
+ - Server overview (version, uptime, clients)
7
+ - Memory usage and fragmentation
8
+ - Throughput and command stats
9
+ - Cache performance (hit/miss ratio)
10
+ - Persistence status
11
+ - Keyspace summary
12
+ - Railway infrastructure metrics (CPU, memory, disk, network)
13
+ - Recent logs
14
+ - Recommendations
15
+
16
+ Usage:
17
+ analyze-redis.py --service <name>
18
+ analyze-redis.py --service <name> --json
19
+ analyze-redis.py --service <name> --step ssh-test
20
+ """
21
+
22
+ import argparse
23
+ import json
24
+ import os
25
+ import subprocess
26
+ import sys
27
+ import re
28
+ from concurrent.futures import ThreadPoolExecutor, as_completed
29
+ from datetime import datetime, timezone
30
+ from typing import Dict, List, Optional, Any, Tuple
31
+ from dataclasses import dataclass, field, asdict
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,
41
+ )
42
+
43
+
44
+ # ---------------------------------------------------------------------------
45
+ # Data model
46
+ # ---------------------------------------------------------------------------
47
+
48
+ @dataclass
49
+ class RedisAnalysisResult:
50
+ """Container for Redis analysis results."""
51
+ service: str
52
+ db_type: str
53
+ timestamp: str
54
+ deployment_status: str = "UNKNOWN"
55
+
56
+ # Redis INFO sections
57
+ overview: Optional[Dict[str, Any]] = None
58
+ memory: Optional[Dict[str, Any]] = None
59
+ throughput: Optional[Dict[str, Any]] = None
60
+ cache: Optional[Dict[str, Any]] = None
61
+ persistence: Optional[Dict[str, Any]] = None
62
+ keyspace: List[Dict[str, Any]] = field(default_factory=list)
63
+ total_keys: int = 0
64
+ command_stats: List[Dict[str, Any]] = field(default_factory=list)
65
+ slowlog_len: Optional[int] = None
66
+ slowlog_entries: List[Dict[str, Any]] = field(default_factory=list)
67
+ big_keys: List[Dict[str, Any]] = field(default_factory=list)
68
+
69
+ # Railway infrastructure
70
+ metrics_history: Optional[Dict[str, Any]] = None
71
+ recent_logs: List[str] = field(default_factory=list)
72
+
73
+ # Status tracking
74
+ collection_status: Dict[str, Dict[str, Any]] = field(default_factory=dict)
75
+ errors: List[str] = field(default_factory=list)
76
+ recommendations: List[Dict[str, str]] = field(default_factory=list)
77
+
78
+
79
+ # ---------------------------------------------------------------------------
80
+ # Redis data collection
81
+ # ---------------------------------------------------------------------------
82
+
83
+ def parse_redis_info(raw: str) -> Dict[str, str]:
84
+ """Parse Redis INFO output into a flat key:value dict.
85
+
86
+ Lines starting with # are section headers and are skipped.
87
+ """
88
+ info: Dict[str, str] = {}
89
+ for line in raw.splitlines():
90
+ line = line.strip()
91
+ if not line or line.startswith("#"):
92
+ continue
93
+ if ":" in line:
94
+ key, _, value = line.partition(":")
95
+ info[key.strip()] = value.strip()
96
+ return info
97
+
98
+
99
+ def extract_overview(info: Dict[str, str]) -> Dict[str, Any]:
100
+ """Extract overview metrics from INFO dict."""
101
+ return {
102
+ "redis_version": info.get("redis_version", "unknown"),
103
+ "uptime_in_seconds": _safe_int(info.get("uptime_in_seconds")),
104
+ "connected_clients": _safe_int(info.get("connected_clients")),
105
+ "blocked_clients": _safe_int(info.get("blocked_clients")),
106
+ "rejected_connections": _safe_int(info.get("rejected_connections")),
107
+ }
108
+
109
+
110
+ def extract_memory(info: Dict[str, str]) -> Dict[str, Any]:
111
+ """Extract memory metrics from INFO dict."""
112
+ return {
113
+ "used_memory_human": info.get("used_memory_human", "N/A"),
114
+ "used_memory_rss_human": info.get("used_memory_rss_human", "N/A"),
115
+ "used_memory_peak_human": info.get("used_memory_peak_human", "N/A"),
116
+ "mem_fragmentation_ratio": _safe_float(info.get("mem_fragmentation_ratio")),
117
+ "maxmemory": _safe_int(info.get("maxmemory")),
118
+ "maxmemory_human": info.get("maxmemory_human", "N/A"),
119
+ "maxmemory_policy": info.get("maxmemory_policy", "unknown"),
120
+ }
121
+
122
+
123
+ def extract_throughput(info: Dict[str, str]) -> Dict[str, Any]:
124
+ """Extract throughput metrics from INFO dict."""
125
+ return {
126
+ "instantaneous_ops_per_sec": _safe_int(info.get("instantaneous_ops_per_sec")),
127
+ "total_commands_processed": _safe_int(info.get("total_commands_processed")),
128
+ "total_connections_received": _safe_int(info.get("total_connections_received")),
129
+ }
130
+
131
+
132
+ def extract_cache(info: Dict[str, str]) -> Dict[str, Any]:
133
+ """Extract cache performance metrics from INFO dict."""
134
+ hits = _safe_int(info.get("keyspace_hits"))
135
+ misses = _safe_int(info.get("keyspace_misses"))
136
+ total = hits + misses
137
+ hit_rate = round(hits / total * 100, 2) if total > 0 else 0.0
138
+ return {
139
+ "keyspace_hits": hits,
140
+ "keyspace_misses": misses,
141
+ "hit_rate": hit_rate,
142
+ "expired_keys": _safe_int(info.get("expired_keys")),
143
+ "evicted_keys": _safe_int(info.get("evicted_keys")),
144
+ }
145
+
146
+
147
+ def extract_persistence(info: Dict[str, str]) -> Dict[str, Any]:
148
+ """Extract persistence metrics from INFO dict."""
149
+ return {
150
+ "rdb_last_save_time": _safe_int(info.get("rdb_last_save_time")),
151
+ "rdb_last_bgsave_status": info.get("rdb_last_bgsave_status", "unknown"),
152
+ "rdb_current_bgsave_time_sec": _safe_int(info.get("rdb_current_bgsave_time_sec")),
153
+ "aof_enabled": info.get("aof_enabled", "0") == "1",
154
+ "aof_last_rewrite_status": info.get("aof_last_rewrite_status", "unknown"),
155
+ }
156
+
157
+
158
+ def extract_keyspace(info: Dict[str, str]) -> Tuple[List[Dict[str, Any]], int]:
159
+ """Extract keyspace metrics from INFO dict.
160
+
161
+ Keyspace entries look like: db0:keys=1234,expires=567,avg_ttl=12345
162
+ Returns (list of db dicts, total_keys).
163
+ """
164
+ databases: List[Dict[str, Any]] = []
165
+ total_keys = 0
166
+ for key, value in info.items():
167
+ if not re.match(r'^db\d+$', key):
168
+ continue
169
+ # Parse keys=X,expires=Y,avg_ttl=Z
170
+ parts = {}
171
+ for item in value.split(","):
172
+ k, _, v = item.partition("=")
173
+ parts[k] = v
174
+ keys = _safe_int(parts.get("keys"))
175
+ total_keys += keys
176
+ databases.append({
177
+ "db": key,
178
+ "keys": keys,
179
+ "expires": _safe_int(parts.get("expires")),
180
+ "avg_ttl": _safe_int(parts.get("avg_ttl")),
181
+ })
182
+ return databases, total_keys
183
+
184
+
185
+ def extract_command_stats(info: Dict[str, str]) -> List[Dict[str, Any]]:
186
+ """Extract command statistics from INFO dict.
187
+
188
+ Command stat entries look like: cmdstat_GET:calls=123,usec=456,usec_per_call=3.71
189
+ Returns list sorted by calls descending.
190
+ """
191
+ stats: List[Dict[str, Any]] = []
192
+ for key, value in info.items():
193
+ if not key.startswith("cmdstat_"):
194
+ continue
195
+ cmd_name = key[len("cmdstat_"):]
196
+ parts = {}
197
+ for item in value.split(","):
198
+ k, _, v = item.partition("=")
199
+ parts[k] = v
200
+ stats.append({
201
+ "command": cmd_name,
202
+ "calls": _safe_int(parts.get("calls")),
203
+ "usec": _safe_int(parts.get("usec")),
204
+ "usec_per_call": _safe_float(parts.get("usec_per_call")),
205
+ })
206
+ stats.sort(key=lambda x: x["calls"], reverse=True)
207
+ return stats
208
+
209
+
210
+ _CLIENT_IP_RE = re.compile(r'^(\[.+\]|(?:\d{1,3}\.){3}\d{1,3}):\d+$')
211
+
212
+
213
+ def parse_slowlog_get(raw: str) -> List[Dict[str, Any]]:
214
+ """Parse SLOWLOG GET output into structured entries.
215
+
216
+ redis-cli --raw SLOWLOG GET format (Redis 4.0+):
217
+ <id>
218
+ <timestamp_unix>
219
+ <duration_us>
220
+ <cmd>
221
+ <arg1>
222
+ ...
223
+ <argN>
224
+ <client_ip:port>
225
+ [<client_name>] (optional, may be absent)
226
+
227
+ There is no num_args field in the raw output. The client IP line
228
+ (IPv4 or IPv6 with port) marks the end of each entry's arguments.
229
+ """
230
+ entries: List[Dict[str, Any]] = []
231
+ lines = [l.strip() for l in raw.strip().splitlines() if l.strip()]
232
+ if not lines:
233
+ return entries
234
+
235
+ i = 0
236
+ while i < len(lines):
237
+ try:
238
+ entry_id = int(lines[i])
239
+ except (ValueError, IndexError):
240
+ i += 1
241
+ continue
242
+
243
+ if i + 3 >= len(lines):
244
+ break
245
+
246
+ try:
247
+ timestamp = int(lines[i + 1])
248
+ duration_us = int(lines[i + 2])
249
+ except (ValueError, IndexError):
250
+ i += 1
251
+ continue
252
+
253
+ # lines[i+3] is the command name; scan forward for client IP
254
+ cmd_start = i + 3
255
+ client_ip_pos = None
256
+ for k in range(cmd_start, min(cmd_start + 30, len(lines))):
257
+ if _CLIENT_IP_RE.match(lines[k]):
258
+ client_ip_pos = k
259
+ break
260
+
261
+ if client_ip_pos is not None:
262
+ cmd_parts = lines[cmd_start:client_ip_pos]
263
+ # Advance past client IP and optional client name
264
+ next_i = client_ip_pos + 1
265
+ if next_i < len(lines):
266
+ try:
267
+ int(lines[next_i])
268
+ except ValueError:
269
+ next_i += 1 # skip client name
270
+ else:
271
+ # No client IP found — take command + first arg only and advance
272
+ cmd_parts = lines[cmd_start:cmd_start + 2]
273
+ next_i = cmd_start + 2
274
+
275
+ command = " ".join(cmd_parts) if cmd_parts else "unknown"
276
+ if len(command) > 120:
277
+ command = command[:117] + "..."
278
+
279
+ entries.append({
280
+ "id": entry_id,
281
+ "timestamp_unix": timestamp,
282
+ "duration_us": duration_us,
283
+ "command": command,
284
+ })
285
+
286
+ i = next_i
287
+
288
+ return entries
289
+
290
+
291
+ def parse_bigkeys(raw: str) -> List[Dict[str, Any]]:
292
+ """Parse redis-cli --bigkeys output into structured entries.
293
+
294
+ Looks for lines like:
295
+ Biggest string found "cache:render:page/dashboard" has 2145832 bytes
296
+ Biggest hash found "user:sessions" has 14291 fields
297
+ Biggest list found "queue:notifications" has 8402 items
298
+ Biggest set found "tags:all" has 291 members
299
+ Biggest zset found "leaderboard:global" has 10042 members
300
+ Biggest stream found "events:main" has 5012 entries
301
+ """
302
+ entries: List[Dict[str, Any]] = []
303
+ # Match: Biggest <type> found "<key>" has <count> <unit>
304
+ # Redis 8+ uses double quotes; older versions used single quotes
305
+ pattern = re.compile(
306
+ r'Biggest\s+(\w+)\s+found\s+["\']([^"\']+)["\']\s+has\s+([\d,]+)\s+(\w+)',
307
+ re.IGNORECASE,
308
+ )
309
+ for line in raw.splitlines():
310
+ m = pattern.search(line)
311
+ if m:
312
+ key_type = m.group(1).lower()
313
+ key_name = m.group(2)
314
+ size_str = m.group(3).replace(",", "")
315
+ unit = m.group(4).lower()
316
+ size = _safe_int(size_str)
317
+
318
+ # Format size for display
319
+ if unit == "bytes":
320
+ detail = _format_bytes_human(size)
321
+ else:
322
+ detail = f"{size:,} {unit}"
323
+
324
+ entries.append({
325
+ "type": key_type,
326
+ "key": key_name,
327
+ "size_or_count": size,
328
+ "detail": detail,
329
+ })
330
+ return entries
331
+
332
+
333
+ # ---------------------------------------------------------------------------
334
+ # Formatting helpers
335
+ # ---------------------------------------------------------------------------
336
+
337
+ def _format_number(n: int) -> str:
338
+ """Format a large number with K/M/B suffixes."""
339
+ if n >= 1_000_000_000:
340
+ return f"{n / 1_000_000_000:.1f}B"
341
+ if n >= 1_000_000:
342
+ return f"{n / 1_000_000:.1f}M"
343
+ if n >= 1_000:
344
+ return f"{n / 1_000:.1f}K"
345
+ return f"{n:,}"
346
+
347
+
348
+ def _format_duration(seconds: int) -> str:
349
+ """Format a duration in seconds to a human-readable relative string."""
350
+ if seconds <= 0:
351
+ return "N/A"
352
+ if seconds < 60:
353
+ return f"{seconds}s ago"
354
+ if seconds < 3600:
355
+ return f"{seconds // 60}m ago"
356
+ if seconds < 86400:
357
+ return f"{seconds // 3600}h ago"
358
+ return f"{seconds // 86400}d ago"
359
+
360
+
361
+ def _format_ttl(ms: int) -> str:
362
+ """Format average TTL in milliseconds to a human-readable string."""
363
+ if ms <= 0:
364
+ return "none"
365
+ seconds = ms // 1000
366
+ if seconds < 60:
367
+ return f"{seconds}s"
368
+ if seconds < 3600:
369
+ return f"{seconds // 60}m"
370
+ if seconds < 86400:
371
+ return f"{seconds // 3600}h"
372
+ return f"{seconds // 86400}d"
373
+
374
+
375
+ def _format_usec(usec: float) -> str:
376
+ """Format microseconds to a human-readable string."""
377
+ if usec < 1000:
378
+ return f"{usec:.1f}us"
379
+ if usec < 1_000_000:
380
+ return f"{usec / 1000:.1f}ms"
381
+ return f"{usec / 1_000_000:.2f}s"
382
+
383
+
384
+ def _format_total_time(usec: int) -> str:
385
+ """Format total microseconds to a readable time string."""
386
+ seconds = usec / 1_000_000
387
+ if seconds < 1:
388
+ return f"{usec / 1000:.1f}ms"
389
+ if seconds < 60:
390
+ return f"{seconds:.1f}s"
391
+ if seconds < 3600:
392
+ return f"{seconds / 60:.1f}m"
393
+ return f"{seconds / 3600:.1f}h"
394
+
395
+
396
+ def _format_bytes_human(nbytes: int) -> str:
397
+ """Format bytes into human-readable string."""
398
+ if nbytes <= 0:
399
+ return "0"
400
+ for unit in ["B", "K", "M", "G", "T"]:
401
+ if nbytes < 1024:
402
+ return f"{nbytes:.1f}{unit}" if nbytes != int(nbytes) else f"{int(nbytes)}{unit}"
403
+ nbytes /= 1024
404
+ return f"{nbytes:.1f}P"
405
+
406
+
407
+ # ---------------------------------------------------------------------------
408
+ # Recommendations engine
409
+ # ---------------------------------------------------------------------------
410
+
411
+ def generate_recommendations(result: RedisAnalysisResult) -> List[Dict[str, str]]:
412
+ """Generate recommendations based on collected metrics."""
413
+ recs: List[Dict[str, str]] = []
414
+
415
+ # Collection failures — surface critical issues when SSH/introspection failed
416
+ if result.collection_status:
417
+ failed = {k: v for k, v in result.collection_status.items() if v.get("status") == "failed"}
418
+ ssh_sources = {"redis_info", "slowlog", "slowlog_entries", "big_keys"}
419
+ ssh_failed = {k: v for k, v in failed.items() if k in ssh_sources}
420
+ if ssh_failed:
421
+ sources = ", ".join(ssh_failed.keys())
422
+ errors = "; ".join(v.get("error", "unknown") for v in ssh_failed.values())
423
+ recs.append({
424
+ "severity": "critical",
425
+ "category": "collection",
426
+ "message": f"SSH introspection failed — unable to collect {sources}. "
427
+ f"Error: {errors}. "
428
+ f"Analysis is incomplete: memory fragmentation, cache hit rate, "
429
+ f"keyspace stats, and persistence health could not be evaluated.",
430
+ })
431
+
432
+ # Memory fragmentation
433
+ if result.memory:
434
+ frag = result.memory.get("mem_fragmentation_ratio", 0)
435
+ if frag > 1.5:
436
+ recs.append({
437
+ "severity": "warning",
438
+ "category": "memory",
439
+ "message": f"High memory fragmentation ({frag:.2f}). Consider restarting Redis to defragment, or enable activedefrag.",
440
+ })
441
+
442
+ # Cache hit rate
443
+ if result.cache:
444
+ hit_rate = result.cache.get("hit_rate", 0)
445
+ if hit_rate < 80 and (result.cache.get("keyspace_hits", 0) + result.cache.get("keyspace_misses", 0)) > 0:
446
+ recs.append({
447
+ "severity": "warning",
448
+ "category": "cache",
449
+ "message": f"Low cache hit rate ({hit_rate:.1f}%). Review key access patterns - many keys may be expired or evicted before use.",
450
+ })
451
+ elif hit_rate < 95 and hit_rate >= 80:
452
+ recs.append({
453
+ "severity": "info",
454
+ "category": "cache",
455
+ "message": f"Cache hit rate at {hit_rate:.1f}% — could be improved. Check if working set fits in memory.",
456
+ })
457
+
458
+ # Evicted keys
459
+ if result.cache:
460
+ evicted = result.cache.get("evicted_keys", 0)
461
+ if evicted > 0:
462
+ recs.append({
463
+ "severity": "warning",
464
+ "category": "memory",
465
+ "message": f"Redis is evicting keys ({_format_number(evicted)} evicted). Increase maxmemory or reduce dataset size.",
466
+ })
467
+
468
+ # Rejected connections
469
+ if result.overview:
470
+ rejected = result.overview.get("rejected_connections", 0)
471
+ if rejected > 0:
472
+ recs.append({
473
+ "severity": "warning",
474
+ "category": "connections",
475
+ "message": f"Connections being rejected ({_format_number(rejected)}). Check maxclients setting.",
476
+ })
477
+
478
+ # Blocked clients
479
+ if result.overview:
480
+ blocked = result.overview.get("blocked_clients", 0)
481
+ if blocked > 0:
482
+ recs.append({
483
+ "severity": "info",
484
+ "category": "connections",
485
+ "message": f"Blocked clients detected ({blocked}). Check for blocking operations (BLPOP, BRPOP, etc.).",
486
+ })
487
+
488
+ # maxmemory not set — on Railway this is expected; autoscaling handles growth
489
+
490
+ # RDB save failure
491
+ if result.persistence:
492
+ rdb_status = result.persistence.get("rdb_last_bgsave_status", "")
493
+ if rdb_status and rdb_status != "ok":
494
+ recs.append({
495
+ "severity": "critical",
496
+ "category": "persistence",
497
+ "message": "Last RDB save failed. Check disk space and permissions.",
498
+ })
499
+
500
+ # Slow log — data-driven when entries are available
501
+ if result.slowlog_entries:
502
+ # Analyze the actual slow commands
503
+ total_entries = len(result.slowlog_entries)
504
+ cmd_counts: Dict[str, int] = {}
505
+ total_duration = 0
506
+ for entry in result.slowlog_entries:
507
+ cmd = entry["command"].split()[0] if entry["command"] else "unknown"
508
+ cmd_counts[cmd] = cmd_counts.get(cmd, 0) + 1
509
+ total_duration += entry["duration_us"]
510
+ top_cmd = max(cmd_counts, key=cmd_counts.get) if cmd_counts else "unknown"
511
+ top_count = cmd_counts.get(top_cmd, 0)
512
+ avg_duration = total_duration / total_entries if total_entries > 0 else 0
513
+
514
+ msg = (f"Slow log contains {result.slowlog_len or total_entries} entries. "
515
+ f"Of the {total_entries} most recent: {top_count} are {top_cmd} commands "
516
+ f"averaging {_format_usec(avg_duration)}.")
517
+ if result.big_keys:
518
+ big_key_types = ", ".join(f"{bk['type']} ({bk['detail']})" for bk in result.big_keys[:3])
519
+ msg += f" Largest keys: {big_key_types} — check if these correlate with slow commands."
520
+ severity = "warning" if (result.slowlog_len or 0) > 100 else "info"
521
+ recs.append({"severity": severity, "category": "performance", "message": msg})
522
+ elif result.slowlog_len is not None and result.slowlog_len > 100:
523
+ recs.append({
524
+ "severity": "warning",
525
+ "category": "performance",
526
+ "message": f"High number of slow log entries ({result.slowlog_len}). Slow log details could not be collected.",
527
+ })
528
+
529
+ # Big keys — standalone recommendation when no slowlog correlation
530
+ if result.big_keys and not result.slowlog_entries:
531
+ big_key_summary = "; ".join(f"{bk['key']} ({bk['type']}: {bk['detail']})" for bk in result.big_keys[:5])
532
+ recs.append({
533
+ "severity": "info",
534
+ "category": "performance",
535
+ "message": f"Largest keys by type: {big_key_summary}. Large keys can cause latency spikes on read/delete operations.",
536
+ })
537
+
538
+ return recs
539
+
540
+
541
+ # ---------------------------------------------------------------------------
542
+ # Report formatting
543
+ # ---------------------------------------------------------------------------
544
+
545
+ def format_report(result: RedisAnalysisResult) -> str:
546
+ """Format the analysis result as a markdown report."""
547
+ lines: List[str] = []
548
+
549
+ lines.append(f"# Redis Analysis: {result.service}")
550
+ lines.append(f"Timestamp: {result.timestamp}")
551
+ lines.append(f"Deployment Status: {result.deployment_status}")
552
+ lines.append("")
553
+
554
+ # --- Overview ---
555
+ if result.overview:
556
+ o = result.overview
557
+ lines.append("## Overview")
558
+ lines.append("| Metric | Value |")
559
+ lines.append("|--------|-------|")
560
+ lines.append(f"| Version | {o.get('redis_version', 'N/A')} |")
561
+ lines.append(f"| Uptime | {_format_uptime(o.get('uptime_in_seconds', 0))} |")
562
+ lines.append(f"| Connected Clients | {o.get('connected_clients', 0):,} |")
563
+ lines.append(f"| Blocked Clients | {o.get('blocked_clients', 0):,} |")
564
+ lines.append(f"| Rejected Connections | {o.get('rejected_connections', 0):,} |")
565
+ lines.append(f"| Total Keys | {result.total_keys:,} |")
566
+ lines.append("")
567
+
568
+ # --- Memory ---
569
+ if result.memory:
570
+ m = result.memory
571
+ lines.append("## Memory")
572
+ lines.append("| Metric | Value | Status |")
573
+ lines.append("|--------|-------|--------|")
574
+
575
+ frag = m.get("mem_fragmentation_ratio", 0)
576
+ frag_status = "OK" if 1.0 <= frag <= 1.5 else ("HIGH" if frag > 1.5 else "LOW")
577
+
578
+ lines.append(f"| Used Memory | {m.get('used_memory_human', 'N/A')} | |")
579
+ lines.append(f"| RSS Memory | {m.get('used_memory_rss_human', 'N/A')} | |")
580
+ lines.append(f"| Peak Memory | {m.get('used_memory_peak_human', 'N/A')} | |")
581
+ lines.append(f"| Fragmentation Ratio | {frag:.2f} | {frag_status} |")
582
+
583
+ maxmem = m.get("maxmemory", 0)
584
+ if maxmem > 0:
585
+ lines.append(f"| Max Memory | {m.get('maxmemory_human', _format_bytes_human(maxmem))} | |")
586
+ else:
587
+ lines.append("| Max Memory | Unlimited | |")
588
+
589
+ lines.append(f"| Eviction Policy | {m.get('maxmemory_policy', 'N/A')} | |")
590
+ lines.append("")
591
+
592
+ # --- Throughput ---
593
+ if result.throughput:
594
+ t = result.throughput
595
+ lines.append("## Throughput")
596
+ lines.append("| Metric | Value |")
597
+ lines.append("|--------|-------|")
598
+ lines.append(f"| Ops/sec | {t.get('instantaneous_ops_per_sec', 0):,} |")
599
+ lines.append(f"| Total Commands | {_format_number(t.get('total_commands_processed', 0))} |")
600
+ lines.append(f"| Total Connections | {_format_number(t.get('total_connections_received', 0))} |")
601
+ if result.slowlog_len is not None:
602
+ lines.append(f"| Slow Log Entries | {result.slowlog_len:,} |")
603
+ lines.append("")
604
+
605
+ # --- Cache Performance ---
606
+ if result.cache:
607
+ c = result.cache
608
+ hit_rate = c.get("hit_rate", 0)
609
+ hit_status = "OK" if hit_rate >= 95 else ("WARN" if hit_rate >= 80 else "LOW")
610
+ evicted = c.get("evicted_keys", 0)
611
+ evict_status = "OK" if evicted == 0 else "WARN"
612
+
613
+ lines.append("## Cache Performance")
614
+ lines.append("| Metric | Value | Status |")
615
+ lines.append("|--------|-------|--------|")
616
+ lines.append(f"| Hit Rate | {hit_rate:.1f}% | {hit_status} |")
617
+ lines.append(f"| Hits | {_format_number(c.get('keyspace_hits', 0))} | |")
618
+ lines.append(f"| Misses | {_format_number(c.get('keyspace_misses', 0))} | |")
619
+ lines.append(f"| Expired Keys | {_format_number(c.get('expired_keys', 0))} | |")
620
+ lines.append(f"| Evicted Keys | {_format_number(evicted)} | {evict_status} |")
621
+ lines.append("")
622
+
623
+ # --- Persistence ---
624
+ if result.persistence:
625
+ p = result.persistence
626
+ rdb_status = p.get("rdb_last_bgsave_status", "unknown")
627
+ rdb_status_display = "OK" if rdb_status == "ok" else "FAIL"
628
+
629
+ lines.append("## Persistence")
630
+ lines.append("| Metric | Value | Status |")
631
+ lines.append("|--------|-------|--------|")
632
+
633
+ rdb_last_save = p.get("rdb_last_save_time", 0)
634
+ if rdb_last_save > 0:
635
+ now_epoch = int(datetime.now(timezone.utc).timestamp())
636
+ save_ago = now_epoch - rdb_last_save
637
+ lines.append(f"| RDB Last Save | {_format_duration(save_ago)} | |")
638
+ else:
639
+ lines.append("| RDB Last Save | never | |")
640
+
641
+ lines.append(f"| RDB Status | {rdb_status} | {rdb_status_display} |")
642
+ lines.append(f"| AOF Enabled | {'Yes' if p.get('aof_enabled') else 'No'} | |")
643
+
644
+ if p.get("aof_enabled"):
645
+ aof_status = p.get("aof_last_rewrite_status", "unknown")
646
+ aof_display = "OK" if aof_status == "ok" else aof_status
647
+ lines.append(f"| AOF Rewrite Status | {aof_status} | {aof_display} |")
648
+
649
+ lines.append("")
650
+
651
+ # --- Command Stats ---
652
+ if result.command_stats:
653
+ top_n = result.command_stats[:20]
654
+ lines.append("## Command Stats (top 20)")
655
+ lines.append("| Command | Calls | Avg Latency | Total Time |")
656
+ lines.append("|---------|-------|-------------|------------|")
657
+ for cs in top_n:
658
+ lines.append(
659
+ f"| {cs['command']} "
660
+ f"| {_format_number(cs['calls'])} "
661
+ f"| {_format_usec(cs['usec_per_call'])} "
662
+ f"| {_format_total_time(cs['usec'])} |"
663
+ )
664
+ lines.append("")
665
+
666
+ # --- Slow Log Entries ---
667
+ if result.slowlog_entries:
668
+ lines.append("## Slow Log Entries (recent)")
669
+ lines.append("| # | Timestamp | Duration | Command |")
670
+ lines.append("|---|-----------|----------|---------|")
671
+ now_epoch = int(datetime.now(timezone.utc).timestamp())
672
+ for entry in result.slowlog_entries:
673
+ age = now_epoch - entry["timestamp_unix"]
674
+ lines.append(
675
+ f"| {entry['id']} "
676
+ f"| {_format_duration(age)} "
677
+ f"| {_format_usec(entry['duration_us'])} "
678
+ f"| {entry['command']} |"
679
+ )
680
+ lines.append("")
681
+
682
+ # --- Biggest Keys ---
683
+ if result.big_keys:
684
+ lines.append("## Biggest Keys")
685
+ lines.append("| Type | Key | Size/Count |")
686
+ lines.append("|------|-----|------------|")
687
+ for bk in result.big_keys:
688
+ lines.append(
689
+ f"| {bk['type']} "
690
+ f"| {bk['key']} "
691
+ f"| {bk['detail']} |"
692
+ )
693
+ lines.append("")
694
+
695
+ # --- Keyspace ---
696
+ if result.keyspace:
697
+ lines.append("## Keyspace")
698
+ lines.append("| Database | Keys | Expires | Avg TTL |")
699
+ lines.append("|----------|------|---------|---------|")
700
+ for db in result.keyspace:
701
+ lines.append(
702
+ f"| {db['db']} "
703
+ f"| {db['keys']:,} "
704
+ f"| {db['expires']:,} "
705
+ f"| {_format_ttl(db['avg_ttl'])} |"
706
+ )
707
+ lines.append("")
708
+
709
+ # --- Infrastructure Metrics ---
710
+ if result.metrics_history:
711
+ windows = result.metrics_history.get("windows", {})
712
+ for window_label, window_data in windows.items():
713
+ mh = window_data.get("metrics", {})
714
+ if not mh:
715
+ continue
716
+ lines.append(f"## Infrastructure Metrics ({window_label})")
717
+ lines.append("| Metric | Current | Min | Max | Avg | Trend |")
718
+ lines.append("|--------|---------|-----|-----|-----|-------|")
719
+ for key in ["cpu", "memory", "disk", "network_rx", "network_tx"]:
720
+ if key in mh:
721
+ entry = mh[key]
722
+ trend = entry.get("trend", {})
723
+ trend_str = trend.get("direction", "N/A")
724
+ change = trend.get("change_pct", 0)
725
+ if change != 0:
726
+ trend_str += f" ({change:+.1f}%)"
727
+ lines.append(
728
+ f"| {key.replace('_', ' ').title()} "
729
+ f"| {entry['current']}{entry['unit']} "
730
+ f"| {entry['min']}{entry['unit']} "
731
+ f"| {entry['max']}{entry['unit']} "
732
+ f"| {entry['avg']}{entry['unit']} "
733
+ f"| {trend_str} |"
734
+ )
735
+ lines.append("")
736
+
737
+ # --- Collection Status ---
738
+ if result.collection_status:
739
+ failed = {k: v for k, v in result.collection_status.items() if v.get("status") == "failed"}
740
+ if failed:
741
+ lines.append("## Collection Issues")
742
+ for source, status in failed.items():
743
+ lines.append(f"- **{source}**: {status.get('error', 'unknown error')}")
744
+ lines.append("")
745
+
746
+ # --- Recommendations ---
747
+ if result.recommendations:
748
+ lines.append("## Recommendations")
749
+ for rec in result.recommendations:
750
+ severity = rec.get("severity", "info").upper()
751
+ lines.append(f"- [{severity}] {rec['message']}")
752
+ lines.append("")
753
+
754
+ return "\n".join(lines)
755
+
756
+
757
+ # ---------------------------------------------------------------------------
758
+ # Main analysis function
759
+ # ---------------------------------------------------------------------------
760
+
761
+ def analyze_redis(service: str, timeout: int = 300, quiet: bool = False,
762
+ skip_logs: bool = False,
763
+ metrics_hours: int = 168,
764
+ project_id: Optional[str] = None,
765
+ environment_id: Optional[str] = None,
766
+ service_id: Optional[str] = None) -> RedisAnalysisResult:
767
+ """Run complete Redis analysis with maximum data collection.
768
+
769
+ Collects Redis INFO ALL, SLOWLOG LEN, SLOWLOG GET 20, --bigkeys,
770
+ Railway metrics, and logs in parallel where possible.
771
+
772
+ Args:
773
+ skip_logs: Skip log fetching for faster analysis
774
+ metrics_hours: Hours of metrics history to fetch (default: 168, max: 168)
775
+ project_id: Project ID (bypasses railway link config)
776
+ environment_id: Environment ID (bypasses railway link config)
777
+ service_id: Service ID (bypasses railway link config)
778
+ """
779
+ if not quiet:
780
+ print(f"Analyzing redis database: {service}", file=sys.stderr)
781
+
782
+ result = RedisAnalysisResult(
783
+ service=service,
784
+ db_type="redis",
785
+ timestamp=datetime.now(timezone.utc).isoformat(),
786
+ )
787
+
788
+ # === FAST CONTEXT LOADING ===
789
+ if not quiet:
790
+ print(" [0/5] Getting Railway context...", file=sys.stderr, flush=True)
791
+ dal._progress_timer.start()
792
+
793
+ if environment_id and service_id:
794
+ dal._ctx = RailwayContext(project_id=project_id, environment_id=environment_id, service_id=service_id)
795
+ if not quiet:
796
+ print(f" using explicit IDs (env={environment_id[:8]}..., svc={service_id[:8]}...)", file=sys.stderr, flush=True)
797
+ else:
798
+ railway_status = get_railway_status()
799
+ if railway_status:
800
+ dal._ctx = RailwayContext(
801
+ project_id=railway_status.get("projectId"),
802
+ environment_id=railway_status.get("environmentId"),
803
+ service_id=railway_status.get("serviceId"),
804
+ )
805
+ environment_id = dal._ctx.environment_id
806
+ service_id = dal._ctx.service_id
807
+
808
+ # Get deployment status via API (~1s)
809
+ progress(1, 5, "Fetching deployment status...", quiet)
810
+ result.deployment_status = get_deployment_status(service, service_id=service_id)
811
+
812
+ # === SSH PRE-CHECK WITH RETRY ===
813
+ progress(2, 5, "Testing SSH connectivity...", quiet)
814
+ ssh_available = False
815
+ ssh_stderr = ""
816
+ ssh_attempts = [30, 60, 90]
817
+ for attempt, attempt_timeout in enumerate(ssh_attempts, 1):
818
+ ssh_code, ssh_stdout, ssh_stderr = run_ssh_query(service, "echo ok", timeout=attempt_timeout)
819
+ if ssh_code == 0 and "ok" in ssh_stdout:
820
+ ssh_available = True
821
+ if not quiet:
822
+ for line in ssh_stderr.splitlines():
823
+ if line.startswith("Using SSH key:"):
824
+ print(f" {line}", file=sys.stderr, flush=True)
825
+ break
826
+ break
827
+ if not quiet:
828
+ remaining = len(ssh_attempts) - attempt
829
+ if remaining > 0:
830
+ 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)
831
+ else:
832
+ print(f" SSH attempt {attempt}/{len(ssh_attempts)} failed ({ssh_stderr or 'no response'}), giving up", file=sys.stderr, flush=True)
833
+
834
+ # === PARALLEL EXECUTION ===
835
+ progress(3, 5, "Running analysis (Redis INFO, slowlog, bigkeys, metrics, logs in parallel)...", quiet)
836
+
837
+ def task_redis_info():
838
+ """Fetch Redis INFO ALL via SSH."""
839
+ if not ssh_available:
840
+ return ("failed", f"SSH not available: {ssh_stderr or 'connection failed'}", "")
841
+ command = 'timeout 30s redis-cli -h localhost -p 6379 -a "$REDISPASSWORD" --no-auth-warning --raw INFO ALL'
842
+ code, stdout, stderr = run_ssh_query(service, command, timeout=45)
843
+ if code == 0 and stdout.strip():
844
+ return ("ok", "", stdout)
845
+ return ("failed", stderr or "empty response", stdout)
846
+
847
+ def task_slowlog():
848
+ """Fetch Redis SLOWLOG LEN via SSH."""
849
+ if not ssh_available:
850
+ return ("failed", f"SSH not available: {ssh_stderr or 'connection failed'}", "")
851
+ command = 'timeout 30s redis-cli -h localhost -p 6379 -a "$REDISPASSWORD" --no-auth-warning --raw SLOWLOG LEN'
852
+ code, stdout, stderr = run_ssh_query(service, command, timeout=45)
853
+ if code == 0 and stdout.strip():
854
+ return ("ok", "", stdout.strip())
855
+ return ("failed", stderr or "empty response", "")
856
+
857
+ def task_slowlog_get():
858
+ """Fetch Redis SLOWLOG GET 20 via SSH for actual slow query details."""
859
+ if not ssh_available:
860
+ return ("failed", f"SSH not available: {ssh_stderr or 'connection failed'}", "")
861
+ command = 'timeout 30s redis-cli -h localhost -p 6379 -a "$REDISPASSWORD" --no-auth-warning --raw SLOWLOG GET 20'
862
+ code, stdout, stderr = run_ssh_query(service, command, timeout=45)
863
+ if code == 0 and stdout.strip():
864
+ return ("ok", "", stdout.strip())
865
+ return ("failed", stderr or "empty response", "")
866
+
867
+ def task_bigkeys():
868
+ """Fetch redis-cli --bigkeys via SSH (SCAN-based, may take longer)."""
869
+ if not ssh_available:
870
+ return ("failed", f"SSH not available: {ssh_stderr or 'connection failed'}", "")
871
+ command = 'timeout 60s redis-cli -h localhost -p 6379 -a "$REDISPASSWORD" --no-auth-warning --bigkeys'
872
+ code, stdout, stderr = run_ssh_query(service, command, timeout=75)
873
+ if code == 0 and stdout.strip():
874
+ return ("ok", "", stdout.strip())
875
+ return ("failed", stderr or "empty response", "")
876
+
877
+ def task_metrics():
878
+ """Fetch all metrics (disk, CPU, memory) in one API call."""
879
+ if environment_id and service_id:
880
+ return get_all_metrics_from_api(environment_id, service_id, hours=metrics_hours)
881
+ return None
882
+
883
+ def task_logs():
884
+ """Fetch recent logs via API (~3s)."""
885
+ if skip_logs:
886
+ return []
887
+ return get_recent_logs(service, lines=LOG_LINES_DEFAULT,
888
+ environment_id=environment_id,
889
+ service_id=service_id)
890
+
891
+ with ThreadPoolExecutor(max_workers=6) as executor:
892
+ future_info = executor.submit(task_redis_info)
893
+ future_slowlog = executor.submit(task_slowlog)
894
+ future_slowlog_get = executor.submit(task_slowlog_get)
895
+ future_bigkeys = executor.submit(task_bigkeys)
896
+ future_metrics = executor.submit(task_metrics)
897
+ future_logs = executor.submit(task_logs)
898
+
899
+ # Collect results
900
+ info_result = future_info.result()
901
+ slowlog_result = future_slowlog.result()
902
+ slowlog_get_result = future_slowlog_get.result()
903
+ bigkeys_result = future_bigkeys.result()
904
+ metrics_result = future_metrics.result()
905
+ logs_result = future_logs.result()
906
+
907
+ # === PROCESS RESULTS ===
908
+ progress(4, 5, "Processing results...", quiet)
909
+
910
+ # Redis INFO ALL
911
+ info_status, info_error, info_raw = info_result
912
+ if info_status == "ok" and info_raw:
913
+ result.collection_status["redis_info"] = {"status": "ok"}
914
+ info = parse_redis_info(info_raw)
915
+
916
+ result.overview = extract_overview(info)
917
+ result.memory = extract_memory(info)
918
+ result.throughput = extract_throughput(info)
919
+ result.cache = extract_cache(info)
920
+ result.persistence = extract_persistence(info)
921
+ result.keyspace, result.total_keys = extract_keyspace(info)
922
+ result.command_stats = extract_command_stats(info)
923
+ else:
924
+ result.collection_status["redis_info"] = {"status": "failed", "error": info_error}
925
+ result.errors.append(f"Redis INFO failed: {info_error}")
926
+
927
+ # SLOWLOG LEN
928
+ sl_status, sl_error, sl_raw = slowlog_result
929
+ if sl_status == "ok" and sl_raw:
930
+ result.collection_status["slowlog"] = {"status": "ok"}
931
+ result.slowlog_len = _safe_int(sl_raw)
932
+ else:
933
+ result.collection_status["slowlog"] = {"status": "failed", "error": sl_error}
934
+
935
+ # SLOWLOG GET 20
936
+ slg_status, slg_error, slg_raw = slowlog_get_result
937
+ if slg_status == "ok" and slg_raw:
938
+ result.collection_status["slowlog_entries"] = {"status": "ok"}
939
+ result.slowlog_entries = parse_slowlog_get(slg_raw)
940
+ else:
941
+ result.collection_status["slowlog_entries"] = {"status": "failed", "error": slg_error}
942
+
943
+ # Big keys
944
+ bk_status, bk_error, bk_raw = bigkeys_result
945
+ if bk_status == "ok" and bk_raw:
946
+ result.collection_status["big_keys"] = {"status": "ok"}
947
+ result.big_keys = parse_bigkeys(bk_raw)
948
+ else:
949
+ result.collection_status["big_keys"] = {"status": "failed", "error": bk_error}
950
+
951
+ # Metrics
952
+ if metrics_result:
953
+ result.collection_status["metrics"] = {"status": "ok"}
954
+ result.metrics_history = metrics_result.get("metrics_history")
955
+ else:
956
+ result.collection_status["metrics"] = {"status": "failed", "error": "no metrics returned"}
957
+
958
+ # Logs
959
+ if logs_result:
960
+ result.collection_status["logs"] = {"status": "ok", "lines": len(logs_result)}
961
+ result.recent_logs = logs_result
962
+ else:
963
+ result.collection_status["logs"] = {"status": "failed", "error": "no logs returned"}
964
+
965
+ # === RECOMMENDATIONS ===
966
+ progress(5, 5, "Generating recommendations...", quiet)
967
+ result.recommendations = generate_recommendations(result)
968
+
969
+ if not quiet:
970
+ elapsed = dal._progress_timer.step_elapsed()
971
+ if elapsed:
972
+ print(f" done{elapsed}", file=sys.stderr, flush=True)
973
+ print(f" Analysis complete{dal._progress_timer.total_elapsed()}", file=sys.stderr, flush=True)
974
+
975
+ return result
976
+
977
+
978
+ # ---------------------------------------------------------------------------
979
+ # Single-step debugging
980
+ # ---------------------------------------------------------------------------
981
+
982
+ def run_single_step(args) -> int:
983
+ """Run a single collection step for debugging."""
984
+ service = args.service
985
+ _init_context(args)
986
+ environment_id = dal._ctx.environment_id
987
+ service_id = dal._ctx.service_id
988
+
989
+ if args.step == "ssh-test":
990
+ print(f"Testing SSH to service: {service}", file=sys.stderr)
991
+ code, stdout, stderr = run_ssh_query(service, "echo ok", timeout=45)
992
+ print(f"Exit code: {code}")
993
+ print(f"Stdout: {stdout.strip()}")
994
+ if stderr:
995
+ print(f"Stderr: {stderr.strip()}")
996
+ return 0 if (code == 0 and "ok" in stdout) else 1
997
+
998
+ elif args.step == "query":
999
+ print(f"Running Redis INFO ALL on: {service}", file=sys.stderr)
1000
+ command = 'timeout 30s redis-cli -h localhost -p 6379 -a "$REDISPASSWORD" --no-auth-warning --raw INFO ALL'
1001
+ code, stdout, stderr = run_ssh_query(service, command, timeout=45)
1002
+ print(f"Exit code: {code}")
1003
+ if code == 0 and stdout:
1004
+ info = parse_redis_info(stdout)
1005
+ print(json.dumps(info, indent=2))
1006
+ else:
1007
+ print(f"Error: {stderr or stdout}")
1008
+ return code
1009
+
1010
+ elif args.step == "logs":
1011
+ print(f"Fetching logs for: {service}", file=sys.stderr)
1012
+ logs = get_recent_logs(service, lines=LOG_LINES_DEFAULT,
1013
+ environment_id=environment_id,
1014
+ service_id=service_id)
1015
+ print(f"Lines fetched: {len(logs)}")
1016
+ for line in logs:
1017
+ print(line)
1018
+ return 0
1019
+
1020
+ elif args.step == "metrics":
1021
+ print(f"Fetching metrics for: {service}", file=sys.stderr)
1022
+ if environment_id and service_id:
1023
+ metrics = get_all_metrics_from_api(environment_id, service_id)
1024
+ if metrics:
1025
+ print(json.dumps(metrics, indent=2))
1026
+ else:
1027
+ print("No metrics returned", file=sys.stderr)
1028
+ return 1
1029
+ else:
1030
+ print("No environment_id or service_id available", file=sys.stderr)
1031
+ return 1
1032
+ return 0
1033
+
1034
+ else:
1035
+ print(f"Unknown step: {args.step}", file=sys.stderr)
1036
+ return 1
1037
+
1038
+
1039
+ # ---------------------------------------------------------------------------
1040
+ # CLI entry point
1041
+ # ---------------------------------------------------------------------------
1042
+
1043
+ def main():
1044
+ parser = argparse.ArgumentParser(
1045
+ description="Redis analysis for Railway services.",
1046
+ formatter_class=argparse.RawDescriptionHelpFormatter,
1047
+ )
1048
+
1049
+ parser.add_argument("--service", required=True, help="Service name")
1050
+ parser.add_argument("--json", action="store_true",
1051
+ help="Output as JSON")
1052
+ parser.add_argument("--timeout", type=int, default=300,
1053
+ help="Timeout in seconds for analysis (default: 300)")
1054
+ parser.add_argument("--quiet", "-q", action="store_true",
1055
+ help="Suppress progress messages")
1056
+ parser.add_argument("--skip-logs", action="store_true",
1057
+ help="Skip log fetching for faster analysis")
1058
+ parser.add_argument("--metrics-hours", type=int, default=168,
1059
+ help="Hours of metrics history to fetch (default: 168, max: 168)")
1060
+ parser.add_argument("--step", choices=["ssh-test", "query", "logs", "metrics"],
1061
+ help="Run a single collection step for debugging")
1062
+ parser.add_argument("--project-id", help="Project ID (bypasses railway link)")
1063
+ parser.add_argument("--environment-id", help="Environment ID (bypasses railway link)")
1064
+ parser.add_argument("--service-id", help="Service ID (bypasses railway link)")
1065
+
1066
+ args = parser.parse_args()
1067
+
1068
+ # Single-step debugging mode
1069
+ if args.step:
1070
+ return run_single_step(args)
1071
+
1072
+ # Run analysis
1073
+ result = analyze_redis(args.service, timeout=args.timeout, quiet=args.quiet,
1074
+ skip_logs=args.skip_logs,
1075
+ metrics_hours=min(args.metrics_hours, 168),
1076
+ project_id=args.project_id,
1077
+ environment_id=args.environment_id,
1078
+ service_id=args.service_id)
1079
+
1080
+ # Output
1081
+ if args.json:
1082
+ print(json.dumps(asdict(result), indent=2))
1083
+ else:
1084
+ print(format_report(result))
1085
+
1086
+ return 0
1087
+
1088
+
1089
+ if __name__ == "__main__":
1090
+ sys.exit(main())