@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,122 @@
|
|
|
1
|
+
import { sendErrorResponse, buildStatusResponse, detectContentType } from './response-helpers';
|
|
2
|
+
|
|
3
|
+
describe('response-helpers', () => {
|
|
4
|
+
describe('sendErrorResponse', () => {
|
|
5
|
+
it('should send a properly formatted error response', () => {
|
|
6
|
+
const mockResponse = {
|
|
7
|
+
body: null as any,
|
|
8
|
+
statusValue: 200,
|
|
9
|
+
status: function(code: number) {
|
|
10
|
+
this.statusValue = code;
|
|
11
|
+
return this;
|
|
12
|
+
},
|
|
13
|
+
json: function(data: any) {
|
|
14
|
+
this.body = data;
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
sendErrorResponse(mockResponse as any, 404, 'Not Found', 'Run not found: abc123');
|
|
19
|
+
|
|
20
|
+
expect(mockResponse.statusValue).toBe(404);
|
|
21
|
+
expect(mockResponse.body).toEqual({
|
|
22
|
+
type: 'https://api.kaseki.local/errors#not-found',
|
|
23
|
+
title: 'Not Found',
|
|
24
|
+
status: 404,
|
|
25
|
+
detail: 'Run not found: abc123',
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should convert title to kebab-case type URL', () => {
|
|
30
|
+
const mockResponse = {
|
|
31
|
+
body: null as any,
|
|
32
|
+
statusValue: 200,
|
|
33
|
+
status: function(code: number) {
|
|
34
|
+
this.statusValue = code;
|
|
35
|
+
return this;
|
|
36
|
+
},
|
|
37
|
+
json: function(data: any) {
|
|
38
|
+
this.body = data;
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
sendErrorResponse(mockResponse as any, 400, 'Bad Request', 'Invalid input');
|
|
43
|
+
|
|
44
|
+
expect(mockResponse.body.type).toBe('https://api.kaseki.local/errors#bad-request');
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('buildStatusResponse', () => {
|
|
49
|
+
it('should build a complete status response', () => {
|
|
50
|
+
const response = buildStatusResponse({
|
|
51
|
+
id: 'run-123',
|
|
52
|
+
status: 'running',
|
|
53
|
+
exitCode: 0,
|
|
54
|
+
failureClass: 'test-failure',
|
|
55
|
+
correlationId: 'corr-456',
|
|
56
|
+
requestId: 'req-789',
|
|
57
|
+
error: 'test error',
|
|
58
|
+
resultDir: '/results/run-123',
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
expect(response).toEqual({
|
|
62
|
+
id: 'run-123',
|
|
63
|
+
status: 'running',
|
|
64
|
+
exitCode: 0,
|
|
65
|
+
failureClass: 'test-failure',
|
|
66
|
+
correlationId: 'corr-456',
|
|
67
|
+
requestId: 'req-789',
|
|
68
|
+
error: 'test error',
|
|
69
|
+
resultDir: '/results/run-123',
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should filter out null and undefined optional fields', () => {
|
|
74
|
+
const response = buildStatusResponse({
|
|
75
|
+
id: 'run-123',
|
|
76
|
+
status: 'completed',
|
|
77
|
+
exitCode: null,
|
|
78
|
+
failureClass: undefined,
|
|
79
|
+
error: null,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
expect(response).toEqual({
|
|
83
|
+
id: 'run-123',
|
|
84
|
+
status: 'completed',
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should handle optional fields being omitted', () => {
|
|
89
|
+
const response = buildStatusResponse({
|
|
90
|
+
id: 'run-123',
|
|
91
|
+
status: 'completed',
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
expect(response).toEqual({
|
|
95
|
+
id: 'run-123',
|
|
96
|
+
status: 'completed',
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe('detectContentType', () => {
|
|
102
|
+
it('should detect JSON content type', () => {
|
|
103
|
+
expect(detectContentType('metadata.json')).toBe('application/json');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('should detect markdown content type', () => {
|
|
107
|
+
expect(detectContentType('result-summary.md')).toBe('text/markdown');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should detect JSONL content type', () => {
|
|
111
|
+
expect(detectContentType('progress.jsonl')).toBe('application/x-jsonl');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('should detect diff content type', () => {
|
|
115
|
+
expect(detectContentType('git.diff')).toBe('text/plain');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('should default to text/plain', () => {
|
|
119
|
+
expect(detectContentType('unknown.xyz')).toBe('text/plain');
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
});
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { Response } from 'express';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import { ErrorResponse, StatusResponse } from '../kaseki-api-types';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Send a standardized error response.
|
|
7
|
+
* Consolidates error response generation across all endpoints.
|
|
8
|
+
*/
|
|
9
|
+
export function sendErrorResponse(
|
|
10
|
+
res: Response,
|
|
11
|
+
status: number,
|
|
12
|
+
title: string,
|
|
13
|
+
detail: string
|
|
14
|
+
): void {
|
|
15
|
+
const response: ErrorResponse = {
|
|
16
|
+
type: 'https://api.kaseki.local/errors#' + title.toLowerCase().replace(/\s+/g, '-'),
|
|
17
|
+
title,
|
|
18
|
+
status,
|
|
19
|
+
detail,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
res.status(status).json(response);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Build a StatusResponse from job metadata.
|
|
27
|
+
* Consolidates StatusResponse construction logic used in multiple routes.
|
|
28
|
+
*/
|
|
29
|
+
export function buildStatusResponse(jobData: {
|
|
30
|
+
id: string;
|
|
31
|
+
status: 'queued' | 'running' | 'completed' | 'failed';
|
|
32
|
+
exitCode?: number | null;
|
|
33
|
+
failureClass?: string | null;
|
|
34
|
+
correlationId?: string;
|
|
35
|
+
requestId?: string;
|
|
36
|
+
error?: string | null;
|
|
37
|
+
resultDir?: string;
|
|
38
|
+
}): StatusResponse {
|
|
39
|
+
const response: StatusResponse = {
|
|
40
|
+
id: jobData.id,
|
|
41
|
+
status: jobData.status,
|
|
42
|
+
correlationId: jobData.correlationId,
|
|
43
|
+
requestId: jobData.requestId,
|
|
44
|
+
resultDir: jobData.resultDir,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// Only include optional fields if they are not null
|
|
48
|
+
if (jobData.exitCode !== null && jobData.exitCode !== undefined) {
|
|
49
|
+
response.exitCode = jobData.exitCode;
|
|
50
|
+
}
|
|
51
|
+
if (jobData.failureClass !== null && jobData.failureClass !== undefined) {
|
|
52
|
+
response.failureClass = jobData.failureClass;
|
|
53
|
+
}
|
|
54
|
+
if (jobData.error !== null && jobData.error !== undefined) {
|
|
55
|
+
response.error = jobData.error;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return response;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Send a file response with appropriate headers for artifact delivery.
|
|
63
|
+
* Handles Content-Type detection and streaming.
|
|
64
|
+
*/
|
|
65
|
+
export function sendFileResponse(
|
|
66
|
+
res: Response,
|
|
67
|
+
filePath: string,
|
|
68
|
+
fileName: string,
|
|
69
|
+
options?: { stream?: boolean }
|
|
70
|
+
): void {
|
|
71
|
+
try {
|
|
72
|
+
const stat = fs.statSync(filePath);
|
|
73
|
+
const contentType = detectContentType(fileName);
|
|
74
|
+
|
|
75
|
+
res.setHeader('Content-Type', contentType);
|
|
76
|
+
res.setHeader('Content-Length', stat.size);
|
|
77
|
+
res.setHeader('Content-Disposition', `inline; filename="${fileName}"`);
|
|
78
|
+
|
|
79
|
+
if (options?.stream) {
|
|
80
|
+
const stream = fs.createReadStream(filePath);
|
|
81
|
+
stream.pipe(res);
|
|
82
|
+
} else {
|
|
83
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
84
|
+
res.send(content);
|
|
85
|
+
}
|
|
86
|
+
} catch (error) {
|
|
87
|
+
sendErrorResponse(res, 500, 'Internal Server Error', `Failed to read file: ${fileName}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Detect Content-Type based on file extension.
|
|
93
|
+
* Used for artifact responses.
|
|
94
|
+
*/
|
|
95
|
+
export function detectContentType(fileName: string): string {
|
|
96
|
+
if (fileName.endsWith('.json')) return 'application/json';
|
|
97
|
+
if (fileName.endsWith('.md')) return 'text/markdown';
|
|
98
|
+
if (fileName.endsWith('.jsonl')) return 'application/x-jsonl';
|
|
99
|
+
if (fileName.endsWith('.diff')) return 'text/plain';
|
|
100
|
+
return 'text/plain';
|
|
101
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Route helper utilities for common patterns across API routes.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Response } from 'express';
|
|
6
|
+
import { JobScheduler } from '../job-scheduler';
|
|
7
|
+
import { Job } from '../kaseki-api-types';
|
|
8
|
+
import { sendErrorResponse } from './response-helpers';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Extract and validate a job ID from a request, sending an error response if not found.
|
|
12
|
+
* Consolidates the pattern used across artifact, log, and status routes.
|
|
13
|
+
*
|
|
14
|
+
* @param scheduler Job scheduler instance
|
|
15
|
+
* @param jobId Job ID to look up
|
|
16
|
+
* @param res Express response object (for error responses)
|
|
17
|
+
* @returns Job object if found, null if error response already sent
|
|
18
|
+
*/
|
|
19
|
+
export function getJobOrRespond(
|
|
20
|
+
scheduler: JobScheduler,
|
|
21
|
+
jobId: string,
|
|
22
|
+
res: Response
|
|
23
|
+
): Job | null {
|
|
24
|
+
const job = scheduler.getJob(jobId);
|
|
25
|
+
if (!job) {
|
|
26
|
+
sendErrorResponse(res, 404, 'Not Found', `Run not found: ${jobId}`);
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
return job;
|
|
30
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import * as path from 'path';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import { Job } from '../kaseki-api-types';
|
|
4
|
+
import { StatusResponse } from '../kaseki-api-types';
|
|
5
|
+
import { KasekiApiConfig } from '../kaseki-api-config';
|
|
6
|
+
import { JobScheduler } from '../job-scheduler';
|
|
7
|
+
import { getRunArtifactMetadata } from '../run-artifact-metadata-cache';
|
|
8
|
+
import { resolveInstanceExitCode, extractValidationFailureReason, extractQualityFailureReason } from '../instance-state-derivation';
|
|
9
|
+
import { toStructuredProgress } from './progress-normalizer';
|
|
10
|
+
import { readLastJsonlEvent } from './file-helpers';
|
|
11
|
+
|
|
12
|
+
const STATUS_KEY_FILES = ['metadata.json', 'analysis.md', 'result-summary.md', 'failure.json', 'stderr.log'] as const;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Builds StatusResponse objects with timing, progress, and artifact information.
|
|
16
|
+
* Encapsulates complex response building logic from status routes.
|
|
17
|
+
*/
|
|
18
|
+
export class StatusResponseBuilder {
|
|
19
|
+
constructor(
|
|
20
|
+
private scheduler: JobScheduler,
|
|
21
|
+
private config: KasekiApiConfig
|
|
22
|
+
) {}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Build a complete StatusResponse for a job.
|
|
26
|
+
*/
|
|
27
|
+
buildStatus(job: Job): StatusResponse {
|
|
28
|
+
const runDir = job.resultDir || path.join(this.config.resultsDir, job.id);
|
|
29
|
+
const exitCode = this.resolveExitCode(job, runDir);
|
|
30
|
+
const metadata = this.readMetadata(runDir);
|
|
31
|
+
const validationReason = extractValidationFailureReason(metadata);
|
|
32
|
+
const qualityReason = extractQualityFailureReason(metadata);
|
|
33
|
+
const response: StatusResponse = {
|
|
34
|
+
id: job.id,
|
|
35
|
+
status: job.status,
|
|
36
|
+
exitCode: exitCode ?? undefined,
|
|
37
|
+
failureClass: job.failureClass,
|
|
38
|
+
validationFailureReason: validationReason ?? undefined,
|
|
39
|
+
qualityFailureReason: qualityReason ?? undefined,
|
|
40
|
+
correlationId: job.correlationId,
|
|
41
|
+
requestId: job.requestId,
|
|
42
|
+
error: job.error,
|
|
43
|
+
resultDir: job.resultDir,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
this.addTimingInfo(response, job);
|
|
47
|
+
this.addProgressInfo(response, job);
|
|
48
|
+
this.addArtifactInfo(response, job);
|
|
49
|
+
|
|
50
|
+
return response;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
private addTimingInfo(response: StatusResponse, job: Job): void {
|
|
54
|
+
if (job.startedAt) {
|
|
55
|
+
const elapsed = (job.completedAt || new Date()).getTime() - job.startedAt.getTime();
|
|
56
|
+
response.elapsedSeconds = Math.round(elapsed / 1000);
|
|
57
|
+
|
|
58
|
+
const timeoutSeconds = job.effectiveTimeoutSeconds ?? this.config.agentTimeoutSeconds;
|
|
59
|
+
const timeoutMs = timeoutSeconds * 1000;
|
|
60
|
+
response.timeoutRiskPercent = Math.round((elapsed / timeoutMs) * 100);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private addProgressInfo(response: StatusResponse, job: Job): void {
|
|
65
|
+
if (job.status !== 'running') {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const progressFile = path.join(this.config.resultsDir, job.id, 'progress.jsonl');
|
|
71
|
+
const lastFileEvent = readLastJsonlEvent(progressFile);
|
|
72
|
+
if (lastFileEvent) {
|
|
73
|
+
const structuredProgress = toStructuredProgress(lastFileEvent);
|
|
74
|
+
if (structuredProgress) {
|
|
75
|
+
response.progress = structuredProgress;
|
|
76
|
+
}
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (typeof this.scheduler.getLiveProgressEvents === 'function') {
|
|
81
|
+
const liveEvents = this.scheduler.getLiveProgressEvents(job.id, 1);
|
|
82
|
+
const lastEvent = liveEvents[liveEvents.length - 1];
|
|
83
|
+
if (lastEvent) {
|
|
84
|
+
const structuredProgress = toStructuredProgress(lastEvent, 'running');
|
|
85
|
+
if (structuredProgress) {
|
|
86
|
+
response.progress = structuredProgress;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
} catch {
|
|
91
|
+
// Ignore progress file errors; status remains resilient
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private addArtifactInfo(response: StatusResponse, job: Job): void {
|
|
96
|
+
if (!(job.status === 'completed' || job.status === 'failed')) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const runDir = job.resultDir || path.join(this.config.resultsDir, job.id);
|
|
101
|
+
const metadata = getRunArtifactMetadata(job.id, runDir, STATUS_KEY_FILES, true);
|
|
102
|
+
const keyFileAvailability = STATUS_KEY_FILES.reduce(
|
|
103
|
+
(acc, fileName) => {
|
|
104
|
+
acc[fileName] = metadata[fileName]?.exists === true && metadata[fileName].size > 0;
|
|
105
|
+
return acc;
|
|
106
|
+
},
|
|
107
|
+
{} as Record<(typeof STATUS_KEY_FILES)[number], boolean>
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
response.artifacts = {
|
|
111
|
+
metadataJson: keyFileAvailability['metadata.json'],
|
|
112
|
+
analysisMd: keyFileAvailability['analysis.md'],
|
|
113
|
+
resultSummaryMd: keyFileAvailability['result-summary.md'],
|
|
114
|
+
failureJson: keyFileAvailability['failure.json'],
|
|
115
|
+
stderrLog: keyFileAvailability['stderr.log'],
|
|
116
|
+
availableFiles: STATUS_KEY_FILES.filter((fileName) => keyFileAvailability[fileName]),
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
if (job.status === 'failed') {
|
|
120
|
+
if (keyFileAvailability['failure.json']) {
|
|
121
|
+
response.diagnosticEntryPoint = 'failure.json';
|
|
122
|
+
} else if (keyFileAvailability['analysis.md']) {
|
|
123
|
+
response.diagnosticEntryPoint = 'analysis.md';
|
|
124
|
+
} else if (keyFileAvailability['result-summary.md']) {
|
|
125
|
+
response.diagnosticEntryPoint = 'result-summary.md';
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private readMetadata(runDir: string): any {
|
|
131
|
+
try {
|
|
132
|
+
const metadataPath = path.join(runDir, 'metadata.json');
|
|
133
|
+
if (fs.existsSync(metadataPath)) {
|
|
134
|
+
return JSON.parse(fs.readFileSync(metadataPath, 'utf-8'));
|
|
135
|
+
}
|
|
136
|
+
} catch {
|
|
137
|
+
// Ignore metadata read errors
|
|
138
|
+
}
|
|
139
|
+
return {};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private resolveExitCode(job: Job, runDir: string): number | null {
|
|
143
|
+
if (job.exitCode !== undefined && job.exitCode !== null) {
|
|
144
|
+
return job.exitCode;
|
|
145
|
+
}
|
|
146
|
+
if (!(job.status === 'completed' || job.status === 'failed')) {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
try {
|
|
150
|
+
const metadataPath = path.join(runDir, 'metadata.json');
|
|
151
|
+
const metadata = fs.existsSync(metadataPath)
|
|
152
|
+
? JSON.parse(fs.readFileSync(metadataPath, 'utf-8'))
|
|
153
|
+
: {};
|
|
154
|
+
return resolveInstanceExitCode(runDir, metadata);
|
|
155
|
+
} catch {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type guard utilities for common type checks.
|
|
3
|
+
* Reduces verbosity and provides reusable predicates across the codebase.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Check if value is a string.
|
|
8
|
+
*/
|
|
9
|
+
export const isString = (value: unknown): value is string => typeof value === 'string';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Check if value is a number.
|
|
13
|
+
*/
|
|
14
|
+
export const isNumber = (value: unknown): value is number => typeof value === 'number';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Check if value is a boolean.
|
|
18
|
+
*/
|
|
19
|
+
export const isBoolean = (value: unknown): value is boolean => typeof value === 'boolean';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Check if value is a record (plain object).
|
|
23
|
+
*/
|
|
24
|
+
export const isRecord = (value: unknown): value is Record<string, unknown> => {
|
|
25
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Check if value is an array.
|
|
30
|
+
*/
|
|
31
|
+
export const isArray = (value: unknown): value is unknown[] => Array.isArray(value);
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Check if value is an array of strings.
|
|
35
|
+
*/
|
|
36
|
+
export const isStringArray = (value: unknown): value is string[] => {
|
|
37
|
+
return Array.isArray(value) && value.every((item) => typeof item === 'string');
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Check if value is an array of objects (records).
|
|
42
|
+
*/
|
|
43
|
+
export const isRecordArray = (value: unknown): value is Record<string, unknown>[] => {
|
|
44
|
+
return Array.isArray(value) && value.every((item) => isRecord(item));
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Check if value is an object with specific keys (shallow validation).
|
|
49
|
+
*/
|
|
50
|
+
export const hasKeys = (value: unknown, ...keys: string[]): value is Record<string, unknown> => {
|
|
51
|
+
return isRecord(value) && keys.every((key) => key in value);
|
|
52
|
+
};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UTF-8 safe string encoding/decoding utilities.
|
|
3
|
+
* Handles proper UTF-8 sequence detection to prevent breaking multi-byte characters.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as fs from 'fs';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Check if a byte is a UTF-8 continuation byte.
|
|
10
|
+
* Continuation bytes have the pattern 10xxxxxx (0x80-0xBF).
|
|
11
|
+
*/
|
|
12
|
+
function isUtf8ContinuationByte(byte: number): boolean {
|
|
13
|
+
return (byte & 0xc0) === 0x80;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Determine the expected length of a UTF-8 sequence from its leading byte.
|
|
18
|
+
*/
|
|
19
|
+
function utf8SequenceLength(leadingByte: number): number {
|
|
20
|
+
if ((leadingByte & 0x80) === 0) return 1; // Single-byte: 0xxxxxxx
|
|
21
|
+
if ((leadingByte & 0xe0) === 0xc0) return 2; // Two-byte: 110xxxxx
|
|
22
|
+
if ((leadingByte & 0xf0) === 0xe0) return 3; // Three-byte: 1110xxxx
|
|
23
|
+
if ((leadingByte & 0xf8) === 0xf0) return 4; // Four-byte: 11110xxx
|
|
24
|
+
return 1; // Invalid; treat as single-byte
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Safely decode a buffer tail to UTF-8, avoiding partial UTF-8 sequences.
|
|
29
|
+
* If the buffer ends in the middle of a multi-byte UTF-8 sequence,
|
|
30
|
+
* the incomplete sequence is truncated.
|
|
31
|
+
*
|
|
32
|
+
* @param buffer Buffer to decode
|
|
33
|
+
* @returns UTF-8 string without incomplete sequences at the end
|
|
34
|
+
*/
|
|
35
|
+
export function decodeUtf8TailSafely(buffer: Buffer): string {
|
|
36
|
+
let end = buffer.length;
|
|
37
|
+
if (end > 0) {
|
|
38
|
+
let continuationCount = 0;
|
|
39
|
+
let candidateLead = end - 1;
|
|
40
|
+
|
|
41
|
+
// Count continuation bytes at the end
|
|
42
|
+
while (candidateLead >= 0 && isUtf8ContinuationByte(buffer[candidateLead])) {
|
|
43
|
+
continuationCount++;
|
|
44
|
+
candidateLead--;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (candidateLead < 0) {
|
|
48
|
+
// All bytes are continuation bytes; skip all
|
|
49
|
+
end = 0;
|
|
50
|
+
} else {
|
|
51
|
+
const sequenceLength = utf8SequenceLength(buffer[candidateLead]);
|
|
52
|
+
const expectedContinuationCount = sequenceLength - 1;
|
|
53
|
+
|
|
54
|
+
// If the continuation byte count doesn't match the expected count, truncate
|
|
55
|
+
if (sequenceLength > 1 && continuationCount !== expectedContinuationCount) {
|
|
56
|
+
end = candidateLead;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return buffer.subarray(0, end).toString('utf-8');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Extract the last N lines from a string.
|
|
66
|
+
*
|
|
67
|
+
* @param content String content to extract from
|
|
68
|
+
* @param maxLines Maximum number of lines to return
|
|
69
|
+
* @returns Last N lines of the content (or entire content if fewer lines)
|
|
70
|
+
*/
|
|
71
|
+
export function tailLogByLines(content: string, maxLines: number): string {
|
|
72
|
+
if (maxLines <= 0) {
|
|
73
|
+
return '';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const lines = content.split(/\r?\n/);
|
|
77
|
+
if (lines.length <= maxLines) {
|
|
78
|
+
return content;
|
|
79
|
+
}
|
|
80
|
+
return lines.slice(-maxLines).join('\n');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Read the tail bytes of a file into a buffer.
|
|
85
|
+
* Useful for reading last N bytes of a large file without loading entire file.
|
|
86
|
+
*
|
|
87
|
+
* @param logFile Path to the file
|
|
88
|
+
* @param size Total size of the file (from fs.statSync)
|
|
89
|
+
* @param maxSize Maximum bytes to read
|
|
90
|
+
* @returns Buffer containing the tail bytes
|
|
91
|
+
*/
|
|
92
|
+
export function readTailBytes(logFile: string, size: number, maxSize: number): Buffer {
|
|
93
|
+
const truncated = Buffer.alloc(maxSize);
|
|
94
|
+
const fd = fs.openSync(logFile, 'r');
|
|
95
|
+
try {
|
|
96
|
+
fs.readSync(fd, truncated, 0, maxSize, size - maxSize);
|
|
97
|
+
} finally {
|
|
98
|
+
fs.closeSync(fd);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return truncated;
|
|
102
|
+
}
|