@niujiajie/freespec 0.0.0-stage → 1.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (308) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +58 -2
  3. package/bin/freespec.js +5 -0
  4. package/dist/cli/index.d.ts +25 -0
  5. package/dist/cli/index.js +359 -0
  6. package/dist/commands/shared-output.d.ts +18 -0
  7. package/dist/commands/shared-output.js +61 -0
  8. package/dist/commands/workflow/default-schema.d.ts +2 -0
  9. package/dist/commands/workflow/default-schema.js +4 -0
  10. package/dist/commands/workflow/index.d.ts +13 -0
  11. package/dist/commands/workflow/index.js +10 -0
  12. package/dist/commands/workflow/instructions.d.ts +39 -0
  13. package/dist/commands/workflow/instructions.js +565 -0
  14. package/dist/commands/workflow/new-change.d.ts +19 -0
  15. package/dist/commands/workflow/new-change.js +135 -0
  16. package/dist/commands/workflow/shared.d.ts +94 -0
  17. package/dist/commands/workflow/shared.js +169 -0
  18. package/dist/commands/workflow/status.d.ts +18 -0
  19. package/dist/commands/workflow/status.js +201 -0
  20. package/dist/core/artifact-graph/graph.d.ts +72 -0
  21. package/dist/core/artifact-graph/graph.js +165 -0
  22. package/dist/core/artifact-graph/index.d.ts +9 -0
  23. package/dist/core/artifact-graph/index.js +14 -0
  24. package/dist/core/artifact-graph/instruction-loader.d.ts +208 -0
  25. package/dist/core/artifact-graph/instruction-loader.js +268 -0
  26. package/dist/core/artifact-graph/outputs.d.ts +19 -0
  27. package/dist/core/artifact-graph/outputs.js +351 -0
  28. package/dist/core/artifact-graph/resolver.d.ts +89 -0
  29. package/dist/core/artifact-graph/resolver.js +342 -0
  30. package/dist/core/artifact-graph/schema.d.ts +27 -0
  31. package/dist/core/artifact-graph/schema.js +160 -0
  32. package/dist/core/artifact-graph/state.d.ts +12 -0
  33. package/dist/core/artifact-graph/state.js +31 -0
  34. package/dist/core/artifact-graph/types.d.ts +40 -0
  35. package/dist/core/artifact-graph/types.js +59 -0
  36. package/dist/core/available-tools.d.ts +18 -0
  37. package/dist/core/available-tools.js +49 -0
  38. package/dist/core/change-layout.d.ts +17 -0
  39. package/dist/core/change-layout.js +30 -0
  40. package/dist/core/change-metadata/index.d.ts +2 -0
  41. package/dist/core/change-metadata/index.js +2 -0
  42. package/dist/core/change-metadata/schema.d.ts +23 -0
  43. package/dist/core/change-metadata/schema.js +46 -0
  44. package/dist/core/change-status-policy.d.ts +49 -0
  45. package/dist/core/change-status-policy.js +50 -0
  46. package/dist/core/command-generation/adapters/amazon-q.d.ts +17 -0
  47. package/dist/core/command-generation/adapters/amazon-q.js +32 -0
  48. package/dist/core/command-generation/adapters/antigravity.d.ts +13 -0
  49. package/dist/core/command-generation/adapters/antigravity.js +27 -0
  50. package/dist/core/command-generation/adapters/atomcode.d.ts +24 -0
  51. package/dist/core/command-generation/adapters/atomcode.js +46 -0
  52. package/dist/core/command-generation/adapters/auggie.d.ts +13 -0
  53. package/dist/core/command-generation/adapters/auggie.js +28 -0
  54. package/dist/core/command-generation/adapters/bob.d.ts +18 -0
  55. package/dist/core/command-generation/adapters/bob.js +33 -0
  56. package/dist/core/command-generation/adapters/claude.d.ts +13 -0
  57. package/dist/core/command-generation/adapters/claude.js +32 -0
  58. package/dist/core/command-generation/adapters/cline.d.ts +14 -0
  59. package/dist/core/command-generation/adapters/cline.js +27 -0
  60. package/dist/core/command-generation/adapters/codeassistant.d.ts +15 -0
  61. package/dist/core/command-generation/adapters/codeassistant.js +29 -0
  62. package/dist/core/command-generation/adapters/codebuddy.d.ts +13 -0
  63. package/dist/core/command-generation/adapters/codebuddy.js +29 -0
  64. package/dist/core/command-generation/adapters/codestudio.d.ts +13 -0
  65. package/dist/core/command-generation/adapters/codestudio.js +27 -0
  66. package/dist/core/command-generation/adapters/command-code.d.ts +19 -0
  67. package/dist/core/command-generation/adapters/command-code.js +34 -0
  68. package/dist/core/command-generation/adapters/continue.d.ts +13 -0
  69. package/dist/core/command-generation/adapters/continue.js +33 -0
  70. package/dist/core/command-generation/adapters/costrict.d.ts +13 -0
  71. package/dist/core/command-generation/adapters/costrict.js +28 -0
  72. package/dist/core/command-generation/adapters/crush.d.ts +13 -0
  73. package/dist/core/command-generation/adapters/crush.js +30 -0
  74. package/dist/core/command-generation/adapters/cursor.d.ts +14 -0
  75. package/dist/core/command-generation/adapters/cursor.js +31 -0
  76. package/dist/core/command-generation/adapters/devin.d.ts +19 -0
  77. package/dist/core/command-generation/adapters/devin.js +36 -0
  78. package/dist/core/command-generation/adapters/easycode.d.ts +17 -0
  79. package/dist/core/command-generation/adapters/easycode.js +28 -0
  80. package/dist/core/command-generation/adapters/factory.d.ts +13 -0
  81. package/dist/core/command-generation/adapters/factory.js +28 -0
  82. package/dist/core/command-generation/adapters/gemini.d.ts +13 -0
  83. package/dist/core/command-generation/adapters/gemini.js +27 -0
  84. package/dist/core/command-generation/adapters/gigacode.d.ts +17 -0
  85. package/dist/core/command-generation/adapters/gigacode.js +31 -0
  86. package/dist/core/command-generation/adapters/github-copilot.d.ts +13 -0
  87. package/dist/core/command-generation/adapters/github-copilot.js +27 -0
  88. package/dist/core/command-generation/adapters/iflow.d.ts +13 -0
  89. package/dist/core/command-generation/adapters/iflow.js +30 -0
  90. package/dist/core/command-generation/adapters/index.d.ts +40 -0
  91. package/dist/core/command-generation/adapters/index.js +40 -0
  92. package/dist/core/command-generation/adapters/junie.d.ts +13 -0
  93. package/dist/core/command-generation/adapters/junie.js +27 -0
  94. package/dist/core/command-generation/adapters/kilocode.d.ts +14 -0
  95. package/dist/core/command-generation/adapters/kilocode.js +23 -0
  96. package/dist/core/command-generation/adapters/kiro.d.ts +13 -0
  97. package/dist/core/command-generation/adapters/kiro.js +27 -0
  98. package/dist/core/command-generation/adapters/lingma.d.ts +13 -0
  99. package/dist/core/command-generation/adapters/lingma.js +30 -0
  100. package/dist/core/command-generation/adapters/oh-my-pi.d.ts +21 -0
  101. package/dist/core/command-generation/adapters/oh-my-pi.js +42 -0
  102. package/dist/core/command-generation/adapters/opencode.d.ts +14 -0
  103. package/dist/core/command-generation/adapters/opencode.js +38 -0
  104. package/dist/core/command-generation/adapters/pi.d.ts +18 -0
  105. package/dist/core/command-generation/adapters/pi.js +39 -0
  106. package/dist/core/command-generation/adapters/qoder.d.ts +13 -0
  107. package/dist/core/command-generation/adapters/qoder.js +30 -0
  108. package/dist/core/command-generation/adapters/qwen.d.ts +17 -0
  109. package/dist/core/command-generation/adapters/qwen.js +31 -0
  110. package/dist/core/command-generation/adapters/roocode.d.ts +14 -0
  111. package/dist/core/command-generation/adapters/roocode.js +27 -0
  112. package/dist/core/command-generation/adapters/trae.d.ts +13 -0
  113. package/dist/core/command-generation/adapters/trae.js +28 -0
  114. package/dist/core/command-generation/adapters/zcode.d.ts +16 -0
  115. package/dist/core/command-generation/adapters/zcode.js +33 -0
  116. package/dist/core/command-generation/generator.d.ts +29 -0
  117. package/dist/core/command-generation/generator.js +43 -0
  118. package/dist/core/command-generation/index.d.ts +22 -0
  119. package/dist/core/command-generation/index.js +24 -0
  120. package/dist/core/command-generation/invocation.d.ts +73 -0
  121. package/dist/core/command-generation/invocation.js +77 -0
  122. package/dist/core/command-generation/registry.d.ts +36 -0
  123. package/dist/core/command-generation/registry.js +114 -0
  124. package/dist/core/command-generation/toml.d.ts +19 -0
  125. package/dist/core/command-generation/toml.js +39 -0
  126. package/dist/core/command-generation/types.d.ts +63 -0
  127. package/dist/core/command-generation/types.js +8 -0
  128. package/dist/core/command-generation/yaml.d.ts +36 -0
  129. package/dist/core/command-generation/yaml.js +48 -0
  130. package/dist/core/command-surface.d.ts +15 -0
  131. package/dist/core/command-surface.js +33 -0
  132. package/dist/core/config-prompts.d.ts +9 -0
  133. package/dist/core/config-prompts.js +56 -0
  134. package/dist/core/config-schema.d.ts +98 -0
  135. package/dist/core/config-schema.js +308 -0
  136. package/dist/core/config.d.ts +50 -0
  137. package/dist/core/config.js +122 -0
  138. package/dist/core/file-state.d.ts +36 -0
  139. package/dist/core/file-state.js +144 -0
  140. package/dist/core/file-view.d.ts +13 -0
  141. package/dist/core/file-view.js +14 -0
  142. package/dist/core/global-config.d.ts +101 -0
  143. package/dist/core/global-config.js +182 -0
  144. package/dist/core/id.d.ts +17 -0
  145. package/dist/core/id.js +30 -0
  146. package/dist/core/index.d.ts +6 -0
  147. package/dist/core/index.js +7 -0
  148. package/dist/core/init.d.ts +57 -0
  149. package/dist/core/init.js +836 -0
  150. package/dist/core/list.d.ts +11 -0
  151. package/dist/core/list.js +197 -0
  152. package/dist/core/migration.d.ts +121 -0
  153. package/dist/core/migration.js +540 -0
  154. package/dist/core/onboarding-commands.d.ts +30 -0
  155. package/dist/core/onboarding-commands.js +38 -0
  156. package/dist/core/openspec-root.d.ts +47 -0
  157. package/dist/core/openspec-root.js +196 -0
  158. package/dist/core/planning-home.d.ts +16 -0
  159. package/dist/core/planning-home.js +67 -0
  160. package/dist/core/profile-sync-drift.d.ts +31 -0
  161. package/dist/core/profile-sync-drift.js +201 -0
  162. package/dist/core/profiles.d.ts +19 -0
  163. package/dist/core/profiles.js +33 -0
  164. package/dist/core/project-config.d.ts +132 -0
  165. package/dist/core/project-config.js +519 -0
  166. package/dist/core/references.d.ts +10 -0
  167. package/dist/core/references.js +46 -0
  168. package/dist/core/root-selection.d.ts +59 -0
  169. package/dist/core/root-selection.js +132 -0
  170. package/dist/core/shared/allowed-tools.d.ts +12 -0
  171. package/dist/core/shared/allowed-tools.js +12 -0
  172. package/dist/core/shared/ide-restart.d.ts +26 -0
  173. package/dist/core/shared/ide-restart.js +42 -0
  174. package/dist/core/shared/index.d.ts +11 -0
  175. package/dist/core/shared/index.js +11 -0
  176. package/dist/core/shared/skill-content-equivalence.d.ts +7 -0
  177. package/dist/core/shared/skill-content-equivalence.js +45 -0
  178. package/dist/core/shared/skill-files.d.ts +7 -0
  179. package/dist/core/shared/skill-files.js +43 -0
  180. package/dist/core/shared/skill-generation.d.ts +49 -0
  181. package/dist/core/shared/skill-generation.js +125 -0
  182. package/dist/core/shared/skill-paths.d.ts +13 -0
  183. package/dist/core/shared/skill-paths.js +23 -0
  184. package/dist/core/shared/tool-detection.d.ts +99 -0
  185. package/dist/core/shared/tool-detection.js +318 -0
  186. package/dist/core/shared-skill-target.d.ts +51 -0
  187. package/dist/core/shared-skill-target.js +239 -0
  188. package/dist/core/store/errors.d.ts +20 -0
  189. package/dist/core/store/errors.js +22 -0
  190. package/dist/core/store/index.d.ts +2 -0
  191. package/dist/core/store/index.js +2 -0
  192. package/dist/core/styles/palette.d.ts +7 -0
  193. package/dist/core/styles/palette.js +8 -0
  194. package/dist/core/templates/optional-workflow.d.ts +69 -0
  195. package/dist/core/templates/optional-workflow.js +140 -0
  196. package/dist/core/templates/types.d.ts +19 -0
  197. package/dist/core/templates/types.js +5 -0
  198. package/dist/core/templates/workflows/personal.d.ts +22 -0
  199. package/dist/core/templates/workflows/personal.js +225 -0
  200. package/dist/core/update-plan.d.ts +127 -0
  201. package/dist/core/update-plan.js +390 -0
  202. package/dist/core/update.d.ts +154 -0
  203. package/dist/core/update.js +723 -0
  204. package/dist/core/version-check.d.ts +188 -0
  205. package/dist/core/version-check.js +906 -0
  206. package/dist/core/zod-issues.d.ts +4 -0
  207. package/dist/core/zod-issues.js +10 -0
  208. package/dist/index.d.ts +3 -0
  209. package/dist/index.js +3 -0
  210. package/dist/prompts/searchable-multi-select.d.ts +35 -0
  211. package/dist/prompts/searchable-multi-select.js +188 -0
  212. package/dist/telemetry/config.d.ts +37 -0
  213. package/dist/telemetry/config.js +148 -0
  214. package/dist/telemetry/index.d.ts +39 -0
  215. package/dist/telemetry/index.js +217 -0
  216. package/dist/telemetry/opt-out.d.ts +17 -0
  217. package/dist/telemetry/opt-out.js +27 -0
  218. package/dist/ui/ascii-patterns.d.ts +16 -0
  219. package/dist/ui/ascii-patterns.js +133 -0
  220. package/dist/ui/welcome-screen.d.ts +18 -0
  221. package/dist/ui/welcome-screen.js +199 -0
  222. package/dist/utils/change-metadata.d.ts +116 -0
  223. package/dist/utils/change-metadata.js +324 -0
  224. package/dist/utils/change-utils.d.ts +79 -0
  225. package/dist/utils/change-utils.js +121 -0
  226. package/dist/utils/ci.d.ts +12 -0
  227. package/dist/utils/ci.js +16 -0
  228. package/dist/utils/command-references.d.ts +104 -0
  229. package/dist/utils/command-references.js +190 -0
  230. package/dist/utils/date.d.ts +8 -0
  231. package/dist/utils/date.js +13 -0
  232. package/dist/utils/executable-path.d.ts +5 -0
  233. package/dist/utils/executable-path.js +44 -0
  234. package/dist/utils/file-system.d.ts +58 -0
  235. package/dist/utils/file-system.js +432 -0
  236. package/dist/utils/index.d.ts +6 -0
  237. package/dist/utils/index.js +9 -0
  238. package/dist/utils/interactive.d.ts +85 -0
  239. package/dist/utils/interactive.js +174 -0
  240. package/dist/utils/item-discovery.d.ts +15 -0
  241. package/dist/utils/item-discovery.js +33 -0
  242. package/dist/utils/line-endings.d.ts +34 -0
  243. package/dist/utils/line-endings.js +46 -0
  244. package/dist/utils/match.d.ts +3 -0
  245. package/dist/utils/match.js +22 -0
  246. package/dist/utils/nested-change.d.ts +35 -0
  247. package/dist/utils/nested-change.js +185 -0
  248. package/dist/utils/shell-detection.d.ts +20 -0
  249. package/dist/utils/shell-detection.js +94 -0
  250. package/dist/utils/task-progress.d.ts +67 -0
  251. package/dist/utils/task-progress.js +184 -0
  252. package/package.json +74 -4
  253. package/schemas//351/273/230/350/256/244/schema.yaml +68 -0
  254. package/schemas//351/273/230/350/256/244/templates//344/273/273/345/212/241/346/270/205/345/215/225.md +25 -0
  255. package/schemas//351/273/230/350/256/244/templates//346/265/213/350/257/225/347/224/250/344/276/213.md +34 -0
  256. package/schemas//351/273/230/350/256/244/templates//350/256/276/350/256/241/346/226/207/346/241/243.md +172 -0
  257. package/schemas//351/273/230/350/256/244/templates//351/234/200/346/261/202/346/226/207/346/241/243.md +19 -0
  258. package/skill-files//344/273/273/345/212/241/references//346/212/244/346/240/217.md +43 -0
  259. package/skill-files//344/273/273/345/212/241/references//347/262/222/345/272/246.md +23 -0
  260. package/skill-files//344/273/273/345/212/241/references//347/272/246/346/235/237.md +8 -0
  261. package/skill-files//344/273/273/345/212/241/scripts/check-tasks.mjs +83 -0
  262. package/skill-files//345/206/231Java/references/Java/350/247/204/350/214/203.md +25 -0
  263. package/skill-files//345/206/231Java/references//345/215/261/351/231/251/345/206/231/346/263/225.md +27 -0
  264. package/skill-files//345/206/231Java/references//345/256/214/346/210/220/345/257/271/347/205/247.md +15 -0
  265. package/skill-files//345/206/231Java/references//346/212/244/346/240/217.md +35 -0
  266. package/skill-files//345/206/231Java/references//347/272/246/346/235/237.md +8 -0
  267. package/skill-files//345/206/231Java/references//350/270/251/345/235/221.md +17 -0
  268. package/skill-files//345/256/241Java/references//346/212/244/346/240/217.md +19 -0
  269. package/skill-files//345/256/241Java/references//347/244/272/344/276/213.md +23 -0
  270. package/skill-files//345/256/241Java/references//347/272/246/346/235/237.md +7 -0
  271. package/skill-files//345/256/241Java/references//350/247/204/345/210/231.md +23 -0
  272. package/skill-files//345/256/241Java/references//351/242/206/345/237/237/346/243/200/351/241/271.md +58 -0
  273. package/skill-files//345/256/241/346/226/207/346/241/243/references//346/212/244/346/240/217.md +18 -0
  274. package/skill-files//345/256/241/346/226/207/346/241/243/references//347/272/246/346/235/237.md +8 -0
  275. package/skill-files//345/256/241/346/226/207/346/241/243/references//347/273/264/345/272/246.md +17 -0
  276. package/skill-files//345/256/241/346/226/207/346/241/243/references//350/270/251/345/235/221.md +11 -0
  277. package/skill-files//346/226/260/345/273/272/references//344/272/244/346/216/245.md +11 -0
  278. package/skill-files//346/226/260/345/273/272/references//346/212/244/346/240/217.md +14 -0
  279. package/skill-files//346/226/260/345/273/272/references//347/272/246/346/235/237.md +7 -0
  280. package/skill-files//346/267/261/346/200/235/config/prompts.yaml +114 -0
  281. package/skill-files//346/267/261/346/200/235/references//344/270/211/346/226/271/346/240/270/345/257/271.md +72 -0
  282. package/skill-files//346/267/261/346/200/235/references//344/276/213/345/255/220.md +39 -0
  283. package/skill-files//346/267/261/346/200/235/references//346/200/216/344/271/210/346/203/263.md +61 -0
  284. package/skill-files//346/267/261/346/200/235/references//346/212/244/346/240/217.md +26 -0
  285. package/skill-files//346/267/261/346/200/235/references//347/202/271/351/200/211/346/217/220/351/227/256.md +66 -0
  286. package/skill-files//346/267/261/346/200/235/references//347/231/273/350/256/260.md +45 -0
  287. package/skill-files//346/267/261/346/200/235/references//347/272/246/346/235/237.md +9 -0
  288. package/skill-files//347/224/250/344/276/213/references//346/200/216/344/271/210/345/206/231.md +13 -0
  289. package/skill-files//347/224/250/344/276/213/references//346/212/244/346/240/217.md +42 -0
  290. package/skill-files//347/224/250/344/276/213/references//347/272/246/346/235/237.md +5 -0
  291. package/skill-files//347/224/250/344/276/213/scripts/check-cases.mjs +70 -0
  292. package/skill-files//350/256/276/350/256/241/references//345/207/206/345/244/207.md +25 -0
  293. package/skill-files//350/256/276/350/256/241/references//346/212/244/346/240/217.md +134 -0
  294. package/skill-files//350/256/276/350/256/241/references//346/226/207/351/243/216.md +17 -0
  295. package/skill-files//350/256/276/350/256/241/references//347/225/214/351/235/242.md +18 -0
  296. package/skill-files//350/256/276/350/256/241/references//347/261/273/345/236/213.md +19 -0
  297. package/skill-files//350/256/276/350/256/241/references//347/272/246/346/235/237.md +25 -0
  298. package/skill-files//350/256/276/350/256/241/scripts/_smoke.mjs +72 -0
  299. package/skill-files//350/256/276/350/256/241/scripts/check-design.mjs +331 -0
  300. package/skill-files//351/234/200/346/261/202/references//346/212/244/346/240/217.md +26 -0
  301. package/skill-files//351/234/200/346/261/202/references//346/226/207/351/243/216.md +23 -0
  302. package/skill-files//351/234/200/346/261/202/references//346/267/261/346/200/235/346/221/204/345/205/245.md +21 -0
  303. package/skill-files//351/234/200/346/261/202/references//347/272/246/346/235/237.md +8 -0
  304. package/skill-files//351/234/200/346/261/202/references//351/234/200/346/261/202/345/276/205/345/256/232.md +13 -0
  305. package/skill-files//351/234/200/346/261/202/scripts/check-requirements.mjs +60 -0
  306. package/skill-files//351/252/214/346/224/266/references//346/212/244/346/240/217.md +27 -0
  307. package/skill-files//351/252/214/346/224/266/references//346/240/270/345/257/271.md +17 -0
  308. package/skill-files//351/252/214/346/224/266/references//347/272/246/346/235/237.md +7 -0
