@codewalla_india/openspec 1.3.1 → 1.3.2

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 (333) hide show
  1. package/LICENSE +22 -22
  2. package/README.md +272 -191
  3. package/bin/openspec.js +5 -5
  4. package/dist/cli/index.js +99 -62
  5. package/dist/commands/change.d.ts +0 -2
  6. package/dist/commands/change.js +107 -86
  7. package/dist/commands/config.js +14 -16
  8. package/dist/commands/doctor.js +9 -4
  9. package/dist/commands/feedback.js +79 -30
  10. package/dist/commands/modify.d.ts +26 -0
  11. package/dist/commands/modify.js +147 -0
  12. package/dist/commands/schema.js +170 -91
  13. package/dist/commands/show.js +5 -2
  14. package/dist/commands/spec.js +49 -29
  15. package/dist/commands/store.js +15 -21
  16. package/dist/commands/validate.d.ts +8 -0
  17. package/dist/commands/validate.js +43 -20
  18. package/dist/commands/workflow/index.d.ts +2 -2
  19. package/dist/commands/workflow/index.js +1 -1
  20. package/dist/commands/workflow/instructions.d.ts +6 -26
  21. package/dist/commands/workflow/instructions.js +183 -376
  22. package/dist/commands/workflow/new-change.d.ts +0 -4
  23. package/dist/commands/workflow/new-change.js +10 -28
  24. package/dist/commands/workflow/shared.d.ts +34 -19
  25. package/dist/commands/workflow/shared.js +34 -4
  26. package/dist/commands/workflow/status.js +10 -32
  27. package/dist/commands/workflow/templates.js +15 -5
  28. package/dist/commands/workset.d.ts +2 -2
  29. package/dist/commands/workset.js +19 -22
  30. package/dist/comprehension-quiz/fingerprint.d.ts +39 -0
  31. package/dist/comprehension-quiz/fingerprint.js +84 -0
  32. package/dist/comprehension-quiz/index.d.ts +42 -0
  33. package/dist/comprehension-quiz/index.js +47 -0
  34. package/dist/comprehension-quiz/pass-record.d.ts +44 -0
  35. package/dist/comprehension-quiz/pass-record.js +98 -0
  36. package/dist/comprehension-quiz/providers/anthropic.d.ts +34 -0
  37. package/dist/comprehension-quiz/providers/anthropic.js +53 -0
  38. package/dist/comprehension-quiz/providers/factory.d.ts +44 -0
  39. package/dist/comprehension-quiz/providers/factory.js +174 -0
  40. package/dist/comprehension-quiz/providers/interface.d.ts +59 -0
  41. package/dist/comprehension-quiz/providers/interface.js +63 -0
  42. package/dist/comprehension-quiz/providers/local.d.ts +34 -0
  43. package/dist/comprehension-quiz/providers/local.js +53 -0
  44. package/dist/comprehension-quiz/providers/ollama.d.ts +34 -0
  45. package/dist/comprehension-quiz/providers/ollama.js +53 -0
  46. package/dist/comprehension-quiz/providers/openai.d.ts +34 -0
  47. package/dist/comprehension-quiz/providers/openai.js +53 -0
  48. package/dist/comprehension-quiz/question-generator.d.ts +69 -0
  49. package/dist/comprehension-quiz/question-generator.js +141 -0
  50. package/dist/comprehension-quiz/quiz-executor.d.ts +70 -0
  51. package/dist/comprehension-quiz/quiz-executor.js +165 -0
  52. package/dist/comprehension-quiz/types.d.ts +115 -0
  53. package/dist/comprehension-quiz/types.js +11 -0
  54. package/dist/core/archive.d.ts +14 -1
  55. package/dist/core/archive.js +1350 -190
  56. package/dist/core/artifact-graph/graph.d.ts +28 -9
  57. package/dist/core/artifact-graph/graph.js +94 -43
  58. package/dist/core/artifact-graph/index.d.ts +1 -1
  59. package/dist/core/artifact-graph/index.js +1 -1
  60. package/dist/core/artifact-graph/instruction-loader.d.ts +30 -3
  61. package/dist/core/artifact-graph/instruction-loader.js +70 -12
  62. package/dist/core/artifact-graph/outputs.d.ts +1 -0
  63. package/dist/core/artifact-graph/outputs.js +67 -5
  64. package/dist/core/artifact-graph/resolver.d.ts +14 -0
  65. package/dist/core/artifact-graph/resolver.js +69 -15
  66. package/dist/core/artifact-graph/types.js +21 -3
  67. package/dist/core/available-tools.d.ts +2 -2
  68. package/dist/core/available-tools.js +14 -3
  69. package/dist/core/change-metadata/schema.d.ts +2 -0
  70. package/dist/core/change-metadata/schema.js +13 -0
  71. package/dist/core/change-status-policy.d.ts +10 -1
  72. package/dist/core/change-status-policy.js +30 -2
  73. package/dist/core/command-generation/adapters/amazon-q.d.ts +4 -0
  74. package/dist/core/command-generation/adapters/amazon-q.js +11 -5
  75. package/dist/core/command-generation/adapters/antigravity.js +6 -5
  76. package/dist/core/command-generation/adapters/auggie.js +7 -6
  77. package/dist/core/command-generation/adapters/bob.d.ts +5 -1
  78. package/dist/core/command-generation/adapters/bob.js +11 -10
  79. package/dist/core/command-generation/adapters/claude.d.ts +1 -1
  80. package/dist/core/command-generation/adapters/claude.js +12 -17
  81. package/dist/core/command-generation/adapters/cline.js +5 -5
  82. package/dist/core/command-generation/adapters/codebuddy.js +8 -7
  83. package/dist/core/command-generation/adapters/continue.js +8 -7
  84. package/dist/core/command-generation/adapters/costrict.js +7 -6
  85. package/dist/core/command-generation/adapters/crush.js +9 -9
  86. package/dist/core/command-generation/adapters/cursor.js +8 -8
  87. package/dist/core/command-generation/adapters/devin.d.ts +19 -0
  88. package/dist/core/command-generation/adapters/devin.js +36 -0
  89. package/dist/core/command-generation/adapters/factory.js +7 -6
  90. package/dist/core/command-generation/adapters/gemini.js +40 -5
  91. package/dist/core/command-generation/adapters/github-copilot.js +6 -5
  92. package/dist/core/command-generation/adapters/iflow.js +9 -8
  93. package/dist/core/command-generation/adapters/index.d.ts +4 -2
  94. package/dist/core/command-generation/adapters/index.js +4 -2
  95. package/dist/core/command-generation/adapters/junie.js +6 -5
  96. package/dist/core/command-generation/adapters/kilocode.js +1 -1
  97. package/dist/core/command-generation/adapters/kiro.js +6 -5
  98. package/dist/core/command-generation/adapters/lingma.js +9 -9
  99. package/dist/core/command-generation/adapters/oh-my-pi.d.ts +21 -0
  100. package/dist/core/command-generation/adapters/oh-my-pi.js +42 -0
  101. package/dist/core/command-generation/adapters/opencode.js +6 -8
  102. package/dist/core/command-generation/adapters/pi.d.ts +2 -2
  103. package/dist/core/command-generation/adapters/pi.js +7 -10
  104. package/dist/core/command-generation/adapters/qoder.js +9 -9
  105. package/dist/core/command-generation/adapters/qwen.d.ts +7 -3
  106. package/dist/core/command-generation/adapters/qwen.js +14 -9
  107. package/dist/core/command-generation/adapters/roocode.d.ts +4 -4
  108. package/dist/core/command-generation/adapters/roocode.js +9 -9
  109. package/dist/core/command-generation/adapters/trae.d.ts +13 -0
  110. package/dist/core/command-generation/adapters/trae.js +28 -0
  111. package/dist/core/command-generation/adapters/zcode.d.ts +16 -0
  112. package/dist/core/command-generation/adapters/zcode.js +33 -0
  113. package/dist/core/command-generation/generator.d.ts +8 -0
  114. package/dist/core/command-generation/generator.js +15 -1
  115. package/dist/core/command-generation/index.d.ts +1 -1
  116. package/dist/core/command-generation/index.js +1 -1
  117. package/dist/core/command-generation/invocation.d.ts +73 -0
  118. package/dist/core/command-generation/invocation.js +77 -0
  119. package/dist/core/command-generation/registry.js +8 -4
  120. package/dist/core/command-generation/types.d.ts +9 -2
  121. package/dist/core/command-generation/yaml.d.ts +20 -6
  122. package/dist/core/command-generation/yaml.js +31 -21
  123. package/dist/core/command-surface.d.ts +15 -0
  124. package/dist/core/command-surface.js +33 -0
  125. package/dist/core/completions/command-registry.js +16 -61
  126. package/dist/core/completions/generators/bash-generator.js +41 -41
  127. package/dist/core/completions/generators/fish-generator.js +7 -7
  128. package/dist/core/completions/generators/powershell-generator.js +44 -37
  129. package/dist/core/completions/generators/zsh-generator.js +33 -33
  130. package/dist/core/completions/installers/zsh-installer.d.ts +9 -0
  131. package/dist/core/completions/installers/zsh-installer.js +20 -4
  132. package/dist/core/completions/templates/bash-templates.js +24 -24
  133. package/dist/core/completions/templates/fish-templates.js +38 -38
  134. package/dist/core/completions/templates/powershell-templates.js +28 -28
  135. package/dist/core/completions/templates/zsh-templates.js +39 -39
  136. package/dist/core/config-prompts.d.ts +1 -1
  137. package/dist/core/config-prompts.js +14 -1
  138. package/dist/core/config-schema.d.ts +10 -1
  139. package/dist/core/config-schema.js +67 -1
  140. package/dist/core/config.d.ts +15 -0
  141. package/dist/core/config.js +46 -5
  142. package/dist/core/file-state.js +56 -24
  143. package/dist/core/github-copilot/cloud-agent.d.ts +95 -0
  144. package/dist/core/github-copilot/cloud-agent.js +497 -0
  145. package/dist/core/global-config.d.ts +16 -0
  146. package/dist/core/init.d.ts +58 -0
  147. package/dist/core/init.js +474 -73
  148. package/dist/core/legacy-cleanup.d.ts +59 -0
  149. package/dist/core/legacy-cleanup.js +253 -24
  150. package/dist/core/list.js +23 -16
  151. package/dist/core/migration.d.ts +73 -1
  152. package/dist/core/migration.js +360 -11
  153. package/dist/core/modify/conflict-detector.d.ts +26 -0
  154. package/dist/core/modify/conflict-detector.js +62 -0
  155. package/dist/core/modify/dependency-propagator.d.ts +38 -0
  156. package/dist/core/modify/dependency-propagator.js +103 -0
  157. package/dist/core/modify/history-tracker.d.ts +34 -0
  158. package/dist/core/modify/history-tracker.js +114 -0
  159. package/dist/core/modify/index.d.ts +7 -0
  160. package/dist/core/modify/index.js +7 -0
  161. package/dist/core/onboarding-commands.d.ts +30 -0
  162. package/dist/core/onboarding-commands.js +38 -0
  163. package/dist/core/openspec-root.js +18 -15
  164. package/dist/core/parsers/change-parser.d.ts +17 -2
  165. package/dist/core/parsers/change-parser.js +35 -21
  166. package/dist/core/parsers/code-fence.d.ts +15 -0
  167. package/dist/core/parsers/code-fence.js +49 -0
  168. package/dist/core/parsers/markdown-parser.d.ts +0 -3
  169. package/dist/core/parsers/markdown-parser.js +7 -60
  170. package/dist/core/parsers/requirement-blocks.d.ts +29 -0
  171. package/dist/core/parsers/requirement-blocks.js +150 -31
  172. package/dist/core/parsers/requirement-text.d.ts +48 -0
  173. package/dist/core/parsers/requirement-text.js +98 -0
  174. package/dist/core/parsers/spec-structure.d.ts +1 -1
  175. package/dist/core/parsers/spec-structure.js +20 -30
  176. package/dist/core/profile-sync-drift.d.ts +0 -8
  177. package/dist/core/profile-sync-drift.js +49 -50
  178. package/dist/core/profiles.d.ts +2 -2
  179. package/dist/core/profiles.js +2 -2
  180. package/dist/core/project-config.d.ts +27 -18
  181. package/dist/core/project-config.js +101 -34
  182. package/dist/core/references.js +42 -4
  183. package/dist/core/relationship-health.d.ts +8 -0
  184. package/dist/core/relationship-health.js +13 -0
  185. package/dist/core/root-selection.d.ts +9 -6
  186. package/dist/core/root-selection.js +42 -5
  187. package/dist/core/schemas/base.schema.js +7 -2
  188. package/dist/core/shared/allowed-tools.d.ts +12 -0
  189. package/dist/core/shared/allowed-tools.js +12 -0
  190. package/dist/core/shared/index.d.ts +1 -0
  191. package/dist/core/shared/index.js +1 -0
  192. package/dist/core/shared/skill-content-equivalence.d.ts +7 -0
  193. package/dist/core/shared/skill-content-equivalence.js +41 -0
  194. package/dist/core/shared/skill-generation.js +17 -13
  195. package/dist/core/shared/skill-paths.d.ts +13 -0
  196. package/dist/core/shared/skill-paths.js +23 -0
  197. package/dist/core/shared/tool-detection.d.ts +27 -7
  198. package/dist/core/shared/tool-detection.js +167 -37
  199. package/dist/core/shared-skill-target.d.ts +16 -0
  200. package/dist/core/shared-skill-target.js +156 -0
  201. package/dist/core/specs-apply.d.ts +80 -33
  202. package/dist/core/specs-apply.js +667 -145
  203. package/dist/core/store/git.d.ts +13 -0
  204. package/dist/core/store/git.js +24 -0
  205. package/dist/core/store/operations.js +20 -0
  206. package/dist/core/templates/skill-templates.d.ts +3 -2
  207. package/dist/core/templates/skill-templates.js +3 -2
  208. package/dist/core/templates/workflows/apply-change.d.ts +10 -0
  209. package/dist/core/templates/workflows/apply-change.js +186 -224
  210. package/dist/core/templates/workflows/archive-change.js +387 -260
  211. package/dist/core/templates/workflows/bulk-archive-change.js +655 -475
  212. package/dist/core/templates/workflows/continue-change.js +210 -219
  213. package/dist/core/templates/workflows/explore.js +479 -443
  214. package/dist/core/templates/workflows/feedback.js +97 -97
  215. package/dist/core/templates/workflows/ff-change.js +200 -191
  216. package/dist/core/templates/workflows/modify-change.d.ts +4 -1
  217. package/dist/core/templates/workflows/modify-change.js +237 -115
  218. package/dist/core/templates/workflows/new-change.js +127 -134
  219. package/dist/core/templates/workflows/onboard.js +548 -542
  220. package/dist/core/templates/workflows/propose.js +273 -217
  221. package/dist/core/templates/workflows/store-selection.d.ts +1 -1
  222. package/dist/core/templates/workflows/store-selection.js +1 -1
  223. package/dist/core/templates/workflows/sync-specs.js +500 -273
  224. package/dist/core/templates/workflows/update-change.d.ts +10 -0
  225. package/dist/core/templates/workflows/update-change.js +177 -0
  226. package/dist/core/templates/workflows/verify-change.js +324 -326
  227. package/dist/core/update.d.ts +40 -5
  228. package/dist/core/update.js +411 -96
  229. package/dist/core/validation/constants.d.ts +4 -1
  230. package/dist/core/validation/constants.js +4 -1
  231. package/dist/core/validation/plan-validator.d.ts +8 -0
  232. package/dist/core/validation/plan-validator.js +45 -0
  233. package/dist/core/validation/task-numbering.d.ts +16 -0
  234. package/dist/core/validation/task-numbering.js +57 -0
  235. package/dist/core/validation/validator.d.ts +27 -3
  236. package/dist/core/validation/validator.js +337 -48
  237. package/dist/core/version-check.d.ts +152 -0
  238. package/dist/core/version-check.js +731 -0
  239. package/dist/core/view.js +13 -18
  240. package/dist/prompts/searchable-multi-select.js +1 -1
  241. package/dist/telemetry/caller-detection.d.ts +27 -0
  242. package/dist/telemetry/caller-detection.js +85 -0
  243. package/dist/telemetry/caller-detection.test.d.ts +5 -0
  244. package/dist/telemetry/config.d.ts +48 -2
  245. package/dist/telemetry/config.js +113 -11
  246. package/dist/telemetry/config.test.d.ts +5 -0
  247. package/dist/telemetry/deduplication.d.ts +35 -0
  248. package/dist/telemetry/deduplication.js +82 -0
  249. package/dist/telemetry/deduplication.test.d.ts +5 -0
  250. package/dist/telemetry/identity.d.ts +62 -22
  251. package/dist/telemetry/identity.js +147 -102
  252. package/dist/telemetry/index.d.ts +126 -15
  253. package/dist/telemetry/index.js +569 -37
  254. package/dist/telemetry/sanitization.d.ts +40 -0
  255. package/dist/telemetry/sanitization.js +114 -0
  256. package/dist/telemetry/sanitization.test.d.ts +5 -0
  257. package/dist/telemetry/throttling.d.ts +20 -0
  258. package/dist/telemetry/throttling.js +70 -0
  259. package/dist/ui/welcome-screen.d.ts +9 -1
  260. package/dist/ui/welcome-screen.js +91 -38
  261. package/dist/utils/change-metadata.d.ts +43 -0
  262. package/dist/utils/change-metadata.js +123 -7
  263. package/dist/utils/change-utils.d.ts +10 -4
  264. package/dist/utils/change-utils.js +20 -13
  265. package/dist/utils/ci.d.ts +12 -0
  266. package/dist/utils/ci.js +16 -0
  267. package/dist/utils/command-references.d.ts +92 -6
  268. package/dist/utils/command-references.js +180 -7
  269. package/dist/utils/date.d.ts +8 -0
  270. package/dist/utils/date.js +13 -0
  271. package/dist/utils/file-system.d.ts +10 -0
  272. package/dist/utils/file-system.js +70 -0
  273. package/dist/utils/index.d.ts +1 -1
  274. package/dist/utils/index.js +1 -1
  275. package/dist/utils/interactive.d.ts +23 -0
  276. package/dist/utils/interactive.js +32 -0
  277. package/dist/utils/item-discovery.d.ts +19 -0
  278. package/dist/utils/item-discovery.js +30 -48
  279. package/dist/utils/shell-detection.d.ts +1 -1
  280. package/dist/utils/shell-detection.js +64 -11
  281. package/dist/utils/spec-discovery.d.ts +40 -0
  282. package/dist/utils/spec-discovery.js +115 -0
  283. package/dist/utils/task-progress.d.ts +29 -1
  284. package/dist/utils/task-progress.js +109 -15
  285. package/package.json +96 -84
  286. package/schemas/spec-driven/schema.yaml +252 -184
  287. package/schemas/spec-driven/templates/design.md +19 -19
  288. package/schemas/spec-driven/templates/plan-template.md +91 -0
  289. package/schemas/spec-driven/templates/proposal.md +29 -24
  290. package/schemas/spec-driven/templates/spec.md +11 -8
  291. package/schemas/spec-driven/templates/tasks.md +9 -9
  292. package/scripts/postinstall.js +83 -83
  293. package/dist/core/command-generation/adapters/codex.d.ts +0 -16
  294. package/dist/core/command-generation/adapters/codex.js +0 -39
  295. package/dist/core/command-generation/adapters/windsurf.d.ts +0 -14
  296. package/dist/core/command-generation/adapters/windsurf.js +0 -38
  297. package/dist/core/comprehension/config.d.ts +0 -20
  298. package/dist/core/comprehension/config.js +0 -23
  299. package/dist/core/comprehension/fingerprint.d.ts +0 -14
  300. package/dist/core/comprehension/fingerprint.js +0 -33
  301. package/dist/core/comprehension/index.d.ts +0 -57
  302. package/dist/core/comprehension/index.js +0 -87
  303. package/dist/core/comprehension/pass-record.d.ts +0 -29
  304. package/dist/core/comprehension/pass-record.js +0 -64
  305. package/dist/core/comprehension/stats.d.ts +0 -35
  306. package/dist/core/comprehension/stats.js +0 -133
  307. package/dist/core/templates/workflows/comprehension-guidance.d.ts +0 -9
  308. package/dist/core/templates/workflows/comprehension-guidance.js +0 -73
  309. package/dist/core/templates/workflows/mcp-guidance.d.ts +0 -13
  310. package/dist/core/templates/workflows/mcp-guidance.js +0 -131
  311. package/dist/core/templates/workflows/user-prompt-guidance.d.ts +0 -16
  312. package/dist/core/templates/workflows/user-prompt-guidance.js +0 -48
  313. package/dist/telemetry/caller.d.ts +0 -5
  314. package/dist/telemetry/caller.js +0 -29
  315. package/dist/telemetry/client.d.ts +0 -27
  316. package/dist/telemetry/client.js +0 -127
  317. package/dist/telemetry/command-context.d.ts +0 -13
  318. package/dist/telemetry/command-context.js +0 -59
  319. package/dist/telemetry/comprehension.d.ts +0 -44
  320. package/dist/telemetry/comprehension.js +0 -105
  321. package/dist/telemetry/content.d.ts +0 -10
  322. package/dist/telemetry/content.js +0 -56
  323. package/dist/telemetry/git-stats.d.ts +0 -12
  324. package/dist/telemetry/git-stats.js +0 -69
  325. package/dist/telemetry/identify-cache.d.ts +0 -7
  326. package/dist/telemetry/identify-cache.js +0 -47
  327. package/dist/telemetry/input.d.ts +0 -17
  328. package/dist/telemetry/input.js +0 -68
  329. package/dist/telemetry/marker.d.ts +0 -37
  330. package/dist/telemetry/marker.js +0 -67
  331. package/dist/telemetry/workflow.d.ts +0 -85
  332. package/dist/telemetry/workflow.js +0 -318
  333. package/schemas/spec-driven/templates/plan.md +0 -19
