@cyanautomation/kaseki-agent 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dockerignore +54 -0
- package/.eslintignore +11 -0
- package/.eslintrc.json +95 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +53 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +53 -0
- package/.github/ISSUE_TEMPLATE/security.md +51 -0
- package/.github/PULL_REQUEST_TEMPLATE/default.md +71 -0
- package/.github/dependabot.yml +38 -0
- package/.github/skills/dependency-cache-optimization/SKILL.md +526 -0
- package/.github/skills/docker-image-management/SKILL.md +532 -0
- package/.github/skills/frontend-design/SKILL.md +782 -0
- package/.github/skills/prompt-engineering/SKILL.md +360 -0
- package/.github/skills/quality-gate-config/SKILL.md +591 -0
- package/.github/skills/result-report-analysis/SKILL.md +576 -0
- package/.github/skills/test-automation/SKILL.md +593 -0
- package/.github/skills/workflow-diagnosis/SKILL.md +468 -0
- package/.github/workflows/build-docker-image.yml +453 -0
- package/.github/workflows/release.yml +68 -0
- package/.releaserc.json +135 -0
- package/CHANGELOG.md +117 -0
- package/CLAUDE.md +336 -0
- package/CONTRIBUTING.md +339 -0
- package/Dockerfile +217 -0
- package/README.md +1527 -0
- package/STYLE.md +521 -0
- package/add-js-extensions.d.ts +9 -0
- package/add-js-extensions.d.ts.map +1 -0
- package/add-js-extensions.js.map +1 -0
- package/dist/add-js-extensions.d.ts +9 -0
- package/dist/add-js-extensions.d.ts.map +1 -0
- package/dist/add-js-extensions.js +52 -0
- package/dist/add-js-extensions.js.map +1 -0
- package/dist/ansi-colors.d.ts +26 -0
- package/dist/ansi-colors.d.ts.map +1 -0
- package/dist/ansi-colors.js +51 -0
- package/dist/ansi-colors.js.map +1 -0
- package/dist/cli/BaseCommand.d.ts +18 -0
- package/dist/cli/BaseCommand.d.ts.map +1 -0
- package/dist/cli/BaseCommand.js +31 -0
- package/dist/cli/BaseCommand.js.map +1 -0
- package/dist/cli/KasekiCLI.d.ts +30 -0
- package/dist/cli/KasekiCLI.d.ts.map +1 -0
- package/dist/cli/KasekiCLI.js +134 -0
- package/dist/cli/KasekiCLI.js.map +1 -0
- package/dist/cli/commands/ConfigCommand.d.ts +13 -0
- package/dist/cli/commands/ConfigCommand.d.ts.map +1 -0
- package/dist/cli/commands/ConfigCommand.js +131 -0
- package/dist/cli/commands/ConfigCommand.js.map +1 -0
- package/dist/cli/commands/DoctorCommand.d.ts +45 -0
- package/dist/cli/commands/DoctorCommand.d.ts.map +1 -0
- package/dist/cli/commands/DoctorCommand.js +309 -0
- package/dist/cli/commands/DoctorCommand.js.map +1 -0
- package/dist/cli/commands/ListCommand.d.ts +9 -0
- package/dist/cli/commands/ListCommand.d.ts.map +1 -0
- package/dist/cli/commands/ListCommand.js +81 -0
- package/dist/cli/commands/ListCommand.js.map +1 -0
- package/dist/cli/commands/ReportCommand.d.ts +9 -0
- package/dist/cli/commands/ReportCommand.d.ts.map +1 -0
- package/dist/cli/commands/ReportCommand.js +98 -0
- package/dist/cli/commands/ReportCommand.js.map +1 -0
- package/dist/cli/commands/RunCommand.d.ts +13 -0
- package/dist/cli/commands/RunCommand.d.ts.map +1 -0
- package/dist/cli/commands/RunCommand.js +191 -0
- package/dist/cli/commands/RunCommand.js.map +1 -0
- package/dist/cli/commands/SecretsCommand.d.ts +9 -0
- package/dist/cli/commands/SecretsCommand.d.ts.map +1 -0
- package/dist/cli/commands/SecretsCommand.js +109 -0
- package/dist/cli/commands/SecretsCommand.js.map +1 -0
- package/dist/cli/commands/ServeCommand.d.ts +9 -0
- package/dist/cli/commands/ServeCommand.d.ts.map +1 -0
- package/dist/cli/commands/ServeCommand.js +50 -0
- package/dist/cli/commands/ServeCommand.js.map +1 -0
- package/dist/cli/commands/SetupCommand.d.ts +42 -0
- package/dist/cli/commands/SetupCommand.d.ts.map +1 -0
- package/dist/cli/commands/SetupCommand.js +249 -0
- package/dist/cli/commands/SetupCommand.js.map +1 -0
- package/dist/cli.d.ts +9 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +130 -0
- package/dist/cli.js.map +1 -0
- package/dist/config/ConfigManager.d.ts +395 -0
- package/dist/config/ConfigManager.d.ts.map +1 -0
- package/dist/config/ConfigManager.js +446 -0
- package/dist/config/ConfigManager.js.map +1 -0
- package/dist/docker/DockerManager.d.ts +69 -0
- package/dist/docker/DockerManager.d.ts.map +1 -0
- package/dist/docker/DockerManager.js +266 -0
- package/dist/docker/DockerManager.js.map +1 -0
- package/dist/event-aggregator.d.ts +71 -0
- package/dist/event-aggregator.d.ts.map +1 -0
- package/dist/event-aggregator.js +95 -0
- package/dist/event-aggregator.js.map +1 -0
- package/dist/github-app-token.d.ts +16 -0
- package/dist/github-app-token.d.ts.map +1 -0
- package/dist/github-app-token.js +148 -0
- package/dist/github-app-token.js.map +1 -0
- package/dist/idempotency-store.d.ts +61 -0
- package/dist/idempotency-store.d.ts.map +1 -0
- package/dist/idempotency-store.js +321 -0
- package/dist/idempotency-store.js.map +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +31 -0
- package/dist/index.js.map +1 -0
- package/dist/instance/InstanceManager.d.ts +81 -0
- package/dist/instance/InstanceManager.d.ts.map +1 -0
- package/dist/instance/InstanceManager.js +220 -0
- package/dist/instance/InstanceManager.js.map +1 -0
- package/dist/instance-metadata-reader.d.ts +48 -0
- package/dist/instance-metadata-reader.d.ts.map +1 -0
- package/dist/instance-metadata-reader.js +94 -0
- package/dist/instance-metadata-reader.js.map +1 -0
- package/dist/instance-state-derivation.d.ts +42 -0
- package/dist/instance-state-derivation.d.ts.map +1 -0
- package/dist/instance-state-derivation.js +133 -0
- package/dist/instance-state-derivation.js.map +1 -0
- package/dist/job-scheduler.d.ts +124 -0
- package/dist/job-scheduler.d.ts.map +1 -0
- package/dist/job-scheduler.js +992 -0
- package/dist/job-scheduler.js.map +1 -0
- package/dist/kaseki-api-client.d.ts +89 -0
- package/dist/kaseki-api-client.d.ts.map +1 -0
- package/dist/kaseki-api-client.js +405 -0
- package/dist/kaseki-api-client.js.map +1 -0
- package/dist/kaseki-api-config.d.ts +34 -0
- package/dist/kaseki-api-config.d.ts.map +1 -0
- package/dist/kaseki-api-config.js +113 -0
- package/dist/kaseki-api-config.js.map +1 -0
- package/dist/kaseki-api-routes.d.ts +13 -0
- package/dist/kaseki-api-routes.d.ts.map +1 -0
- package/dist/kaseki-api-routes.js +559 -0
- package/dist/kaseki-api-routes.js.map +1 -0
- package/dist/kaseki-api-service-wrapper.d.ts +43 -0
- package/dist/kaseki-api-service-wrapper.d.ts.map +1 -0
- package/dist/kaseki-api-service-wrapper.js +150 -0
- package/dist/kaseki-api-service-wrapper.js.map +1 -0
- package/dist/kaseki-api-service.d.ts +16 -0
- package/dist/kaseki-api-service.d.ts.map +1 -0
- package/dist/kaseki-api-service.js +143 -0
- package/dist/kaseki-api-service.js.map +1 -0
- package/dist/kaseki-api-types.d.ts +440 -0
- package/dist/kaseki-api-types.d.ts.map +1 -0
- package/dist/kaseki-api-types.js +64 -0
- package/dist/kaseki-api-types.js.map +1 -0
- package/dist/kaseki-cli-lib.d.ts +219 -0
- package/dist/kaseki-cli-lib.d.ts.map +1 -0
- package/dist/kaseki-cli-lib.js +523 -0
- package/dist/kaseki-cli-lib.js.map +1 -0
- package/dist/kaseki-cli.d.ts +38 -0
- package/dist/kaseki-cli.d.ts.map +1 -0
- package/dist/kaseki-cli.js +559 -0
- package/dist/kaseki-cli.js.map +1 -0
- package/dist/kaseki-report.d.ts +3 -0
- package/dist/kaseki-report.d.ts.map +1 -0
- package/dist/kaseki-report.js +140 -0
- package/dist/kaseki-report.js.map +1 -0
- package/dist/lib/subprocess-helpers.d.ts +98 -0
- package/dist/lib/subprocess-helpers.d.ts.map +1 -0
- package/dist/lib/subprocess-helpers.js +136 -0
- package/dist/lib/subprocess-helpers.js.map +1 -0
- package/dist/logger.d.ts +39 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +79 -0
- package/dist/logger.js.map +1 -0
- package/dist/metrics.d.ts +19 -0
- package/dist/metrics.d.ts.map +1 -0
- package/dist/metrics.js +59 -0
- package/dist/metrics.js.map +1 -0
- package/dist/middleware/job-lookup.d.ts +27 -0
- package/dist/middleware/job-lookup.d.ts.map +1 -0
- package/dist/middleware/job-lookup.js +28 -0
- package/dist/middleware/job-lookup.js.map +1 -0
- package/dist/pi-event-filter.d.ts +3 -0
- package/dist/pi-event-filter.d.ts.map +1 -0
- package/dist/pi-event-filter.js +126 -0
- package/dist/pi-event-filter.js.map +1 -0
- package/dist/pi-progress-stream.d.ts +3 -0
- package/dist/pi-progress-stream.d.ts.map +1 -0
- package/dist/pi-progress-stream.js +205 -0
- package/dist/pi-progress-stream.js.map +1 -0
- package/dist/pi-progress-summarizer.d.ts +61 -0
- package/dist/pi-progress-summarizer.d.ts.map +1 -0
- package/dist/pi-progress-summarizer.js +246 -0
- package/dist/pi-progress-summarizer.js.map +1 -0
- package/dist/pre-flight-validator.d.ts +72 -0
- package/dist/pre-flight-validator.d.ts.map +1 -0
- package/dist/pre-flight-validator.js +513 -0
- package/dist/pre-flight-validator.js.map +1 -0
- package/dist/progress-stream-utils.d.ts +3 -0
- package/dist/progress-stream-utils.d.ts.map +1 -0
- package/dist/progress-stream-utils.js +15 -0
- package/dist/progress-stream-utils.js.map +1 -0
- package/dist/result-cache.d.ts +52 -0
- package/dist/result-cache.d.ts.map +1 -0
- package/dist/result-cache.js +134 -0
- package/dist/result-cache.js.map +1 -0
- package/dist/routes/artifact-routes.d.ts +10 -0
- package/dist/routes/artifact-routes.d.ts.map +1 -0
- package/dist/routes/artifact-routes.js +126 -0
- package/dist/routes/artifact-routes.js.map +1 -0
- package/dist/routes/log-routes.d.ts +8 -0
- package/dist/routes/log-routes.d.ts.map +1 -0
- package/dist/routes/log-routes.js +345 -0
- package/dist/routes/log-routes.js.map +1 -0
- package/dist/routes/status-routes.d.ts +8 -0
- package/dist/routes/status-routes.d.ts.map +1 -0
- package/dist/routes/status-routes.js +82 -0
- package/dist/routes/status-routes.js.map +1 -0
- package/dist/routes/webhook-routes.d.ts +6 -0
- package/dist/routes/webhook-routes.d.ts.map +1 -0
- package/dist/routes/webhook-routes.js +86 -0
- package/dist/routes/webhook-routes.js.map +1 -0
- package/dist/run-artifact-metadata-cache.d.ts +42 -0
- package/dist/run-artifact-metadata-cache.d.ts.map +1 -0
- package/dist/run-artifact-metadata-cache.js +139 -0
- package/dist/run-artifact-metadata-cache.js.map +1 -0
- package/dist/secret-value-cache.d.ts +13 -0
- package/dist/secret-value-cache.d.ts.map +1 -0
- package/dist/secret-value-cache.js +44 -0
- package/dist/secret-value-cache.js.map +1 -0
- package/dist/secrets/SecretsManager.d.ts +80 -0
- package/dist/secrets/SecretsManager.d.ts.map +1 -0
- package/dist/secrets/SecretsManager.js +306 -0
- package/dist/secrets/SecretsManager.js.map +1 -0
- package/dist/test-utils.d.ts +55 -0
- package/dist/test-utils.d.ts.map +1 -0
- package/dist/test-utils.js +48 -0
- package/dist/test-utils.js.map +1 -0
- package/dist/timestamp-tracker.d.ts +75 -0
- package/dist/timestamp-tracker.d.ts.map +1 -0
- package/dist/timestamp-tracker.js +121 -0
- package/dist/timestamp-tracker.js.map +1 -0
- package/dist/utils/failure-artifact-writer.d.ts +29 -0
- package/dist/utils/failure-artifact-writer.d.ts.map +1 -0
- package/dist/utils/failure-artifact-writer.js +157 -0
- package/dist/utils/failure-artifact-writer.js.map +1 -0
- package/dist/utils/file-helpers.d.ts +41 -0
- package/dist/utils/file-helpers.d.ts.map +1 -0
- package/dist/utils/file-helpers.js +143 -0
- package/dist/utils/file-helpers.js.map +1 -0
- package/dist/utils/http-client-factory.d.ts +46 -0
- package/dist/utils/http-client-factory.d.ts.map +1 -0
- package/dist/utils/http-client-factory.js +114 -0
- package/dist/utils/http-client-factory.js.map +1 -0
- package/dist/utils/progress-normalizer.d.ts +13 -0
- package/dist/utils/progress-normalizer.d.ts.map +1 -0
- package/dist/utils/progress-normalizer.js +57 -0
- package/dist/utils/progress-normalizer.js.map +1 -0
- package/dist/utils/response-helpers.d.ts +34 -0
- package/dist/utils/response-helpers.d.ts.map +1 -0
- package/dist/utils/response-helpers.js +78 -0
- package/dist/utils/response-helpers.js.map +1 -0
- package/dist/utils/route-helpers.d.ts +17 -0
- package/dist/utils/route-helpers.d.ts.map +1 -0
- package/dist/utils/route-helpers.js +22 -0
- package/dist/utils/route-helpers.js.map +1 -0
- package/dist/utils/status-response-builder.d.ts +23 -0
- package/dist/utils/status-response-builder.d.ts.map +1 -0
- package/dist/utils/status-response-builder.js +144 -0
- package/dist/utils/status-response-builder.js.map +1 -0
- package/dist/utils/type-guards.d.ts +37 -0
- package/dist/utils/type-guards.d.ts.map +1 -0
- package/dist/utils/type-guards.js +45 -0
- package/dist/utils/type-guards.js.map +1 -0
- package/dist/utils/utf8-helpers.d.ts +32 -0
- package/dist/utils/utf8-helpers.d.ts.map +1 -0
- package/dist/utils/utf8-helpers.js +97 -0
- package/dist/utils/utf8-helpers.js.map +1 -0
- package/dist/utils/webhook-event-builder.d.ts +26 -0
- package/dist/utils/webhook-event-builder.d.ts.map +1 -0
- package/dist/utils/webhook-event-builder.js +77 -0
- package/dist/utils/webhook-event-builder.js.map +1 -0
- package/dist/webhook-manager.d.ts +56 -0
- package/dist/webhook-manager.d.ts.map +1 -0
- package/dist/webhook-manager.js +359 -0
- package/dist/webhook-manager.js.map +1 -0
- package/docker/workspace-cache/package-lock.json +13 -0
- package/docker/workspace-cache/package.json +7 -0
- package/docker-compose.yml +53 -0
- package/docs/API.md +708 -0
- package/docs/BACKLOG.md +19 -0
- package/docs/BUILD_STRATEGY.md +404 -0
- package/docs/CLI.md +569 -0
- package/docs/DEPLOYMENT.md +521 -0
- package/docs/DEVELOPMENT.md +459 -0
- package/docs/DOCKER_SETUP.md +522 -0
- package/docs/ENHANCED_PROGRESS_LOGS.md +264 -0
- package/docs/IMPLEMENTATION_SUMMARY.md +549 -0
- package/docs/INTEGRATION_EXAMPLE.md +217 -0
- package/docs/NPM_SETUP.md +468 -0
- package/docs/PHASE1-4_IMPLEMENTATION.md +302 -0
- package/docs/PHASE1_COMPLETION.md +192 -0
- package/docs/PHASE2_COMPLETION.md +134 -0
- package/docs/PHASE6_MIGRATION.md +392 -0
- package/docs/PRINTF_SAFETY_FIX.md +282 -0
- package/docs/QUALITY_GATES.md +369 -0
- package/docs/SETUP_GUIDE.md +482 -0
- package/docs/TASK_PROMPT_TEMPLATES.md +533 -0
- package/docs/VALIDATION_FIX.md +139 -0
- package/docs/VERIFICATION_CHECKLIST.md +335 -0
- package/docs/repo-maturity.md +760 -0
- package/fix-tests.d.ts +9 -0
- package/fix-tests.d.ts.map +1 -0
- package/fix-tests.js.map +1 -0
- package/fix-tests.ts +53 -0
- package/jest.config.ts +31 -0
- package/kaseki +183 -0
- package/kaseki-agent.sh +1961 -0
- package/ops/logrotate/kaseki +10 -0
- package/package.json +83 -0
- package/perf/README.md +54 -0
- package/perf/pi-event-filter.benchmark.test.ts +98 -0
- package/run-kaseki-json.test.sh +106 -0
- package/run-kaseki.sh +990 -0
- package/scripts/allowlist-helper.sh +56 -0
- package/scripts/cleanup-kaseki.sh +168 -0
- package/scripts/deploy-pi-template.sh +293 -0
- package/scripts/docker-entrypoint.sh +71 -0
- package/scripts/dry-run-allowlist.sh +161 -0
- package/scripts/kaseki-activate.sh +396 -0
- package/scripts/kaseki-api.service +62 -0
- package/scripts/kaseki-container-entrypoint-wrapper.sh +119 -0
- package/scripts/kaseki-container-setup-remote.sh +172 -0
- package/scripts/kaseki-container-setup.sh +193 -0
- package/scripts/kaseki-healthcheck.sh +95 -0
- package/scripts/kaseki-install.sh +50 -0
- package/scripts/kaseki-maturity-score.sh +291 -0
- package/scripts/kaseki-performance-metrics.sh +122 -0
- package/scripts/kaseki-preflight.sh +270 -0
- package/scripts/kaseki-setup.sh +265 -0
- package/scripts/pi-setup-remote.sh +213 -0
- package/scripts/setup-github-labels.sh +42 -0
- package/scripts/suggest-allowlist.sh +68 -0
- package/scripts/templates/MULTI_HOST_DISTRIBUTED.md +337 -0
- package/scripts/templates/REST_API_SERVICE.md +490 -0
- package/scripts/templates/SINGLE_HOST_CLI.md +194 -0
- package/scripts/test-github-app.sh +248 -0
- package/src/add-js-extensions.ts +61 -0
- package/src/ansi-colors.test.ts +62 -0
- package/src/ansi-colors.ts +67 -0
- package/src/cli/BaseCommand.ts +40 -0
- package/src/cli/KasekiCLI.ts +154 -0
- package/src/cli/commands/ConfigCommand.ts +145 -0
- package/src/cli/commands/DoctorCommand.ts +329 -0
- package/src/cli/commands/ListCommand.ts +105 -0
- package/src/cli/commands/ReportCommand.ts +110 -0
- package/src/cli/commands/RunCommand.ts +218 -0
- package/src/cli/commands/SecretsCommand.ts +120 -0
- package/src/cli/commands/ServeCommand.ts +62 -0
- package/src/cli/commands/SetupCommand.ts +301 -0
- package/src/cli.ts +138 -0
- package/src/config/ConfigManager.ts +476 -0
- package/src/docker/DockerManager.ts +319 -0
- package/src/docker-entrypoint-packaging.test.ts +33 -0
- package/src/event-aggregator.test.ts +117 -0
- package/src/event-aggregator.ts +126 -0
- package/src/github-app-token.ts +215 -0
- package/src/idempotency-store.test.ts +117 -0
- package/src/idempotency-store.ts +385 -0
- package/src/index.ts +89 -0
- package/src/instance/InstanceManager.ts +285 -0
- package/src/instance-metadata-reader.test.ts +190 -0
- package/src/instance-metadata-reader.ts +129 -0
- package/src/instance-state-derivation.test.ts +263 -0
- package/src/instance-state-derivation.ts +148 -0
- package/src/job-scheduler.test.ts +1236 -0
- package/src/job-scheduler.ts +1117 -0
- package/src/kaseki-api-client.ts +488 -0
- package/src/kaseki-api-config.test.ts +315 -0
- package/src/kaseki-api-config.ts +175 -0
- package/src/kaseki-api-routes.test.ts +1615 -0
- package/src/kaseki-api-routes.ts +643 -0
- package/src/kaseki-api-service-wrapper.ts +188 -0
- package/src/kaseki-api-service.test.ts +418 -0
- package/src/kaseki-api-service.ts +192 -0
- package/src/kaseki-api-types.ts +320 -0
- package/src/kaseki-cli-lib.test.ts +552 -0
- package/src/kaseki-cli-lib.ts +760 -0
- package/src/kaseki-cli.ts +682 -0
- package/src/kaseki-report.test.ts +118 -0
- package/src/kaseki-report.ts +192 -0
- package/src/lib/subprocess-helpers.ts +177 -0
- package/src/logger.ts +114 -0
- package/src/metrics.ts +66 -0
- package/src/middleware/job-lookup.test.ts +113 -0
- package/src/middleware/job-lookup.ts +45 -0
- package/src/pi-event-filter.test.ts +183 -0
- package/src/pi-event-filter.ts +183 -0
- package/src/pi-progress-stream.ts +287 -0
- package/src/pi-progress-summarizer.test.ts +302 -0
- package/src/pi-progress-summarizer.ts +287 -0
- package/src/pre-flight-validator.test.ts +512 -0
- package/src/pre-flight-validator.ts +618 -0
- package/src/progress-stream-utils.test.ts +35 -0
- package/src/progress-stream-utils.ts +14 -0
- package/src/result-cache.test.ts +195 -0
- package/src/result-cache.ts +181 -0
- package/src/routes/artifact-routes.ts +169 -0
- package/src/routes/log-routes.ts +391 -0
- package/src/routes/status-routes.ts +92 -0
- package/src/routes/webhook-routes.ts +97 -0
- package/src/run-artifact-metadata-cache.test.ts +80 -0
- package/src/run-artifact-metadata-cache.ts +184 -0
- package/src/secret-value-cache.test.ts +66 -0
- package/src/secret-value-cache.ts +55 -0
- package/src/secrets/SecretsManager.ts +343 -0
- package/src/test-utils.ts +81 -0
- package/src/timestamp-tracker.test.ts +134 -0
- package/src/timestamp-tracker.ts +132 -0
- package/src/utils/failure-artifact-writer.ts +187 -0
- package/src/utils/file-helpers.test.ts +235 -0
- package/src/utils/file-helpers.ts +150 -0
- package/src/utils/http-client-factory.test.ts +245 -0
- package/src/utils/http-client-factory.ts +157 -0
- package/src/utils/progress-normalizer.test.ts +442 -0
- package/src/utils/progress-normalizer.ts +68 -0
- package/src/utils/response-helpers.test.ts +122 -0
- package/src/utils/response-helpers.ts +101 -0
- package/src/utils/route-helpers.ts +30 -0
- package/src/utils/status-response-builder.ts +159 -0
- package/src/utils/type-guards.ts +52 -0
- package/src/utils/utf8-helpers.ts +102 -0
- package/src/utils/webhook-event-builder.test.ts +143 -0
- package/src/utils/webhook-event-builder.ts +87 -0
- package/src/webhook-manager.test.ts +152 -0
- package/src/webhook-manager.ts +445 -0
- package/templates/allowlist-api-route.txt +7 -0
- package/templates/allowlist-comprehensive.txt +8 -0
- package/templates/allowlist-parser-fix.txt +6 -0
- package/templates/allowlist-ui-component.txt +9 -0
- package/templates/allowlist-utility.txt +9 -0
- package/test/actual-model-metadata.test.sh +102 -0
- package/test/dry-run.test.sh +131 -0
- package/test/fixtures/kaseki-report-exit-codes/metadata-exit-0.json +1 -0
- package/test/fixtures/kaseki-report-exit-codes/metadata-exit-1.json +1 -0
- package/test/fixtures/kaseki-report-exit-codes/metadata-exit-invalid.json +1 -0
- package/test/fixtures/kaseki-report-exit-codes/metadata-exit-str-0.json +1 -0
- package/test/fixtures/kaseki-report-exit-codes/metadata-exit-str-1.json +1 -0
- package/test/kaseki-api.integration.test.sh +165 -0
- package/test/pi-event-filter-failure.test.sh +83 -0
- package/test/printf-safety-focused.test.sh +99 -0
- package/test/printf-safety-results/results/restoration.jsonl +10 -0
- package/test/printf-safety-results/results/test.jsonl +0 -0
- package/test/printf-safety.test.sh +297 -0
- package/test/validation-fix.test.sh +79 -0
- package/test/validation-integration.test.sh +109 -0
- package/tests/allowlist-glob.test.sh +61 -0
- package/tests/dependency-cache-key.test.sh +48 -0
- package/tests/dependency-restore-mode.test.sh +48 -0
- package/tests/doctor-template-parity.test.sh +95 -0
- package/tests/github-operations.test.sh +142 -0
- package/tests/npm-install-flags.test.sh +58 -0
- package/tests/quality-gates.test.sh +178 -0
- package/tests/repo-memory.test.sh +103 -0
- package/tests/restore-disallowed-changes.test.sh +80 -0
- package/tests/validation-missing-npm-scripts.test.sh +93 -0
- package/tests/validation-strict-mode.test.sh +118 -0
- package/tsconfig.changed.json +7 -0
- package/tsconfig.json +39 -0
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
import { Router, Request, Response } from 'express';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
import { JobScheduler } from '../job-scheduler';
|
|
5
|
+
import { KasekiApiConfig } from '../kaseki-api-config';
|
|
6
|
+
import { LogResponse, AnalysisResponse } from '../kaseki-api-types';
|
|
7
|
+
import { sendErrorResponse } from '../utils/response-helpers';
|
|
8
|
+
import { isNonEmptyFile } from '../utils/file-helpers';
|
|
9
|
+
import { decodeUtf8TailSafely, tailLogByLines, readTailBytes } from '../utils/utf8-helpers';
|
|
10
|
+
import { getJobOrRespond } from '../utils/route-helpers';
|
|
11
|
+
import { normalizeProgressEvent } from '../utils/progress-normalizer';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Create log-related routes (progress, events, logs, analysis).
|
|
15
|
+
*/
|
|
16
|
+
export function createLogRoutes(scheduler: JobScheduler, config: KasekiApiConfig): Router {
|
|
17
|
+
const router = Router();
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* GET /api/runs/:id/progress - Retrieve progress events (supports Server-Sent Events streaming).
|
|
21
|
+
*/
|
|
22
|
+
router.get('/runs/:id/progress', (req: Request, res: Response) => {
|
|
23
|
+
const job = getJobOrRespond(scheduler, req.params.id, res);
|
|
24
|
+
if (!job) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Check if client wants SSE streaming
|
|
29
|
+
const wantsSSE = req.query.stream === 'sse' || req.get('Accept')?.includes('text/event-stream');
|
|
30
|
+
|
|
31
|
+
if (wantsSSE) {
|
|
32
|
+
// Server-Sent Events streaming
|
|
33
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
34
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
35
|
+
res.setHeader('Connection', 'keep-alive');
|
|
36
|
+
|
|
37
|
+
let lastEventCount = 0;
|
|
38
|
+
let noChangeCount = 0;
|
|
39
|
+
const maxNoChangeAttempts = 10; // Stop after 10 checks with no change
|
|
40
|
+
|
|
41
|
+
const sendProgressUpdate = () => {
|
|
42
|
+
const progressFile = path.join(config.resultsDir, job.id, 'progress.jsonl');
|
|
43
|
+
if (!fs.existsSync(progressFile)) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const content = fs.readFileSync(progressFile, 'utf-8');
|
|
49
|
+
const lines = content.trim().length > 0 ? content.trim().split('\n') : [];
|
|
50
|
+
|
|
51
|
+
if (lines.length > lastEventCount) {
|
|
52
|
+
// Send new events
|
|
53
|
+
const newLines = lines.slice(lastEventCount);
|
|
54
|
+
for (const line of newLines) {
|
|
55
|
+
try {
|
|
56
|
+
const event = normalizeProgressEvent(JSON.parse(line));
|
|
57
|
+
res.write(`data: ${JSON.stringify(event)}\n\n`);
|
|
58
|
+
} catch {
|
|
59
|
+
// Skip invalid JSON lines
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
lastEventCount = lines.length;
|
|
63
|
+
noChangeCount = 0;
|
|
64
|
+
} else if (job.status !== 'running') {
|
|
65
|
+
// Job is not running anymore, send final status
|
|
66
|
+
const currentJob = scheduler.getJob(job.id);
|
|
67
|
+
if (currentJob) {
|
|
68
|
+
res.write(
|
|
69
|
+
`data: ${JSON.stringify({
|
|
70
|
+
type: 'status',
|
|
71
|
+
status: currentJob.status,
|
|
72
|
+
elapsed: Math.round((new Date().getTime() - (currentJob.startedAt?.getTime() || 0)) / 1000),
|
|
73
|
+
})}\n\n`
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
res.end();
|
|
77
|
+
return;
|
|
78
|
+
} else {
|
|
79
|
+
noChangeCount++;
|
|
80
|
+
if (noChangeCount >= maxNoChangeAttempts) {
|
|
81
|
+
// No new events for a while, close connection
|
|
82
|
+
res.end();
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
} catch {
|
|
87
|
+
// Ignore file read errors
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// Send initial status
|
|
92
|
+
res.write(`data: ${JSON.stringify({ type: 'start', jobId: job.id, status: job.status })}\n\n`);
|
|
93
|
+
|
|
94
|
+
// Send progress updates every 2 seconds
|
|
95
|
+
const interval = setInterval(() => {
|
|
96
|
+
if (res.destroyed) {
|
|
97
|
+
clearInterval(interval);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
sendProgressUpdate();
|
|
101
|
+
}, 2000);
|
|
102
|
+
|
|
103
|
+
// Clean up on client disconnect
|
|
104
|
+
req.on('close', () => {
|
|
105
|
+
clearInterval(interval);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Regular JSONL response
|
|
112
|
+
const progressFile = path.join(config.resultsDir, job.id, 'progress.jsonl');
|
|
113
|
+
if (!fs.existsSync(progressFile)) {
|
|
114
|
+
const tailParam = Number(req.query.tail ?? 25);
|
|
115
|
+
const tail = Number.isFinite(tailParam) ? Math.max(0, Math.floor(tailParam)) : 25;
|
|
116
|
+
const events =
|
|
117
|
+
typeof scheduler.getLiveProgressEvents === 'function'
|
|
118
|
+
? scheduler.getLiveProgressEvents(job.id, tail).map((event) => normalizeProgressEvent(event))
|
|
119
|
+
: [];
|
|
120
|
+
if (events.length > 0) {
|
|
121
|
+
return res.json({
|
|
122
|
+
id: job.id,
|
|
123
|
+
status: job.status,
|
|
124
|
+
events,
|
|
125
|
+
total: events.length,
|
|
126
|
+
source: 'docker-logs',
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
return sendErrorResponse(res, 404, 'Not Found', 'Progress file not found');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
const content = fs.readFileSync(progressFile, 'utf-8');
|
|
134
|
+
const lines = content.trim().length > 0 ? content.trim().split('\n') : [];
|
|
135
|
+
const tailParam = Number(req.query.tail ?? lines.length);
|
|
136
|
+
const tail = Number.isFinite(tailParam) ? Math.max(0, Math.floor(tailParam)) : lines.length;
|
|
137
|
+
const selectedLines = tail > 0 ? lines.slice(-tail) : [];
|
|
138
|
+
const events = selectedLines
|
|
139
|
+
.map((line) => {
|
|
140
|
+
try {
|
|
141
|
+
return normalizeProgressEvent(JSON.parse(line));
|
|
142
|
+
} catch {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
})
|
|
146
|
+
.filter((event): event is Record<string, unknown> => event !== null);
|
|
147
|
+
|
|
148
|
+
res.json({
|
|
149
|
+
id: job.id,
|
|
150
|
+
status: job.status,
|
|
151
|
+
events,
|
|
152
|
+
total: lines.length,
|
|
153
|
+
});
|
|
154
|
+
} catch (err) {
|
|
155
|
+
sendErrorResponse(res, 500, 'Internal Server Error', `Failed to read progress: ${(err as Error).message}`);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* GET /api/runs/:id/events - Controller-friendly event stream snapshot.
|
|
161
|
+
*
|
|
162
|
+
* This endpoint always prefers promoted progress.jsonl events, then appends
|
|
163
|
+
* live Docker progress while a worker is still running.
|
|
164
|
+
*/
|
|
165
|
+
router.get('/runs/:id/events', (req: Request, res: Response) => {
|
|
166
|
+
const job = getJobOrRespond(scheduler, req.params.id, res);
|
|
167
|
+
if (!job) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const tailParam = Number(req.query.tail ?? 50);
|
|
172
|
+
const tail = Number.isFinite(tailParam) ? Math.max(0, Math.floor(tailParam)) : 50;
|
|
173
|
+
const progressFile = path.join(config.resultsDir, job.id, 'progress.jsonl');
|
|
174
|
+
const events: Array<Record<string, unknown>> = [];
|
|
175
|
+
const sources = new Set<string>();
|
|
176
|
+
|
|
177
|
+
if (fs.existsSync(progressFile) && isNonEmptyFile(progressFile)) {
|
|
178
|
+
try {
|
|
179
|
+
const lines = fs.readFileSync(progressFile, 'utf-8').trim().split('\n');
|
|
180
|
+
for (const line of lines) {
|
|
181
|
+
try {
|
|
182
|
+
events.push(normalizeProgressEvent(JSON.parse(line)));
|
|
183
|
+
} catch {
|
|
184
|
+
// Skip partial or malformed progress records.
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
sources.add('progress.jsonl');
|
|
188
|
+
} catch {
|
|
189
|
+
// Live Docker fallback below keeps the endpoint useful while a run is active.
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (job.status === 'running' && typeof scheduler.getLiveProgressEvents === 'function') {
|
|
194
|
+
const liveEvents = scheduler.getLiveProgressEvents(job.id, tail);
|
|
195
|
+
for (const event of liveEvents) {
|
|
196
|
+
events.push(normalizeProgressEvent(event));
|
|
197
|
+
}
|
|
198
|
+
if (liveEvents.length > 0) {
|
|
199
|
+
sources.add('docker-logs');
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const selectedEvents = tail > 0 ? events.slice(-tail) : [];
|
|
204
|
+
res.json({
|
|
205
|
+
id: job.id,
|
|
206
|
+
status: job.status,
|
|
207
|
+
events: selectedEvents,
|
|
208
|
+
total: events.length,
|
|
209
|
+
sources: Array.from(sources),
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* GET /api/runs/:id/logs/:logtype - Retrieve logs.
|
|
215
|
+
*/
|
|
216
|
+
router.get('/runs/:id/logs/:logtype', (req: Request, res: Response) => {
|
|
217
|
+
const job = getJobOrRespond(scheduler, req.params.id, res);
|
|
218
|
+
if (!job) {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const logType = req.params.logtype;
|
|
223
|
+
const validLogTypes = ['stdout', 'stderr', 'validation', 'progress', 'quality', 'secret-scan'];
|
|
224
|
+
|
|
225
|
+
if (!validLogTypes.includes(logType)) {
|
|
226
|
+
return sendErrorResponse(
|
|
227
|
+
res,
|
|
228
|
+
400,
|
|
229
|
+
'Bad Request',
|
|
230
|
+
`Unknown log type: ${logType}. Valid types: ${validLogTypes.join(', ')}`
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
try {
|
|
235
|
+
const logFile = path.join(config.resultsDir, job.id, logType === 'stdout' ? 'stdout.log' : `${logType}.log`);
|
|
236
|
+
|
|
237
|
+
if (!fs.existsSync(logFile)) {
|
|
238
|
+
if (
|
|
239
|
+
job.status === 'running' &&
|
|
240
|
+
(logType === 'stdout' || logType === 'stderr' || logType === 'progress') &&
|
|
241
|
+
typeof scheduler.getLiveDockerLogTail === 'function'
|
|
242
|
+
) {
|
|
243
|
+
const liveContent = scheduler.getLiveDockerLogTail(job.id, 300);
|
|
244
|
+
if (liveContent) {
|
|
245
|
+
const response: LogResponse = {
|
|
246
|
+
logType: logType as any,
|
|
247
|
+
content: liveContent,
|
|
248
|
+
size: Buffer.byteLength(liveContent, 'utf-8'),
|
|
249
|
+
};
|
|
250
|
+
return res.json(response);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (logType === 'stderr' && job.status === 'failed') {
|
|
254
|
+
const syntheticStderr = [
|
|
255
|
+
'[kaseki] Synthetic stderr fallback',
|
|
256
|
+
`job id: ${job.id}`,
|
|
257
|
+
`exit code: ${job.exitCode ?? 'unknown'}`,
|
|
258
|
+
`failure class: ${job.failureClass ?? 'unknown'}`,
|
|
259
|
+
`job.error: ${job.error ?? 'unknown'}`,
|
|
260
|
+
'canonical stderr.log was not generated for this failed run.',
|
|
261
|
+
].join('\n');
|
|
262
|
+
|
|
263
|
+
const fallbackResponse: LogResponse = {
|
|
264
|
+
logType: 'stderr',
|
|
265
|
+
content: syntheticStderr,
|
|
266
|
+
size: Buffer.byteLength(syntheticStderr, 'utf-8'),
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
return res.status(200).json(fallbackResponse);
|
|
270
|
+
}
|
|
271
|
+
return sendErrorResponse(res, 404, 'Not Found', `Log file not found: ${logType}`);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const stat = fs.statSync(logFile);
|
|
275
|
+
const size = stat.size;
|
|
276
|
+
|
|
277
|
+
// For large files, just return metadata and a truncated tail
|
|
278
|
+
const maxSize = 1024 * 100; // 100 KB
|
|
279
|
+
let content = '';
|
|
280
|
+
|
|
281
|
+
if (size > maxSize) {
|
|
282
|
+
const truncated = readTailBytes(logFile, size, maxSize);
|
|
283
|
+
|
|
284
|
+
let tailContent = decodeUtf8TailSafely(truncated);
|
|
285
|
+
if (req.query.tail === 'lines') {
|
|
286
|
+
const lineCount = Number(req.query.lines ?? 200);
|
|
287
|
+
const maxLines = Number.isFinite(lineCount) ? Math.max(1, Math.floor(lineCount)) : 200;
|
|
288
|
+
tailContent = tailLogByLines(tailContent, maxLines);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
content = `[... truncated, showing last ${maxSize} bytes ...]\n${tailContent}`;
|
|
292
|
+
} else {
|
|
293
|
+
content = fs.readFileSync(logFile, 'utf-8');
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const response: LogResponse = {
|
|
297
|
+
logType: logType as any,
|
|
298
|
+
content,
|
|
299
|
+
size,
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
res.json(response);
|
|
303
|
+
} catch (err) {
|
|
304
|
+
sendErrorResponse(res, 500, 'Internal Server Error', `Failed to read log: ${(err as Error).message}`);
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* GET /api/runs/:id/analysis - Comprehensive run analysis.
|
|
310
|
+
*/
|
|
311
|
+
router.get('/runs/:id/analysis', (req: Request, res: Response) => {
|
|
312
|
+
const job = getJobOrRespond(scheduler, req.params.id, res);
|
|
313
|
+
if (!job) {
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
try {
|
|
318
|
+
const response: AnalysisResponse = {
|
|
319
|
+
id: job.id,
|
|
320
|
+
status: job.status,
|
|
321
|
+
createdAt: job.createdAt.toISOString(),
|
|
322
|
+
completedAt: job.completedAt?.toISOString(),
|
|
323
|
+
exitCode: job.exitCode,
|
|
324
|
+
failureClass: job.failureClass,
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
// Add timing
|
|
328
|
+
if (job.startedAt) {
|
|
329
|
+
const elapsed = (job.completedAt || new Date()).getTime() - job.startedAt.getTime();
|
|
330
|
+
response.elapsedSeconds = Math.round(elapsed / 1000);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Try to read metadata
|
|
334
|
+
const metadataPath = path.join(config.resultsDir, job.id, 'metadata.json');
|
|
335
|
+
if (fs.existsSync(metadataPath)) {
|
|
336
|
+
const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf-8'));
|
|
337
|
+
response.metadata = {
|
|
338
|
+
model: metadata.model,
|
|
339
|
+
instance: metadata.instance,
|
|
340
|
+
repo: metadata.repo,
|
|
341
|
+
ref: metadata.ref,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Try to read changed files
|
|
346
|
+
const changedFilesPath = path.join(config.resultsDir, job.id, 'changed-files.txt');
|
|
347
|
+
if (fs.existsSync(changedFilesPath)) {
|
|
348
|
+
const changedFiles = fs
|
|
349
|
+
.readFileSync(changedFilesPath, 'utf-8')
|
|
350
|
+
.trim()
|
|
351
|
+
.split('\n')
|
|
352
|
+
.filter((f) => f);
|
|
353
|
+
|
|
354
|
+
const diffPath = path.join(config.resultsDir, job.id, 'git.diff');
|
|
355
|
+
const diffSize = fs.existsSync(diffPath) ? fs.statSync(diffPath).size : 0;
|
|
356
|
+
|
|
357
|
+
response.changes = {
|
|
358
|
+
changedFiles,
|
|
359
|
+
diffSize,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Try to read validation results
|
|
364
|
+
const validationPath = path.join(config.resultsDir, job.id, 'validation-timings.tsv');
|
|
365
|
+
if (fs.existsSync(validationPath)) {
|
|
366
|
+
const lines = fs.readFileSync(validationPath, 'utf-8').trim().split('\n');
|
|
367
|
+
const commandResults = lines
|
|
368
|
+
.slice(1) // Skip header
|
|
369
|
+
.map((line) => {
|
|
370
|
+
const [command, exitCode, elapsed] = line.split('\t');
|
|
371
|
+
return {
|
|
372
|
+
command,
|
|
373
|
+
exitCode: parseInt(exitCode, 10),
|
|
374
|
+
elapsed: parseInt(elapsed, 10),
|
|
375
|
+
};
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
response.validation = {
|
|
379
|
+
passed: commandResults.every((r) => r.exitCode === 0),
|
|
380
|
+
commandResults,
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
res.json(response);
|
|
385
|
+
} catch (err) {
|
|
386
|
+
sendErrorResponse(res, 500, 'Internal Server Error', `Failed to analyze run: ${(err as Error).message}`);
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
return router;
|
|
391
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { Router, Request, Response } from 'express';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { JobScheduler } from '../job-scheduler';
|
|
5
|
+
import { DEFAULT_JOB_INDEX_MAX_ENTRIES, KasekiApiConfig } from '../kaseki-api-config';
|
|
6
|
+
import { Job, RunsListResponse } from '../kaseki-api-types';
|
|
7
|
+
import { resolveInstanceExitCode } from '../instance-state-derivation';
|
|
8
|
+
import { sendErrorResponse } from '../utils/response-helpers';
|
|
9
|
+
import { getJobOrRespond } from '../utils/route-helpers';
|
|
10
|
+
import { StatusResponseBuilder } from '../utils/status-response-builder';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Create status-related routes (runs list, status, cancel).
|
|
14
|
+
*/
|
|
15
|
+
export function createStatusRoutes(scheduler: JobScheduler, config: KasekiApiConfig): Router {
|
|
16
|
+
const router = Router();
|
|
17
|
+
const statusBuilder = new StatusResponseBuilder(scheduler, config);
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* GET /api/runs - List all runs.
|
|
21
|
+
*/
|
|
22
|
+
router.get('/runs', (_req: Request, res: Response) => {
|
|
23
|
+
const allJobs = scheduler.listJobs();
|
|
24
|
+
|
|
25
|
+
const response: RunsListResponse = {
|
|
26
|
+
runs: allJobs.map((job) => ({
|
|
27
|
+
id: job.id,
|
|
28
|
+
status: job.status,
|
|
29
|
+
createdAt: job.createdAt.toISOString(),
|
|
30
|
+
completedAt: job.completedAt?.toISOString(),
|
|
31
|
+
resultDir: job.resultDir,
|
|
32
|
+
exitCode: resolveJobExitCode(job, config),
|
|
33
|
+
failureClass: job.failureClass,
|
|
34
|
+
error: job.error,
|
|
35
|
+
})),
|
|
36
|
+
total: allJobs.length,
|
|
37
|
+
retention: {
|
|
38
|
+
terminalJobIndexMaxEntries: config.jobIndexMaxEntries ?? DEFAULT_JOB_INDEX_MAX_ENTRIES,
|
|
39
|
+
note: 'Older terminal runs may be omitted from this API index after compaction; their artifacts remain on disk under the results directory.',
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
res.json(response);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* GET /api/runs/:id/status - Get run status.
|
|
48
|
+
*/
|
|
49
|
+
router.get('/runs/:id/status', (req: Request, res: Response) => {
|
|
50
|
+
const job = getJobOrRespond(scheduler, req.params.id, res);
|
|
51
|
+
if (!job) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const response = statusBuilder.buildStatus(job);
|
|
56
|
+
res.json(response);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* POST /api/runs/:id/cancel - Cancel a queued or running run.
|
|
61
|
+
*/
|
|
62
|
+
router.post('/runs/:id/cancel', (req: Request, res: Response) => {
|
|
63
|
+
const job = scheduler.cancelJob(req.params.id);
|
|
64
|
+
if (!job) {
|
|
65
|
+
return sendErrorResponse(res, 404, 'Not Found', `Run not found: ${req.params.id}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const response = statusBuilder.buildStatus(job);
|
|
69
|
+
res.json(response);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
return router;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function resolveJobExitCode(job: Job, config: KasekiApiConfig): number | undefined {
|
|
76
|
+
if (job.exitCode !== undefined && job.exitCode !== null) {
|
|
77
|
+
return job.exitCode;
|
|
78
|
+
}
|
|
79
|
+
if (!(job.status === 'completed' || job.status === 'failed')) {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
const runDir = job.resultDir || path.join(config.resultsDir, job.id);
|
|
83
|
+
try {
|
|
84
|
+
const metadataPath = path.join(runDir, 'metadata.json');
|
|
85
|
+
const metadata = fs.existsSync(metadataPath)
|
|
86
|
+
? JSON.parse(fs.readFileSync(metadataPath, 'utf-8'))
|
|
87
|
+
: {};
|
|
88
|
+
return resolveInstanceExitCode(runDir, metadata) ?? undefined;
|
|
89
|
+
} catch {
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { Router, Request, Response } from 'express';
|
|
2
|
+
import * as crypto from 'crypto';
|
|
3
|
+
import { sendErrorResponse } from '../utils/response-helpers';
|
|
4
|
+
import { createEventLogger } from '../logger';
|
|
5
|
+
|
|
6
|
+
const logger = createEventLogger('api');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Create webhook-related routes.
|
|
10
|
+
*/
|
|
11
|
+
export function createWebhookRoutes(): Router {
|
|
12
|
+
const router = Router();
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* POST /api/webhooks/test - Test webhook configuration.
|
|
16
|
+
*/
|
|
17
|
+
router.post('/webhooks/test', async (req: Request, res: Response) => {
|
|
18
|
+
try {
|
|
19
|
+
const { url, secret } = req.body;
|
|
20
|
+
|
|
21
|
+
if (!url || typeof url !== 'string') {
|
|
22
|
+
return sendErrorResponse(res, 400, 'Bad Request', 'Webhook URL is required');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Validate URL format
|
|
26
|
+
try {
|
|
27
|
+
new URL(url);
|
|
28
|
+
} catch {
|
|
29
|
+
return sendErrorResponse(res, 400, 'Bad Request', 'Invalid webhook URL format');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Send test webhook
|
|
33
|
+
let statusCode: number | undefined;
|
|
34
|
+
let error: string | undefined;
|
|
35
|
+
let durationMs = 0;
|
|
36
|
+
const startTime = Date.now();
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const testPayload = {
|
|
40
|
+
eventType: 'webhook.test',
|
|
41
|
+
jobId: 'test',
|
|
42
|
+
timestamp: new Date().toISOString(),
|
|
43
|
+
data: { message: 'This is a test webhook from kaseki-agent API' },
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// Generate HMAC signature if secret provided
|
|
47
|
+
let signature: string | null = null;
|
|
48
|
+
if (secret && typeof secret === 'string') {
|
|
49
|
+
const body = JSON.stringify(testPayload);
|
|
50
|
+
signature = crypto.createHmac('sha256', secret).update(body).digest('hex');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const response = await fetch(url, {
|
|
54
|
+
method: 'POST',
|
|
55
|
+
headers: {
|
|
56
|
+
'Content-Type': 'application/json',
|
|
57
|
+
'X-Kaseki-Event': 'webhook.test',
|
|
58
|
+
'X-Kaseki-Job-Id': 'test',
|
|
59
|
+
...(signature && { 'X-Kaseki-Signature': `sha256=${signature}` }),
|
|
60
|
+
},
|
|
61
|
+
body: JSON.stringify(testPayload),
|
|
62
|
+
signal: AbortSignal.timeout(10000),
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
durationMs = Date.now() - startTime;
|
|
66
|
+
statusCode = response.status;
|
|
67
|
+
|
|
68
|
+
if (!response.ok) {
|
|
69
|
+
error = `HTTP ${response.status} ${response.statusText}`;
|
|
70
|
+
}
|
|
71
|
+
} catch (err) {
|
|
72
|
+
durationMs = Date.now() - startTime;
|
|
73
|
+
error = err instanceof Error ? err.message : String(err);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const result = {
|
|
77
|
+
url,
|
|
78
|
+
statusCode,
|
|
79
|
+
durationMs,
|
|
80
|
+
success: !error,
|
|
81
|
+
error,
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
logger.event('webhook_test', result);
|
|
85
|
+
|
|
86
|
+
res.json(result);
|
|
87
|
+
} catch (err) {
|
|
88
|
+
logger.event('api_error', {
|
|
89
|
+
path: '/webhooks/test',
|
|
90
|
+
error: (err as Error).message,
|
|
91
|
+
});
|
|
92
|
+
return sendErrorResponse(res, 400, 'Bad Request', (err as Error).message);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
return router;
|
|
97
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as os from 'os';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { RunArtifactMetadataCache } from './run-artifact-metadata-cache';
|
|
5
|
+
|
|
6
|
+
describe('RunArtifactMetadataCache', () => {
|
|
7
|
+
let resultsDir: string;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
resultsDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kaseki-artifact-metadata-cache-'));
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
fs.rmSync(resultsDir, { recursive: true, force: true });
|
|
15
|
+
jest.restoreAllMocks();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('terminal metadata requests populate and reuse a cache entry', () => {
|
|
19
|
+
const cache = new RunArtifactMetadataCache();
|
|
20
|
+
const jobDir = path.join(resultsDir, 'kaseki-terminal-cache-hit');
|
|
21
|
+
fs.mkdirSync(jobDir, { recursive: true });
|
|
22
|
+
fs.writeFileSync(path.join(jobDir, 'metadata.json'), '{}');
|
|
23
|
+
|
|
24
|
+
const first = cache.get('kaseki-terminal-cache-hit', jobDir, ['metadata.json'], true);
|
|
25
|
+
const second = cache.get('kaseki-terminal-cache-hit', jobDir, ['metadata.json'], true);
|
|
26
|
+
|
|
27
|
+
expect(first['metadata.json']).toEqual(second['metadata.json']);
|
|
28
|
+
expect(second['metadata.json']).toMatchObject({ exists: true, size: 2 });
|
|
29
|
+
expect(cache.getStats()).toEqual({ entries: 1 });
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('non-terminal metadata requests are not cached', () => {
|
|
33
|
+
const cache = new RunArtifactMetadataCache();
|
|
34
|
+
const jobDir = path.join(resultsDir, 'kaseki-running-no-cache');
|
|
35
|
+
const metadataPath = path.join(jobDir, 'metadata.json');
|
|
36
|
+
fs.mkdirSync(jobDir, { recursive: true });
|
|
37
|
+
fs.writeFileSync(metadataPath, '{}');
|
|
38
|
+
|
|
39
|
+
const first = cache.get('kaseki-running-no-cache', jobDir, ['metadata.json'], false);
|
|
40
|
+
fs.writeFileSync(metadataPath, '{"updated":true}');
|
|
41
|
+
const second = cache.get('kaseki-running-no-cache', jobDir, ['metadata.json'], false);
|
|
42
|
+
|
|
43
|
+
expect(first['metadata.json'].size).toBe(2);
|
|
44
|
+
expect(second['metadata.json'].size).toBe(16);
|
|
45
|
+
expect(cache.getStats()).toEqual({ entries: 0 });
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('terminal cache entries invalidate when artifact size and mtime change', () => {
|
|
49
|
+
const cache = new RunArtifactMetadataCache();
|
|
50
|
+
const jobDir = path.join(resultsDir, 'kaseki-terminal-cache-invalidate');
|
|
51
|
+
const summaryPath = path.join(jobDir, 'result-summary.md');
|
|
52
|
+
fs.mkdirSync(jobDir, { recursive: true });
|
|
53
|
+
fs.writeFileSync(summaryPath, 'one');
|
|
54
|
+
|
|
55
|
+
const first = cache.get('kaseki-terminal-cache-invalidate', jobDir, ['result-summary.md'], true);
|
|
56
|
+
const nextMtime = new Date(Date.now() + 10_000);
|
|
57
|
+
fs.writeFileSync(summaryPath, 'one plus more');
|
|
58
|
+
fs.utimesSync(summaryPath, nextMtime, nextMtime);
|
|
59
|
+
const second = cache.get('kaseki-terminal-cache-invalidate', jobDir, ['result-summary.md'], true);
|
|
60
|
+
|
|
61
|
+
expect(first['result-summary.md'].size).toBe(3);
|
|
62
|
+
expect(second['result-summary.md'].size).toBe(13);
|
|
63
|
+
expect(second['result-summary.md'].mtimeMs).not.toBe(first['result-summary.md'].mtimeMs);
|
|
64
|
+
expect(cache.getStats()).toEqual({ entries: 1 });
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('clear removes a terminal cache entry for a job result directory', () => {
|
|
68
|
+
const cache = new RunArtifactMetadataCache();
|
|
69
|
+
const jobDir = path.join(resultsDir, 'kaseki-clear-cache');
|
|
70
|
+
fs.mkdirSync(jobDir, { recursive: true });
|
|
71
|
+
fs.writeFileSync(path.join(jobDir, 'failure.json'), '{}');
|
|
72
|
+
|
|
73
|
+
cache.get('kaseki-clear-cache', jobDir, ['failure.json'], true);
|
|
74
|
+
expect(cache.getStats()).toEqual({ entries: 1 });
|
|
75
|
+
|
|
76
|
+
cache.clear('kaseki-clear-cache', jobDir);
|
|
77
|
+
|
|
78
|
+
expect(cache.getStats()).toEqual({ entries: 0 });
|
|
79
|
+
});
|
|
80
|
+
});
|