@@ -0,0 +1,906 @@
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 { fileURLToPath } from 'url';
7
+ import chalk from 'chalk';
8
+ import { isCiEnvironment } from '../utils/ci.js';
9
+ import { isTelemetryOptedOutByEnv } from '../telemetry/opt-out.js';
10
+ import { FileSystemUtils } from '../utils/file-system.js';
11
+ import { getGlobalConfig, isGlobalConfigUnreadable } from './global-config.js';
12
+ import { findCopiesOnPath, samePath } from '../utils/executable-path.js';
13
+ const require = createRequire(import.meta.url);
14
+ const { name: PACKAGE_NAME, version: OPENSPEC_VERSION } = require('../../package.json');
15
+ const DEFAULT_REGISTRY = 'https://registry.npmjs.org';
16
+ const REQUEST_TIMEOUT_MS = 1500;
17
+ const MAX_RESPONSE_BYTES = 256 * 1024;
18
+ const VERSION_PROBE_TIMEOUT_MS = 5000;
19
+ const MAX_REDIRECTS = 3;
20
+ /**
21
+ * A version we are willing to print. The registry only ever serves SemVer here,
22
+ * so anything else is either a broken mirror or a hostile response — and since
23
+ * this string lands in the terminal next to an install command, an unvalidated
24
+ * one could smuggle ANSI cursor controls and repaint the lines around it.
25
+ */
26
+ const SAFE_VERSION = /^\d{1,10}\.\d{1,10}\.\d{1,10}(?:-[0-9A-Za-z.-]{1,64})?(?:\+[0-9A-Za-z.-]{1,64})?$/;
27
+ let standaloneBinaryPath = null;
28
+ /**
29
+ * Called by the standalone binary's entry point. That copy has no package
30
+ * manager behind it, so a package-manager command is the wrong upgrade advice.
31
+ */
32
+ export function markStandaloneBinary(executablePath) {
33
+ standaloneBinaryPath = executablePath;
34
+ }
35
+ /** The standalone binary's own path, or null when running from a package. */
36
+ export function getStandaloneBinaryPath() {
37
+ return standaloneBinaryPath;
38
+ }
39
+ /**
40
+ * `freespec install` reinstalls the standalone binary where it already is,
41
+ * under its own name, so the new copy cannot end up shadowed by the old one.
42
+ * Spelled with the full path when `freespec` on PATH is some other copy.
43
+ * Null when this is not the binary.
44
+ */
45
+ function standaloneUpgradeCommand() {
46
+ if (!standaloneBinaryPath)
47
+ return null;
48
+ const first = findCopiesOnPath()[0];
49
+ if (first && samePath(first, standaloneBinaryPath))
50
+ return 'freespec install';
51
+ return process.platform === 'win32'
52
+ ? `& '${standaloneBinaryPath.replace(/'/g, "''")}' install`
53
+ : `'${standaloneBinaryPath.replace(/'/g, `'\\''`)}' install`;
54
+ }
55
+ /**
56
+ * The check is opt-out and must never get in the way: no network in CI or
57
+ * tests, an explicit escape hatch for anyone offline or air-gapped, and the
58
+ * same privacy signals telemetry already honors — a user who set DO_NOT_TRACK
59
+ * or telemetry.enabled false did not agree to a different outbound request.
60
+ */
61
+ function isCheckEnabled() {
62
+ if (process.env.OPENSPEC_NO_UPDATE_CHECK !== undefined)
63
+ return false;
64
+ if (isTelemetryOptedOutByEnv())
65
+ return false;
66
+ if (isCiEnvironment())
67
+ return false;
68
+ if (process.env.NODE_ENV === 'test')
69
+ return false;
70
+ // Same config opt-out as telemetry (env remains the hard override above).
71
+ if (getGlobalConfig().telemetry?.enabled === false)
72
+ return false;
73
+ // A config that cannot be parsed may be hiding that opt-out.
74
+ if (isGlobalConfigUnreadable())
75
+ return false;
76
+ return true;
77
+ }
78
+ /**
79
+ * The configured registry, if npm exported one. Not trustworthy: npm exports
80
+ * every config source it read, including a `registry=` line in a
81
+ * repository-local .npmrc that travels with a clone.
82
+ */
83
+ function configuredRegistry() {
84
+ const configured = process.env.npm_config_registry?.trim();
85
+ return configured ? configured : undefined;
86
+ }
87
+ /**
88
+ * The registry to ask. A configured registry is honored only over TLS: a
89
+ * cleartext hop would aim the request at a link-local or internal address and
90
+ * let anyone on the path choose the "newer version" answer.
91
+ *
92
+ * A rejected registry disables the check rather than falling back to public
93
+ * npm. Falling back would send a request an organization on an internal mirror
94
+ * deliberately avoided, and would then report a version resolved against a
95
+ * registry the eventual `npm install -g` does not use.
96
+ */
97
+ export function registryUrl() {
98
+ const configured = configuredRegistry();
99
+ if (configured && !/^https:\/\//i.test(configured))
100
+ return null;
101
+ const base = configured ?? DEFAULT_REGISTRY;
102
+ return `${base.replace(/\/+$/, '')}/${PACKAGE_NAME}/latest`;
103
+ }
104
+ /**
105
+ * True when the check is talking to the public npm registry. Read from the raw
106
+ * env var, not from registryUrl(), so a registry that was rejected above still
107
+ * disqualifies a self-upgrade instead of looking like the default.
108
+ */
109
+ function isDefaultRegistry() {
110
+ const configured = configuredRegistry();
111
+ if (!configured)
112
+ return true;
113
+ try {
114
+ return new URL(configured).origin === new URL(DEFAULT_REGISTRY).origin;
115
+ }
116
+ catch {
117
+ return false;
118
+ }
119
+ }
120
+ /**
121
+ * Compares two prerelease tags per SemVer: dot-separated identifiers compared
122
+ * one by one, numeric identifiers numerically (so beta.10 > beta.2), numeric
123
+ * ranking below alphanumeric, and a longer identifier list winning ties.
124
+ */
125
+ function comparePrerelease(a, b) {
126
+ if (a === b)
127
+ return 0;
128
+ if (a === '')
129
+ return 1;
130
+ if (b === '')
131
+ return -1;
132
+ const left = a.split('.');
133
+ const right = b.split('.');
134
+ for (let i = 0; i < Math.max(left.length, right.length); i++) {
135
+ const l = left[i];
136
+ const r = right[i];
137
+ if (l === undefined)
138
+ return -1;
139
+ if (r === undefined)
140
+ return 1;
141
+ const lNumeric = /^\d+$/.test(l);
142
+ const rNumeric = /^\d+$/.test(r);
143
+ if (lNumeric && rNumeric) {
144
+ const diff = Number.parseInt(l, 10) - Number.parseInt(r, 10);
145
+ if (diff !== 0)
146
+ return diff > 0 ? 1 : -1;
147
+ continue;
148
+ }
149
+ if (lNumeric !== rNumeric)
150
+ return lNumeric ? -1 : 1;
151
+ if (l !== r)
152
+ return l > r ? 1 : -1;
153
+ }
154
+ return 0;
155
+ }
156
+ /**
157
+ * Compares two semver-ish versions. Returns 1 when a > b, -1 when a < b, 0
158
+ * otherwise. Prereleases sort below their release (1.7.0-beta.1 < 1.7.0).
159
+ */
160
+ export function compareVersions(a, b) {
161
+ const parse = (version) => {
162
+ const withoutBuild = version.trim().replace(/^v/, '').split('+', 1)[0] ?? '';
163
+ const separator = withoutBuild.indexOf('-');
164
+ const core = separator === -1 ? withoutBuild : withoutBuild.slice(0, separator);
165
+ const prerelease = separator === -1 ? '' : withoutBuild.slice(separator + 1);
166
+ const parts = core.split('.').map((n) => Number.parseInt(n, 10));
167
+ return {
168
+ numbers: [parts[0] || 0, parts[1] || 0, parts[2] || 0],
169
+ prerelease,
170
+ };
171
+ };
172
+ const left = parse(a);
173
+ const right = parse(b);
174
+ for (let i = 0; i < 3; i++) {
175
+ if (left.numbers[i] > right.numbers[i])
176
+ return 1;
177
+ if (left.numbers[i] < right.numbers[i])
178
+ return -1;
179
+ }
180
+ return comparePrerelease(left.prerelease, right.prerelease);
181
+ }
182
+ /**
183
+ * Reads the `latest` dist-tag. Sends no custom Accept header: the registry
184
+ * answers `/<pkg>/latest` with 406 for npm's abbreviated-metadata type, which
185
+ * it only serves on the full packument.
186
+ *
187
+ * Uses node:http(s) rather than fetch so the timeout can destroy the socket.
188
+ * Aborting a fetch that is still completing its TCP handshake — a firewall
189
+ * dropping packets, a captive portal — leaves the connect handle open and the
190
+ * CLI cannot exit until the OS gives up, long after the hint has printed.
191
+ */
192
+ function fetchLatestVersion() {
193
+ return new Promise((resolve) => {
194
+ let settled = false;
195
+ let timer;
196
+ const finish = (version) => {
197
+ if (settled)
198
+ return;
199
+ settled = true;
200
+ if (timer)
201
+ clearTimeout(timer);
202
+ resolve(version);
203
+ };
204
+ let url;
205
+ try {
206
+ const resolved = registryUrl();
207
+ if (resolved === null) {
208
+ resolve(null);
209
+ return;
210
+ }
211
+ url = new URL(resolved);
212
+ }
213
+ catch {
214
+ resolve(null);
215
+ return;
216
+ }
217
+ // Mirrors and corporate front-ends redirect; without following one the
218
+ // check would be permanently and silently dead for them.
219
+ let redirectsLeft = MAX_REDIRECTS;
220
+ // The budget timer must tear down whichever request is open when it
221
+ // fires. Closing over the first hop's request would leave a redirected
222
+ // socket alive: a target that trickles bytes keeps resetting its idle
223
+ // timeout, and only the body-size cap would end it.
224
+ let activeRequest;
225
+ const send = (target) => {
226
+ const request = (target.protocol === 'http:' ? http : https).get(target, { timeout: REQUEST_TIMEOUT_MS }, (response) => {
227
+ const status = response.statusCode ?? 0;
228
+ const location = response.headers.location;
229
+ if (status >= 300 && status < 400 && location) {
230
+ response.resume();
231
+ request.destroy();
232
+ if (redirectsLeft <= 0) {
233
+ finish(null);
234
+ return;
235
+ }
236
+ redirectsLeft -= 1;
237
+ try {
238
+ const next = new URL(location, target);
239
+ // Cross-host is allowed - that is the whole point of following a
240
+ // redirect for a mirror or corporate front-end - but never off
241
+ // TLS: a MITM on a cleartext hop would control the "newer
242
+ // version" answer, and the reply cannot install anything on its
243
+ // own (canSelfUpgrade refuses a non-default registry, and the
244
+ // version is re-validated against a strict pattern).
245
+ if (next.protocol === 'https:') {
246
+ send(next);
247
+ return;
248
+ }
249
+ }
250
+ catch {
251
+ // Unparseable Location.
252
+ }
253
+ finish(null);
254
+ return;
255
+ }
256
+ if (status !== 200) {
257
+ response.resume();
258
+ request.destroy();
259
+ finish(null);
260
+ return;
261
+ }
262
+ let body = '';
263
+ response.setEncoding('utf-8');
264
+ response.on('data', (chunk) => {
265
+ body += chunk;
266
+ // The dist-tag document is small; refuse to buffer a firehose.
267
+ if (body.length > MAX_RESPONSE_BYTES) {
268
+ request.destroy();
269
+ finish(null);
270
+ }
271
+ });
272
+ response.on('end', () => {
273
+ try {
274
+ const parsed = JSON.parse(body);
275
+ const version = parsed.version;
276
+ finish(typeof version === 'string' && SAFE_VERSION.test(version) ? version : null);
277
+ }
278
+ catch {
279
+ finish(null);
280
+ }
281
+ });
282
+ response.on('error', () => finish(null));
283
+ });
284
+ activeRequest = request;
285
+ request.on('timeout', () => {
286
+ request.destroy();
287
+ finish(null);
288
+ });
289
+ request.on('error', () => finish(null));
290
+ // One budget for the whole exchange, redirects included.
291
+ if (!timer) {
292
+ timer = setTimeout(() => {
293
+ activeRequest?.destroy();
294
+ finish(null);
295
+ }, REQUEST_TIMEOUT_MS);
296
+ }
297
+ };
298
+ send(url);
299
+ });
300
+ }
301
+ /**
302
+ * Checks the registry while preserving enough detail for callers to explain
303
+ * why no newer version was reported. Never throws.
304
+ */
305
+ export async function checkForCliUpdate() {
306
+ if (!isCheckEnabled() || registryUrl() === null) {
307
+ return { status: 'disabled', latest: null };
308
+ }
309
+ try {
310
+ const latest = await fetchLatestVersion();
311
+ if (!latest)
312
+ return { status: 'offline', latest: null };
313
+ return {
314
+ status: compareVersions(latest, OPENSPEC_VERSION) > 0 ? 'available' : 'current',
315
+ latest,
316
+ };
317
+ }
318
+ catch {
319
+ return { status: 'offline', latest: null };
320
+ }
321
+ }
322
+ /**
323
+ * Returns the published version when the installed CLI is behind it, otherwise
324
+ * null. Kept as the compatibility surface used by `freespec update`.
325
+ */
326
+ export async function getAvailableCliUpdate() {
327
+ const result = await checkForCliUpdate();
328
+ return result.status === 'available' ? result.latest : null;
329
+ }
330
+ /**
331
+ * Directory the running CLI was loaded from, or null when it cannot be
332
+ * resolved. Shown in the upgrade hint so anyone who upgraded but still runs an
333
+ * old binary — a stale pnpm/volta/npx shim, or two installs on PATH — can see
334
+ * which copy is actually answering.
335
+ */
336
+ export function getInstallDir() {
337
+ // The binary is not a package install, so no package-manager logic applies.
338
+ if (standaloneBinaryPath)
339
+ return null;
340
+ // Resolved at runtime from this module's own (symlink-resolved) location.
341
+ // require.resolve would be resolved by the binary's bundler at build time,
342
+ // baking the build machine's path into it.
343
+ const packageJson = fileURLToPath(new URL('../../package.json', import.meta.url));
344
+ return fs.existsSync(packageJson) ? path.dirname(packageJson) : null;
345
+ }
346
+ /**
347
+ * True when the running CLI resolves from a `node_modules` belonging to the
348
+ * project being updated or any ancestor of it — the hoisted-root layout npm and
349
+ * pnpm workspaces produce. Anchored on the target path rather than the working
350
+ * directory, since `freespec update <path>` and running from a sub-package are
351
+ * both normal. Never throws: process.cwd() fails when the directory has been
352
+ * deleted, and a wrong upgrade hint must not take down a successful update.
353
+ */
354
+ export function isProjectLocalInstall(installDir, projectPath = '.') {
355
+ if (!installDir)
356
+ return false;
357
+ // Windows paths differ in case and drive-letter casing between sources.
358
+ const normalize = (value) => process.platform === 'win32' ? value.toLowerCase() : value;
359
+ try {
360
+ let dir = FileSystemUtils.canonicalizeExistingPath(projectPath);
361
+ const target = normalize(FileSystemUtils.canonicalizeExistingPath(installDir));
362
+ for (;;) {
363
+ if (target.startsWith(normalize(path.join(dir, 'node_modules') + path.sep))) {
364
+ return true;
365
+ }
366
+ const parent = path.dirname(dir);
367
+ if (parent === dir)
368
+ return false;
369
+ dir = parent;
370
+ }
371
+ }
372
+ catch {
373
+ return false;
374
+ }
375
+ }
376
+ /**
377
+ * True for the throwaway caches npx/pnpm dlx/bunx unpack into. Telling those
378
+ * users to install globally would create the second copy on PATH they were
379
+ * deliberately avoiding.
380
+ */
381
+ export function isEphemeralRunnerInstall(installDir) {
382
+ if (!installDir)
383
+ return false;
384
+ const segments = installDir.split(/[\\/]/).map((segment) => segment.toLowerCase());
385
+ return segments.some((segment, i) => segment === '_npx' ||
386
+ segment === '_bunx' ||
387
+ // Only a package manager's own cache, never a user directory that
388
+ // happens to be called "dlx". Windows uses pnpm-cache for the same job.
389
+ (segment === 'dlx' &&
390
+ ['pnpm', 'bun', '.pnpm', 'pnpm-cache', 'bun-cache'].includes(segments[i - 1] ?? '')));
391
+ }
392
+ /**
393
+ * Directories npm installs global packages into. Derived from the running node
394
+ * rather than by shelling out to `npm prefix -g`, which would cost more than
395
+ * the version check itself. Only a hint: `process.execPath` is realpath'd, so
396
+ * on Homebrew it lands in the Cellar rather than the brew prefix — which is
397
+ * why the install's own layout is the primary signal below.
398
+ */
399
+ export function npmGlobalRoots() {
400
+ const roots = [];
401
+ const nodeDir = path.dirname(process.execPath);
402
+ if (process.platform === 'win32') {
403
+ roots.push(path.join(nodeDir, 'node_modules'));
404
+ if (process.env.APPDATA) {
405
+ roots.push(path.join(process.env.APPDATA, 'npm', 'node_modules'));
406
+ }
407
+ }
408
+ else {
409
+ roots.push(path.resolve(nodeDir, '..', 'lib', 'node_modules'));
410
+ }
411
+ const prefix = process.env.npm_config_prefix;
412
+ if (prefix) {
413
+ roots.push(process.platform === 'win32'
414
+ ? path.join(prefix, 'node_modules')
415
+ : path.join(prefix, 'lib', 'node_modules'));
416
+ }
417
+ return roots;
418
+ }
419
+ /**
420
+ * The prefix of an npm global install, read from the install's own shape:
421
+ * `<prefix>/lib/node_modules/<pkg>` on POSIX, `<prefix>/node_modules/<pkg>` on
422
+ * Windows. Self-describing, so it holds for Homebrew, nvm, Debian and anywhere
423
+ * else npm's prefix is not derivable from the node binary. Null when the
424
+ * layout does not match.
425
+ */
426
+ export function npmPrefixFromInstallDir(installDir) {
427
+ if (!installDir)
428
+ return null;
429
+ let dir = installDir;
430
+ for (;;) {
431
+ const parent = path.dirname(dir);
432
+ if (parent === dir)
433
+ return null;
434
+ if (path.basename(dir).toLowerCase() === 'node_modules')
435
+ break;
436
+ dir = parent;
437
+ }
438
+ const container = path.dirname(dir);
439
+ if (process.platform === 'win32')
440
+ return container;
441
+ // POSIX npm always nests the root under lib/.
442
+ return path.basename(container).toLowerCase() === 'lib' ? path.dirname(container) : null;
443
+ }
444
+ /**
445
+ * True only when npm itself owns this copy. Everything else — a pnpm, bun,
446
+ * yarn or volta global — would be made worse by `npm install -g`, which adds a
447
+ * second copy that may not even be the one on PATH.
448
+ */
449
+ export function isNpmGlobalInstall(installDir, roots = npmGlobalRoots()) {
450
+ if (!installDir)
451
+ return false;
452
+ // Another manager's layout can still look like npm's (volta nests a whole
453
+ // node install), so who owns it is decided before where it sits.
454
+ if (detectPackageManager(installDir) !== 'npm')
455
+ return false;
456
+ const normalize = (value) => process.platform === 'win32' ? value.toLowerCase() : value;
457
+ const target = normalize(installDir);
458
+ if (roots.some((root) => target.startsWith(normalize(root + path.sep))))
459
+ return true;
460
+ // The derived roots miss any prefix that is not beside the node binary, so
461
+ // fall back to the install's own shape plus the bin directory npm would
462
+ // have written the shim into.
463
+ const prefix = npmPrefixFromInstallDir(installDir);
464
+ if (!prefix)
465
+ return false;
466
+ try {
467
+ // Corroborate with something npm itself wrote: the bin dir on POSIX, the
468
+ // .cmd shim on Windows. The prefix alone proves nothing — it is just the
469
+ // parent of the node_modules dir the CLI resolved from, so a hand-copied
470
+ // portable tree would pass and be offered an npm upgrade it never had.
471
+ return fs.existsSync(process.platform === 'win32' ? path.join(prefix, 'freespec.cmd') : path.join(prefix, 'bin'));
472
+ }
473
+ catch {
474
+ return false;
475
+ }
476
+ }
477
+ /**
478
+ * True when the CLI is running from a clone rather than an install. Upgrade
479
+ * advice is meaningless there: the version is whatever the branch says.
480
+ */
481
+ export function isSourceCheckout(installDir) {
482
+ if (!installDir)
483
+ return false;
484
+ try {
485
+ return fs.existsSync(path.join(installDir, '.git'));
486
+ }
487
+ catch {
488
+ return false;
489
+ }
490
+ }
491
+ function detectKnownPackageManager(installDir) {
492
+ const segments = (installDir ?? '').split(/[\\/]/).map((segment) => segment.toLowerCase());
493
+ const has = (...names) => names.some((name) => segments.includes(name));
494
+ if (has('.volta') || (has('volta') && has('tools') && has('image')))
495
+ return 'volta';
496
+ if (has('.bun', '_bunx', 'bun-cache'))
497
+ return 'bun';
498
+ if (has('_npx'))
499
+ return 'npm';
500
+ if (has('.pnpm', '.pnpm-global', 'pnpm-cache'))
501
+ return 'pnpm';
502
+ if (has('pnpm') && has('global', 'dlx', 'store'))
503
+ return 'pnpm';
504
+ if (has('.yarn') || (has('yarn') && has('global')))
505
+ return 'yarn';
506
+ return null;
507
+ }
508
+ /**
509
+ * The package manager that owns this copy, so the printed command is one the
510
+ * user's setup will actually honor.
511
+ */
512
+ export function detectPackageManager(installDir) {
513
+ return detectKnownPackageManager(installDir) ?? 'npm';
514
+ }
515
+ const GLOBAL_UPGRADE_COMMANDS = {
516
+ npm: `npm install -g ${PACKAGE_NAME}@latest`,
517
+ pnpm: `pnpm add -g ${PACKAGE_NAME}@latest`,
518
+ bun: `bun add -g ${PACKAGE_NAME}@latest`,
519
+ yarn: `yarn global add ${PACKAGE_NAME}@latest`,
520
+ volta: `volta install ${PACKAGE_NAME}@latest`,
521
+ };
522
+ function detectGlobalPackageManager(installDir) {
523
+ if (isNpmGlobalInstall(installDir))
524
+ return 'npm';
525
+ if (!installDir)
526
+ return null;
527
+ const segments = installDir.split(/[\\/]/).map((segment) => segment.toLowerCase());
528
+ const has = (...names) => names.some((name) => segments.includes(name));
529
+ const hasSequence = (...names) => segments.some((_, index) => names.every((name, offset) => segments[index + offset] === name));
530
+ if (hasSequence('.volta', 'tools', 'image') || hasSequence('volta', 'tools', 'image')) {
531
+ return 'volta';
532
+ }
533
+ if (has('.pnpm-global') || hasSequence('pnpm', 'global'))
534
+ return 'pnpm';
535
+ if (hasSequence('yarn', 'global') || hasSequence('yarn', 'data', 'global'))
536
+ return 'yarn';
537
+ if (hasSequence('.bun', 'install', 'global'))
538
+ return 'bun';
539
+ return null;
540
+ }
541
+ /** Describes the running copy without guessing when its owner is ambiguous. */
542
+ export function getCliInstallInfo(installDir = getInstallDir(), projectPath = '.') {
543
+ if (standaloneBinaryPath) {
544
+ return { location: path.dirname(standaloneBinaryPath), packageManager: null, scope: 'standalone' };
545
+ }
546
+ if (!installDir) {
547
+ return { location: null, packageManager: null, scope: null };
548
+ }
549
+ if (isSourceCheckout(installDir)) {
550
+ return { location: installDir, packageManager: null, scope: 'source' };
551
+ }
552
+ if (isEphemeralRunnerInstall(installDir)) {
553
+ return {
554
+ location: installDir,
555
+ packageManager: detectKnownPackageManager(installDir),
556
+ scope: 'temporary',
557
+ };
558
+ }
559
+ if (isProjectLocalInstall(installDir, projectPath)) {
560
+ return {
561
+ location: installDir,
562
+ packageManager: detectKnownPackageManager(installDir),
563
+ scope: 'project',
564
+ };
565
+ }
566
+ const packageManager = detectGlobalPackageManager(installDir);
567
+ return {
568
+ location: installDir,
569
+ packageManager,
570
+ scope: packageManager ? 'global' : null,
571
+ };
572
+ }
573
+ /** Returns a safe update command only for an install with known global ownership. */
574
+ export function getCliUpdateCommand(install) {
575
+ if (install.scope === 'standalone')
576
+ return standaloneUpgradeCommand();
577
+ if (install.scope !== 'global' || install.packageManager === null)
578
+ return null;
579
+ return GLOBAL_UPGRADE_COMMANDS[install.packageManager];
580
+ }
581
+ /** Builds the concise human-readable form of the version report. */
582
+ export function buildVersionReportLines(version, install, update, command = null) {
583
+ const details = [install.packageManager, install.scope].filter(Boolean).join(', ');
584
+ const lines = [`FreeSpec ${version}${details ? ` (${details})` : ''}`];
585
+ if (!update)
586
+ return lines;
587
+ if (update.status === 'available') {
588
+ lines.push(`Update available: ${update.latest}`);
589
+ if (command)
590
+ lines.push(` ${command}`);
591
+ }
592
+ else if (update.status === 'current') {
593
+ lines.push('FreeSpec is up to date.');
594
+ }
595
+ else if (update.status === 'disabled') {
596
+ lines.push('Update check disabled.');
597
+ }
598
+ else {
599
+ lines.push('Could not check for updates.');
600
+ }
601
+ return lines;
602
+ }
603
+ /**
604
+ * Builds the hint, with the upgrade command chosen for how this copy of the CLI
605
+ * was installed. Pure so every branch is assertable.
606
+ */
607
+ export function buildCliUpdateLines(latestVersion, installDir, projectPath, options = {}) {
608
+ const lines = [`A newer FreeSpec CLI is available (v${OPENSPEC_VERSION} → v${latestVersion}).`];
609
+ // Omitted when we are about to offer to run it — printing a command and then
610
+ // asking to run that same command reads like the user has to do both.
611
+ if (options.withCommand !== false) {
612
+ lines.push(...buildUpgradeCommandLines(installDir, projectPath));
613
+ }
614
+ if (installDir) {
615
+ lines.push(` Running from: ${installDir}`);
616
+ }
617
+ return lines;
618
+ }
619
+ /**
620
+ * The upgrade command for however this copy was installed, plus the reminder
621
+ * that instruction files come from the CLI and so need a second pass.
622
+ */
623
+ export function buildUpgradeCommandLines(installDir, projectPath) {
624
+ const lines = [];
625
+ const standalone = standaloneUpgradeCommand();
626
+ if (standalone) {
627
+ lines.push(` ${standalone}`);
628
+ lines.push(' Then run "freespec update" again to pick up new workflows.');
629
+ return lines;
630
+ }
631
+ if (isEphemeralRunnerInstall(installDir)) {
632
+ // That command *is* the update, so there is nothing to run afterwards.
633
+ lines.push(` npx ${PACKAGE_NAME}@latest update`);
634
+ return lines;
635
+ }
636
+ if (isProjectLocalInstall(installDir, projectPath)) {
637
+ // Its package manager owns the lockfile; naming npm could be wrong.
638
+ lines.push(` Update the ${PACKAGE_NAME} dependency in this project.`);
639
+ }
640
+ else {
641
+ lines.push(` ${GLOBAL_UPGRADE_COMMANDS[detectPackageManager(installDir)]}`);
642
+ }
643
+ lines.push(' Then run "freespec update" again to pick up new workflows.');
644
+ return lines;
645
+ }
646
+ // cross-spawn resolves npm's shim on Windows, where spawning "npm" directly
647
+ // fails. Loaded lazily so ordinary runs skip its module graph.
648
+ let cachedSpawn;
649
+ function loadSpawn() {
650
+ if (cachedSpawn === undefined) {
651
+ cachedSpawn = require('cross-spawn');
652
+ }
653
+ return cachedSpawn;
654
+ }
655
+ /**
656
+ * Whether we can run the upgrade for the user instead of only printing it.
657
+ *
658
+ * Only an npm-owned global install qualifies, because `npm install -g` is the
659
+ * only command we run: a pnpm/bun/yarn/volta global would get a second copy
660
+ * that may not be the one on PATH, a project dependency belongs to that
661
+ * project's package manager, an npx/dlx cache has nothing to upgrade, and a
662
+ * source checkout is not an install at all.
663
+ *
664
+ * A non-default registry is also disqualifying. `npm install -g` resolves
665
+ * against whatever `npm_config_registry` says, which npm may have read from a
666
+ * cloned repository's .npmrc — so such a registry may still inform the check,
667
+ * but must never be the thing that offers to install a global package.
668
+ */
669
+ export function canSelfUpgrade(installDir, projectPath) {
670
+ if (!installDir)
671
+ return false;
672
+ if (!isDefaultRegistry())
673
+ return false;
674
+ if (isEphemeralRunnerInstall(installDir))
675
+ return false;
676
+ // Both anchors matter: `freespec update ../other` from a project that owns
677
+ // the CLI as a dependency is still a project-local install.
678
+ if (isProjectLocalInstall(installDir, projectPath))
679
+ return false;
680
+ if (isProjectLocalInstall(installDir))
681
+ return false;
682
+ if (isSourceCheckout(installDir))
683
+ return false;
684
+ return isNpmGlobalInstall(installDir);
685
+ }
686
+ /**
687
+ * Whether to offer the upgrade rather than just print the command. Kept here,
688
+ * as a pure function of the environment, because the interesting mistakes live
689
+ * in this decision: offering where `npm install -g` cannot help, or asking a
690
+ * question no one can answer.
691
+ */
692
+ export function shouldOfferUpgrade(params) {
693
+ // A prompt written to a redirected stdout is a question the user never sees
694
+ // and the command waits on forever.
695
+ if (!params.interactive || !params.stdoutIsTty)
696
+ return false;
697
+ return canSelfUpgrade(params.installDir, params.projectPath);
698
+ }
699
+ /**
700
+ * Runs `npm install -g <pkg>@latest`, inheriting stdio so npm's own output —
701
+ * including any auth or permission prompt — reaches the user directly.
702
+ * Resolves true only on a clean exit.
703
+ */
704
+ async function runGlobalUpgrade() {
705
+ const spawn = loadSpawn();
706
+ return new Promise((resolve) => {
707
+ const child = spawn('npm', ['install', '-g', `${PACKAGE_NAME}@latest`], {
708
+ stdio: 'inherit',
709
+ });
710
+ child.on('error', () => resolve(false));
711
+ child.on('close', (code) => resolve(code === 0));
712
+ });
713
+ }
714
+ /**
715
+ * The `freespec` npm installs alongside its global package, so the upgrade can
716
+ * be handed to the copy npm just wrote rather than to whatever PATH resolves.
717
+ * Null when it cannot be found, in which case PATH is the only option left.
718
+ */
719
+ export function upgradedBinPath(roots = npmGlobalRoots(), installDir = getInstallDir()) {
720
+ // The copy npm just replaced tells us exactly which prefix it wrote to;
721
+ // a root derived from the node binary can point at an unrelated install.
722
+ const ownPrefix = npmPrefixFromInstallDir(installDir);
723
+ const ordered = ownPrefix
724
+ ? [
725
+ process.platform === 'win32'
726
+ ? path.join(ownPrefix, 'node_modules')
727
+ : path.join(ownPrefix, 'lib', 'node_modules'),
728
+ ...roots,
729
+ ]
730
+ : roots;
731
+ for (const root of ordered) {
732
+ // npm writes the shim beside the global root on Windows
733
+ // (%APPDATA%\\npm\\freespec.cmd) and in <prefix>/bin on POSIX.
734
+ const candidates = process.platform === 'win32'
735
+ ? [path.join(path.dirname(root), 'freespec.cmd')]
736
+ : [path.resolve(root, '..', '..', 'bin', 'freespec')];
737
+ for (const candidate of candidates) {
738
+ try {
739
+ if (fs.existsSync(candidate))
740
+ return candidate;
741
+ }
742
+ catch {
743
+ // Unreadable candidate; try the next one.
744
+ }
745
+ }
746
+ }
747
+ return null;
748
+ }
749
+ /**
750
+ * Asks a CLI binary its version. Used to confirm an upgrade actually landed:
751
+ * `npm install -g` exits 0 even when it installed nothing, so its exit code
752
+ * alone cannot justify telling the user they are on a new version.
753
+ */
754
+ export function readCliVersion(binPath) {
755
+ const spawn = loadSpawn();
756
+ return new Promise((resolve) => {
757
+ let output = '';
758
+ let child;
759
+ try {
760
+ child = spawn(binPath, ['--version'], { stdio: ['ignore', 'pipe', 'ignore'] });
761
+ }
762
+ catch {
763
+ resolve(null);
764
+ return;
765
+ }
766
+ // Never let a probe hold the CLI open: a wrapper that traps SIGTERM would
767
+ // otherwise keep the process alive for as long as it runs.
768
+ child.unref();
769
+ const timer = setTimeout(() => {
770
+ child.kill('SIGKILL');
771
+ resolve(null);
772
+ }, VERSION_PROBE_TIMEOUT_MS);
773
+ child.stdout?.on('data', (chunk) => {
774
+ output += chunk.toString();
775
+ });
776
+ child.on('error', () => {
777
+ clearTimeout(timer);
778
+ resolve(null);
779
+ });
780
+ child.on('close', () => {
781
+ clearTimeout(timer);
782
+ // A line that is only a version, not the first version-shaped token
783
+ // anywhere: a wrapper banner ("Node.js v25.8.1 | FreeSpec") would
784
+ // otherwise be read as the answer.
785
+ const version = output
786
+ .split(/\r?\n/)
787
+ .map((line) => line.trim())
788
+ .filter((line) => SAFE_VERSION.test(line.replace(/^v/, '')))
789
+ .pop();
790
+ resolve(version ? version.replace(/^v/, '') : null);
791
+ });
792
+ });
793
+ }
794
+ function isPromptCancellation(error) {
795
+ const name = error?.name;
796
+ return name === 'ExitPromptError' || name === 'AbortPromptError';
797
+ }
798
+ /**
799
+ * Offers to run the upgrade and reports what actually happened. The version is
800
+ * read back from the installed binary rather than assumed, so "upgraded" is a
801
+ * fact and a PATH that still answers with the old copy is caught here instead
802
+ * of silently doing nothing.
803
+ */
804
+ export async function offerCliUpgrade(latestVersion) {
805
+ const { confirm } = await import('@inquirer/prompts');
806
+ let accepted = false;
807
+ try {
808
+ accepted = await confirm({
809
+ message: `Upgrade to v${latestVersion} now?`,
810
+ default: true,
811
+ });
812
+ }
813
+ catch (error) {
814
+ // Ctrl-C means stop, not "no thanks, carry on with everything else".
815
+ return isPromptCancellation(error) ? 'cancelled' : 'declined';
816
+ }
817
+ if (!accepted)
818
+ return 'declined';
819
+ console.log();
820
+ const installed = await runGlobalUpgrade();
821
+ console.log();
822
+ if (!installed) {
823
+ console.log(chalk.yellow('The upgrade did not complete. A global install may need'));
824
+ console.log(chalk.yellow('elevated permissions, or a different package manager.'));
825
+ return 'failed';
826
+ }
827
+ const binPath = upgradedBinPath();
828
+ const version = await readCliVersion(binPath ?? 'freespec');
829
+ if (!version) {
830
+ console.log(chalk.yellow('Upgrade finished, but no "freespec" could be run to confirm it.'));
831
+ return 'not-on-path';
832
+ }
833
+ if (compareVersions(version, OPENSPEC_VERSION) <= 0) {
834
+ console.log(chalk.yellow(`Upgrade finished, but "freespec" still reports v${version}.`));
835
+ console.log(chalk.dim(binPath
836
+ ? // We asked the installed copy directly, so PATH is not the story.
837
+ ` npm reported success, but ${binPath} did not change.`
838
+ : ' Another install earlier on your PATH is answering first.'));
839
+ return 'not-on-path';
840
+ }
841
+ console.log(chalk.green(`✓ Upgraded to v${version}.`));
842
+ return 'upgraded';
843
+ }
844
+ /**
845
+ * Runs `freespec update` again with the CLI that was just installed — this
846
+ * process is still the old code, so it cannot write the new workflows itself.
847
+ * Resolves the exit code to pass along; when no `freespec` is on PATH the
848
+ * upgrade still landed but nothing was regenerated, so it says so and
849
+ * resolves 0 rather than reporting a failure the upgrade did not have.
850
+ */
851
+ export async function rerunUpdateWithUpgradedCli(projectPath, options = {}) {
852
+ const spawn = loadSpawn();
853
+ const binPath = options.binPath ?? upgradedBinPath() ?? 'freespec';
854
+ // The re-run stands in for the command the user typed, so it has to carry
855
+ // the flags they typed with it.
856
+ const args = ['update'];
857
+ if (options.force)
858
+ args.push('--force');
859
+ // `--` so a path that looks like a flag stays a path.
860
+ args.push('--', projectPath);
861
+ return new Promise((resolve) => {
862
+ const child = spawn(binPath, args, {
863
+ stdio: 'inherit',
864
+ env: {
865
+ ...process.env,
866
+ // The child must not offer the upgrade again: if PATH still resolves
867
+ // to the old binary, prompting would loop forever.
868
+ OPENSPEC_NO_UPDATE_CHECK: '1',
869
+ // This is a continuation of the command the user already ran, and the
870
+ // parent recorded it; counting it twice would overstate usage.
871
+ OPENSPEC_TELEMETRY: '0',
872
+ },
873
+ });
874
+ child.on('error', () => {
875
+ // Nothing to hand off to: the upgrade landed but the instruction files
876
+ // are still the old ones, so this run did not do what was asked.
877
+ console.log(chalk.yellow('Instruction files were not regenerated.'));
878
+ console.log(chalk.dim(' Run "freespec update" to pick up the new workflows.'));
879
+ resolve(1);
880
+ });
881
+ // A child killed by a signal reports no code; that is not success.
882
+ child.on('close', (code) => resolve(code ?? 1));
883
+ });
884
+ }
885
+ /**
886
+ * Prints the upgrade hint. Instruction files are generated by the installed
887
+ * CLI, so "up to date" only ever means "matches this CLI" — without this note
888
+ * a stale install looks like a successful update.
889
+ */
890
+ export function displayCliUpdateNote(latestVersion, projectPath = '.', options = {}) {
891
+ const [headline, ...rest] = buildCliUpdateLines(latestVersion, getInstallDir(), projectPath, options);
892
+ console.log();
893
+ console.log(chalk.yellow(headline));
894
+ for (const line of rest) {
895
+ console.log(chalk.dim(line));
896
+ }
897
+ }
898
+ /**
899
+ * Prints just the manual command, for when the offer was declined or failed.
900
+ */
901
+ export function displayUpgradeCommand(projectPath = '.') {
902
+ for (const line of buildUpgradeCommandLines(getInstallDir(), projectPath)) {
903
+ console.log(chalk.dim(line));
904
+ }
905
+ }
906
+ //# sourceMappingURL=version-check.js.map