@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,723 @@
1
+ /**
2
+ * Update Command
3
+ *
4
+ * Refreshes FreeSpec skills and commands for configured tools.
5
+ * Supports profile-aware updates, delivery changes, migration, and smart update detection.
6
+ */
7
+ import path from 'path';
8
+ import chalk from 'chalk';
9
+ import ora from 'ora';
10
+ import { createRequire } from 'module';
11
+ import { FileSystemUtils } from '../utils/file-system.js';
12
+ import { getTransformerForTool } from '../utils/command-references.js';
13
+ import { AI_TOOLS, OPENSPEC_DIR_NAME } from './config.js';
14
+ import { generateCommands, CommandAdapterRegistry, } from './command-generation/index.js';
15
+ import { getToolSkillStatus, getToolVersionStatus, getSkillTemplates, getCommandContents, generateSkillContent, copySkillExtras, hasGlobalSkillTarget, resolveToolSkillsDir, toolSupportsSkills, formatIdeRestart, } from './shared/index.js';
16
+ import { isInteractive } from '../utils/interactive.js';
17
+ import { getGlobalConfig, getGlobalConfigPath, serializeGlobalConfig } from './global-config.js';
18
+ import { getProfileWorkflows, ALL_WORKFLOWS, CORE_WORKFLOWS } from './profiles.js';
19
+ import { getAvailableTools } from './available-tools.js';
20
+ import { WORKFLOW_TO_SKILL_DIR, getConfiguredToolsForProfileSync, getToolsNeedingProfileSync, } from './profile-sync-drift.js';
21
+ import { scanInstalledWorkflows as scanInstalledWorkflowsShared, migrateIfNeeded as migrateIfNeededShared, computeProfileMigration, applyProfileMigration, findLegacyToolMigrations, planLegacyToolMigrations, migrateLegacyToolDirs, describeLegacyMigration, legacyMigrationNotice, keptInPlaceNotice, hasMovableContent, } from './migration.js';
22
+ import { resolveCommandSurfaceCapability, resolveCommandInvocation, shouldGenerateCommandsForTool, shouldGenerateSkillsForTool, shouldReconcileCommandFilesForTool, shouldRemoveSkillsForTool, } from './command-surface.js';
23
+ import { resolveSharedSkillWriters, sharedSkillTargetMarkerPath, } from './shared-skill-target.js';
24
+ import { DISK_VIEW } from './file-view.js';
25
+ import { realFileOps, UpdatePlanRecorder, } from './update-plan.js';
26
+ const require = createRequire(import.meta.url);
27
+ const { version: OPENSPEC_VERSION } = require('../../package.json');
28
+ function throwIfUpdateFailed(failedTools) {
29
+ if (failedTools.length > 0) {
30
+ throw new Error(`FreeSpec update failed for: ${failedTools.map((tool) => tool.name).join(', ')}`);
31
+ }
32
+ }
33
+ /**
34
+ * Checkout artifacts that are not real content drift: a UTF-8 BOM and the CRLF
35
+ * line endings a Windows clone with `core.autocrlf` reintroduces on every
36
+ * checkout of committed generated files.
37
+ */
38
+ function normalizeGeneratedFile(content) {
39
+ return content.replace(/^\uFEFF/, '').replace(/\r\n/g, '\n');
40
+ }
41
+ /**
42
+ * Scans installed workflow artifacts (skills and managed commands) across all configured tools.
43
+ * Returns the union of detected workflow IDs that match ALL_WORKFLOWS.
44
+ *
45
+ * Wrapper around the shared migration module's scanInstalledWorkflows that accepts tool IDs.
46
+ */
47
+ export function scanInstalledWorkflows(projectPath, toolIds, view) {
48
+ const tools = toolIds
49
+ .map((id) => AI_TOOLS.find((t) => t.value === id))
50
+ .filter((t) => t != null);
51
+ return scanInstalledWorkflowsShared(projectPath, tools, view);
52
+ }
53
+ export class UpdateCommand {
54
+ force;
55
+ dryRun;
56
+ recorder = null;
57
+ ops = realFileOps;
58
+ constructor(options = {}) {
59
+ this.force = options.force ?? false;
60
+ this.dryRun = options.dryRun ?? false;
61
+ }
62
+ /**
63
+ * Reports what `execute` would add, change or delete, writing nothing.
64
+ *
65
+ * Same code path as the real update: the plan is recorded by the write seam
66
+ * the generation loop already goes through, so it cannot drift from it.
67
+ */
68
+ async plan(projectPath) {
69
+ if (!this.dryRun) {
70
+ throw new Error('plan() requires the update command to be constructed with dryRun: true');
71
+ }
72
+ await this.execute(projectPath);
73
+ return this.recorder?.getPlan() ?? { changes: [], notes: [], failures: [] };
74
+ }
75
+ /** Prints progress on a real run, stays quiet while planning. */
76
+ log(...args) {
77
+ if (this.dryRun)
78
+ return;
79
+ console.log(...args);
80
+ }
81
+ /** A spinner on a real run, a no-op while planning. */
82
+ startSpinner(text) {
83
+ if (this.dryRun) {
84
+ return { succeed: () => { }, fail: () => { }, info: () => { } };
85
+ }
86
+ return ora(text).start();
87
+ }
88
+ /**
89
+ * Refreshes FreeSpec skills and commands for all configured tools,
90
+ * regenerating artifacts according to the effective profile and delivery mode.
91
+ *
92
+ * @param projectPath - Path to the project root containing the freespec directory
93
+ */
94
+ async execute(projectPath) {
95
+ const resolvedProjectPath = path.resolve(projectPath);
96
+ const freespecPath = path.join(resolvedProjectPath, OPENSPEC_DIR_NAME);
97
+ if (this.dryRun) {
98
+ this.recorder = new UpdatePlanRecorder(resolvedProjectPath);
99
+ this.ops = this.recorder;
100
+ }
101
+ // 1. Check freespec directory exists
102
+ if (!await FileSystemUtils.directoryExists(freespecPath)) {
103
+ throw new Error(`No FreeSpec directory found. Run 'freespec init' first.`);
104
+ }
105
+ // 2. Migrate FreeSpec-managed skills left in renamed tool directories
106
+ // (e.g. .kimi -> .kimi-code) so they stay detected and get refreshed,
107
+ // then perform the one-time profile migration if needed before any
108
+ // legacy upgrade generation.
109
+ if (!this.dryRun) {
110
+ for (const migration of migrateLegacyToolDirs(resolvedProjectPath)) {
111
+ if (hasMovableContent(migration)) {
112
+ this.log(chalk.dim(`Migrated ${describeLegacyMigration(migration)}: ${migration.from} → ${migration.to}`));
113
+ }
114
+ this.reportKeptInPlace(migration);
115
+ }
116
+ }
117
+ // A dry run plans the same moves. Every later step reads `this.view` -
118
+ // the disk in a real run, the tree as planned in a dry run - so it sees
119
+ // the moved files exactly as the real update would.
120
+ if (this.dryRun) {
121
+ await this.planLegacyToolMoves(resolvedProjectPath, 'before-generation');
122
+ }
123
+ const declinedMigrations = this.dryRun
124
+ ? []
125
+ : await this.offerConsentedLegacyMigrations(resolvedProjectPath);
126
+ // Use detected tool directories to preserve existing freespec skills/commands.
127
+ const detectedTools = getAvailableTools(resolvedProjectPath, this.view);
128
+ if (!this.dryRun) {
129
+ migrateIfNeededShared(resolvedProjectPath, detectedTools);
130
+ }
131
+ // 3. Read global config for profile/delivery. A dry run plans the one-time
132
+ // profile migration's save instead of making it, and resolves against
133
+ // the config that save would leave — otherwise the preview would plan
134
+ // for a different profile than the real update acts on.
135
+ let globalConfig = getGlobalConfig();
136
+ if (this.recorder) {
137
+ const migration = computeProfileMigration(resolvedProjectPath, detectedTools, this.view);
138
+ if (migration) {
139
+ globalConfig = applyProfileMigration(migration);
140
+ await this.recorder.writeFile(getGlobalConfigPath(), serializeGlobalConfig(globalConfig), 'freespec');
141
+ }
142
+ }
143
+ const profile = globalConfig.profile ?? 'core';
144
+ const delivery = globalConfig.delivery ?? 'both';
145
+ const profileWorkflows = getProfileWorkflows(profile, globalConfig.workflows);
146
+ const desiredWorkflows = profileWorkflows.filter((workflow) => ALL_WORKFLOWS.includes(workflow));
147
+ // 5. Find configured tools
148
+ const configuredTools = getConfiguredToolsForProfileSync(resolvedProjectPath, this.view);
149
+ if (configuredTools.length === 0) {
150
+ if (declinedMigrations.length > 0) {
151
+ // Not an unconfigured project — a configured one the user chose to
152
+ // leave in its former directory. Saying "run init" would be wrong.
153
+ for (const migration of declinedMigrations) {
154
+ this.log(chalk.yellow(`Nothing to update: this project's FreeSpec files are still in ${migration.from}/, ` +
155
+ `which FreeSpec no longer writes.`));
156
+ this.log(chalk.dim(`Re-run "freespec update" and accept the move to ${migration.to}/ to resume updates.`));
157
+ }
158
+ return;
159
+ }
160
+ this.log(chalk.yellow('No configured tools found.'));
161
+ this.log(chalk.dim('Run "freespec init" to set up tools.'));
162
+ return;
163
+ }
164
+ // 6. Check version status for all configured tools, against the same workflow set
165
+ // the generation loop below writes — otherwise a legacy-upgraded tool would be
166
+ // fingerprinted against commands it was never given.
167
+ const toolStatuses = configuredTools.map((toolId) => getToolVersionStatus(resolvedProjectPath, toolId, OPENSPEC_VERSION, {
168
+ workflows: desiredWorkflows,
169
+ delivery,
170
+ view: this.view,
171
+ }));
172
+ const statusByTool = new Map(toolStatuses.map((status) => [status.toolId, status]));
173
+ // 7. Smart update detection
174
+ const toolsNeedingVersionUpdate = toolStatuses
175
+ .filter((s) => {
176
+ if (!s.needsUpdate || delivery !== 'commands') {
177
+ return s.needsUpdate;
178
+ }
179
+ const tool = AI_TOOLS.find((candidate) => candidate.value === s.toolId);
180
+ return !tool || !hasGlobalSkillTarget(tool);
181
+ })
182
+ .map((s) => s.toolId);
183
+ const toolsNeedingConfigSync = getToolsNeedingProfileSync(resolvedProjectPath, desiredWorkflows, delivery, configuredTools, this.view);
184
+ const toolsWithDriftedSkills = this.findToolsWithDriftedSkills(resolvedProjectPath, configuredTools, delivery, (toolId) => desiredWorkflows);
185
+ const toolsToUpdateSet = new Set([
186
+ ...toolsNeedingVersionUpdate,
187
+ ...toolsNeedingConfigSync,
188
+ ...toolsWithDriftedSkills,
189
+ ]);
190
+ const toolsUpToDate = toolStatuses.filter((s) => !toolsToUpdateSet.has(s.toolId));
191
+ if (!this.force && toolsToUpdateSet.size === 0) {
192
+ // All tools are up to date
193
+ this.displayUpToDateMessage(toolStatuses);
194
+ // Still check for new tool directories and extra workflows
195
+ this.detectNewTools(resolvedProjectPath, configuredTools);
196
+ this.displayProfileNotes(resolvedProjectPath, configuredTools, desiredWorkflows, profile, delivery);
197
+ this.displaySetupNotes(configuredTools);
198
+ return;
199
+ }
200
+ // 8. Display update plan
201
+ if (this.force) {
202
+ this.log(`Force updating ${configuredTools.length} tool(s): ${configuredTools.join(', ')}`);
203
+ }
204
+ else {
205
+ this.displayUpdatePlan([...toolsToUpdateSet], statusByTool, toolsUpToDate, new Set(toolsWithDriftedSkills));
206
+ }
207
+ this.log();
208
+ // 9. Determine what to generate based on delivery
209
+ const deliveryIncludesCommands = delivery !== 'skills';
210
+ // 10. Update tools (all if force, otherwise only those needing update)
211
+ const toolsToUpdate = this.force ? configuredTools : [...toolsToUpdateSet];
212
+ const sharedSkillWriters = resolveSharedSkillWriters(resolvedProjectPath, configuredTools
213
+ .map((toolId) => AI_TOOLS.find((tool) => tool.value === toolId))
214
+ .filter((tool) => tool !== undefined), this.view);
215
+ const updatedTools = [];
216
+ const updatedToolIds = [];
217
+ const failedTools = [];
218
+ const skillsInvocableCommandSkips = [];
219
+ const zeroArtifactTools = [];
220
+ let removedCommandCount = 0;
221
+ let removedSkillCount = 0;
222
+ let removedDeselectedCommandCount = 0;
223
+ let removedDeselectedSkillCount = 0;
224
+ for (const toolId of toolsToUpdate) {
225
+ const tool = AI_TOOLS.find((t) => t.value === toolId);
226
+ if (!tool || !toolSupportsSkills(tool))
227
+ continue;
228
+ const spinner = this.startSpinner(`Updating ${tool.name}...`);
229
+ try {
230
+ const skillsDir = resolveToolSkillsDir(resolvedProjectPath, tool);
231
+ const skillsRoot = hasGlobalSkillTarget(tool) ? skillsDir : resolvedProjectPath;
232
+ const shouldGenerateSkills = shouldGenerateSkillsForTool(tool.value, delivery);
233
+ const shouldGenerateCommands = shouldGenerateCommandsForTool(tool.value, delivery);
234
+ const writesSkills = !tool.skillsDir || sharedSkillWriters.has(tool.value);
235
+ const toolWorkflows = desiredWorkflows;
236
+ const skillTemplates = getSkillTemplates(toolWorkflows);
237
+ const commandContents = getCommandContents(toolWorkflows);
238
+ // Generate skill files if delivery includes skills
239
+ if (shouldGenerateSkills && writesSkills) {
240
+ for (const { template, dirName } of skillTemplates) {
241
+ const skillDir = path.join(skillsDir, dirName);
242
+ const skillFile = path.join(skillDir, 'SKILL.md');
243
+ const transformer = getTransformerForTool(tool.value, delivery, resolveCommandSurfaceCapability(tool.value), resolveCommandInvocation(tool.value));
244
+ const skillContent = generateSkillContent(template, OPENSPEC_VERSION, transformer);
245
+ FileSystemUtils.assertPathWithin(skillsRoot, skillFile);
246
+ await this.ops.writeFile(skillFile, skillContent, tool.value);
247
+ await copySkillExtras(skillDir, template.name);
248
+ }
249
+ await this.writeSharedSkillTarget(resolvedProjectPath, tool.value);
250
+ removedDeselectedSkillCount += await this.removeUnselectedSkillDirs(skillsRoot, skillsDir, toolWorkflows, tool.value);
251
+ }
252
+ // Delete skill directories if delivery is commands-only
253
+ if (shouldRemoveSkillsForTool(tool.value, delivery) &&
254
+ writesSkills &&
255
+ !hasGlobalSkillTarget(tool)) {
256
+ removedSkillCount += await this.removeSkillDirs(skillsRoot, skillsDir, tool.value);
257
+ // Persist the selected owner even when commands-only delivery leaves
258
+ // this target with no generated skills.
259
+ await this.writeSharedSkillTarget(resolvedProjectPath, tool.value);
260
+ // A tool with no command adapter now has zero FreeSpec artifacts;
261
+ // say so like init does, rather than deleting its skills silently
262
+ // and letting tool detection re-suggest an init that would also
263
+ // generate nothing under this delivery setting.
264
+ if (!shouldGenerateCommandsForTool(tool.value, delivery)) {
265
+ zeroArtifactTools.push(tool.name);
266
+ }
267
+ }
268
+ // Generate commands if delivery includes commands
269
+ if (shouldGenerateCommands) {
270
+ const adapter = CommandAdapterRegistry.get(tool.value);
271
+ if (adapter) {
272
+ const generatedCommands = generateCommands(commandContents, adapter);
273
+ for (const cmd of generatedCommands) {
274
+ const commandFile = FileSystemUtils.resolveProjectArtifactPath(resolvedProjectPath, cmd.path);
275
+ await this.ops.writeFile(commandFile, cmd.fileContent, tool.value);
276
+ }
277
+ removedDeselectedCommandCount += await this.removeUnselectedCommandFiles(resolvedProjectPath, toolId, toolWorkflows);
278
+ }
279
+ }
280
+ else if (deliveryIncludesCommands && resolveCommandSurfaceCapability(tool.value) === 'skills-invocable') {
281
+ skillsInvocableCommandSkips.push(tool.value);
282
+ }
283
+ // Delete command files if delivery is skills-only
284
+ if (shouldReconcileCommandFilesForTool(tool.value, delivery)) {
285
+ removedCommandCount += await this.removeCommandFiles(resolvedProjectPath, toolId);
286
+ }
287
+ spinner.succeed(`Updated ${tool.name}`);
288
+ updatedTools.push(tool.name);
289
+ updatedToolIds.push(tool.value);
290
+ if (!this.dryRun) {
291
+ for (const migration of migrateLegacyToolDirs(resolvedProjectPath, [tool.value], 'after-generation')) {
292
+ if (hasMovableContent(migration)) {
293
+ this.log(chalk.dim(`Migrated ${describeLegacyMigration(migration)}: ${migration.from} → ${migration.to}`));
294
+ }
295
+ this.reportKeptInPlace(migration);
296
+ }
297
+ }
298
+ else {
299
+ await this.planLegacyToolMoves(resolvedProjectPath, 'after-generation', [tool.value]);
300
+ }
301
+ }
302
+ catch (error) {
303
+ spinner.fail(`Failed to update ${tool.name}`);
304
+ const message = error instanceof Error ? error.message : String(error);
305
+ failedTools.push({ name: tool.name, error: message });
306
+ // A dry run reports the failure in the plan rather than throwing it:
307
+ // one broken tool must not cost the caller the other tools' changes.
308
+ this.recorder?.addFailure(tool.value, message);
309
+ }
310
+ }
311
+ // 11. Summary
312
+ this.log();
313
+ if (updatedTools.length > 0) {
314
+ this.log(chalk.green(`✓ Updated: ${updatedTools.join(', ')} (v${OPENSPEC_VERSION})`));
315
+ }
316
+ if (failedTools.length > 0) {
317
+ this.log(chalk.red(`✗ Failed: ${failedTools.map(f => `${f.name} (${f.error})`).join(', ')}`));
318
+ }
319
+ if (skillsInvocableCommandSkips.length > 0) {
320
+ this.log(chalk.dim(`Commands skipped for: ${skillsInvocableCommandSkips.join(', ')} (uses skills)`));
321
+ }
322
+ if (removedCommandCount > 0) {
323
+ this.log(chalk.dim(`Removed: ${removedCommandCount} command files (delivery: skills)`));
324
+ }
325
+ if (removedSkillCount > 0) {
326
+ this.log(chalk.dim(`Removed: ${removedSkillCount} skill directories (delivery: commands)`));
327
+ }
328
+ if (zeroArtifactTools.length > 0) {
329
+ const names = zeroArtifactTools.join(', ');
330
+ this.log(chalk.yellow(`No skills or commands remain for ${names}: delivery is set to 'commands' but ` +
331
+ `${zeroArtifactTools.length === 1 ? 'it supports' : 'they support'} only skills. ` +
332
+ `Run 'freespec config set delivery both' to generate skills.`));
333
+ }
334
+ if (removedDeselectedCommandCount > 0) {
335
+ this.log(chalk.dim(`Removed: ${removedDeselectedCommandCount} command files (deselected workflows)`));
336
+ }
337
+ if (removedDeselectedSkillCount > 0) {
338
+ this.log(chalk.dim(`Removed: ${removedDeselectedSkillCount} skill directories (deselected workflows)`));
339
+ }
340
+ // 13. Detect new tool directories not currently configured
341
+ this.detectNewTools(resolvedProjectPath, configuredTools);
342
+ // 14. Display the profile notes
343
+ this.displayProfileNotes(resolvedProjectPath, configuredTools, desiredWorkflows, profile, delivery);
344
+ this.displaySetupNotes(configuredTools);
345
+ // 15. List affected tools
346
+ if (updatedTools.length > 0) {
347
+ const toolDisplayNames = updatedTools;
348
+ this.log(chalk.dim(`Tools: ${toolDisplayNames.join(', ')}`));
349
+ }
350
+ this.log();
351
+ const affectedToolIds = updatedToolIds;
352
+ const restartHint = formatIdeRestart(affectedToolIds, delivery);
353
+ if (restartHint) {
354
+ this.log(chalk.dim(restartHint));
355
+ }
356
+ this.stopIfUpdateFailed(failedTools);
357
+ }
358
+ /**
359
+ * Records the shared-skills-root owner through the write seam, so a dry run
360
+ * lists the marker file a real update would write.
361
+ */
362
+ async writeSharedSkillTarget(projectPath, toolId) {
363
+ const marker = sharedSkillTargetMarkerPath(projectPath, toolId);
364
+ if (!marker)
365
+ return;
366
+ FileSystemUtils.assertProjectArtifactPath(projectPath, marker);
367
+ await this.ops.writeFile(marker, `${toolId}\n`, toolId);
368
+ }
369
+ /**
370
+ * Where a real update throws for failed tools, it also stops. A dry run
371
+ * stops at the same point - so it plans nothing the real update would not
372
+ * reach - but returns instead of throwing: the failures are already in the
373
+ * plan, and the caller still gets every change planned before them.
374
+ *
375
+ * @returns whether the update stops here.
376
+ */
377
+ stopIfUpdateFailed(failedTools) {
378
+ if (!this.dryRun)
379
+ throwIfUpdateFailed(failedTools);
380
+ return failedTools.length > 0;
381
+ }
382
+ /** The tree as the real update would see it at this point. */
383
+ get view() {
384
+ return this.recorder?.view ?? DISK_VIEW;
385
+ }
386
+ /**
387
+ * Plans the moves out of former tool roots that a non-interactive update
388
+ * makes, against the tree as planned so far.
389
+ */
390
+ async planLegacyToolMoves(projectPath, timing, toolIds) {
391
+ const recorder = this.recorder;
392
+ if (!recorder)
393
+ return;
394
+ for (const op of planLegacyToolMigrations(projectPath, timing, toolIds, recorder.view)) {
395
+ try {
396
+ if (op.disposition === 'move') {
397
+ await recorder.moveFile(op.source, op.destination, op.toolId);
398
+ }
399
+ else {
400
+ await recorder.removeFile(op.source, op.toolId);
401
+ }
402
+ }
403
+ catch {
404
+ // The real migration leaves a file it cannot move where it is, and
405
+ // says nothing; so does the plan.
406
+ continue;
407
+ }
408
+ if (op.needsConsent) {
409
+ recorder.addNote('legacy-tool-directories', 'Moves out of a renamed tool directory are listed as a non-interactive update makes them. At a terminal, update asks first, and declining leaves those files where they are.');
410
+ }
411
+ }
412
+ }
413
+ /**
414
+ * Tools whose SKILL.md bodies no longer match what this CLI generates.
415
+ *
416
+ * Skill freshness was decided solely by the `generatedBy:` line, so a body
417
+ * edited after generation — a "helpful" PR touching `.claude/skills/**`, a
418
+ * dotfile sync, another agent — left `update` reporting the install healthy.
419
+ * Command files never had that gap: `areCommandFilesUpToDate` content-
420
+ * compares them, and the same comparison belongs on the higher-authority
421
+ * surface. A missing skill file is left to the profile-sync check, which
422
+ * already knows what a partial install means.
423
+ */
424
+ findToolsWithDriftedSkills(projectPath, toolIds, delivery, workflowsForTool) {
425
+ return toolIds.filter((toolId) => {
426
+ const tool = AI_TOOLS.find((t) => t.value === toolId);
427
+ if (!tool || !toolSupportsSkills(tool))
428
+ return false;
429
+ if (!shouldGenerateSkillsForTool(tool.value, delivery))
430
+ return false;
431
+ // A shared skills root is generated with its owner's transformer, so
432
+ // only the owner may compare it. getToolSkillStatus settles ownership.
433
+ if (!getToolSkillStatus(projectPath, tool.value, this.view).configured)
434
+ return false;
435
+ const skillsDir = resolveToolSkillsDir(projectPath, tool);
436
+ const transformer = getTransformerForTool(tool.value, delivery, resolveCommandSurfaceCapability(tool.value), resolveCommandInvocation(tool.value));
437
+ return getSkillTemplates(workflowsForTool(toolId)).some(({ template, dirName }) => {
438
+ const skillFile = path.join(skillsDir, dirName, 'SKILL.md');
439
+ if (!this.view.exists(skillFile))
440
+ return false;
441
+ try {
442
+ const existing = this.view.read(skillFile);
443
+ const generated = generateSkillContent(template, OPENSPEC_VERSION, transformer);
444
+ return normalizeGeneratedFile(existing) !== normalizeGeneratedFile(generated);
445
+ }
446
+ catch {
447
+ return true;
448
+ }
449
+ });
450
+ });
451
+ }
452
+ /**
453
+ * Display message when all tools are up to date.
454
+ */
455
+ displayUpToDateMessage(toolStatuses) {
456
+ const toolNames = toolStatuses.map((s) => s.toolId);
457
+ this.log(chalk.green(`✓ All ${toolStatuses.length} tool(s) up to date (v${OPENSPEC_VERSION})`));
458
+ this.log(chalk.dim(` Tools: ${toolNames.join(', ')}`));
459
+ this.log();
460
+ this.log(chalk.dim('Use --force to refresh files anyway.'));
461
+ }
462
+ /**
463
+ * Display the update plan showing which tools need updating.
464
+ */
465
+ displayUpdatePlan(toolsToUpdate, statusByTool, upToDate, driftedSkills = new Set()) {
466
+ const updates = toolsToUpdate.map((toolId) => {
467
+ const status = statusByTool.get(toolId);
468
+ if (status?.needsUpdate) {
469
+ const fromVersion = status.generatedByVersion ?? 'unknown';
470
+ return `${status.toolId} (${fromVersion} → ${OPENSPEC_VERSION})`;
471
+ }
472
+ // Say why: a user who edited a SKILL.md on purpose is owed the reason
473
+ // their edit is about to be overwritten.
474
+ if (driftedSkills.has(toolId)) {
475
+ return `${toolId} (skill files differ from the generated content)`;
476
+ }
477
+ return `${toolId} (config sync)`;
478
+ });
479
+ this.log(`Updating ${toolsToUpdate.length} tool(s): ${updates.join(', ')}`);
480
+ if (upToDate.length > 0) {
481
+ const upToDateNames = upToDate.map((s) => s.toolId);
482
+ this.log(chalk.dim(`Already up to date: ${upToDateNames.join(', ')}`));
483
+ }
484
+ }
485
+ /**
486
+ * Shows manual setup notes for configured tools that need extra
487
+ * configuration before they pick up generated files.
488
+ */
489
+ displaySetupNotes(toolIds) {
490
+ for (const toolId of toolIds) {
491
+ const tool = AI_TOOLS.find((t) => t.value === toolId);
492
+ if (tool?.setupNote) {
493
+ this.log(chalk.yellow(`Setup required for ${tool.name}: ${tool.setupNote}`));
494
+ }
495
+ }
496
+ }
497
+ /**
498
+ * Detects new tool directories that aren't currently configured and displays a hint.
499
+ */
500
+ detectNewTools(projectPath, configuredTools) {
501
+ const availableTools = getAvailableTools(projectPath);
502
+ const configuredSet = new Set(configuredTools);
503
+ const newTools = availableTools.filter((t) => !configuredSet.has(t.value));
504
+ if (newTools.length > 0) {
505
+ const newToolNames = newTools.map((tool) => tool.name);
506
+ const isSingleTool = newToolNames.length === 1;
507
+ const toolNoun = isSingleTool ? 'tool' : 'tools';
508
+ const pronoun = isSingleTool ? 'it' : 'them';
509
+ this.log();
510
+ this.log(chalk.yellow(`Detected new ${toolNoun}: ${newToolNames.join(', ')}. Run 'freespec init' to add ${pronoun}.`));
511
+ }
512
+ }
513
+ /**
514
+ * Prints the profile notes, in order, with one pointer at
515
+ * `freespec config profile` rather than three.
516
+ *
517
+ * Every note is evaluated: reading them as one short-circuited `||` chain
518
+ * would swallow whichever ran second.
519
+ */
520
+ displayProfileNotes(projectPath, configuredTools, desiredWorkflows, profile, _delivery) {
521
+ this.displayExtraWorkflowsNote(projectPath, configuredTools, desiredWorkflows ?? []);
522
+ this.displayMissingCoreWorkflowsNote(profile, desiredWorkflows);
523
+ }
524
+ /**
525
+ * Displays a note about extra workflows installed that aren't in the current profile.
526
+ */
527
+ displayExtraWorkflowsNote(projectPath, configuredTools, profileWorkflows) {
528
+ const installedWorkflows = scanInstalledWorkflows(projectPath, configuredTools);
529
+ const profileSet = new Set(profileWorkflows);
530
+ const extraWorkflows = installedWorkflows.filter((w) => !profileSet.has(w));
531
+ if (extraWorkflows.length > 0) {
532
+ this.log(chalk.dim(`Note: ${extraWorkflows.length} extra workflows not in profile (use \`freespec config profile\` to manage)`));
533
+ return true;
534
+ }
535
+ return false;
536
+ }
537
+ /**
538
+ * Point out core workflows a custom profile is missing, so releases that
539
+ * grow CORE_WORKFLOWS stay discoverable. Keep custom profiles user-owned;
540
+ * do not mutate them.
541
+ */
542
+ displayMissingCoreWorkflowsNote(profile, workflows) {
543
+ if (profile !== 'custom' || !workflows) {
544
+ return false;
545
+ }
546
+ const workflowSet = new Set(workflows);
547
+ const missing = CORE_WORKFLOWS.filter((workflow) => !workflowSet.has(workflow));
548
+ if (missing.length === 0) {
549
+ return false;
550
+ }
551
+ const label = missing.length === 1 ? 'workflow' : 'workflows';
552
+ const pronoun = missing.length === 1 ? 'it' : 'them';
553
+ this.log(chalk.dim(`Note: Your custom profile is missing ${missing.length} core ${label}: ${missing.join(', ')}`));
554
+ this.log(chalk.dim(`Run \`freespec config profile\` to add ${pronoun}, or \`freespec config profile core\` to use the core set.`));
555
+ return true;
556
+ }
557
+ /**
558
+ * Removes skill directories for workflows when delivery changed to commands-only.
559
+ * Returns the number of directories removed.
560
+ */
561
+ async removeSkillDirs(skillsRoot, skillsDir, toolId) {
562
+ let removed = 0;
563
+ for (const dirName of Object.values(WORKFLOW_TO_SKILL_DIR)) {
564
+ const skillDir = path.join(skillsDir, dirName);
565
+ if (!this.view.exists(skillDir))
566
+ continue;
567
+ FileSystemUtils.assertPathWithin(skillsRoot, skillDir);
568
+ try {
569
+ await this.ops.removeDirectory(skillDir, toolId);
570
+ removed++;
571
+ }
572
+ catch {
573
+ // Ignore errors
574
+ }
575
+ }
576
+ return removed;
577
+ }
578
+ /**
579
+ * Removes skill directories for workflows that are no longer selected in the active profile.
580
+ * Returns the number of directories removed.
581
+ */
582
+ async removeUnselectedSkillDirs(skillsRoot, skillsDir, desiredWorkflows, toolId) {
583
+ const desiredSet = new Set(desiredWorkflows);
584
+ let removed = 0;
585
+ for (const workflow of ALL_WORKFLOWS) {
586
+ if (desiredSet.has(workflow))
587
+ continue;
588
+ const dirName = WORKFLOW_TO_SKILL_DIR[workflow];
589
+ if (!dirName)
590
+ continue;
591
+ const skillDir = path.join(skillsDir, dirName);
592
+ if (!this.view.exists(skillDir))
593
+ continue;
594
+ FileSystemUtils.assertPathWithin(skillsRoot, skillDir);
595
+ try {
596
+ await this.ops.removeDirectory(skillDir, toolId);
597
+ removed++;
598
+ }
599
+ catch {
600
+ // Ignore errors
601
+ }
602
+ }
603
+ return removed;
604
+ }
605
+ /**
606
+ * Removes command files for workflows when delivery changed to skills-only.
607
+ * Returns the number of files removed.
608
+ */
609
+ async removeCommandFiles(projectPath, toolId) {
610
+ let removed = 0;
611
+ const adapter = CommandAdapterRegistry.get(toolId);
612
+ if (!adapter)
613
+ return 0;
614
+ for (const workflow of ALL_WORKFLOWS) {
615
+ const cmdPath = adapter.getFilePath(workflow);
616
+ const fullPath = FileSystemUtils.resolveProjectArtifactPath(projectPath, cmdPath);
617
+ try {
618
+ if (this.view.exists(fullPath)) {
619
+ await this.ops.removeFile(fullPath, toolId);
620
+ removed++;
621
+ }
622
+ }
623
+ catch {
624
+ // Ignore errors
625
+ }
626
+ }
627
+ return removed;
628
+ }
629
+ /**
630
+ * Removes command files for workflows that are no longer selected in the active profile.
631
+ * Returns the number of files removed.
632
+ */
633
+ async removeUnselectedCommandFiles(projectPath, toolId, desiredWorkflows) {
634
+ let removed = 0;
635
+ const adapter = CommandAdapterRegistry.get(toolId);
636
+ if (!adapter)
637
+ return 0;
638
+ const desiredSet = new Set(desiredWorkflows);
639
+ for (const workflow of ALL_WORKFLOWS) {
640
+ if (desiredSet.has(workflow))
641
+ continue;
642
+ const cmdPath = adapter.getFilePath(workflow);
643
+ const fullPath = FileSystemUtils.resolveProjectArtifactPath(projectPath, cmdPath);
644
+ try {
645
+ if (this.view.exists(fullPath)) {
646
+ await this.ops.removeFile(fullPath, toolId);
647
+ removed++;
648
+ }
649
+ }
650
+ catch {
651
+ // Ignore errors
652
+ }
653
+ }
654
+ return removed;
655
+ }
656
+ /**
657
+ * Offers to move FreeSpec content out of a renamed tool's former directory
658
+ * when the old location might still be the live one — today, Windsurf's
659
+ * `.windsurf/` after the Devin Desktop rebrand.
660
+ *
661
+ * Interactive runs are asked, because nothing on disk distinguishes a user
662
+ * who took the rebrand from one still on a pre-rebrand Windsurf build that
663
+ * reads only `.windsurf/`. `--force` and non-interactive runs migrate, which
664
+ * is what an unattended upgrade wants.
665
+ */
666
+ /** Surfaces files the move left behind rather than overwriting. */
667
+ reportKeptInPlace(migration) {
668
+ const notice = keptInPlaceNotice(migration);
669
+ if (notice)
670
+ this.log(chalk.dim(notice));
671
+ }
672
+ async offerConsentedLegacyMigrations(projectPath) {
673
+ const pending = findLegacyToolMigrations(projectPath).filter((m) => m.needsConsent);
674
+ const declined = [];
675
+ if (pending.length === 0)
676
+ return declined;
677
+ for (const migration of pending) {
678
+ // Nothing movable: every legacy file differs from its counterpart, so
679
+ // there is no move to offer. Still say so — silence would leave two
680
+ // divergent copies the user never hears about.
681
+ if (!hasMovableContent(migration)) {
682
+ this.reportKeptInPlace(migration);
683
+ this.log();
684
+ continue;
685
+ }
686
+ this.log(chalk.yellow(legacyMigrationNotice(migration)));
687
+ if (!this.force && isInteractive()) {
688
+ const { confirm } = await import('@inquirer/prompts');
689
+ let shouldMigrate;
690
+ try {
691
+ shouldMigrate = await confirm({
692
+ message: `Move ${describeLegacyMigration(migration)} from ${migration.from}/ to ${migration.to}/?`,
693
+ default: true,
694
+ });
695
+ }
696
+ catch {
697
+ // Closed stdin is not consent, and it must not abort the update.
698
+ shouldMigrate = false;
699
+ }
700
+ if (!shouldMigrate) {
701
+ // Say what declining costs. FreeSpec writes the current root now, so
702
+ // the files keep working where they are, but FreeSpec stops managing
703
+ // them — it no longer looks in the former directory.
704
+ this.log(chalk.dim(`Left in place. FreeSpec writes ${migration.to}/ now and will not manage ` +
705
+ `${migration.from}/, so those files stay as they are until you move them. ` +
706
+ `You will be asked again next run.`));
707
+ this.log();
708
+ declined.push(migration);
709
+ continue;
710
+ }
711
+ }
712
+ for (const applied of migrateLegacyToolDirs(projectPath, [migration.toolId])) {
713
+ if (hasMovableContent(applied)) {
714
+ this.log(chalk.dim(`Migrated ${describeLegacyMigration(applied)}: ${applied.from} → ${applied.to}`));
715
+ }
716
+ this.reportKeptInPlace(applied);
717
+ }
718
+ this.log();
719
+ }
720
+ return declined;
721
+ }
722
+ }
723
+ //# sourceMappingURL=update.js.map