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.
Files changed (311) hide show
  1. package/.gitmodules +3 -0
  2. package/CHAT.md +3 -0
  3. package/package.json +12 -0
  4. package/references/openspec/.changeset/README.md +6 -0
  5. package/references/openspec/.changeset/config.json +12 -0
  6. package/references/openspec/.coderabbit.yaml +11 -0
  7. package/references/openspec/.devcontainer/README.md +92 -0
  8. package/references/openspec/.devcontainer/devcontainer.json +68 -0
  9. package/references/openspec/.github/CODEOWNERS +2 -0
  10. package/references/openspec/.github/workflows/ci.yml +222 -0
  11. package/references/openspec/.github/workflows/release-prepare.yml +50 -0
  12. package/references/openspec/AGENTS.md +18 -0
  13. package/references/openspec/CHANGELOG.md +205 -0
  14. package/references/openspec/LICENSE +22 -0
  15. package/references/openspec/README.md +374 -0
  16. package/references/openspec/assets/openspec_dashboard.png +0 -0
  17. package/references/openspec/assets/openspec_pixel_dark.svg +89 -0
  18. package/references/openspec/assets/openspec_pixel_light.svg +89 -0
  19. package/references/openspec/bin/openspec.js +3 -0
  20. package/references/openspec/build.js +31 -0
  21. package/references/openspec/openspec/AGENTS.md +454 -0
  22. package/references/openspec/openspec/changes/IMPLEMENTATION_ORDER.md +68 -0
  23. package/references/openspec/openspec/changes/add-antigravity-support/proposal.md +11 -0
  24. package/references/openspec/openspec/changes/add-antigravity-support/specs/cli-init/spec.md +9 -0
  25. package/references/openspec/openspec/changes/add-antigravity-support/specs/cli-update/spec.md +8 -0
  26. package/references/openspec/openspec/changes/add-antigravity-support/tasks.md +12 -0
  27. package/references/openspec/openspec/changes/add-scaffold-command/proposal.md +11 -0
  28. package/references/openspec/openspec/changes/add-scaffold-command/specs/cli-scaffold/spec.md +36 -0
  29. package/references/openspec/openspec/changes/add-scaffold-command/tasks.md +12 -0
  30. package/references/openspec/openspec/changes/archive/2025-01-11-add-update-command/design.md +86 -0
  31. package/references/openspec/openspec/changes/archive/2025-01-11-add-update-command/proposal.md +29 -0
  32. package/references/openspec/openspec/changes/archive/2025-01-11-add-update-command/specs/cli-update/spec.md +59 -0
  33. package/references/openspec/openspec/changes/archive/2025-01-11-add-update-command/tasks.md +20 -0
  34. package/references/openspec/openspec/changes/archive/2025-01-13-add-list-command/proposal.md +20 -0
  35. package/references/openspec/openspec/changes/archive/2025-01-13-add-list-command/specs/cli-list/spec.md +69 -0
  36. package/references/openspec/openspec/changes/archive/2025-01-13-add-list-command/tasks.md +26 -0
  37. package/references/openspec/openspec/changes/archive/2025-08-05-initialize-typescript-project/design.md +64 -0
  38. package/references/openspec/openspec/changes/archive/2025-08-05-initialize-typescript-project/proposal.md +18 -0
  39. package/references/openspec/openspec/changes/archive/2025-08-05-initialize-typescript-project/tasks.md +25 -0
  40. package/references/openspec/openspec/changes/archive/2025-08-06-add-init-command/design.md +104 -0
  41. package/references/openspec/openspec/changes/archive/2025-08-06-add-init-command/proposal.md +30 -0
  42. package/references/openspec/openspec/changes/archive/2025-08-06-add-init-command/specs/cli-init/spec.md +148 -0
  43. package/references/openspec/openspec/changes/archive/2025-08-06-add-init-command/tasks.md +38 -0
  44. package/references/openspec/openspec/changes/archive/2025-08-06-adopt-future-state-storage/proposal.md +24 -0
  45. package/references/openspec/openspec/changes/archive/2025-08-06-adopt-future-state-storage/specs/openspec-conventions/spec.md +120 -0
  46. package/references/openspec/openspec/changes/archive/2025-08-06-adopt-future-state-storage/tasks.md +38 -0
  47. package/references/openspec/openspec/changes/archive/2025-08-11-add-complexity-guidelines/proposal.md +13 -0
  48. package/references/openspec/openspec/changes/archive/2025-08-11-add-complexity-guidelines/specs/openspec-docs/README.md +472 -0
  49. package/references/openspec/openspec/changes/archive/2025-08-11-add-complexity-guidelines/tasks.md +9 -0
  50. package/references/openspec/openspec/changes/archive/2025-08-13-add-archive-command/proposal.md +15 -0
  51. package/references/openspec/openspec/changes/archive/2025-08-13-add-archive-command/specs/cli-archive/spec.md +111 -0
  52. package/references/openspec/openspec/changes/archive/2025-08-13-add-archive-command/tasks.md +44 -0
  53. package/references/openspec/openspec/changes/archive/2025-08-13-add-diff-command/proposal.md +19 -0
  54. package/references/openspec/openspec/changes/archive/2025-08-13-add-diff-command/specs/cli-diff/spec.md +77 -0
  55. package/references/openspec/openspec/changes/archive/2025-08-13-add-diff-command/tasks.md +23 -0
  56. package/references/openspec/openspec/changes/archive/2025-08-19-add-change-commands/design.md +56 -0
  57. package/references/openspec/openspec/changes/archive/2025-08-19-add-change-commands/proposal.md +17 -0
  58. package/references/openspec/openspec/changes/archive/2025-08-19-add-change-commands/specs/cli-change/spec.md +48 -0
  59. package/references/openspec/openspec/changes/archive/2025-08-19-add-change-commands/specs/cli-list/spec.md +12 -0
  60. package/references/openspec/openspec/changes/archive/2025-08-19-add-change-commands/tasks.md +34 -0
  61. package/references/openspec/openspec/changes/archive/2025-08-19-add-interactive-show-command/proposal.md +20 -0
  62. package/references/openspec/openspec/changes/archive/2025-08-19-add-interactive-show-command/specs/cli-change/spec.md +23 -0
  63. package/references/openspec/openspec/changes/archive/2025-08-19-add-interactive-show-command/specs/cli-show/spec.md +83 -0
  64. package/references/openspec/openspec/changes/archive/2025-08-19-add-interactive-show-command/specs/cli-spec/spec.md +23 -0
  65. package/references/openspec/openspec/changes/archive/2025-08-19-add-interactive-show-command/tasks.md +142 -0
  66. package/references/openspec/openspec/changes/archive/2025-08-19-add-skip-specs-archive-option/proposal.md +13 -0
  67. package/references/openspec/openspec/changes/archive/2025-08-19-add-skip-specs-archive-option/specs/cli-archive/spec.md +191 -0
  68. package/references/openspec/openspec/changes/archive/2025-08-19-add-skip-specs-archive-option/tasks.md +57 -0
  69. package/references/openspec/openspec/changes/archive/2025-08-19-add-spec-commands/design.md +45 -0
  70. package/references/openspec/openspec/changes/archive/2025-08-19-add-spec-commands/proposal.md +19 -0
  71. package/references/openspec/openspec/changes/archive/2025-08-19-add-spec-commands/specs/cli-spec/spec.md +43 -0
  72. package/references/openspec/openspec/changes/archive/2025-08-19-add-spec-commands/tasks.md +22 -0
  73. package/references/openspec/openspec/changes/archive/2025-08-19-add-zod-validation/design.md +104 -0
  74. package/references/openspec/openspec/changes/archive/2025-08-19-add-zod-validation/proposal.md +22 -0
  75. package/references/openspec/openspec/changes/archive/2025-08-19-add-zod-validation/specs/cli-archive/spec.md +18 -0
  76. package/references/openspec/openspec/changes/archive/2025-08-19-add-zod-validation/specs/cli-diff/spec.md +12 -0
  77. package/references/openspec/openspec/changes/archive/2025-08-19-add-zod-validation/tasks.md +59 -0
  78. package/references/openspec/openspec/changes/archive/2025-08-19-adopt-delta-based-changes/proposal.md +93 -0
  79. package/references/openspec/openspec/changes/archive/2025-08-19-adopt-delta-based-changes/specs/cli-archive/spec.md +48 -0
  80. package/references/openspec/openspec/changes/archive/2025-08-19-adopt-delta-based-changes/specs/cli-diff/spec.md +45 -0
  81. package/references/openspec/openspec/changes/archive/2025-08-19-adopt-delta-based-changes/specs/openspec-conventions/spec.md +101 -0
  82. package/references/openspec/openspec/changes/archive/2025-08-19-adopt-delta-based-changes/tasks.md +55 -0
  83. package/references/openspec/openspec/changes/archive/2025-08-19-adopt-verb-noun-cli-structure/design.md +19 -0
  84. package/references/openspec/openspec/changes/archive/2025-08-19-adopt-verb-noun-cli-structure/proposal.md +67 -0
  85. package/references/openspec/openspec/changes/archive/2025-08-19-adopt-verb-noun-cli-structure/specs/cli-list/spec.md +57 -0
  86. package/references/openspec/openspec/changes/archive/2025-08-19-adopt-verb-noun-cli-structure/specs/openspec-conventions/spec.md +23 -0
  87. package/references/openspec/openspec/changes/archive/2025-08-19-adopt-verb-noun-cli-structure/tasks.md +27 -0
  88. package/references/openspec/openspec/changes/archive/2025-08-19-bulk-validation-interactive-selection/proposal.md +20 -0
  89. package/references/openspec/openspec/changes/archive/2025-08-19-bulk-validation-interactive-selection/specs/cli-change/spec.md +22 -0
  90. package/references/openspec/openspec/changes/archive/2025-08-19-bulk-validation-interactive-selection/specs/cli-spec/spec.md +23 -0
  91. package/references/openspec/openspec/changes/archive/2025-08-19-bulk-validation-interactive-selection/specs/cli-validate/spec.md +149 -0
  92. package/references/openspec/openspec/changes/archive/2025-08-19-bulk-validation-interactive-selection/tasks.md +81 -0
  93. package/references/openspec/openspec/changes/archive/2025-08-19-fix-update-tool-selection/proposal.md +40 -0
  94. package/references/openspec/openspec/changes/archive/2025-08-19-fix-update-tool-selection/specs/cli-update/spec.md +23 -0
  95. package/references/openspec/openspec/changes/archive/2025-08-19-fix-update-tool-selection/tasks.md +21 -0
  96. package/references/openspec/openspec/changes/archive/2025-08-19-improve-validate-error-messages/proposal.md +25 -0
  97. package/references/openspec/openspec/changes/archive/2025-08-19-improve-validate-error-messages/specs/cli-validate/spec.md +55 -0
  98. package/references/openspec/openspec/changes/archive/2025-08-19-improve-validate-error-messages/tasks.md +21 -0
  99. package/references/openspec/openspec/changes/archive/2025-08-19-structured-spec-format/proposal.md +36 -0
  100. package/references/openspec/openspec/changes/archive/2025-08-19-structured-spec-format/specs/openspec-conventions/spec.md +192 -0
  101. package/references/openspec/openspec/changes/archive/2025-08-19-structured-spec-format/tasks.md +19 -0
  102. package/references/openspec/openspec/changes/archive/2025-09-12-add-view-dashboard-command/proposal.md +38 -0
  103. package/references/openspec/openspec/changes/archive/2025-09-12-add-view-dashboard-command/specs/cli-view/spec.md +109 -0
  104. package/references/openspec/openspec/changes/archive/2025-09-12-add-view-dashboard-command/tasks.md +47 -0
  105. package/references/openspec/openspec/changes/archive/2025-09-29-add-agents-md-config/proposal.md +28 -0
  106. package/references/openspec/openspec/changes/archive/2025-09-29-add-agents-md-config/specs/cli-init/spec.md +71 -0
  107. package/references/openspec/openspec/changes/archive/2025-09-29-add-agents-md-config/specs/cli-update/spec.md +41 -0
  108. package/references/openspec/openspec/changes/archive/2025-09-29-add-agents-md-config/tasks.md +17 -0
  109. package/references/openspec/openspec/changes/archive/2025-09-29-add-multi-agent-init/proposal.md +35 -0
  110. package/references/openspec/openspec/changes/archive/2025-09-29-add-multi-agent-init/specs/cli-init/spec.md +45 -0
  111. package/references/openspec/openspec/changes/archive/2025-09-29-add-multi-agent-init/tasks.md +16 -0
  112. package/references/openspec/openspec/changes/archive/2025-09-29-add-slash-command-support/proposal.md +119 -0
  113. package/references/openspec/openspec/changes/archive/2025-09-29-add-slash-command-support/specs/cli-init/spec.md +21 -0
  114. package/references/openspec/openspec/changes/archive/2025-09-29-add-slash-command-support/specs/cli-update/spec.md +22 -0
  115. package/references/openspec/openspec/changes/archive/2025-09-29-add-slash-command-support/tasks.md +20 -0
  116. package/references/openspec/openspec/changes/archive/2025-09-29-improve-cli-e2e-plan/proposal.md +19 -0
  117. package/references/openspec/openspec/changes/archive/2025-09-29-improve-cli-e2e-plan/tasks.md +9 -0
  118. package/references/openspec/openspec/changes/archive/2025-09-29-improve-deterministic-tests/proposal.md +78 -0
  119. package/references/openspec/openspec/changes/archive/2025-09-29-improve-deterministic-tests/tasks.md +25 -0
  120. package/references/openspec/openspec/changes/archive/2025-09-29-improve-init-onboarding/proposal.md +13 -0
  121. package/references/openspec/openspec/changes/archive/2025-09-29-improve-init-onboarding/specs/cli-init/spec.md +92 -0
  122. package/references/openspec/openspec/changes/archive/2025-09-29-improve-init-onboarding/tasks.md +12 -0
  123. package/references/openspec/openspec/changes/archive/2025-09-29-remove-diff-command/proposal.md +81 -0
  124. package/references/openspec/openspec/changes/archive/2025-09-29-remove-diff-command/tasks.md +37 -0
  125. package/references/openspec/openspec/changes/archive/2025-09-29-sort-active-changes-by-progress/proposal.md +25 -0
  126. package/references/openspec/openspec/changes/archive/2025-09-29-sort-active-changes-by-progress/specs/cli-view/spec.md +9 -0
  127. package/references/openspec/openspec/changes/archive/2025-09-29-sort-active-changes-by-progress/tasks.md +8 -0
  128. package/references/openspec/openspec/changes/archive/2025-09-29-update-agent-file-name/proposal.md +29 -0
  129. package/references/openspec/openspec/changes/archive/2025-09-29-update-agent-file-name/specs/cli-init/spec.md +40 -0
  130. package/references/openspec/openspec/changes/archive/2025-09-29-update-agent-file-name/specs/cli-update/spec.md +22 -0
  131. package/references/openspec/openspec/changes/archive/2025-09-29-update-agent-file-name/specs/openspec-conventions/spec.md +27 -0
  132. package/references/openspec/openspec/changes/archive/2025-09-29-update-agent-file-name/tasks.md +22 -0
  133. package/references/openspec/openspec/changes/archive/2025-09-29-update-agent-instructions/design.md +130 -0
  134. package/references/openspec/openspec/changes/archive/2025-09-29-update-agent-instructions/proposal.md +117 -0
  135. package/references/openspec/openspec/changes/archive/2025-09-29-update-agent-instructions/tasks.md +69 -0
  136. package/references/openspec/openspec/changes/archive/2025-09-29-update-markdown-parser-crlf/proposal.md +19 -0
  137. package/references/openspec/openspec/changes/archive/2025-09-29-update-markdown-parser-crlf/specs/cli-validate/spec.md +9 -0
  138. package/references/openspec/openspec/changes/archive/2025-09-29-update-markdown-parser-crlf/tasks.md +11 -0
  139. package/references/openspec/openspec/changes/archive/2025-10-14-add-codex-slash-command-support/proposal.md +25 -0
  140. package/references/openspec/openspec/changes/archive/2025-10-14-add-codex-slash-command-support/specs/cli-init/spec.md +56 -0
  141. package/references/openspec/openspec/changes/archive/2025-10-14-add-codex-slash-command-support/specs/cli-update/spec.md +41 -0
  142. package/references/openspec/openspec/changes/archive/2025-10-14-add-codex-slash-command-support/tasks.md +19 -0
  143. package/references/openspec/openspec/changes/archive/2025-10-14-add-github-copilot-prompts/proposal.md +25 -0
  144. package/references/openspec/openspec/changes/archive/2025-10-14-add-github-copilot-prompts/specs/cli-init/spec.md +48 -0
  145. package/references/openspec/openspec/changes/archive/2025-10-14-add-github-copilot-prompts/specs/cli-update/spec.md +48 -0
  146. package/references/openspec/openspec/changes/archive/2025-10-14-add-github-copilot-prompts/tasks.md +30 -0
  147. package/references/openspec/openspec/changes/archive/2025-10-14-add-kilocode-workflows/proposal.md +17 -0
  148. package/references/openspec/openspec/changes/archive/2025-10-14-add-kilocode-workflows/specs/cli-init/spec.md +43 -0
  149. package/references/openspec/openspec/changes/archive/2025-10-14-add-kilocode-workflows/specs/cli-update/spec.md +27 -0
  150. package/references/openspec/openspec/changes/archive/2025-10-14-add-kilocode-workflows/tasks.md +15 -0
  151. package/references/openspec/openspec/changes/archive/2025-10-14-add-non-interactive-init-options/proposal.md +12 -0
  152. package/references/openspec/openspec/changes/archive/2025-10-14-add-non-interactive-init-options/specs/cli-init/spec.md +39 -0
  153. package/references/openspec/openspec/changes/archive/2025-10-14-add-non-interactive-init-options/tasks.md +17 -0
  154. package/references/openspec/openspec/changes/archive/2025-10-14-add-windsurf-workflows/proposal.md +17 -0
  155. package/references/openspec/openspec/changes/archive/2025-10-14-add-windsurf-workflows/specs/cli-init/spec.md +42 -0
  156. package/references/openspec/openspec/changes/archive/2025-10-14-add-windsurf-workflows/specs/cli-update/spec.md +27 -0
  157. package/references/openspec/openspec/changes/archive/2025-10-14-add-windsurf-workflows/tasks.md +17 -0
  158. package/references/openspec/openspec/changes/archive/2025-10-14-enhance-validation-error-messages/proposal.md +12 -0
  159. package/references/openspec/openspec/changes/archive/2025-10-14-enhance-validation-error-messages/specs/cli-validate/spec.md +39 -0
  160. package/references/openspec/openspec/changes/archive/2025-10-14-enhance-validation-error-messages/tasks.md +12 -0
  161. package/references/openspec/openspec/changes/archive/2025-10-14-improve-agent-instruction-usability/proposal.md +12 -0
  162. package/references/openspec/openspec/changes/archive/2025-10-14-improve-agent-instruction-usability/specs/docs-agent-instructions/spec.md +33 -0
  163. package/references/openspec/openspec/changes/archive/2025-10-14-improve-agent-instruction-usability/tasks.md +11 -0
  164. package/references/openspec/openspec/changes/archive/2025-10-14-slim-root-agents-file/proposal.md +13 -0
  165. package/references/openspec/openspec/changes/archive/2025-10-14-slim-root-agents-file/tasks.md +15 -0
  166. package/references/openspec/openspec/changes/archive/2025-10-14-update-cli-init-enter-selection/proposal.md +14 -0
  167. package/references/openspec/openspec/changes/archive/2025-10-14-update-cli-init-enter-selection/specs/cli-init/spec.md +10 -0
  168. package/references/openspec/openspec/changes/archive/2025-10-14-update-cli-init-enter-selection/tasks.md +8 -0
  169. package/references/openspec/openspec/changes/archive/2025-10-14-update-cli-init-root-agents/proposal.md +15 -0
  170. package/references/openspec/openspec/changes/archive/2025-10-14-update-cli-init-root-agents/specs/cli-init/spec.md +32 -0
  171. package/references/openspec/openspec/changes/archive/2025-10-14-update-cli-init-root-agents/specs/cli-update/spec.md +10 -0
  172. package/references/openspec/openspec/changes/archive/2025-10-14-update-cli-init-root-agents/tasks.md +11 -0
  173. package/references/openspec/openspec/changes/archive/2025-10-14-update-release-automation/proposal.md +49 -0
  174. package/references/openspec/openspec/changes/archive/2025-10-14-update-release-automation/tasks.md +12 -0
  175. package/references/openspec/openspec/changes/archive/2025-10-22-add-archive-command-arguments/proposal.md +17 -0
  176. package/references/openspec/openspec/changes/archive/2025-10-22-add-archive-command-arguments/specs/cli-update/spec.md +32 -0
  177. package/references/openspec/openspec/changes/archive/2025-10-22-add-archive-command-arguments/tasks.md +15 -0
  178. package/references/openspec/openspec/changes/archive/2025-10-22-add-cline-support/proposal.md +15 -0
  179. package/references/openspec/openspec/changes/archive/2025-10-22-add-cline-support/specs/cli-init/spec.md +97 -0
  180. package/references/openspec/openspec/changes/archive/2025-10-22-add-cline-support/tasks.md +19 -0
  181. package/references/openspec/openspec/changes/archive/2025-10-22-add-crush-support/proposal.md +13 -0
  182. package/references/openspec/openspec/changes/archive/2025-10-22-add-crush-support/specs/cli-init/spec.md +67 -0
  183. package/references/openspec/openspec/changes/archive/2025-10-22-add-crush-support/tasks.md +7 -0
  184. package/references/openspec/openspec/changes/archive/2025-10-22-add-factory-slash-commands/proposal.md +12 -0
  185. package/references/openspec/openspec/changes/archive/2025-10-22-add-factory-slash-commands/specs/cli-init/spec.md +54 -0
  186. package/references/openspec/openspec/changes/archive/2025-10-22-add-factory-slash-commands/specs/cli-update/spec.md +54 -0
  187. package/references/openspec/openspec/changes/archive/2025-10-22-add-factory-slash-commands/tasks.md +11 -0
  188. package/references/openspec/openspec/changes/fix-cline-workflows-implementation/proposal.md +13 -0
  189. package/references/openspec/openspec/changes/fix-cline-workflows-implementation/specs/cli-init/spec.md +11 -0
  190. package/references/openspec/openspec/changes/fix-cline-workflows-implementation/tasks.md +13 -0
  191. package/references/openspec/openspec/changes/make-validation-scope-aware/proposal.md +12 -0
  192. package/references/openspec/openspec/changes/make-validation-scope-aware/specs/cli-validate/spec.md +25 -0
  193. package/references/openspec/openspec/changes/make-validation-scope-aware/tasks.md +16 -0
  194. package/references/openspec/openspec/project.md +53 -0
  195. package/references/openspec/openspec/specs/cli-archive/spec.md +210 -0
  196. package/references/openspec/openspec/specs/cli-change/spec.md +91 -0
  197. package/references/openspec/openspec/specs/cli-init/spec.md +311 -0
  198. package/references/openspec/openspec/specs/cli-list/spec.md +103 -0
  199. package/references/openspec/openspec/specs/cli-show/spec.md +85 -0
  200. package/references/openspec/openspec/specs/cli-spec/spec.md +87 -0
  201. package/references/openspec/openspec/specs/cli-update/spec.md +190 -0
  202. package/references/openspec/openspec/specs/cli-validate/spec.md +218 -0
  203. package/references/openspec/openspec/specs/cli-view/spec.md +105 -0
  204. package/references/openspec/openspec/specs/docs-agent-instructions/spec.md +38 -0
  205. package/references/openspec/openspec/specs/openspec-conventions/spec.md +474 -0
  206. package/references/openspec/openspec-parallel-merge-plan.md +98 -0
  207. package/references/openspec/package.json +73 -0
  208. package/references/openspec/pnpm-lock.yaml +2324 -0
  209. package/references/openspec/scripts/pack-version-check.mjs +111 -0
  210. package/references/openspec/src/cli/index.ts +253 -0
  211. package/references/openspec/src/commands/change.ts +291 -0
  212. package/references/openspec/src/commands/show.ts +139 -0
  213. package/references/openspec/src/commands/spec.ts +250 -0
  214. package/references/openspec/src/commands/validate.ts +305 -0
  215. package/references/openspec/src/core/archive.ts +606 -0
  216. package/references/openspec/src/core/config.ts +41 -0
  217. package/references/openspec/src/core/configurators/agents.ts +23 -0
  218. package/references/openspec/src/core/configurators/base.ts +6 -0
  219. package/references/openspec/src/core/configurators/claude.ts +23 -0
  220. package/references/openspec/src/core/configurators/cline.ts +23 -0
  221. package/references/openspec/src/core/configurators/codebuddy.ts +24 -0
  222. package/references/openspec/src/core/configurators/costrict.ts +23 -0
  223. package/references/openspec/src/core/configurators/iflow.ts +23 -0
  224. package/references/openspec/src/core/configurators/qoder.ts +53 -0
  225. package/references/openspec/src/core/configurators/qwen.ts +47 -0
  226. package/references/openspec/src/core/configurators/registry.ts +49 -0
  227. package/references/openspec/src/core/configurators/slash/amazon-q.ts +51 -0
  228. package/references/openspec/src/core/configurators/slash/antigravity.ts +28 -0
  229. package/references/openspec/src/core/configurators/slash/auggie.ts +37 -0
  230. package/references/openspec/src/core/configurators/slash/base.ts +95 -0
  231. package/references/openspec/src/core/configurators/slash/claude.ts +42 -0
  232. package/references/openspec/src/core/configurators/slash/cline.ts +27 -0
  233. package/references/openspec/src/core/configurators/slash/codebuddy.ts +43 -0
  234. package/references/openspec/src/core/configurators/slash/codex.ts +126 -0
  235. package/references/openspec/src/core/configurators/slash/costrict.ts +36 -0
  236. package/references/openspec/src/core/configurators/slash/crush.ts +42 -0
  237. package/references/openspec/src/core/configurators/slash/cursor.ts +42 -0
  238. package/references/openspec/src/core/configurators/slash/factory.ts +41 -0
  239. package/references/openspec/src/core/configurators/slash/gemini.ts +27 -0
  240. package/references/openspec/src/core/configurators/slash/github-copilot.ts +39 -0
  241. package/references/openspec/src/core/configurators/slash/iflow.ts +42 -0
  242. package/references/openspec/src/core/configurators/slash/kilocode.ts +21 -0
  243. package/references/openspec/src/core/configurators/slash/opencode.ts +83 -0
  244. package/references/openspec/src/core/configurators/slash/qoder.ts +84 -0
  245. package/references/openspec/src/core/configurators/slash/qwen.ts +55 -0
  246. package/references/openspec/src/core/configurators/slash/registry.ts +81 -0
  247. package/references/openspec/src/core/configurators/slash/roocode.ts +27 -0
  248. package/references/openspec/src/core/configurators/slash/toml-base.ts +66 -0
  249. package/references/openspec/src/core/configurators/slash/windsurf.ts +27 -0
  250. package/references/openspec/src/core/converters/json-converter.ts +61 -0
  251. package/references/openspec/src/core/index.ts +2 -0
  252. package/references/openspec/src/core/init.ts +986 -0
  253. package/references/openspec/src/core/list.ts +104 -0
  254. package/references/openspec/src/core/parsers/change-parser.ts +234 -0
  255. package/references/openspec/src/core/parsers/markdown-parser.ts +237 -0
  256. package/references/openspec/src/core/parsers/requirement-blocks.ts +234 -0
  257. package/references/openspec/src/core/schemas/base.schema.ts +20 -0
  258. package/references/openspec/src/core/schemas/change.schema.ts +42 -0
  259. package/references/openspec/src/core/schemas/index.ts +20 -0
  260. package/references/openspec/src/core/schemas/spec.schema.ts +17 -0
  261. package/references/openspec/src/core/styles/palette.ts +8 -0
  262. package/references/openspec/src/core/templates/agents-root-stub.ts +16 -0
  263. package/references/openspec/src/core/templates/agents-template.ts +457 -0
  264. package/references/openspec/src/core/templates/claude-template.ts +1 -0
  265. package/references/openspec/src/core/templates/cline-template.ts +1 -0
  266. package/references/openspec/src/core/templates/costrict-template.ts +1 -0
  267. package/references/openspec/src/core/templates/index.ts +50 -0
  268. package/references/openspec/src/core/templates/project-template.ts +38 -0
  269. package/references/openspec/src/core/templates/slash-command-templates.ts +60 -0
  270. package/references/openspec/src/core/update.ts +129 -0
  271. package/references/openspec/src/core/validation/constants.ts +48 -0
  272. package/references/openspec/src/core/validation/types.ts +19 -0
  273. package/references/openspec/src/core/validation/validator.ts +448 -0
  274. package/references/openspec/src/core/view.ts +189 -0
  275. package/references/openspec/src/index.ts +2 -0
  276. package/references/openspec/src/utils/file-system.ts +187 -0
  277. package/references/openspec/src/utils/index.ts +2 -0
  278. package/references/openspec/src/utils/interactive.ts +7 -0
  279. package/references/openspec/src/utils/item-discovery.ts +45 -0
  280. package/references/openspec/src/utils/match.ts +26 -0
  281. package/references/openspec/src/utils/task-progress.ts +43 -0
  282. package/references/openspec/test/cli-e2e/basic.test.ts +156 -0
  283. package/references/openspec/test/commands/change.interactive-show.test.ts +45 -0
  284. package/references/openspec/test/commands/change.interactive-validate.test.ts +48 -0
  285. package/references/openspec/test/commands/show.test.ts +123 -0
  286. package/references/openspec/test/commands/spec.interactive-show.test.ts +44 -0
  287. package/references/openspec/test/commands/spec.interactive-validate.test.ts +44 -0
  288. package/references/openspec/test/commands/spec.test.ts +324 -0
  289. package/references/openspec/test/commands/validate.enriched-output.test.ts +49 -0
  290. package/references/openspec/test/commands/validate.test.ts +133 -0
  291. package/references/openspec/test/core/archive.test.ts +680 -0
  292. package/references/openspec/test/core/commands/change-command.list.test.ts +76 -0
  293. package/references/openspec/test/core/commands/change-command.show-validate.test.ts +111 -0
  294. package/references/openspec/test/core/converters/json-converter.test.ts +184 -0
  295. package/references/openspec/test/core/init.test.ts +1710 -0
  296. package/references/openspec/test/core/list.test.ts +165 -0
  297. package/references/openspec/test/core/parsers/change-parser.test.ts +52 -0
  298. package/references/openspec/test/core/parsers/markdown-parser.test.ts +291 -0
  299. package/references/openspec/test/core/update.test.ts +1642 -0
  300. package/references/openspec/test/core/validation.enriched-messages.test.ts +74 -0
  301. package/references/openspec/test/core/validation.test.ts +489 -0
  302. package/references/openspec/test/core/view.test.ts +79 -0
  303. package/references/openspec/test/fixtures/tmp-init/openspec/changes/c1/proposal.md +7 -0
  304. package/references/openspec/test/fixtures/tmp-init/openspec/changes/c1/specs/alpha/spec.md +8 -0
  305. package/references/openspec/test/fixtures/tmp-init/openspec/specs/alpha/spec.md +12 -0
  306. package/references/openspec/test/helpers/run-cli.ts +139 -0
  307. package/references/openspec/test/utils/file-system.test.ts +211 -0
  308. package/references/openspec/test/utils/marker-updates.test.ts +287 -0
  309. package/references/openspec/tsconfig.json +21 -0
  310. package/references/openspec/vitest.config.ts +25 -0
  311. 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
+ });