@@ -0,0 +1,731 @@
1
+ import fs from 'fs';
2
+ import http from 'http';
3
+ import https from 'https';
4
+ import path from 'path';
5
+ import { createRequire } from 'module';
6
+ import chalk from 'chalk';
7
+ import { isCiEnvironment } from '../utils/ci.js';
8
+ import { getGlobalConfig } from './global-config.js';
9
+ const require = createRequire(import.meta.url);
10
+ const { name: PACKAGE_NAME, version: OPENSPEC_VERSION } = require('../../package.json');
11
+ const DEFAULT_REGISTRY = 'https://registry.npmjs.org';
12
+ const REQUEST_TIMEOUT_MS = 1500;
13
+ const MAX_RESPONSE_BYTES = 256 * 1024;
14
+ const VERSION_PROBE_TIMEOUT_MS = 5000;
15
+ const MAX_REDIRECTS = 3;
16
+ /**
17
+ * A version we are willing to print. The registry only ever serves SemVer here,
18
+ * so anything else is either a broken mirror or a hostile response — and since
19
+ * this string lands in the terminal next to an install command, an unvalidated
20
+ * one could smuggle ANSI cursor controls and repaint the lines around it.
21
+ */
22
+ const SAFE_VERSION = /^\d{1,10}\.\d{1,10}\.\d{1,10}(?:-[0-9A-Za-z.-]{1,64})?(?:\+[0-9A-Za-z.-]{1,64})?$/;
23
+ /**
24
+ * The check is opt-out and must never get in the way: no network in CI or
25
+ * tests, an explicit escape hatch for anyone offline or air-gapped, and the
26
+ * same privacy signals telemetry already honors — a user who set DO_NOT_TRACK
27
+ * or telemetry.enabled false did not agree to a different outbound request.
28
+ */
29
+ function isCheckEnabled() {
30
+ if (process.env.OPENSPEC_NO_UPDATE_CHECK !== undefined)
31
+ return false;
32
+ if (process.env.DO_NOT_TRACK === '1')
33
+ return false;
34
+ if (process.env.OPENSPEC_TELEMETRY === '0')
35
+ return false;
36
+ if (isCiEnvironment())
37
+ return false;
38
+ if (process.env.NODE_ENV === 'test')
39
+ return false;
40
+ // Same config opt-out as telemetry (env remains the hard override above).
41
+ if (getGlobalConfig().telemetry?.enabled === false)
42
+ return false;
43
+ return true;
44
+ }
45
+ /**
46
+ * The registry to ask: only the environment variable npm exports (under
47
+ * `npm run`, or an explicit export). Deliberately not a `registry=` line from
48
+ * any .npmrc — letting file contents choose the destination of an outbound
49
+ * request is a flow worth avoiding for a convenience this small, and a project
50
+ * file would travel with a cloned repository. Anyone on a private mirror can
51
+ * export `npm_config_registry`, or turn the check off entirely.
52
+ */
53
+ export function registryUrl() {
54
+ const configured = process.env.npm_config_registry?.trim();
55
+ const base = configured && /^https?:\/\//i.test(configured) ? configured : DEFAULT_REGISTRY;
56
+ return `${base.replace(/\/+$/, '')}/${PACKAGE_NAME}/latest`;
57
+ }
58
+ /**
59
+ * Compares two prerelease tags per SemVer: dot-separated identifiers compared
60
+ * one by one, numeric identifiers numerically (so beta.10 > beta.2), numeric
61
+ * ranking below alphanumeric, and a longer identifier list winning ties.
62
+ */
63
+ function comparePrerelease(a, b) {
64
+ if (a === b)
65
+ return 0;
66
+ if (a === '')
67
+ return 1;
68
+ if (b === '')
69
+ return -1;
70
+ const left = a.split('.');
71
+ const right = b.split('.');
72
+ for (let i = 0; i < Math.max(left.length, right.length); i++) {
73
+ const l = left[i];
74
+ const r = right[i];
75
+ if (l === undefined)
76
+ return -1;
77
+ if (r === undefined)
78
+ return 1;
79
+ const lNumeric = /^\d+$/.test(l);
80
+ const rNumeric = /^\d+$/.test(r);
81
+ if (lNumeric && rNumeric) {
82
+ const diff = Number.parseInt(l, 10) - Number.parseInt(r, 10);
83
+ if (diff !== 0)
84
+ return diff > 0 ? 1 : -1;
85
+ continue;
86
+ }
87
+ if (lNumeric !== rNumeric)
88
+ return lNumeric ? -1 : 1;
89
+ if (l !== r)
90
+ return l > r ? 1 : -1;
91
+ }
92
+ return 0;
93
+ }
94
+ /**
95
+ * Compares two semver-ish versions. Returns 1 when a > b, -1 when a < b, 0
96
+ * otherwise. Prereleases sort below their release (1.7.0-beta.1 < 1.7.0).
97
+ */
98
+ export function compareVersions(a, b) {
99
+ const parse = (version) => {
100
+ const withoutBuild = version.trim().replace(/^v/, '').split('+', 1)[0] ?? '';
101
+ const separator = withoutBuild.indexOf('-');
102
+ const core = separator === -1 ? withoutBuild : withoutBuild.slice(0, separator);
103
+ const prerelease = separator === -1 ? '' : withoutBuild.slice(separator + 1);
104
+ const parts = core.split('.').map((n) => Number.parseInt(n, 10));
105
+ return {
106
+ numbers: [parts[0] || 0, parts[1] || 0, parts[2] || 0],
107
+ prerelease,
108
+ };
109
+ };
110
+ const left = parse(a);
111
+ const right = parse(b);
112
+ for (let i = 0; i < 3; i++) {
113
+ if (left.numbers[i] > right.numbers[i])
114
+ return 1;
115
+ if (left.numbers[i] < right.numbers[i])
116
+ return -1;
117
+ }
118
+ return comparePrerelease(left.prerelease, right.prerelease);
119
+ }
120
+ /**
121
+ * Reads the `latest` dist-tag. Sends no custom Accept header: the registry
122
+ * answers `/<pkg>/latest` with 406 for npm's abbreviated-metadata type, which
123
+ * it only serves on the full packument.
124
+ *
125
+ * Uses node:http(s) rather than fetch so the timeout can destroy the socket.
126
+ * Aborting a fetch that is still completing its TCP handshake — a firewall
127
+ * dropping packets, a captive portal — leaves the connect handle open and the
128
+ * CLI cannot exit until the OS gives up, long after the hint has printed.
129
+ */
130
+ function fetchLatestVersion() {
131
+ return new Promise((resolve) => {
132
+ let settled = false;
133
+ let timer;
134
+ const finish = (version) => {
135
+ if (settled)
136
+ return;
137
+ settled = true;
138
+ if (timer)
139
+ clearTimeout(timer);
140
+ resolve(version);
141
+ };
142
+ let url;
143
+ try {
144
+ url = new URL(registryUrl());
145
+ }
146
+ catch {
147
+ resolve(null);
148
+ return;
149
+ }
150
+ // Mirrors and corporate front-ends redirect; without following one the
151
+ // check would be permanently and silently dead for them.
152
+ let redirectsLeft = MAX_REDIRECTS;
153
+ // The budget timer must tear down whichever request is open when it
154
+ // fires. Closing over the first hop's request would leave a redirected
155
+ // socket alive: a target that trickles bytes keeps resetting its idle
156
+ // timeout, and only the body-size cap would end it.
157
+ let activeRequest;
158
+ const send = (target) => {
159
+ const request = (target.protocol === 'http:' ? http : https).get(target, { timeout: REQUEST_TIMEOUT_MS }, (response) => {
160
+ const status = response.statusCode ?? 0;
161
+ const location = response.headers.location;
162
+ if (status >= 300 && status < 400 && location) {
163
+ response.resume();
164
+ request.destroy();
165
+ if (redirectsLeft <= 0) {
166
+ finish(null);
167
+ return;
168
+ }
169
+ redirectsLeft -= 1;
170
+ try {
171
+ const next = new URL(location, target);
172
+ // Never follow a downgrade to plain http: a MITM on the reply
173
+ // would control the "newer version" answer.
174
+ const downgrade = target.protocol === 'https:' && next.protocol === 'http:';
175
+ if (!downgrade && (next.protocol === 'http:' || next.protocol === 'https:')) {
176
+ send(next);
177
+ return;
178
+ }
179
+ }
180
+ catch {
181
+ // Unparseable Location.
182
+ }
183
+ finish(null);
184
+ return;
185
+ }
186
+ if (status !== 200) {
187
+ response.resume();
188
+ request.destroy();
189
+ finish(null);
190
+ return;
191
+ }
192
+ let body = '';
193
+ response.setEncoding('utf-8');
194
+ response.on('data', (chunk) => {
195
+ body += chunk;
196
+ // The dist-tag document is small; refuse to buffer a firehose.
197
+ if (body.length > MAX_RESPONSE_BYTES) {
198
+ request.destroy();
199
+ finish(null);
200
+ }
201
+ });
202
+ response.on('end', () => {
203
+ try {
204
+ const parsed = JSON.parse(body);
205
+ const version = parsed.version;
206
+ finish(typeof version === 'string' && SAFE_VERSION.test(version) ? version : null);
207
+ }
208
+ catch {
209
+ finish(null);
210
+ }
211
+ });
212
+ response.on('error', () => finish(null));
213
+ });
214
+ activeRequest = request;
215
+ request.on('timeout', () => {
216
+ request.destroy();
217
+ finish(null);
218
+ });
219
+ request.on('error', () => finish(null));
220
+ // One budget for the whole exchange, redirects included.
221
+ if (!timer) {
222
+ timer = setTimeout(() => {
223
+ activeRequest?.destroy();
224
+ finish(null);
225
+ }, REQUEST_TIMEOUT_MS);
226
+ }
227
+ };
228
+ send(url);
229
+ });
230
+ }
231
+ /**
232
+ * Returns the published version when the installed CLI is behind it, otherwise
233
+ * null. Never throws and never blocks for longer than the request timeout.
234
+ */
235
+ export async function getAvailableCliUpdate() {
236
+ if (!isCheckEnabled())
237
+ return null;
238
+ try {
239
+ const latest = await fetchLatestVersion();
240
+ if (!latest)
241
+ return null;
242
+ return compareVersions(latest, OPENSPEC_VERSION) > 0 ? latest : null;
243
+ }
244
+ catch {
245
+ return null;
246
+ }
247
+ }
248
+ /**
249
+ * Directory the running CLI was loaded from, or null when it cannot be
250
+ * resolved. Shown in the upgrade hint so anyone who upgraded but still runs an
251
+ * old binary — a stale pnpm/volta/npx shim, or two installs on PATH — can see
252
+ * which copy is actually answering.
253
+ */
254
+ export function getInstallDir() {
255
+ try {
256
+ return path.dirname(require.resolve('../../package.json'));
257
+ }
258
+ catch {
259
+ return null;
260
+ }
261
+ }
262
+ /**
263
+ * True when the running CLI resolves from a `node_modules` belonging to the
264
+ * project being updated or any ancestor of it — the hoisted-root layout npm and
265
+ * pnpm workspaces produce. Anchored on the target path rather than the working
266
+ * directory, since `openspec update <path>` and running from a sub-package are
267
+ * both normal. Never throws: process.cwd() fails when the directory has been
268
+ * deleted, and a wrong upgrade hint must not take down a successful update.
269
+ */
270
+ export function isProjectLocalInstall(installDir, projectPath = '.') {
271
+ if (!installDir)
272
+ return false;
273
+ // Windows paths differ in case and drive-letter casing between sources.
274
+ const normalize = (value) => process.platform === 'win32' ? value.toLowerCase() : value;
275
+ try {
276
+ let dir = path.resolve(projectPath);
277
+ const target = normalize(installDir);
278
+ for (;;) {
279
+ if (target.startsWith(normalize(path.join(dir, 'node_modules') + path.sep))) {
280
+ return true;
281
+ }
282
+ const parent = path.dirname(dir);
283
+ if (parent === dir)
284
+ return false;
285
+ dir = parent;
286
+ }
287
+ }
288
+ catch {
289
+ return false;
290
+ }
291
+ }
292
+ /**
293
+ * True for the throwaway caches npx/pnpm dlx/bunx unpack into. Telling those
294
+ * users to install globally would create the second copy on PATH they were
295
+ * deliberately avoiding.
296
+ */
297
+ export function isEphemeralRunnerInstall(installDir) {
298
+ if (!installDir)
299
+ return false;
300
+ const segments = installDir.split(/[\\/]/).map((segment) => segment.toLowerCase());
301
+ return segments.some((segment, i) => segment === '_npx' ||
302
+ segment === '_bunx' ||
303
+ // Only a package manager's own cache, never a user directory that
304
+ // happens to be called "dlx". Windows uses pnpm-cache for the same job.
305
+ (segment === 'dlx' &&
306
+ ['pnpm', 'bun', '.pnpm', 'pnpm-cache', 'bun-cache'].includes(segments[i - 1] ?? '')));
307
+ }
308
+ /**
309
+ * Directories npm installs global packages into. Derived from the running node
310
+ * rather than by shelling out to `npm prefix -g`, which would cost more than
311
+ * the version check itself. Only a hint: `process.execPath` is realpath'd, so
312
+ * on Homebrew it lands in the Cellar rather than the brew prefix — which is
313
+ * why the install's own layout is the primary signal below.
314
+ */
315
+ export function npmGlobalRoots() {
316
+ const roots = [];
317
+ const nodeDir = path.dirname(process.execPath);
318
+ if (process.platform === 'win32') {
319
+ roots.push(path.join(nodeDir, 'node_modules'));
320
+ if (process.env.APPDATA) {
321
+ roots.push(path.join(process.env.APPDATA, 'npm', 'node_modules'));
322
+ }
323
+ }
324
+ else {
325
+ roots.push(path.resolve(nodeDir, '..', 'lib', 'node_modules'));
326
+ }
327
+ const prefix = process.env.npm_config_prefix;
328
+ if (prefix) {
329
+ roots.push(process.platform === 'win32'
330
+ ? path.join(prefix, 'node_modules')
331
+ : path.join(prefix, 'lib', 'node_modules'));
332
+ }
333
+ return roots;
334
+ }
335
+ /**
336
+ * The prefix of an npm global install, read from the install's own shape:
337
+ * `<prefix>/lib/node_modules/<pkg>` on POSIX, `<prefix>/node_modules/<pkg>` on
338
+ * Windows. Self-describing, so it holds for Homebrew, nvm, Debian and anywhere
339
+ * else npm's prefix is not derivable from the node binary. Null when the
340
+ * layout does not match.
341
+ */
342
+ export function npmPrefixFromInstallDir(installDir) {
343
+ if (!installDir)
344
+ return null;
345
+ let dir = installDir;
346
+ for (;;) {
347
+ const parent = path.dirname(dir);
348
+ if (parent === dir)
349
+ return null;
350
+ if (path.basename(dir).toLowerCase() === 'node_modules')
351
+ break;
352
+ dir = parent;
353
+ }
354
+ const container = path.dirname(dir);
355
+ if (process.platform === 'win32')
356
+ return container;
357
+ // POSIX npm always nests the root under lib/.
358
+ return path.basename(container).toLowerCase() === 'lib' ? path.dirname(container) : null;
359
+ }
360
+ /**
361
+ * True only when npm itself owns this copy. Everything else — a pnpm, bun,
362
+ * yarn or volta global — would be made worse by `npm install -g`, which adds a
363
+ * second copy that may not even be the one on PATH.
364
+ */
365
+ export function isNpmGlobalInstall(installDir, roots = npmGlobalRoots()) {
366
+ if (!installDir)
367
+ return false;
368
+ // Another manager's layout can still look like npm's (volta nests a whole
369
+ // node install), so who owns it is decided before where it sits.
370
+ if (detectPackageManager(installDir) !== 'npm')
371
+ return false;
372
+ const normalize = (value) => process.platform === 'win32' ? value.toLowerCase() : value;
373
+ const target = normalize(installDir);
374
+ if (roots.some((root) => target.startsWith(normalize(root + path.sep))))
375
+ return true;
376
+ // The derived roots miss any prefix that is not beside the node binary, so
377
+ // fall back to the install's own shape plus the bin directory npm would
378
+ // have written the shim into.
379
+ const prefix = npmPrefixFromInstallDir(installDir);
380
+ if (!prefix)
381
+ return false;
382
+ try {
383
+ // Corroborate with something npm itself wrote: the bin dir on POSIX, the
384
+ // .cmd shim on Windows. The prefix alone proves nothing — it is just the
385
+ // parent of the node_modules dir the CLI resolved from, so a hand-copied
386
+ // portable tree would pass and be offered an npm upgrade it never had.
387
+ return fs.existsSync(process.platform === 'win32' ? path.join(prefix, 'openspec.cmd') : path.join(prefix, 'bin'));
388
+ }
389
+ catch {
390
+ return false;
391
+ }
392
+ }
393
+ /**
394
+ * True when the CLI is running from a clone rather than an install. Upgrade
395
+ * advice is meaningless there: the version is whatever the branch says.
396
+ */
397
+ export function isSourceCheckout(installDir) {
398
+ if (!installDir)
399
+ return false;
400
+ try {
401
+ return fs.existsSync(path.join(installDir, '.git'));
402
+ }
403
+ catch {
404
+ return false;
405
+ }
406
+ }
407
+ /**
408
+ * The package manager that owns this copy, so the printed command is one the
409
+ * user's setup will actually honor.
410
+ */
411
+ export function detectPackageManager(installDir) {
412
+ // Lowercased because the Windows directories are capitalized and undotted:
413
+ // %LOCALAPPDATA%\\Volta, \\Yarn\\Data, \\pnpm-cache.
414
+ const segments = (installDir ?? '').split(/[\\/]/).map((segment) => segment.toLowerCase());
415
+ const has = (...names) => names.some((name) => segments.includes(name));
416
+ // The undotted spelling exists for Windows (%LOCALAPPDATA%\Volta), whose
417
+ // layout nests tools\image; require both segments so a user or project
418
+ // directory merely named "volta" (even one with its own "tools" dir) does
419
+ // not steal the install.
420
+ if (has('.volta') || (has('volta') && has('tools') && has('image')))
421
+ return 'volta';
422
+ if (has('.bun'))
423
+ return 'bun';
424
+ // These two need a corroborating segment: a directory merely named "pnpm" or
425
+ // "yarn" (a user's home, a project) is not a global install of one.
426
+ if (has('.pnpm-global', 'pnpm-cache'))
427
+ return 'pnpm';
428
+ if (has('pnpm') && has('global', 'dlx', 'store'))
429
+ return 'pnpm';
430
+ if (has('.yarn') || (has('yarn') && has('global')))
431
+ return 'yarn';
432
+ return 'npm';
433
+ }
434
+ const GLOBAL_UPGRADE_COMMANDS = {
435
+ npm: `npm install -g ${PACKAGE_NAME}@latest`,
436
+ pnpm: `pnpm add -g ${PACKAGE_NAME}@latest`,
437
+ bun: `bun add -g ${PACKAGE_NAME}@latest`,
438
+ yarn: `yarn global add ${PACKAGE_NAME}@latest`,
439
+ volta: `volta install ${PACKAGE_NAME}@latest`,
440
+ };
441
+ /**
442
+ * Builds the hint, with the upgrade command chosen for how this copy of the CLI
443
+ * was installed. Pure so every branch is assertable.
444
+ */
445
+ export function buildCliUpdateLines(latestVersion, installDir, projectPath, options = {}) {
446
+ const lines = [`A newer OpenSpec CLI is available (v${OPENSPEC_VERSION} → v${latestVersion}).`];
447
+ // Omitted when we are about to offer to run it — printing a command and then
448
+ // asking to run that same command reads like the user has to do both.
449
+ if (options.withCommand !== false) {
450
+ lines.push(...buildUpgradeCommandLines(installDir, projectPath));
451
+ }
452
+ if (installDir) {
453
+ lines.push(` Running from: ${installDir}`);
454
+ }
455
+ return lines;
456
+ }
457
+ /**
458
+ * The upgrade command for however this copy was installed, plus the reminder
459
+ * that instruction files come from the CLI and so need a second pass.
460
+ */
461
+ export function buildUpgradeCommandLines(installDir, projectPath) {
462
+ const lines = [];
463
+ if (isEphemeralRunnerInstall(installDir)) {
464
+ // That command *is* the update, so there is nothing to run afterwards.
465
+ lines.push(` npx ${PACKAGE_NAME}@latest update`);
466
+ return lines;
467
+ }
468
+ if (isProjectLocalInstall(installDir, projectPath)) {
469
+ // Its package manager owns the lockfile; naming npm could be wrong.
470
+ lines.push(` Update the ${PACKAGE_NAME} dependency in this project.`);
471
+ }
472
+ else {
473
+ lines.push(` ${GLOBAL_UPGRADE_COMMANDS[detectPackageManager(installDir)]}`);
474
+ }
475
+ lines.push(' Then run "openspec update" again to pick up new workflows.');
476
+ return lines;
477
+ }
478
+ // cross-spawn resolves npm's shim on Windows, where spawning "npm" directly
479
+ // fails. Loaded lazily so ordinary runs skip its module graph.
480
+ let cachedSpawn;
481
+ function loadSpawn() {
482
+ if (cachedSpawn === undefined) {
483
+ cachedSpawn = require('cross-spawn');
484
+ }
485
+ return cachedSpawn;
486
+ }
487
+ /**
488
+ * Whether we can run the upgrade for the user instead of only printing it.
489
+ *
490
+ * Only an npm-owned global install qualifies, because `npm install -g` is the
491
+ * only command we run: a pnpm/bun/yarn/volta global would get a second copy
492
+ * that may not be the one on PATH, a project dependency belongs to that
493
+ * project's package manager, an npx/dlx cache has nothing to upgrade, and a
494
+ * source checkout is not an install at all.
495
+ */
496
+ export function canSelfUpgrade(installDir, projectPath) {
497
+ if (!installDir)
498
+ return false;
499
+ if (isEphemeralRunnerInstall(installDir))
500
+ return false;
501
+ // Both anchors matter: `openspec update ../other` from a project that owns
502
+ // the CLI as a dependency is still a project-local install.
503
+ if (isProjectLocalInstall(installDir, projectPath))
504
+ return false;
505
+ if (isProjectLocalInstall(installDir))
506
+ return false;
507
+ if (isSourceCheckout(installDir))
508
+ return false;
509
+ return isNpmGlobalInstall(installDir);
510
+ }
511
+ /**
512
+ * Whether to offer the upgrade rather than just print the command. Kept here,
513
+ * as a pure function of the environment, because the interesting mistakes live
514
+ * in this decision: offering where `npm install -g` cannot help, or asking a
515
+ * question no one can answer.
516
+ */
517
+ export function shouldOfferUpgrade(params) {
518
+ // A prompt written to a redirected stdout is a question the user never sees
519
+ // and the command waits on forever.
520
+ if (!params.interactive || !params.stdoutIsTty)
521
+ return false;
522
+ return canSelfUpgrade(params.installDir, params.projectPath);
523
+ }
524
+ /**
525
+ * Runs `npm install -g <pkg>@latest`, inheriting stdio so npm's own output —
526
+ * including any auth or permission prompt — reaches the user directly.
527
+ * Resolves true only on a clean exit.
528
+ */
529
+ async function runGlobalUpgrade() {
530
+ const spawn = loadSpawn();
531
+ return new Promise((resolve) => {
532
+ const child = spawn('npm', ['install', '-g', `${PACKAGE_NAME}@latest`], {
533
+ stdio: 'inherit',
534
+ });
535
+ child.on('error', () => resolve(false));
536
+ child.on('close', (code) => resolve(code === 0));
537
+ });
538
+ }
539
+ /**
540
+ * The `openspec` npm installs alongside its global package, so the upgrade can
541
+ * be handed to the copy npm just wrote rather than to whatever PATH resolves.
542
+ * Null when it cannot be found, in which case PATH is the only option left.
543
+ */
544
+ export function upgradedBinPath(roots = npmGlobalRoots(), installDir = getInstallDir()) {
545
+ // The copy npm just replaced tells us exactly which prefix it wrote to;
546
+ // a root derived from the node binary can point at an unrelated install.
547
+ const ownPrefix = npmPrefixFromInstallDir(installDir);
548
+ const ordered = ownPrefix
549
+ ? [
550
+ process.platform === 'win32'
551
+ ? path.join(ownPrefix, 'node_modules')
552
+ : path.join(ownPrefix, 'lib', 'node_modules'),
553
+ ...roots,
554
+ ]
555
+ : roots;
556
+ for (const root of ordered) {
557
+ // npm writes the shim beside the global root on Windows
558
+ // (%APPDATA%\\npm\\openspec.cmd) and in <prefix>/bin on POSIX.
559
+ const candidates = process.platform === 'win32'
560
+ ? [path.join(path.dirname(root), 'openspec.cmd')]
561
+ : [path.resolve(root, '..', '..', 'bin', 'openspec')];
562
+ for (const candidate of candidates) {
563
+ try {
564
+ if (fs.existsSync(candidate))
565
+ return candidate;
566
+ }
567
+ catch {
568
+ // Unreadable candidate; try the next one.
569
+ }
570
+ }
571
+ }
572
+ return null;
573
+ }
574
+ /**
575
+ * Asks a CLI binary its version. Used to confirm an upgrade actually landed:
576
+ * `npm install -g` exits 0 even when it installed nothing, so its exit code
577
+ * alone cannot justify telling the user they are on a new version.
578
+ */
579
+ export function readCliVersion(binPath) {
580
+ const spawn = loadSpawn();
581
+ return new Promise((resolve) => {
582
+ let output = '';
583
+ let child;
584
+ try {
585
+ child = spawn(binPath, ['--version'], { stdio: ['ignore', 'pipe', 'ignore'] });
586
+ }
587
+ catch {
588
+ resolve(null);
589
+ return;
590
+ }
591
+ // Never let a probe hold the CLI open: a wrapper that traps SIGTERM would
592
+ // otherwise keep the process alive for as long as it runs.
593
+ child.unref();
594
+ const timer = setTimeout(() => {
595
+ child.kill('SIGKILL');
596
+ resolve(null);
597
+ }, VERSION_PROBE_TIMEOUT_MS);
598
+ child.stdout?.on('data', (chunk) => {
599
+ output += chunk.toString();
600
+ });
601
+ child.on('error', () => {
602
+ clearTimeout(timer);
603
+ resolve(null);
604
+ });
605
+ child.on('close', () => {
606
+ clearTimeout(timer);
607
+ // A line that is only a version, not the first version-shaped token
608
+ // anywhere: a wrapper banner ("Node.js v25.8.1 | OpenSpec") would
609
+ // otherwise be read as the answer.
610
+ const version = output
611
+ .split(/\r?\n/)
612
+ .map((line) => line.trim())
613
+ .filter((line) => SAFE_VERSION.test(line.replace(/^v/, '')))
614
+ .pop();
615
+ resolve(version ? version.replace(/^v/, '') : null);
616
+ });
617
+ });
618
+ }
619
+ function isPromptCancellation(error) {
620
+ const name = error?.name;
621
+ return name === 'ExitPromptError' || name === 'AbortPromptError';
622
+ }
623
+ /**
624
+ * Offers to run the upgrade and reports what actually happened. The version is
625
+ * read back from the installed binary rather than assumed, so "upgraded" is a
626
+ * fact and a PATH that still answers with the old copy is caught here instead
627
+ * of silently doing nothing.
628
+ */
629
+ export async function offerCliUpgrade(latestVersion) {
630
+ const { confirm } = await import('@inquirer/prompts');
631
+ let accepted = false;
632
+ try {
633
+ accepted = await confirm({
634
+ message: `Upgrade to v${latestVersion} now?`,
635
+ default: true,
636
+ });
637
+ }
638
+ catch (error) {
639
+ // Ctrl-C means stop, not "no thanks, carry on with everything else".
640
+ return isPromptCancellation(error) ? 'cancelled' : 'declined';
641
+ }
642
+ if (!accepted)
643
+ return 'declined';
644
+ console.log();
645
+ const installed = await runGlobalUpgrade();
646
+ console.log();
647
+ if (!installed) {
648
+ console.log(chalk.yellow('The upgrade did not complete. A global install may need'));
649
+ console.log(chalk.yellow('elevated permissions, or a different package manager.'));
650
+ return 'failed';
651
+ }
652
+ const binPath = upgradedBinPath();
653
+ const version = await readCliVersion(binPath ?? 'openspec');
654
+ if (!version) {
655
+ console.log(chalk.yellow('Upgrade finished, but no "openspec" could be run to confirm it.'));
656
+ return 'not-on-path';
657
+ }
658
+ if (compareVersions(version, OPENSPEC_VERSION) <= 0) {
659
+ console.log(chalk.yellow(`Upgrade finished, but "openspec" still reports v${version}.`));
660
+ console.log(chalk.dim(binPath
661
+ ? // We asked the installed copy directly, so PATH is not the story.
662
+ ` npm reported success, but ${binPath} did not change.`
663
+ : ' Another install earlier on your PATH is answering first.'));
664
+ return 'not-on-path';
665
+ }
666
+ console.log(chalk.green(`✓ Upgraded to v${version}.`));
667
+ return 'upgraded';
668
+ }
669
+ /**
670
+ * Runs `openspec update` again with the CLI that was just installed — this
671
+ * process is still the old code, so it cannot write the new workflows itself.
672
+ * Resolves the exit code to pass along; when no `openspec` is on PATH the
673
+ * upgrade still landed but nothing was regenerated, so it says so and
674
+ * resolves 0 rather than reporting a failure the upgrade did not have.
675
+ */
676
+ export async function rerunUpdateWithUpgradedCli(projectPath, options = {}) {
677
+ const spawn = loadSpawn();
678
+ const binPath = options.binPath ?? upgradedBinPath() ?? 'openspec';
679
+ // The re-run stands in for the command the user typed, so it has to carry
680
+ // the flags they typed with it.
681
+ const args = ['update'];
682
+ if (options.force)
683
+ args.push('--force');
684
+ // `--` so a path that looks like a flag stays a path.
685
+ args.push('--', projectPath);
686
+ return new Promise((resolve) => {
687
+ const child = spawn(binPath, args, {
688
+ stdio: 'inherit',
689
+ env: {
690
+ ...process.env,
691
+ // The child must not offer the upgrade again: if PATH still resolves
692
+ // to the old binary, prompting would loop forever.
693
+ OPENSPEC_NO_UPDATE_CHECK: '1',
694
+ // This is a continuation of the command the user already ran, and the
695
+ // parent recorded it; counting it twice would overstate usage.
696
+ OPENSPEC_TELEMETRY: '0',
697
+ },
698
+ });
699
+ child.on('error', () => {
700
+ // Nothing to hand off to: the upgrade landed but the instruction files
701
+ // are still the old ones, so this run did not do what was asked.
702
+ console.log(chalk.yellow('Instruction files were not regenerated.'));
703
+ console.log(chalk.dim(' Run "openspec update" to pick up the new workflows.'));
704
+ resolve(1);
705
+ });
706
+ // A child killed by a signal reports no code; that is not success.
707
+ child.on('close', (code) => resolve(code ?? 1));
708
+ });
709
+ }
710
+ /**
711
+ * Prints the upgrade hint. Instruction files are generated by the installed
712
+ * CLI, so "up to date" only ever means "matches this CLI" — without this note
713
+ * a stale install looks like a successful update.
714
+ */
715
+ export function displayCliUpdateNote(latestVersion, projectPath = '.', options = {}) {
716
+ const [headline, ...rest] = buildCliUpdateLines(latestVersion, getInstallDir(), projectPath, options);
717
+ console.log();
718
+ console.log(chalk.yellow(headline));
719
+ for (const line of rest) {
720
+ console.log(chalk.dim(line));
721
+ }
722
+ }
723
+ /**
724
+ * Prints just the manual command, for when the offer was declined or failed.
725
+ */
726
+ export function displayUpgradeCommand(projectPath = '.') {
727
+ for (const line of buildUpgradeCommandLines(getInstallDir(), projectPath)) {
728
+ console.log(chalk.dim(line));
729
+ }
730
+ }
731
+ //# sourceMappingURL=version-check.js.map