@codewalla_india/openspec 1.0.1

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 (356) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +225 -0
  3. package/bin/openspec.js +5 -0
  4. package/dist/cli/index.d.ts +10 -0
  5. package/dist/cli/index.js +548 -0
  6. package/dist/commands/change.d.ts +39 -0
  7. package/dist/commands/change.js +279 -0
  8. package/dist/commands/completion.d.ts +72 -0
  9. package/dist/commands/completion.js +264 -0
  10. package/dist/commands/config.d.ts +36 -0
  11. package/dist/commands/config.js +552 -0
  12. package/dist/commands/context.d.ts +3 -0
  13. package/dist/commands/context.js +155 -0
  14. package/dist/commands/doctor.d.ts +8 -0
  15. package/dist/commands/doctor.js +163 -0
  16. package/dist/commands/feedback.d.ts +9 -0
  17. package/dist/commands/feedback.js +183 -0
  18. package/dist/commands/schema.d.ts +6 -0
  19. package/dist/commands/schema.js +869 -0
  20. package/dist/commands/shared-gather.d.ts +14 -0
  21. package/dist/commands/shared-gather.js +31 -0
  22. package/dist/commands/shared-output.d.ts +18 -0
  23. package/dist/commands/shared-output.js +61 -0
  24. package/dist/commands/show.d.ts +19 -0
  25. package/dist/commands/show.js +177 -0
  26. package/dist/commands/spec.d.ts +19 -0
  27. package/dist/commands/spec.js +236 -0
  28. package/dist/commands/store.d.ts +3 -0
  29. package/dist/commands/store.js +547 -0
  30. package/dist/commands/validate.d.ts +26 -0
  31. package/dist/commands/validate.js +330 -0
  32. package/dist/commands/workflow/index.d.ts +17 -0
  33. package/dist/commands/workflow/index.js +12 -0
  34. package/dist/commands/workflow/instructions.d.ts +45 -0
  35. package/dist/commands/workflow/instructions.js +500 -0
  36. package/dist/commands/workflow/new-change.d.ts +20 -0
  37. package/dist/commands/workflow/new-change.js +106 -0
  38. package/dist/commands/workflow/schemas.d.ts +10 -0
  39. package/dist/commands/workflow/schemas.js +34 -0
  40. package/dist/commands/workflow/shared.d.ts +84 -0
  41. package/dist/commands/workflow/shared.js +133 -0
  42. package/dist/commands/workflow/status.d.ts +16 -0
  43. package/dist/commands/workflow/status.js +92 -0
  44. package/dist/commands/workflow/templates.d.ts +16 -0
  45. package/dist/commands/workflow/templates.js +69 -0
  46. package/dist/commands/workset-input.d.ts +19 -0
  47. package/dist/commands/workset-input.js +112 -0
  48. package/dist/commands/workset-prompts.d.ts +12 -0
  49. package/dist/commands/workset-prompts.js +143 -0
  50. package/dist/commands/workset.d.ts +25 -0
  51. package/dist/commands/workset.js +446 -0
  52. package/dist/core/archive.d.ts +22 -0
  53. package/dist/core/archive.js +471 -0
  54. package/dist/core/artifact-graph/graph.d.ts +56 -0
  55. package/dist/core/artifact-graph/graph.js +141 -0
  56. package/dist/core/artifact-graph/index.d.ts +9 -0
  57. package/dist/core/artifact-graph/index.js +14 -0
  58. package/dist/core/artifact-graph/instruction-loader.d.ts +188 -0
  59. package/dist/core/artifact-graph/instruction-loader.js +233 -0
  60. package/dist/core/artifact-graph/outputs.d.ts +14 -0
  61. package/dist/core/artifact-graph/outputs.js +39 -0
  62. package/dist/core/artifact-graph/resolver.d.ts +81 -0
  63. package/dist/core/artifact-graph/resolver.js +257 -0
  64. package/dist/core/artifact-graph/schema.d.ts +13 -0
  65. package/dist/core/artifact-graph/schema.js +108 -0
  66. package/dist/core/artifact-graph/state.d.ts +12 -0
  67. package/dist/core/artifact-graph/state.js +31 -0
  68. package/dist/core/artifact-graph/types.d.ts +40 -0
  69. package/dist/core/artifact-graph/types.js +29 -0
  70. package/dist/core/available-tools.d.ts +17 -0
  71. package/dist/core/available-tools.js +43 -0
  72. package/dist/core/change-metadata/index.d.ts +2 -0
  73. package/dist/core/change-metadata/index.js +2 -0
  74. package/dist/core/change-metadata/schema.d.ts +19 -0
  75. package/dist/core/change-metadata/schema.js +30 -0
  76. package/dist/core/change-status-policy.d.ts +37 -0
  77. package/dist/core/change-status-policy.js +35 -0
  78. package/dist/core/command-generation/adapters/amazon-q.d.ts +13 -0
  79. package/dist/core/command-generation/adapters/amazon-q.js +26 -0
  80. package/dist/core/command-generation/adapters/antigravity.d.ts +13 -0
  81. package/dist/core/command-generation/adapters/antigravity.js +26 -0
  82. package/dist/core/command-generation/adapters/auggie.d.ts +13 -0
  83. package/dist/core/command-generation/adapters/auggie.js +27 -0
  84. package/dist/core/command-generation/adapters/bob.d.ts +14 -0
  85. package/dist/core/command-generation/adapters/bob.js +32 -0
  86. package/dist/core/command-generation/adapters/claude.d.ts +13 -0
  87. package/dist/core/command-generation/adapters/claude.js +37 -0
  88. package/dist/core/command-generation/adapters/cline.d.ts +14 -0
  89. package/dist/core/command-generation/adapters/cline.js +27 -0
  90. package/dist/core/command-generation/adapters/codebuddy.d.ts +13 -0
  91. package/dist/core/command-generation/adapters/codebuddy.js +28 -0
  92. package/dist/core/command-generation/adapters/codex.d.ts +16 -0
  93. package/dist/core/command-generation/adapters/codex.js +39 -0
  94. package/dist/core/command-generation/adapters/continue.d.ts +13 -0
  95. package/dist/core/command-generation/adapters/continue.js +28 -0
  96. package/dist/core/command-generation/adapters/costrict.d.ts +13 -0
  97. package/dist/core/command-generation/adapters/costrict.js +27 -0
  98. package/dist/core/command-generation/adapters/crush.d.ts +13 -0
  99. package/dist/core/command-generation/adapters/crush.js +30 -0
  100. package/dist/core/command-generation/adapters/cursor.d.ts +14 -0
  101. package/dist/core/command-generation/adapters/cursor.js +31 -0
  102. package/dist/core/command-generation/adapters/factory.d.ts +13 -0
  103. package/dist/core/command-generation/adapters/factory.js +27 -0
  104. package/dist/core/command-generation/adapters/gemini.d.ts +13 -0
  105. package/dist/core/command-generation/adapters/gemini.js +26 -0
  106. package/dist/core/command-generation/adapters/github-copilot.d.ts +13 -0
  107. package/dist/core/command-generation/adapters/github-copilot.js +26 -0
  108. package/dist/core/command-generation/adapters/iflow.d.ts +13 -0
  109. package/dist/core/command-generation/adapters/iflow.js +29 -0
  110. package/dist/core/command-generation/adapters/index.d.ts +32 -0
  111. package/dist/core/command-generation/adapters/index.js +32 -0
  112. package/dist/core/command-generation/adapters/junie.d.ts +13 -0
  113. package/dist/core/command-generation/adapters/junie.js +26 -0
  114. package/dist/core/command-generation/adapters/kilocode.d.ts +14 -0
  115. package/dist/core/command-generation/adapters/kilocode.js +23 -0
  116. package/dist/core/command-generation/adapters/kiro.d.ts +13 -0
  117. package/dist/core/command-generation/adapters/kiro.js +26 -0
  118. package/dist/core/command-generation/adapters/lingma.d.ts +13 -0
  119. package/dist/core/command-generation/adapters/lingma.js +30 -0
  120. package/dist/core/command-generation/adapters/opencode.d.ts +13 -0
  121. package/dist/core/command-generation/adapters/opencode.js +29 -0
  122. package/dist/core/command-generation/adapters/pi.d.ts +18 -0
  123. package/dist/core/command-generation/adapters/pi.js +42 -0
  124. package/dist/core/command-generation/adapters/qoder.d.ts +13 -0
  125. package/dist/core/command-generation/adapters/qoder.js +30 -0
  126. package/dist/core/command-generation/adapters/qwen.d.ts +13 -0
  127. package/dist/core/command-generation/adapters/qwen.js +26 -0
  128. package/dist/core/command-generation/adapters/roocode.d.ts +14 -0
  129. package/dist/core/command-generation/adapters/roocode.js +27 -0
  130. package/dist/core/command-generation/adapters/windsurf.d.ts +14 -0
  131. package/dist/core/command-generation/adapters/windsurf.js +38 -0
  132. package/dist/core/command-generation/generator.d.ts +21 -0
  133. package/dist/core/command-generation/generator.js +27 -0
  134. package/dist/core/command-generation/index.d.ts +22 -0
  135. package/dist/core/command-generation/index.js +24 -0
  136. package/dist/core/command-generation/registry.d.ts +36 -0
  137. package/dist/core/command-generation/registry.js +98 -0
  138. package/dist/core/command-generation/types.d.ts +56 -0
  139. package/dist/core/command-generation/types.js +8 -0
  140. package/dist/core/command-generation/yaml.d.ts +22 -0
  141. package/dist/core/command-generation/yaml.js +38 -0
  142. package/dist/core/completions/command-registry.d.ts +3 -0
  143. package/dist/core/completions/command-registry.js +778 -0
  144. package/dist/core/completions/completion-provider.d.ts +71 -0
  145. package/dist/core/completions/completion-provider.js +129 -0
  146. package/dist/core/completions/factory.d.ts +64 -0
  147. package/dist/core/completions/factory.js +75 -0
  148. package/dist/core/completions/generators/bash-generator.d.ts +35 -0
  149. package/dist/core/completions/generators/bash-generator.js +230 -0
  150. package/dist/core/completions/generators/fish-generator.d.ts +32 -0
  151. package/dist/core/completions/generators/fish-generator.js +160 -0
  152. package/dist/core/completions/generators/powershell-generator.d.ts +36 -0
  153. package/dist/core/completions/generators/powershell-generator.js +266 -0
  154. package/dist/core/completions/generators/zsh-generator.d.ts +47 -0
  155. package/dist/core/completions/generators/zsh-generator.js +276 -0
  156. package/dist/core/completions/installers/bash-installer.d.ts +87 -0
  157. package/dist/core/completions/installers/bash-installer.js +321 -0
  158. package/dist/core/completions/installers/fish-installer.d.ts +43 -0
  159. package/dist/core/completions/installers/fish-installer.js +151 -0
  160. package/dist/core/completions/installers/powershell-installer.d.ts +102 -0
  161. package/dist/core/completions/installers/powershell-installer.js +415 -0
  162. package/dist/core/completions/installers/zsh-installer.d.ts +117 -0
  163. package/dist/core/completions/installers/zsh-installer.js +424 -0
  164. package/dist/core/completions/shared-flags.d.ts +13 -0
  165. package/dist/core/completions/shared-flags.js +33 -0
  166. package/dist/core/completions/templates/bash-templates.d.ts +6 -0
  167. package/dist/core/completions/templates/bash-templates.js +30 -0
  168. package/dist/core/completions/templates/fish-templates.d.ts +7 -0
  169. package/dist/core/completions/templates/fish-templates.js +45 -0
  170. package/dist/core/completions/templates/powershell-templates.d.ts +6 -0
  171. package/dist/core/completions/templates/powershell-templates.js +34 -0
  172. package/dist/core/completions/templates/zsh-templates.d.ts +6 -0
  173. package/dist/core/completions/templates/zsh-templates.js +45 -0
  174. package/dist/core/completions/types.d.ts +101 -0
  175. package/dist/core/completions/types.js +2 -0
  176. package/dist/core/comprehension/config.d.ts +20 -0
  177. package/dist/core/comprehension/config.js +23 -0
  178. package/dist/core/comprehension/fingerprint.d.ts +5 -0
  179. package/dist/core/comprehension/fingerprint.js +25 -0
  180. package/dist/core/comprehension/index.d.ts +49 -0
  181. package/dist/core/comprehension/index.js +78 -0
  182. package/dist/core/comprehension/pass-record.d.ts +29 -0
  183. package/dist/core/comprehension/pass-record.js +64 -0
  184. package/dist/core/comprehension/stats.d.ts +18 -0
  185. package/dist/core/comprehension/stats.js +41 -0
  186. package/dist/core/config-prompts.d.ts +9 -0
  187. package/dist/core/config-prompts.js +34 -0
  188. package/dist/core/config-schema.d.ts +87 -0
  189. package/dist/core/config-schema.js +239 -0
  190. package/dist/core/config.d.ts +18 -0
  191. package/dist/core/config.js +39 -0
  192. package/dist/core/converters/json-converter.d.ts +6 -0
  193. package/dist/core/converters/json-converter.js +51 -0
  194. package/dist/core/file-state.d.ts +36 -0
  195. package/dist/core/file-state.js +112 -0
  196. package/dist/core/global-config.d.ts +51 -0
  197. package/dist/core/global-config.js +124 -0
  198. package/dist/core/id.d.ts +17 -0
  199. package/dist/core/id.js +30 -0
  200. package/dist/core/index.d.ts +6 -0
  201. package/dist/core/index.js +7 -0
  202. package/dist/core/init.d.ts +37 -0
  203. package/dist/core/init.js +613 -0
  204. package/dist/core/legacy-cleanup.d.ts +162 -0
  205. package/dist/core/legacy-cleanup.js +514 -0
  206. package/dist/core/list.d.ts +11 -0
  207. package/dist/core/list.js +185 -0
  208. package/dist/core/migration.d.ts +23 -0
  209. package/dist/core/migration.js +108 -0
  210. package/dist/core/openers.d.ts +77 -0
  211. package/dist/core/openers.js +251 -0
  212. package/dist/core/openspec-root.d.ts +45 -0
  213. package/dist/core/openspec-root.js +192 -0
  214. package/dist/core/parsers/change-parser.d.ts +13 -0
  215. package/dist/core/parsers/change-parser.js +197 -0
  216. package/dist/core/parsers/markdown-parser.d.ts +26 -0
  217. package/dist/core/parsers/markdown-parser.js +227 -0
  218. package/dist/core/parsers/requirement-blocks.d.ts +37 -0
  219. package/dist/core/parsers/requirement-blocks.js +201 -0
  220. package/dist/core/parsers/spec-structure.d.ts +9 -0
  221. package/dist/core/parsers/spec-structure.js +88 -0
  222. package/dist/core/planning-home.d.ts +16 -0
  223. package/dist/core/planning-home.js +67 -0
  224. package/dist/core/profile-sync-drift.d.ts +38 -0
  225. package/dist/core/profile-sync-drift.js +200 -0
  226. package/dist/core/profiles.d.ts +26 -0
  227. package/dist/core/profiles.js +40 -0
  228. package/dist/core/project-config.d.ts +120 -0
  229. package/dist/core/project-config.js +406 -0
  230. package/dist/core/references.d.ts +63 -0
  231. package/dist/core/references.js +310 -0
  232. package/dist/core/relationship-health.d.ts +65 -0
  233. package/dist/core/relationship-health.js +64 -0
  234. package/dist/core/root-selection.d.ts +122 -0
  235. package/dist/core/root-selection.js +337 -0
  236. package/dist/core/schemas/base.schema.d.ts +13 -0
  237. package/dist/core/schemas/base.schema.js +13 -0
  238. package/dist/core/schemas/change.schema.d.ts +73 -0
  239. package/dist/core/schemas/change.schema.js +31 -0
  240. package/dist/core/schemas/index.d.ts +4 -0
  241. package/dist/core/schemas/index.js +4 -0
  242. package/dist/core/schemas/spec.schema.d.ts +18 -0
  243. package/dist/core/schemas/spec.schema.js +15 -0
  244. package/dist/core/shared/index.d.ts +8 -0
  245. package/dist/core/shared/index.js +8 -0
  246. package/dist/core/shared/skill-generation.d.ts +49 -0
  247. package/dist/core/shared/skill-generation.js +96 -0
  248. package/dist/core/shared/tool-detection.d.ts +71 -0
  249. package/dist/core/shared/tool-detection.js +158 -0
  250. package/dist/core/specs-apply.d.ts +78 -0
  251. package/dist/core/specs-apply.js +394 -0
  252. package/dist/core/store/errors.d.ts +20 -0
  253. package/dist/core/store/errors.js +22 -0
  254. package/dist/core/store/foundation.d.ts +56 -0
  255. package/dist/core/store/foundation.js +251 -0
  256. package/dist/core/store/git.d.ts +23 -0
  257. package/dist/core/store/git.js +137 -0
  258. package/dist/core/store/index.d.ts +5 -0
  259. package/dist/core/store/index.js +5 -0
  260. package/dist/core/store/operations.d.ts +114 -0
  261. package/dist/core/store/operations.js +783 -0
  262. package/dist/core/store/registry.d.ts +58 -0
  263. package/dist/core/store/registry.js +275 -0
  264. package/dist/core/styles/palette.d.ts +7 -0
  265. package/dist/core/styles/palette.js +8 -0
  266. package/dist/core/templates/index.d.ts +8 -0
  267. package/dist/core/templates/index.js +9 -0
  268. package/dist/core/templates/skill-templates.d.ts +19 -0
  269. package/dist/core/templates/skill-templates.js +18 -0
  270. package/dist/core/templates/types.d.ts +19 -0
  271. package/dist/core/templates/types.js +5 -0
  272. package/dist/core/templates/workflows/apply-change.d.ts +10 -0
  273. package/dist/core/templates/workflows/apply-change.js +337 -0
  274. package/dist/core/templates/workflows/archive-change.d.ts +10 -0
  275. package/dist/core/templates/workflows/archive-change.js +278 -0
  276. package/dist/core/templates/workflows/bulk-archive-change.d.ts +10 -0
  277. package/dist/core/templates/workflows/bulk-archive-change.js +493 -0
  278. package/dist/core/templates/workflows/comprehension-guidance.d.ts +9 -0
  279. package/dist/core/templates/workflows/comprehension-guidance.js +58 -0
  280. package/dist/core/templates/workflows/continue-change.d.ts +10 -0
  281. package/dist/core/templates/workflows/continue-change.js +239 -0
  282. package/dist/core/templates/workflows/explore.d.ts +10 -0
  283. package/dist/core/templates/workflows/explore.js +464 -0
  284. package/dist/core/templates/workflows/feedback.d.ts +9 -0
  285. package/dist/core/templates/workflows/feedback.js +108 -0
  286. package/dist/core/templates/workflows/ff-change.d.ts +10 -0
  287. package/dist/core/templates/workflows/ff-change.js +205 -0
  288. package/dist/core/templates/workflows/mcp-guidance.d.ts +13 -0
  289. package/dist/core/templates/workflows/mcp-guidance.js +116 -0
  290. package/dist/core/templates/workflows/new-change.d.ts +10 -0
  291. package/dist/core/templates/workflows/new-change.js +148 -0
  292. package/dist/core/templates/workflows/onboard.d.ts +10 -0
  293. package/dist/core/templates/workflows/onboard.js +566 -0
  294. package/dist/core/templates/workflows/propose.d.ts +10 -0
  295. package/dist/core/templates/workflows/propose.js +228 -0
  296. package/dist/core/templates/workflows/store-selection.d.ts +8 -0
  297. package/dist/core/templates/workflows/store-selection.js +8 -0
  298. package/dist/core/templates/workflows/sync-specs.d.ts +10 -0
  299. package/dist/core/templates/workflows/sync-specs.js +291 -0
  300. package/dist/core/templates/workflows/verify-change.d.ts +10 -0
  301. package/dist/core/templates/workflows/verify-change.js +346 -0
  302. package/dist/core/update.d.ts +82 -0
  303. package/dist/core/update.js +557 -0
  304. package/dist/core/validation/constants.d.ts +34 -0
  305. package/dist/core/validation/constants.js +40 -0
  306. package/dist/core/validation/types.d.ts +18 -0
  307. package/dist/core/validation/types.js +2 -0
  308. package/dist/core/validation/validator.d.ts +44 -0
  309. package/dist/core/validation/validator.js +435 -0
  310. package/dist/core/view.d.ts +8 -0
  311. package/dist/core/view.js +168 -0
  312. package/dist/core/working-set.d.ts +47 -0
  313. package/dist/core/working-set.js +43 -0
  314. package/dist/core/worksets.d.ts +75 -0
  315. package/dist/core/worksets.js +245 -0
  316. package/dist/core/zod-issues.d.ts +4 -0
  317. package/dist/core/zod-issues.js +10 -0
  318. package/dist/index.d.ts +3 -0
  319. package/dist/index.js +3 -0
  320. package/dist/prompts/searchable-multi-select.d.ts +28 -0
  321. package/dist/prompts/searchable-multi-select.js +159 -0
  322. package/dist/telemetry/config.d.ts +38 -0
  323. package/dist/telemetry/config.js +136 -0
  324. package/dist/telemetry/index.d.ts +31 -0
  325. package/dist/telemetry/index.js +164 -0
  326. package/dist/ui/ascii-patterns.d.ts +16 -0
  327. package/dist/ui/ascii-patterns.js +133 -0
  328. package/dist/ui/welcome-screen.d.ts +10 -0
  329. package/dist/ui/welcome-screen.js +146 -0
  330. package/dist/utils/change-metadata.d.ts +55 -0
  331. package/dist/utils/change-metadata.js +141 -0
  332. package/dist/utils/change-utils.d.ts +71 -0
  333. package/dist/utils/change-utils.js +138 -0
  334. package/dist/utils/command-references.d.ts +18 -0
  335. package/dist/utils/command-references.js +20 -0
  336. package/dist/utils/file-system.d.ts +41 -0
  337. package/dist/utils/file-system.js +320 -0
  338. package/dist/utils/index.d.ts +6 -0
  339. package/dist/utils/index.js +9 -0
  340. package/dist/utils/interactive.d.ts +18 -0
  341. package/dist/utils/interactive.js +21 -0
  342. package/dist/utils/item-discovery.d.ts +4 -0
  343. package/dist/utils/item-discovery.js +72 -0
  344. package/dist/utils/match.d.ts +3 -0
  345. package/dist/utils/match.js +22 -0
  346. package/dist/utils/shell-detection.d.ts +20 -0
  347. package/dist/utils/shell-detection.js +41 -0
  348. package/dist/utils/task-progress.d.ts +8 -0
  349. package/dist/utils/task-progress.js +36 -0
  350. package/package.json +84 -0
  351. package/schemas/spec-driven/schema.yaml +153 -0
  352. package/schemas/spec-driven/templates/design.md +19 -0
  353. package/schemas/spec-driven/templates/proposal.md +23 -0
  354. package/schemas/spec-driven/templates/spec.md +8 -0
  355. package/schemas/spec-driven/templates/tasks.md +9 -0
  356. package/scripts/postinstall.js +83 -0
