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