rafcode 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +32 -0
- package/CLAUDE.md +187 -0
- package/LICENSE +21 -0
- package/RAF/001-raf-task-improvements/input.md +9 -0
- package/RAF/001-raf-task-improvements/outcomes/001-add-decisions-folder.md +21 -0
- package/RAF/001-raf-task-improvements/outcomes/002-fix-write-error-on-shutdown.md +22 -0
- package/RAF/001-raf-task-improvements/outcomes/003-stash-changes-on-failure.md +34 -0
- package/RAF/001-raf-task-improvements/outcomes/004-add-project-name-to-commits.md +28 -0
- package/RAF/001-raf-task-improvements/outcomes/005-add-running-time-display.md +36 -0
- package/RAF/001-raf-task-improvements/outcomes/006-add-task-name-to-logs.md +22 -0
- package/RAF/001-raf-task-improvements/outcomes/007-show-model-at-task-start.md +52 -0
- package/RAF/001-raf-task-improvements/outcomes/009-remove-editor-placeholder-text.md +20 -0
- package/RAF/001-raf-task-improvements/outcomes/SUMMARY.md +83 -0
- package/RAF/001-raf-task-improvements/plans/001-add-decisions-folder.md +38 -0
- package/RAF/001-raf-task-improvements/plans/002-fix-write-error-on-shutdown.md +33 -0
- package/RAF/001-raf-task-improvements/plans/003-stash-changes-on-failure.md +37 -0
- package/RAF/001-raf-task-improvements/plans/004-add-project-name-to-commits.md +34 -0
- package/RAF/001-raf-task-improvements/plans/005-add-running-time-display.md +39 -0
- package/RAF/001-raf-task-improvements/plans/006-add-task-name-to-logs.md +37 -0
- package/RAF/001-raf-task-improvements/plans/009-remove-editor-placeholder-text.md +34 -0
- package/RAF/002-raf-task-improvements-execution/decisions/DECISIONS.md +13 -0
- package/RAF/002-raf-task-improvements-execution/input.md +3 -0
- package/RAF/002-raf-task-improvements-execution/outcomes/001-commit-show-model-at-task-start.md +17 -0
- package/RAF/002-raf-task-improvements-execution/outcomes/002-delete-skipped-plan.md +23 -0
- package/RAF/002-raf-task-improvements-execution/outcomes/SUMMARY.md +32 -0
- package/RAF/002-raf-task-improvements-execution/plans/001-commit-show-model-at-task-start.md +37 -0
- package/RAF/002-raf-task-improvements-execution/plans/002-delete-skipped-plan.md +23 -0
- package/RAF/003-multi-project-execution/decisions/DECISIONS.md +68 -0
- package/RAF/003-multi-project-execution/input.md +6 -0
- package/RAF/003-multi-project-execution/outcomes/001-remove-state-json.md +52 -0
- package/RAF/003-multi-project-execution/outcomes/002-update-raf-status.md +50 -0
- package/RAF/003-multi-project-execution/outcomes/003-simplify-git-logic.md +35 -0
- package/RAF/003-multi-project-execution/outcomes/004-auto-commit-planning.md +43 -0
- package/RAF/003-multi-project-execution/outcomes/005-rerun-failed-tasks.md +43 -0
- package/RAF/003-multi-project-execution/outcomes/006-multi-project-execution.md +42 -0
- package/RAF/003-multi-project-execution/outcomes/007-verify-timeout.md +54 -0
- package/RAF/003-multi-project-execution/outcomes/008-move-decisions-file.md +38 -0
- package/RAF/003-multi-project-execution/outcomes/SUMMARY.md +79 -0
- package/RAF/003-multi-project-execution/plans/001-remove-state-json.md +71 -0
- package/RAF/003-multi-project-execution/plans/002-update-raf-status.md +65 -0
- package/RAF/003-multi-project-execution/plans/003-simplify-git-logic.md +74 -0
- package/RAF/003-multi-project-execution/plans/004-auto-commit-planning.md +57 -0
- package/RAF/003-multi-project-execution/plans/005-rerun-failed-tasks.md +69 -0
- package/RAF/003-multi-project-execution/plans/006-multi-project-execution.md +81 -0
- package/RAF/003-multi-project-execution/plans/007-verify-timeout.md +63 -0
- package/RAF/003-multi-project-execution/plans/008-move-decisions-file.md +78 -0
- package/RAF/004-task-naming-optimization/decisions.md +22 -0
- package/RAF/004-task-naming-optimization/input.md +6 -0
- package/RAF/004-task-naming-optimization/outcomes/001-remove-summary-file.md +17 -0
- package/RAF/004-task-naming-optimization/outcomes/002-base36-project-numbering.md +32 -0
- package/RAF/004-task-naming-optimization/outcomes/003-improve-haiku-prompt.md +20 -0
- package/RAF/004-task-naming-optimization/outcomes/SUMMARY.md +28 -0
- package/RAF/004-task-naming-optimization/plans/001-remove-summary-file.md +34 -0
- package/RAF/004-task-naming-optimization/plans/002-base36-project-numbering.md +56 -0
- package/RAF/004-task-naming-optimization/plans/003-improve-haiku-prompt.md +50 -0
- package/RAF/005-task-naming-improvements/decisions.md +60 -0
- package/RAF/005-task-naming-improvements/input.md +2 -0
- package/RAF/005-task-naming-improvements/outcomes/001-enhance-identifier-resolution.md +42 -0
- package/RAF/005-task-naming-improvements/outcomes/002-add-identifier-support-to-status.md +38 -0
- package/RAF/005-task-naming-improvements/outcomes/003-update-do-for-full-folder-names.md +44 -0
- package/RAF/005-task-naming-improvements/outcomes/004-implement-amend-flag-for-plan.md +55 -0
- package/RAF/005-task-naming-improvements/outcomes/005-commit-outcomes-on-complete.md +47 -0
- package/RAF/005-task-naming-improvements/outcomes/006-update-execution-prompt-commit-schema.md +40 -0
- package/RAF/005-task-naming-improvements/outcomes/007-allow-pending-task-amendments.md +38 -0
- package/RAF/005-task-naming-improvements/outcomes/008-fix-timeout-label.md +24 -0
- package/RAF/005-task-naming-improvements/plans/001-enhance-identifier-resolution.md +46 -0
- package/RAF/005-task-naming-improvements/plans/002-add-identifier-support-to-status.md +36 -0
- package/RAF/005-task-naming-improvements/plans/003-update-do-for-full-folder-names.md +38 -0
- package/RAF/005-task-naming-improvements/plans/004-implement-amend-flag-for-plan.md +67 -0
- package/RAF/005-task-naming-improvements/plans/005-commit-outcomes-on-complete.md +86 -0
- package/RAF/005-task-naming-improvements/plans/006-update-execution-prompt-commit-schema.md +60 -0
- package/RAF/005-task-naming-improvements/plans/007-allow-pending-task-amendments.md +60 -0
- package/RAF/005-task-naming-improvements/plans/008-fix-timeout-label.md +31 -0
- package/RAF/006-fix-double-summary-headers/decisions.md +28 -0
- package/RAF/006-fix-double-summary-headers/input.md +3 -0
- package/RAF/006-fix-double-summary-headers/outcomes/001-fix-double-summary-headers.md +29 -0
- package/RAF/006-fix-double-summary-headers/outcomes/002-update-readme-for-npm.md +31 -0
- package/RAF/006-fix-double-summary-headers/outcomes/003-npm-publish-instructions.md +30 -0
- package/RAF/006-fix-double-summary-headers/outcomes/004-flexible-project-lookup.md +47 -0
- package/RAF/006-fix-double-summary-headers/plans/001-fix-double-summary-headers.md +42 -0
- package/RAF/006-fix-double-summary-headers/plans/002-update-readme-for-npm.md +44 -0
- package/RAF/006-fix-double-summary-headers/plans/003-npm-publish-instructions.md +45 -0
- package/RAF/006-fix-double-summary-headers/plans/004-flexible-project-lookup.md +40 -0
- package/RAF/007-improve-outcome-format/decisions.md +28 -0
- package/RAF/007-improve-outcome-format/input.md +2 -0
- package/RAF/007-improve-outcome-format/outcomes/001-update-execution-prompt.md +10 -0
- package/RAF/007-improve-outcome-format/outcomes/002-update-state-derivation.md +17 -0
- package/RAF/007-improve-outcome-format/outcomes/003-update-do-command-outcome-handling.md +16 -0
- package/RAF/007-improve-outcome-format/outcomes/004-implement-failure-analysis.md +16 -0
- package/RAF/007-improve-outcome-format/outcomes/005-update-documentation.md +15 -0
- package/RAF/007-improve-outcome-format/plans/001-update-execution-prompt.md +36 -0
- package/RAF/007-improve-outcome-format/plans/002-update-state-derivation.md +35 -0
- package/RAF/007-improve-outcome-format/plans/003-update-do-command-outcome-handling.md +37 -0
- package/RAF/007-improve-outcome-format/plans/004-implement-failure-analysis.md +44 -0
- package/RAF/007-improve-outcome-format/plans/005-update-documentation.md +33 -0
- package/RAF/008-beautiful-do/decisions.md +31 -0
- package/RAF/008-beautiful-do/input.md +1 -0
- package/RAF/008-beautiful-do/outcomes/001-terminal-symbols.md +55 -0
- package/RAF/008-beautiful-do/outcomes/002-refactor-do-output.md +95 -0
- package/RAF/008-beautiful-do/outcomes/003-refactor-status-output.md +71 -0
- package/RAF/008-beautiful-do/outcomes/004-simplify-logger.md +53 -0
- package/RAF/008-beautiful-do/outcomes/005-add-tests.md +41 -0
- package/RAF/008-beautiful-do/plans/001-terminal-symbols.md +41 -0
- package/RAF/008-beautiful-do/plans/002-refactor-do-output.md +44 -0
- package/RAF/008-beautiful-do/plans/003-refactor-status-output.md +37 -0
- package/RAF/008-beautiful-do/plans/004-simplify-logger.md +32 -0
- package/RAF/008-beautiful-do/plans/005-add-tests.md +40 -0
- package/RAF/009-system-promt-ammend/decisions.md +13 -0
- package/RAF/009-system-promt-ammend/input.md +9 -0
- package/RAF/009-system-promt-ammend/outcomes/001-model-override.md +79 -0
- package/RAF/009-system-promt-ammend/outcomes/002-system-prompt-append.md +51 -0
- package/RAF/009-system-promt-ammend/outcomes/003-retry-context.md +60 -0
- package/RAF/009-system-promt-ammend/plans/001-model-override.md +61 -0
- package/RAF/009-system-promt-ammend/plans/002-system-prompt-append.md +56 -0
- package/RAF/009-system-promt-ammend/plans/003-retry-context.md +76 -0
- package/RAF/010-outcome-marker-fallback/decisions.md +19 -0
- package/RAF/010-outcome-marker-fallback/input.md +1 -0
- package/RAF/010-outcome-marker-fallback/outcomes/001-outcome-file-marker-fallback.md +35 -0
- package/RAF/010-outcome-marker-fallback/outcomes/002-creative-project-naming.md +47 -0
- package/RAF/010-outcome-marker-fallback/plans/001-outcome-file-marker-fallback.md +58 -0
- package/RAF/010-outcome-marker-fallback/plans/002-creative-project-naming.md +68 -0
- package/RAF/011-do-task-in-commit/decisions.md +22 -0
- package/RAF/011-do-task-in-commit/input.md +1 -0
- package/RAF/011-do-task-in-commit/outcomes/001-update-execution-prompt.md +54 -0
- package/RAF/011-do-task-in-commit/outcomes/002-update-tests.md +61 -0
- package/RAF/011-do-task-in-commit/outcomes/003-update-documentation.md +51 -0
- package/RAF/011-do-task-in-commit/plans/001-update-execution-prompt.md +46 -0
- package/RAF/011-do-task-in-commit/plans/002-update-tests.md +51 -0
- package/RAF/011-do-task-in-commit/plans/003-update-documentation.md +45 -0
- package/RAF/012-name-picker-buffet/decisions.md +40 -0
- package/RAF/012-name-picker-buffet/input.md +6 -0
- package/RAF/012-name-picker-buffet/outcomes/001-name-picker-for-raf-plan.md +49 -0
- package/RAF/012-name-picker-buffet/outcomes/002-interactive-project-picker-for-raf-do.md +49 -0
- package/RAF/012-name-picker-buffet/outcomes/003-raf-status-truncation.md +55 -0
- package/RAF/012-name-picker-buffet/outcomes/004-failure-reason-details.md +65 -0
- package/RAF/012-name-picker-buffet/outcomes/005-remove-raf-commits.md +57 -0
- package/RAF/012-name-picker-buffet/outcomes/006-update-execution-prompt-for-commits.md +47 -0
- package/RAF/012-name-picker-buffet/outcomes/007-fix-plan-mode-user-prompt.md +83 -0
- package/RAF/012-name-picker-buffet/outcomes/008-add-auto-flag-for-plan-mode.md +77 -0
- package/RAF/012-name-picker-buffet/plans/001-name-picker-for-raf-plan.md +47 -0
- package/RAF/012-name-picker-buffet/plans/002-interactive-project-picker-for-raf-do.md +43 -0
- package/RAF/012-name-picker-buffet/plans/003-raf-status-truncation.md +36 -0
- package/RAF/012-name-picker-buffet/plans/004-failure-reason-details.md +46 -0
- package/RAF/012-name-picker-buffet/plans/005-remove-raf-commits.md +42 -0
- package/RAF/012-name-picker-buffet/plans/006-update-execution-prompt-for-commits.md +47 -0
- package/RAF/012-name-picker-buffet/plans/007-fix-plan-mode-user-prompt.md +55 -0
- package/RAF/012-name-picker-buffet/plans/008-add-auto-flag-for-plan-mode.md +49 -0
- package/RAF/013-dependencies-watchdog/decisions.md +37 -0
- package/RAF/013-dependencies-watchdog/input.md +1 -0
- package/RAF/013-dependencies-watchdog/outcomes/001-define-dependency-syntax.md +56 -0
- package/RAF/013-dependencies-watchdog/outcomes/002-update-planning-prompts.md +60 -0
- package/RAF/013-dependencies-watchdog/outcomes/003-parse-dependencies-update-state.md +81 -0
- package/RAF/013-dependencies-watchdog/outcomes/004-implement-dependency-checking-in-do.md +116 -0
- package/RAF/013-dependencies-watchdog/outcomes/005-update-execution-prompts.md +75 -0
- package/RAF/013-dependencies-watchdog/outcomes/006-add-tests.md +100 -0
- package/RAF/013-dependencies-watchdog/outcomes/007-add-act-alias.md +46 -0
- package/RAF/013-dependencies-watchdog/outcomes/008-add-exit-message.md +52 -0
- package/RAF/013-dependencies-watchdog/plans/001-define-dependency-syntax.md +32 -0
- package/RAF/013-dependencies-watchdog/plans/002-update-planning-prompts.md +38 -0
- package/RAF/013-dependencies-watchdog/plans/003-parse-dependencies-update-state.md +46 -0
- package/RAF/013-dependencies-watchdog/plans/004-implement-dependency-checking-in-do.md +48 -0
- package/RAF/013-dependencies-watchdog/plans/005-update-execution-prompts.md +44 -0
- package/RAF/013-dependencies-watchdog/plans/006-add-tests.md +54 -0
- package/RAF/013-dependencies-watchdog/plans/007-add-act-alias.md +26 -0
- package/RAF/013-dependencies-watchdog/plans/008-add-exit-message.md +31 -0
- package/RAF/014-watchdog/decisions.md +16 -0
- package/RAF/014-watchdog/input.md +2 -0
- package/RAF/014-watchdog/outcomes/001-amend-flag-position.md +50 -0
- package/RAF/014-watchdog/outcomes/002-details-only-on-failure.md +58 -0
- package/RAF/014-watchdog/plans/001-amend-flag-position.md +34 -0
- package/RAF/014-watchdog/plans/002-details-only-on-failure.md +46 -0
- package/RAF/015-name-lottery/decisions.md +14 -0
- package/RAF/015-name-lottery/input.md +3 -0
- package/RAF/015-name-lottery/outcomes/001-auto-pick-project-name.md +31 -0
- package/RAF/015-name-lottery/outcomes/002-mention-plan-files-in-commit.md +23 -0
- package/RAF/015-name-lottery/outcomes/003-fix-input-md-in-amend-flow.md +44 -0
- package/RAF/015-name-lottery/plans/001-auto-pick-project-name.md +38 -0
- package/RAF/015-name-lottery/plans/002-mention-plan-files-in-commit.md +32 -0
- package/RAF/015-name-lottery/plans/003-fix-input-md-in-amend-flow.md +44 -0
- package/README.md +116 -0
- package/dist/commands/do.d.ts +12 -0
- package/dist/commands/do.d.ts.map +1 -0
- package/dist/commands/do.js +684 -0
- package/dist/commands/do.js.map +1 -0
- package/dist/commands/plan.d.ts +3 -0
- package/dist/commands/plan.d.ts.map +1 -0
- package/dist/commands/plan.js +345 -0
- package/dist/commands/plan.js.map +1 -0
- package/dist/commands/status.d.ts +3 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +117 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/core/claude-runner.d.ts +78 -0
- package/dist/core/claude-runner.d.ts.map +1 -0
- package/dist/core/claude-runner.js +297 -0
- package/dist/core/claude-runner.js.map +1 -0
- package/dist/core/editor.d.ts +10 -0
- package/dist/core/editor.d.ts.map +1 -0
- package/dist/core/editor.js +77 -0
- package/dist/core/editor.js.map +1 -0
- package/dist/core/failure-analyzer.d.ts +28 -0
- package/dist/core/failure-analyzer.d.ts.map +1 -0
- package/dist/core/failure-analyzer.js +305 -0
- package/dist/core/failure-analyzer.js.map +1 -0
- package/dist/core/git.d.ts +42 -0
- package/dist/core/git.d.ts.map +1 -0
- package/dist/core/git.js +148 -0
- package/dist/core/git.js.map +1 -0
- package/dist/core/project-manager.d.ts +72 -0
- package/dist/core/project-manager.d.ts.map +1 -0
- package/dist/core/project-manager.js +193 -0
- package/dist/core/project-manager.js.map +1 -0
- package/dist/core/retry-handler.d.ts +19 -0
- package/dist/core/retry-handler.d.ts.map +1 -0
- package/dist/core/retry-handler.js +51 -0
- package/dist/core/retry-handler.js.map +1 -0
- package/dist/core/shutdown-handler.d.ts +30 -0
- package/dist/core/shutdown-handler.d.ts.map +1 -0
- package/dist/core/shutdown-handler.js +79 -0
- package/dist/core/shutdown-handler.js.map +1 -0
- package/dist/core/state-derivation.d.ts +82 -0
- package/dist/core/state-derivation.d.ts.map +1 -0
- package/dist/core/state-derivation.js +271 -0
- package/dist/core/state-derivation.js.map +1 -0
- package/dist/core/state-manager.d.ts +54 -0
- package/dist/core/state-manager.d.ts.map +1 -0
- package/dist/core/state-manager.js +198 -0
- package/dist/core/state-manager.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/parsers/output-parser.d.ts +19 -0
- package/dist/parsers/output-parser.d.ts.map +1 -0
- package/dist/parsers/output-parser.js +137 -0
- package/dist/parsers/output-parser.js.map +1 -0
- package/dist/prompts/amend.d.ts +20 -0
- package/dist/prompts/amend.d.ts.map +1 -0
- package/dist/prompts/amend.js +166 -0
- package/dist/prompts/amend.js.map +1 -0
- package/dist/prompts/execution.d.ts +30 -0
- package/dist/prompts/execution.d.ts.map +1 -0
- package/dist/prompts/execution.js +179 -0
- package/dist/prompts/execution.js.map +1 -0
- package/dist/prompts/planning.d.ts +15 -0
- package/dist/prompts/planning.d.ts.map +1 -0
- package/dist/prompts/planning.js +163 -0
- package/dist/prompts/planning.js.map +1 -0
- package/dist/types/config.d.ts +26 -0
- package/dist/types/config.d.ts.map +1 -0
- package/dist/types/config.js +7 -0
- package/dist/types/config.js.map +1 -0
- package/dist/types/state.d.ts +33 -0
- package/dist/types/state.d.ts.map +1 -0
- package/dist/types/state.js +28 -0
- package/dist/types/state.js.map +1 -0
- package/dist/ui/name-picker-subprocess.d.ts +11 -0
- package/dist/ui/name-picker-subprocess.d.ts.map +1 -0
- package/dist/ui/name-picker-subprocess.js +83 -0
- package/dist/ui/name-picker-subprocess.js.map +1 -0
- package/dist/ui/name-picker.d.ts +19 -0
- package/dist/ui/name-picker.d.ts.map +1 -0
- package/dist/ui/name-picker.js +173 -0
- package/dist/ui/name-picker.js.map +1 -0
- package/dist/ui/project-picker.d.ts +27 -0
- package/dist/ui/project-picker.d.ts.map +1 -0
- package/dist/ui/project-picker.js +58 -0
- package/dist/ui/project-picker.js.map +1 -0
- package/dist/utils/config.d.ts +24 -0
- package/dist/utils/config.d.ts.map +1 -0
- package/dist/utils/config.js +63 -0
- package/dist/utils/config.js.map +1 -0
- package/dist/utils/logger.d.ts +32 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +60 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/name-generator.d.ts +20 -0
- package/dist/utils/name-generator.d.ts.map +1 -0
- package/dist/utils/name-generator.js +183 -0
- package/dist/utils/name-generator.js.map +1 -0
- package/dist/utils/paths.d.ts +132 -0
- package/dist/utils/paths.d.ts.map +1 -0
- package/dist/utils/paths.js +412 -0
- package/dist/utils/paths.js.map +1 -0
- package/dist/utils/status-line.d.ts +14 -0
- package/dist/utils/status-line.d.ts.map +1 -0
- package/dist/utils/status-line.js +36 -0
- package/dist/utils/status-line.js.map +1 -0
- package/dist/utils/terminal-symbols.d.ts +50 -0
- package/dist/utils/terminal-symbols.d.ts.map +1 -0
- package/dist/utils/terminal-symbols.js +97 -0
- package/dist/utils/terminal-symbols.js.map +1 -0
- package/dist/utils/timer.d.ts +17 -0
- package/dist/utils/timer.d.ts.map +1 -0
- package/dist/utils/timer.js +56 -0
- package/dist/utils/timer.js.map +1 -0
- package/dist/utils/validation.d.ts +17 -0
- package/dist/utils/validation.d.ts.map +1 -0
- package/dist/utils/validation.js +106 -0
- package/dist/utils/validation.js.map +1 -0
- package/dist/utils/version.d.ts +2 -0
- package/dist/utils/version.d.ts.map +1 -0
- package/dist/utils/version.js +12 -0
- package/dist/utils/version.js.map +1 -0
- package/jest.config.ts +30 -0
- package/package.json +55 -0
- package/src/commands/do.ts +829 -0
- package/src/commands/plan.ts +422 -0
- package/src/commands/status.ts +146 -0
- package/src/core/claude-runner.ts +374 -0
- package/src/core/editor.ts +85 -0
- package/src/core/failure-analyzer.ts +372 -0
- package/src/core/git.ts +166 -0
- package/src/core/project-manager.ts +243 -0
- package/src/core/retry-handler.ts +72 -0
- package/src/core/shutdown-handler.ts +93 -0
- package/src/core/state-derivation.ts +343 -0
- package/src/index.ts +20 -0
- package/src/parsers/output-parser.ts +164 -0
- package/src/prompts/amend.ts +194 -0
- package/src/prompts/execution.ts +223 -0
- package/src/prompts/planning.ts +175 -0
- package/src/types/config.ts +35 -0
- package/src/ui/name-picker-subprocess.ts +96 -0
- package/src/ui/name-picker.ts +198 -0
- package/src/ui/project-picker.ts +80 -0
- package/src/utils/config.ts +69 -0
- package/src/utils/logger.ts +81 -0
- package/src/utils/name-generator.ts +211 -0
- package/src/utils/paths.ts +497 -0
- package/src/utils/status-line.ts +45 -0
- package/src/utils/terminal-symbols.ts +124 -0
- package/src/utils/timer.ts +64 -0
- package/src/utils/validation.ts +132 -0
- package/src/utils/version.ts +12 -0
- package/tests/unit/claude-runner-interactive.test.ts +343 -0
- package/tests/unit/claude-runner.test.ts +629 -0
- package/tests/unit/command-output.test.ts +295 -0
- package/tests/unit/config.test.ts +72 -0
- package/tests/unit/dependency-integration.test.ts +559 -0
- package/tests/unit/do-blocked-tasks.test.ts +323 -0
- package/tests/unit/do-command.test.ts +198 -0
- package/tests/unit/do-multiproject.test.ts +270 -0
- package/tests/unit/do-rerun.test.ts +270 -0
- package/tests/unit/execution-prompt.test.ts +406 -0
- package/tests/unit/failure-analyzer.test.ts +276 -0
- package/tests/unit/failure-history.test.ts +143 -0
- package/tests/unit/git-stash.test.ts +138 -0
- package/tests/unit/git.test.ts +80 -0
- package/tests/unit/logger.test.ts +132 -0
- package/tests/unit/name-generator.test.ts +283 -0
- package/tests/unit/name-picker.test.ts +179 -0
- package/tests/unit/outcome-content.test.ts +166 -0
- package/tests/unit/output-parser.test.ts +178 -0
- package/tests/unit/paths.test.ts +741 -0
- package/tests/unit/plan-command-amend-flag.test.ts +115 -0
- package/tests/unit/plan-command-amend-input.test.ts +156 -0
- package/tests/unit/plan-command-auto-flag.test.ts +112 -0
- package/tests/unit/plan-command.test.ts +580 -0
- package/tests/unit/planning-prompt.test.ts +137 -0
- package/tests/unit/project-manager.test.ts +265 -0
- package/tests/unit/project-picker.test.ts +338 -0
- package/tests/unit/retry-handler.test.ts +89 -0
- package/tests/unit/state-derivation.test.ts +714 -0
- package/tests/unit/status-command.test.ts +271 -0
- package/tests/unit/status-line.test.ts +92 -0
- package/tests/unit/terminal-symbols.test.ts +214 -0
- package/tests/unit/timer.test.ts +102 -0
- package/tests/unit/validation.test.ts +118 -0
- package/tsconfig.json +26 -0
|
@@ -0,0 +1,741 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import * as os from 'node:os';
|
|
4
|
+
import {
|
|
5
|
+
getNextProjectNumber,
|
|
6
|
+
formatProjectNumber,
|
|
7
|
+
listProjects,
|
|
8
|
+
extractProjectNumber,
|
|
9
|
+
extractProjectName,
|
|
10
|
+
extractTaskNameFromPlanFile,
|
|
11
|
+
resolveProjectIdentifier,
|
|
12
|
+
resolveProjectIdentifierWithDetails,
|
|
13
|
+
getDecisionsPath,
|
|
14
|
+
encodeBase36,
|
|
15
|
+
decodeBase36,
|
|
16
|
+
isBase36Prefix,
|
|
17
|
+
parseProjectPrefix,
|
|
18
|
+
} from '../../src/utils/paths.js';
|
|
19
|
+
|
|
20
|
+
describe('Paths', () => {
|
|
21
|
+
let tempDir: string;
|
|
22
|
+
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'raf-test-'));
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('getNextProjectNumber', () => {
|
|
32
|
+
it('should return 1 for empty directory', () => {
|
|
33
|
+
expect(getNextProjectNumber(tempDir)).toBe(1);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should return 1 for non-existent directory', () => {
|
|
37
|
+
expect(getNextProjectNumber('/non/existent/path')).toBe(1);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should return next number after existing projects', () => {
|
|
41
|
+
fs.mkdirSync(path.join(tempDir, '01-first'));
|
|
42
|
+
fs.mkdirSync(path.join(tempDir, '02-second'));
|
|
43
|
+
|
|
44
|
+
expect(getNextProjectNumber(tempDir)).toBe(3);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should handle gaps in numbering', () => {
|
|
48
|
+
fs.mkdirSync(path.join(tempDir, '01-first'));
|
|
49
|
+
fs.mkdirSync(path.join(tempDir, '05-fifth'));
|
|
50
|
+
|
|
51
|
+
expect(getNextProjectNumber(tempDir)).toBe(6);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe('formatProjectNumber', () => {
|
|
56
|
+
it('should pad single digits to 3 digits', () => {
|
|
57
|
+
expect(formatProjectNumber(1)).toBe('001');
|
|
58
|
+
expect(formatProjectNumber(9)).toBe('009');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should pad double digits to 3 digits', () => {
|
|
62
|
+
expect(formatProjectNumber(10)).toBe('010');
|
|
63
|
+
expect(formatProjectNumber(99)).toBe('099');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should not pad triple digits', () => {
|
|
67
|
+
expect(formatProjectNumber(100)).toBe('100');
|
|
68
|
+
expect(formatProjectNumber(999)).toBe('999');
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe('listProjects', () => {
|
|
73
|
+
it('should list projects in order', () => {
|
|
74
|
+
fs.mkdirSync(path.join(tempDir, '02-second'));
|
|
75
|
+
fs.mkdirSync(path.join(tempDir, '01-first'));
|
|
76
|
+
fs.mkdirSync(path.join(tempDir, '03-third'));
|
|
77
|
+
|
|
78
|
+
const projects = listProjects(tempDir);
|
|
79
|
+
expect(projects).toHaveLength(3);
|
|
80
|
+
expect(projects[0]?.name).toBe('first');
|
|
81
|
+
expect(projects[1]?.name).toBe('second');
|
|
82
|
+
expect(projects[2]?.name).toBe('third');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should ignore non-project directories', () => {
|
|
86
|
+
fs.mkdirSync(path.join(tempDir, '01-valid'));
|
|
87
|
+
fs.mkdirSync(path.join(tempDir, 'not-a-project')); // 'not' is valid base36, but project name must follow hyphen
|
|
88
|
+
fs.mkdirSync(path.join(tempDir, 'random')); // No hyphen, not a project
|
|
89
|
+
fs.mkdirSync(path.join(tempDir, 'ab')); // Too short prefix
|
|
90
|
+
|
|
91
|
+
const projects = listProjects(tempDir);
|
|
92
|
+
// 'not-a-project' is now recognized as base36 project (not = 18741)
|
|
93
|
+
expect(projects).toHaveLength(2);
|
|
94
|
+
expect(projects[0]?.name).toBe('valid');
|
|
95
|
+
expect(projects[0]?.number).toBe(1);
|
|
96
|
+
expect(projects[1]?.name).toBe('a-project');
|
|
97
|
+
expect(projects[1]?.number).toBe(18741); // 'not' in base36
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should return empty array for non-existent directory', () => {
|
|
101
|
+
const projects = listProjects('/non/existent/path');
|
|
102
|
+
expect(projects).toEqual([]);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe('extractProjectNumber', () => {
|
|
107
|
+
it('should extract 3-digit project number from path', () => {
|
|
108
|
+
expect(extractProjectNumber('/Users/foo/RAF/001-my-project')).toBe('001');
|
|
109
|
+
expect(extractProjectNumber('/RAF/123-another-project')).toBe('123');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('should extract 2-digit project number from path', () => {
|
|
113
|
+
expect(extractProjectNumber('/RAF/01-first')).toBe('01');
|
|
114
|
+
expect(extractProjectNumber('/RAF/99-last')).toBe('99');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('should return null for invalid paths', () => {
|
|
118
|
+
expect(extractProjectNumber('/RAF/my-project')).toBeNull(); // 'my' is only 2 chars
|
|
119
|
+
expect(extractProjectNumber('')).toBeNull();
|
|
120
|
+
// 'not' is a valid base36 prefix, so not-numbered returns 'not'
|
|
121
|
+
expect(extractProjectNumber('/RAF/not-numbered')).toBe('not');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should handle path with trailing slash', () => {
|
|
125
|
+
expect(extractProjectNumber('/RAF/001-my-project/')).toBe('001');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('should only match valid prefixes at the start of folder name', () => {
|
|
129
|
+
// 'abc' is a valid base36 prefix, so abc-001-project returns 'abc'
|
|
130
|
+
expect(extractProjectNumber('/RAF/abc-001-project')).toBe('abc');
|
|
131
|
+
// 'project' is too long for a valid prefix (must be 2-3 digits or 3 char base36)
|
|
132
|
+
expect(extractProjectNumber('/RAF/project-001')).toBeNull();
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe('extractProjectName', () => {
|
|
137
|
+
it('should extract project name from 3-digit numbered path', () => {
|
|
138
|
+
expect(extractProjectName('/Users/foo/RAF/001-my-project')).toBe('my-project');
|
|
139
|
+
expect(extractProjectName('/RAF/123-another-project')).toBe('another-project');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('should extract project name from 2-digit numbered path', () => {
|
|
143
|
+
expect(extractProjectName('/RAF/01-first')).toBe('first');
|
|
144
|
+
expect(extractProjectName('/RAF/99-last')).toBe('last');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('should return null for invalid paths', () => {
|
|
148
|
+
expect(extractProjectName('/RAF/my-project')).toBeNull(); // 'my' is only 2 chars
|
|
149
|
+
expect(extractProjectName('')).toBeNull();
|
|
150
|
+
// 'not' is a valid base36 prefix, so not-numbered extracts 'numbered' as name
|
|
151
|
+
expect(extractProjectName('/RAF/not-numbered')).toBe('numbered');
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('should handle path with trailing slash', () => {
|
|
155
|
+
expect(extractProjectName('/RAF/001-my-project/')).toBe('my-project');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('should only match valid prefixes at the start of folder name', () => {
|
|
159
|
+
// 'abc' is a valid base36 prefix, so abc-001-project extracts '001-project' as name
|
|
160
|
+
expect(extractProjectName('/RAF/abc-001-project')).toBe('001-project');
|
|
161
|
+
// 'project' is too long for a valid prefix
|
|
162
|
+
expect(extractProjectName('/RAF/project-001')).toBeNull();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('should handle project names with hyphens', () => {
|
|
166
|
+
expect(extractProjectName('/RAF/001-my-complex-project-name')).toBe('my-complex-project-name');
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('should handle project names with numbers', () => {
|
|
170
|
+
expect(extractProjectName('/RAF/001-project-v2')).toBe('project-v2');
|
|
171
|
+
expect(extractProjectName('/RAF/001-123-test')).toBe('123-test');
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
describe('extractTaskNameFromPlanFile', () => {
|
|
176
|
+
it('should extract task name from 3-digit numbered plan file', () => {
|
|
177
|
+
expect(extractTaskNameFromPlanFile('001-fix-login-bug.md')).toBe('fix-login-bug');
|
|
178
|
+
expect(extractTaskNameFromPlanFile('123-add-feature.md')).toBe('add-feature');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('should extract task name from 2-digit numbered plan file', () => {
|
|
182
|
+
expect(extractTaskNameFromPlanFile('01-first-task.md')).toBe('first-task');
|
|
183
|
+
expect(extractTaskNameFromPlanFile('99-last-task.md')).toBe('last-task');
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('should return null for invalid filenames', () => {
|
|
187
|
+
expect(extractTaskNameFromPlanFile('my-task.md')).toBeNull();
|
|
188
|
+
expect(extractTaskNameFromPlanFile('not-numbered.md')).toBeNull();
|
|
189
|
+
expect(extractTaskNameFromPlanFile('')).toBeNull();
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('should handle task names with hyphens', () => {
|
|
193
|
+
expect(extractTaskNameFromPlanFile('001-my-complex-task-name.md')).toBe('my-complex-task-name');
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('should handle task names with numbers', () => {
|
|
197
|
+
expect(extractTaskNameFromPlanFile('001-task-v2.md')).toBe('task-v2');
|
|
198
|
+
expect(extractTaskNameFromPlanFile('001-123-test.md')).toBe('123-test');
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('should handle full paths', () => {
|
|
202
|
+
expect(extractTaskNameFromPlanFile('/path/to/plans/002-fix-login-bug.md')).toBe('fix-login-bug');
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('should handle files without .md extension', () => {
|
|
206
|
+
expect(extractTaskNameFromPlanFile('001-task-name')).toBe('task-name');
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
describe('resolveProjectIdentifier', () => {
|
|
211
|
+
it('should resolve project by exact number (3 digits)', () => {
|
|
212
|
+
fs.mkdirSync(path.join(tempDir, '003-my-project'));
|
|
213
|
+
const result = resolveProjectIdentifier(tempDir, '003');
|
|
214
|
+
expect(result).toBe(path.join(tempDir, '003-my-project'));
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('should resolve project by number without leading zeros', () => {
|
|
218
|
+
fs.mkdirSync(path.join(tempDir, '003-my-project'));
|
|
219
|
+
const result = resolveProjectIdentifier(tempDir, '3');
|
|
220
|
+
expect(result).toBe(path.join(tempDir, '003-my-project'));
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('should resolve project by 2-digit number', () => {
|
|
224
|
+
fs.mkdirSync(path.join(tempDir, '42-my-project'));
|
|
225
|
+
const result = resolveProjectIdentifier(tempDir, '42');
|
|
226
|
+
expect(result).toBe(path.join(tempDir, '42-my-project'));
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('should resolve project by name', () => {
|
|
230
|
+
fs.mkdirSync(path.join(tempDir, '005-my-awesome-project'));
|
|
231
|
+
const result = resolveProjectIdentifier(tempDir, 'my-awesome-project');
|
|
232
|
+
expect(result).toBe(path.join(tempDir, '005-my-awesome-project'));
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('should return null for non-existent project number', () => {
|
|
236
|
+
fs.mkdirSync(path.join(tempDir, '001-first'));
|
|
237
|
+
const result = resolveProjectIdentifier(tempDir, '999');
|
|
238
|
+
expect(result).toBeNull();
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('should return null for non-existent project name', () => {
|
|
242
|
+
fs.mkdirSync(path.join(tempDir, '001-first'));
|
|
243
|
+
const result = resolveProjectIdentifier(tempDir, 'non-existent');
|
|
244
|
+
expect(result).toBeNull();
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('should return null for non-existent directory', () => {
|
|
248
|
+
const result = resolveProjectIdentifier('/non/existent/path', '001');
|
|
249
|
+
expect(result).toBeNull();
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('should handle multiple projects and find correct one by number', () => {
|
|
253
|
+
fs.mkdirSync(path.join(tempDir, '001-first'));
|
|
254
|
+
fs.mkdirSync(path.join(tempDir, '002-second'));
|
|
255
|
+
fs.mkdirSync(path.join(tempDir, '003-third'));
|
|
256
|
+
|
|
257
|
+
expect(resolveProjectIdentifier(tempDir, '1')).toBe(path.join(tempDir, '001-first'));
|
|
258
|
+
expect(resolveProjectIdentifier(tempDir, '2')).toBe(path.join(tempDir, '002-second'));
|
|
259
|
+
expect(resolveProjectIdentifier(tempDir, '003')).toBe(path.join(tempDir, '003-third'));
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it('should handle multiple projects and find correct one by name', () => {
|
|
263
|
+
fs.mkdirSync(path.join(tempDir, '001-first'));
|
|
264
|
+
fs.mkdirSync(path.join(tempDir, '002-second'));
|
|
265
|
+
fs.mkdirSync(path.join(tempDir, '003-third'));
|
|
266
|
+
|
|
267
|
+
expect(resolveProjectIdentifier(tempDir, 'first')).toBe(path.join(tempDir, '001-first'));
|
|
268
|
+
expect(resolveProjectIdentifier(tempDir, 'second')).toBe(path.join(tempDir, '002-second'));
|
|
269
|
+
expect(resolveProjectIdentifier(tempDir, 'third')).toBe(path.join(tempDir, '003-third'));
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('should not resolve partial name match', () => {
|
|
273
|
+
fs.mkdirSync(path.join(tempDir, '001-my-project'));
|
|
274
|
+
expect(resolveProjectIdentifier(tempDir, 'my')).toBeNull();
|
|
275
|
+
expect(resolveProjectIdentifier(tempDir, 'project')).toBeNull();
|
|
276
|
+
expect(resolveProjectIdentifier(tempDir, 'my-proj')).toBeNull();
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('should prefer number match when identifier is numeric', () => {
|
|
280
|
+
fs.mkdirSync(path.join(tempDir, '005-project'));
|
|
281
|
+
// Numeric identifier "5" should match project number 5
|
|
282
|
+
const result = resolveProjectIdentifier(tempDir, '5');
|
|
283
|
+
expect(result).toBe(path.join(tempDir, '005-project'));
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
describe('getDecisionsPath', () => {
|
|
288
|
+
it('should return decisions.md at project root', () => {
|
|
289
|
+
const projectPath = '/Users/foo/RAF/001-my-project';
|
|
290
|
+
expect(getDecisionsPath(projectPath)).toBe(path.join(projectPath, 'decisions.md'));
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
// Base36 project numbering tests
|
|
295
|
+
describe('encodeBase36', () => {
|
|
296
|
+
it('should encode 1000 as a00', () => {
|
|
297
|
+
expect(encodeBase36(1000)).toBe('a00');
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('should encode 1001 as a01', () => {
|
|
301
|
+
expect(encodeBase36(1001)).toBe('a01');
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it('should encode 1035 as a0z', () => {
|
|
305
|
+
expect(encodeBase36(1035)).toBe('a0z');
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it('should encode 1036 as a10', () => {
|
|
309
|
+
expect(encodeBase36(1036)).toBe('a10');
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it('should encode 2296 as b00 (1000 + 1296)', () => {
|
|
313
|
+
// 1296 = 36 * 36, so b00 = 1000 + 1296 = 2296
|
|
314
|
+
expect(encodeBase36(2296)).toBe('b00');
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it('should encode larger numbers correctly', () => {
|
|
318
|
+
// c00 = 1000 + 2*1296 = 1000 + 2592 = 3592
|
|
319
|
+
expect(encodeBase36(3592)).toBe('c00');
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it('should throw for numbers less than 1000', () => {
|
|
323
|
+
expect(() => encodeBase36(999)).toThrow();
|
|
324
|
+
expect(() => encodeBase36(0)).toThrow();
|
|
325
|
+
expect(() => encodeBase36(500)).toThrow();
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it('should handle boundary between digits', () => {
|
|
329
|
+
// a0z = 1035, a10 = 1036
|
|
330
|
+
expect(encodeBase36(1035)).toBe('a0z');
|
|
331
|
+
expect(encodeBase36(1036)).toBe('a10');
|
|
332
|
+
// azz = 1000 + 35*36 + 35 = 1000 + 1260 + 35 = 2295
|
|
333
|
+
expect(encodeBase36(2295)).toBe('azz');
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
describe('decodeBase36', () => {
|
|
338
|
+
it('should decode a00 as 1000', () => {
|
|
339
|
+
expect(decodeBase36('a00')).toBe(1000);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it('should decode a01 as 1001', () => {
|
|
343
|
+
expect(decodeBase36('a01')).toBe(1001);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it('should decode a0z as 1035', () => {
|
|
347
|
+
expect(decodeBase36('a0z')).toBe(1035);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it('should decode a10 as 1036', () => {
|
|
351
|
+
expect(decodeBase36('a10')).toBe(1036);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it('should decode b00 as 2296', () => {
|
|
355
|
+
expect(decodeBase36('b00')).toBe(2296);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it('should return null for invalid format', () => {
|
|
359
|
+
expect(decodeBase36('001')).toBeNull(); // Starts with digit
|
|
360
|
+
expect(decodeBase36('1a0')).toBeNull(); // Starts with digit
|
|
361
|
+
expect(decodeBase36('ab')).toBeNull(); // Too short
|
|
362
|
+
expect(decodeBase36('abcd')).toBeNull(); // Too long
|
|
363
|
+
expect(decodeBase36('')).toBeNull(); // Empty
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it('should handle uppercase input', () => {
|
|
367
|
+
expect(decodeBase36('A00')).toBe(1000);
|
|
368
|
+
expect(decodeBase36('B00')).toBe(2296);
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it('should be inverse of encodeBase36', () => {
|
|
372
|
+
for (const num of [1000, 1001, 1035, 1036, 2000, 2296, 5000, 10000]) {
|
|
373
|
+
expect(decodeBase36(encodeBase36(num))).toBe(num);
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
describe('isBase36Prefix', () => {
|
|
379
|
+
it('should return true for valid base36 prefixes', () => {
|
|
380
|
+
expect(isBase36Prefix('a00')).toBe(true);
|
|
381
|
+
expect(isBase36Prefix('a01')).toBe(true);
|
|
382
|
+
expect(isBase36Prefix('azz')).toBe(true);
|
|
383
|
+
expect(isBase36Prefix('b00')).toBe(true);
|
|
384
|
+
expect(isBase36Prefix('z99')).toBe(true);
|
|
385
|
+
expect(isBase36Prefix('zzz')).toBe(true);
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
it('should return false for numeric prefixes', () => {
|
|
389
|
+
expect(isBase36Prefix('001')).toBe(false);
|
|
390
|
+
expect(isBase36Prefix('999')).toBe(false);
|
|
391
|
+
expect(isBase36Prefix('123')).toBe(false);
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
it('should return false for invalid formats', () => {
|
|
395
|
+
expect(isBase36Prefix('ab')).toBe(false); // Too short
|
|
396
|
+
expect(isBase36Prefix('abcd')).toBe(false); // Too long
|
|
397
|
+
expect(isBase36Prefix('1ab')).toBe(false); // Starts with digit
|
|
398
|
+
expect(isBase36Prefix('')).toBe(false); // Empty
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
it('should handle uppercase', () => {
|
|
402
|
+
expect(isBase36Prefix('A00')).toBe(true);
|
|
403
|
+
expect(isBase36Prefix('ABC')).toBe(true);
|
|
404
|
+
});
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
describe('parseProjectPrefix', () => {
|
|
408
|
+
it('should parse numeric prefixes', () => {
|
|
409
|
+
expect(parseProjectPrefix('001')).toBe(1);
|
|
410
|
+
expect(parseProjectPrefix('123')).toBe(123);
|
|
411
|
+
expect(parseProjectPrefix('999')).toBe(999);
|
|
412
|
+
expect(parseProjectPrefix('01')).toBe(1);
|
|
413
|
+
expect(parseProjectPrefix('99')).toBe(99);
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
it('should parse base36 prefixes', () => {
|
|
417
|
+
expect(parseProjectPrefix('a00')).toBe(1000);
|
|
418
|
+
expect(parseProjectPrefix('a01')).toBe(1001);
|
|
419
|
+
expect(parseProjectPrefix('b00')).toBe(2296);
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
it('should return null for invalid prefixes', () => {
|
|
423
|
+
expect(parseProjectPrefix('abc-')).toBeNull();
|
|
424
|
+
expect(parseProjectPrefix('1')).toBeNull(); // Too short
|
|
425
|
+
expect(parseProjectPrefix('1234')).toBeNull(); // Too long for numeric
|
|
426
|
+
expect(parseProjectPrefix('')).toBeNull();
|
|
427
|
+
});
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
describe('formatProjectNumber (base36)', () => {
|
|
431
|
+
it('should use numeric format for numbers 1-999', () => {
|
|
432
|
+
expect(formatProjectNumber(1)).toBe('001');
|
|
433
|
+
expect(formatProjectNumber(999)).toBe('999');
|
|
434
|
+
expect(formatProjectNumber(500)).toBe('500');
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
it('should use base36 format for numbers >= 1000', () => {
|
|
438
|
+
expect(formatProjectNumber(1000)).toBe('a00');
|
|
439
|
+
expect(formatProjectNumber(1001)).toBe('a01');
|
|
440
|
+
expect(formatProjectNumber(2296)).toBe('b00');
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
it('should correctly format transition from 999 to 1000', () => {
|
|
444
|
+
expect(formatProjectNumber(999)).toBe('999');
|
|
445
|
+
expect(formatProjectNumber(1000)).toBe('a00');
|
|
446
|
+
});
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
describe('extractProjectNumber (base36)', () => {
|
|
450
|
+
it('should extract base36 project number from path', () => {
|
|
451
|
+
expect(extractProjectNumber('/RAF/a00-my-project')).toBe('a00');
|
|
452
|
+
expect(extractProjectNumber('/RAF/a01-another-project')).toBe('a01');
|
|
453
|
+
expect(extractProjectNumber('/RAF/b00-third-project')).toBe('b00');
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
it('should handle uppercase base36 prefixes', () => {
|
|
457
|
+
expect(extractProjectNumber('/RAF/A00-my-project')).toBe('a00');
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
it('should prefer numeric format when ambiguous', () => {
|
|
461
|
+
// Numeric should be matched first
|
|
462
|
+
expect(extractProjectNumber('/RAF/001-my-project')).toBe('001');
|
|
463
|
+
});
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
describe('extractProjectName (base36)', () => {
|
|
467
|
+
it('should extract project name from base36 numbered path', () => {
|
|
468
|
+
expect(extractProjectName('/RAF/a00-my-project')).toBe('my-project');
|
|
469
|
+
expect(extractProjectName('/RAF/b00-another-project')).toBe('another-project');
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
it('should handle uppercase base36 prefixes', () => {
|
|
473
|
+
expect(extractProjectName('/RAF/A00-my-project')).toBe('my-project');
|
|
474
|
+
});
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
describe('getNextProjectNumber (base36)', () => {
|
|
478
|
+
it('should return next number after base36 projects', () => {
|
|
479
|
+
fs.mkdirSync(path.join(tempDir, 'a00-project1000'));
|
|
480
|
+
expect(getNextProjectNumber(tempDir)).toBe(1001);
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
it('should handle mixed numeric and base36 projects', () => {
|
|
484
|
+
fs.mkdirSync(path.join(tempDir, '999-last-numeric'));
|
|
485
|
+
fs.mkdirSync(path.join(tempDir, 'a00-first-base36'));
|
|
486
|
+
expect(getNextProjectNumber(tempDir)).toBe(1001);
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
it('should find max across mixed formats', () => {
|
|
490
|
+
fs.mkdirSync(path.join(tempDir, '001-first'));
|
|
491
|
+
fs.mkdirSync(path.join(tempDir, 'a05-middle'));
|
|
492
|
+
fs.mkdirSync(path.join(tempDir, '500-later'));
|
|
493
|
+
// a05 = 1005, which is the max
|
|
494
|
+
expect(getNextProjectNumber(tempDir)).toBe(1006);
|
|
495
|
+
});
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
describe('listProjects (base36)', () => {
|
|
499
|
+
it('should list base36 projects with correct number', () => {
|
|
500
|
+
fs.mkdirSync(path.join(tempDir, 'a00-project1000'));
|
|
501
|
+
const projects = listProjects(tempDir);
|
|
502
|
+
expect(projects).toHaveLength(1);
|
|
503
|
+
expect(projects[0]?.number).toBe(1000);
|
|
504
|
+
expect(projects[0]?.name).toBe('project1000');
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
it('should sort mixed numeric and base36 projects correctly', () => {
|
|
508
|
+
fs.mkdirSync(path.join(tempDir, 'a00-project1000'));
|
|
509
|
+
fs.mkdirSync(path.join(tempDir, '001-first'));
|
|
510
|
+
fs.mkdirSync(path.join(tempDir, '999-last-numeric'));
|
|
511
|
+
|
|
512
|
+
const projects = listProjects(tempDir);
|
|
513
|
+
expect(projects).toHaveLength(3);
|
|
514
|
+
expect(projects[0]?.number).toBe(1);
|
|
515
|
+
expect(projects[1]?.number).toBe(999);
|
|
516
|
+
expect(projects[2]?.number).toBe(1000);
|
|
517
|
+
});
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
describe('resolveProjectIdentifier (base36)', () => {
|
|
521
|
+
it('should resolve base36 project by prefix', () => {
|
|
522
|
+
fs.mkdirSync(path.join(tempDir, 'a00-my-project'));
|
|
523
|
+
const result = resolveProjectIdentifier(tempDir, 'a00');
|
|
524
|
+
expect(result).toBe(path.join(tempDir, 'a00-my-project'));
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
it('should resolve base36 project by numeric value', () => {
|
|
528
|
+
fs.mkdirSync(path.join(tempDir, 'a00-my-project'));
|
|
529
|
+
const result = resolveProjectIdentifier(tempDir, '1000');
|
|
530
|
+
expect(result).toBe(path.join(tempDir, 'a00-my-project'));
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
it('should resolve base36 project by name', () => {
|
|
534
|
+
fs.mkdirSync(path.join(tempDir, 'a00-my-project'));
|
|
535
|
+
const result = resolveProjectIdentifier(tempDir, 'my-project');
|
|
536
|
+
expect(result).toBe(path.join(tempDir, 'a00-my-project'));
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
it('should handle mixed projects correctly', () => {
|
|
540
|
+
fs.mkdirSync(path.join(tempDir, '001-first'));
|
|
541
|
+
fs.mkdirSync(path.join(tempDir, 'a00-second'));
|
|
542
|
+
|
|
543
|
+
expect(resolveProjectIdentifier(tempDir, '1')).toBe(path.join(tempDir, '001-first'));
|
|
544
|
+
expect(resolveProjectIdentifier(tempDir, '1000')).toBe(path.join(tempDir, 'a00-second'));
|
|
545
|
+
expect(resolveProjectIdentifier(tempDir, 'a00')).toBe(path.join(tempDir, 'a00-second'));
|
|
546
|
+
expect(resolveProjectIdentifier(tempDir, 'first')).toBe(path.join(tempDir, '001-first'));
|
|
547
|
+
expect(resolveProjectIdentifier(tempDir, 'second')).toBe(path.join(tempDir, 'a00-second'));
|
|
548
|
+
});
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
describe('resolveProjectIdentifier (full folder name)', () => {
|
|
552
|
+
it('should resolve project by full numeric folder name', () => {
|
|
553
|
+
fs.mkdirSync(path.join(tempDir, '001-fix-stuff'));
|
|
554
|
+
const result = resolveProjectIdentifier(tempDir, '001-fix-stuff');
|
|
555
|
+
expect(result).toBe(path.join(tempDir, '001-fix-stuff'));
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
it('should resolve project by full base36 folder name', () => {
|
|
559
|
+
fs.mkdirSync(path.join(tempDir, 'a01-important-project'));
|
|
560
|
+
const result = resolveProjectIdentifier(tempDir, 'a01-important-project');
|
|
561
|
+
expect(result).toBe(path.join(tempDir, 'a01-important-project'));
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
it('should resolve project with hyphens in name', () => {
|
|
565
|
+
fs.mkdirSync(path.join(tempDir, '001-my-cool-project'));
|
|
566
|
+
const result = resolveProjectIdentifier(tempDir, '001-my-cool-project');
|
|
567
|
+
expect(result).toBe(path.join(tempDir, '001-my-cool-project'));
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
it('should return null for wrong prefix with correct name format', () => {
|
|
571
|
+
fs.mkdirSync(path.join(tempDir, '001-correct-name'));
|
|
572
|
+
// Full folder format but wrong prefix - should NOT match
|
|
573
|
+
const result = resolveProjectIdentifier(tempDir, '002-correct-name');
|
|
574
|
+
expect(result).toBeNull();
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
it('should return null for correct prefix with wrong name format', () => {
|
|
578
|
+
fs.mkdirSync(path.join(tempDir, '001-correct-name'));
|
|
579
|
+
// Full folder format but wrong name - should NOT match
|
|
580
|
+
const result = resolveProjectIdentifier(tempDir, '001-wrong-name');
|
|
581
|
+
expect(result).toBeNull();
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
it('should handle case-insensitive folder matching', () => {
|
|
585
|
+
fs.mkdirSync(path.join(tempDir, 'A01-My-Project'));
|
|
586
|
+
// Lowercase identifier should still match
|
|
587
|
+
const result = resolveProjectIdentifier(tempDir, 'a01-my-project');
|
|
588
|
+
expect(result).toBe(path.join(tempDir, 'A01-My-Project'));
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
it('should resolve 2-digit prefix full folder names', () => {
|
|
592
|
+
fs.mkdirSync(path.join(tempDir, '01-first-project'));
|
|
593
|
+
const result = resolveProjectIdentifier(tempDir, '01-first-project');
|
|
594
|
+
expect(result).toBe(path.join(tempDir, '01-first-project'));
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
it('should still resolve by name alone after full folder check', () => {
|
|
598
|
+
fs.mkdirSync(path.join(tempDir, '001-my-project'));
|
|
599
|
+
// Name-only should still work via fallback
|
|
600
|
+
const result = resolveProjectIdentifier(tempDir, 'my-project');
|
|
601
|
+
expect(result).toBe(path.join(tempDir, '001-my-project'));
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
it('should still resolve by number alone after full folder check', () => {
|
|
605
|
+
fs.mkdirSync(path.join(tempDir, '003-my-project'));
|
|
606
|
+
// Number-only should still work via fallback
|
|
607
|
+
const result = resolveProjectIdentifier(tempDir, '3');
|
|
608
|
+
expect(result).toBe(path.join(tempDir, '003-my-project'));
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
it('should prefer exact full folder match over name-only match', () => {
|
|
612
|
+
// Create two projects where one's name is a full folder format
|
|
613
|
+
fs.mkdirSync(path.join(tempDir, '001-project'));
|
|
614
|
+
fs.mkdirSync(path.join(tempDir, '002-001-project')); // Name portion is "001-project"
|
|
615
|
+
|
|
616
|
+
// "001-project" as full folder name should match the first project
|
|
617
|
+
const result = resolveProjectIdentifier(tempDir, '001-project');
|
|
618
|
+
expect(result).toBe(path.join(tempDir, '001-project'));
|
|
619
|
+
});
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
describe('resolveProjectIdentifier (case-insensitive name matching)', () => {
|
|
623
|
+
it('should match project name case-insensitively', () => {
|
|
624
|
+
fs.mkdirSync(path.join(tempDir, '001-fix-double-summary-headers'));
|
|
625
|
+
|
|
626
|
+
// All these should match
|
|
627
|
+
expect(resolveProjectIdentifier(tempDir, 'fix-double-summary-headers')).toBe(
|
|
628
|
+
path.join(tempDir, '001-fix-double-summary-headers')
|
|
629
|
+
);
|
|
630
|
+
expect(resolveProjectIdentifier(tempDir, 'Fix-Double-Summary-Headers')).toBe(
|
|
631
|
+
path.join(tempDir, '001-fix-double-summary-headers')
|
|
632
|
+
);
|
|
633
|
+
expect(resolveProjectIdentifier(tempDir, 'FIX-DOUBLE-SUMMARY-HEADERS')).toBe(
|
|
634
|
+
path.join(tempDir, '001-fix-double-summary-headers')
|
|
635
|
+
);
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
it('should match mixed case project name', () => {
|
|
639
|
+
fs.mkdirSync(path.join(tempDir, '001-MyProject'));
|
|
640
|
+
|
|
641
|
+
expect(resolveProjectIdentifier(tempDir, 'myproject')).toBe(
|
|
642
|
+
path.join(tempDir, '001-MyProject')
|
|
643
|
+
);
|
|
644
|
+
expect(resolveProjectIdentifier(tempDir, 'MYPROJECT')).toBe(
|
|
645
|
+
path.join(tempDir, '001-MyProject')
|
|
646
|
+
);
|
|
647
|
+
expect(resolveProjectIdentifier(tempDir, 'MyProject')).toBe(
|
|
648
|
+
path.join(tempDir, '001-MyProject')
|
|
649
|
+
);
|
|
650
|
+
});
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
describe('resolveProjectIdentifierWithDetails', () => {
|
|
654
|
+
it('should return path for unique name match', () => {
|
|
655
|
+
fs.mkdirSync(path.join(tempDir, '001-my-project'));
|
|
656
|
+
|
|
657
|
+
const result = resolveProjectIdentifierWithDetails(tempDir, 'my-project');
|
|
658
|
+
expect(result.path).toBe(path.join(tempDir, '001-my-project'));
|
|
659
|
+
expect(result.error).toBeUndefined();
|
|
660
|
+
expect(result.matches).toBeUndefined();
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
it('should return ambiguous error for multiple projects with same name', () => {
|
|
664
|
+
fs.mkdirSync(path.join(tempDir, '001-my-project'));
|
|
665
|
+
fs.mkdirSync(path.join(tempDir, '002-my-project'));
|
|
666
|
+
|
|
667
|
+
const result = resolveProjectIdentifierWithDetails(tempDir, 'my-project');
|
|
668
|
+
expect(result.path).toBeNull();
|
|
669
|
+
expect(result.error).toBe('ambiguous');
|
|
670
|
+
expect(result.matches).toHaveLength(2);
|
|
671
|
+
expect(result.matches?.[0]?.folder).toBe('001-my-project');
|
|
672
|
+
expect(result.matches?.[1]?.folder).toBe('002-my-project');
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
it('should return ambiguous error for case-insensitive duplicate names', () => {
|
|
676
|
+
fs.mkdirSync(path.join(tempDir, '001-MyProject'));
|
|
677
|
+
fs.mkdirSync(path.join(tempDir, '002-myproject'));
|
|
678
|
+
|
|
679
|
+
const result = resolveProjectIdentifierWithDetails(tempDir, 'myproject');
|
|
680
|
+
expect(result.path).toBeNull();
|
|
681
|
+
expect(result.error).toBe('ambiguous');
|
|
682
|
+
expect(result.matches).toHaveLength(2);
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
it('should return not_found error for non-existent project', () => {
|
|
686
|
+
fs.mkdirSync(path.join(tempDir, '001-existing-project'));
|
|
687
|
+
|
|
688
|
+
const result = resolveProjectIdentifierWithDetails(tempDir, 'non-existent');
|
|
689
|
+
expect(result.path).toBeNull();
|
|
690
|
+
expect(result.error).toBe('not_found');
|
|
691
|
+
expect(result.matches).toBeUndefined();
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
it('should return not_found error for non-existent directory', () => {
|
|
695
|
+
const result = resolveProjectIdentifierWithDetails('/non/existent/path', 'project');
|
|
696
|
+
expect(result.path).toBeNull();
|
|
697
|
+
expect(result.error).toBe('not_found');
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
it('should resolve by number even with duplicate names', () => {
|
|
701
|
+
fs.mkdirSync(path.join(tempDir, '001-my-project'));
|
|
702
|
+
fs.mkdirSync(path.join(tempDir, '002-my-project'));
|
|
703
|
+
|
|
704
|
+
// By number should still work unambiguously
|
|
705
|
+
const result1 = resolveProjectIdentifierWithDetails(tempDir, '1');
|
|
706
|
+
expect(result1.path).toBe(path.join(tempDir, '001-my-project'));
|
|
707
|
+
expect(result1.error).toBeUndefined();
|
|
708
|
+
|
|
709
|
+
const result2 = resolveProjectIdentifierWithDetails(tempDir, '2');
|
|
710
|
+
expect(result2.path).toBe(path.join(tempDir, '002-my-project'));
|
|
711
|
+
expect(result2.error).toBeUndefined();
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
it('should resolve by full folder name even with duplicate names', () => {
|
|
715
|
+
fs.mkdirSync(path.join(tempDir, '001-my-project'));
|
|
716
|
+
fs.mkdirSync(path.join(tempDir, '002-my-project'));
|
|
717
|
+
|
|
718
|
+
// Full folder name should work unambiguously
|
|
719
|
+
const result1 = resolveProjectIdentifierWithDetails(tempDir, '001-my-project');
|
|
720
|
+
expect(result1.path).toBe(path.join(tempDir, '001-my-project'));
|
|
721
|
+
expect(result1.error).toBeUndefined();
|
|
722
|
+
|
|
723
|
+
const result2 = resolveProjectIdentifierWithDetails(tempDir, '002-my-project');
|
|
724
|
+
expect(result2.path).toBe(path.join(tempDir, '002-my-project'));
|
|
725
|
+
expect(result2.error).toBeUndefined();
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
it('should sort matches by project number', () => {
|
|
729
|
+
fs.mkdirSync(path.join(tempDir, '005-duplicate'));
|
|
730
|
+
fs.mkdirSync(path.join(tempDir, '001-duplicate'));
|
|
731
|
+
fs.mkdirSync(path.join(tempDir, '003-duplicate'));
|
|
732
|
+
|
|
733
|
+
const result = resolveProjectIdentifierWithDetails(tempDir, 'duplicate');
|
|
734
|
+
expect(result.error).toBe('ambiguous');
|
|
735
|
+
expect(result.matches).toHaveLength(3);
|
|
736
|
+
expect(result.matches?.[0]?.number).toBe(1);
|
|
737
|
+
expect(result.matches?.[1]?.number).toBe(3);
|
|
738
|
+
expect(result.matches?.[2]?.number).toBe(5);
|
|
739
|
+
});
|
|
740
|
+
});
|
|
741
|
+
});
|