openspecui 0.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/.gitmodules +3 -0
- package/CHAT.md +3 -0
- package/package.json +12 -0
- package/references/openspec/.changeset/README.md +6 -0
- package/references/openspec/.changeset/config.json +12 -0
- package/references/openspec/.coderabbit.yaml +11 -0
- package/references/openspec/.devcontainer/README.md +92 -0
- package/references/openspec/.devcontainer/devcontainer.json +68 -0
- package/references/openspec/.github/CODEOWNERS +2 -0
- package/references/openspec/.github/workflows/ci.yml +222 -0
- package/references/openspec/.github/workflows/release-prepare.yml +50 -0
- package/references/openspec/AGENTS.md +18 -0
- package/references/openspec/CHANGELOG.md +205 -0
- package/references/openspec/LICENSE +22 -0
- package/references/openspec/README.md +374 -0
- package/references/openspec/assets/openspec_dashboard.png +0 -0
- package/references/openspec/assets/openspec_pixel_dark.svg +89 -0
- package/references/openspec/assets/openspec_pixel_light.svg +89 -0
- package/references/openspec/bin/openspec.js +3 -0
- package/references/openspec/build.js +31 -0
- package/references/openspec/openspec/AGENTS.md +454 -0
- package/references/openspec/openspec/changes/IMPLEMENTATION_ORDER.md +68 -0
- package/references/openspec/openspec/changes/add-antigravity-support/proposal.md +11 -0
- package/references/openspec/openspec/changes/add-antigravity-support/specs/cli-init/spec.md +9 -0
- package/references/openspec/openspec/changes/add-antigravity-support/specs/cli-update/spec.md +8 -0
- package/references/openspec/openspec/changes/add-antigravity-support/tasks.md +12 -0
- package/references/openspec/openspec/changes/add-scaffold-command/proposal.md +11 -0
- package/references/openspec/openspec/changes/add-scaffold-command/specs/cli-scaffold/spec.md +36 -0
- package/references/openspec/openspec/changes/add-scaffold-command/tasks.md +12 -0
- package/references/openspec/openspec/changes/archive/2025-01-11-add-update-command/design.md +86 -0
- package/references/openspec/openspec/changes/archive/2025-01-11-add-update-command/proposal.md +29 -0
- package/references/openspec/openspec/changes/archive/2025-01-11-add-update-command/specs/cli-update/spec.md +59 -0
- package/references/openspec/openspec/changes/archive/2025-01-11-add-update-command/tasks.md +20 -0
- package/references/openspec/openspec/changes/archive/2025-01-13-add-list-command/proposal.md +20 -0
- package/references/openspec/openspec/changes/archive/2025-01-13-add-list-command/specs/cli-list/spec.md +69 -0
- package/references/openspec/openspec/changes/archive/2025-01-13-add-list-command/tasks.md +26 -0
- package/references/openspec/openspec/changes/archive/2025-08-05-initialize-typescript-project/design.md +64 -0
- package/references/openspec/openspec/changes/archive/2025-08-05-initialize-typescript-project/proposal.md +18 -0
- package/references/openspec/openspec/changes/archive/2025-08-05-initialize-typescript-project/tasks.md +25 -0
- package/references/openspec/openspec/changes/archive/2025-08-06-add-init-command/design.md +104 -0
- package/references/openspec/openspec/changes/archive/2025-08-06-add-init-command/proposal.md +30 -0
- package/references/openspec/openspec/changes/archive/2025-08-06-add-init-command/specs/cli-init/spec.md +148 -0
- package/references/openspec/openspec/changes/archive/2025-08-06-add-init-command/tasks.md +38 -0
- package/references/openspec/openspec/changes/archive/2025-08-06-adopt-future-state-storage/proposal.md +24 -0
- package/references/openspec/openspec/changes/archive/2025-08-06-adopt-future-state-storage/specs/openspec-conventions/spec.md +120 -0
- package/references/openspec/openspec/changes/archive/2025-08-06-adopt-future-state-storage/tasks.md +38 -0
- package/references/openspec/openspec/changes/archive/2025-08-11-add-complexity-guidelines/proposal.md +13 -0
- package/references/openspec/openspec/changes/archive/2025-08-11-add-complexity-guidelines/specs/openspec-docs/README.md +472 -0
- package/references/openspec/openspec/changes/archive/2025-08-11-add-complexity-guidelines/tasks.md +9 -0
- package/references/openspec/openspec/changes/archive/2025-08-13-add-archive-command/proposal.md +15 -0
- package/references/openspec/openspec/changes/archive/2025-08-13-add-archive-command/specs/cli-archive/spec.md +111 -0
- package/references/openspec/openspec/changes/archive/2025-08-13-add-archive-command/tasks.md +44 -0
- package/references/openspec/openspec/changes/archive/2025-08-13-add-diff-command/proposal.md +19 -0
- package/references/openspec/openspec/changes/archive/2025-08-13-add-diff-command/specs/cli-diff/spec.md +77 -0
- package/references/openspec/openspec/changes/archive/2025-08-13-add-diff-command/tasks.md +23 -0
- package/references/openspec/openspec/changes/archive/2025-08-19-add-change-commands/design.md +56 -0
- package/references/openspec/openspec/changes/archive/2025-08-19-add-change-commands/proposal.md +17 -0
- package/references/openspec/openspec/changes/archive/2025-08-19-add-change-commands/specs/cli-change/spec.md +48 -0
- package/references/openspec/openspec/changes/archive/2025-08-19-add-change-commands/specs/cli-list/spec.md +12 -0
- package/references/openspec/openspec/changes/archive/2025-08-19-add-change-commands/tasks.md +34 -0
- package/references/openspec/openspec/changes/archive/2025-08-19-add-interactive-show-command/proposal.md +20 -0
- package/references/openspec/openspec/changes/archive/2025-08-19-add-interactive-show-command/specs/cli-change/spec.md +23 -0
- package/references/openspec/openspec/changes/archive/2025-08-19-add-interactive-show-command/specs/cli-show/spec.md +83 -0
- package/references/openspec/openspec/changes/archive/2025-08-19-add-interactive-show-command/specs/cli-spec/spec.md +23 -0
- package/references/openspec/openspec/changes/archive/2025-08-19-add-interactive-show-command/tasks.md +142 -0
- package/references/openspec/openspec/changes/archive/2025-08-19-add-skip-specs-archive-option/proposal.md +13 -0
- package/references/openspec/openspec/changes/archive/2025-08-19-add-skip-specs-archive-option/specs/cli-archive/spec.md +191 -0
- package/references/openspec/openspec/changes/archive/2025-08-19-add-skip-specs-archive-option/tasks.md +57 -0
- package/references/openspec/openspec/changes/archive/2025-08-19-add-spec-commands/design.md +45 -0
- package/references/openspec/openspec/changes/archive/2025-08-19-add-spec-commands/proposal.md +19 -0
- package/references/openspec/openspec/changes/archive/2025-08-19-add-spec-commands/specs/cli-spec/spec.md +43 -0
- package/references/openspec/openspec/changes/archive/2025-08-19-add-spec-commands/tasks.md +22 -0
- package/references/openspec/openspec/changes/archive/2025-08-19-add-zod-validation/design.md +104 -0
- package/references/openspec/openspec/changes/archive/2025-08-19-add-zod-validation/proposal.md +22 -0
- package/references/openspec/openspec/changes/archive/2025-08-19-add-zod-validation/specs/cli-archive/spec.md +18 -0
- package/references/openspec/openspec/changes/archive/2025-08-19-add-zod-validation/specs/cli-diff/spec.md +12 -0
- package/references/openspec/openspec/changes/archive/2025-08-19-add-zod-validation/tasks.md +59 -0
- package/references/openspec/openspec/changes/archive/2025-08-19-adopt-delta-based-changes/proposal.md +93 -0
- package/references/openspec/openspec/changes/archive/2025-08-19-adopt-delta-based-changes/specs/cli-archive/spec.md +48 -0
- package/references/openspec/openspec/changes/archive/2025-08-19-adopt-delta-based-changes/specs/cli-diff/spec.md +45 -0
- package/references/openspec/openspec/changes/archive/2025-08-19-adopt-delta-based-changes/specs/openspec-conventions/spec.md +101 -0
- package/references/openspec/openspec/changes/archive/2025-08-19-adopt-delta-based-changes/tasks.md +55 -0
- package/references/openspec/openspec/changes/archive/2025-08-19-adopt-verb-noun-cli-structure/design.md +19 -0
- package/references/openspec/openspec/changes/archive/2025-08-19-adopt-verb-noun-cli-structure/proposal.md +67 -0
- package/references/openspec/openspec/changes/archive/2025-08-19-adopt-verb-noun-cli-structure/specs/cli-list/spec.md +57 -0
- package/references/openspec/openspec/changes/archive/2025-08-19-adopt-verb-noun-cli-structure/specs/openspec-conventions/spec.md +23 -0
- package/references/openspec/openspec/changes/archive/2025-08-19-adopt-verb-noun-cli-structure/tasks.md +27 -0
- package/references/openspec/openspec/changes/archive/2025-08-19-bulk-validation-interactive-selection/proposal.md +20 -0
- package/references/openspec/openspec/changes/archive/2025-08-19-bulk-validation-interactive-selection/specs/cli-change/spec.md +22 -0
- package/references/openspec/openspec/changes/archive/2025-08-19-bulk-validation-interactive-selection/specs/cli-spec/spec.md +23 -0
- package/references/openspec/openspec/changes/archive/2025-08-19-bulk-validation-interactive-selection/specs/cli-validate/spec.md +149 -0
- package/references/openspec/openspec/changes/archive/2025-08-19-bulk-validation-interactive-selection/tasks.md +81 -0
- package/references/openspec/openspec/changes/archive/2025-08-19-fix-update-tool-selection/proposal.md +40 -0
- package/references/openspec/openspec/changes/archive/2025-08-19-fix-update-tool-selection/specs/cli-update/spec.md +23 -0
- package/references/openspec/openspec/changes/archive/2025-08-19-fix-update-tool-selection/tasks.md +21 -0
- package/references/openspec/openspec/changes/archive/2025-08-19-improve-validate-error-messages/proposal.md +25 -0
- package/references/openspec/openspec/changes/archive/2025-08-19-improve-validate-error-messages/specs/cli-validate/spec.md +55 -0
- package/references/openspec/openspec/changes/archive/2025-08-19-improve-validate-error-messages/tasks.md +21 -0
- package/references/openspec/openspec/changes/archive/2025-08-19-structured-spec-format/proposal.md +36 -0
- package/references/openspec/openspec/changes/archive/2025-08-19-structured-spec-format/specs/openspec-conventions/spec.md +192 -0
- package/references/openspec/openspec/changes/archive/2025-08-19-structured-spec-format/tasks.md +19 -0
- package/references/openspec/openspec/changes/archive/2025-09-12-add-view-dashboard-command/proposal.md +38 -0
- package/references/openspec/openspec/changes/archive/2025-09-12-add-view-dashboard-command/specs/cli-view/spec.md +109 -0
- package/references/openspec/openspec/changes/archive/2025-09-12-add-view-dashboard-command/tasks.md +47 -0
- package/references/openspec/openspec/changes/archive/2025-09-29-add-agents-md-config/proposal.md +28 -0
- package/references/openspec/openspec/changes/archive/2025-09-29-add-agents-md-config/specs/cli-init/spec.md +71 -0
- package/references/openspec/openspec/changes/archive/2025-09-29-add-agents-md-config/specs/cli-update/spec.md +41 -0
- package/references/openspec/openspec/changes/archive/2025-09-29-add-agents-md-config/tasks.md +17 -0
- package/references/openspec/openspec/changes/archive/2025-09-29-add-multi-agent-init/proposal.md +35 -0
- package/references/openspec/openspec/changes/archive/2025-09-29-add-multi-agent-init/specs/cli-init/spec.md +45 -0
- package/references/openspec/openspec/changes/archive/2025-09-29-add-multi-agent-init/tasks.md +16 -0
- package/references/openspec/openspec/changes/archive/2025-09-29-add-slash-command-support/proposal.md +119 -0
- package/references/openspec/openspec/changes/archive/2025-09-29-add-slash-command-support/specs/cli-init/spec.md +21 -0
- package/references/openspec/openspec/changes/archive/2025-09-29-add-slash-command-support/specs/cli-update/spec.md +22 -0
- package/references/openspec/openspec/changes/archive/2025-09-29-add-slash-command-support/tasks.md +20 -0
- package/references/openspec/openspec/changes/archive/2025-09-29-improve-cli-e2e-plan/proposal.md +19 -0
- package/references/openspec/openspec/changes/archive/2025-09-29-improve-cli-e2e-plan/tasks.md +9 -0
- package/references/openspec/openspec/changes/archive/2025-09-29-improve-deterministic-tests/proposal.md +78 -0
- package/references/openspec/openspec/changes/archive/2025-09-29-improve-deterministic-tests/tasks.md +25 -0
- package/references/openspec/openspec/changes/archive/2025-09-29-improve-init-onboarding/proposal.md +13 -0
- package/references/openspec/openspec/changes/archive/2025-09-29-improve-init-onboarding/specs/cli-init/spec.md +92 -0
- package/references/openspec/openspec/changes/archive/2025-09-29-improve-init-onboarding/tasks.md +12 -0
- package/references/openspec/openspec/changes/archive/2025-09-29-remove-diff-command/proposal.md +81 -0
- package/references/openspec/openspec/changes/archive/2025-09-29-remove-diff-command/tasks.md +37 -0
- package/references/openspec/openspec/changes/archive/2025-09-29-sort-active-changes-by-progress/proposal.md +25 -0
- package/references/openspec/openspec/changes/archive/2025-09-29-sort-active-changes-by-progress/specs/cli-view/spec.md +9 -0
- package/references/openspec/openspec/changes/archive/2025-09-29-sort-active-changes-by-progress/tasks.md +8 -0
- package/references/openspec/openspec/changes/archive/2025-09-29-update-agent-file-name/proposal.md +29 -0
- package/references/openspec/openspec/changes/archive/2025-09-29-update-agent-file-name/specs/cli-init/spec.md +40 -0
- package/references/openspec/openspec/changes/archive/2025-09-29-update-agent-file-name/specs/cli-update/spec.md +22 -0
- package/references/openspec/openspec/changes/archive/2025-09-29-update-agent-file-name/specs/openspec-conventions/spec.md +27 -0
- package/references/openspec/openspec/changes/archive/2025-09-29-update-agent-file-name/tasks.md +22 -0
- package/references/openspec/openspec/changes/archive/2025-09-29-update-agent-instructions/design.md +130 -0
- package/references/openspec/openspec/changes/archive/2025-09-29-update-agent-instructions/proposal.md +117 -0
- package/references/openspec/openspec/changes/archive/2025-09-29-update-agent-instructions/tasks.md +69 -0
- package/references/openspec/openspec/changes/archive/2025-09-29-update-markdown-parser-crlf/proposal.md +19 -0
- package/references/openspec/openspec/changes/archive/2025-09-29-update-markdown-parser-crlf/specs/cli-validate/spec.md +9 -0
- package/references/openspec/openspec/changes/archive/2025-09-29-update-markdown-parser-crlf/tasks.md +11 -0
- package/references/openspec/openspec/changes/archive/2025-10-14-add-codex-slash-command-support/proposal.md +25 -0
- package/references/openspec/openspec/changes/archive/2025-10-14-add-codex-slash-command-support/specs/cli-init/spec.md +56 -0
- package/references/openspec/openspec/changes/archive/2025-10-14-add-codex-slash-command-support/specs/cli-update/spec.md +41 -0
- package/references/openspec/openspec/changes/archive/2025-10-14-add-codex-slash-command-support/tasks.md +19 -0
- package/references/openspec/openspec/changes/archive/2025-10-14-add-github-copilot-prompts/proposal.md +25 -0
- package/references/openspec/openspec/changes/archive/2025-10-14-add-github-copilot-prompts/specs/cli-init/spec.md +48 -0
- package/references/openspec/openspec/changes/archive/2025-10-14-add-github-copilot-prompts/specs/cli-update/spec.md +48 -0
- package/references/openspec/openspec/changes/archive/2025-10-14-add-github-copilot-prompts/tasks.md +30 -0
- package/references/openspec/openspec/changes/archive/2025-10-14-add-kilocode-workflows/proposal.md +17 -0
- package/references/openspec/openspec/changes/archive/2025-10-14-add-kilocode-workflows/specs/cli-init/spec.md +43 -0
- package/references/openspec/openspec/changes/archive/2025-10-14-add-kilocode-workflows/specs/cli-update/spec.md +27 -0
- package/references/openspec/openspec/changes/archive/2025-10-14-add-kilocode-workflows/tasks.md +15 -0
- package/references/openspec/openspec/changes/archive/2025-10-14-add-non-interactive-init-options/proposal.md +12 -0
- package/references/openspec/openspec/changes/archive/2025-10-14-add-non-interactive-init-options/specs/cli-init/spec.md +39 -0
- package/references/openspec/openspec/changes/archive/2025-10-14-add-non-interactive-init-options/tasks.md +17 -0
- package/references/openspec/openspec/changes/archive/2025-10-14-add-windsurf-workflows/proposal.md +17 -0
- package/references/openspec/openspec/changes/archive/2025-10-14-add-windsurf-workflows/specs/cli-init/spec.md +42 -0
- package/references/openspec/openspec/changes/archive/2025-10-14-add-windsurf-workflows/specs/cli-update/spec.md +27 -0
- package/references/openspec/openspec/changes/archive/2025-10-14-add-windsurf-workflows/tasks.md +17 -0
- package/references/openspec/openspec/changes/archive/2025-10-14-enhance-validation-error-messages/proposal.md +12 -0
- package/references/openspec/openspec/changes/archive/2025-10-14-enhance-validation-error-messages/specs/cli-validate/spec.md +39 -0
- package/references/openspec/openspec/changes/archive/2025-10-14-enhance-validation-error-messages/tasks.md +12 -0
- package/references/openspec/openspec/changes/archive/2025-10-14-improve-agent-instruction-usability/proposal.md +12 -0
- package/references/openspec/openspec/changes/archive/2025-10-14-improve-agent-instruction-usability/specs/docs-agent-instructions/spec.md +33 -0
- package/references/openspec/openspec/changes/archive/2025-10-14-improve-agent-instruction-usability/tasks.md +11 -0
- package/references/openspec/openspec/changes/archive/2025-10-14-slim-root-agents-file/proposal.md +13 -0
- package/references/openspec/openspec/changes/archive/2025-10-14-slim-root-agents-file/tasks.md +15 -0
- package/references/openspec/openspec/changes/archive/2025-10-14-update-cli-init-enter-selection/proposal.md +14 -0
- package/references/openspec/openspec/changes/archive/2025-10-14-update-cli-init-enter-selection/specs/cli-init/spec.md +10 -0
- package/references/openspec/openspec/changes/archive/2025-10-14-update-cli-init-enter-selection/tasks.md +8 -0
- package/references/openspec/openspec/changes/archive/2025-10-14-update-cli-init-root-agents/proposal.md +15 -0
- package/references/openspec/openspec/changes/archive/2025-10-14-update-cli-init-root-agents/specs/cli-init/spec.md +32 -0
- package/references/openspec/openspec/changes/archive/2025-10-14-update-cli-init-root-agents/specs/cli-update/spec.md +10 -0
- package/references/openspec/openspec/changes/archive/2025-10-14-update-cli-init-root-agents/tasks.md +11 -0
- package/references/openspec/openspec/changes/archive/2025-10-14-update-release-automation/proposal.md +49 -0
- package/references/openspec/openspec/changes/archive/2025-10-14-update-release-automation/tasks.md +12 -0
- package/references/openspec/openspec/changes/archive/2025-10-22-add-archive-command-arguments/proposal.md +17 -0
- package/references/openspec/openspec/changes/archive/2025-10-22-add-archive-command-arguments/specs/cli-update/spec.md +32 -0
- package/references/openspec/openspec/changes/archive/2025-10-22-add-archive-command-arguments/tasks.md +15 -0
- package/references/openspec/openspec/changes/archive/2025-10-22-add-cline-support/proposal.md +15 -0
- package/references/openspec/openspec/changes/archive/2025-10-22-add-cline-support/specs/cli-init/spec.md +97 -0
- package/references/openspec/openspec/changes/archive/2025-10-22-add-cline-support/tasks.md +19 -0
- package/references/openspec/openspec/changes/archive/2025-10-22-add-crush-support/proposal.md +13 -0
- package/references/openspec/openspec/changes/archive/2025-10-22-add-crush-support/specs/cli-init/spec.md +67 -0
- package/references/openspec/openspec/changes/archive/2025-10-22-add-crush-support/tasks.md +7 -0
- package/references/openspec/openspec/changes/archive/2025-10-22-add-factory-slash-commands/proposal.md +12 -0
- package/references/openspec/openspec/changes/archive/2025-10-22-add-factory-slash-commands/specs/cli-init/spec.md +54 -0
- package/references/openspec/openspec/changes/archive/2025-10-22-add-factory-slash-commands/specs/cli-update/spec.md +54 -0
- package/references/openspec/openspec/changes/archive/2025-10-22-add-factory-slash-commands/tasks.md +11 -0
- package/references/openspec/openspec/changes/fix-cline-workflows-implementation/proposal.md +13 -0
- package/references/openspec/openspec/changes/fix-cline-workflows-implementation/specs/cli-init/spec.md +11 -0
- package/references/openspec/openspec/changes/fix-cline-workflows-implementation/tasks.md +13 -0
- package/references/openspec/openspec/changes/make-validation-scope-aware/proposal.md +12 -0
- package/references/openspec/openspec/changes/make-validation-scope-aware/specs/cli-validate/spec.md +25 -0
- package/references/openspec/openspec/changes/make-validation-scope-aware/tasks.md +16 -0
- package/references/openspec/openspec/project.md +53 -0
- package/references/openspec/openspec/specs/cli-archive/spec.md +210 -0
- package/references/openspec/openspec/specs/cli-change/spec.md +91 -0
- package/references/openspec/openspec/specs/cli-init/spec.md +311 -0
- package/references/openspec/openspec/specs/cli-list/spec.md +103 -0
- package/references/openspec/openspec/specs/cli-show/spec.md +85 -0
- package/references/openspec/openspec/specs/cli-spec/spec.md +87 -0
- package/references/openspec/openspec/specs/cli-update/spec.md +190 -0
- package/references/openspec/openspec/specs/cli-validate/spec.md +218 -0
- package/references/openspec/openspec/specs/cli-view/spec.md +105 -0
- package/references/openspec/openspec/specs/docs-agent-instructions/spec.md +38 -0
- package/references/openspec/openspec/specs/openspec-conventions/spec.md +474 -0
- package/references/openspec/openspec-parallel-merge-plan.md +98 -0
- package/references/openspec/package.json +73 -0
- package/references/openspec/pnpm-lock.yaml +2324 -0
- package/references/openspec/scripts/pack-version-check.mjs +111 -0
- package/references/openspec/src/cli/index.ts +253 -0
- package/references/openspec/src/commands/change.ts +291 -0
- package/references/openspec/src/commands/show.ts +139 -0
- package/references/openspec/src/commands/spec.ts +250 -0
- package/references/openspec/src/commands/validate.ts +305 -0
- package/references/openspec/src/core/archive.ts +606 -0
- package/references/openspec/src/core/config.ts +41 -0
- package/references/openspec/src/core/configurators/agents.ts +23 -0
- package/references/openspec/src/core/configurators/base.ts +6 -0
- package/references/openspec/src/core/configurators/claude.ts +23 -0
- package/references/openspec/src/core/configurators/cline.ts +23 -0
- package/references/openspec/src/core/configurators/codebuddy.ts +24 -0
- package/references/openspec/src/core/configurators/costrict.ts +23 -0
- package/references/openspec/src/core/configurators/iflow.ts +23 -0
- package/references/openspec/src/core/configurators/qoder.ts +53 -0
- package/references/openspec/src/core/configurators/qwen.ts +47 -0
- package/references/openspec/src/core/configurators/registry.ts +49 -0
- package/references/openspec/src/core/configurators/slash/amazon-q.ts +51 -0
- package/references/openspec/src/core/configurators/slash/antigravity.ts +28 -0
- package/references/openspec/src/core/configurators/slash/auggie.ts +37 -0
- package/references/openspec/src/core/configurators/slash/base.ts +95 -0
- package/references/openspec/src/core/configurators/slash/claude.ts +42 -0
- package/references/openspec/src/core/configurators/slash/cline.ts +27 -0
- package/references/openspec/src/core/configurators/slash/codebuddy.ts +43 -0
- package/references/openspec/src/core/configurators/slash/codex.ts +126 -0
- package/references/openspec/src/core/configurators/slash/costrict.ts +36 -0
- package/references/openspec/src/core/configurators/slash/crush.ts +42 -0
- package/references/openspec/src/core/configurators/slash/cursor.ts +42 -0
- package/references/openspec/src/core/configurators/slash/factory.ts +41 -0
- package/references/openspec/src/core/configurators/slash/gemini.ts +27 -0
- package/references/openspec/src/core/configurators/slash/github-copilot.ts +39 -0
- package/references/openspec/src/core/configurators/slash/iflow.ts +42 -0
- package/references/openspec/src/core/configurators/slash/kilocode.ts +21 -0
- package/references/openspec/src/core/configurators/slash/opencode.ts +83 -0
- package/references/openspec/src/core/configurators/slash/qoder.ts +84 -0
- package/references/openspec/src/core/configurators/slash/qwen.ts +55 -0
- package/references/openspec/src/core/configurators/slash/registry.ts +81 -0
- package/references/openspec/src/core/configurators/slash/roocode.ts +27 -0
- package/references/openspec/src/core/configurators/slash/toml-base.ts +66 -0
- package/references/openspec/src/core/configurators/slash/windsurf.ts +27 -0
- package/references/openspec/src/core/converters/json-converter.ts +61 -0
- package/references/openspec/src/core/index.ts +2 -0
- package/references/openspec/src/core/init.ts +986 -0
- package/references/openspec/src/core/list.ts +104 -0
- package/references/openspec/src/core/parsers/change-parser.ts +234 -0
- package/references/openspec/src/core/parsers/markdown-parser.ts +237 -0
- package/references/openspec/src/core/parsers/requirement-blocks.ts +234 -0
- package/references/openspec/src/core/schemas/base.schema.ts +20 -0
- package/references/openspec/src/core/schemas/change.schema.ts +42 -0
- package/references/openspec/src/core/schemas/index.ts +20 -0
- package/references/openspec/src/core/schemas/spec.schema.ts +17 -0
- package/references/openspec/src/core/styles/palette.ts +8 -0
- package/references/openspec/src/core/templates/agents-root-stub.ts +16 -0
- package/references/openspec/src/core/templates/agents-template.ts +457 -0
- package/references/openspec/src/core/templates/claude-template.ts +1 -0
- package/references/openspec/src/core/templates/cline-template.ts +1 -0
- package/references/openspec/src/core/templates/costrict-template.ts +1 -0
- package/references/openspec/src/core/templates/index.ts +50 -0
- package/references/openspec/src/core/templates/project-template.ts +38 -0
- package/references/openspec/src/core/templates/slash-command-templates.ts +60 -0
- package/references/openspec/src/core/update.ts +129 -0
- package/references/openspec/src/core/validation/constants.ts +48 -0
- package/references/openspec/src/core/validation/types.ts +19 -0
- package/references/openspec/src/core/validation/validator.ts +448 -0
- package/references/openspec/src/core/view.ts +189 -0
- package/references/openspec/src/index.ts +2 -0
- package/references/openspec/src/utils/file-system.ts +187 -0
- package/references/openspec/src/utils/index.ts +2 -0
- package/references/openspec/src/utils/interactive.ts +7 -0
- package/references/openspec/src/utils/item-discovery.ts +45 -0
- package/references/openspec/src/utils/match.ts +26 -0
- package/references/openspec/src/utils/task-progress.ts +43 -0
- package/references/openspec/test/cli-e2e/basic.test.ts +156 -0
- package/references/openspec/test/commands/change.interactive-show.test.ts +45 -0
- package/references/openspec/test/commands/change.interactive-validate.test.ts +48 -0
- package/references/openspec/test/commands/show.test.ts +123 -0
- package/references/openspec/test/commands/spec.interactive-show.test.ts +44 -0
- package/references/openspec/test/commands/spec.interactive-validate.test.ts +44 -0
- package/references/openspec/test/commands/spec.test.ts +324 -0
- package/references/openspec/test/commands/validate.enriched-output.test.ts +49 -0
- package/references/openspec/test/commands/validate.test.ts +133 -0
- package/references/openspec/test/core/archive.test.ts +680 -0
- package/references/openspec/test/core/commands/change-command.list.test.ts +76 -0
- package/references/openspec/test/core/commands/change-command.show-validate.test.ts +111 -0
- package/references/openspec/test/core/converters/json-converter.test.ts +184 -0
- package/references/openspec/test/core/init.test.ts +1710 -0
- package/references/openspec/test/core/list.test.ts +165 -0
- package/references/openspec/test/core/parsers/change-parser.test.ts +52 -0
- package/references/openspec/test/core/parsers/markdown-parser.test.ts +291 -0
- package/references/openspec/test/core/update.test.ts +1642 -0
- package/references/openspec/test/core/validation.enriched-messages.test.ts +74 -0
- package/references/openspec/test/core/validation.test.ts +489 -0
- package/references/openspec/test/core/view.test.ts +79 -0
- package/references/openspec/test/fixtures/tmp-init/openspec/changes/c1/proposal.md +7 -0
- package/references/openspec/test/fixtures/tmp-init/openspec/changes/c1/specs/alpha/spec.md +8 -0
- package/references/openspec/test/fixtures/tmp-init/openspec/specs/alpha/spec.md +12 -0
- package/references/openspec/test/helpers/run-cli.ts +139 -0
- package/references/openspec/test/utils/file-system.test.ts +211 -0
- package/references/openspec/test/utils/marker-updates.test.ts +287 -0
- package/references/openspec/tsconfig.json +21 -0
- package/references/openspec/vitest.config.ts +25 -0
- package/references/openspec/vitest.setup.ts +6 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { getTaskProgressForChange, formatTaskStatus } from '../utils/task-progress.js';
|
|
5
|
+
import { MarkdownParser } from './parsers/markdown-parser.js';
|
|
6
|
+
|
|
7
|
+
export class ViewCommand {
|
|
8
|
+
async execute(targetPath: string = '.'): Promise<void> {
|
|
9
|
+
const openspecDir = path.join(targetPath, 'openspec');
|
|
10
|
+
|
|
11
|
+
if (!fs.existsSync(openspecDir)) {
|
|
12
|
+
console.error(chalk.red('No openspec directory found'));
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
console.log(chalk.bold('\nOpenSpec Dashboard\n'));
|
|
17
|
+
console.log('═'.repeat(60));
|
|
18
|
+
|
|
19
|
+
// Get changes and specs data
|
|
20
|
+
const changesData = await this.getChangesData(openspecDir);
|
|
21
|
+
const specsData = await this.getSpecsData(openspecDir);
|
|
22
|
+
|
|
23
|
+
// Display summary metrics
|
|
24
|
+
this.displaySummary(changesData, specsData);
|
|
25
|
+
|
|
26
|
+
// Display active changes
|
|
27
|
+
if (changesData.active.length > 0) {
|
|
28
|
+
console.log(chalk.bold.cyan('\nActive Changes'));
|
|
29
|
+
console.log('─'.repeat(60));
|
|
30
|
+
changesData.active.forEach(change => {
|
|
31
|
+
const progressBar = this.createProgressBar(change.progress.completed, change.progress.total);
|
|
32
|
+
const percentage = change.progress.total > 0
|
|
33
|
+
? Math.round((change.progress.completed / change.progress.total) * 100)
|
|
34
|
+
: 0;
|
|
35
|
+
|
|
36
|
+
console.log(
|
|
37
|
+
` ${chalk.yellow('◉')} ${chalk.bold(change.name.padEnd(30))} ${progressBar} ${chalk.dim(`${percentage}%`)}`
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Display completed changes
|
|
43
|
+
if (changesData.completed.length > 0) {
|
|
44
|
+
console.log(chalk.bold.green('\nCompleted Changes'));
|
|
45
|
+
console.log('─'.repeat(60));
|
|
46
|
+
changesData.completed.forEach(change => {
|
|
47
|
+
console.log(` ${chalk.green('✓')} ${change.name}`);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Display specifications
|
|
52
|
+
if (specsData.length > 0) {
|
|
53
|
+
console.log(chalk.bold.blue('\nSpecifications'));
|
|
54
|
+
console.log('─'.repeat(60));
|
|
55
|
+
|
|
56
|
+
// Sort specs by requirement count (descending)
|
|
57
|
+
specsData.sort((a, b) => b.requirementCount - a.requirementCount);
|
|
58
|
+
|
|
59
|
+
specsData.forEach(spec => {
|
|
60
|
+
const reqLabel = spec.requirementCount === 1 ? 'requirement' : 'requirements';
|
|
61
|
+
console.log(
|
|
62
|
+
` ${chalk.blue('▪')} ${chalk.bold(spec.name.padEnd(30))} ${chalk.dim(`${spec.requirementCount} ${reqLabel}`)}`
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
console.log('\n' + '═'.repeat(60));
|
|
68
|
+
console.log(chalk.dim(`\nUse ${chalk.white('openspec list --changes')} or ${chalk.white('openspec list --specs')} for detailed views`));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private async getChangesData(openspecDir: string): Promise<{
|
|
72
|
+
active: Array<{ name: string; progress: { total: number; completed: number } }>;
|
|
73
|
+
completed: Array<{ name: string }>;
|
|
74
|
+
}> {
|
|
75
|
+
const changesDir = path.join(openspecDir, 'changes');
|
|
76
|
+
|
|
77
|
+
if (!fs.existsSync(changesDir)) {
|
|
78
|
+
return { active: [], completed: [] };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const active: Array<{ name: string; progress: { total: number; completed: number } }> = [];
|
|
82
|
+
const completed: Array<{ name: string }> = [];
|
|
83
|
+
|
|
84
|
+
const entries = fs.readdirSync(changesDir, { withFileTypes: true });
|
|
85
|
+
|
|
86
|
+
for (const entry of entries) {
|
|
87
|
+
if (entry.isDirectory() && entry.name !== 'archive') {
|
|
88
|
+
const progress = await getTaskProgressForChange(changesDir, entry.name);
|
|
89
|
+
|
|
90
|
+
if (progress.total === 0 || progress.completed === progress.total) {
|
|
91
|
+
completed.push({ name: entry.name });
|
|
92
|
+
} else {
|
|
93
|
+
active.push({ name: entry.name, progress });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Sort active changes by completion percentage (ascending) and then by name for deterministic ordering
|
|
99
|
+
active.sort((a, b) => {
|
|
100
|
+
const percentageA = a.progress.total > 0 ? a.progress.completed / a.progress.total : 0;
|
|
101
|
+
const percentageB = b.progress.total > 0 ? b.progress.completed / b.progress.total : 0;
|
|
102
|
+
|
|
103
|
+
if (percentageA < percentageB) return -1;
|
|
104
|
+
if (percentageA > percentageB) return 1;
|
|
105
|
+
return a.name.localeCompare(b.name);
|
|
106
|
+
});
|
|
107
|
+
completed.sort((a, b) => a.name.localeCompare(b.name));
|
|
108
|
+
|
|
109
|
+
return { active, completed };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private async getSpecsData(openspecDir: string): Promise<Array<{ name: string; requirementCount: number }>> {
|
|
113
|
+
const specsDir = path.join(openspecDir, 'specs');
|
|
114
|
+
|
|
115
|
+
if (!fs.existsSync(specsDir)) {
|
|
116
|
+
return [];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const specs: Array<{ name: string; requirementCount: number }> = [];
|
|
120
|
+
const entries = fs.readdirSync(specsDir, { withFileTypes: true });
|
|
121
|
+
|
|
122
|
+
for (const entry of entries) {
|
|
123
|
+
if (entry.isDirectory()) {
|
|
124
|
+
const specFile = path.join(specsDir, entry.name, 'spec.md');
|
|
125
|
+
|
|
126
|
+
if (fs.existsSync(specFile)) {
|
|
127
|
+
try {
|
|
128
|
+
const content = fs.readFileSync(specFile, 'utf-8');
|
|
129
|
+
const parser = new MarkdownParser(content);
|
|
130
|
+
const spec = parser.parseSpec(entry.name);
|
|
131
|
+
const requirementCount = spec.requirements.length;
|
|
132
|
+
specs.push({ name: entry.name, requirementCount });
|
|
133
|
+
} catch (error) {
|
|
134
|
+
// If spec cannot be parsed, include with 0 count
|
|
135
|
+
specs.push({ name: entry.name, requirementCount: 0 });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return specs;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private displaySummary(
|
|
145
|
+
changesData: { active: any[]; completed: any[] },
|
|
146
|
+
specsData: any[]
|
|
147
|
+
): void {
|
|
148
|
+
const totalChanges = changesData.active.length + changesData.completed.length;
|
|
149
|
+
const totalSpecs = specsData.length;
|
|
150
|
+
const totalRequirements = specsData.reduce((sum, spec) => sum + spec.requirementCount, 0);
|
|
151
|
+
|
|
152
|
+
// Calculate total task progress
|
|
153
|
+
let totalTasks = 0;
|
|
154
|
+
let completedTasks = 0;
|
|
155
|
+
|
|
156
|
+
changesData.active.forEach(change => {
|
|
157
|
+
totalTasks += change.progress.total;
|
|
158
|
+
completedTasks += change.progress.completed;
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
changesData.completed.forEach(() => {
|
|
162
|
+
// Completed changes count as 100% done (we don't know exact task count)
|
|
163
|
+
// This is a simplification
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
console.log(chalk.bold('Summary:'));
|
|
167
|
+
console.log(` ${chalk.cyan('●')} Specifications: ${chalk.bold(totalSpecs)} specs, ${chalk.bold(totalRequirements)} requirements`);
|
|
168
|
+
console.log(` ${chalk.yellow('●')} Active Changes: ${chalk.bold(changesData.active.length)} in progress`);
|
|
169
|
+
console.log(` ${chalk.green('●')} Completed Changes: ${chalk.bold(changesData.completed.length)}`);
|
|
170
|
+
|
|
171
|
+
if (totalTasks > 0) {
|
|
172
|
+
const overallProgress = Math.round((completedTasks / totalTasks) * 100);
|
|
173
|
+
console.log(` ${chalk.magenta('●')} Task Progress: ${chalk.bold(`${completedTasks}/${totalTasks}`)} (${overallProgress}% complete)`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
private createProgressBar(completed: number, total: number, width: number = 20): string {
|
|
178
|
+
if (total === 0) return chalk.dim('─'.repeat(width));
|
|
179
|
+
|
|
180
|
+
const percentage = completed / total;
|
|
181
|
+
const filled = Math.round(percentage * width);
|
|
182
|
+
const empty = width - filled;
|
|
183
|
+
|
|
184
|
+
const filledBar = chalk.green('█'.repeat(filled));
|
|
185
|
+
const emptyBar = chalk.dim('░'.repeat(empty));
|
|
186
|
+
|
|
187
|
+
return `[${filledBar}${emptyBar}]`;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
function isMarkerOnOwnLine(content: string, markerIndex: number, markerLength: number): boolean {
|
|
5
|
+
let leftIndex = markerIndex - 1;
|
|
6
|
+
while (leftIndex >= 0 && content[leftIndex] !== '\n') {
|
|
7
|
+
const char = content[leftIndex];
|
|
8
|
+
if (char !== ' ' && char !== '\t' && char !== '\r') {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
leftIndex--;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let rightIndex = markerIndex + markerLength;
|
|
15
|
+
while (rightIndex < content.length && content[rightIndex] !== '\n') {
|
|
16
|
+
const char = content[rightIndex];
|
|
17
|
+
if (char !== ' ' && char !== '\t' && char !== '\r') {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
rightIndex++;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function findMarkerIndex(
|
|
27
|
+
content: string,
|
|
28
|
+
marker: string,
|
|
29
|
+
fromIndex = 0
|
|
30
|
+
): number {
|
|
31
|
+
let currentIndex = content.indexOf(marker, fromIndex);
|
|
32
|
+
|
|
33
|
+
while (currentIndex !== -1) {
|
|
34
|
+
if (isMarkerOnOwnLine(content, currentIndex, marker.length)) {
|
|
35
|
+
return currentIndex;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
currentIndex = content.indexOf(marker, currentIndex + marker.length);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return -1;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export class FileSystemUtils {
|
|
45
|
+
private static isWindowsBasePath(basePath: string): boolean {
|
|
46
|
+
return /^[A-Za-z]:[\\/]/.test(basePath) || basePath.startsWith('\\');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private static normalizeSegments(segments: string[]): string[] {
|
|
50
|
+
return segments
|
|
51
|
+
.flatMap((segment) => segment.split(/[\\/]+/u))
|
|
52
|
+
.filter((part) => part.length > 0);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
static joinPath(basePath: string, ...segments: string[]): string {
|
|
56
|
+
const normalizedSegments = this.normalizeSegments(segments);
|
|
57
|
+
|
|
58
|
+
if (this.isWindowsBasePath(basePath)) {
|
|
59
|
+
const normalizedBasePath = path.win32.normalize(basePath);
|
|
60
|
+
return normalizedSegments.length
|
|
61
|
+
? path.win32.join(normalizedBasePath, ...normalizedSegments)
|
|
62
|
+
: normalizedBasePath;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const posixBasePath = basePath.replace(/\\/g, '/');
|
|
66
|
+
|
|
67
|
+
return normalizedSegments.length
|
|
68
|
+
? path.posix.join(posixBasePath, ...normalizedSegments)
|
|
69
|
+
: path.posix.normalize(posixBasePath);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
static async createDirectory(dirPath: string): Promise<void> {
|
|
73
|
+
await fs.mkdir(dirPath, { recursive: true });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
static async fileExists(filePath: string): Promise<boolean> {
|
|
77
|
+
try {
|
|
78
|
+
await fs.access(filePath);
|
|
79
|
+
return true;
|
|
80
|
+
} catch (error: any) {
|
|
81
|
+
if (error.code !== 'ENOENT') {
|
|
82
|
+
console.debug(`Unable to check if file exists at ${filePath}: ${error.message}`);
|
|
83
|
+
}
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
static async canWriteFile(filePath: string): Promise<boolean> {
|
|
89
|
+
try {
|
|
90
|
+
const stats = await fs.stat(filePath);
|
|
91
|
+
|
|
92
|
+
if (!stats.isFile()) {
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return (stats.mode & 0o222) !== 0;
|
|
97
|
+
} catch (error: any) {
|
|
98
|
+
if (error.code === 'ENOENT') {
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
console.debug(`Unable to determine write permissions for ${filePath}: ${error.message}`);
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
static async directoryExists(dirPath: string): Promise<boolean> {
|
|
108
|
+
try {
|
|
109
|
+
const stats = await fs.stat(dirPath);
|
|
110
|
+
return stats.isDirectory();
|
|
111
|
+
} catch (error: any) {
|
|
112
|
+
if (error.code !== 'ENOENT') {
|
|
113
|
+
console.debug(`Unable to check if directory exists at ${dirPath}: ${error.message}`);
|
|
114
|
+
}
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
static async writeFile(filePath: string, content: string): Promise<void> {
|
|
120
|
+
const dir = path.dirname(filePath);
|
|
121
|
+
await this.createDirectory(dir);
|
|
122
|
+
await fs.writeFile(filePath, content, 'utf-8');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
static async readFile(filePath: string): Promise<string> {
|
|
126
|
+
return await fs.readFile(filePath, 'utf-8');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
static async updateFileWithMarkers(
|
|
130
|
+
filePath: string,
|
|
131
|
+
content: string,
|
|
132
|
+
startMarker: string,
|
|
133
|
+
endMarker: string
|
|
134
|
+
): Promise<void> {
|
|
135
|
+
let existingContent = '';
|
|
136
|
+
|
|
137
|
+
if (await this.fileExists(filePath)) {
|
|
138
|
+
existingContent = await this.readFile(filePath);
|
|
139
|
+
|
|
140
|
+
const startIndex = findMarkerIndex(existingContent, startMarker);
|
|
141
|
+
const endIndex = startIndex !== -1
|
|
142
|
+
? findMarkerIndex(existingContent, endMarker, startIndex + startMarker.length)
|
|
143
|
+
: findMarkerIndex(existingContent, endMarker);
|
|
144
|
+
|
|
145
|
+
if (startIndex !== -1 && endIndex !== -1) {
|
|
146
|
+
if (endIndex < startIndex) {
|
|
147
|
+
throw new Error(
|
|
148
|
+
`Invalid marker state in ${filePath}. End marker appears before start marker.`
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const before = existingContent.substring(0, startIndex);
|
|
153
|
+
const after = existingContent.substring(endIndex + endMarker.length);
|
|
154
|
+
existingContent = before + startMarker + '\n' + content + '\n' + endMarker + after;
|
|
155
|
+
} else if (startIndex === -1 && endIndex === -1) {
|
|
156
|
+
existingContent = startMarker + '\n' + content + '\n' + endMarker + '\n\n' + existingContent;
|
|
157
|
+
} else {
|
|
158
|
+
throw new Error(`Invalid marker state in ${filePath}. Found start: ${startIndex !== -1}, Found end: ${endIndex !== -1}`);
|
|
159
|
+
}
|
|
160
|
+
} else {
|
|
161
|
+
existingContent = startMarker + '\n' + content + '\n' + endMarker;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
await this.writeFile(filePath, existingContent);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
static async ensureWritePermissions(dirPath: string): Promise<boolean> {
|
|
168
|
+
try {
|
|
169
|
+
// If directory doesn't exist, check parent directory permissions
|
|
170
|
+
if (!await this.directoryExists(dirPath)) {
|
|
171
|
+
const parentDir = path.dirname(dirPath);
|
|
172
|
+
if (!await this.directoryExists(parentDir)) {
|
|
173
|
+
await this.createDirectory(parentDir);
|
|
174
|
+
}
|
|
175
|
+
return await this.ensureWritePermissions(parentDir);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const testFile = path.join(dirPath, '.openspec-test-' + Date.now());
|
|
179
|
+
await fs.writeFile(testFile, '');
|
|
180
|
+
await fs.unlink(testFile);
|
|
181
|
+
return true;
|
|
182
|
+
} catch (error: any) {
|
|
183
|
+
console.debug(`Insufficient permissions to write to ${dirPath}: ${error.message}`);
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
export async function getActiveChangeIds(root: string = process.cwd()): Promise<string[]> {
|
|
5
|
+
const changesPath = path.join(root, 'openspec', 'changes');
|
|
6
|
+
try {
|
|
7
|
+
const entries = await fs.readdir(changesPath, { withFileTypes: true });
|
|
8
|
+
const result: string[] = [];
|
|
9
|
+
for (const entry of entries) {
|
|
10
|
+
if (!entry.isDirectory() || entry.name.startsWith('.') || entry.name === 'archive') continue;
|
|
11
|
+
const proposalPath = path.join(changesPath, entry.name, 'proposal.md');
|
|
12
|
+
try {
|
|
13
|
+
await fs.access(proposalPath);
|
|
14
|
+
result.push(entry.name);
|
|
15
|
+
} catch {
|
|
16
|
+
// skip directories without proposal.md
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return result.sort();
|
|
20
|
+
} catch {
|
|
21
|
+
return [];
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function getSpecIds(root: string = process.cwd()): Promise<string[]> {
|
|
26
|
+
const specsPath = path.join(root, 'openspec', 'specs');
|
|
27
|
+
const result: string[] = [];
|
|
28
|
+
try {
|
|
29
|
+
const entries = await fs.readdir(specsPath, { withFileTypes: true });
|
|
30
|
+
for (const entry of entries) {
|
|
31
|
+
if (!entry.isDirectory() || entry.name.startsWith('.')) continue;
|
|
32
|
+
const specFile = path.join(specsPath, entry.name, 'spec.md');
|
|
33
|
+
try {
|
|
34
|
+
await fs.access(specFile);
|
|
35
|
+
result.push(entry.name);
|
|
36
|
+
} catch {
|
|
37
|
+
// ignore
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
} catch {
|
|
41
|
+
// ignore
|
|
42
|
+
}
|
|
43
|
+
return result.sort();
|
|
44
|
+
}
|
|
45
|
+
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export function nearestMatches(input: string, candidates: string[], max: number = 5): string[] {
|
|
2
|
+
const scored = candidates.map(candidate => ({ candidate, distance: levenshtein(input, candidate) }));
|
|
3
|
+
scored.sort((a, b) => a.distance - b.distance);
|
|
4
|
+
return scored.slice(0, max).map(s => s.candidate);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function levenshtein(a: string, b: string): number {
|
|
8
|
+
const m = a.length;
|
|
9
|
+
const n = b.length;
|
|
10
|
+
const dp: number[][] = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
|
|
11
|
+
for (let i = 0; i <= m; i++) dp[i][0] = i;
|
|
12
|
+
for (let j = 0; j <= n; j++) dp[0][j] = j;
|
|
13
|
+
for (let i = 1; i <= m; i++) {
|
|
14
|
+
for (let j = 1; j <= n; j++) {
|
|
15
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
16
|
+
dp[i][j] = Math.min(
|
|
17
|
+
dp[i - 1][j] + 1,
|
|
18
|
+
dp[i][j - 1] + 1,
|
|
19
|
+
dp[i - 1][j - 1] + cost
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return dp[m][n];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
const TASK_PATTERN = /^[-*]\s+\[[\sx]\]/i;
|
|
5
|
+
const COMPLETED_TASK_PATTERN = /^[-*]\s+\[x\]/i;
|
|
6
|
+
|
|
7
|
+
export interface TaskProgress {
|
|
8
|
+
total: number;
|
|
9
|
+
completed: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function countTasksFromContent(content: string): TaskProgress {
|
|
13
|
+
const lines = content.split('\n');
|
|
14
|
+
let total = 0;
|
|
15
|
+
let completed = 0;
|
|
16
|
+
for (const line of lines) {
|
|
17
|
+
if (line.match(TASK_PATTERN)) {
|
|
18
|
+
total++;
|
|
19
|
+
if (line.match(COMPLETED_TASK_PATTERN)) {
|
|
20
|
+
completed++;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return { total, completed };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function getTaskProgressForChange(changesDir: string, changeName: string): Promise<TaskProgress> {
|
|
28
|
+
const tasksPath = path.join(changesDir, changeName, 'tasks.md');
|
|
29
|
+
try {
|
|
30
|
+
const content = await fs.readFile(tasksPath, 'utf-8');
|
|
31
|
+
return countTasksFromContent(content);
|
|
32
|
+
} catch {
|
|
33
|
+
return { total: 0, completed: 0 };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function formatTaskStatus(progress: TaskProgress): string {
|
|
38
|
+
if (progress.total === 0) return 'No tasks';
|
|
39
|
+
if (progress.completed === progress.total) return '✓ Complete';
|
|
40
|
+
return `${progress.completed}/${progress.total} tasks`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { afterAll, describe, it, expect } from 'vitest';
|
|
2
|
+
import { promises as fs } from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { tmpdir } from 'os';
|
|
5
|
+
import { runCLI, cliProjectRoot } from '../helpers/run-cli.js';
|
|
6
|
+
import { AI_TOOLS } from '../../src/core/config.js';
|
|
7
|
+
|
|
8
|
+
async function fileExists(filePath: string): Promise<boolean> {
|
|
9
|
+
try {
|
|
10
|
+
await fs.access(filePath);
|
|
11
|
+
return true;
|
|
12
|
+
} catch {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const tempRoots: string[] = [];
|
|
18
|
+
|
|
19
|
+
async function prepareFixture(fixtureName: string): Promise<string> {
|
|
20
|
+
const base = await fs.mkdtemp(path.join(tmpdir(), 'openspec-cli-e2e-'));
|
|
21
|
+
tempRoots.push(base);
|
|
22
|
+
const projectDir = path.join(base, 'project');
|
|
23
|
+
await fs.mkdir(projectDir, { recursive: true });
|
|
24
|
+
const fixtureDir = path.join(cliProjectRoot, 'test', 'fixtures', fixtureName);
|
|
25
|
+
await fs.cp(fixtureDir, projectDir, { recursive: true });
|
|
26
|
+
return projectDir;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
afterAll(async () => {
|
|
30
|
+
await Promise.all(tempRoots.map((dir) => fs.rm(dir, { recursive: true, force: true })));
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe('openspec CLI e2e basics', () => {
|
|
34
|
+
it('shows help output', async () => {
|
|
35
|
+
const result = await runCLI(['--help']);
|
|
36
|
+
expect(result.exitCode).toBe(0);
|
|
37
|
+
expect(result.stdout).toContain('Usage: openspec');
|
|
38
|
+
expect(result.stderr).toBe('');
|
|
39
|
+
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('shows dynamic tool ids in init help', async () => {
|
|
43
|
+
const result = await runCLI(['init', '--help']);
|
|
44
|
+
expect(result.exitCode).toBe(0);
|
|
45
|
+
|
|
46
|
+
const expectedTools = AI_TOOLS.filter((tool) => tool.available)
|
|
47
|
+
.map((tool) => tool.value)
|
|
48
|
+
.join(', ');
|
|
49
|
+
const normalizedOutput = result.stdout.replace(/\s+/g, ' ').trim();
|
|
50
|
+
expect(normalizedOutput).toContain(
|
|
51
|
+
`Use "all", "none", or a comma-separated list of: ${expectedTools}`
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('reports the package version', async () => {
|
|
56
|
+
const pkgRaw = await fs.readFile(path.join(cliProjectRoot, 'package.json'), 'utf-8');
|
|
57
|
+
const pkg = JSON.parse(pkgRaw);
|
|
58
|
+
const result = await runCLI(['--version']);
|
|
59
|
+
expect(result.exitCode).toBe(0);
|
|
60
|
+
expect(result.stdout.trim()).toBe(pkg.version);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('validates the tmp-init fixture with --all --json', async () => {
|
|
64
|
+
const projectDir = await prepareFixture('tmp-init');
|
|
65
|
+
const result = await runCLI(['validate', '--all', '--json'], { cwd: projectDir });
|
|
66
|
+
expect(result.exitCode).toBe(0);
|
|
67
|
+
const output = result.stdout.trim();
|
|
68
|
+
expect(output).not.toBe('');
|
|
69
|
+
const json = JSON.parse(output);
|
|
70
|
+
expect(json.summary?.totals?.failed).toBe(0);
|
|
71
|
+
expect(json.items.some((item: any) => item.id === 'c1' && item.type === 'change')).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('returns an error for unknown items in the fixture', async () => {
|
|
75
|
+
const projectDir = await prepareFixture('tmp-init');
|
|
76
|
+
const result = await runCLI(['validate', 'does-not-exist'], { cwd: projectDir });
|
|
77
|
+
expect(result.exitCode).toBe(1);
|
|
78
|
+
expect(result.stderr).toContain("Unknown item 'does-not-exist'");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe('init command non-interactive options', () => {
|
|
82
|
+
it('initializes with --tools all option', async () => {
|
|
83
|
+
const projectDir = await prepareFixture('tmp-init');
|
|
84
|
+
const emptyProjectDir = path.join(projectDir, '..', 'empty-project');
|
|
85
|
+
await fs.mkdir(emptyProjectDir, { recursive: true });
|
|
86
|
+
|
|
87
|
+
const codexHome = path.join(emptyProjectDir, '.codex');
|
|
88
|
+
const result = await runCLI(['init', '--tools', 'all'], {
|
|
89
|
+
cwd: emptyProjectDir,
|
|
90
|
+
env: { CODEX_HOME: codexHome },
|
|
91
|
+
});
|
|
92
|
+
expect(result.exitCode).toBe(0);
|
|
93
|
+
expect(result.stdout).toContain('Tool summary:');
|
|
94
|
+
|
|
95
|
+
// Check that tool configurations were created
|
|
96
|
+
const claudePath = path.join(emptyProjectDir, 'CLAUDE.md');
|
|
97
|
+
const cursorProposal = path.join(emptyProjectDir, '.cursor/commands/openspec-proposal.md');
|
|
98
|
+
expect(await fileExists(claudePath)).toBe(true);
|
|
99
|
+
expect(await fileExists(cursorProposal)).toBe(true);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('initializes with --tools list option', async () => {
|
|
103
|
+
const projectDir = await prepareFixture('tmp-init');
|
|
104
|
+
const emptyProjectDir = path.join(projectDir, '..', 'empty-project');
|
|
105
|
+
await fs.mkdir(emptyProjectDir, { recursive: true });
|
|
106
|
+
|
|
107
|
+
const result = await runCLI(['init', '--tools', 'claude'], { cwd: emptyProjectDir });
|
|
108
|
+
expect(result.exitCode).toBe(0);
|
|
109
|
+
expect(result.stdout).toContain('Tool summary:');
|
|
110
|
+
|
|
111
|
+
const claudePath = path.join(emptyProjectDir, 'CLAUDE.md');
|
|
112
|
+
const cursorProposal = path.join(emptyProjectDir, '.cursor/commands/openspec-proposal.md');
|
|
113
|
+
expect(await fileExists(claudePath)).toBe(true);
|
|
114
|
+
expect(await fileExists(cursorProposal)).toBe(false); // Not selected
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('initializes with --tools none option', async () => {
|
|
118
|
+
const projectDir = await prepareFixture('tmp-init');
|
|
119
|
+
const emptyProjectDir = path.join(projectDir, '..', 'empty-project');
|
|
120
|
+
await fs.mkdir(emptyProjectDir, { recursive: true });
|
|
121
|
+
|
|
122
|
+
const result = await runCLI(['init', '--tools', 'none'], { cwd: emptyProjectDir });
|
|
123
|
+
expect(result.exitCode).toBe(0);
|
|
124
|
+
expect(result.stdout).toContain('Tool summary:');
|
|
125
|
+
|
|
126
|
+
const claudePath = path.join(emptyProjectDir, 'CLAUDE.md');
|
|
127
|
+
const cursorProposal = path.join(emptyProjectDir, '.cursor/commands/openspec-proposal.md');
|
|
128
|
+
const rootAgentsPath = path.join(emptyProjectDir, 'AGENTS.md');
|
|
129
|
+
|
|
130
|
+
expect(await fileExists(rootAgentsPath)).toBe(true);
|
|
131
|
+
expect(await fileExists(claudePath)).toBe(false);
|
|
132
|
+
expect(await fileExists(cursorProposal)).toBe(false);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('returns error for invalid tool names', async () => {
|
|
136
|
+
const projectDir = await prepareFixture('tmp-init');
|
|
137
|
+
const emptyProjectDir = path.join(projectDir, '..', 'empty-project');
|
|
138
|
+
await fs.mkdir(emptyProjectDir, { recursive: true });
|
|
139
|
+
|
|
140
|
+
const result = await runCLI(['init', '--tools', 'invalid-tool'], { cwd: emptyProjectDir });
|
|
141
|
+
expect(result.exitCode).toBe(1);
|
|
142
|
+
expect(result.stderr).toContain('Invalid tool(s): invalid-tool');
|
|
143
|
+
expect(result.stderr).toContain('Available values:');
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('returns error when combining reserved keywords with explicit ids', async () => {
|
|
147
|
+
const projectDir = await prepareFixture('tmp-init');
|
|
148
|
+
const emptyProjectDir = path.join(projectDir, '..', 'empty-project');
|
|
149
|
+
await fs.mkdir(emptyProjectDir, { recursive: true });
|
|
150
|
+
|
|
151
|
+
const result = await runCLI(['init', '--tools', 'all,claude'], { cwd: emptyProjectDir });
|
|
152
|
+
expect(result.exitCode).toBe(1);
|
|
153
|
+
expect(result.stderr).toContain('Cannot combine reserved values "all" or "none" with specific tool IDs');
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
});
|