@@ -0,0 +1,783 @@
1
+ import { execFile } from 'node:child_process';
2
+ import * as nodeFs from 'node:fs';
3
+ import * as os from 'node:os';
4
+ import * as path from 'node:path';
5
+ import { promisify } from 'node:util';
6
+ import { FileSystemUtils } from '../../utils/file-system.js';
7
+ import { ANCHORED_OPENSPEC_DIRS, DIRECTORY_ANCHOR_FILE_NAME, OPENSPEC_ROOT_DIR, ensureOpenSpecRoot, inspectOpenSpecRoot, rollbackCreatedPaths, } from '../openspec-root.js';
8
+ import { STORE_METADATA_DIR_NAME, getStoreMetadataDir, getStoreMetadataPath, getStoreRegistryPath, listStoreRegistryEntries, readStoreRegistryState, readOptionalStoreMetadataState, resolveGitStoreBackendConfig, validateStoreId, writeStoreMetadataState, } from './foundation.js';
9
+ import { StoreError, makeStoreDiagnostic } from './errors.js';
10
+ import { assertGitCommitIdentity, commitStoreFiles, gitDirectoryHasTrackedFiles, gitHasCommits, gitHasRemote, gitHasUncommittedChanges, gitOriginUrl, initGitRepository, isGitRepositoryAtRoot, } from './git.js';
11
+ import { getStoreRootForBackend, assertNoRegisteredStoreConflict, commitStoreRegistration, getRegisteredStore, listRegisteredStores, unregisterStoreRegistration, } from './registry.js';
12
+ const fs = nodeFs.promises;
13
+ const execFileAsync = promisify(execFile);
14
+ async function pathKind(targetPath) {
15
+ try {
16
+ const stat = await fs.stat(targetPath);
17
+ if (stat.isDirectory())
18
+ return 'directory';
19
+ if (stat.isFile())
20
+ return 'file';
21
+ return 'other';
22
+ }
23
+ catch (error) {
24
+ if (typeof error === 'object' &&
25
+ error !== null &&
26
+ 'code' in error &&
27
+ error.code === 'ENOENT') {
28
+ return 'missing';
29
+ }
30
+ throw error;
31
+ }
32
+ }
33
+ async function isDirectoryEmpty(directory) {
34
+ return (await fs.readdir(directory)).length === 0;
35
+ }
36
+ async function readStoreMetadataForOperation(storeRoot) {
37
+ try {
38
+ return await readOptionalStoreMetadataState(storeRoot);
39
+ }
40
+ catch (error) {
41
+ throw new StoreError(error instanceof Error ? error.message : String(error), 'invalid_store_metadata', {
42
+ target: 'store.metadata',
43
+ fix: `Repair ${getStoreMetadataPath(storeRoot)}.`,
44
+ });
45
+ }
46
+ }
47
+ async function isGitOnlyDirectory(storeRoot) {
48
+ const entries = await fs.readdir(storeRoot);
49
+ return entries.length === 1 && entries[0] === '.git' && await isGitRepositoryAtRoot(storeRoot);
50
+ }
51
+ function alreadyRegisteredDiagnostic(id) {
52
+ return makeStoreDiagnostic('info', 'store_already_registered', `Store '${id}' is already registered at this path.`, {
53
+ target: 'store.registry',
54
+ });
55
+ }
56
+ function createdPath(relativePath, absolutePath, kind) {
57
+ return {
58
+ relativePath,
59
+ absolutePath,
60
+ kind,
61
+ };
62
+ }
63
+ async function nearestExistingDirectory(targetPath) {
64
+ let current = path.resolve(targetPath);
65
+ while (true) {
66
+ const kind = await pathKind(current);
67
+ if (kind === 'directory')
68
+ return current;
69
+ if (kind !== 'missing')
70
+ return null;
71
+ const parent = path.dirname(current);
72
+ if (parent === current)
73
+ return null;
74
+ current = parent;
75
+ }
76
+ }
77
+ async function findContainingGitRepositoryRoot(storeRoot) {
78
+ const resolvedStoreRoot = path.resolve(storeRoot);
79
+ const nearestParent = await nearestExistingDirectory(path.dirname(resolvedStoreRoot));
80
+ if (!nearestParent)
81
+ return null;
82
+ const comparableStoreRoot = path.resolve(FileSystemUtils.canonicalizeExistingPath(nearestParent), path.relative(nearestParent, resolvedStoreRoot));
83
+ const gitRootContainsStore = (gitRoot) => {
84
+ const normalizedGitRoot = FileSystemUtils.canonicalizeExistingPath(gitRoot);
85
+ const relative = path.relative(normalizedGitRoot, comparableStoreRoot);
86
+ return relative.length > 0 && !relative.startsWith('..') && !path.isAbsolute(relative)
87
+ ? normalizedGitRoot
88
+ : null;
89
+ };
90
+ try {
91
+ const { stdout } = await execFileAsync('git', [
92
+ '-C',
93
+ nearestParent,
94
+ 'rev-parse',
95
+ '--show-toplevel',
96
+ ]);
97
+ return gitRootContainsStore(stdout.trim());
98
+ }
99
+ catch {
100
+ let current = nearestParent;
101
+ while (true) {
102
+ if (await isGitRepositoryAtRoot(current)) {
103
+ return gitRootContainsStore(current);
104
+ }
105
+ const parent = path.dirname(current);
106
+ if (parent === current)
107
+ return null;
108
+ current = parent;
109
+ }
110
+ }
111
+ }
112
+ async function assertSetupPathIsNotNestedInGitRepo(storeRoot, options) {
113
+ if (options.allowInsideGitRepository)
114
+ return;
115
+ const containingGitRoot = await findContainingGitRepositoryRoot(storeRoot);
116
+ if (!containingGitRoot)
117
+ return;
118
+ throw new StoreError(`Store setup path is inside another Git repository: ${containingGitRoot}`, 'store_setup_inside_git_repo', {
119
+ target: 'store.root',
120
+ fix: 'Choose a path outside that Git repository.',
121
+ });
122
+ }
123
+ export function expandUserPath(inputPath) {
124
+ const trimmed = inputPath.trim();
125
+ if (trimmed === '~')
126
+ return os.homedir();
127
+ if (trimmed.startsWith('~/') || trimmed.startsWith('~\\')) {
128
+ return path.join(os.homedir(), trimmed.slice(2));
129
+ }
130
+ return trimmed;
131
+ }
132
+ function resolveSetupRoot(id, inputPath) {
133
+ // A store is a repo the user places; setup never silently picks app data.
134
+ if (inputPath === undefined || inputPath.trim().length === 0) {
135
+ throw new StoreError('Pass --path with the folder where this store should live.', 'store_setup_path_required', {
136
+ target: 'store.root',
137
+ fix: `openspec store setup ${id} --path ~/openspec/${id}`,
138
+ });
139
+ }
140
+ return path.resolve(expandUserPath(inputPath));
141
+ }
142
+ function resolveRegisterRoot(inputPath) {
143
+ if (inputPath === undefined || inputPath.trim().length === 0) {
144
+ throw new StoreError('Pass a store path.', 'store_path_required', {
145
+ target: 'store.root',
146
+ fix: 'openspec store register /path/to/store',
147
+ });
148
+ }
149
+ return path.resolve(expandUserPath(inputPath));
150
+ }
151
+ function inferStoreIdFromPath(storeRoot) {
152
+ return validateStoreId(path.basename(storeRoot));
153
+ }
154
+ function normalizeRegistryPathForComparison(targetPath) {
155
+ try {
156
+ return FileSystemUtils.canonicalizeExistingPath(targetPath);
157
+ }
158
+ catch {
159
+ return path.resolve(targetPath);
160
+ }
161
+ }
162
+ function isRegisteredAtPath(registry, id, storeRoot) {
163
+ const entry = registry?.stores?.[id];
164
+ if (!entry)
165
+ return false;
166
+ return (normalizeRegistryPathForComparison(getStoreRootForBackend(entry.backend)) ===
167
+ normalizeRegistryPathForComparison(storeRoot));
168
+ }
169
+ function mutationPayload(id, storeRoot, git, createdFiles, registry, diagnostics = [], remotes) {
170
+ return {
171
+ store: {
172
+ id,
173
+ root: storeRoot,
174
+ metadataPath: getStoreMetadataPath(storeRoot),
175
+ },
176
+ ...(remotes && (remotes.canonical || remotes.observed) ? { remotes } : {}),
177
+ registryCommit: {
178
+ path: getStoreRegistryPath(),
179
+ registered: registry.registered,
180
+ alreadyRegistered: registry.alreadyRegistered,
181
+ },
182
+ git: {
183
+ isRepository: git.isRepository,
184
+ initialized: git.initialized,
185
+ committed: git.committed,
186
+ },
187
+ createdArtifacts: createdFiles,
188
+ diagnostics,
189
+ };
190
+ }
191
+ function remoteRequiresHandEditError(id, storeRoot) {
192
+ return new StoreError(`Store '${id}' already has an identity file; --remote cannot change it.`, 'store_remote_requires_hand_edit', {
193
+ target: 'store.metadata',
194
+ fix: `Edit ${getStoreMetadataPath(storeRoot)} and commit it.`,
195
+ });
196
+ }
197
+ /**
198
+ * Backend config carrying the observed origin. Guarded by an at-root
199
+ * repository check: `git -C` discovers repositories by walking UP the
200
+ * tree, so probing a non-repo store folder nested inside another repo
201
+ * would record the ENCLOSING repo's origin.
202
+ */
203
+ async function resolveBackendWithObservedOrigin(storeRoot) {
204
+ const origin = (await isGitRepositoryAtRoot(storeRoot))
205
+ ? await gitOriginUrl(storeRoot)
206
+ : null;
207
+ return resolveGitStoreBackendConfig({
208
+ localPath: storeRoot,
209
+ ...(origin ? { remote: origin } : {}),
210
+ });
211
+ }
212
+ async function prepareSetupPlan(input) {
213
+ const id = validateStoreId(input.id ?? '');
214
+ if (input.remote !== undefined && input.remote.length === 0) {
215
+ throw new StoreError('Store remote must not be empty when provided.', 'store_remote_empty', {
216
+ target: 'store.metadata',
217
+ fix: 'Pass a clone URL: --remote <url>.',
218
+ });
219
+ }
220
+ const storeRoot = resolveSetupRoot(id, input.path);
221
+ const kind = await pathKind(storeRoot);
222
+ if (kind === 'file' || kind === 'other') {
223
+ throw new StoreError(`Store setup path is not a directory: ${storeRoot}`, 'store_setup_path_not_directory', {
224
+ target: 'store.root',
225
+ fix: 'Choose an empty directory or an existing healthy OpenSpec root.',
226
+ });
227
+ }
228
+ // Stores may be Git-backed, but creating one inside an implementation
229
+ // repo is almost always an accidental nested-repo setup.
230
+ await assertSetupPathIsNotNestedInGitRepo(storeRoot, {
231
+ allowInsideGitRepository: input.allowInsideGitRepository,
232
+ });
233
+ let metadata = null;
234
+ let backend;
235
+ if (kind === 'directory') {
236
+ metadata = await readStoreMetadataForOperation(storeRoot);
237
+ if (metadata) {
238
+ if (metadata.id !== id) {
239
+ throw new StoreError(`Store metadata id '${metadata.id}' does not match requested id '${id}'.`, 'store_metadata_id_mismatch', {
240
+ target: 'store.metadata',
241
+ fix: `Use id '${metadata.id}' or choose a different setup path.`,
242
+ });
243
+ }
244
+ if (input.remote !== undefined) {
245
+ // Silent acceptance is the forbidden outcome: the identity file
246
+ // already exists, so --remote cannot reach the committed shape.
247
+ throw remoteRequiresHandEditError(id, storeRoot);
248
+ }
249
+ }
250
+ else {
251
+ const openspecRoot = await inspectOpenSpecRoot(storeRoot);
252
+ const safeFreshDirectory = await isDirectoryEmpty(storeRoot) || await isGitOnlyDirectory(storeRoot);
253
+ if (!openspecRoot.healthy && !safeFreshDirectory) {
254
+ throw new StoreError('Store setup does not support initializing a non-empty folder that is not a healthy OpenSpec root.', 'store_setup_non_empty_directory', {
255
+ target: 'store.root',
256
+ fix: 'Choose an empty folder, a Git-only folder, or an existing healthy OpenSpec root.',
257
+ });
258
+ }
259
+ }
260
+ backend = await resolveBackendWithObservedOrigin(storeRoot);
261
+ }
262
+ const registry = await readStoreRegistryState();
263
+ const conflictBackend = backend ?? {
264
+ type: 'git',
265
+ local_path: FileSystemUtils.canonicalizeExistingPath(storeRoot),
266
+ };
267
+ assertNoRegisteredStoreConflict(registry, id, conflictBackend);
268
+ return {
269
+ id,
270
+ storeRoot,
271
+ kind,
272
+ registry,
273
+ ...(backend ? { backend } : {}),
274
+ };
275
+ }
276
+ /**
277
+ * Resolves the effective Git mode for a prepared setup: on by default for new
278
+ * stores, off for reruns of an already-registered store (which must stay
279
+ * no-ops), and always honoring an explicit --init-git/--no-init-git.
280
+ */
281
+ export function resolveSetupGitEnabled(prepared, initGit) {
282
+ return initGit ?? !isRegisteredAtPath(prepared.registry, prepared.id, prepared.root);
283
+ }
284
+ export async function prepareStoreSetup(input) {
285
+ const plan = await prepareSetupPlan(input);
286
+ return {
287
+ id: plan.id,
288
+ root: plan.storeRoot,
289
+ rootKind: plan.kind,
290
+ registry: plan.registry,
291
+ ...(plan.backend ? { backend: plan.backend } : {}),
292
+ ...(input.remote !== undefined ? { remote: input.remote } : {}),
293
+ };
294
+ }
295
+ export async function setupPreparedStore(prepared, input = {}) {
296
+ const plan = {
297
+ id: prepared.id,
298
+ storeRoot: prepared.root,
299
+ kind: prepared.rootKind,
300
+ registry: prepared.registry,
301
+ ...(prepared.backend ? { backend: prepared.backend } : {}),
302
+ };
303
+ const { id, storeRoot, kind, registry } = plan;
304
+ let { backend } = plan;
305
+ // The prepare/execute split can span an unbounded interactive
306
+ // confirmation. Re-assert the prepare-time directory facts: if the
307
+ // path appeared in the gap, the plan (and its rollback policy) no
308
+ // longer describes reality - refuse and let a rerun re-prepare.
309
+ if (kind === 'missing' && (await fs.access(storeRoot).then(() => true, () => false))) {
310
+ throw new StoreError(`The path ${storeRoot} was created while setup was waiting for confirmation.`, 'store_setup_path_changed', {
311
+ target: 'store.root',
312
+ fix: 'Rerun openspec store setup to re-evaluate the directory.',
313
+ });
314
+ }
315
+ const createdFiles = [];
316
+ let createdPaths = [];
317
+ let gitInitialized = false;
318
+ let committed = false;
319
+ // Reruns for an already-registered store stay strict no-ops: no anchor
320
+ // retrofit, no git init, no new commit, no identity requirement. Only an
321
+ // explicit --init-git overrides that for the git side.
322
+ const alreadyRegisteredHere = isRegisteredAtPath(registry, id, storeRoot);
323
+ // --no-init-git opts out of every Git action: no preflight, no init, no
324
+ // commit, even when the target is already a repository.
325
+ const gitEnabled = input.initGit ?? !alreadyRegisteredHere;
326
+ const repoExisted = await isGitRepositoryAtRoot(storeRoot);
327
+ // Identity preflight runs before anything is created so a missing identity
328
+ // never leaves half-made state behind.
329
+ if (gitEnabled) {
330
+ await assertGitCommitIdentity((await nearestExistingDirectory(storeRoot)) ?? process.cwd());
331
+ }
332
+ try {
333
+ const root = await ensureOpenSpecRoot(storeRoot, {
334
+ anchorEmptyDirectories: !alreadyRegisteredHere,
335
+ });
336
+ createdFiles.push(...root.createdArtifacts);
337
+ createdPaths = root.createdPaths;
338
+ backend ??= await resolveBackendWithObservedOrigin(storeRoot);
339
+ assertNoRegisteredStoreConflict(registry, id, backend);
340
+ // The identity file is written before the initial commit so clones carry
341
+ // it; without it, register falls back to the conversion prompt.
342
+ const existingMetadata = await readStoreMetadataForOperation(storeRoot);
343
+ if (existingMetadata && prepared.remote !== undefined) {
344
+ // Re-assert the prepare-phase refusal: metadata that materialized
345
+ // between prepare and execute must not silently swallow --remote.
346
+ throw remoteRequiresHandEditError(id, storeRoot);
347
+ }
348
+ if (!existingMetadata) {
349
+ const metadataDir = getStoreMetadataDir(storeRoot);
350
+ const metadataDirMissing = (await pathKind(metadataDir)) === 'missing';
351
+ await writeStoreMetadataState(storeRoot, {
352
+ version: 1,
353
+ id,
354
+ ...(prepared.remote !== undefined ? { remote: prepared.remote } : {}),
355
+ });
356
+ if (metadataDirMissing) {
357
+ createdPaths.push(createdPath('.openspec-store/', metadataDir, 'directory'));
358
+ }
359
+ createdPaths.push(createdPath('.openspec-store/store.yaml', getStoreMetadataPath(storeRoot), 'file'));
360
+ createdFiles.push('.openspec-store/store.yaml');
361
+ }
362
+ gitInitialized = gitEnabled ? await initGitRepository(storeRoot) : false;
363
+ const isRepository = gitInitialized || repoExisted;
364
+ // "Files created for rollback" and "files a clone needs" are different
365
+ // sets: when setup initialized the repository itself, the initial commit
366
+ // must contain the full store shape or clones of a converted root would
367
+ // be unhealthy. In a pre-existing repo the user owns the history, so
368
+ // setup commits only what it created.
369
+ const commitPathspecs = gitInitialized
370
+ ? [OPENSPEC_ROOT_DIR, STORE_METADATA_DIR_NAME]
371
+ : createdPaths
372
+ .filter((entry) => entry.kind === 'file')
373
+ .map((entry) => entry.relativePath);
374
+ committed = gitEnabled && isRepository
375
+ ? await commitStoreFiles(storeRoot, id, commitPathspecs)
376
+ : false;
377
+ // Identity creation is setup's job (done above, before the commit);
378
+ // registration only verifies it and records the machine-local entry.
379
+ const registered = await commitStoreRegistration({
380
+ id,
381
+ backend,
382
+ writeMetadataIfMissing: false,
383
+ });
384
+ const diagnostics = registered.alreadyRegistered && createdFiles.length === 0
385
+ ? [alreadyRegisteredDiagnostic(id)]
386
+ : [];
387
+ const canonical = prepared.remote ?? existingMetadata?.remote;
388
+ return mutationPayload(id, registered.storeRoot, {
389
+ isRepository,
390
+ initialized: gitInitialized,
391
+ committed,
392
+ }, createdFiles, {
393
+ registered: registered.registryUpdated,
394
+ alreadyRegistered: registered.alreadyRegistered,
395
+ }, diagnostics, {
396
+ ...(canonical ? { canonical } : {}),
397
+ ...(backend.remote ? { observed: backend.remote } : {}),
398
+ });
399
+ }
400
+ catch (error) {
401
+ // Once the initial commit landed in a (possibly user-owned) repository,
402
+ // the files are durable state; deleting them would orphan the commit.
403
+ // The only remaining failure is the registry write, which is retryable.
404
+ if (committed) {
405
+ throw error;
406
+ }
407
+ if (createdPaths.length > 0) {
408
+ await rollbackCreatedPaths(createdPaths);
409
+ }
410
+ // G14: a half-made .git is never durable state pre-commit - clean it
411
+ // up regardless of whether the ledger recorded other creations, or a
412
+ // rerun registers a commitless store.
413
+ if (gitInitialized) {
414
+ await fs.rm(path.join(storeRoot, '.git'), { recursive: true, force: true }).catch(() => undefined);
415
+ }
416
+ if (kind === 'missing') {
417
+ // Non-recursive both ways: never delete content this operation did
418
+ // not create (the execute-time re-check guarantees kind is accurate,
419
+ // but rmdir is the belt to that suspender).
420
+ await fs.rmdir(storeRoot).catch(() => undefined);
421
+ }
422
+ throw error;
423
+ }
424
+ }
425
+ export async function setupStore(input) {
426
+ return setupPreparedStore(await prepareStoreSetup(input), {
427
+ initGit: input.initGit,
428
+ });
429
+ }
430
+ export async function registerExistingStore(input) {
431
+ const storeRoot = resolveRegisterRoot(input.path);
432
+ const kind = await pathKind(storeRoot);
433
+ if (kind === 'missing') {
434
+ throw new StoreError(`Store path does not exist: ${storeRoot}`, 'store_path_missing', {
435
+ target: 'store.root',
436
+ fix: 'Clone or create the store folder before registering it.',
437
+ });
438
+ }
439
+ if (kind !== 'directory') {
440
+ throw new StoreError(`Store path is not a directory: ${storeRoot}`, 'store_path_not_directory', {
441
+ target: 'store.root',
442
+ fix: 'Pass an existing store directory.',
443
+ });
444
+ }
445
+ const openspecRoot = await inspectOpenSpecRoot(storeRoot);
446
+ if (!openspecRoot.healthy) {
447
+ const problems = openspecRoot.diagnostics.map((diagnostic) => diagnostic.message).join(' ') ||
448
+ 'The OpenSpec root is missing or incomplete.';
449
+ const isEmptyCloneSuspect = (await isGitRepositoryAtRoot(storeRoot)) &&
450
+ (await gitHasCommits(storeRoot)) === false;
451
+ const emptyCloneHint = isEmptyCloneSuspect
452
+ ? ' This folder is a Git repository with no commits — if it is a clone, the origin store needs an initial commit before the clone has any files.'
453
+ : '';
454
+ throw new StoreError(`Store register requires an existing healthy OpenSpec root. ${problems}${emptyCloneHint}`, 'store_register_root_unhealthy', {
455
+ target: 'openspec.root',
456
+ fix: isEmptyCloneSuspect
457
+ ? 'If this is a store clone: commit and push the origin store, pull it into this clone, then rerun register.'
458
+ : 'Run openspec store setup for a new store, or point register at a checkout whose openspec/ files are present.',
459
+ });
460
+ }
461
+ const metadata = await readStoreMetadataForOperation(storeRoot);
462
+ const explicitId = input.id !== undefined ? validateStoreId(input.id) : undefined;
463
+ if (metadata && explicitId !== undefined && metadata.id !== explicitId) {
464
+ // The fix must account for whether the metadata id is already registered,
465
+ // so following it never lands on the already-registered error.
466
+ const currentRegistry = await readStoreRegistryState();
467
+ const registeredElsewhere = currentRegistry?.stores?.[metadata.id] !== undefined &&
468
+ !isRegisteredAtPath(currentRegistry, metadata.id, storeRoot);
469
+ throw new StoreError(`Store metadata id '${metadata.id}' does not match --id '${explicitId}'. The id comes from the store's committed .openspec-store/store.yaml.`, 'store_metadata_id_mismatch', {
470
+ target: 'store.id',
471
+ fix: registeredElsewhere
472
+ ? `One checkout per store id is supported, and '${metadata.id}' is already registered. Run openspec store unregister ${metadata.id} first to register this checkout instead.`
473
+ : `Use --id ${metadata.id} or register a different folder.`,
474
+ });
475
+ }
476
+ const id = metadata?.id ?? explicitId ?? inferStoreIdFromPath(storeRoot);
477
+ if (!metadata && !input.allowCreateIdentity) {
478
+ throw new StoreError(`Turn this OpenSpec root into store '${id}'?`, 'store_register_identity_confirmation_required', {
479
+ target: 'store.metadata',
480
+ fix: `Run interactively or pass --yes to create ${getStoreMetadataPath(storeRoot)}.`,
481
+ });
482
+ }
483
+ const backend = await resolveBackendWithObservedOrigin(storeRoot);
484
+ const registry = await readStoreRegistryState();
485
+ assertNoRegisteredStoreConflict(registry, id, backend);
486
+ const createdFiles = [];
487
+ const isRepository = await isGitRepositoryAtRoot(storeRoot);
488
+ const registered = await commitStoreRegistration({
489
+ id,
490
+ backend,
491
+ writeMetadataIfMissing: true,
492
+ });
493
+ if (registered.metadataCreated) {
494
+ createdFiles.push('.openspec-store/store.yaml');
495
+ }
496
+ const diagnostics = registered.alreadyRegistered && createdFiles.length === 0
497
+ ? [alreadyRegisteredDiagnostic(id)]
498
+ : [];
499
+ // Register never commits; converted roots are the user's repo to commit.
500
+ return mutationPayload(id, registered.storeRoot, {
501
+ isRepository,
502
+ initialized: false,
503
+ committed: false,
504
+ }, createdFiles, {
505
+ registered: registered.registryUpdated,
506
+ alreadyRegistered: registered.alreadyRegistered,
507
+ }, diagnostics, {
508
+ ...(metadata?.remote ? { canonical: metadata.remote } : {}),
509
+ ...(backend.remote ? { observed: backend.remote } : {}),
510
+ });
511
+ }
512
+ function cleanupStoreOutput(id, storeRoot) {
513
+ return {
514
+ id,
515
+ root: storeRoot,
516
+ metadataPath: getStoreMetadataPath(storeRoot),
517
+ };
518
+ }
519
+ export async function prepareStoreCleanup(input) {
520
+ const id = validateStoreId(input.id);
521
+ const entry = await getRegisteredStore({
522
+ id,
523
+ globalDataDir: input.globalDataDir,
524
+ });
525
+ return {
526
+ ...cleanupStoreOutput(entry.id, entry.storeRoot),
527
+ backend: entry.backend,
528
+ ...(input.globalDataDir ? { globalDataDir: input.globalDataDir } : {}),
529
+ };
530
+ }
531
+ export async function unregisterStore(input) {
532
+ const target = await prepareStoreCleanup(input);
533
+ const removed = await unregisterStoreRegistration({
534
+ id: target.id,
535
+ expectedBackend: target.backend,
536
+ globalDataDir: target.globalDataDir,
537
+ });
538
+ return {
539
+ store: cleanupStoreOutput(removed.id, removed.storeRoot),
540
+ registryCommit: {
541
+ path: getStoreRegistryPath({ globalDataDir: target.globalDataDir }),
542
+ removed: true,
543
+ },
544
+ files: {
545
+ deleted: false,
546
+ leftOnDisk: removed.storeRoot,
547
+ },
548
+ diagnostics: [],
549
+ };
550
+ }
551
+ async function assertSafeToDeleteStoreRoot(storeRoot, id) {
552
+ const kind = await pathKind(storeRoot);
553
+ if (kind === 'missing') {
554
+ return { exists: false };
555
+ }
556
+ if (kind !== 'directory') {
557
+ throw new StoreError(`Store path is not a directory: ${storeRoot}`, 'store_remove_path_not_directory', {
558
+ target: 'store.root',
559
+ fix: 'Run "openspec store unregister <id>" if you only want to forget this local registry entry.',
560
+ });
561
+ }
562
+ const metadata = await readStoreMetadataForOperation(storeRoot);
563
+ if (!metadata) {
564
+ throw new StoreError('Store remove refuses to delete a folder without store metadata.', 'store_remove_metadata_missing', {
565
+ target: 'store.metadata',
566
+ fix: 'Run "openspec store unregister <id>" if you only want to forget this local registry entry.',
567
+ });
568
+ }
569
+ if (metadata.id !== id) {
570
+ throw new StoreError(`Store metadata id '${metadata.id}' does not match requested id '${id}'.`, 'store_metadata_id_mismatch', {
571
+ target: 'store.metadata',
572
+ fix: 'Repair the registry or run store unregister instead of deleting this folder.',
573
+ });
574
+ }
575
+ return { exists: true };
576
+ }
577
+ export async function removeStore(target) {
578
+ const id = validateStoreId(target.id);
579
+ const diagnostics = [];
580
+ let deleted = false;
581
+ // Order matters: the registry entry goes first, the files second. A
582
+ // failed file deletion leaves recoverable orphan files; the reverse
583
+ // order would leave a phantom registration pointing at nothing.
584
+ let rootMissing = false;
585
+ const removed = await unregisterStoreRegistration({
586
+ id,
587
+ expectedBackend: target.backend,
588
+ globalDataDir: target.globalDataDir,
589
+ beforeCommit: async (entry) => {
590
+ const safeTarget = await assertSafeToDeleteStoreRoot(entry.storeRoot, id);
591
+ rootMissing = !safeTarget.exists;
592
+ },
593
+ });
594
+ if (rootMissing) {
595
+ diagnostics.push(makeStoreDiagnostic('warning', 'store_root_missing', 'Store files were already missing.', {
596
+ target: 'store.root',
597
+ }));
598
+ }
599
+ else {
600
+ try {
601
+ await fs.rm(removed.storeRoot, { recursive: true, force: true });
602
+ deleted = true;
603
+ }
604
+ catch (error) {
605
+ diagnostics.push(makeStoreDiagnostic('warning', 'store_files_left_on_disk', `The registration was removed, but deleting ${removed.storeRoot} failed (${error.message}).`, {
606
+ target: 'store.root',
607
+ fix: `Delete the folder manually: ${removed.storeRoot}`,
608
+ }));
609
+ }
610
+ }
611
+ return {
612
+ store: cleanupStoreOutput(removed.id, removed.storeRoot),
613
+ registryCommit: {
614
+ path: getStoreRegistryPath({ globalDataDir: target.globalDataDir }),
615
+ removed: true,
616
+ },
617
+ files: {
618
+ deleted,
619
+ ...(deleted ? { deletedPath: removed.storeRoot } : {}),
620
+ },
621
+ diagnostics,
622
+ };
623
+ }
624
+ export async function listStores() {
625
+ const entries = await listRegisteredStores();
626
+ return {
627
+ stores: entries.map((entry) => ({
628
+ id: entry.id,
629
+ root: entry.storeRoot,
630
+ })),
631
+ };
632
+ }
633
+ function doctorStatusForError(error, code, target, fix) {
634
+ if (error instanceof StoreError) {
635
+ return error.diagnostic;
636
+ }
637
+ return makeStoreDiagnostic('error', code, error instanceof Error ? error.message : String(error), {
638
+ target,
639
+ ...(fix ? { fix } : {}),
640
+ });
641
+ }
642
+ async function inspectStore(entry) {
643
+ const root = getStoreRootForBackend(entry.backend);
644
+ const metadataPath = getStoreMetadataPath(root);
645
+ const diagnostics = [];
646
+ const kind = await pathKind(root);
647
+ let metadata = {
648
+ present: null,
649
+ valid: null,
650
+ remote: null,
651
+ };
652
+ let git = {
653
+ isRepository: null,
654
+ hasCommits: null,
655
+ hasUncommittedChanges: null,
656
+ hasRemote: null,
657
+ originUrl: null,
658
+ };
659
+ let openspecRoot = await inspectOpenSpecRoot(root);
660
+ if (kind === 'missing') {
661
+ diagnostics.push(makeStoreDiagnostic('error', 'store_root_missing', 'Store location does not exist.', {
662
+ target: 'store.root',
663
+ fix: `Run openspec store register /path/to/${entry.id} --id ${entry.id}.`,
664
+ }));
665
+ }
666
+ else if (kind !== 'directory') {
667
+ diagnostics.push(makeStoreDiagnostic('error', 'store_root_not_directory', 'Store location is not a directory.', {
668
+ target: 'store.root',
669
+ fix: 'Register a directory path for this store.',
670
+ }));
671
+ }
672
+ else {
673
+ openspecRoot = await inspectOpenSpecRoot(root);
674
+ diagnostics.push(...openspecRoot.diagnostics);
675
+ try {
676
+ const parsed = await readOptionalStoreMetadataState(root);
677
+ if (!parsed) {
678
+ metadata = { present: false, valid: false, remote: null };
679
+ diagnostics.push(makeStoreDiagnostic('error', 'store_metadata_missing', 'Store metadata is missing.', {
680
+ target: 'store.metadata',
681
+ fix: `Create ${metadataPath} or rerun store register.`,
682
+ }));
683
+ }
684
+ else if (parsed.id !== entry.id) {
685
+ metadata = { present: true, valid: false, id: parsed.id, remote: null };
686
+ diagnostics.push(makeStoreDiagnostic('error', 'store_metadata_id_mismatch', `Store metadata id '${parsed.id}' does not match registry id '${entry.id}'.`, {
687
+ target: 'store.metadata',
688
+ fix: 'Repair the local registry or store metadata so the ids match.',
689
+ }));
690
+ }
691
+ else {
692
+ metadata = {
693
+ present: true,
694
+ valid: true,
695
+ id: parsed.id,
696
+ remote: parsed.remote ?? null,
697
+ };
698
+ }
699
+ }
700
+ catch (error) {
701
+ metadata = { present: true, valid: false, remote: null };
702
+ diagnostics.push(doctorStatusForError(error, 'store_metadata_invalid', 'store.metadata', `Repair ${metadataPath}.`));
703
+ }
704
+ const isRepository = await isGitRepositoryAtRoot(root);
705
+ git = {
706
+ isRepository,
707
+ hasCommits: null,
708
+ hasUncommittedChanges: null,
709
+ hasRemote: null,
710
+ originUrl: null,
711
+ };
712
+ // Read-only Git facts; doctor reports and never repairs.
713
+ if (isRepository) {
714
+ git.hasCommits = await gitHasCommits(root);
715
+ git.hasUncommittedChanges = await gitHasUncommittedChanges(root);
716
+ git.hasRemote = await gitHasRemote(root);
717
+ git.originUrl = await gitOriginUrl(root);
718
+ if (git.hasCommits === false) {
719
+ diagnostics.push(makeStoreDiagnostic('warning', 'store_git_no_commits', 'Git repository has no commits yet; clones of this store will be empty until an initial commit exists.', {
720
+ target: 'store.git',
721
+ fix: 'Commit the store files, then push to share them.',
722
+ }));
723
+ }
724
+ else if (git.hasCommits === true) {
725
+ const fragileDirs = [];
726
+ for (const relativeDir of ANCHORED_OPENSPEC_DIRS) {
727
+ const dirKind = await pathKind(path.join(root, relativeDir));
728
+ if (dirKind !== 'directory')
729
+ continue;
730
+ if ((await gitDirectoryHasTrackedFiles(root, relativeDir)) === false) {
731
+ fragileDirs.push(`${relativeDir}/`);
732
+ }
733
+ }
734
+ if (fragileDirs.length > 0) {
735
+ diagnostics.push(makeStoreDiagnostic('warning', 'store_clone_fragile_directories', `These directories contain no tracked files and will be lost in clones: ${fragileDirs.join(', ')}.`, {
736
+ target: 'store.git',
737
+ fix: `Track a file in each directory (for example ${DIRECTORY_ANCHOR_FILE_NAME}) and commit it.`,
738
+ }));
739
+ }
740
+ }
741
+ }
742
+ }
743
+ return {
744
+ id: entry.id,
745
+ root,
746
+ metadataPath,
747
+ openspecRoot,
748
+ metadata,
749
+ git,
750
+ diagnostics,
751
+ };
752
+ }
753
+ export async function doctorStores(id) {
754
+ const selectedId = id !== undefined ? validateStoreId(id) : undefined;
755
+ const registry = await readStoreRegistryState();
756
+ if (!registry) {
757
+ if (selectedId !== undefined) {
758
+ throw new StoreError(`Unknown store '${selectedId}'.`, 'store_not_found', {
759
+ target: 'store.id',
760
+ fix: 'Run openspec store list to see registered stores.',
761
+ });
762
+ }
763
+ return { stores: [], diagnostics: [] };
764
+ }
765
+ const entries = listStoreRegistryEntries(registry);
766
+ const selected = selectedId
767
+ ? entries.filter((entry) => entry.id === selectedId)
768
+ : entries;
769
+ if (selectedId && selected.length === 0) {
770
+ throw new StoreError(`Unknown store '${selectedId}'.`, 'store_not_found', {
771
+ target: 'store.id',
772
+ fix: 'Run openspec store list to see registered stores.',
773
+ });
774
+ }
775
+ return {
776
+ stores: await Promise.all(selected.map(inspectStore)),
777
+ diagnostics: [],
778
+ };
779
+ }
780
+ export function normalizeStorePathForComparison(targetPath) {
781
+ return FileSystemUtils.canonicalizeExistingPath(targetPath);
782
+ }
783
+ //# sourceMappingURL=operations.js.map