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,1642 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import { UpdateCommand } from '../../src/core/update.js';
|
|
3
|
+
import { FileSystemUtils } from '../../src/utils/file-system.js';
|
|
4
|
+
import { ToolRegistry } from '../../src/core/configurators/registry.js';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import fs from 'fs/promises';
|
|
7
|
+
import os from 'os';
|
|
8
|
+
import { randomUUID } from 'crypto';
|
|
9
|
+
|
|
10
|
+
describe('UpdateCommand', () => {
|
|
11
|
+
let testDir: string;
|
|
12
|
+
let updateCommand: UpdateCommand;
|
|
13
|
+
let prevCodexHome: string | undefined;
|
|
14
|
+
|
|
15
|
+
beforeEach(async () => {
|
|
16
|
+
// Create a temporary test directory
|
|
17
|
+
testDir = path.join(os.tmpdir(), `openspec-test-${randomUUID()}`);
|
|
18
|
+
await fs.mkdir(testDir, { recursive: true });
|
|
19
|
+
|
|
20
|
+
// Create openspec directory
|
|
21
|
+
const openspecDir = path.join(testDir, 'openspec');
|
|
22
|
+
await fs.mkdir(openspecDir, { recursive: true });
|
|
23
|
+
|
|
24
|
+
updateCommand = new UpdateCommand();
|
|
25
|
+
|
|
26
|
+
// Route Codex global directory into the test sandbox
|
|
27
|
+
prevCodexHome = process.env.CODEX_HOME;
|
|
28
|
+
process.env.CODEX_HOME = path.join(testDir, '.codex');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
afterEach(async () => {
|
|
32
|
+
// Clean up test directory
|
|
33
|
+
await fs.rm(testDir, { recursive: true, force: true });
|
|
34
|
+
if (prevCodexHome === undefined) delete process.env.CODEX_HOME;
|
|
35
|
+
else process.env.CODEX_HOME = prevCodexHome;
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should update only existing CLAUDE.md file', async () => {
|
|
39
|
+
// Create CLAUDE.md file with initial content
|
|
40
|
+
const claudePath = path.join(testDir, 'CLAUDE.md');
|
|
41
|
+
const initialContent = `# Project Instructions
|
|
42
|
+
|
|
43
|
+
Some existing content here.
|
|
44
|
+
|
|
45
|
+
<!-- OPENSPEC:START -->
|
|
46
|
+
Old OpenSpec content
|
|
47
|
+
<!-- OPENSPEC:END -->
|
|
48
|
+
|
|
49
|
+
More content after.`;
|
|
50
|
+
await fs.writeFile(claudePath, initialContent);
|
|
51
|
+
|
|
52
|
+
const consoleSpy = vi.spyOn(console, 'log');
|
|
53
|
+
|
|
54
|
+
// Execute update command
|
|
55
|
+
await updateCommand.execute(testDir);
|
|
56
|
+
|
|
57
|
+
// Check that CLAUDE.md was updated
|
|
58
|
+
const updatedContent = await fs.readFile(claudePath, 'utf-8');
|
|
59
|
+
expect(updatedContent).toContain('<!-- OPENSPEC:START -->');
|
|
60
|
+
expect(updatedContent).toContain('<!-- OPENSPEC:END -->');
|
|
61
|
+
expect(updatedContent).toContain("@/openspec/AGENTS.md");
|
|
62
|
+
expect(updatedContent).toContain('openspec update');
|
|
63
|
+
expect(updatedContent).toContain('Some existing content here');
|
|
64
|
+
expect(updatedContent).toContain('More content after');
|
|
65
|
+
|
|
66
|
+
// Check console output
|
|
67
|
+
const [logMessage] = consoleSpy.mock.calls[0];
|
|
68
|
+
expect(logMessage).toContain(
|
|
69
|
+
'Updated OpenSpec instructions (openspec/AGENTS.md'
|
|
70
|
+
);
|
|
71
|
+
expect(logMessage).toContain('AGENTS.md (created)');
|
|
72
|
+
expect(logMessage).toContain('Updated AI tool files: CLAUDE.md');
|
|
73
|
+
consoleSpy.mockRestore();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should update only existing QWEN.md file', async () => {
|
|
77
|
+
const qwenPath = path.join(testDir, 'QWEN.md');
|
|
78
|
+
const initialContent = `# Qwen Instructions
|
|
79
|
+
|
|
80
|
+
Some existing content.
|
|
81
|
+
|
|
82
|
+
<!-- OPENSPEC:START -->
|
|
83
|
+
Old OpenSpec content
|
|
84
|
+
<!-- OPENSPEC:END -->
|
|
85
|
+
|
|
86
|
+
More notes here.`;
|
|
87
|
+
await fs.writeFile(qwenPath, initialContent);
|
|
88
|
+
|
|
89
|
+
const consoleSpy = vi.spyOn(console, 'log');
|
|
90
|
+
|
|
91
|
+
await updateCommand.execute(testDir);
|
|
92
|
+
|
|
93
|
+
const updatedContent = await fs.readFile(qwenPath, 'utf-8');
|
|
94
|
+
expect(updatedContent).toContain('<!-- OPENSPEC:START -->');
|
|
95
|
+
expect(updatedContent).toContain('<!-- OPENSPEC:END -->');
|
|
96
|
+
expect(updatedContent).toContain("@/openspec/AGENTS.md");
|
|
97
|
+
expect(updatedContent).toContain('openspec update');
|
|
98
|
+
expect(updatedContent).toContain('Some existing content.');
|
|
99
|
+
expect(updatedContent).toContain('More notes here.');
|
|
100
|
+
|
|
101
|
+
const [logMessage] = consoleSpy.mock.calls[0];
|
|
102
|
+
expect(logMessage).toContain(
|
|
103
|
+
'Updated OpenSpec instructions (openspec/AGENTS.md'
|
|
104
|
+
);
|
|
105
|
+
expect(logMessage).toContain('AGENTS.md (created)');
|
|
106
|
+
expect(logMessage).toContain('Updated AI tool files: QWEN.md');
|
|
107
|
+
|
|
108
|
+
consoleSpy.mockRestore();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('should refresh existing Claude slash command files', async () => {
|
|
112
|
+
const proposalPath = path.join(
|
|
113
|
+
testDir,
|
|
114
|
+
'.claude/commands/openspec/proposal.md'
|
|
115
|
+
);
|
|
116
|
+
await fs.mkdir(path.dirname(proposalPath), { recursive: true });
|
|
117
|
+
const initialContent = `---
|
|
118
|
+
name: OpenSpec: Proposal
|
|
119
|
+
description: Old description
|
|
120
|
+
category: OpenSpec
|
|
121
|
+
tags: [openspec, change]
|
|
122
|
+
---
|
|
123
|
+
<!-- OPENSPEC:START -->
|
|
124
|
+
Old slash content
|
|
125
|
+
<!-- OPENSPEC:END -->`;
|
|
126
|
+
await fs.writeFile(proposalPath, initialContent);
|
|
127
|
+
|
|
128
|
+
const consoleSpy = vi.spyOn(console, 'log');
|
|
129
|
+
|
|
130
|
+
await updateCommand.execute(testDir);
|
|
131
|
+
|
|
132
|
+
const updated = await fs.readFile(proposalPath, 'utf-8');
|
|
133
|
+
expect(updated).toContain('name: OpenSpec: Proposal');
|
|
134
|
+
expect(updated).toContain('**Guardrails**');
|
|
135
|
+
expect(updated).toContain(
|
|
136
|
+
'Validate with `openspec validate <id> --strict`'
|
|
137
|
+
);
|
|
138
|
+
expect(updated).not.toContain('Old slash content');
|
|
139
|
+
|
|
140
|
+
const [logMessage] = consoleSpy.mock.calls[0];
|
|
141
|
+
expect(logMessage).toContain(
|
|
142
|
+
'Updated OpenSpec instructions (openspec/AGENTS.md'
|
|
143
|
+
);
|
|
144
|
+
expect(logMessage).toContain('AGENTS.md (created)');
|
|
145
|
+
expect(logMessage).toContain(
|
|
146
|
+
'Updated slash commands: .claude/commands/openspec/proposal.md'
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
consoleSpy.mockRestore();
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('should refresh existing Qwen slash command files', async () => {
|
|
153
|
+
const applyPath = path.join(
|
|
154
|
+
testDir,
|
|
155
|
+
'.qwen/commands/openspec-apply.toml'
|
|
156
|
+
);
|
|
157
|
+
await fs.mkdir(path.dirname(applyPath), { recursive: true });
|
|
158
|
+
const initialContent = `description = "Implement an approved OpenSpec change and keep tasks in sync."
|
|
159
|
+
|
|
160
|
+
prompt = """
|
|
161
|
+
<!-- OPENSPEC:START -->
|
|
162
|
+
Old body
|
|
163
|
+
<!-- OPENSPEC:END -->
|
|
164
|
+
"""
|
|
165
|
+
`;
|
|
166
|
+
await fs.writeFile(applyPath, initialContent);
|
|
167
|
+
|
|
168
|
+
const consoleSpy = vi.spyOn(console, 'log');
|
|
169
|
+
|
|
170
|
+
await updateCommand.execute(testDir);
|
|
171
|
+
|
|
172
|
+
const updated = await fs.readFile(applyPath, 'utf-8');
|
|
173
|
+
expect(updated).toContain('description = "Implement an approved OpenSpec change and keep tasks in sync."');
|
|
174
|
+
expect(updated).toContain('prompt = """');
|
|
175
|
+
expect(updated).toContain('<!-- OPENSPEC:START -->');
|
|
176
|
+
expect(updated).toContain('Work through tasks sequentially');
|
|
177
|
+
expect(updated).not.toContain('Old body');
|
|
178
|
+
|
|
179
|
+
const [logMessage] = consoleSpy.mock.calls[0];
|
|
180
|
+
expect(logMessage).toContain(
|
|
181
|
+
'Updated OpenSpec instructions (openspec/AGENTS.md'
|
|
182
|
+
);
|
|
183
|
+
expect(logMessage).toContain('AGENTS.md (created)');
|
|
184
|
+
expect(logMessage).toContain(
|
|
185
|
+
'Updated slash commands: .qwen/commands/openspec-apply.toml'
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
consoleSpy.mockRestore();
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('should not create missing Qwen slash command files on update', async () => {
|
|
192
|
+
const applyPath = path.join(
|
|
193
|
+
testDir,
|
|
194
|
+
'.qwen/commands/openspec-apply.toml'
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
await fs.mkdir(path.dirname(applyPath), { recursive: true });
|
|
198
|
+
await fs.writeFile(
|
|
199
|
+
applyPath,
|
|
200
|
+
`description = "Old description"
|
|
201
|
+
|
|
202
|
+
prompt = """
|
|
203
|
+
<!-- OPENSPEC:START -->
|
|
204
|
+
Old content
|
|
205
|
+
<!-- OPENSPEC:END -->
|
|
206
|
+
"""
|
|
207
|
+
`
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
await updateCommand.execute(testDir);
|
|
211
|
+
|
|
212
|
+
const updatedApply = await fs.readFile(applyPath, 'utf-8');
|
|
213
|
+
expect(updatedApply).toContain('Work through tasks sequentially');
|
|
214
|
+
expect(updatedApply).not.toContain('Old content');
|
|
215
|
+
|
|
216
|
+
const proposalPath = path.join(
|
|
217
|
+
testDir,
|
|
218
|
+
'.qwen/commands/openspec-proposal.toml'
|
|
219
|
+
);
|
|
220
|
+
const archivePath = path.join(
|
|
221
|
+
testDir,
|
|
222
|
+
'.qwen/commands/openspec-archive.toml'
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
await expect(FileSystemUtils.fileExists(proposalPath)).resolves.toBe(false);
|
|
226
|
+
await expect(FileSystemUtils.fileExists(archivePath)).resolves.toBe(false);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('should not create CLAUDE.md if it does not exist', async () => {
|
|
230
|
+
// Ensure CLAUDE.md does not exist
|
|
231
|
+
const claudePath = path.join(testDir, 'CLAUDE.md');
|
|
232
|
+
|
|
233
|
+
// Execute update command
|
|
234
|
+
await updateCommand.execute(testDir);
|
|
235
|
+
|
|
236
|
+
// Check that CLAUDE.md was not created
|
|
237
|
+
const fileExists = await FileSystemUtils.fileExists(claudePath);
|
|
238
|
+
expect(fileExists).toBe(false);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('should not create QWEN.md if it does not exist', async () => {
|
|
242
|
+
const qwenPath = path.join(testDir, 'QWEN.md');
|
|
243
|
+
await updateCommand.execute(testDir);
|
|
244
|
+
await expect(FileSystemUtils.fileExists(qwenPath)).resolves.toBe(false);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('should update only existing CLINE.md file', async () => {
|
|
248
|
+
// Create CLINE.md file with initial content
|
|
249
|
+
const clinePath = path.join(testDir, 'CLINE.md');
|
|
250
|
+
const initialContent = `# Cline Rules
|
|
251
|
+
|
|
252
|
+
Some existing Cline rules here.
|
|
253
|
+
|
|
254
|
+
<!-- OPENSPEC:START -->
|
|
255
|
+
Old OpenSpec content
|
|
256
|
+
<!-- OPENSPEC:END -->
|
|
257
|
+
|
|
258
|
+
More rules after.`;
|
|
259
|
+
await fs.writeFile(clinePath, initialContent);
|
|
260
|
+
|
|
261
|
+
const consoleSpy = vi.spyOn(console, 'log');
|
|
262
|
+
|
|
263
|
+
// Execute update command
|
|
264
|
+
await updateCommand.execute(testDir);
|
|
265
|
+
|
|
266
|
+
// Check that CLINE.md was updated
|
|
267
|
+
const updatedContent = await fs.readFile(clinePath, 'utf-8');
|
|
268
|
+
expect(updatedContent).toContain('<!-- OPENSPEC:START -->');
|
|
269
|
+
expect(updatedContent).toContain('<!-- OPENSPEC:END -->');
|
|
270
|
+
expect(updatedContent).toContain("@/openspec/AGENTS.md");
|
|
271
|
+
expect(updatedContent).toContain('openspec update');
|
|
272
|
+
expect(updatedContent).toContain('Some existing Cline rules here');
|
|
273
|
+
expect(updatedContent).toContain('More rules after');
|
|
274
|
+
|
|
275
|
+
// Check console output
|
|
276
|
+
const [logMessage] = consoleSpy.mock.calls[0];
|
|
277
|
+
expect(logMessage).toContain(
|
|
278
|
+
'Updated OpenSpec instructions (openspec/AGENTS.md'
|
|
279
|
+
);
|
|
280
|
+
expect(logMessage).toContain('AGENTS.md (created)');
|
|
281
|
+
expect(logMessage).toContain('Updated AI tool files: CLINE.md');
|
|
282
|
+
consoleSpy.mockRestore();
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it('should not create CLINE.md if it does not exist', async () => {
|
|
286
|
+
// Ensure CLINE.md does not exist
|
|
287
|
+
const clinePath = path.join(testDir, 'CLINE.md');
|
|
288
|
+
|
|
289
|
+
// Execute update command
|
|
290
|
+
await updateCommand.execute(testDir);
|
|
291
|
+
|
|
292
|
+
// Check that CLINE.md was not created
|
|
293
|
+
const fileExists = await FileSystemUtils.fileExists(clinePath);
|
|
294
|
+
expect(fileExists).toBe(false);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('should refresh existing Cline workflow files', async () => {
|
|
298
|
+
const proposalPath = path.join(
|
|
299
|
+
testDir,
|
|
300
|
+
'.clinerules/workflows/openspec-proposal.md'
|
|
301
|
+
);
|
|
302
|
+
await fs.mkdir(path.dirname(proposalPath), { recursive: true });
|
|
303
|
+
const initialContent = `# OpenSpec: Proposal
|
|
304
|
+
|
|
305
|
+
Scaffold a new OpenSpec change and validate strictly.
|
|
306
|
+
|
|
307
|
+
<!-- OPENSPEC:START -->
|
|
308
|
+
Old slash content
|
|
309
|
+
<!-- OPENSPEC:END -->`;
|
|
310
|
+
await fs.writeFile(proposalPath, initialContent);
|
|
311
|
+
|
|
312
|
+
const consoleSpy = vi.spyOn(console, 'log');
|
|
313
|
+
|
|
314
|
+
await updateCommand.execute(testDir);
|
|
315
|
+
|
|
316
|
+
const updated = await fs.readFile(proposalPath, 'utf-8');
|
|
317
|
+
expect(updated).toContain('# OpenSpec: Proposal');
|
|
318
|
+
expect(updated).toContain('**Guardrails**');
|
|
319
|
+
expect(updated).toContain(
|
|
320
|
+
'Validate with `openspec validate <id> --strict`'
|
|
321
|
+
);
|
|
322
|
+
expect(updated).not.toContain('Old slash content');
|
|
323
|
+
|
|
324
|
+
const [logMessage] = consoleSpy.mock.calls[0];
|
|
325
|
+
expect(logMessage).toContain(
|
|
326
|
+
'Updated OpenSpec instructions (openspec/AGENTS.md'
|
|
327
|
+
);
|
|
328
|
+
expect(logMessage).toContain('AGENTS.md (created)');
|
|
329
|
+
expect(logMessage).toContain(
|
|
330
|
+
'Updated slash commands: .clinerules/workflows/openspec-proposal.md'
|
|
331
|
+
);
|
|
332
|
+
|
|
333
|
+
consoleSpy.mockRestore();
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it('should refresh existing Cursor slash command files', async () => {
|
|
337
|
+
const cursorPath = path.join(testDir, '.cursor/commands/openspec-apply.md');
|
|
338
|
+
await fs.mkdir(path.dirname(cursorPath), { recursive: true });
|
|
339
|
+
const initialContent = `---
|
|
340
|
+
name: /openspec-apply
|
|
341
|
+
id: openspec-apply
|
|
342
|
+
category: OpenSpec
|
|
343
|
+
description: Old description
|
|
344
|
+
---
|
|
345
|
+
<!-- OPENSPEC:START -->
|
|
346
|
+
Old body
|
|
347
|
+
<!-- OPENSPEC:END -->`;
|
|
348
|
+
await fs.writeFile(cursorPath, initialContent);
|
|
349
|
+
|
|
350
|
+
const consoleSpy = vi.spyOn(console, 'log');
|
|
351
|
+
|
|
352
|
+
await updateCommand.execute(testDir);
|
|
353
|
+
|
|
354
|
+
const updated = await fs.readFile(cursorPath, 'utf-8');
|
|
355
|
+
expect(updated).toContain('id: openspec-apply');
|
|
356
|
+
expect(updated).toContain('Work through tasks sequentially');
|
|
357
|
+
expect(updated).not.toContain('Old body');
|
|
358
|
+
|
|
359
|
+
const [logMessage] = consoleSpy.mock.calls[0];
|
|
360
|
+
expect(logMessage).toContain(
|
|
361
|
+
'Updated OpenSpec instructions (openspec/AGENTS.md'
|
|
362
|
+
);
|
|
363
|
+
expect(logMessage).toContain('AGENTS.md (created)');
|
|
364
|
+
expect(logMessage).toContain(
|
|
365
|
+
'Updated slash commands: .cursor/commands/openspec-apply.md'
|
|
366
|
+
);
|
|
367
|
+
|
|
368
|
+
consoleSpy.mockRestore();
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it('should refresh existing OpenCode slash command files', async () => {
|
|
372
|
+
const openCodePath = path.join(
|
|
373
|
+
testDir,
|
|
374
|
+
'.opencode/command/openspec-apply.md'
|
|
375
|
+
);
|
|
376
|
+
await fs.mkdir(path.dirname(openCodePath), { recursive: true });
|
|
377
|
+
const initialContent = `---
|
|
378
|
+
name: /openspec-apply
|
|
379
|
+
id: openspec-apply
|
|
380
|
+
category: OpenSpec
|
|
381
|
+
description: Old description
|
|
382
|
+
---
|
|
383
|
+
<!-- OPENSPEC:START -->
|
|
384
|
+
Old body
|
|
385
|
+
<!-- OPENSPEC:END -->`;
|
|
386
|
+
await fs.writeFile(openCodePath, initialContent);
|
|
387
|
+
|
|
388
|
+
const consoleSpy = vi.spyOn(console, 'log');
|
|
389
|
+
|
|
390
|
+
await updateCommand.execute(testDir);
|
|
391
|
+
|
|
392
|
+
const updated = await fs.readFile(openCodePath, 'utf-8');
|
|
393
|
+
expect(updated).toContain('id: openspec-apply');
|
|
394
|
+
expect(updated).toContain('Work through tasks sequentially');
|
|
395
|
+
expect(updated).not.toContain('Old body');
|
|
396
|
+
|
|
397
|
+
const [logMessage] = consoleSpy.mock.calls[0];
|
|
398
|
+
expect(logMessage).toContain(
|
|
399
|
+
'Updated OpenSpec instructions (openspec/AGENTS.md'
|
|
400
|
+
);
|
|
401
|
+
expect(logMessage).toContain('AGENTS.md (created)');
|
|
402
|
+
expect(logMessage).toContain(
|
|
403
|
+
'Updated slash commands: .opencode/command/openspec-apply.md'
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
consoleSpy.mockRestore();
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
it('should refresh existing Kilo Code workflows', async () => {
|
|
410
|
+
const kilocodePath = path.join(
|
|
411
|
+
testDir,
|
|
412
|
+
'.kilocode/workflows/openspec-apply.md'
|
|
413
|
+
);
|
|
414
|
+
await fs.mkdir(path.dirname(kilocodePath), { recursive: true });
|
|
415
|
+
const initialContent = `<!-- OPENSPEC:START -->
|
|
416
|
+
Old body
|
|
417
|
+
<!-- OPENSPEC:END -->`;
|
|
418
|
+
await fs.writeFile(kilocodePath, initialContent);
|
|
419
|
+
|
|
420
|
+
const consoleSpy = vi.spyOn(console, 'log');
|
|
421
|
+
|
|
422
|
+
await updateCommand.execute(testDir);
|
|
423
|
+
|
|
424
|
+
const updated = await fs.readFile(kilocodePath, 'utf-8');
|
|
425
|
+
expect(updated).toContain('Work through tasks sequentially');
|
|
426
|
+
expect(updated).not.toContain('Old body');
|
|
427
|
+
expect(updated.startsWith('<!-- OPENSPEC:START -->')).toBe(true);
|
|
428
|
+
|
|
429
|
+
const [logMessage] = consoleSpy.mock.calls[0];
|
|
430
|
+
expect(logMessage).toContain(
|
|
431
|
+
'Updated slash commands: .kilocode/workflows/openspec-apply.md'
|
|
432
|
+
);
|
|
433
|
+
|
|
434
|
+
consoleSpy.mockRestore();
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
it('should refresh existing Windsurf workflows', async () => {
|
|
438
|
+
const wsPath = path.join(
|
|
439
|
+
testDir,
|
|
440
|
+
'.windsurf/workflows/openspec-apply.md'
|
|
441
|
+
);
|
|
442
|
+
await fs.mkdir(path.dirname(wsPath), { recursive: true });
|
|
443
|
+
const initialContent = `## OpenSpec: Apply (Windsurf)
|
|
444
|
+
Intro
|
|
445
|
+
<!-- OPENSPEC:START -->
|
|
446
|
+
Old body
|
|
447
|
+
<!-- OPENSPEC:END -->`;
|
|
448
|
+
await fs.writeFile(wsPath, initialContent);
|
|
449
|
+
|
|
450
|
+
const consoleSpy = vi.spyOn(console, 'log');
|
|
451
|
+
|
|
452
|
+
await updateCommand.execute(testDir);
|
|
453
|
+
|
|
454
|
+
const updated = await fs.readFile(wsPath, 'utf-8');
|
|
455
|
+
expect(updated).toContain('Work through tasks sequentially');
|
|
456
|
+
expect(updated).not.toContain('Old body');
|
|
457
|
+
expect(updated).toContain('## OpenSpec: Apply (Windsurf)');
|
|
458
|
+
|
|
459
|
+
const [logMessage] = consoleSpy.mock.calls[0];
|
|
460
|
+
expect(logMessage).toContain(
|
|
461
|
+
'Updated slash commands: .windsurf/workflows/openspec-apply.md'
|
|
462
|
+
);
|
|
463
|
+
consoleSpy.mockRestore();
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
it('should refresh existing Antigravity workflows', async () => {
|
|
467
|
+
const agPath = path.join(
|
|
468
|
+
testDir,
|
|
469
|
+
'.agent/workflows/openspec-apply.md'
|
|
470
|
+
);
|
|
471
|
+
await fs.mkdir(path.dirname(agPath), { recursive: true });
|
|
472
|
+
const initialContent = `---
|
|
473
|
+
description: Implement an approved OpenSpec change and keep tasks in sync.
|
|
474
|
+
---
|
|
475
|
+
|
|
476
|
+
<!-- OPENSPEC:START -->
|
|
477
|
+
Old body
|
|
478
|
+
<!-- OPENSPEC:END -->`;
|
|
479
|
+
await fs.writeFile(agPath, initialContent);
|
|
480
|
+
|
|
481
|
+
const consoleSpy = vi.spyOn(console, 'log');
|
|
482
|
+
|
|
483
|
+
await updateCommand.execute(testDir);
|
|
484
|
+
|
|
485
|
+
const updated = await fs.readFile(agPath, 'utf-8');
|
|
486
|
+
expect(updated).toContain('Work through tasks sequentially');
|
|
487
|
+
expect(updated).not.toContain('Old body');
|
|
488
|
+
expect(updated).toContain('description: Implement an approved OpenSpec change and keep tasks in sync.');
|
|
489
|
+
expect(updated).not.toContain('auto_execution_mode: 3');
|
|
490
|
+
|
|
491
|
+
const [logMessage] = consoleSpy.mock.calls[0];
|
|
492
|
+
expect(logMessage).toContain(
|
|
493
|
+
'Updated slash commands: .agent/workflows/openspec-apply.md'
|
|
494
|
+
);
|
|
495
|
+
consoleSpy.mockRestore();
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
it('should refresh existing Codex prompts', async () => {
|
|
499
|
+
const codexPath = path.join(
|
|
500
|
+
testDir,
|
|
501
|
+
'.codex/prompts/openspec-apply.md'
|
|
502
|
+
);
|
|
503
|
+
await fs.mkdir(path.dirname(codexPath), { recursive: true });
|
|
504
|
+
const initialContent = `---\ndescription: Old description\nargument-hint: old-hint\n---\n\n$ARGUMENTS\n<!-- OPENSPEC:START -->\nOld body\n<!-- OPENSPEC:END -->`;
|
|
505
|
+
await fs.writeFile(codexPath, initialContent);
|
|
506
|
+
|
|
507
|
+
const consoleSpy = vi.spyOn(console, 'log');
|
|
508
|
+
|
|
509
|
+
await updateCommand.execute(testDir);
|
|
510
|
+
|
|
511
|
+
const updated = await fs.readFile(codexPath, 'utf-8');
|
|
512
|
+
expect(updated).toContain('description: Implement an approved OpenSpec change and keep tasks in sync.');
|
|
513
|
+
expect(updated).toContain('argument-hint: change-id');
|
|
514
|
+
expect(updated).toContain('$ARGUMENTS');
|
|
515
|
+
expect(updated).toContain('Work through tasks sequentially');
|
|
516
|
+
expect(updated).not.toContain('Old body');
|
|
517
|
+
expect(updated).not.toContain('Old description');
|
|
518
|
+
|
|
519
|
+
const [logMessage] = consoleSpy.mock.calls[0];
|
|
520
|
+
expect(logMessage).toContain(
|
|
521
|
+
'Updated slash commands: .codex/prompts/openspec-apply.md'
|
|
522
|
+
);
|
|
523
|
+
|
|
524
|
+
consoleSpy.mockRestore();
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
it('should not create missing Codex prompts on update', async () => {
|
|
528
|
+
const codexApply = path.join(
|
|
529
|
+
testDir,
|
|
530
|
+
'.codex/prompts/openspec-apply.md'
|
|
531
|
+
);
|
|
532
|
+
|
|
533
|
+
// Only create apply; leave proposal and archive missing
|
|
534
|
+
await fs.mkdir(path.dirname(codexApply), { recursive: true });
|
|
535
|
+
await fs.writeFile(
|
|
536
|
+
codexApply,
|
|
537
|
+
'---\ndescription: Old\nargument-hint: old\n---\n\n$ARGUMENTS\n<!-- OPENSPEC:START -->\nOld\n<!-- OPENSPEC:END -->'
|
|
538
|
+
);
|
|
539
|
+
|
|
540
|
+
await updateCommand.execute(testDir);
|
|
541
|
+
|
|
542
|
+
const codexProposal = path.join(
|
|
543
|
+
testDir,
|
|
544
|
+
'.codex/prompts/openspec-proposal.md'
|
|
545
|
+
);
|
|
546
|
+
const codexArchive = path.join(
|
|
547
|
+
testDir,
|
|
548
|
+
'.codex/prompts/openspec-archive.md'
|
|
549
|
+
);
|
|
550
|
+
|
|
551
|
+
// Confirm they weren't created by update
|
|
552
|
+
await expect(FileSystemUtils.fileExists(codexProposal)).resolves.toBe(false);
|
|
553
|
+
await expect(FileSystemUtils.fileExists(codexArchive)).resolves.toBe(false);
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
it('should refresh existing GitHub Copilot prompts', async () => {
|
|
557
|
+
const ghPath = path.join(
|
|
558
|
+
testDir,
|
|
559
|
+
'.github/prompts/openspec-apply.prompt.md'
|
|
560
|
+
);
|
|
561
|
+
await fs.mkdir(path.dirname(ghPath), { recursive: true });
|
|
562
|
+
const initialContent = `---
|
|
563
|
+
description: Implement an approved OpenSpec change and keep tasks in sync.
|
|
564
|
+
---
|
|
565
|
+
|
|
566
|
+
$ARGUMENTS
|
|
567
|
+
<!-- OPENSPEC:START -->
|
|
568
|
+
Old body
|
|
569
|
+
<!-- OPENSPEC:END -->`;
|
|
570
|
+
await fs.writeFile(ghPath, initialContent);
|
|
571
|
+
|
|
572
|
+
const consoleSpy = vi.spyOn(console, 'log');
|
|
573
|
+
|
|
574
|
+
await updateCommand.execute(testDir);
|
|
575
|
+
|
|
576
|
+
const updated = await fs.readFile(ghPath, 'utf-8');
|
|
577
|
+
expect(updated).toContain('description: Implement an approved OpenSpec change and keep tasks in sync.');
|
|
578
|
+
expect(updated).toContain('$ARGUMENTS');
|
|
579
|
+
expect(updated).toContain('Work through tasks sequentially');
|
|
580
|
+
expect(updated).not.toContain('Old body');
|
|
581
|
+
|
|
582
|
+
const [logMessage] = consoleSpy.mock.calls[0];
|
|
583
|
+
expect(logMessage).toContain(
|
|
584
|
+
'Updated slash commands: .github/prompts/openspec-apply.prompt.md'
|
|
585
|
+
);
|
|
586
|
+
|
|
587
|
+
consoleSpy.mockRestore();
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
it('should not create missing GitHub Copilot prompts on update', async () => {
|
|
591
|
+
const ghApply = path.join(
|
|
592
|
+
testDir,
|
|
593
|
+
'.github/prompts/openspec-apply.prompt.md'
|
|
594
|
+
);
|
|
595
|
+
|
|
596
|
+
// Only create apply; leave proposal and archive missing
|
|
597
|
+
await fs.mkdir(path.dirname(ghApply), { recursive: true });
|
|
598
|
+
await fs.writeFile(
|
|
599
|
+
ghApply,
|
|
600
|
+
'---\ndescription: Old\n---\n\n$ARGUMENTS\n<!-- OPENSPEC:START -->\nOld\n<!-- OPENSPEC:END -->'
|
|
601
|
+
);
|
|
602
|
+
|
|
603
|
+
await updateCommand.execute(testDir);
|
|
604
|
+
|
|
605
|
+
const ghProposal = path.join(
|
|
606
|
+
testDir,
|
|
607
|
+
'.github/prompts/openspec-proposal.prompt.md'
|
|
608
|
+
);
|
|
609
|
+
const ghArchive = path.join(
|
|
610
|
+
testDir,
|
|
611
|
+
'.github/prompts/openspec-archive.prompt.md'
|
|
612
|
+
);
|
|
613
|
+
|
|
614
|
+
// Confirm they weren't created by update
|
|
615
|
+
await expect(FileSystemUtils.fileExists(ghProposal)).resolves.toBe(false);
|
|
616
|
+
await expect(FileSystemUtils.fileExists(ghArchive)).resolves.toBe(false);
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
it('should refresh existing Gemini CLI TOML files without creating new ones', async () => {
|
|
620
|
+
const geminiProposal = path.join(
|
|
621
|
+
testDir,
|
|
622
|
+
'.gemini/commands/openspec/proposal.toml'
|
|
623
|
+
);
|
|
624
|
+
await fs.mkdir(path.dirname(geminiProposal), { recursive: true });
|
|
625
|
+
const initialContent = `description = "Scaffold a new OpenSpec change and validate strictly."
|
|
626
|
+
|
|
627
|
+
prompt = """
|
|
628
|
+
<!-- OPENSPEC:START -->
|
|
629
|
+
Old Gemini body
|
|
630
|
+
<!-- OPENSPEC:END -->
|
|
631
|
+
"""
|
|
632
|
+
`;
|
|
633
|
+
await fs.writeFile(geminiProposal, initialContent);
|
|
634
|
+
|
|
635
|
+
const consoleSpy = vi.spyOn(console, 'log');
|
|
636
|
+
|
|
637
|
+
await updateCommand.execute(testDir);
|
|
638
|
+
|
|
639
|
+
const updated = await fs.readFile(geminiProposal, 'utf-8');
|
|
640
|
+
expect(updated).toContain('description = "Scaffold a new OpenSpec change and validate strictly."');
|
|
641
|
+
expect(updated).toContain('prompt = """');
|
|
642
|
+
expect(updated).toContain('<!-- OPENSPEC:START -->');
|
|
643
|
+
expect(updated).toContain('**Guardrails**');
|
|
644
|
+
expect(updated).toContain('<!-- OPENSPEC:END -->');
|
|
645
|
+
expect(updated).not.toContain('Old Gemini body');
|
|
646
|
+
|
|
647
|
+
const geminiApply = path.join(
|
|
648
|
+
testDir,
|
|
649
|
+
'.gemini/commands/openspec/apply.toml'
|
|
650
|
+
);
|
|
651
|
+
const geminiArchive = path.join(
|
|
652
|
+
testDir,
|
|
653
|
+
'.gemini/commands/openspec/archive.toml'
|
|
654
|
+
);
|
|
655
|
+
|
|
656
|
+
await expect(FileSystemUtils.fileExists(geminiApply)).resolves.toBe(false);
|
|
657
|
+
await expect(FileSystemUtils.fileExists(geminiArchive)).resolves.toBe(false);
|
|
658
|
+
|
|
659
|
+
const [logMessage] = consoleSpy.mock.calls[0];
|
|
660
|
+
expect(logMessage).toContain(
|
|
661
|
+
'Updated slash commands: .gemini/commands/openspec/proposal.toml'
|
|
662
|
+
);
|
|
663
|
+
|
|
664
|
+
consoleSpy.mockRestore();
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
it('should refresh existing IFLOW slash commands', async () => {
|
|
668
|
+
const iflowProposal = path.join(
|
|
669
|
+
testDir,
|
|
670
|
+
'.iflow/commands/openspec-proposal.md'
|
|
671
|
+
);
|
|
672
|
+
await fs.mkdir(path.dirname(iflowProposal), { recursive: true });
|
|
673
|
+
const initialContent = `description: Scaffold a new OpenSpec change and validate strictly."
|
|
674
|
+
|
|
675
|
+
prompt = """
|
|
676
|
+
<!-- OPENSPEC:START -->
|
|
677
|
+
Old IFlow body
|
|
678
|
+
<!-- OPENSPEC:END -->
|
|
679
|
+
"""
|
|
680
|
+
`;
|
|
681
|
+
await fs.writeFile(iflowProposal, initialContent);
|
|
682
|
+
|
|
683
|
+
const consoleSpy = vi.spyOn(console, 'log');
|
|
684
|
+
|
|
685
|
+
await updateCommand.execute(testDir);
|
|
686
|
+
|
|
687
|
+
const updated = await fs.readFile(iflowProposal, 'utf-8');
|
|
688
|
+
expect(updated).toContain('description: Scaffold a new OpenSpec change and validate strictly.');
|
|
689
|
+
expect(updated).toContain('<!-- OPENSPEC:START -->');
|
|
690
|
+
expect(updated).toContain('**Guardrails**');
|
|
691
|
+
expect(updated).toContain('<!-- OPENSPEC:END -->');
|
|
692
|
+
expect(updated).not.toContain('Old IFlow body');
|
|
693
|
+
|
|
694
|
+
const iflowApply = path.join(
|
|
695
|
+
testDir,
|
|
696
|
+
'.iflow/commands/openspec-apply.md'
|
|
697
|
+
);
|
|
698
|
+
const iflowArchive = path.join(
|
|
699
|
+
testDir,
|
|
700
|
+
'.iflow/commands/openspec-archive.md'
|
|
701
|
+
);
|
|
702
|
+
|
|
703
|
+
await expect(FileSystemUtils.fileExists(iflowApply)).resolves.toBe(false);
|
|
704
|
+
await expect(FileSystemUtils.fileExists(iflowArchive)).resolves.toBe(false);
|
|
705
|
+
|
|
706
|
+
const [logMessage] = consoleSpy.mock.calls[0];
|
|
707
|
+
expect(logMessage).toContain(
|
|
708
|
+
'Updated slash commands: .iflow/commands/openspec-proposal.md'
|
|
709
|
+
);
|
|
710
|
+
|
|
711
|
+
consoleSpy.mockRestore();
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
it('should refresh existing Factory slash commands', async () => {
|
|
715
|
+
const factoryPath = path.join(
|
|
716
|
+
testDir,
|
|
717
|
+
'.factory/commands/openspec-proposal.md'
|
|
718
|
+
);
|
|
719
|
+
await fs.mkdir(path.dirname(factoryPath), { recursive: true });
|
|
720
|
+
const initialContent = `---
|
|
721
|
+
description: Scaffold a new OpenSpec change and validate strictly.
|
|
722
|
+
argument-hint: request or feature description
|
|
723
|
+
---
|
|
724
|
+
|
|
725
|
+
<!-- OPENSPEC:START -->
|
|
726
|
+
Old body
|
|
727
|
+
<!-- OPENSPEC:END -->`;
|
|
728
|
+
await fs.writeFile(factoryPath, initialContent);
|
|
729
|
+
|
|
730
|
+
const consoleSpy = vi.spyOn(console, 'log');
|
|
731
|
+
|
|
732
|
+
await updateCommand.execute(testDir);
|
|
733
|
+
|
|
734
|
+
const updated = await fs.readFile(factoryPath, 'utf-8');
|
|
735
|
+
expect(updated).toContain('description: Scaffold a new OpenSpec change and validate strictly.');
|
|
736
|
+
expect(updated).toContain('argument-hint: request or feature description');
|
|
737
|
+
expect(
|
|
738
|
+
/<!-- OPENSPEC:START -->([\s\S]*?)<!-- OPENSPEC:END -->/u.exec(updated)?.[1]
|
|
739
|
+
).toContain('$ARGUMENTS');
|
|
740
|
+
expect(updated).toContain('**Guardrails**');
|
|
741
|
+
expect(updated).not.toContain('Old body');
|
|
742
|
+
|
|
743
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
744
|
+
expect.stringContaining('.factory/commands/openspec-proposal.md')
|
|
745
|
+
);
|
|
746
|
+
|
|
747
|
+
consoleSpy.mockRestore();
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
it('should not create missing Factory slash command files on update', async () => {
|
|
751
|
+
const factoryApply = path.join(
|
|
752
|
+
testDir,
|
|
753
|
+
'.factory/commands/openspec-apply.md'
|
|
754
|
+
);
|
|
755
|
+
|
|
756
|
+
await fs.mkdir(path.dirname(factoryApply), { recursive: true });
|
|
757
|
+
await fs.writeFile(
|
|
758
|
+
factoryApply,
|
|
759
|
+
`---
|
|
760
|
+
description: Old
|
|
761
|
+
argument-hint: old
|
|
762
|
+
---
|
|
763
|
+
|
|
764
|
+
<!-- OPENSPEC:START -->
|
|
765
|
+
Old body
|
|
766
|
+
<!-- OPENSPEC:END -->`
|
|
767
|
+
);
|
|
768
|
+
|
|
769
|
+
await updateCommand.execute(testDir);
|
|
770
|
+
|
|
771
|
+
const factoryProposal = path.join(
|
|
772
|
+
testDir,
|
|
773
|
+
'.factory/commands/openspec-proposal.md'
|
|
774
|
+
);
|
|
775
|
+
const factoryArchive = path.join(
|
|
776
|
+
testDir,
|
|
777
|
+
'.factory/commands/openspec-archive.md'
|
|
778
|
+
);
|
|
779
|
+
|
|
780
|
+
await expect(FileSystemUtils.fileExists(factoryProposal)).resolves.toBe(false);
|
|
781
|
+
await expect(FileSystemUtils.fileExists(factoryArchive)).resolves.toBe(false);
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
it('should refresh existing Amazon Q Developer prompts', async () => {
|
|
785
|
+
const aqPath = path.join(
|
|
786
|
+
testDir,
|
|
787
|
+
'.amazonq/prompts/openspec-apply.md'
|
|
788
|
+
);
|
|
789
|
+
await fs.mkdir(path.dirname(aqPath), { recursive: true });
|
|
790
|
+
const initialContent = `---
|
|
791
|
+
description: Implement an approved OpenSpec change and keep tasks in sync.
|
|
792
|
+
---
|
|
793
|
+
|
|
794
|
+
The user wants to apply the following change. Use the openspec instructions to implement the approved change.
|
|
795
|
+
|
|
796
|
+
<ChangeId>
|
|
797
|
+
$ARGUMENTS
|
|
798
|
+
</ChangeId>
|
|
799
|
+
<!-- OPENSPEC:START -->
|
|
800
|
+
Old body
|
|
801
|
+
<!-- OPENSPEC:END -->`;
|
|
802
|
+
await fs.writeFile(aqPath, initialContent);
|
|
803
|
+
|
|
804
|
+
const consoleSpy = vi.spyOn(console, 'log');
|
|
805
|
+
|
|
806
|
+
await updateCommand.execute(testDir);
|
|
807
|
+
|
|
808
|
+
const updatedContent = await fs.readFile(aqPath, 'utf-8');
|
|
809
|
+
expect(updatedContent).toContain('**Guardrails**');
|
|
810
|
+
expect(updatedContent).toContain('<!-- OPENSPEC:START -->');
|
|
811
|
+
expect(updatedContent).toContain('<!-- OPENSPEC:END -->');
|
|
812
|
+
expect(updatedContent).not.toContain('Old body');
|
|
813
|
+
|
|
814
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
815
|
+
expect.stringContaining('.amazonq/prompts/openspec-apply.md')
|
|
816
|
+
);
|
|
817
|
+
|
|
818
|
+
consoleSpy.mockRestore();
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
it('should not create missing Amazon Q Developer prompts on update', async () => {
|
|
822
|
+
const aqApply = path.join(
|
|
823
|
+
testDir,
|
|
824
|
+
'.amazonq/prompts/openspec-apply.md'
|
|
825
|
+
);
|
|
826
|
+
|
|
827
|
+
// Only create apply; leave proposal and archive missing
|
|
828
|
+
await fs.mkdir(path.dirname(aqApply), { recursive: true });
|
|
829
|
+
await fs.writeFile(
|
|
830
|
+
aqApply,
|
|
831
|
+
'---\ndescription: Old\n---\n\nThe user wants to apply the following change.\n\n<ChangeId>\n $ARGUMENTS\n</ChangeId>\n<!-- OPENSPEC:START -->\nOld\n<!-- OPENSPEC:END -->'
|
|
832
|
+
);
|
|
833
|
+
|
|
834
|
+
await updateCommand.execute(testDir);
|
|
835
|
+
|
|
836
|
+
const aqProposal = path.join(
|
|
837
|
+
testDir,
|
|
838
|
+
'.amazonq/prompts/openspec-proposal.md'
|
|
839
|
+
);
|
|
840
|
+
const aqArchive = path.join(
|
|
841
|
+
testDir,
|
|
842
|
+
'.amazonq/prompts/openspec-archive.md'
|
|
843
|
+
);
|
|
844
|
+
|
|
845
|
+
// Confirm they weren't created by update
|
|
846
|
+
await expect(FileSystemUtils.fileExists(aqProposal)).resolves.toBe(false);
|
|
847
|
+
await expect(FileSystemUtils.fileExists(aqArchive)).resolves.toBe(false);
|
|
848
|
+
});
|
|
849
|
+
|
|
850
|
+
it('should refresh existing Auggie slash command files', async () => {
|
|
851
|
+
const auggiePath = path.join(
|
|
852
|
+
testDir,
|
|
853
|
+
'.augment/commands/openspec-apply.md'
|
|
854
|
+
);
|
|
855
|
+
await fs.mkdir(path.dirname(auggiePath), { recursive: true });
|
|
856
|
+
const initialContent = `---
|
|
857
|
+
description: Implement an approved OpenSpec change and keep tasks in sync.
|
|
858
|
+
argument-hint: change-id
|
|
859
|
+
---
|
|
860
|
+
<!-- OPENSPEC:START -->
|
|
861
|
+
Old body
|
|
862
|
+
<!-- OPENSPEC:END -->`;
|
|
863
|
+
await fs.writeFile(auggiePath, initialContent);
|
|
864
|
+
|
|
865
|
+
const consoleSpy = vi.spyOn(console, 'log');
|
|
866
|
+
|
|
867
|
+
await updateCommand.execute(testDir);
|
|
868
|
+
|
|
869
|
+
const updatedContent = await fs.readFile(auggiePath, 'utf-8');
|
|
870
|
+
expect(updatedContent).toContain('**Guardrails**');
|
|
871
|
+
expect(updatedContent).toContain('<!-- OPENSPEC:START -->');
|
|
872
|
+
expect(updatedContent).toContain('<!-- OPENSPEC:END -->');
|
|
873
|
+
expect(updatedContent).not.toContain('Old body');
|
|
874
|
+
|
|
875
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
876
|
+
expect.stringContaining('.augment/commands/openspec-apply.md')
|
|
877
|
+
);
|
|
878
|
+
|
|
879
|
+
consoleSpy.mockRestore();
|
|
880
|
+
});
|
|
881
|
+
|
|
882
|
+
it('should not create missing Auggie slash command files on update', async () => {
|
|
883
|
+
const auggieApply = path.join(
|
|
884
|
+
testDir,
|
|
885
|
+
'.augment/commands/openspec-apply.md'
|
|
886
|
+
);
|
|
887
|
+
|
|
888
|
+
// Only create apply; leave proposal and archive missing
|
|
889
|
+
await fs.mkdir(path.dirname(auggieApply), { recursive: true });
|
|
890
|
+
await fs.writeFile(
|
|
891
|
+
auggieApply,
|
|
892
|
+
'---\ndescription: Old\nargument-hint: old\n---\n<!-- OPENSPEC:START -->\nOld\n<!-- OPENSPEC:END -->'
|
|
893
|
+
);
|
|
894
|
+
|
|
895
|
+
await updateCommand.execute(testDir);
|
|
896
|
+
|
|
897
|
+
const auggieProposal = path.join(
|
|
898
|
+
testDir,
|
|
899
|
+
'.augment/commands/openspec-proposal.md'
|
|
900
|
+
);
|
|
901
|
+
const auggieArchive = path.join(
|
|
902
|
+
testDir,
|
|
903
|
+
'.augment/commands/openspec-archive.md'
|
|
904
|
+
);
|
|
905
|
+
|
|
906
|
+
// Confirm they weren't created by update
|
|
907
|
+
await expect(FileSystemUtils.fileExists(auggieProposal)).resolves.toBe(false);
|
|
908
|
+
await expect(FileSystemUtils.fileExists(auggieArchive)).resolves.toBe(false);
|
|
909
|
+
});
|
|
910
|
+
|
|
911
|
+
it('should refresh existing CodeBuddy slash command files', async () => {
|
|
912
|
+
const codeBuddyPath = path.join(
|
|
913
|
+
testDir,
|
|
914
|
+
'.codebuddy/commands/openspec/proposal.md'
|
|
915
|
+
);
|
|
916
|
+
await fs.mkdir(path.dirname(codeBuddyPath), { recursive: true });
|
|
917
|
+
const initialContent = `---
|
|
918
|
+
name: OpenSpec: Proposal
|
|
919
|
+
description: Old description
|
|
920
|
+
category: OpenSpec
|
|
921
|
+
tags: [openspec, change]
|
|
922
|
+
---
|
|
923
|
+
<!-- OPENSPEC:START -->
|
|
924
|
+
Old slash content
|
|
925
|
+
<!-- OPENSPEC:END -->`;
|
|
926
|
+
await fs.writeFile(codeBuddyPath, initialContent);
|
|
927
|
+
|
|
928
|
+
const consoleSpy = vi.spyOn(console, 'log');
|
|
929
|
+
|
|
930
|
+
await updateCommand.execute(testDir);
|
|
931
|
+
|
|
932
|
+
const updated = await fs.readFile(codeBuddyPath, 'utf-8');
|
|
933
|
+
expect(updated).toContain('name: OpenSpec: Proposal');
|
|
934
|
+
expect(updated).toContain('**Guardrails**');
|
|
935
|
+
expect(updated).toContain(
|
|
936
|
+
'Validate with `openspec validate <id> --strict`'
|
|
937
|
+
);
|
|
938
|
+
expect(updated).not.toContain('Old slash content');
|
|
939
|
+
|
|
940
|
+
const [logMessage] = consoleSpy.mock.calls[0];
|
|
941
|
+
expect(logMessage).toContain(
|
|
942
|
+
'Updated OpenSpec instructions (openspec/AGENTS.md'
|
|
943
|
+
);
|
|
944
|
+
expect(logMessage).toContain('AGENTS.md (created)');
|
|
945
|
+
expect(logMessage).toContain(
|
|
946
|
+
'Updated slash commands: .codebuddy/commands/openspec/proposal.md'
|
|
947
|
+
);
|
|
948
|
+
|
|
949
|
+
consoleSpy.mockRestore();
|
|
950
|
+
});
|
|
951
|
+
|
|
952
|
+
it('should not create missing CodeBuddy slash command files on update', async () => {
|
|
953
|
+
const codeBuddyApply = path.join(
|
|
954
|
+
testDir,
|
|
955
|
+
'.codebuddy/commands/openspec/apply.md'
|
|
956
|
+
);
|
|
957
|
+
|
|
958
|
+
// Only create apply; leave proposal and archive missing
|
|
959
|
+
await fs.mkdir(path.dirname(codeBuddyApply), { recursive: true });
|
|
960
|
+
await fs.writeFile(
|
|
961
|
+
codeBuddyApply,
|
|
962
|
+
`---
|
|
963
|
+
name: OpenSpec: Apply
|
|
964
|
+
description: Old description
|
|
965
|
+
category: OpenSpec
|
|
966
|
+
tags: [openspec, apply]
|
|
967
|
+
---
|
|
968
|
+
<!-- OPENSPEC:START -->
|
|
969
|
+
Old body
|
|
970
|
+
<!-- OPENSPEC:END -->`
|
|
971
|
+
);
|
|
972
|
+
|
|
973
|
+
await updateCommand.execute(testDir);
|
|
974
|
+
|
|
975
|
+
const codeBuddyProposal = path.join(
|
|
976
|
+
testDir,
|
|
977
|
+
'.codebuddy/commands/openspec/proposal.md'
|
|
978
|
+
);
|
|
979
|
+
const codeBuddyArchive = path.join(
|
|
980
|
+
testDir,
|
|
981
|
+
'.codebuddy/commands/openspec/archive.md'
|
|
982
|
+
);
|
|
983
|
+
|
|
984
|
+
// Confirm they weren't created by update
|
|
985
|
+
await expect(FileSystemUtils.fileExists(codeBuddyProposal)).resolves.toBe(false);
|
|
986
|
+
await expect(FileSystemUtils.fileExists(codeBuddyArchive)).resolves.toBe(false);
|
|
987
|
+
});
|
|
988
|
+
|
|
989
|
+
it('should refresh existing Crush slash command files', async () => {
|
|
990
|
+
const crushPath = path.join(
|
|
991
|
+
testDir,
|
|
992
|
+
'.crush/commands/openspec/proposal.md'
|
|
993
|
+
);
|
|
994
|
+
await fs.mkdir(path.dirname(crushPath), { recursive: true });
|
|
995
|
+
const initialContent = `---
|
|
996
|
+
name: OpenSpec: Proposal
|
|
997
|
+
description: Old description
|
|
998
|
+
category: OpenSpec
|
|
999
|
+
tags: [openspec, change]
|
|
1000
|
+
---
|
|
1001
|
+
<!-- OPENSPEC:START -->
|
|
1002
|
+
Old slash content
|
|
1003
|
+
<!-- OPENSPEC:END -->`;
|
|
1004
|
+
await fs.writeFile(crushPath, initialContent);
|
|
1005
|
+
|
|
1006
|
+
const consoleSpy = vi.spyOn(console, 'log');
|
|
1007
|
+
|
|
1008
|
+
await updateCommand.execute(testDir);
|
|
1009
|
+
|
|
1010
|
+
const updated = await fs.readFile(crushPath, 'utf-8');
|
|
1011
|
+
expect(updated).toContain('name: OpenSpec: Proposal');
|
|
1012
|
+
expect(updated).toContain('**Guardrails**');
|
|
1013
|
+
expect(updated).toContain(
|
|
1014
|
+
'Validate with `openspec validate <id> --strict`'
|
|
1015
|
+
);
|
|
1016
|
+
expect(updated).not.toContain('Old slash content');
|
|
1017
|
+
|
|
1018
|
+
const [logMessage] = consoleSpy.mock.calls[0];
|
|
1019
|
+
expect(logMessage).toContain(
|
|
1020
|
+
'Updated OpenSpec instructions (openspec/AGENTS.md'
|
|
1021
|
+
);
|
|
1022
|
+
expect(logMessage).toContain('AGENTS.md (created)');
|
|
1023
|
+
expect(logMessage).toContain(
|
|
1024
|
+
'Updated slash commands: .crush/commands/openspec/proposal.md'
|
|
1025
|
+
);
|
|
1026
|
+
|
|
1027
|
+
consoleSpy.mockRestore();
|
|
1028
|
+
});
|
|
1029
|
+
|
|
1030
|
+
it('should not create missing Crush slash command files on update', async () => {
|
|
1031
|
+
const crushApply = path.join(
|
|
1032
|
+
testDir,
|
|
1033
|
+
'.crush/commands/openspec-apply.md'
|
|
1034
|
+
);
|
|
1035
|
+
|
|
1036
|
+
// Only create apply; leave proposal and archive missing
|
|
1037
|
+
await fs.mkdir(path.dirname(crushApply), { recursive: true });
|
|
1038
|
+
await fs.writeFile(
|
|
1039
|
+
crushApply,
|
|
1040
|
+
`---
|
|
1041
|
+
name: OpenSpec: Apply
|
|
1042
|
+
description: Old description
|
|
1043
|
+
category: OpenSpec
|
|
1044
|
+
tags: [openspec, apply]
|
|
1045
|
+
---
|
|
1046
|
+
<!-- OPENSPEC:START -->
|
|
1047
|
+
Old body
|
|
1048
|
+
<!-- OPENSPEC:END -->`
|
|
1049
|
+
);
|
|
1050
|
+
|
|
1051
|
+
await updateCommand.execute(testDir);
|
|
1052
|
+
|
|
1053
|
+
const crushProposal = path.join(
|
|
1054
|
+
testDir,
|
|
1055
|
+
'.crush/commands/openspec-proposal.md'
|
|
1056
|
+
);
|
|
1057
|
+
const crushArchive = path.join(
|
|
1058
|
+
testDir,
|
|
1059
|
+
'.crush/commands/openspec-archive.md'
|
|
1060
|
+
);
|
|
1061
|
+
|
|
1062
|
+
// Confirm they weren't created by update
|
|
1063
|
+
await expect(FileSystemUtils.fileExists(crushProposal)).resolves.toBe(false);
|
|
1064
|
+
await expect(FileSystemUtils.fileExists(crushArchive)).resolves.toBe(false);
|
|
1065
|
+
});
|
|
1066
|
+
|
|
1067
|
+
it('should refresh existing CoStrict slash command files', async () => {
|
|
1068
|
+
const costrictPath = path.join(
|
|
1069
|
+
testDir,
|
|
1070
|
+
'.cospec/openspec/commands/openspec-proposal.md'
|
|
1071
|
+
);
|
|
1072
|
+
await fs.mkdir(path.dirname(costrictPath), { recursive: true });
|
|
1073
|
+
const initialContent = `---
|
|
1074
|
+
description: "Old description"
|
|
1075
|
+
argument-hint: old-hint
|
|
1076
|
+
---
|
|
1077
|
+
<!-- OPENSPEC:START -->
|
|
1078
|
+
Old body
|
|
1079
|
+
<!-- OPENSPEC:END -->`;
|
|
1080
|
+
await fs.writeFile(costrictPath, initialContent);
|
|
1081
|
+
|
|
1082
|
+
const consoleSpy = vi.spyOn(console, 'log');
|
|
1083
|
+
|
|
1084
|
+
await updateCommand.execute(testDir);
|
|
1085
|
+
|
|
1086
|
+
const updated = await fs.readFile(costrictPath, 'utf-8');
|
|
1087
|
+
// For slash commands, only the content between OpenSpec markers is updated
|
|
1088
|
+
expect(updated).toContain('description: "Old description"');
|
|
1089
|
+
expect(updated).toContain('argument-hint: old-hint');
|
|
1090
|
+
expect(updated).toContain('**Guardrails**');
|
|
1091
|
+
expect(updated).toContain(
|
|
1092
|
+
'Validate with `openspec validate <id> --strict`'
|
|
1093
|
+
);
|
|
1094
|
+
expect(updated).not.toContain('Old body');
|
|
1095
|
+
|
|
1096
|
+
const [logMessage] = consoleSpy.mock.calls[0];
|
|
1097
|
+
expect(logMessage).toContain(
|
|
1098
|
+
'Updated OpenSpec instructions (openspec/AGENTS.md'
|
|
1099
|
+
);
|
|
1100
|
+
expect(logMessage).toContain('AGENTS.md (created)');
|
|
1101
|
+
expect(logMessage).toContain(
|
|
1102
|
+
'Updated slash commands: .cospec/openspec/commands/openspec-proposal.md'
|
|
1103
|
+
);
|
|
1104
|
+
|
|
1105
|
+
consoleSpy.mockRestore();
|
|
1106
|
+
});
|
|
1107
|
+
|
|
1108
|
+
it('should refresh existing Qoder slash command files', async () => {
|
|
1109
|
+
const qoderPath = path.join(
|
|
1110
|
+
testDir,
|
|
1111
|
+
'.qoder/commands/openspec/proposal.md'
|
|
1112
|
+
);
|
|
1113
|
+
await fs.mkdir(path.dirname(qoderPath), { recursive: true });
|
|
1114
|
+
const initialContent = `---
|
|
1115
|
+
name: OpenSpec: Proposal
|
|
1116
|
+
description: Old description
|
|
1117
|
+
category: OpenSpec
|
|
1118
|
+
tags: [openspec, change]
|
|
1119
|
+
---
|
|
1120
|
+
<!-- OPENSPEC:START -->
|
|
1121
|
+
Old slash content
|
|
1122
|
+
<!-- OPENSPEC:END -->`;
|
|
1123
|
+
await fs.writeFile(qoderPath, initialContent);
|
|
1124
|
+
|
|
1125
|
+
const consoleSpy = vi.spyOn(console, 'log');
|
|
1126
|
+
|
|
1127
|
+
await updateCommand.execute(testDir);
|
|
1128
|
+
|
|
1129
|
+
const updated = await fs.readFile(qoderPath, 'utf-8');
|
|
1130
|
+
expect(updated).toContain('name: OpenSpec: Proposal');
|
|
1131
|
+
expect(updated).toContain('**Guardrails**');
|
|
1132
|
+
expect(updated).toContain(
|
|
1133
|
+
'Validate with `openspec validate <id> --strict`'
|
|
1134
|
+
);
|
|
1135
|
+
expect(updated).not.toContain('Old slash content');
|
|
1136
|
+
|
|
1137
|
+
const [logMessage] = consoleSpy.mock.calls[0];
|
|
1138
|
+
expect(logMessage).toContain(
|
|
1139
|
+
'Updated OpenSpec instructions (openspec/AGENTS.md'
|
|
1140
|
+
);
|
|
1141
|
+
expect(logMessage).toContain('AGENTS.md (created)');
|
|
1142
|
+
expect(logMessage).toContain(
|
|
1143
|
+
'Updated slash commands: .qoder/commands/openspec/proposal.md'
|
|
1144
|
+
);
|
|
1145
|
+
|
|
1146
|
+
consoleSpy.mockRestore();
|
|
1147
|
+
});
|
|
1148
|
+
|
|
1149
|
+
it('should refresh existing RooCode slash command files', async () => {
|
|
1150
|
+
const rooPath = path.join(
|
|
1151
|
+
testDir,
|
|
1152
|
+
'.roo/commands/openspec-proposal.md'
|
|
1153
|
+
);
|
|
1154
|
+
await fs.mkdir(path.dirname(rooPath), { recursive: true });
|
|
1155
|
+
const initialContent = `# OpenSpec: Proposal
|
|
1156
|
+
|
|
1157
|
+
Old description
|
|
1158
|
+
|
|
1159
|
+
<!-- OPENSPEC:START -->
|
|
1160
|
+
Old body
|
|
1161
|
+
<!-- OPENSPEC:END -->`;
|
|
1162
|
+
await fs.writeFile(rooPath, initialContent);
|
|
1163
|
+
|
|
1164
|
+
const consoleSpy = vi.spyOn(console, 'log');
|
|
1165
|
+
|
|
1166
|
+
await updateCommand.execute(testDir);
|
|
1167
|
+
|
|
1168
|
+
const updated = await fs.readFile(rooPath, 'utf-8');
|
|
1169
|
+
// For RooCode, the header is Markdown, preserve it and update only managed block
|
|
1170
|
+
expect(updated).toContain('# OpenSpec: Proposal');
|
|
1171
|
+
expect(updated).toContain('**Guardrails**');
|
|
1172
|
+
expect(updated).toContain(
|
|
1173
|
+
'Validate with `openspec validate <id> --strict`'
|
|
1174
|
+
);
|
|
1175
|
+
expect(updated).not.toContain('Old body');
|
|
1176
|
+
|
|
1177
|
+
const [logMessage] = consoleSpy.mock.calls[0];
|
|
1178
|
+
expect(logMessage).toContain(
|
|
1179
|
+
'Updated OpenSpec instructions (openspec/AGENTS.md'
|
|
1180
|
+
);
|
|
1181
|
+
expect(logMessage).toContain('AGENTS.md (created)');
|
|
1182
|
+
expect(logMessage).toContain(
|
|
1183
|
+
'Updated slash commands: .roo/commands/openspec-proposal.md'
|
|
1184
|
+
);
|
|
1185
|
+
|
|
1186
|
+
consoleSpy.mockRestore();
|
|
1187
|
+
});
|
|
1188
|
+
|
|
1189
|
+
it('should not create missing RooCode slash command files on update', async () => {
|
|
1190
|
+
const rooApply = path.join(
|
|
1191
|
+
testDir,
|
|
1192
|
+
'.roo/commands/openspec-apply.md'
|
|
1193
|
+
);
|
|
1194
|
+
|
|
1195
|
+
// Only create apply; leave proposal and archive missing
|
|
1196
|
+
await fs.mkdir(path.dirname(rooApply), { recursive: true });
|
|
1197
|
+
await fs.writeFile(
|
|
1198
|
+
rooApply,
|
|
1199
|
+
`# OpenSpec: Apply
|
|
1200
|
+
|
|
1201
|
+
<!-- OPENSPEC:START -->
|
|
1202
|
+
Old body
|
|
1203
|
+
<!-- OPENSPEC:END -->`
|
|
1204
|
+
);
|
|
1205
|
+
|
|
1206
|
+
await updateCommand.execute(testDir);
|
|
1207
|
+
|
|
1208
|
+
const rooProposal = path.join(
|
|
1209
|
+
testDir,
|
|
1210
|
+
'.roo/commands/openspec-proposal.md'
|
|
1211
|
+
);
|
|
1212
|
+
const rooArchive = path.join(
|
|
1213
|
+
testDir,
|
|
1214
|
+
'.roo/commands/openspec-archive.md'
|
|
1215
|
+
);
|
|
1216
|
+
|
|
1217
|
+
// Confirm they weren't created by update
|
|
1218
|
+
await expect(FileSystemUtils.fileExists(rooProposal)).resolves.toBe(false);
|
|
1219
|
+
await expect(FileSystemUtils.fileExists(rooArchive)).resolves.toBe(false);
|
|
1220
|
+
});
|
|
1221
|
+
|
|
1222
|
+
it('should not create missing CoStrict slash command files on update', async () => {
|
|
1223
|
+
const costrictApply = path.join(
|
|
1224
|
+
testDir,
|
|
1225
|
+
'.cospec/openspec/commands/openspec-apply.md'
|
|
1226
|
+
);
|
|
1227
|
+
|
|
1228
|
+
// Only create apply; leave proposal and archive missing
|
|
1229
|
+
await fs.mkdir(path.dirname(costrictApply), { recursive: true });
|
|
1230
|
+
await fs.writeFile(
|
|
1231
|
+
costrictApply,
|
|
1232
|
+
`---
|
|
1233
|
+
description: "Old"
|
|
1234
|
+
argument-hint: old
|
|
1235
|
+
---
|
|
1236
|
+
<!-- OPENSPEC:START -->
|
|
1237
|
+
Old
|
|
1238
|
+
<!-- OPENSPEC:END -->`
|
|
1239
|
+
);
|
|
1240
|
+
|
|
1241
|
+
await updateCommand.execute(testDir);
|
|
1242
|
+
|
|
1243
|
+
const costrictProposal = path.join(
|
|
1244
|
+
testDir,
|
|
1245
|
+
'.cospec/openspec/commands/openspec-proposal.md'
|
|
1246
|
+
);
|
|
1247
|
+
const costrictArchive = path.join(
|
|
1248
|
+
testDir,
|
|
1249
|
+
'.cospec/openspec/commands/openspec-archive.md'
|
|
1250
|
+
);
|
|
1251
|
+
|
|
1252
|
+
// Confirm they weren't created by update
|
|
1253
|
+
await expect(FileSystemUtils.fileExists(costrictProposal)).resolves.toBe(false);
|
|
1254
|
+
await expect(FileSystemUtils.fileExists(costrictArchive)).resolves.toBe(false);
|
|
1255
|
+
});
|
|
1256
|
+
|
|
1257
|
+
it('should not create missing Qoder slash command files on update', async () => {
|
|
1258
|
+
const qoderApply = path.join(
|
|
1259
|
+
testDir,
|
|
1260
|
+
'.qoder/commands/openspec/apply.md'
|
|
1261
|
+
);
|
|
1262
|
+
|
|
1263
|
+
// Only create apply; leave proposal and archive missing
|
|
1264
|
+
await fs.mkdir(path.dirname(qoderApply), { recursive: true });
|
|
1265
|
+
await fs.writeFile(
|
|
1266
|
+
qoderApply,
|
|
1267
|
+
`---
|
|
1268
|
+
name: OpenSpec: Apply
|
|
1269
|
+
description: Old description
|
|
1270
|
+
category: OpenSpec
|
|
1271
|
+
tags: [openspec, apply]
|
|
1272
|
+
---
|
|
1273
|
+
<!-- OPENSPEC:START -->
|
|
1274
|
+
Old body
|
|
1275
|
+
<!-- OPENSPEC:END -->`
|
|
1276
|
+
);
|
|
1277
|
+
|
|
1278
|
+
await updateCommand.execute(testDir);
|
|
1279
|
+
|
|
1280
|
+
const qoderProposal = path.join(
|
|
1281
|
+
testDir,
|
|
1282
|
+
'.qoder/commands/openspec/proposal.md'
|
|
1283
|
+
);
|
|
1284
|
+
const qoderArchive = path.join(
|
|
1285
|
+
testDir,
|
|
1286
|
+
'.qoder/commands/openspec/archive.md'
|
|
1287
|
+
);
|
|
1288
|
+
|
|
1289
|
+
// Confirm they weren't created by update
|
|
1290
|
+
await expect(FileSystemUtils.fileExists(qoderProposal)).resolves.toBe(false);
|
|
1291
|
+
await expect(FileSystemUtils.fileExists(qoderArchive)).resolves.toBe(false);
|
|
1292
|
+
});
|
|
1293
|
+
|
|
1294
|
+
it('should update only existing COSTRICT.md file', async () => {
|
|
1295
|
+
// Create COSTRICT.md file with initial content
|
|
1296
|
+
const costrictPath = path.join(testDir, 'COSTRICT.md');
|
|
1297
|
+
const initialContent = `# CoStrict Instructions
|
|
1298
|
+
|
|
1299
|
+
Some existing CoStrict instructions here.
|
|
1300
|
+
|
|
1301
|
+
<!-- OPENSPEC:START -->
|
|
1302
|
+
Old OpenSpec content
|
|
1303
|
+
<!-- OPENSPEC:END -->
|
|
1304
|
+
|
|
1305
|
+
More instructions after.`;
|
|
1306
|
+
await fs.writeFile(costrictPath, initialContent);
|
|
1307
|
+
|
|
1308
|
+
const consoleSpy = vi.spyOn(console, 'log');
|
|
1309
|
+
|
|
1310
|
+
// Execute update command
|
|
1311
|
+
await updateCommand.execute(testDir);
|
|
1312
|
+
|
|
1313
|
+
// Check that COSTRICT.md was updated
|
|
1314
|
+
const updatedContent = await fs.readFile(costrictPath, 'utf-8');
|
|
1315
|
+
expect(updatedContent).toContain('<!-- OPENSPEC:START -->');
|
|
1316
|
+
expect(updatedContent).toContain('<!-- OPENSPEC:END -->');
|
|
1317
|
+
expect(updatedContent).toContain("@/openspec/AGENTS.md");
|
|
1318
|
+
expect(updatedContent).toContain('openspec update');
|
|
1319
|
+
expect(updatedContent).toContain('Some existing CoStrict instructions here');
|
|
1320
|
+
expect(updatedContent).toContain('More instructions after');
|
|
1321
|
+
|
|
1322
|
+
// Check console output
|
|
1323
|
+
const [logMessage] = consoleSpy.mock.calls[0];
|
|
1324
|
+
expect(logMessage).toContain(
|
|
1325
|
+
'Updated OpenSpec instructions (openspec/AGENTS.md'
|
|
1326
|
+
);
|
|
1327
|
+
expect(logMessage).toContain('AGENTS.md (created)');
|
|
1328
|
+
expect(logMessage).toContain('Updated AI tool files: COSTRICT.md');
|
|
1329
|
+
consoleSpy.mockRestore();
|
|
1330
|
+
});
|
|
1331
|
+
|
|
1332
|
+
|
|
1333
|
+
it('should not create COSTRICT.md if it does not exist', async () => {
|
|
1334
|
+
// Ensure COSTRICT.md does not exist
|
|
1335
|
+
const costrictPath = path.join(testDir, 'COSTRICT.md');
|
|
1336
|
+
|
|
1337
|
+
// Execute update command
|
|
1338
|
+
await updateCommand.execute(testDir);
|
|
1339
|
+
|
|
1340
|
+
// Check that COSTRICT.md was not created
|
|
1341
|
+
const fileExists = await FileSystemUtils.fileExists(costrictPath);
|
|
1342
|
+
expect(fileExists).toBe(false);
|
|
1343
|
+
});
|
|
1344
|
+
|
|
1345
|
+
it('should preserve CoStrict content outside markers during update', async () => {
|
|
1346
|
+
const costrictPath = path.join(
|
|
1347
|
+
testDir,
|
|
1348
|
+
'.cospec/openspec/commands/openspec-proposal.md'
|
|
1349
|
+
);
|
|
1350
|
+
await fs.mkdir(path.dirname(costrictPath), { recursive: true });
|
|
1351
|
+
const initialContent = `## Custom Intro Title\nSome intro text\n<!-- OPENSPEC:START -->\nOld body\n<!-- OPENSPEC:END -->\n\nFooter stays`;
|
|
1352
|
+
await fs.writeFile(costrictPath, initialContent);
|
|
1353
|
+
|
|
1354
|
+
await updateCommand.execute(testDir);
|
|
1355
|
+
|
|
1356
|
+
const updated = await fs.readFile(costrictPath, 'utf-8');
|
|
1357
|
+
expect(updated).toContain('## Custom Intro Title');
|
|
1358
|
+
expect(updated).toContain('Footer stays');
|
|
1359
|
+
expect(updated).not.toContain('Old body');
|
|
1360
|
+
expect(updated).toContain('Validate with `openspec validate <id> --strict`');
|
|
1361
|
+
});
|
|
1362
|
+
|
|
1363
|
+
it('should handle configurator errors gracefully for CoStrict', async () => {
|
|
1364
|
+
// Create COSTRICT.md file but make it read-only to cause an error
|
|
1365
|
+
const costrictPath = path.join(testDir, 'COSTRICT.md');
|
|
1366
|
+
await fs.writeFile(
|
|
1367
|
+
costrictPath,
|
|
1368
|
+
'<!-- OPENSPEC:START -->\nOld\n<!-- OPENSPEC:END -->'
|
|
1369
|
+
);
|
|
1370
|
+
|
|
1371
|
+
const consoleSpy = vi.spyOn(console, 'log');
|
|
1372
|
+
const errorSpy = vi.spyOn(console, 'error');
|
|
1373
|
+
const originalWriteFile = FileSystemUtils.writeFile.bind(FileSystemUtils);
|
|
1374
|
+
const writeSpy = vi
|
|
1375
|
+
.spyOn(FileSystemUtils, 'writeFile')
|
|
1376
|
+
.mockImplementation(async (filePath, content) => {
|
|
1377
|
+
if (filePath.endsWith('COSTRICT.md')) {
|
|
1378
|
+
throw new Error('EACCES: permission denied, open');
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
return originalWriteFile(filePath, content);
|
|
1382
|
+
});
|
|
1383
|
+
|
|
1384
|
+
// Execute update command - should not throw
|
|
1385
|
+
await updateCommand.execute(testDir);
|
|
1386
|
+
|
|
1387
|
+
// Should report the failure
|
|
1388
|
+
expect(errorSpy).toHaveBeenCalled();
|
|
1389
|
+
const [logMessage] = consoleSpy.mock.calls[0];
|
|
1390
|
+
expect(logMessage).toContain(
|
|
1391
|
+
'Updated OpenSpec instructions (openspec/AGENTS.md'
|
|
1392
|
+
);
|
|
1393
|
+
expect(logMessage).toContain('AGENTS.md (created)');
|
|
1394
|
+
expect(logMessage).toContain('Failed to update: COSTRICT.md');
|
|
1395
|
+
|
|
1396
|
+
consoleSpy.mockRestore();
|
|
1397
|
+
errorSpy.mockRestore();
|
|
1398
|
+
writeSpy.mockRestore();
|
|
1399
|
+
});
|
|
1400
|
+
|
|
1401
|
+
it('should preserve Windsurf content outside markers during update', async () => {
|
|
1402
|
+
const wsPath = path.join(
|
|
1403
|
+
testDir,
|
|
1404
|
+
'.windsurf/workflows/openspec-proposal.md'
|
|
1405
|
+
);
|
|
1406
|
+
await fs.mkdir(path.dirname(wsPath), { recursive: true });
|
|
1407
|
+
const initialContent = `## Custom Intro Title\nSome intro text\n<!-- OPENSPEC:START -->\nOld body\n<!-- OPENSPEC:END -->\n\nFooter stays`;
|
|
1408
|
+
await fs.writeFile(wsPath, initialContent);
|
|
1409
|
+
|
|
1410
|
+
await updateCommand.execute(testDir);
|
|
1411
|
+
|
|
1412
|
+
const updated = await fs.readFile(wsPath, 'utf-8');
|
|
1413
|
+
expect(updated).toContain('## Custom Intro Title');
|
|
1414
|
+
expect(updated).toContain('Footer stays');
|
|
1415
|
+
expect(updated).not.toContain('Old body');
|
|
1416
|
+
expect(updated).toContain('Validate with `openspec validate <id> --strict`');
|
|
1417
|
+
});
|
|
1418
|
+
|
|
1419
|
+
it('should not create missing Windsurf workflows on update', async () => {
|
|
1420
|
+
const wsApply = path.join(
|
|
1421
|
+
testDir,
|
|
1422
|
+
'.windsurf/workflows/openspec-apply.md'
|
|
1423
|
+
);
|
|
1424
|
+
// Only create apply; leave proposal and archive missing
|
|
1425
|
+
await fs.mkdir(path.dirname(wsApply), { recursive: true });
|
|
1426
|
+
await fs.writeFile(
|
|
1427
|
+
wsApply,
|
|
1428
|
+
'<!-- OPENSPEC:START -->\nOld\n<!-- OPENSPEC:END -->'
|
|
1429
|
+
);
|
|
1430
|
+
|
|
1431
|
+
await updateCommand.execute(testDir);
|
|
1432
|
+
|
|
1433
|
+
const wsProposal = path.join(
|
|
1434
|
+
testDir,
|
|
1435
|
+
'.windsurf/workflows/openspec-proposal.md'
|
|
1436
|
+
);
|
|
1437
|
+
const wsArchive = path.join(
|
|
1438
|
+
testDir,
|
|
1439
|
+
'.windsurf/workflows/openspec-archive.md'
|
|
1440
|
+
);
|
|
1441
|
+
|
|
1442
|
+
// Confirm they weren't created by update
|
|
1443
|
+
await expect(FileSystemUtils.fileExists(wsProposal)).resolves.toBe(false);
|
|
1444
|
+
await expect(FileSystemUtils.fileExists(wsArchive)).resolves.toBe(false);
|
|
1445
|
+
});
|
|
1446
|
+
|
|
1447
|
+
it('should handle no AI tool files present', async () => {
|
|
1448
|
+
// Execute update command with no AI tool files
|
|
1449
|
+
const consoleSpy = vi.spyOn(console, 'log');
|
|
1450
|
+
await updateCommand.execute(testDir);
|
|
1451
|
+
|
|
1452
|
+
// Should only update OpenSpec instructions
|
|
1453
|
+
const [logMessage] = consoleSpy.mock.calls[0];
|
|
1454
|
+
expect(logMessage).toContain(
|
|
1455
|
+
'Updated OpenSpec instructions (openspec/AGENTS.md'
|
|
1456
|
+
);
|
|
1457
|
+
expect(logMessage).toContain('AGENTS.md (created)');
|
|
1458
|
+
consoleSpy.mockRestore();
|
|
1459
|
+
});
|
|
1460
|
+
|
|
1461
|
+
it('should update multiple AI tool files if present', async () => {
|
|
1462
|
+
// TODO: When additional configurators are added (Cursor, Aider, etc.),
|
|
1463
|
+
// enhance this test to create multiple AI tool files and verify
|
|
1464
|
+
// that all existing files are updated in a single operation.
|
|
1465
|
+
// For now, we test with just CLAUDE.md.
|
|
1466
|
+
const claudePath = path.join(testDir, 'CLAUDE.md');
|
|
1467
|
+
await fs.mkdir(path.dirname(claudePath), { recursive: true });
|
|
1468
|
+
await fs.writeFile(
|
|
1469
|
+
claudePath,
|
|
1470
|
+
'<!-- OPENSPEC:START -->\nOld\n<!-- OPENSPEC:END -->'
|
|
1471
|
+
);
|
|
1472
|
+
|
|
1473
|
+
const consoleSpy = vi.spyOn(console, 'log');
|
|
1474
|
+
await updateCommand.execute(testDir);
|
|
1475
|
+
|
|
1476
|
+
// Should report updating with new format
|
|
1477
|
+
const [logMessage] = consoleSpy.mock.calls[0];
|
|
1478
|
+
expect(logMessage).toContain(
|
|
1479
|
+
'Updated OpenSpec instructions (openspec/AGENTS.md'
|
|
1480
|
+
);
|
|
1481
|
+
expect(logMessage).toContain('AGENTS.md (created)');
|
|
1482
|
+
expect(logMessage).toContain('Updated AI tool files: CLAUDE.md');
|
|
1483
|
+
consoleSpy.mockRestore();
|
|
1484
|
+
});
|
|
1485
|
+
|
|
1486
|
+
it('should skip creating missing slash commands during update', async () => {
|
|
1487
|
+
const proposalPath = path.join(
|
|
1488
|
+
testDir,
|
|
1489
|
+
'.claude/commands/openspec/proposal.md'
|
|
1490
|
+
);
|
|
1491
|
+
await fs.mkdir(path.dirname(proposalPath), { recursive: true });
|
|
1492
|
+
await fs.writeFile(
|
|
1493
|
+
proposalPath,
|
|
1494
|
+
`---
|
|
1495
|
+
name: OpenSpec: Proposal
|
|
1496
|
+
description: Existing file
|
|
1497
|
+
category: OpenSpec
|
|
1498
|
+
tags: [openspec, change]
|
|
1499
|
+
---
|
|
1500
|
+
<!-- OPENSPEC:START -->
|
|
1501
|
+
Old content
|
|
1502
|
+
<!-- OPENSPEC:END -->`
|
|
1503
|
+
);
|
|
1504
|
+
|
|
1505
|
+
await updateCommand.execute(testDir);
|
|
1506
|
+
|
|
1507
|
+
const applyExists = await FileSystemUtils.fileExists(
|
|
1508
|
+
path.join(testDir, '.claude/commands/openspec/apply.md')
|
|
1509
|
+
);
|
|
1510
|
+
const archiveExists = await FileSystemUtils.fileExists(
|
|
1511
|
+
path.join(testDir, '.claude/commands/openspec/archive.md')
|
|
1512
|
+
);
|
|
1513
|
+
|
|
1514
|
+
expect(applyExists).toBe(false);
|
|
1515
|
+
expect(archiveExists).toBe(false);
|
|
1516
|
+
});
|
|
1517
|
+
|
|
1518
|
+
it('should never create new AI tool files', async () => {
|
|
1519
|
+
// Get all configurators
|
|
1520
|
+
const configurators = ToolRegistry.getAll();
|
|
1521
|
+
|
|
1522
|
+
// Execute update command
|
|
1523
|
+
await updateCommand.execute(testDir);
|
|
1524
|
+
|
|
1525
|
+
// Check that no new AI tool files were created
|
|
1526
|
+
for (const configurator of configurators) {
|
|
1527
|
+
const configPath = path.join(testDir, configurator.configFileName);
|
|
1528
|
+
const fileExists = await FileSystemUtils.fileExists(configPath);
|
|
1529
|
+
if (configurator.configFileName === 'AGENTS.md') {
|
|
1530
|
+
expect(fileExists).toBe(true);
|
|
1531
|
+
} else {
|
|
1532
|
+
expect(fileExists).toBe(false);
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
});
|
|
1536
|
+
|
|
1537
|
+
it('should update AGENTS.md in openspec directory', async () => {
|
|
1538
|
+
// Execute update command
|
|
1539
|
+
await updateCommand.execute(testDir);
|
|
1540
|
+
|
|
1541
|
+
// Check that AGENTS.md was created/updated
|
|
1542
|
+
const agentsPath = path.join(testDir, 'openspec', 'AGENTS.md');
|
|
1543
|
+
const fileExists = await FileSystemUtils.fileExists(agentsPath);
|
|
1544
|
+
expect(fileExists).toBe(true);
|
|
1545
|
+
|
|
1546
|
+
const content = await fs.readFile(agentsPath, 'utf-8');
|
|
1547
|
+
expect(content).toContain('# OpenSpec Instructions');
|
|
1548
|
+
});
|
|
1549
|
+
|
|
1550
|
+
it('should create root AGENTS.md with managed block when missing', async () => {
|
|
1551
|
+
await updateCommand.execute(testDir);
|
|
1552
|
+
|
|
1553
|
+
const rootAgentsPath = path.join(testDir, 'AGENTS.md');
|
|
1554
|
+
const exists = await FileSystemUtils.fileExists(rootAgentsPath);
|
|
1555
|
+
expect(exists).toBe(true);
|
|
1556
|
+
|
|
1557
|
+
const content = await fs.readFile(rootAgentsPath, 'utf-8');
|
|
1558
|
+
expect(content).toContain('<!-- OPENSPEC:START -->');
|
|
1559
|
+
expect(content).toContain("@/openspec/AGENTS.md");
|
|
1560
|
+
expect(content).toContain('openspec update');
|
|
1561
|
+
expect(content).toContain('<!-- OPENSPEC:END -->');
|
|
1562
|
+
});
|
|
1563
|
+
|
|
1564
|
+
it('should refresh root AGENTS.md while preserving surrounding content', async () => {
|
|
1565
|
+
const rootAgentsPath = path.join(testDir, 'AGENTS.md');
|
|
1566
|
+
const original = `# Custom intro\n\n<!-- OPENSPEC:START -->\nOld content\n<!-- OPENSPEC:END -->\n\n# Footnotes`;
|
|
1567
|
+
await fs.writeFile(rootAgentsPath, original);
|
|
1568
|
+
|
|
1569
|
+
const consoleSpy = vi.spyOn(console, 'log');
|
|
1570
|
+
|
|
1571
|
+
await updateCommand.execute(testDir);
|
|
1572
|
+
|
|
1573
|
+
const updated = await fs.readFile(rootAgentsPath, 'utf-8');
|
|
1574
|
+
expect(updated).toContain('# Custom intro');
|
|
1575
|
+
expect(updated).toContain('# Footnotes');
|
|
1576
|
+
expect(updated).toContain("@/openspec/AGENTS.md");
|
|
1577
|
+
expect(updated).toContain('openspec update');
|
|
1578
|
+
expect(updated).not.toContain('Old content');
|
|
1579
|
+
|
|
1580
|
+
const [logMessage] = consoleSpy.mock.calls[0];
|
|
1581
|
+
expect(logMessage).toContain(
|
|
1582
|
+
'Updated OpenSpec instructions (openspec/AGENTS.md, AGENTS.md)'
|
|
1583
|
+
);
|
|
1584
|
+
expect(logMessage).not.toContain('AGENTS.md (created)');
|
|
1585
|
+
|
|
1586
|
+
consoleSpy.mockRestore();
|
|
1587
|
+
});
|
|
1588
|
+
|
|
1589
|
+
it('should throw error if openspec directory does not exist', async () => {
|
|
1590
|
+
// Remove openspec directory
|
|
1591
|
+
await fs.rm(path.join(testDir, 'openspec'), {
|
|
1592
|
+
recursive: true,
|
|
1593
|
+
force: true,
|
|
1594
|
+
});
|
|
1595
|
+
|
|
1596
|
+
// Execute update command and expect error
|
|
1597
|
+
await expect(updateCommand.execute(testDir)).rejects.toThrow(
|
|
1598
|
+
"No OpenSpec directory found. Run 'openspec init' first."
|
|
1599
|
+
);
|
|
1600
|
+
});
|
|
1601
|
+
|
|
1602
|
+
it('should handle configurator errors gracefully', async () => {
|
|
1603
|
+
// Create CLAUDE.md file but make it read-only to cause an error
|
|
1604
|
+
const claudePath = path.join(testDir, 'CLAUDE.md');
|
|
1605
|
+
await fs.writeFile(
|
|
1606
|
+
claudePath,
|
|
1607
|
+
'<!-- OPENSPEC:START -->\nOld\n<!-- OPENSPEC:END -->'
|
|
1608
|
+
);
|
|
1609
|
+
await fs.chmod(claudePath, 0o444); // Read-only
|
|
1610
|
+
|
|
1611
|
+
const consoleSpy = vi.spyOn(console, 'log');
|
|
1612
|
+
const errorSpy = vi.spyOn(console, 'error');
|
|
1613
|
+
const originalWriteFile = FileSystemUtils.writeFile.bind(FileSystemUtils);
|
|
1614
|
+
const writeSpy = vi
|
|
1615
|
+
.spyOn(FileSystemUtils, 'writeFile')
|
|
1616
|
+
.mockImplementation(async (filePath, content) => {
|
|
1617
|
+
if (filePath.endsWith('CLAUDE.md')) {
|
|
1618
|
+
throw new Error('EACCES: permission denied, open');
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
return originalWriteFile(filePath, content);
|
|
1622
|
+
});
|
|
1623
|
+
|
|
1624
|
+
// Execute update command - should not throw
|
|
1625
|
+
await updateCommand.execute(testDir);
|
|
1626
|
+
|
|
1627
|
+
// Should report the failure
|
|
1628
|
+
expect(errorSpy).toHaveBeenCalled();
|
|
1629
|
+
const [logMessage] = consoleSpy.mock.calls[0];
|
|
1630
|
+
expect(logMessage).toContain(
|
|
1631
|
+
'Updated OpenSpec instructions (openspec/AGENTS.md'
|
|
1632
|
+
);
|
|
1633
|
+
expect(logMessage).toContain('AGENTS.md (created)');
|
|
1634
|
+
expect(logMessage).toContain('Failed to update: CLAUDE.md');
|
|
1635
|
+
|
|
1636
|
+
// Restore permissions for cleanup
|
|
1637
|
+
await fs.chmod(claudePath, 0o644);
|
|
1638
|
+
consoleSpy.mockRestore();
|
|
1639
|
+
errorSpy.mockRestore();
|
|
1640
|
+
writeSpy.mockRestore();
|
|
1641
|
+
});
|
|
1642
|
+
});
|