@nathapp/nax 0.18.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/.gitlab-ci.yml +96 -0
- package/BRIEF.md +140 -0
- package/CHANGELOG.md +60 -0
- package/CLAUDE.md +159 -0
- package/README.md +373 -0
- package/US-007-IMPLEMENTATION.md +139 -0
- package/bin/nax.ts +930 -0
- package/biome.json +14 -0
- package/bun.lock +168 -0
- package/bunfig.toml +11 -0
- package/docs/20260216-fix-plan-context-review.md +56 -0
- package/docs/20260216-relentless-vs-ngent-comparison.md +208 -0
- package/docs/20260216-v02-plan.md +136 -0
- package/docs/20260216-v02-review.md +685 -0
- package/docs/20260217-dogfood-findings.md +56 -0
- package/docs/20260217-p2-plus-plan.md +117 -0
- package/docs/20260217-partial-fixes-plan.md +62 -0
- package/docs/20260217-plan-analyze-spec.md +117 -0
- package/docs/20260217-post-impl-review.md +1137 -0
- package/docs/20260217-quick-wins-plan.md +66 -0
- package/docs/20260217-split-runner-plan.md +75 -0
- package/docs/20260217-v03-impl-plan.md +80 -0
- package/docs/20260217-v03-post-impl-review.md +589 -0
- package/docs/20260217-v04-impl-plan.md +86 -0
- package/docs/20260217-v05-post-impl-review.md +850 -0
- package/docs/20260217-v06-post-impl-review.md +817 -0
- package/docs/20260218-adr003-port-plan.md +151 -0
- package/docs/20260218-review-adr003-verification.md +175 -0
- package/docs/20260219-fix-plan-bug16-19.md +79 -0
- package/docs/20260219-fix-plan-bug20-22.md +114 -0
- package/docs/20260219-plan-llm-routing.md +116 -0
- package/docs/20260219-review-bug20-22-fixes.md +135 -0
- package/docs/20260219-routing-baseline-keyword.md +63 -0
- package/docs/20260220-plan-structured-logging-p1.md +80 -0
- package/docs/20260220-plan-structured-logging-p2.md +37 -0
- package/docs/20260220-review-llm-routing.md +180 -0
- package/docs/20260220-review-post-fix-llm-routing.md +70 -0
- package/docs/20260221-fix-plan-relevantfiles-split.md +101 -0
- package/docs/20260221-fix-plan-routing-mode.md +125 -0
- package/docs/20260221-review-v0.9-implementation.md +379 -0
- package/docs/20260222-fix-plan-v091-routing-isolation.md +197 -0
- package/docs/20260223-fix-plan-prompt-audit.md +62 -0
- package/docs/20260224-nax-roadmap-phases.md +189 -0
- package/docs/20260225-phase2-llm-service-layer.md +401 -0
- package/docs/20260225-review-v0.10.1.md +187 -0
- package/docs/20260303-v010-implementation-plan.md +165 -0
- package/docs/CLAUDE.md.bak +191 -0
- package/docs/ROADMAP.md +165 -0
- package/docs/SPEC-rectification.md +0 -0
- package/docs/SPEC.md +324 -0
- package/docs/US-001-plugin-loading-verification.md +152 -0
- package/docs/architecture-analysis.md +1076 -0
- package/docs/bugs/BUG-21-escalation-null-attempts.md +48 -0
- package/docs/bugs-from-dogfood-run-c.md +243 -0
- package/docs/code-review-20260228.md +612 -0
- package/docs/code-review-v0.15.0.md +629 -0
- package/docs/hook-lifecycle-test-plan.md +149 -0
- package/docs/releases/v0.11.0-and-earlier.md +20 -0
- package/docs/releases/v0.12.0.md +15 -0
- package/docs/releases/v0.13.0.md +14 -0
- package/docs/releases/v0.14.0.md +20 -0
- package/docs/releases/v0.14.1.md +36 -0
- package/docs/releases/v0.14.2.md +51 -0
- package/docs/releases/v0.14.3.md +174 -0
- package/docs/releases/v0.14.4.md +94 -0
- package/docs/releases/v0.15.0.md +502 -0
- package/docs/releases/v0.15.1.md +170 -0
- package/docs/releases/v0.15.3.md +193 -0
- package/docs/specs/status-file-v0.10.1.md +812 -0
- package/docs/v0.10-global-config.md +206 -0
- package/docs/v0.10-plugin-system.md +415 -0
- package/docs/v0.10-prompt-optimizer.md +234 -0
- package/docs/v0.3-spec.md +244 -0
- package/docs/v0.4-spec.md +140 -0
- package/docs/v0.5-spec.md +237 -0
- package/docs/v0.6-spec.md +371 -0
- package/docs/v0.7-spec.md +177 -0
- package/docs/v0.8-llm-routing.md +206 -0
- package/docs/v0.8-structured-logging.md +132 -0
- package/docs/v0.9.3-prompt-audit.md +112 -0
- package/examples/plugins/console-reporter/index.test.ts +207 -0
- package/examples/plugins/console-reporter/index.ts +110 -0
- package/nax/config.json +147 -0
- package/nax/features/bugfix-v0171/prd.json +52 -0
- package/nax/features/config-management/prd.json +108 -0
- package/nax/features/config-management/progress.txt +5 -0
- package/nax/features/diagnose/acceptance.test.ts +412 -0
- package/nax/features/diagnose/prd.json +41 -0
- package/nax/features/orchestration-fixes/prd.json +89 -0
- package/nax/features/orchestration-fixes/progress.txt +1 -0
- package/nax/features/plugin-integration/US-007-VERIFICATION.md +259 -0
- package/nax/features/plugin-integration/prd.json +208 -0
- package/nax/features/plugin-integration/progress.txt +5 -0
- package/nax/features/precheck/prd.json +205 -0
- package/nax/features/precheck/progress.txt +15 -0
- package/nax/features/structured-logging/prd.json +199 -0
- package/nax/features/unlock/prd.json +36 -0
- package/package.json +47 -0
- package/src/acceptance/fix-generator.ts +348 -0
- package/src/acceptance/generator.ts +282 -0
- package/src/acceptance/index.ts +30 -0
- package/src/acceptance/types.ts +79 -0
- package/src/agents/claude-decompose.ts +169 -0
- package/src/agents/claude-plan.ts +139 -0
- package/src/agents/claude.ts +324 -0
- package/src/agents/cost.ts +268 -0
- package/src/agents/index.ts +13 -0
- package/src/agents/registry.ts +48 -0
- package/src/agents/types-extended.ts +133 -0
- package/src/agents/types.ts +113 -0
- package/src/agents/validation.ts +69 -0
- package/src/analyze/classifier.ts +305 -0
- package/src/analyze/index.ts +16 -0
- package/src/analyze/scanner.ts +175 -0
- package/src/analyze/types.ts +51 -0
- package/src/cli/accept.ts +108 -0
- package/src/cli/analyze-parser.ts +284 -0
- package/src/cli/analyze.ts +207 -0
- package/src/cli/config.ts +561 -0
- package/src/cli/constitution.ts +109 -0
- package/src/cli/diagnose-analysis.ts +159 -0
- package/src/cli/diagnose-formatter.ts +87 -0
- package/src/cli/diagnose.ts +203 -0
- package/src/cli/generate.ts +127 -0
- package/src/cli/index.ts +37 -0
- package/src/cli/init.ts +188 -0
- package/src/cli/interact.ts +295 -0
- package/src/cli/plan.ts +198 -0
- package/src/cli/plugins.ts +111 -0
- package/src/cli/prompts.ts +295 -0
- package/src/cli/runs.ts +174 -0
- package/src/cli/status-cost.ts +151 -0
- package/src/cli/status-features.ts +338 -0
- package/src/cli/status.ts +13 -0
- package/src/commands/common.ts +171 -0
- package/src/commands/diagnose.ts +17 -0
- package/src/commands/index.ts +8 -0
- package/src/commands/logs.ts +384 -0
- package/src/commands/precheck.ts +86 -0
- package/src/commands/unlock.ts +96 -0
- package/src/config/defaults.ts +160 -0
- package/src/config/index.ts +22 -0
- package/src/config/loader.ts +121 -0
- package/src/config/merger.ts +147 -0
- package/src/config/path-security.ts +121 -0
- package/src/config/paths.ts +27 -0
- package/src/config/schema.ts +56 -0
- package/src/config/schemas.ts +286 -0
- package/src/config/types.ts +423 -0
- package/src/config/validate.ts +103 -0
- package/src/constitution/generator.ts +191 -0
- package/src/constitution/generators/aider.ts +41 -0
- package/src/constitution/generators/claude.ts +35 -0
- package/src/constitution/generators/cursor.ts +36 -0
- package/src/constitution/generators/opencode.ts +38 -0
- package/src/constitution/generators/types.ts +33 -0
- package/src/constitution/generators/windsurf.ts +36 -0
- package/src/constitution/index.ts +10 -0
- package/src/constitution/loader.ts +133 -0
- package/src/constitution/types.ts +31 -0
- package/src/context/auto-detect.ts +227 -0
- package/src/context/builder.ts +246 -0
- package/src/context/elements.ts +83 -0
- package/src/context/formatter.ts +107 -0
- package/src/context/generator.ts +129 -0
- package/src/context/generators/aider.ts +34 -0
- package/src/context/generators/claude.ts +28 -0
- package/src/context/generators/cursor.ts +28 -0
- package/src/context/generators/opencode.ts +30 -0
- package/src/context/generators/windsurf.ts +28 -0
- package/src/context/greenfield.ts +114 -0
- package/src/context/index.ts +33 -0
- package/src/context/injector.ts +279 -0
- package/src/context/test-scanner.ts +370 -0
- package/src/context/types.ts +98 -0
- package/src/errors.ts +67 -0
- package/src/execution/batching.ts +157 -0
- package/src/execution/crash-recovery.ts +373 -0
- package/src/execution/escalation/escalation.ts +44 -0
- package/src/execution/escalation/index.ts +13 -0
- package/src/execution/escalation/tier-escalation.ts +295 -0
- package/src/execution/escalation/tier-outcome.ts +158 -0
- package/src/execution/helpers.ts +38 -0
- package/src/execution/index.ts +45 -0
- package/src/execution/lifecycle/acceptance-loop.ts +272 -0
- package/src/execution/lifecycle/headless-formatter.ts +85 -0
- package/src/execution/lifecycle/index.ts +12 -0
- package/src/execution/lifecycle/parallel-lifecycle.ts +101 -0
- package/src/execution/lifecycle/precheck-runner.ts +140 -0
- package/src/execution/lifecycle/run-cleanup.ts +81 -0
- package/src/execution/lifecycle/run-completion.ts +129 -0
- package/src/execution/lifecycle/run-initialization.ts +141 -0
- package/src/execution/lifecycle/run-lifecycle.ts +312 -0
- package/src/execution/lifecycle/run-setup.ts +204 -0
- package/src/execution/lifecycle/story-hooks.ts +38 -0
- package/src/execution/lifecycle/story-size-prompts.ts +123 -0
- package/src/execution/lock.ts +115 -0
- package/src/execution/parallel-executor.ts +216 -0
- package/src/execution/parallel.ts +400 -0
- package/src/execution/pid-registry.ts +280 -0
- package/src/execution/pipeline-result-handler.ts +388 -0
- package/src/execution/post-verify-rectification.ts +188 -0
- package/src/execution/post-verify.ts +274 -0
- package/src/execution/progress.ts +25 -0
- package/src/execution/prompts.ts +127 -0
- package/src/execution/queue-handler.ts +109 -0
- package/src/execution/rectification.ts +13 -0
- package/src/execution/runner.ts +377 -0
- package/src/execution/sequential-executor.ts +388 -0
- package/src/execution/status-file.ts +264 -0
- package/src/execution/status-writer.ts +139 -0
- package/src/execution/story-context.ts +229 -0
- package/src/execution/test-output-parser.ts +14 -0
- package/src/execution/verification.ts +72 -0
- package/src/hooks/index.ts +2 -0
- package/src/hooks/runner.ts +286 -0
- package/src/hooks/types.ts +67 -0
- package/src/interaction/chain.ts +154 -0
- package/src/interaction/index.ts +60 -0
- package/src/interaction/init.ts +83 -0
- package/src/interaction/plugins/auto.ts +217 -0
- package/src/interaction/plugins/cli.ts +300 -0
- package/src/interaction/plugins/telegram.ts +384 -0
- package/src/interaction/plugins/webhook.ts +258 -0
- package/src/interaction/state.ts +171 -0
- package/src/interaction/triggers.ts +229 -0
- package/src/interaction/types.ts +163 -0
- package/src/logger/formatters.ts +84 -0
- package/src/logger/index.ts +16 -0
- package/src/logger/logger.ts +298 -0
- package/src/logger/types.ts +48 -0
- package/src/logging/formatter.ts +355 -0
- package/src/logging/index.ts +22 -0
- package/src/logging/types.ts +93 -0
- package/src/metrics/aggregator.ts +190 -0
- package/src/metrics/index.ts +14 -0
- package/src/metrics/tracker.ts +200 -0
- package/src/metrics/types.ts +109 -0
- package/src/optimizer/index.ts +62 -0
- package/src/optimizer/noop.optimizer.ts +24 -0
- package/src/optimizer/rule-based.optimizer.ts +248 -0
- package/src/optimizer/types.ts +53 -0
- package/src/pipeline/events.ts +130 -0
- package/src/pipeline/index.ts +19 -0
- package/src/pipeline/runner.ts +161 -0
- package/src/pipeline/stages/acceptance.ts +197 -0
- package/src/pipeline/stages/completion.ts +99 -0
- package/src/pipeline/stages/constitution.ts +63 -0
- package/src/pipeline/stages/context.ts +117 -0
- package/src/pipeline/stages/execution.ts +194 -0
- package/src/pipeline/stages/index.ts +62 -0
- package/src/pipeline/stages/optimizer.ts +74 -0
- package/src/pipeline/stages/prompt.ts +57 -0
- package/src/pipeline/stages/queue-check.ts +103 -0
- package/src/pipeline/stages/review.ts +181 -0
- package/src/pipeline/stages/routing.ts +81 -0
- package/src/pipeline/stages/verify.ts +100 -0
- package/src/pipeline/types.ts +167 -0
- package/src/plugins/index.ts +31 -0
- package/src/plugins/loader.ts +287 -0
- package/src/plugins/registry.ts +168 -0
- package/src/plugins/types.ts +327 -0
- package/src/plugins/validator.ts +352 -0
- package/src/prd/index.ts +172 -0
- package/src/prd/types.ts +202 -0
- package/src/precheck/checks-blockers.ts +391 -0
- package/src/precheck/checks-warnings.ts +142 -0
- package/src/precheck/checks.ts +30 -0
- package/src/precheck/index.ts +247 -0
- package/src/precheck/story-size-gate.ts +144 -0
- package/src/precheck/types.ts +31 -0
- package/src/queue/index.ts +2 -0
- package/src/queue/manager.ts +254 -0
- package/src/queue/types.ts +54 -0
- package/src/review/index.ts +8 -0
- package/src/review/runner.ts +172 -0
- package/src/review/types.ts +66 -0
- package/src/routing/builder.ts +81 -0
- package/src/routing/chain.ts +74 -0
- package/src/routing/index.ts +16 -0
- package/src/routing/loader.ts +58 -0
- package/src/routing/router.ts +303 -0
- package/src/routing/strategies/adaptive.ts +215 -0
- package/src/routing/strategies/index.ts +8 -0
- package/src/routing/strategies/keyword.ts +163 -0
- package/src/routing/strategies/llm-prompts.ts +209 -0
- package/src/routing/strategies/llm.ts +235 -0
- package/src/routing/strategies/manual.ts +50 -0
- package/src/routing/strategy.ts +99 -0
- package/src/tdd/cleanup.ts +111 -0
- package/src/tdd/index.ts +23 -0
- package/src/tdd/isolation.ts +123 -0
- package/src/tdd/orchestrator.ts +383 -0
- package/src/tdd/prompts.ts +270 -0
- package/src/tdd/rectification-gate.ts +183 -0
- package/src/tdd/session-runner.ts +179 -0
- package/src/tdd/types.ts +81 -0
- package/src/tdd/verdict.ts +271 -0
- package/src/tui/App.tsx +265 -0
- package/src/tui/components/AgentPanel.tsx +75 -0
- package/src/tui/components/CostOverlay.tsx +118 -0
- package/src/tui/components/HelpOverlay.tsx +107 -0
- package/src/tui/components/StatusBar.tsx +63 -0
- package/src/tui/components/StoriesPanel.tsx +177 -0
- package/src/tui/hooks/useKeyboard.ts +142 -0
- package/src/tui/hooks/useLayout.ts +137 -0
- package/src/tui/hooks/usePipelineEvents.ts +183 -0
- package/src/tui/hooks/usePty.ts +194 -0
- package/src/tui/index.tsx +38 -0
- package/src/tui/types.ts +76 -0
- package/src/utils/git.ts +83 -0
- package/src/utils/queue-writer.ts +54 -0
- package/src/verification/executor.ts +235 -0
- package/src/verification/gate.ts +207 -0
- package/src/verification/index.ts +12 -0
- package/src/verification/parser.ts +230 -0
- package/src/verification/rectification.ts +108 -0
- package/src/verification/types.ts +113 -0
- package/src/worktree/dispatcher.ts +65 -0
- package/src/worktree/index.ts +2 -0
- package/src/worktree/manager.ts +187 -0
- package/src/worktree/merge.ts +301 -0
- package/src/worktree/types.ts +4 -0
- package/test/TEST_COVERAGE_US001.md +217 -0
- package/test/TEST_COVERAGE_US003.md +84 -0
- package/test/TEST_COVERAGE_US005.md +86 -0
- package/test/US-002-orchestrator.test.ts +246 -0
- package/test/acceptance/cm-003-default-view.test.ts +194 -0
- package/test/execution/pid-registry.test.ts +240 -0
- package/test/execution/post-verify.test.ts +224 -0
- package/test/helpers/timeout.ts +42 -0
- package/test/integration/US-002-TEST-SUMMARY.md +107 -0
- package/test/integration/US-003-TEST-SUMMARY.md +149 -0
- package/test/integration/US-004-TEST-SUMMARY.md +106 -0
- package/test/integration/US-005-TEST-SUMMARY.md +138 -0
- package/test/integration/US-007-TEST-SUMMARY.md +100 -0
- package/test/integration/agent-validation.test.ts +439 -0
- package/test/integration/analyze-integration.test.ts +261 -0
- package/test/integration/analyze-scanner.test.ts +131 -0
- package/test/integration/cli-config-default-edge-cases.test.ts +222 -0
- package/test/integration/cli-config-default-view.test.ts +229 -0
- package/test/integration/cli-config-diff.test.ts +460 -0
- package/test/integration/cli-config.test.ts +736 -0
- package/test/integration/cli-diagnose.test.ts +592 -0
- package/test/integration/cli-logs.test.ts +314 -0
- package/test/integration/cli-plugins.test.ts +678 -0
- package/test/integration/cli-precheck.test.ts +371 -0
- package/test/integration/cli-run-headless.test.ts +173 -0
- package/test/integration/cli.test.ts +75 -0
- package/test/integration/config/merger.test.ts +465 -0
- package/test/integration/config/paths.test.ts +51 -0
- package/test/integration/config-loader.test.ts +265 -0
- package/test/integration/config.test.ts +444 -0
- package/test/integration/context-integration.test.ts +702 -0
- package/test/integration/context-provider-injection.test.ts +506 -0
- package/test/integration/context-verification-integration.test.ts +295 -0
- package/test/integration/e2e.test.ts +896 -0
- package/test/integration/execution.test.ts +625 -0
- package/test/integration/helpers.test.ts +295 -0
- package/test/integration/hooks.test.ts +361 -0
- package/test/integration/interaction-chain-pipeline.test.ts +464 -0
- package/test/integration/isolation.test.ts +143 -0
- package/test/integration/logger.test.ts +461 -0
- package/test/integration/parallel.test.ts +250 -0
- package/test/integration/path-security.test.ts +173 -0
- package/test/integration/pipeline-acceptance.test.ts +302 -0
- package/test/integration/pipeline-events.test.ts +475 -0
- package/test/integration/pipeline.test.ts +658 -0
- package/test/integration/plan.test.ts +157 -0
- package/test/integration/plugin-routing.test.ts +921 -0
- package/test/integration/plugins/config-integration.test.ts +172 -0
- package/test/integration/plugins/config-resolution.test.ts +522 -0
- package/test/integration/plugins/loader.test.ts +641 -0
- package/test/integration/plugins/registry.test.ts +746 -0
- package/test/integration/plugins/validator.test.ts +563 -0
- package/test/integration/prd-pause.test.ts +205 -0
- package/test/integration/prd-resolvers.test.ts +185 -0
- package/test/integration/precheck-integration.test.ts +468 -0
- package/test/integration/precheck.test.ts +805 -0
- package/test/integration/progress.test.ts +34 -0
- package/test/integration/rectification-flow.test.ts +512 -0
- package/test/integration/reporter-lifecycle.test.ts +860 -0
- package/test/integration/review-config-commands.test.ts +319 -0
- package/test/integration/review-config-schema.test.ts +116 -0
- package/test/integration/review-plugin-integration.test.ts +722 -0
- package/test/integration/review.test.ts +149 -0
- package/test/integration/routing-stage-bug-021.test.ts +274 -0
- package/test/integration/routing-stage-greenfield.test.ts +286 -0
- package/test/integration/runner-config-plugins.test.ts +461 -0
- package/test/integration/runner-fixes.test.ts +399 -0
- package/test/integration/runner-plugin-integration.test.ts +543 -0
- package/test/integration/runner.test.ts +1679 -0
- package/test/integration/s5-greenfield-fallback.test.ts +297 -0
- package/test/integration/status-file-integration.test.ts +325 -0
- package/test/integration/status-file.test.ts +379 -0
- package/test/integration/status-writer.test.ts +345 -0
- package/test/integration/story-id-in-events.test.ts +273 -0
- package/test/integration/tdd-cleanup.test.ts +246 -0
- package/test/integration/tdd-orchestrator.test.ts +1762 -0
- package/test/integration/test-scanner.test.ts +403 -0
- package/test/integration/verification-asset-check.test.ts +142 -0
- package/test/integration/verify-stage.test.ts +275 -0
- package/test/integration/worktree/manager.test.ts +218 -0
- package/test/integration/worktree/merge.test.ts +341 -0
- package/test/manual/logging-formatter-demo.ts +158 -0
- package/test/ui/tui-agent-panel.test.tsx +99 -0
- package/test/ui/tui-controls.test.ts +334 -0
- package/test/ui/tui-cost-and-pty.test.ts +189 -0
- package/test/ui/tui-layout.test.ts +378 -0
- package/test/ui/tui-pty-integration.test.tsx +159 -0
- package/test/ui/tui-stories.test.ts +332 -0
- package/test/unit/acceptance.test.ts +186 -0
- package/test/unit/agent-stderr-capture.test.ts +146 -0
- package/test/unit/analyze-classifier.test.ts +215 -0
- package/test/unit/analyze.test.ts +224 -0
- package/test/unit/auto-detect.test.ts +249 -0
- package/test/unit/cli-status.test.ts +417 -0
- package/test/unit/commands/common.test.ts +320 -0
- package/test/unit/commands/logs.test.ts +416 -0
- package/test/unit/commands/unlock.test.ts +319 -0
- package/test/unit/constitution-generators.test.ts +160 -0
- package/test/unit/constitution.test.ts +209 -0
- package/test/unit/context.test.ts +1722 -0
- package/test/unit/cost.test.ts +231 -0
- package/test/unit/crash-recovery.test.ts +308 -0
- package/test/unit/escalation.test.ts +126 -0
- package/test/unit/execution-logging-stderr.test.ts +156 -0
- package/test/unit/execution-stage.test.ts +122 -0
- package/test/unit/fix-generator.test.ts +275 -0
- package/test/unit/formatters.test.ts +469 -0
- package/test/unit/greenfield.test.ts +179 -0
- package/test/unit/helpers.test.ts +317 -0
- package/test/unit/interaction/human-review-trigger.test.ts +164 -0
- package/test/unit/interaction-network-failures.test.ts +389 -0
- package/test/unit/interaction-plugins.test.ts +164 -0
- package/test/unit/isolation.test.ts +134 -0
- package/test/unit/logging/formatter.test.ts +455 -0
- package/test/unit/merge.test.ts +268 -0
- package/test/unit/metrics.test.ts +276 -0
- package/test/unit/optimizer/noop.optimizer.test.ts +125 -0
- package/test/unit/optimizer/rule-based.optimizer.test.ts +358 -0
- package/test/unit/prd-auto-default.test.ts +290 -0
- package/test/unit/prd-failure-category.test.ts +176 -0
- package/test/unit/prd-get-next-story.test.ts +186 -0
- package/test/unit/precheck-checks.test.ts +840 -0
- package/test/unit/precheck-story-size-gate.test.ts +287 -0
- package/test/unit/precheck-types.test.ts +142 -0
- package/test/unit/prompts.test.ts +475 -0
- package/test/unit/queue.test.ts +237 -0
- package/test/unit/rectification.test.ts +284 -0
- package/test/unit/registry.test.ts +287 -0
- package/test/unit/routing.test.ts +937 -0
- package/test/unit/run-lifecycle.test.ts +140 -0
- package/test/unit/storyid-events.test.ts +224 -0
- package/test/unit/tdd-verdict.test.ts +492 -0
- package/test/unit/test-output-parser.test.ts +377 -0
- package/test/unit/verdict.test.ts +324 -0
- package/test/unit/worktree-manager.test.ts +158 -0
- package/tsconfig.json +27 -0
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logs command implementation
|
|
3
|
+
*
|
|
4
|
+
* Displays run logs with filtering, follow mode, and multiple output formats.
|
|
5
|
+
* Uses resolveProject() for directory resolution and formatter for output.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import chalk from "chalk";
|
|
11
|
+
import type { LogEntry, LogLevel } from "../logger/types";
|
|
12
|
+
import { type FormattedEntry, formatLogEntry, formatRunSummary } from "../logging/formatter";
|
|
13
|
+
import type { RunSummary, VerbosityMode } from "../logging/types";
|
|
14
|
+
import { type ResolveProjectOptions, resolveProject } from "./common";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Options for logs command
|
|
18
|
+
*/
|
|
19
|
+
export interface LogsOptions {
|
|
20
|
+
/** Explicit project directory (from -d flag) */
|
|
21
|
+
dir?: string;
|
|
22
|
+
/** Follow mode - stream new entries real-time (from --follow / -f flag) */
|
|
23
|
+
follow?: boolean;
|
|
24
|
+
/** Filter to specific story (from --story / -s flag) */
|
|
25
|
+
story?: string;
|
|
26
|
+
/** Filter by log level (from --level flag) */
|
|
27
|
+
level?: LogLevel;
|
|
28
|
+
/** List all runs in table format (from --list / -l flag) */
|
|
29
|
+
list?: boolean;
|
|
30
|
+
/** Select specific run by timestamp (from --run / -r flag) */
|
|
31
|
+
run?: string;
|
|
32
|
+
/** Output raw JSONL (from --json / -j flag) */
|
|
33
|
+
json?: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Log level hierarchy for filtering
|
|
38
|
+
*/
|
|
39
|
+
const LOG_LEVEL_PRIORITY: Record<LogLevel, number> = {
|
|
40
|
+
debug: 0,
|
|
41
|
+
info: 1,
|
|
42
|
+
warn: 2,
|
|
43
|
+
error: 3,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Display logs with filtering and formatting
|
|
48
|
+
*
|
|
49
|
+
* @param options - Command options
|
|
50
|
+
*/
|
|
51
|
+
export async function logsCommand(options: LogsOptions): Promise<void> {
|
|
52
|
+
// Resolve project directory
|
|
53
|
+
const resolved = resolveProject({ dir: options.dir });
|
|
54
|
+
const naxDir = join(resolved.projectDir, "nax");
|
|
55
|
+
|
|
56
|
+
// Read config to get feature name
|
|
57
|
+
const configPath = resolved.configPath;
|
|
58
|
+
const configFile = Bun.file(configPath);
|
|
59
|
+
const config = await configFile.json();
|
|
60
|
+
const featureName = config.feature;
|
|
61
|
+
|
|
62
|
+
if (!featureName) {
|
|
63
|
+
throw new Error("No feature specified in config.json");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const featureDir = join(naxDir, "features", featureName);
|
|
67
|
+
const runsDir = join(featureDir, "runs");
|
|
68
|
+
|
|
69
|
+
// Validate runs directory exists
|
|
70
|
+
if (!existsSync(runsDir)) {
|
|
71
|
+
throw new Error(`No runs directory found for feature: ${featureName}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Handle --list mode (show runs table)
|
|
75
|
+
if (options.list) {
|
|
76
|
+
await displayRunsList(runsDir);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Determine which run to display
|
|
81
|
+
const runFile = await selectRunFile(runsDir, options.run);
|
|
82
|
+
|
|
83
|
+
if (!runFile) {
|
|
84
|
+
throw new Error("No runs found for this feature");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Handle follow mode
|
|
88
|
+
if (options.follow) {
|
|
89
|
+
await followLogs(runFile, options);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Display static logs
|
|
94
|
+
await displayLogs(runFile, options);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Select which run file to display
|
|
99
|
+
*/
|
|
100
|
+
async function selectRunFile(runsDir: string, runTimestamp?: string): Promise<string | null> {
|
|
101
|
+
const files = readdirSync(runsDir)
|
|
102
|
+
.filter((f) => f.endsWith(".jsonl") && f !== "latest.jsonl")
|
|
103
|
+
.sort()
|
|
104
|
+
.reverse();
|
|
105
|
+
|
|
106
|
+
if (files.length === 0) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// If no specific run requested, use latest
|
|
111
|
+
if (!runTimestamp) {
|
|
112
|
+
return join(runsDir, files[0]);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Find matching run by partial timestamp
|
|
116
|
+
const matchingFile = files.find((f) => f.startsWith(runTimestamp));
|
|
117
|
+
|
|
118
|
+
if (!matchingFile) {
|
|
119
|
+
throw new Error(`Run not found: ${runTimestamp}`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return join(runsDir, matchingFile);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Display runs table
|
|
127
|
+
*/
|
|
128
|
+
async function displayRunsList(runsDir: string): Promise<void> {
|
|
129
|
+
const files = readdirSync(runsDir)
|
|
130
|
+
.filter((f) => f.endsWith(".jsonl") && f !== "latest.jsonl")
|
|
131
|
+
.sort()
|
|
132
|
+
.reverse();
|
|
133
|
+
|
|
134
|
+
if (files.length === 0) {
|
|
135
|
+
console.log(chalk.dim("No runs found"));
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
console.log(chalk.bold("\nRuns:\n"));
|
|
140
|
+
console.log(chalk.gray(" Timestamp Stories Duration Cost Status"));
|
|
141
|
+
console.log(chalk.gray(" ─────────────────────────────────────────────────────────"));
|
|
142
|
+
|
|
143
|
+
for (const file of files) {
|
|
144
|
+
const filePath = join(runsDir, file);
|
|
145
|
+
const summary = await extractRunSummary(filePath);
|
|
146
|
+
|
|
147
|
+
const timestamp = file.replace(".jsonl", "");
|
|
148
|
+
const stories = summary ? `${summary.passed}/${summary.total}` : "?/?";
|
|
149
|
+
const duration = summary ? formatDuration(summary.durationMs) : "?";
|
|
150
|
+
const cost = summary ? `$${summary.totalCost.toFixed(4)}` : "$?.????";
|
|
151
|
+
const status = summary ? (summary.failed === 0 ? chalk.green("✓") : chalk.red("✗")) : "?";
|
|
152
|
+
|
|
153
|
+
console.log(` ${timestamp} ${stories.padEnd(7)} ${duration.padEnd(8)} ${cost.padEnd(8)} ${status}`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
console.log();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Extract run summary from log file
|
|
161
|
+
*/
|
|
162
|
+
async function extractRunSummary(filePath: string): Promise<RunSummary | null> {
|
|
163
|
+
const file = Bun.file(filePath);
|
|
164
|
+
const content = await file.text();
|
|
165
|
+
const lines = content.trim().split("\n");
|
|
166
|
+
|
|
167
|
+
let total = 0;
|
|
168
|
+
let passed = 0;
|
|
169
|
+
let failed = 0;
|
|
170
|
+
let skipped = 0;
|
|
171
|
+
let totalCost = 0;
|
|
172
|
+
let startedAt = "";
|
|
173
|
+
let completedAt: string | undefined;
|
|
174
|
+
let firstTimestamp = "";
|
|
175
|
+
let lastTimestamp = "";
|
|
176
|
+
|
|
177
|
+
for (const line of lines) {
|
|
178
|
+
if (!line.trim()) continue;
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
const entry: LogEntry = JSON.parse(line);
|
|
182
|
+
|
|
183
|
+
if (!firstTimestamp) {
|
|
184
|
+
firstTimestamp = entry.timestamp;
|
|
185
|
+
}
|
|
186
|
+
lastTimestamp = entry.timestamp;
|
|
187
|
+
|
|
188
|
+
if (entry.stage === "run.start") {
|
|
189
|
+
startedAt = entry.timestamp;
|
|
190
|
+
const runData = entry.data as Record<string, unknown>;
|
|
191
|
+
total = typeof runData?.totalStories === "number" ? runData.totalStories : 0;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (entry.stage === "story.complete" || entry.stage === "agent.complete") {
|
|
195
|
+
const data = entry.data as Record<string, unknown>;
|
|
196
|
+
const success = data?.success ?? true;
|
|
197
|
+
const action = data?.finalAction || data?.action;
|
|
198
|
+
|
|
199
|
+
if (success) {
|
|
200
|
+
passed++;
|
|
201
|
+
} else if (action === "skip") {
|
|
202
|
+
skipped++;
|
|
203
|
+
} else {
|
|
204
|
+
failed++;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (data?.cost && typeof data.cost === "number") {
|
|
208
|
+
totalCost += data.cost;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (entry.stage === "run.end") {
|
|
213
|
+
completedAt = entry.timestamp;
|
|
214
|
+
}
|
|
215
|
+
} catch {
|
|
216
|
+
// Skip invalid JSON lines
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (!startedAt) {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const durationMs = lastTimestamp ? new Date(lastTimestamp).getTime() - new Date(firstTimestamp).getTime() : 0;
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
total,
|
|
228
|
+
passed,
|
|
229
|
+
failed,
|
|
230
|
+
skipped,
|
|
231
|
+
durationMs,
|
|
232
|
+
totalCost,
|
|
233
|
+
startedAt,
|
|
234
|
+
completedAt,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Display static logs
|
|
240
|
+
*/
|
|
241
|
+
async function displayLogs(filePath: string, options: LogsOptions): Promise<void> {
|
|
242
|
+
const file = Bun.file(filePath);
|
|
243
|
+
const content = await file.text();
|
|
244
|
+
const lines = content.trim().split("\n");
|
|
245
|
+
|
|
246
|
+
const mode: VerbosityMode = options.json ? "json" : "normal";
|
|
247
|
+
|
|
248
|
+
for (const line of lines) {
|
|
249
|
+
if (!line.trim()) continue;
|
|
250
|
+
|
|
251
|
+
try {
|
|
252
|
+
const entry: LogEntry = JSON.parse(line);
|
|
253
|
+
|
|
254
|
+
// Apply filters
|
|
255
|
+
if (!shouldDisplayEntry(entry, options)) {
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Format and display
|
|
260
|
+
const formatted = formatLogEntry(entry, { mode, useColor: true });
|
|
261
|
+
|
|
262
|
+
if (formatted.shouldDisplay && formatted.output) {
|
|
263
|
+
console.log(formatted.output);
|
|
264
|
+
}
|
|
265
|
+
} catch {
|
|
266
|
+
// Skip invalid JSON lines
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Display summary footer (unless in json mode)
|
|
271
|
+
if (!options.json) {
|
|
272
|
+
const summary = await extractRunSummary(filePath);
|
|
273
|
+
if (summary) {
|
|
274
|
+
console.log(formatRunSummary(summary, { mode: "normal", useColor: true }));
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Follow logs in real-time (tail -f mode)
|
|
281
|
+
*/
|
|
282
|
+
async function followLogs(filePath: string, options: LogsOptions): Promise<void> {
|
|
283
|
+
const mode: VerbosityMode = options.json ? "json" : "normal";
|
|
284
|
+
|
|
285
|
+
// Display existing logs first
|
|
286
|
+
const file = Bun.file(filePath);
|
|
287
|
+
const content = await file.text();
|
|
288
|
+
const lines = content.trim().split("\n");
|
|
289
|
+
|
|
290
|
+
for (const line of lines) {
|
|
291
|
+
if (!line.trim()) continue;
|
|
292
|
+
|
|
293
|
+
try {
|
|
294
|
+
const entry: LogEntry = JSON.parse(line);
|
|
295
|
+
|
|
296
|
+
if (!shouldDisplayEntry(entry, options)) {
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const formatted = formatLogEntry(entry, { mode, useColor: true });
|
|
301
|
+
|
|
302
|
+
if (formatted.shouldDisplay && formatted.output) {
|
|
303
|
+
console.log(formatted.output);
|
|
304
|
+
}
|
|
305
|
+
} catch {
|
|
306
|
+
// Skip invalid JSON lines
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Now watch for new lines
|
|
311
|
+
let lastSize = (await Bun.file(filePath).stat()).size;
|
|
312
|
+
|
|
313
|
+
while (true) {
|
|
314
|
+
await Bun.sleep(500);
|
|
315
|
+
|
|
316
|
+
const currentSize = (await Bun.file(filePath).stat()).size;
|
|
317
|
+
|
|
318
|
+
if (currentSize > lastSize) {
|
|
319
|
+
// File has grown, read new content
|
|
320
|
+
const newFile = Bun.file(filePath);
|
|
321
|
+
const newContent = await newFile.text();
|
|
322
|
+
const newLines = newContent.slice(lastSize).trim().split("\n");
|
|
323
|
+
|
|
324
|
+
for (const line of newLines) {
|
|
325
|
+
if (!line.trim()) continue;
|
|
326
|
+
|
|
327
|
+
try {
|
|
328
|
+
const entry: LogEntry = JSON.parse(line);
|
|
329
|
+
|
|
330
|
+
if (!shouldDisplayEntry(entry, options)) {
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const formatted = formatLogEntry(entry, { mode, useColor: true });
|
|
335
|
+
|
|
336
|
+
if (formatted.shouldDisplay && formatted.output) {
|
|
337
|
+
console.log(formatted.output);
|
|
338
|
+
}
|
|
339
|
+
} catch {
|
|
340
|
+
// Skip invalid JSON lines
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
lastSize = currentSize;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Check if entry should be displayed based on filters
|
|
351
|
+
*/
|
|
352
|
+
function shouldDisplayEntry(entry: LogEntry, options: LogsOptions): boolean {
|
|
353
|
+
// Story filter
|
|
354
|
+
if (options.story && entry.storyId !== options.story) {
|
|
355
|
+
return false;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Level filter
|
|
359
|
+
if (options.level) {
|
|
360
|
+
const entryPriority = LOG_LEVEL_PRIORITY[entry.level];
|
|
361
|
+
const filterPriority = LOG_LEVEL_PRIORITY[options.level];
|
|
362
|
+
|
|
363
|
+
if (entryPriority < filterPriority) {
|
|
364
|
+
return false;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return true;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Format duration in milliseconds
|
|
373
|
+
*/
|
|
374
|
+
function formatDuration(ms: number): string {
|
|
375
|
+
if (ms < 1000) {
|
|
376
|
+
return `${ms}ms`;
|
|
377
|
+
}
|
|
378
|
+
if (ms < 60000) {
|
|
379
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
380
|
+
}
|
|
381
|
+
const minutes = Math.floor(ms / 60000);
|
|
382
|
+
const seconds = Math.floor((ms % 60000) / 1000);
|
|
383
|
+
return `${minutes}m${seconds}s`;
|
|
384
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Precheck command implementation
|
|
3
|
+
*
|
|
4
|
+
* Runs precheck validations and displays results in human or JSON format.
|
|
5
|
+
* Uses resolveProject() for directory resolution.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync } from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import chalk from "chalk";
|
|
11
|
+
import { loadConfig } from "../config";
|
|
12
|
+
import { loadPRD } from "../prd";
|
|
13
|
+
import { EXIT_CODES, runPrecheck } from "../precheck";
|
|
14
|
+
import { resolveProject } from "./common";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Options for precheck command
|
|
18
|
+
*/
|
|
19
|
+
export interface PrecheckOptions {
|
|
20
|
+
/** Feature name (from -f flag) */
|
|
21
|
+
feature?: string;
|
|
22
|
+
/** Explicit project directory (from -d flag) */
|
|
23
|
+
dir?: string;
|
|
24
|
+
/** Output JSON format (from --json flag) */
|
|
25
|
+
json?: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Run precheck command
|
|
30
|
+
*
|
|
31
|
+
* Validates feature readiness before execution.
|
|
32
|
+
* Exits with code 0 (pass), 1 (blocker), or 2 (invalid PRD).
|
|
33
|
+
*/
|
|
34
|
+
export async function precheckCommand(options: PrecheckOptions): Promise<void> {
|
|
35
|
+
// Resolve project directory and feature
|
|
36
|
+
const resolved = resolveProject({
|
|
37
|
+
dir: options.dir,
|
|
38
|
+
feature: options.feature,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// Determine feature name (from flag or config)
|
|
42
|
+
let featureName = options.feature;
|
|
43
|
+
if (!featureName) {
|
|
44
|
+
// Read from config.json
|
|
45
|
+
const configFile = Bun.file(resolved.configPath);
|
|
46
|
+
const config = await configFile.json();
|
|
47
|
+
featureName = config.feature;
|
|
48
|
+
|
|
49
|
+
if (!featureName) {
|
|
50
|
+
console.error(chalk.red("No feature specified. Use -f flag or set feature in config.json"));
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Get feature directory
|
|
56
|
+
const naxDir = join(resolved.projectDir, "nax");
|
|
57
|
+
const featureDir = join(naxDir, "features", featureName);
|
|
58
|
+
const prdPath = join(featureDir, "prd.json");
|
|
59
|
+
|
|
60
|
+
// Validate feature directory exists
|
|
61
|
+
if (!existsSync(featureDir)) {
|
|
62
|
+
console.error(chalk.red(`Feature not found: ${featureName}`));
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Validate prd.json exists
|
|
67
|
+
if (!existsSync(prdPath)) {
|
|
68
|
+
console.error(chalk.red(`Missing prd.json for feature: ${featureName}`));
|
|
69
|
+
console.error(chalk.dim(`Run: nax analyze -f ${featureName}`));
|
|
70
|
+
process.exit(EXIT_CODES.INVALID_PRD);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Load config and PRD
|
|
74
|
+
const config = await loadConfig(resolved.projectDir);
|
|
75
|
+
const prd = await loadPRD(prdPath);
|
|
76
|
+
|
|
77
|
+
// Run precheck
|
|
78
|
+
const format = options.json ? "json" : "human";
|
|
79
|
+
const result = await runPrecheck(config, prd, {
|
|
80
|
+
workdir: resolved.projectDir,
|
|
81
|
+
format,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Exit with appropriate code
|
|
85
|
+
process.exit(result.exitCode);
|
|
86
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unlock command implementation
|
|
3
|
+
*
|
|
4
|
+
* Releases stale locks from crashed nax processes.
|
|
5
|
+
* Checks if lock-holding process is still alive before removing.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import chalk from "chalk";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Options for unlock command
|
|
13
|
+
*/
|
|
14
|
+
export interface UnlockOptions {
|
|
15
|
+
/** Explicit project directory (from -d flag) */
|
|
16
|
+
dir?: string;
|
|
17
|
+
/** Force unlock without liveness check (from --force flag) */
|
|
18
|
+
force?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Check if a process with given PID is still alive
|
|
23
|
+
*/
|
|
24
|
+
function isProcessAlive(pid: number): boolean {
|
|
25
|
+
try {
|
|
26
|
+
// kill(pid, 0) checks if process exists without actually sending a signal
|
|
27
|
+
process.kill(pid, 0);
|
|
28
|
+
return true;
|
|
29
|
+
} catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Format lock age in minutes
|
|
36
|
+
*/
|
|
37
|
+
function formatLockAge(ageMs: number): string {
|
|
38
|
+
const minutes = Math.round(ageMs / (60 * 1000));
|
|
39
|
+
return `${minutes} min`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Run unlock command
|
|
44
|
+
*
|
|
45
|
+
* Reads nax.lock, checks if holding process is alive, and removes lock if safe.
|
|
46
|
+
* Exits with code 0 on success, 1 on failure.
|
|
47
|
+
*/
|
|
48
|
+
export async function unlockCommand(options: UnlockOptions): Promise<void> {
|
|
49
|
+
const workdir = options.dir ?? process.cwd();
|
|
50
|
+
const lockPath = join(workdir, "nax.lock");
|
|
51
|
+
|
|
52
|
+
// Check if lock file exists
|
|
53
|
+
const lockFile = Bun.file(lockPath);
|
|
54
|
+
const exists = await lockFile.exists();
|
|
55
|
+
|
|
56
|
+
if (!exists) {
|
|
57
|
+
console.log("No lock file found");
|
|
58
|
+
process.exit(0);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Read lock file
|
|
62
|
+
let lockData: { pid: number; timestamp: number };
|
|
63
|
+
try {
|
|
64
|
+
const lockContent = await lockFile.text();
|
|
65
|
+
lockData = JSON.parse(lockContent);
|
|
66
|
+
} catch (error) {
|
|
67
|
+
console.error(chalk.red("Failed to parse lock file"));
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const { pid, timestamp } = lockData;
|
|
72
|
+
const ageMs = Date.now() - timestamp;
|
|
73
|
+
|
|
74
|
+
// Check if process is alive (unless --force)
|
|
75
|
+
if (!options.force) {
|
|
76
|
+
if (isProcessAlive(pid)) {
|
|
77
|
+
console.error(chalk.red(`nax is still running (PID ${pid}). Use --force to override.`));
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Print lock info before removing
|
|
83
|
+
console.log(`Stale lock found (PID ${pid}, age: ${formatLockAge(ageMs)})`);
|
|
84
|
+
|
|
85
|
+
// Remove lock file
|
|
86
|
+
const proc = Bun.spawn(["rm", lockPath], { stdout: "pipe" });
|
|
87
|
+
const rmExitCode = await proc.exited;
|
|
88
|
+
if (rmExitCode !== 0) {
|
|
89
|
+
console.error(chalk.red(`Failed to remove lock: rm exited with code ${rmExitCode}`));
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
// Wait a bit for filesystem to sync (prevents race in tests)
|
|
93
|
+
await Bun.sleep(10);
|
|
94
|
+
console.log("Lock removed");
|
|
95
|
+
process.exit(0);
|
|
96
|
+
}
|