@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.
- package/dist/index.js +11 -4
- package/package.json +3 -4
- package/skills/ATTRIBUTION.md +80 -0
- package/skills/accessibility-audit/SKILL.md +325 -0
- package/skills/accessibility-audit/reference/wcag-checklist.md +103 -0
- package/skills/apns-notifier/SKILL.md +86 -0
- package/skills/approval-policy-enforcer/SKILL.md +66 -0
- package/skills/apps-sdk-builder/LICENSE.txt +201 -0
- package/skills/apps-sdk-builder/SKILL.md +328 -0
- package/skills/apps-sdk-builder/agents/openai.yaml +13 -0
- package/skills/apps-sdk-builder/references/app-archetypes.md +132 -0
- package/skills/apps-sdk-builder/references/apps-sdk-docs-workflow.md +135 -0
- package/skills/apps-sdk-builder/references/interactive-state-sync-patterns.md +113 -0
- package/skills/apps-sdk-builder/references/repo-contract-and-validation.md +93 -0
- package/skills/apps-sdk-builder/references/search-fetch-standard.md +67 -0
- package/skills/apps-sdk-builder/references/upstream-example-workflow.md +79 -0
- package/skills/apps-sdk-builder/references/window-openai-patterns.md +79 -0
- package/skills/apps-sdk-builder/scripts/scaffold_node_ext_apps.mjs +606 -0
- package/skills/architecture-selector/SKILL.md +64 -0
- package/skills/backlog-planner/SKILL.md +68 -0
- package/skills/carplay-entitlement-checker/SKILL.md +82 -0
- package/skills/concept-discovery/SKILL.md +517 -0
- package/skills/concept-discovery/assets/sample-analysis.json +81 -0
- package/skills/concept-discovery/expected_outputs/sample-enum-dictionary.md +25 -0
- package/skills/concept-discovery/expected_outputs/sample-page-user-list.md +83 -0
- package/skills/concept-discovery/expected_outputs/sample-prd-readme.md +43 -0
- package/skills/concept-discovery/references/framework-patterns.md +228 -0
- package/skills/concept-discovery/references/prd-quality-checklist.md +65 -0
- package/skills/concept-discovery/scripts/codebase_analyzer.py +732 -0
- package/skills/concept-discovery/scripts/prd_scaffolder.py +435 -0
- package/skills/dast-zap/SKILL.md +453 -0
- package/skills/dast-zap/assets/.gitkeep +9 -0
- package/skills/dast-zap/assets/github_action.yml +207 -0
- package/skills/dast-zap/assets/gitlab_ci.yml +226 -0
- package/skills/dast-zap/assets/zap_automation.yaml +196 -0
- package/skills/dast-zap/assets/zap_context.xml +192 -0
- package/skills/dast-zap/references/EXAMPLE.md +40 -0
- package/skills/dast-zap/references/api_testing_guide.md +475 -0
- package/skills/dast-zap/references/authentication_guide.md +431 -0
- package/skills/dast-zap/references/false_positive_handling.md +427 -0
- package/skills/dast-zap/references/owasp_mapping.md +255 -0
- package/skills/dep-sbom-scan/SKILL.md +466 -0
- package/skills/deploy-cloudflare/SKILL.md +930 -0
- package/skills/deploy-docker/SKILL.md +55 -0
- package/skills/deploy-fly/SKILL.md +228 -0
- package/skills/deploy-k8s/SKILL.md +108 -0
- package/skills/deploy-k8s/assets/logo.png +0 -0
- package/skills/deploy-k8s/docs/README.md +29 -0
- package/skills/deploy-k8s/docs/SUMMARY.md +56 -0
- package/skills/deploy-k8s/docs/advanced/token-efficiency.md +61 -0
- package/skills/deploy-k8s/docs/architecture/multi-tenancy.md +96 -0
- package/skills/deploy-k8s/docs/architecture/storage-and-state.md +102 -0
- package/skills/deploy-k8s/docs/architecture/workload-patterns.md +87 -0
- package/skills/deploy-k8s/docs/book.json +16 -0
- package/skills/deploy-k8s/docs/community/changelog.md +34 -0
- package/skills/deploy-k8s/docs/community/contributing.md +67 -0
- package/skills/deploy-k8s/docs/core-concepts/failure-modes.md +153 -0
- package/skills/deploy-k8s/docs/core-concepts/philosophy.md +83 -0
- package/skills/deploy-k8s/docs/core-concepts/workflow.md +124 -0
- package/skills/deploy-k8s/docs/examples/bad-patterns.md +47 -0
- package/skills/deploy-k8s/docs/examples/do-dont-checklist.md +37 -0
- package/skills/deploy-k8s/docs/examples/good-patterns.md +49 -0
- package/skills/deploy-k8s/docs/failure-modes/api-drift.md +104 -0
- package/skills/deploy-k8s/docs/failure-modes/fragile-rollouts.md +99 -0
- package/skills/deploy-k8s/docs/failure-modes/insecure-workload-defaults.md +80 -0
- package/skills/deploy-k8s/docs/failure-modes/network-exposure.md +98 -0
- package/skills/deploy-k8s/docs/failure-modes/privilege-sprawl.md +91 -0
- package/skills/deploy-k8s/docs/failure-modes/resource-starvation.md +85 -0
- package/skills/deploy-k8s/docs/getting-started/installation.md +152 -0
- package/skills/deploy-k8s/docs/getting-started/quick-start.md +115 -0
- package/skills/deploy-k8s/docs/guides/helm-patterns.md +71 -0
- package/skills/deploy-k8s/docs/guides/kustomize-patterns.md +65 -0
- package/skills/deploy-k8s/docs/guides/observability.md +67 -0
- package/skills/deploy-k8s/docs/guides/security-hardening.md +59 -0
- package/skills/deploy-k8s/docs/guides/validation-and-policy.md +66 -0
- package/skills/deploy-k8s/docs/integrations/mcp-integration.md +52 -0
- package/skills/deploy-k8s/docs/package-lock.json +2892 -0
- package/skills/deploy-k8s/docs/package.json +13 -0
- package/skills/deploy-k8s/references/api-drift.md +298 -0
- package/skills/deploy-k8s/references/conditional/aks-patterns.md +70 -0
- package/skills/deploy-k8s/references/conditional/eks-patterns.md +79 -0
- package/skills/deploy-k8s/references/conditional/gitops-controllers.md +71 -0
- package/skills/deploy-k8s/references/conditional/gke-patterns.md +74 -0
- package/skills/deploy-k8s/references/conditional/observability-stacks.md +80 -0
- package/skills/deploy-k8s/references/conditional/openshift-patterns.md +67 -0
- package/skills/deploy-k8s/references/daemonset-operator-patterns.md +155 -0
- package/skills/deploy-k8s/references/deployment-patterns.md +146 -0
- package/skills/deploy-k8s/references/do-dont-patterns.md +87 -0
- package/skills/deploy-k8s/references/examples-bad.md +282 -0
- package/skills/deploy-k8s/references/examples-good.md +440 -0
- package/skills/deploy-k8s/references/fragile-rollouts.md +303 -0
- package/skills/deploy-k8s/references/helm-patterns.md +203 -0
- package/skills/deploy-k8s/references/insecure-workload-defaults.md +300 -0
- package/skills/deploy-k8s/references/job-patterns.md +120 -0
- package/skills/deploy-k8s/references/kustomize-patterns.md +239 -0
- package/skills/deploy-k8s/references/multi-tenancy.md +343 -0
- package/skills/deploy-k8s/references/network-exposure.md +481 -0
- package/skills/deploy-k8s/references/observability.md +302 -0
- package/skills/deploy-k8s/references/privilege-sprawl.md +273 -0
- package/skills/deploy-k8s/references/resource-starvation.md +374 -0
- package/skills/deploy-k8s/references/security-hardening.md +209 -0
- package/skills/deploy-k8s/references/stateful-patterns.md +130 -0
- package/skills/deploy-k8s/references/storage-and-state.md +330 -0
- package/skills/deploy-k8s/references/validation-and-policy.md +242 -0
- package/skills/deploy-railway/SKILL.md +235 -0
- package/skills/deploy-railway/references/analyze-db-mongo.md +84 -0
- package/skills/deploy-railway/references/analyze-db-mysql.md +254 -0
- package/skills/deploy-railway/references/analyze-db-postgres.md +479 -0
- package/skills/deploy-railway/references/analyze-db-redis.md +208 -0
- package/skills/deploy-railway/references/analyze-db.md +344 -0
- package/skills/deploy-railway/references/configure.md +309 -0
- package/skills/deploy-railway/references/deploy.md +195 -0
- package/skills/deploy-railway/references/operate.md +214 -0
- package/skills/deploy-railway/references/request.md +248 -0
- package/skills/deploy-railway/references/setup.md +312 -0
- package/skills/deploy-railway/scripts/analyze-mongo.py +1549 -0
- package/skills/deploy-railway/scripts/analyze-mysql.py +1195 -0
- package/skills/deploy-railway/scripts/analyze-postgres.py +3058 -0
- package/skills/deploy-railway/scripts/analyze-redis.py +1090 -0
- package/skills/deploy-railway/scripts/dal.py +671 -0
- package/skills/deploy-railway/scripts/enable-pg-stats.py +170 -0
- package/skills/deploy-railway/scripts/pg-extensions.py +370 -0
- package/skills/deploy-railway/scripts/railway-api.sh +52 -0
- package/skills/deploy-ssh/SKILL.md +91 -0
- package/skills/deploy-vercel/SKILL.md +304 -0
- package/skills/deploy-vercel/resources/deploy-codex.sh +301 -0
- package/skills/deploy-vercel/resources/deploy.sh +301 -0
- package/skills/docs-runbooks/SKILL.md +399 -0
- package/skills/drive-status-renderer/SKILL.md +62 -0
- package/skills/iac-scan/SKILL.md +680 -0
- package/skills/iac-scan/assets/.gitkeep +9 -0
- package/skills/iac-scan/assets/checkov_config.yaml +94 -0
- package/skills/iac-scan/assets/github_actions.yml +199 -0
- package/skills/iac-scan/assets/gitlab_ci.yml +218 -0
- package/skills/iac-scan/assets/pre_commit_config.yaml +92 -0
- package/skills/iac-scan/references/EXAMPLE.md +40 -0
- package/skills/iac-scan/references/compliance_mapping.md +237 -0
- package/skills/iac-scan/references/custom_policies.md +460 -0
- package/skills/iac-scan/references/suppression_guide.md +431 -0
- package/skills/incident-briefing/SKILL.md +66 -0
- package/skills/incident-triage/SKILL.md +481 -0
- package/{LICENSE → skills/mcp-builder/LICENSE.txt} +15 -14
- package/skills/mcp-builder/SKILL.md +244 -0
- package/skills/mcp-builder/reference/evaluation.md +602 -0
- package/skills/mcp-builder/reference/mcp_best_practices.md +249 -0
- package/skills/mcp-builder/reference/node_mcp_server.md +970 -0
- package/skills/mcp-builder/reference/python_mcp_server.md +719 -0
- package/skills/mcp-builder/scripts/connections.py +151 -0
- package/skills/mcp-builder/scripts/evaluation.py +373 -0
- package/skills/mcp-builder/scripts/example_evaluation.xml +22 -0
- package/skills/mcp-builder/scripts/requirements.txt +2 -0
- package/skills/mobile-pairing/SKILL.md +52 -0
- package/skills/ops-sre/SKILL.md +297 -0
- package/skills/playwright-qa/LICENSE.txt +201 -0
- package/skills/playwright-qa/NOTICE.txt +14 -0
- package/skills/playwright-qa/SKILL.md +156 -0
- package/skills/playwright-qa/agents/openai.yaml +6 -0
- package/skills/playwright-qa/assets/playwright-small.svg +3 -0
- package/skills/playwright-qa/assets/playwright.png +0 -0
- package/skills/playwright-qa/references/cli.md +116 -0
- package/skills/playwright-qa/references/workflows.md +95 -0
- package/skills/playwright-qa/scripts/playwright_cli.sh +25 -0
- package/skills/release-publish/SKILL.md +85 -0
- package/skills/repo-bootstrap/SKILL.md +92 -0
- package/skills/repo-bootstrap/assets/example-workflows/validate-agents.yml +89 -0
- package/skills/repo-bootstrap/assets/root-thin.md +141 -0
- package/skills/repo-bootstrap/assets/root-verbose.md +149 -0
- package/skills/repo-bootstrap/assets/scoped/backend-go.md +107 -0
- package/skills/repo-bootstrap/assets/scoped/backend-php.md +94 -0
- package/skills/repo-bootstrap/assets/scoped/backend-python.md +84 -0
- package/skills/repo-bootstrap/assets/scoped/backend-typescript.md +89 -0
- package/skills/repo-bootstrap/assets/scoped/claude-code-skill.md +101 -0
- package/skills/repo-bootstrap/assets/scoped/cli.md +83 -0
- package/skills/repo-bootstrap/assets/scoped/concourse.md +196 -0
- package/skills/repo-bootstrap/assets/scoped/ddev.md +68 -0
- package/skills/repo-bootstrap/assets/scoped/docker.md +160 -0
- package/skills/repo-bootstrap/assets/scoped/documentation.md +98 -0
- package/skills/repo-bootstrap/assets/scoped/examples.md +96 -0
- package/skills/repo-bootstrap/assets/scoped/frontend-typescript.md +88 -0
- package/skills/repo-bootstrap/assets/scoped/github-actions.md +174 -0
- package/skills/repo-bootstrap/assets/scoped/gitlab-ci.md +174 -0
- package/skills/repo-bootstrap/assets/scoped/oro-bundle.md +209 -0
- package/skills/repo-bootstrap/assets/scoped/oro-project.md +170 -0
- package/skills/repo-bootstrap/assets/scoped/python-modern.md +170 -0
- package/skills/repo-bootstrap/assets/scoped/resources.md +96 -0
- package/skills/repo-bootstrap/assets/scoped/skill-repo.md +139 -0
- package/skills/repo-bootstrap/assets/scoped/symfony.md +168 -0
- package/skills/repo-bootstrap/assets/scoped/testing.md +87 -0
- package/skills/repo-bootstrap/assets/scoped/typo3-docs.md +103 -0
- package/skills/repo-bootstrap/assets/scoped/typo3-extension.md +133 -0
- package/skills/repo-bootstrap/assets/scoped/typo3-project.md +137 -0
- package/skills/repo-bootstrap/assets/scoped/typo3-testing.md +80 -0
- package/skills/repo-bootstrap/checkpoints.yaml +279 -0
- package/skills/repo-bootstrap/evals/evals.json +385 -0
- package/skills/repo-bootstrap/references/ai-contribution-guidelines.md +63 -0
- package/skills/repo-bootstrap/references/ai-tool-compatibility.md +223 -0
- package/skills/repo-bootstrap/references/directory-coverage.md +82 -0
- package/skills/repo-bootstrap/references/examples/coding-agent-cli/AGENTS.md +70 -0
- package/skills/repo-bootstrap/references/examples/coding-agent-cli/go.mod +3 -0
- package/skills/repo-bootstrap/references/examples/coding-agent-cli/scripts-AGENTS.md +389 -0
- package/skills/repo-bootstrap/references/examples/express-api-ts/.env.example +13 -0
- package/skills/repo-bootstrap/references/examples/express-api-ts/AGENTS.md +91 -0
- package/skills/repo-bootstrap/references/examples/express-api-ts/package.json +33 -0
- package/skills/repo-bootstrap/references/examples/express-api-ts/pnpm-lock.yaml +3 -0
- package/skills/repo-bootstrap/references/examples/express-api-ts/src/AGENTS.md +91 -0
- package/skills/repo-bootstrap/references/examples/express-api-ts/src/config.ts +28 -0
- package/skills/repo-bootstrap/references/examples/express-api-ts/src/controllers/userController.ts +74 -0
- package/skills/repo-bootstrap/references/examples/express-api-ts/src/index.ts +26 -0
- package/skills/repo-bootstrap/references/examples/express-api-ts/src/middleware/errorHandler.ts +45 -0
- package/skills/repo-bootstrap/references/examples/express-api-ts/src/middleware/requestLogger.ts +18 -0
- package/skills/repo-bootstrap/references/examples/express-api-ts/src/routes/health.ts +18 -0
- package/skills/repo-bootstrap/references/examples/express-api-ts/src/routes/users.ts +13 -0
- package/skills/repo-bootstrap/references/examples/express-api-ts/src/utils/errors.ts +40 -0
- package/skills/repo-bootstrap/references/examples/express-api-ts/src/utils/logger.ts +14 -0
- package/skills/repo-bootstrap/references/examples/express-api-ts/tsconfig.json +24 -0
- package/skills/repo-bootstrap/references/examples/fastapi-app/.env.example +19 -0
- package/skills/repo-bootstrap/references/examples/fastapi-app/AGENTS.md +92 -0
- package/skills/repo-bootstrap/references/examples/fastapi-app/pyproject.toml +88 -0
- package/skills/repo-bootstrap/references/examples/fastapi-app/src/AGENTS.md +85 -0
- package/skills/repo-bootstrap/references/examples/fastapi-app/src/__init__.py +3 -0
- package/skills/repo-bootstrap/references/examples/fastapi-app/src/config.py +49 -0
- package/skills/repo-bootstrap/references/examples/fastapi-app/src/main.py +66 -0
- package/skills/repo-bootstrap/references/examples/fastapi-app/src/models/__init__.py +13 -0
- package/skills/repo-bootstrap/references/examples/fastapi-app/src/models/item.py +43 -0
- package/skills/repo-bootstrap/references/examples/fastapi-app/src/models/user.py +40 -0
- package/skills/repo-bootstrap/references/examples/fastapi-app/src/routes/__init__.py +5 -0
- package/skills/repo-bootstrap/references/examples/fastapi-app/src/routes/health.py +20 -0
- package/skills/repo-bootstrap/references/examples/fastapi-app/src/routes/items.py +61 -0
- package/skills/repo-bootstrap/references/examples/fastapi-app/src/routes/users.py +55 -0
- package/skills/repo-bootstrap/references/examples/fastapi-app/src/services/__init__.py +6 -0
- package/skills/repo-bootstrap/references/examples/fastapi-app/src/services/item_service.py +77 -0
- package/skills/repo-bootstrap/references/examples/fastapi-app/src/services/user_service.py +69 -0
- package/skills/repo-bootstrap/references/examples/fastapi-app/uv.lock +4 -0
- package/skills/repo-bootstrap/references/examples/go-api-with-react-admin/.scopes +3 -0
- package/skills/repo-bootstrap/references/examples/go-api-with-react-admin/AGENTS.md +86 -0
- package/skills/repo-bootstrap/references/examples/go-api-with-react-admin/admin/package.json +20 -0
- package/skills/repo-bootstrap/references/examples/go-api-with-react-admin/admin/src/App.tsx +5 -0
- package/skills/repo-bootstrap/references/examples/go-api-with-react-admin/cmd/api/main.go +7 -0
- package/skills/repo-bootstrap/references/examples/go-api-with-react-admin/go.mod +2 -0
- package/skills/repo-bootstrap/references/examples/go-api-with-react-admin/main.go +7 -0
- package/skills/repo-bootstrap/references/examples/go-with-internal-web-tsx/.scopes +3 -0
- package/skills/repo-bootstrap/references/examples/go-with-internal-web-tsx/AGENTS.md +89 -0
- package/skills/repo-bootstrap/references/examples/go-with-internal-web-tsx/go.mod +2 -0
- package/skills/repo-bootstrap/references/examples/go-with-internal-web-tsx/internal/web/AGENTS.md +90 -0
- package/skills/repo-bootstrap/references/examples/go-with-internal-web-tsx/internal/web/package.json +17 -0
- package/skills/repo-bootstrap/references/examples/go-with-internal-web-tsx/internal/web/src/App.tsx +1 -0
- package/skills/repo-bootstrap/references/examples/go-with-internal-web-tsx/internal/web/src/Button.tsx +1 -0
- package/skills/repo-bootstrap/references/examples/go-with-internal-web-tsx/internal/web/src/Footer.tsx +1 -0
- package/skills/repo-bootstrap/references/examples/go-with-internal-web-tsx/internal/web/src/Header.tsx +1 -0
- package/skills/repo-bootstrap/references/examples/go-with-internal-web-tsx/internal/web/src/Sidebar.tsx +1 -0
- package/skills/repo-bootstrap/references/examples/go-with-internal-web-tsx/main.go +7 -0
- package/skills/repo-bootstrap/references/examples/go-with-internal-web-tsx/package-lock.json +0 -0
- package/skills/repo-bootstrap/references/examples/go-with-internal-web-tsx/package.json +12 -0
- package/skills/repo-bootstrap/references/examples/ldap-selfservice/AGENTS.md +70 -0
- package/skills/repo-bootstrap/references/examples/ldap-selfservice/go.mod +3 -0
- package/skills/repo-bootstrap/references/examples/ldap-selfservice/internal-AGENTS.md +371 -0
- package/skills/repo-bootstrap/references/examples/ldap-selfservice/internal-web-AGENTS.md +448 -0
- package/skills/repo-bootstrap/references/examples/php-with-frontend/.scopes +3 -0
- package/skills/repo-bootstrap/references/examples/php-with-frontend/AGENTS.md +91 -0
- package/skills/repo-bootstrap/references/examples/php-with-frontend/composer.json +8 -0
- package/skills/repo-bootstrap/references/examples/php-with-frontend/package.json +15 -0
- package/skills/repo-bootstrap/references/examples/php-with-frontend/pnpm-lock.yaml +0 -0
- package/skills/repo-bootstrap/references/examples/php-with-frontend/src/Controller.php +3 -0
- package/skills/repo-bootstrap/references/examples/php-with-frontend/web/AGENTS.md +92 -0
- package/skills/repo-bootstrap/references/examples/php-with-frontend/web/package.json +26 -0
- package/skills/repo-bootstrap/references/examples/php-with-frontend/web/src/App.tsx +3 -0
- package/skills/repo-bootstrap/references/examples/php-with-frontend/web/src/Button.tsx +10 -0
- package/skills/repo-bootstrap/references/examples/php-with-frontend/web/src/Footer.tsx +9 -0
- package/skills/repo-bootstrap/references/examples/php-with-frontend/web/src/Header.tsx +9 -0
- package/skills/repo-bootstrap/references/examples/php-with-frontend/web/src/main.tsx +3 -0
- package/skills/repo-bootstrap/references/examples/php-with-frontend/web/tsconfig.json +13 -0
- package/skills/repo-bootstrap/references/examples/pnpm-workspace/AGENTS.md +75 -0
- package/skills/repo-bootstrap/references/examples/pnpm-workspace/package.json +7 -0
- package/skills/repo-bootstrap/references/examples/pnpm-workspace/packages/web/package.json +11 -0
- package/skills/repo-bootstrap/references/examples/pnpm-workspace/packages/web/src/index.ts +11 -0
- package/skills/repo-bootstrap/references/examples/pnpm-workspace/pnpm-lock.yaml +42 -0
- package/skills/repo-bootstrap/references/examples/pnpm-workspace/pnpm-workspace.yaml +2 -0
- package/skills/repo-bootstrap/references/examples/simple-ldap-go/AGENTS.md +70 -0
- package/skills/repo-bootstrap/references/examples/simple-ldap-go/examples-AGENTS.md +45 -0
- package/skills/repo-bootstrap/references/examples/simple-ldap-go/go.mod +3 -0
- package/skills/repo-bootstrap/references/examples/t3x-rte-ckeditor-image/AGENTS.md +70 -0
- package/skills/repo-bootstrap/references/examples/t3x-rte-ckeditor-image/Classes-AGENTS.md +392 -0
- package/skills/repo-bootstrap/references/examples/t3x-rte-ckeditor-image/composer.json +8 -0
- package/skills/repo-bootstrap/references/feedback-memory-schema.md +135 -0
- package/skills/repo-bootstrap/references/git-hooks-setup.md +79 -0
- package/skills/repo-bootstrap/references/output-structure.md +124 -0
- package/skills/repo-bootstrap/references/scripts-guide.md +175 -0
- package/skills/repo-bootstrap/references/verification-guide.md +137 -0
- package/skills/repo-bootstrap/scripts/analyze-git-history.sh +315 -0
- package/skills/repo-bootstrap/scripts/check-freshness.sh +230 -0
- package/skills/repo-bootstrap/scripts/detect-golden-samples.sh +161 -0
- package/skills/repo-bootstrap/scripts/detect-heuristics.sh +93 -0
- package/skills/repo-bootstrap/scripts/detect-project.sh +486 -0
- package/skills/repo-bootstrap/scripts/detect-scopes.sh +330 -0
- package/skills/repo-bootstrap/scripts/detect-utilities.sh +133 -0
- package/skills/repo-bootstrap/scripts/extract-adrs.sh +194 -0
- package/skills/repo-bootstrap/scripts/extract-agent-configs.sh +331 -0
- package/skills/repo-bootstrap/scripts/extract-architecture-rules.sh +522 -0
- package/skills/repo-bootstrap/scripts/extract-ci-commands.sh +385 -0
- package/skills/repo-bootstrap/scripts/extract-ci-rules.sh +384 -0
- package/skills/repo-bootstrap/scripts/extract-commands.sh +358 -0
- package/skills/repo-bootstrap/scripts/extract-documentation.sh +308 -0
- package/skills/repo-bootstrap/scripts/extract-github-rulesets.sh +96 -0
- package/skills/repo-bootstrap/scripts/extract-github-settings.sh +88 -0
- package/skills/repo-bootstrap/scripts/extract-ide-settings.sh +228 -0
- package/skills/repo-bootstrap/scripts/extract-platform-files.sh +290 -0
- package/skills/repo-bootstrap/scripts/extract-quality-configs.sh +442 -0
- package/skills/repo-bootstrap/scripts/generate-agents.sh +2424 -0
- package/skills/repo-bootstrap/scripts/generate-file-map.sh +153 -0
- package/skills/repo-bootstrap/scripts/lib/config-root.sh +211 -0
- package/skills/repo-bootstrap/scripts/lib/summary.sh +244 -0
- package/skills/repo-bootstrap/scripts/lib/template.sh +397 -0
- package/skills/repo-bootstrap/scripts/validate-structure.sh +324 -0
- package/skills/repo-bootstrap/scripts/verify-commands.sh +615 -0
- package/skills/repo-bootstrap/scripts/verify-content.sh +302 -0
- package/skills/schema-api-contracts/SKILL.md +56 -0
- package/skills/secret-hygiene/SKILL.md +511 -0
- package/skills/secret-hygiene/assets/.gitkeep +9 -0
- package/skills/secret-hygiene/assets/config-balanced.toml +81 -0
- package/skills/secret-hygiene/assets/config-custom.toml +178 -0
- package/skills/secret-hygiene/assets/config-strict.toml +48 -0
- package/skills/secret-hygiene/assets/github-action.yml +181 -0
- package/skills/secret-hygiene/assets/gitlab-ci.yml +257 -0
- package/skills/secret-hygiene/assets/precommit-config.yaml +70 -0
- package/skills/secret-hygiene/references/EXAMPLE.md +40 -0
- package/skills/secret-hygiene/references/compliance_mapping.md +538 -0
- package/skills/secret-hygiene/references/detection_rules.md +276 -0
- package/skills/secret-hygiene/references/false_positives.md +598 -0
- package/skills/secret-hygiene/references/remediation_guide.md +530 -0
- package/skills/stack-selector/SKILL.md +56 -0
- package/skills/telegram-control/SKILL.md +110 -0
- package/skills/telegram-control/references/architecture.md +184 -0
- package/skills/telegram-control/references/convex.md +173 -0
- package/skills/telegram-control/references/error_handling.md +212 -0
- package/skills/telegram-control/references/initial_setup.md +165 -0
- package/skills/telegram-control/references/telegram_api.md +156 -0
- package/skills/telegram-control/scripts/cancel_message.ts +53 -0
- package/skills/telegram-control/scripts/list_scheduled.ts +103 -0
- package/skills/telegram-control/scripts/logger.ts +121 -0
- package/skills/telegram-control/scripts/proxy-util.ts +11 -0
- package/skills/telegram-control/scripts/schedule_message.ts +216 -0
- package/skills/telegram-control/scripts/send_message.ts +115 -0
- package/skills/telegram-control/scripts/setup.ts +185 -0
- package/skills/telegram-control/scripts/types.ts +75 -0
- package/skills/telegram-control/scripts/view_history.ts +74 -0
- package/skills/test-strategy/SKILL.md +352 -0
- package/skills/threat-model/SKILL.md +303 -0
- package/skills/threat-model/examples/example-output.md +196 -0
- package/skills/threat-model/template.md +96 -0
- package/skills/ts-lint/SKILL.md +80 -0
- package/skills/ui-flow/SKILL.md +668 -0
- package/skills/voice-command-router/SKILL.md +51 -0
- package/skills/widget-live-activity-sync/SKILL.md +66 -0
|
@@ -0,0 +1,671 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Shared Railway infrastructure helpers for database analysis scripts."""
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
from datetime import datetime, timezone
|
|
9
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
|
|
12
|
+
LOG_LINES_DEFAULT = 1000 # Number of log lines to fetch via API
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ProgressTimer:
|
|
16
|
+
"""Track elapsed time for progress messages."""
|
|
17
|
+
def __init__(self):
|
|
18
|
+
self.start_time = None
|
|
19
|
+
self.step_start_time = None
|
|
20
|
+
|
|
21
|
+
def start(self):
|
|
22
|
+
"""Start the overall timer."""
|
|
23
|
+
self.start_time = datetime.now()
|
|
24
|
+
self.step_start_time = self.start_time
|
|
25
|
+
|
|
26
|
+
def step_elapsed(self) -> str:
|
|
27
|
+
"""Get elapsed time since last step, then reset step timer."""
|
|
28
|
+
if self.step_start_time is None:
|
|
29
|
+
return ""
|
|
30
|
+
now = datetime.now()
|
|
31
|
+
elapsed = (now - self.step_start_time).total_seconds()
|
|
32
|
+
self.step_start_time = now
|
|
33
|
+
if elapsed < 0.1:
|
|
34
|
+
return ""
|
|
35
|
+
return f" ({elapsed:.1f}s)"
|
|
36
|
+
|
|
37
|
+
def total_elapsed(self) -> str:
|
|
38
|
+
"""Get total elapsed time."""
|
|
39
|
+
if self.start_time is None:
|
|
40
|
+
return ""
|
|
41
|
+
elapsed = (datetime.now() - self.start_time).total_seconds()
|
|
42
|
+
return f" (total: {elapsed:.1f}s)"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# Global timer instance
|
|
46
|
+
_progress_timer = ProgressTimer()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class RailwayContext:
|
|
51
|
+
"""Explicit Railway IDs that bypass railway link."""
|
|
52
|
+
project_id: Optional[str] = None
|
|
53
|
+
environment_id: Optional[str] = None
|
|
54
|
+
service_id: Optional[str] = None
|
|
55
|
+
|
|
56
|
+
def ssh_flags(self) -> List[str]:
|
|
57
|
+
"""Return CLI flags for railway ssh."""
|
|
58
|
+
flags = ["--native"]
|
|
59
|
+
if self.project_id:
|
|
60
|
+
flags.extend(["--project", self.project_id])
|
|
61
|
+
if self.environment_id:
|
|
62
|
+
flags.extend(["--environment", self.environment_id])
|
|
63
|
+
if self.service_id:
|
|
64
|
+
flags.extend(["--service", self.service_id])
|
|
65
|
+
return flags
|
|
66
|
+
|
|
67
|
+
def logs_flags(self) -> List[str]:
|
|
68
|
+
"""Return CLI flags for railway logs."""
|
|
69
|
+
flags = []
|
|
70
|
+
if self.environment_id:
|
|
71
|
+
flags.extend(["--environment", self.environment_id])
|
|
72
|
+
return flags
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# Global context — set once at startup, used by all CLI calls
|
|
76
|
+
_ctx = RailwayContext()
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _init_context(args) -> None:
|
|
80
|
+
"""Initialize global context from CLI args or railway config."""
|
|
81
|
+
global _ctx
|
|
82
|
+
if args.environment_id and args.service_id:
|
|
83
|
+
_ctx = RailwayContext(
|
|
84
|
+
project_id=getattr(args, 'project_id', None),
|
|
85
|
+
environment_id=args.environment_id,
|
|
86
|
+
service_id=args.service_id,
|
|
87
|
+
)
|
|
88
|
+
else:
|
|
89
|
+
railway_status = get_railway_status()
|
|
90
|
+
if railway_status:
|
|
91
|
+
_ctx = RailwayContext(
|
|
92
|
+
project_id=railway_status.get("projectId"),
|
|
93
|
+
environment_id=railway_status.get("environmentId"),
|
|
94
|
+
service_id=railway_status.get("serviceId"),
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def progress(step: int, total: int, message: str, quiet: bool = False):
|
|
99
|
+
"""Print progress message to stderr with elapsed time."""
|
|
100
|
+
if not quiet:
|
|
101
|
+
# Show elapsed time from previous step (before current step message)
|
|
102
|
+
elapsed = _progress_timer.step_elapsed()
|
|
103
|
+
if elapsed:
|
|
104
|
+
print(f" done{elapsed}", file=sys.stderr, flush=True)
|
|
105
|
+
print(f" [{step}/{total}] {message}", file=sys.stderr, flush=True)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def run_railway_command(args: List[str], timeout: int = 30) -> Tuple[int, str, str]:
|
|
109
|
+
"""Run a railway CLI command and return (returncode, stdout, stderr)."""
|
|
110
|
+
try:
|
|
111
|
+
result = subprocess.run(
|
|
112
|
+
["railway"] + args,
|
|
113
|
+
capture_output=True,
|
|
114
|
+
text=True,
|
|
115
|
+
timeout=timeout
|
|
116
|
+
)
|
|
117
|
+
return result.returncode, result.stdout, result.stderr
|
|
118
|
+
except subprocess.TimeoutExpired:
|
|
119
|
+
return 124, "", "Command timed out"
|
|
120
|
+
except FileNotFoundError:
|
|
121
|
+
return 127, "", "railway CLI not found"
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _cli_fatal_error(returncode: int, stderr: str) -> Optional[str]:
|
|
125
|
+
"""Return a friendly error string if the CLI itself is broken, else None.
|
|
126
|
+
|
|
127
|
+
These errors are unrecoverable — retrying won't help.
|
|
128
|
+
"""
|
|
129
|
+
if returncode == 127 or "railway CLI not found" in stderr:
|
|
130
|
+
return (
|
|
131
|
+
"Railway CLI not found. "
|
|
132
|
+
"Install it with: npm i -g @railway/cli "
|
|
133
|
+
"or brew install railway"
|
|
134
|
+
)
|
|
135
|
+
lower = stderr.lower()
|
|
136
|
+
if "unknown flag" in lower or "flag provided but not defined" in lower:
|
|
137
|
+
return (
|
|
138
|
+
"Railway CLI is outdated — the --native SSH flag is not supported. "
|
|
139
|
+
"Update it with: npm i -g @railway/cli@latest "
|
|
140
|
+
"or brew upgrade railway"
|
|
141
|
+
)
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def run_ssh_query(service: str, command: str, timeout: int = 60,
|
|
146
|
+
max_attempts: int = 3) -> Tuple[int, str, str]:
|
|
147
|
+
"""Run a command via railway ssh, retrying up to max_attempts times.
|
|
148
|
+
|
|
149
|
+
Passes the command as a single argument after '--'. Railway ssh
|
|
150
|
+
interprets it through a shell on the remote end, so pipes, env vars,
|
|
151
|
+
and redirects all work without an explicit sh -c wrapper.
|
|
152
|
+
|
|
153
|
+
Retries on non-zero exit code or empty stdout (covers transient errors
|
|
154
|
+
like 'exec request failed on channel 0'). Never retries when the CLI
|
|
155
|
+
itself is missing or outdated — those errors are unrecoverable.
|
|
156
|
+
"""
|
|
157
|
+
flags = _ctx.ssh_flags()
|
|
158
|
+
# Only pass --service <name> if context didn't already provide --service <id>
|
|
159
|
+
if not _ctx.service_id:
|
|
160
|
+
flags += ["--service", service]
|
|
161
|
+
args = ["ssh"] + flags + ["--", command]
|
|
162
|
+
last_code, last_stdout, last_stderr = 1, "", ""
|
|
163
|
+
for attempt in range(1, max_attempts + 1):
|
|
164
|
+
last_code, last_stdout, last_stderr = run_railway_command(args, timeout)
|
|
165
|
+
if last_code == 0 and last_stdout.strip():
|
|
166
|
+
return last_code, last_stdout, last_stderr
|
|
167
|
+
fatal = _cli_fatal_error(last_code, last_stderr)
|
|
168
|
+
if fatal:
|
|
169
|
+
return last_code, last_stdout, fatal
|
|
170
|
+
if attempt < max_attempts:
|
|
171
|
+
print(
|
|
172
|
+
f" SSH attempt {attempt}/{max_attempts} failed "
|
|
173
|
+
f"({last_stderr.strip() or 'empty response'}), retrying...",
|
|
174
|
+
file=sys.stderr, flush=True,
|
|
175
|
+
)
|
|
176
|
+
return last_code, last_stdout, last_stderr
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def run_psql_query(service: str, query: str, timeout: int = 60) -> Tuple[int, str]:
|
|
180
|
+
"""Run a psql query via railway ssh and return (returncode, output).
|
|
181
|
+
|
|
182
|
+
Normalizes query whitespace and suppresses psql warnings (e.g. collation
|
|
183
|
+
version mismatch) that would otherwise pollute stdout.
|
|
184
|
+
"""
|
|
185
|
+
query = " ".join(query.split())
|
|
186
|
+
command = f'''PAGER='' psql $DATABASE_URL -P pager=off -t -A -c "{query}" 2>/dev/null'''
|
|
187
|
+
code, stdout, stderr = run_ssh_query(service, command, timeout)
|
|
188
|
+
if code != 0:
|
|
189
|
+
return code, stderr or stdout
|
|
190
|
+
return 0, stdout
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def get_railway_status() -> Optional[Dict[str, Any]]:
|
|
194
|
+
"""Get environment and service IDs from Railway config file.
|
|
195
|
+
|
|
196
|
+
Reads directly from ~/.railway/config.json instead of calling CLI (~15s saved).
|
|
197
|
+
"""
|
|
198
|
+
config_path = os.path.expanduser("~/.railway/config.json")
|
|
199
|
+
if not os.path.exists(config_path):
|
|
200
|
+
return None
|
|
201
|
+
|
|
202
|
+
try:
|
|
203
|
+
with open(config_path, "r") as f:
|
|
204
|
+
config = json.load(f)
|
|
205
|
+
|
|
206
|
+
# Get linked project for current directory
|
|
207
|
+
cwd = os.getcwd()
|
|
208
|
+
projects = config.get("projects", {})
|
|
209
|
+
|
|
210
|
+
# Find project config for current directory or parent
|
|
211
|
+
project_config = None
|
|
212
|
+
check_path = cwd
|
|
213
|
+
while check_path != "/":
|
|
214
|
+
if check_path in projects:
|
|
215
|
+
project_config = projects[check_path]
|
|
216
|
+
break
|
|
217
|
+
check_path = os.path.dirname(check_path)
|
|
218
|
+
|
|
219
|
+
if not project_config:
|
|
220
|
+
return None
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
"projectId": project_config.get("project"),
|
|
224
|
+
"environmentId": project_config.get("environment"),
|
|
225
|
+
"serviceId": project_config.get("service"),
|
|
226
|
+
"serviceName": project_config.get("name", ""),
|
|
227
|
+
}
|
|
228
|
+
except (json.JSONDecodeError, IOError):
|
|
229
|
+
return None
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def get_deployment_status(service: str, service_id: Optional[str] = None) -> str:
|
|
233
|
+
"""Get deployment status for service.
|
|
234
|
+
|
|
235
|
+
Uses direct API call if service_id provided (~1s), falls back to CLI (~15s).
|
|
236
|
+
"""
|
|
237
|
+
# Fast path: use API directly if we have service_id
|
|
238
|
+
if service_id:
|
|
239
|
+
script_dir = os.path.dirname(os.path.abspath(__file__))
|
|
240
|
+
api_script = os.path.join(script_dir, "railway-api.sh")
|
|
241
|
+
|
|
242
|
+
if os.path.exists(api_script):
|
|
243
|
+
query = '''query svc($id: String!) {
|
|
244
|
+
service(id: $id) {
|
|
245
|
+
deployments(first: 1) {
|
|
246
|
+
edges { node { status } }
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}'''
|
|
250
|
+
try:
|
|
251
|
+
result = subprocess.run(
|
|
252
|
+
[api_script, query, json.dumps({"id": service_id})],
|
|
253
|
+
capture_output=True,
|
|
254
|
+
text=True,
|
|
255
|
+
timeout=10
|
|
256
|
+
)
|
|
257
|
+
if result.returncode == 0:
|
|
258
|
+
data = json.loads(result.stdout)
|
|
259
|
+
edges = data.get("data", {}).get("service", {}).get("deployments", {}).get("edges", [])
|
|
260
|
+
if edges:
|
|
261
|
+
return edges[0].get("node", {}).get("status", "UNKNOWN")
|
|
262
|
+
except (subprocess.TimeoutExpired, json.JSONDecodeError):
|
|
263
|
+
pass
|
|
264
|
+
|
|
265
|
+
# Fallback: use CLI (slow, ~15s)
|
|
266
|
+
code, stdout, stderr = run_railway_command(
|
|
267
|
+
["service", "status", "--service", service, "--json"]
|
|
268
|
+
)
|
|
269
|
+
if code != 0:
|
|
270
|
+
return "UNKNOWN"
|
|
271
|
+
try:
|
|
272
|
+
data = json.loads(stdout)
|
|
273
|
+
status = data.get("status", "UNKNOWN")
|
|
274
|
+
if data.get("stopped"):
|
|
275
|
+
return f"{status} (stopped)"
|
|
276
|
+
return status
|
|
277
|
+
except json.JSONDecodeError:
|
|
278
|
+
return "UNKNOWN"
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def get_all_metrics_from_api(environment_id: str, service_id: str, hours: int = 24) -> Optional[Dict[str, Any]]:
|
|
282
|
+
"""Get disk, CPU, memory, and network usage from Railway metrics API.
|
|
283
|
+
|
|
284
|
+
Fetches time-series data and computes trend analysis including
|
|
285
|
+
min/max/avg, spike detection, and directional trends.
|
|
286
|
+
|
|
287
|
+
Args:
|
|
288
|
+
hours: Hours of history to fetch (default: 24, max: 168)
|
|
289
|
+
"""
|
|
290
|
+
from datetime import timedelta
|
|
291
|
+
|
|
292
|
+
start_date = (datetime.now(timezone.utc) - timedelta(hours=hours)).isoformat()
|
|
293
|
+
script_dir = os.path.dirname(os.path.abspath(__file__))
|
|
294
|
+
api_script = os.path.join(script_dir, "railway-api.sh")
|
|
295
|
+
|
|
296
|
+
if not os.path.exists(api_script):
|
|
297
|
+
return None
|
|
298
|
+
|
|
299
|
+
query = '''query metrics($environmentId: String!, $serviceId: String, $startDate: DateTime!, $measurements: [MetricMeasurement!]!) {
|
|
300
|
+
metrics(environmentId: $environmentId, serviceId: $serviceId, startDate: $startDate, measurements: $measurements) {
|
|
301
|
+
measurement values { ts value }
|
|
302
|
+
}
|
|
303
|
+
}'''
|
|
304
|
+
|
|
305
|
+
variables = json.dumps({
|
|
306
|
+
"environmentId": environment_id,
|
|
307
|
+
"serviceId": service_id,
|
|
308
|
+
"startDate": start_date,
|
|
309
|
+
"measurements": [
|
|
310
|
+
"DISK_USAGE_GB", "CPU_USAGE", "MEMORY_USAGE_GB",
|
|
311
|
+
"MEMORY_LIMIT_GB", "CPU_LIMIT",
|
|
312
|
+
"NETWORK_RX_GB", "NETWORK_TX_GB",
|
|
313
|
+
]
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
try:
|
|
317
|
+
result = subprocess.run(
|
|
318
|
+
[api_script, query, variables],
|
|
319
|
+
capture_output=True,
|
|
320
|
+
text=True,
|
|
321
|
+
timeout=30
|
|
322
|
+
)
|
|
323
|
+
if result.returncode != 0:
|
|
324
|
+
return None
|
|
325
|
+
|
|
326
|
+
data = json.loads(result.stdout)
|
|
327
|
+
metrics = data.get("data", {}).get("metrics", [])
|
|
328
|
+
|
|
329
|
+
combined = {"disk_usage": None, "cpu_memory": {}, "metrics_history": None}
|
|
330
|
+
|
|
331
|
+
# Raw time series keyed by measurement name
|
|
332
|
+
raw_series: Dict[str, List[Dict[str, Any]]] = {}
|
|
333
|
+
|
|
334
|
+
for metric in metrics:
|
|
335
|
+
measurement = metric.get("measurement")
|
|
336
|
+
values = metric.get("values", [])
|
|
337
|
+
if values:
|
|
338
|
+
raw_series[measurement] = values
|
|
339
|
+
latest = values[-1].get("value", 0)
|
|
340
|
+
if measurement == "DISK_USAGE_GB":
|
|
341
|
+
combined["disk_usage"] = {
|
|
342
|
+
"used_gb": round(latest, 2),
|
|
343
|
+
"used": f"{round(latest, 1)} GB"
|
|
344
|
+
}
|
|
345
|
+
elif measurement == "CPU_USAGE":
|
|
346
|
+
combined["cpu_memory"]["cpu_percent"] = round(latest, 1)
|
|
347
|
+
elif measurement == "MEMORY_USAGE_GB":
|
|
348
|
+
combined["cpu_memory"]["memory_gb"] = round(latest, 2)
|
|
349
|
+
elif measurement == "MEMORY_LIMIT_GB":
|
|
350
|
+
combined["cpu_memory"]["memory_limit_gb"] = round(latest, 2)
|
|
351
|
+
elif measurement == "CPU_LIMIT":
|
|
352
|
+
combined["cpu_memory"]["cpu_limit"] = round(latest, 1)
|
|
353
|
+
|
|
354
|
+
if not combined["cpu_memory"]:
|
|
355
|
+
combined["cpu_memory"] = None
|
|
356
|
+
|
|
357
|
+
# Build time-series history with trend analysis
|
|
358
|
+
if raw_series:
|
|
359
|
+
combined["metrics_history"] = _build_metrics_history(raw_series, hours=hours)
|
|
360
|
+
|
|
361
|
+
return combined
|
|
362
|
+
except (subprocess.TimeoutExpired, json.JSONDecodeError, FileNotFoundError):
|
|
363
|
+
pass
|
|
364
|
+
|
|
365
|
+
return None
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def _analyze_window(values: List[Dict[str, Any]], nums: List[float], d: int,
|
|
369
|
+
unit: str) -> Dict[str, Any]:
|
|
370
|
+
"""Analyze a single time window of metric data.
|
|
371
|
+
|
|
372
|
+
Returns summary stats, trend, spike detection, and downsampled series.
|
|
373
|
+
"""
|
|
374
|
+
if not nums:
|
|
375
|
+
return {}
|
|
376
|
+
|
|
377
|
+
avg_val = sum(nums) / len(nums)
|
|
378
|
+
min_val = min(nums)
|
|
379
|
+
max_val = max(nums)
|
|
380
|
+
|
|
381
|
+
entry: Dict[str, Any] = {
|
|
382
|
+
"unit": unit,
|
|
383
|
+
"current": round(nums[-1], d),
|
|
384
|
+
"min": round(min_val, d),
|
|
385
|
+
"max": round(max_val, d),
|
|
386
|
+
"avg": round(avg_val, d),
|
|
387
|
+
"samples": len(nums),
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
# Trend: compare first quarter avg to last quarter avg
|
|
391
|
+
q_size = max(len(nums) // 4, 1)
|
|
392
|
+
first_q = nums[:q_size]
|
|
393
|
+
last_q = nums[-q_size:]
|
|
394
|
+
first_avg = sum(first_q) / len(first_q)
|
|
395
|
+
last_avg = sum(last_q) / len(last_q)
|
|
396
|
+
|
|
397
|
+
if first_avg > 0:
|
|
398
|
+
change_pct = round(((last_avg - first_avg) / first_avg) * 100, 1)
|
|
399
|
+
elif last_avg > 0:
|
|
400
|
+
change_pct = 100.0
|
|
401
|
+
else:
|
|
402
|
+
change_pct = 0.0
|
|
403
|
+
|
|
404
|
+
if change_pct > 10:
|
|
405
|
+
trend_dir = "increasing"
|
|
406
|
+
elif change_pct < -10:
|
|
407
|
+
trend_dir = "decreasing"
|
|
408
|
+
else:
|
|
409
|
+
trend_dir = "stable"
|
|
410
|
+
|
|
411
|
+
entry["trend"] = {
|
|
412
|
+
"direction": trend_dir,
|
|
413
|
+
"change_pct": change_pct,
|
|
414
|
+
"first_quarter_avg": round(first_avg, d),
|
|
415
|
+
"last_quarter_avg": round(last_avg, d),
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
# Spike detection
|
|
419
|
+
if len(nums) >= 10:
|
|
420
|
+
variance = sum((x - avg_val) ** 2 for x in nums) / len(nums)
|
|
421
|
+
stddev = variance ** 0.5
|
|
422
|
+
threshold = avg_val + 2 * stddev
|
|
423
|
+
if stddev > 0 and threshold > 0:
|
|
424
|
+
spikes = []
|
|
425
|
+
for v in values:
|
|
426
|
+
val = v.get("value")
|
|
427
|
+
if val is not None and val > threshold:
|
|
428
|
+
spikes.append({"ts": v["ts"], "value": round(val, d)})
|
|
429
|
+
if spikes:
|
|
430
|
+
entry["spikes"] = {
|
|
431
|
+
"count": len(spikes),
|
|
432
|
+
"threshold": round(threshold, d),
|
|
433
|
+
"peaks": spikes[:10],
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
# Compact time series: downsample to ~48 points
|
|
437
|
+
series_points = []
|
|
438
|
+
for v in values:
|
|
439
|
+
ts = v.get("ts")
|
|
440
|
+
val = v.get("value")
|
|
441
|
+
if ts is not None and val is not None:
|
|
442
|
+
series_points.append({"ts": ts, "value": round(val, d)})
|
|
443
|
+
|
|
444
|
+
if len(series_points) > 48:
|
|
445
|
+
step = len(series_points) / 48
|
|
446
|
+
downsampled = []
|
|
447
|
+
for i in range(48):
|
|
448
|
+
idx = int(i * step)
|
|
449
|
+
downsampled.append(series_points[idx])
|
|
450
|
+
downsampled.append(series_points[-1])
|
|
451
|
+
entry["series"] = downsampled
|
|
452
|
+
else:
|
|
453
|
+
entry["series"] = series_points
|
|
454
|
+
|
|
455
|
+
return entry
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def _build_metrics_history(raw_series: Dict[str, List[Dict[str, Any]]], hours: int = 168) -> Dict[str, Any]:
|
|
459
|
+
"""Build multi-window time-series history with trend analysis.
|
|
460
|
+
|
|
461
|
+
Always produces a full-window analysis. If the window is > 24h, also
|
|
462
|
+
produces a 24h short-window analysis from the tail of the data so the
|
|
463
|
+
LLM can compare long-term vs short-term trends.
|
|
464
|
+
"""
|
|
465
|
+
from datetime import timedelta
|
|
466
|
+
|
|
467
|
+
metric_info = {
|
|
468
|
+
"CPU_USAGE": {"name": "cpu", "unit": "vCPU", "decimals": 2},
|
|
469
|
+
"MEMORY_USAGE_GB": {"name": "memory", "unit": "GB", "decimals": 2},
|
|
470
|
+
"MEMORY_LIMIT_GB": {"name": "memory_limit", "unit": "GB", "decimals": 2},
|
|
471
|
+
"CPU_LIMIT": {"name": "cpu_limit", "unit": "vCPU", "decimals": 2},
|
|
472
|
+
"DISK_USAGE_GB": {"name": "disk", "unit": "GB", "decimals": 2},
|
|
473
|
+
"NETWORK_RX_GB": {"name": "network_rx", "unit": "GB", "decimals": 3},
|
|
474
|
+
"NETWORK_TX_GB": {"name": "network_tx", "unit": "GB", "decimals": 3},
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
# Determine the 24h cutoff timestamp
|
|
478
|
+
now_ts = int(datetime.now(timezone.utc).timestamp())
|
|
479
|
+
cutoff_24h = now_ts - (24 * 3600)
|
|
480
|
+
|
|
481
|
+
produce_short_window = hours > 24
|
|
482
|
+
|
|
483
|
+
full_window: Dict[str, Any] = {}
|
|
484
|
+
short_window: Dict[str, Any] = {}
|
|
485
|
+
|
|
486
|
+
for measurement, values in raw_series.items():
|
|
487
|
+
info = metric_info.get(measurement)
|
|
488
|
+
if not info or len(values) < 2:
|
|
489
|
+
continue
|
|
490
|
+
|
|
491
|
+
nums = [v["value"] for v in values if v.get("value") is not None]
|
|
492
|
+
if not nums:
|
|
493
|
+
continue
|
|
494
|
+
|
|
495
|
+
d = info["decimals"]
|
|
496
|
+
name = info["name"]
|
|
497
|
+
|
|
498
|
+
# Full window analysis
|
|
499
|
+
full_window[name] = _analyze_window(values, nums, d, info["unit"])
|
|
500
|
+
|
|
501
|
+
# Short window (last 24h) analysis
|
|
502
|
+
if produce_short_window:
|
|
503
|
+
recent_values = [v for v in values if v.get("ts", 0) >= cutoff_24h]
|
|
504
|
+
recent_nums = [v["value"] for v in recent_values if v.get("value") is not None]
|
|
505
|
+
if len(recent_nums) >= 2:
|
|
506
|
+
short_window[name] = _analyze_window(recent_values, recent_nums, d, info["unit"])
|
|
507
|
+
|
|
508
|
+
# Build the result with named windows
|
|
509
|
+
windows: Dict[str, Any] = {}
|
|
510
|
+
|
|
511
|
+
# Label the full window
|
|
512
|
+
if hours >= 168:
|
|
513
|
+
full_label = "7d"
|
|
514
|
+
elif hours >= 72:
|
|
515
|
+
full_label = f"{hours // 24}d"
|
|
516
|
+
else:
|
|
517
|
+
full_label = f"{hours}h"
|
|
518
|
+
|
|
519
|
+
windows[full_label] = {
|
|
520
|
+
"window_hours": hours,
|
|
521
|
+
"metrics": full_window,
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
if produce_short_window and short_window:
|
|
525
|
+
windows["24h"] = {
|
|
526
|
+
"window_hours": 24,
|
|
527
|
+
"metrics": short_window,
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
return {"windows": windows}
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
def info(msg: str) -> None:
|
|
534
|
+
"""Print an [INFO] message to stdout."""
|
|
535
|
+
print(f"[INFO] {msg}")
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def error(msg: str) -> None:
|
|
539
|
+
"""Print an [ERROR] message to stderr and exit."""
|
|
540
|
+
print(f"[ERROR] {msg}", file=sys.stderr)
|
|
541
|
+
sys.exit(1)
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
def confirm_with_user(prompt: str) -> bool:
|
|
545
|
+
"""Get confirmation directly from the terminal.
|
|
546
|
+
|
|
547
|
+
Reads from /dev/tty to ensure it's an actual user at a terminal,
|
|
548
|
+
not piped input. This prevents automated scripts from bypassing confirmation.
|
|
549
|
+
"""
|
|
550
|
+
try:
|
|
551
|
+
with open('/dev/tty', 'r') as tty:
|
|
552
|
+
print(prompt, end=' ', flush=True)
|
|
553
|
+
response = tty.readline().strip().lower()
|
|
554
|
+
return response in ('y', 'yes')
|
|
555
|
+
except (OSError, IOError):
|
|
556
|
+
print("\n[ERROR] This command requires interactive terminal confirmation.")
|
|
557
|
+
print("It cannot be run with piped input or in non-interactive mode.")
|
|
558
|
+
print("Please run this command directly in a terminal.")
|
|
559
|
+
return False
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
def _safe_int(val: Any, default: int = 0) -> int:
|
|
563
|
+
"""Safely convert a value to int, returning default on failure."""
|
|
564
|
+
try:
|
|
565
|
+
return int(val)
|
|
566
|
+
except (ValueError, TypeError):
|
|
567
|
+
return default
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
def _safe_float(val: Any, default: float = 0.0) -> float:
|
|
571
|
+
"""Safely convert a value to float, returning default on failure."""
|
|
572
|
+
try:
|
|
573
|
+
return float(val)
|
|
574
|
+
except (ValueError, TypeError):
|
|
575
|
+
return default
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
def _format_uptime(seconds: int) -> str:
|
|
579
|
+
"""Format uptime seconds into a human-readable string."""
|
|
580
|
+
if seconds <= 0:
|
|
581
|
+
return "N/A"
|
|
582
|
+
days = seconds // 86400
|
|
583
|
+
hours = (seconds % 86400) // 3600
|
|
584
|
+
minutes = (seconds % 3600) // 60
|
|
585
|
+
parts = []
|
|
586
|
+
if days > 0:
|
|
587
|
+
parts.append(f"{days}d")
|
|
588
|
+
if hours > 0:
|
|
589
|
+
parts.append(f"{hours}h")
|
|
590
|
+
if minutes > 0 and days == 0:
|
|
591
|
+
parts.append(f"{minutes}m")
|
|
592
|
+
return " ".join(parts) if parts else "< 1m"
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
def _trend_indicator(metrics_history: Optional[Dict[str, Any]], metric_name: str) -> str:
|
|
596
|
+
"""Return a compact trend string like ' (^ +15.2% 24h)' for use in summary rows."""
|
|
597
|
+
if not metrics_history:
|
|
598
|
+
return ""
|
|
599
|
+
windows = metrics_history.get("windows", {})
|
|
600
|
+
window_data = windows.get("24h") or next(iter(windows.values()), None) if windows else None
|
|
601
|
+
if not window_data or not window_data.get("metrics"):
|
|
602
|
+
return ""
|
|
603
|
+
m = window_data["metrics"].get(metric_name)
|
|
604
|
+
if not m or "trend" not in m:
|
|
605
|
+
return ""
|
|
606
|
+
t = m["trend"]
|
|
607
|
+
direction = t.get("direction", "stable")
|
|
608
|
+
change = t.get("change_pct", 0)
|
|
609
|
+
window_label = "24h" if "24h" in windows else next(iter(windows), "")
|
|
610
|
+
arrow = {"increasing": "^", "decreasing": "v", "stable": "~"}.get(direction, "")
|
|
611
|
+
if direction == "stable":
|
|
612
|
+
return f" ({arrow} stable {window_label})"
|
|
613
|
+
return f" ({arrow} {change:+.1f}% {window_label})"
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
def get_recent_logs(service: str, lines: int = LOG_LINES_DEFAULT,
|
|
617
|
+
environment_id: Optional[str] = None,
|
|
618
|
+
service_id: Optional[str] = None) -> List[str]:
|
|
619
|
+
"""Get recent logs for LLM analysis.
|
|
620
|
+
|
|
621
|
+
Uses API if environment_id and service_id provided (~3s),
|
|
622
|
+
retries once with longer timeout on failure,
|
|
623
|
+
falls back to CLI (~27s for 100 lines).
|
|
624
|
+
"""
|
|
625
|
+
# Fast path: use API directly
|
|
626
|
+
if environment_id and service_id:
|
|
627
|
+
script_dir = os.path.dirname(os.path.abspath(__file__))
|
|
628
|
+
api_script = os.path.join(script_dir, "railway-api.sh")
|
|
629
|
+
|
|
630
|
+
if os.path.exists(api_script):
|
|
631
|
+
# Use environmentLogs API with service filter
|
|
632
|
+
query = f'''query {{
|
|
633
|
+
environmentLogs(
|
|
634
|
+
environmentId: "{environment_id}",
|
|
635
|
+
beforeLimit: {lines},
|
|
636
|
+
filter: "@service:{service_id}"
|
|
637
|
+
) {{
|
|
638
|
+
timestamp
|
|
639
|
+
message
|
|
640
|
+
}}
|
|
641
|
+
}}'''
|
|
642
|
+
# Try API twice: first with 15s timeout, retry with 30s
|
|
643
|
+
for attempt_timeout in [15, 30]:
|
|
644
|
+
try:
|
|
645
|
+
result = subprocess.run(
|
|
646
|
+
[api_script, query],
|
|
647
|
+
capture_output=True,
|
|
648
|
+
text=True,
|
|
649
|
+
timeout=attempt_timeout
|
|
650
|
+
)
|
|
651
|
+
if result.returncode == 0:
|
|
652
|
+
data = json.loads(result.stdout)
|
|
653
|
+
logs_data = data.get("data", {}).get("environmentLogs", [])
|
|
654
|
+
if logs_data:
|
|
655
|
+
return [f"{log['timestamp']} {log['message']}" for log in logs_data]
|
|
656
|
+
except (subprocess.TimeoutExpired, json.JSONDecodeError):
|
|
657
|
+
pass
|
|
658
|
+
|
|
659
|
+
# Fallback: use CLI (slow, ~27s)
|
|
660
|
+
code, stdout, stderr = run_railway_command(
|
|
661
|
+
["logs"] + _ctx.logs_flags() + ["--service", service, "--lines", str(lines)],
|
|
662
|
+
timeout=30
|
|
663
|
+
)
|
|
664
|
+
if code != 0:
|
|
665
|
+
return []
|
|
666
|
+
|
|
667
|
+
logs = []
|
|
668
|
+
for line in stdout.strip().split("\n"):
|
|
669
|
+
if line.strip():
|
|
670
|
+
logs.append(line.strip())
|
|
671
|
+
return logs
|