@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,432 @@
1
+ import * as nodeFs from 'fs';
2
+ import path from 'path';
3
+ import { detectLineEnding, matchLineEnding } from './line-endings.js';
4
+ const fs = nodeFs.promises;
5
+ const { constants: fsConstants } = nodeFs;
6
+ function hasOwnerGroupOrOtherWriteBit(stats) {
7
+ return (stats.mode & 0o222) !== 0;
8
+ }
9
+ function hasOwnerGroupOrOtherExecuteBit(stats) {
10
+ return (stats.mode & 0o111) !== 0;
11
+ }
12
+ async function hasWritableModeAndAccess(targetPath) {
13
+ try {
14
+ const stats = await fs.stat(targetPath);
15
+ // POSIX root can often write despite mode bits, but FreeSpec should respect
16
+ // explicit read-only file/directory modes when deciding whether an install
17
+ // path is user-writable. This also keeps permission checks deterministic in
18
+ // root-run CI containers. On Windows, chmod mode bits are not authoritative,
19
+ // so rely on fs.access below.
20
+ if (process.platform !== 'win32' && !hasOwnerGroupOrOtherWriteBit(stats)) {
21
+ return false;
22
+ }
23
+ if (process.platform !== 'win32' && stats.isDirectory() && !hasOwnerGroupOrOtherExecuteBit(stats)) {
24
+ return false;
25
+ }
26
+ const accessMode = stats.isDirectory()
27
+ ? fsConstants.W_OK | fsConstants.X_OK
28
+ : fsConstants.W_OK;
29
+ await fs.access(targetPath, accessMode);
30
+ return true;
31
+ }
32
+ catch {
33
+ return false;
34
+ }
35
+ }
36
+ function isMarkerOnOwnLine(content, markerIndex, markerLength) {
37
+ let leftIndex = markerIndex - 1;
38
+ while (leftIndex >= 0 && content[leftIndex] !== '\n') {
39
+ const char = content[leftIndex];
40
+ if (char !== ' ' && char !== '\t' && char !== '\r') {
41
+ return false;
42
+ }
43
+ leftIndex--;
44
+ }
45
+ let rightIndex = markerIndex + markerLength;
46
+ while (rightIndex < content.length && content[rightIndex] !== '\n') {
47
+ const char = content[rightIndex];
48
+ if (char !== ' ' && char !== '\t' && char !== '\r') {
49
+ return false;
50
+ }
51
+ rightIndex++;
52
+ }
53
+ return true;
54
+ }
55
+ function findMarkerIndex(content, marker, fromIndex = 0) {
56
+ let currentIndex = content.indexOf(marker, fromIndex);
57
+ while (currentIndex !== -1) {
58
+ if (isMarkerOnOwnLine(content, currentIndex, marker.length)) {
59
+ return currentIndex;
60
+ }
61
+ currentIndex = content.indexOf(marker, currentIndex + marker.length);
62
+ }
63
+ return -1;
64
+ }
65
+ export class FileSystemUtils {
66
+ /**
67
+ * Converts a path to use forward slashes (POSIX style).
68
+ * Essential for cross-platform compatibility with glob libraries like tinyglobby.
69
+ */
70
+ static toPosixPath(p) {
71
+ return p.replace(/\\/g, '/');
72
+ }
73
+ /**
74
+ * Returns a canonical absolute path when the target exists.
75
+ * Falls back to path.resolve() so callers can still produce a stable absolute path.
76
+ */
77
+ static canonicalizeExistingPath(targetPath) {
78
+ try {
79
+ // Prefer the native resolver so Windows short-path aliases are expanded.
80
+ return nodeFs.realpathSync.native(targetPath);
81
+ }
82
+ catch {
83
+ try {
84
+ return nodeFs.realpathSync(targetPath);
85
+ }
86
+ catch {
87
+ return path.resolve(targetPath);
88
+ }
89
+ }
90
+ }
91
+ /**
92
+ * Refuses a target that leaves an allowed directory, including through an
93
+ * existing symlink in either the target or one of its parent directories.
94
+ * Missing suffixes are resolved from their nearest existing ancestor.
95
+ */
96
+ static assertPathWithin(allowedDirectory, targetPath) {
97
+ const resolvedDirectory = path.resolve(allowedDirectory);
98
+ const resolvedTarget = path.resolve(targetPath);
99
+ if (!this.isPathWithin(resolvedDirectory, resolvedTarget)) {
100
+ throw new Error(`Path is outside the allowed directory: ${targetPath}`);
101
+ }
102
+ const canonicalDirectory = this.canonicalizePotentialPath(resolvedDirectory);
103
+ const canonicalTarget = this.canonicalizePotentialPath(resolvedTarget);
104
+ if (!this.isPathWithin(canonicalDirectory, canonicalTarget)) {
105
+ throw new Error(`Path is outside the allowed directory: ${targetPath}`);
106
+ }
107
+ }
108
+ /**
109
+ * Like assertPathWithin, but a targetPath that is itself a symlink may
110
+ * resolve outside allowedDirectory, so a project can keep a managed
111
+ * directory (for example freespec) in another repository. Its parent
112
+ * must still be confined and the link must resolve to an existing path.
113
+ */
114
+ static assertPathWithinAllowingLinkedLeaf(allowedDirectory, targetPath) {
115
+ const resolvedTarget = path.resolve(targetPath);
116
+ let isLink = false;
117
+ try {
118
+ isLink = nodeFs.lstatSync(resolvedTarget).isSymbolicLink();
119
+ }
120
+ catch (error) {
121
+ if (error.code !== 'ENOENT') {
122
+ throw error;
123
+ }
124
+ }
125
+ if (!isLink) {
126
+ this.assertPathWithin(allowedDirectory, resolvedTarget);
127
+ return;
128
+ }
129
+ this.assertPathWithin(allowedDirectory, path.dirname(resolvedTarget));
130
+ // Throws for a dangling link, which cannot be proven to be a directory.
131
+ nodeFs.realpathSync.native(resolvedTarget);
132
+ }
133
+ static resolveProjectArtifactPath(projectPath, artifactPath) {
134
+ if (path.isAbsolute(artifactPath)) {
135
+ throw new Error(`Refusing to manage an artifact outside the project: ${artifactPath}`);
136
+ }
137
+ const targetPath = path.join(projectPath, artifactPath);
138
+ this.assertPathWithin(projectPath, targetPath);
139
+ return targetPath;
140
+ }
141
+ static assertProjectArtifactPath(projectPath, targetPath) {
142
+ this.assertPathWithin(projectPath, targetPath);
143
+ }
144
+ static isPathWithin(allowedDirectory, targetPath) {
145
+ const relative = path.relative(allowedDirectory, targetPath);
146
+ return (relative === '' ||
147
+ (relative !== '..' &&
148
+ !relative.startsWith(`..${path.sep}`) &&
149
+ !path.isAbsolute(relative)));
150
+ }
151
+ static canonicalizePotentialPath(targetPath) {
152
+ let existingPath = targetPath;
153
+ const missingSegments = [];
154
+ while (true) {
155
+ try {
156
+ // lstat distinguishes a missing path from a dangling symlink. A
157
+ // dangling link cannot be proven confined, so realpath must fail it.
158
+ nodeFs.lstatSync(existingPath);
159
+ const canonicalExisting = nodeFs.realpathSync.native(existingPath);
160
+ return path.resolve(canonicalExisting, ...missingSegments);
161
+ }
162
+ catch (error) {
163
+ const code = error.code;
164
+ if (code !== 'ENOENT') {
165
+ throw error;
166
+ }
167
+ try {
168
+ if (nodeFs.lstatSync(existingPath).isSymbolicLink()) {
169
+ throw new Error(`Cannot verify dangling symbolic link: ${existingPath}`);
170
+ }
171
+ }
172
+ catch (lstatError) {
173
+ if (lstatError.code !== 'ENOENT') {
174
+ throw lstatError;
175
+ }
176
+ }
177
+ const parent = path.dirname(existingPath);
178
+ if (parent === existingPath) {
179
+ throw new Error(`Cannot resolve an existing parent for ${targetPath}`);
180
+ }
181
+ missingSegments.unshift(path.basename(existingPath));
182
+ existingPath = parent;
183
+ }
184
+ }
185
+ }
186
+ static isWindowsBasePath(basePath) {
187
+ return /^[A-Za-z]:[\\/]/.test(basePath) || basePath.startsWith('\\');
188
+ }
189
+ static normalizeSegments(segments) {
190
+ return segments
191
+ .flatMap((segment) => segment.split(/[\\/]+/u))
192
+ .filter((part) => part.length > 0);
193
+ }
194
+ static joinPath(basePath, ...segments) {
195
+ const normalizedSegments = this.normalizeSegments(segments);
196
+ if (this.isWindowsBasePath(basePath)) {
197
+ const normalizedBasePath = path.win32.normalize(basePath);
198
+ return normalizedSegments.length
199
+ ? path.win32.join(normalizedBasePath, ...normalizedSegments)
200
+ : normalizedBasePath;
201
+ }
202
+ const posixBasePath = basePath.replace(/\\/g, '/');
203
+ return normalizedSegments.length
204
+ ? path.posix.join(posixBasePath, ...normalizedSegments)
205
+ : path.posix.normalize(posixBasePath);
206
+ }
207
+ static async createDirectory(dirPath) {
208
+ await fs.mkdir(dirPath, { recursive: true });
209
+ }
210
+ static async fileExists(filePath) {
211
+ try {
212
+ await fs.access(filePath);
213
+ return true;
214
+ }
215
+ catch (error) {
216
+ if (error.code !== 'ENOENT') {
217
+ console.debug(`Unable to check if file exists at ${filePath}: ${error.message}`);
218
+ }
219
+ return false;
220
+ }
221
+ }
222
+ /**
223
+ * Finds the first existing parent directory by walking up the directory tree.
224
+ * @param dirPath Starting directory path
225
+ * @returns The first existing directory path, or null if root is reached without finding one
226
+ */
227
+ static async findFirstExistingDirectory(dirPath) {
228
+ let currentDir = dirPath;
229
+ while (true) {
230
+ try {
231
+ const stats = await fs.stat(currentDir);
232
+ if (stats.isDirectory()) {
233
+ return currentDir;
234
+ }
235
+ // Path component exists but is not a directory (edge case)
236
+ console.debug(`Path component ${currentDir} exists but is not a directory`);
237
+ return null;
238
+ }
239
+ catch (error) {
240
+ if (error.code === 'ENOENT') {
241
+ // Directory doesn't exist, move up one level
242
+ const parentDir = path.dirname(currentDir);
243
+ if (parentDir === currentDir) {
244
+ // Reached filesystem root without finding existing directory
245
+ return null;
246
+ }
247
+ currentDir = parentDir;
248
+ }
249
+ else {
250
+ // Unexpected error (permissions, I/O error, etc.)
251
+ console.debug(`Error checking directory ${currentDir}: ${error.message}`);
252
+ return null;
253
+ }
254
+ }
255
+ }
256
+ }
257
+ static async canWriteFile(filePath) {
258
+ try {
259
+ const stats = await fs.stat(filePath);
260
+ if (stats.isDirectory()) {
261
+ return hasWritableModeAndAccess(filePath);
262
+ }
263
+ if (!stats.isFile()) {
264
+ return true;
265
+ }
266
+ return hasWritableModeAndAccess(filePath);
267
+ }
268
+ catch (error) {
269
+ if (error.code === 'ENOENT') {
270
+ // File doesn't exist - find first existing parent directory and check its permissions
271
+ const parentDir = path.dirname(filePath);
272
+ const existingDir = await this.findFirstExistingDirectory(parentDir);
273
+ if (existingDir === null) {
274
+ // No existing parent directory found (edge case)
275
+ return false;
276
+ }
277
+ // Check if the existing parent directory is writable.
278
+ return hasWritableModeAndAccess(existingDir);
279
+ }
280
+ console.debug(`Unable to determine write permissions for ${filePath}: ${error.message}`);
281
+ return false;
282
+ }
283
+ }
284
+ static async directoryExists(dirPath) {
285
+ try {
286
+ const stats = await fs.stat(dirPath);
287
+ return stats.isDirectory();
288
+ }
289
+ catch (error) {
290
+ if (error.code !== 'ENOENT') {
291
+ console.debug(`Unable to check if directory exists at ${dirPath}: ${error.message}`);
292
+ }
293
+ return false;
294
+ }
295
+ }
296
+ static async writeFile(filePath, content) {
297
+ const dir = path.dirname(filePath);
298
+ await this.createDirectory(dir);
299
+ await fs.writeFile(filePath, content, 'utf-8');
300
+ }
301
+ static async readFile(filePath) {
302
+ return await fs.readFile(filePath, 'utf-8');
303
+ }
304
+ static async updateFileWithMarkers(filePath, content, startMarker, endMarker) {
305
+ let existingContent = '';
306
+ // The managed block is composed with '\n', so splicing it into a CRLF file
307
+ // would leave mixed endings behind. bash reports a stray '\r' in .bashrc as
308
+ // "$'\r': command not found", so settle the whole file on the convention it
309
+ // already used. A file that does not exist yet stays LF.
310
+ let originalContent;
311
+ if (await this.fileExists(filePath)) {
312
+ existingContent = await this.readFile(filePath);
313
+ originalContent = existingContent;
314
+ const startIndex = findMarkerIndex(existingContent, startMarker);
315
+ const endIndex = startIndex !== -1
316
+ ? findMarkerIndex(existingContent, endMarker, startIndex + startMarker.length)
317
+ : findMarkerIndex(existingContent, endMarker);
318
+ if (startIndex !== -1 && endIndex !== -1) {
319
+ if (endIndex < startIndex) {
320
+ throw new Error(`Invalid marker state in ${filePath}. End marker appears before start marker.`);
321
+ }
322
+ const before = existingContent.substring(0, startIndex);
323
+ const after = existingContent.substring(endIndex + endMarker.length);
324
+ existingContent = before + startMarker + '\n' + content + '\n' + endMarker + after;
325
+ }
326
+ else if (startIndex === -1 && endIndex === -1) {
327
+ existingContent = startMarker + '\n' + content + '\n' + endMarker + '\n\n' + existingContent;
328
+ }
329
+ else {
330
+ throw new Error(`Invalid marker state in ${filePath}. Found start: ${startIndex !== -1}, Found end: ${endIndex !== -1}`);
331
+ }
332
+ }
333
+ else {
334
+ existingContent = startMarker + '\n' + content + '\n' + endMarker;
335
+ }
336
+ await this.writeFile(filePath, originalContent === undefined
337
+ ? existingContent
338
+ : matchLineEnding(existingContent, originalContent));
339
+ }
340
+ static async ensureWritePermissions(dirPath) {
341
+ try {
342
+ // If directory doesn't exist, check parent directory permissions
343
+ if (!await this.directoryExists(dirPath)) {
344
+ const parentDir = path.dirname(dirPath);
345
+ if (!await this.directoryExists(parentDir)) {
346
+ await this.createDirectory(parentDir);
347
+ }
348
+ return await this.ensureWritePermissions(parentDir);
349
+ }
350
+ const testFile = path.join(dirPath, '.freespec-test-' + Date.now() + '-' + Math.random().toString(36).slice(2));
351
+ await fs.writeFile(testFile, '');
352
+ // On Windows, file may be temporarily locked by antivirus or indexing services.
353
+ // Retry unlink with a small delay if it fails.
354
+ const maxRetries = 3;
355
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
356
+ try {
357
+ await fs.unlink(testFile);
358
+ break;
359
+ }
360
+ catch (unlinkError) {
361
+ if (attempt === maxRetries - 1) {
362
+ // Last attempt failed, but we successfully wrote the file, so permissions are OK
363
+ // Just log and continue - the temp file will be cleaned up eventually
364
+ console.debug(`Could not clean up test file ${testFile}: ${unlinkError.message}`);
365
+ }
366
+ else {
367
+ // Wait briefly before retrying (Windows file lock release)
368
+ await new Promise((resolve) => setTimeout(resolve, 50));
369
+ }
370
+ }
371
+ }
372
+ return true;
373
+ }
374
+ catch (error) {
375
+ console.debug(`Insufficient permissions to write to ${dirPath}: ${error.message}`);
376
+ return false;
377
+ }
378
+ }
379
+ }
380
+ /**
381
+ * Removes a marker block from file content.
382
+ * Only removes markers that are on their own lines (ignores inline mentions).
383
+ * Cleans up double blank lines that may result from removal.
384
+ *
385
+ * @param content - File content with markers
386
+ * @param startMarker - The start marker string
387
+ * @param endMarker - The end marker string
388
+ * @returns Content with marker block removed, or original content if markers not found/invalid
389
+ */
390
+ export function removeMarkerBlock(content, startMarker, endMarker) {
391
+ const startIndex = findMarkerIndex(content, startMarker);
392
+ const endIndex = startIndex !== -1
393
+ ? findMarkerIndex(content, endMarker, startIndex + startMarker.length)
394
+ : findMarkerIndex(content, endMarker);
395
+ if (startIndex === -1 || endIndex === -1 || endIndex <= startIndex) {
396
+ return content;
397
+ }
398
+ // Find the start of the line containing the start marker
399
+ let lineStart = startIndex;
400
+ while (lineStart > 0 && content[lineStart - 1] !== '\n') {
401
+ lineStart--;
402
+ }
403
+ // Find the end of the line containing the end marker
404
+ let lineEnd = endIndex + endMarker.length;
405
+ while (lineEnd < content.length && content[lineEnd] !== '\n') {
406
+ lineEnd++;
407
+ }
408
+ // Include the trailing newline if present
409
+ if (lineEnd < content.length && content[lineEnd] === '\n') {
410
+ lineEnd++;
411
+ }
412
+ const before = content.substring(0, lineStart);
413
+ const after = content.substring(lineEnd);
414
+ // The file's own newline, used for every ending this function writes. The
415
+ // blank-line collapse below rebuilds the separator it matched, so spelling it
416
+ // '\n' would leave a CRLF file with a mixed pair wherever a run was collapsed
417
+ // - the stray '\r' that bash reports as "$'\r': command not found".
418
+ //
419
+ // Dominant rather than "contains a CRLF anywhere", so that one stray CRLF in
420
+ // an otherwise-LF file does not pull the whole rewrite to CRLF. This is the
421
+ // same reading matchLineEnding uses, so both write paths agree.
422
+ const newline = detectLineEnding(content) ?? '\n';
423
+ // Clean up double blank lines (handle both Unix \n and Windows \r\n)
424
+ let result = before + after;
425
+ result = result.replace(/(\r?\n){3,}/g, newline + newline);
426
+ // Trim trailing whitespace but preserve leading whitespace and original newline style
427
+ if (result.trimEnd() === '') {
428
+ return '';
429
+ }
430
+ return result.trimEnd() + newline;
431
+ }
432
+ //# sourceMappingURL=file-system.js.map
@@ -0,0 +1,6 @@
1
+ export { validateChangeName, createChange } from './change-utils.js';
2
+ export type { ValidationResult, CreateChangeOptions } from './change-utils.js';
3
+ export { readChangeMetadata, writeChangeMetadata, resolveSchemaForChange, validateSchemaName, ChangeMetadataError, listUnknownChangeMetadataKeys, readUnknownChangeMetadataKeys, formatUnknownChangeMetadataKeysMessage, CHANGE_METADATA_KNOWN_KEYS, } from './change-metadata.js';
4
+ export { FileSystemUtils, removeMarkerBlock } from './file-system.js';
5
+ export { transformCommandInvocations, transformToSkillReferences, getSkillReferenceTransformer, getTransformerForTool, } from './command-references.js';
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,9 @@
1
+ // Shared utilities
2
+ export { validateChangeName, createChange } from './change-utils.js';
3
+ // Change metadata utilities
4
+ export { readChangeMetadata, writeChangeMetadata, resolveSchemaForChange, validateSchemaName, ChangeMetadataError, listUnknownChangeMetadataKeys, readUnknownChangeMetadataKeys, formatUnknownChangeMetadataKeysMessage, CHANGE_METADATA_KNOWN_KEYS, } from './change-metadata.js';
5
+ // File system utilities
6
+ export { FileSystemUtils, removeMarkerBlock } from './file-system.js';
7
+ // Command reference utilities
8
+ export { transformCommandInvocations, transformToSkillReferences, getSkillReferenceTransformer, getTransformerForTool, } from './command-references.js';
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,85 @@
1
+ import type { Readable, Writable } from 'node:stream';
2
+ export type InteractiveOptions = {
3
+ /**
4
+ * Explicit "disable prompts" flag passed by internal callers.
5
+ */
6
+ noInteractive?: boolean;
7
+ /**
8
+ * Commander-style negated option: `--no-interactive` sets this to false.
9
+ */
10
+ interactive?: boolean;
11
+ };
12
+ /**
13
+ * Resolves whether non-interactive mode is requested.
14
+ * Handles both explicit `noInteractive: true` and Commander.js style `interactive: false`.
15
+ * Use this helper instead of manually checking options.noInteractive to avoid bugs.
16
+ */
17
+ export declare function resolveNoInteractive(value?: boolean | InteractiveOptions): boolean;
18
+ export declare function isInteractive(value?: boolean | InteractiveOptions): boolean;
19
+ /**
20
+ * True when a prompt failed because no answer could be read — an agent or a
21
+ * script that ran the command with stdin closed, a CI job, or a shell whose
22
+ * stdin is not a terminal. @inquirer rejects those with `User force closed
23
+ * the prompt with 0 null`, which is accurate and useless: it names no flag
24
+ * and no next step (#1479).
25
+ *
26
+ * Two things it deliberately is not:
27
+ *
28
+ * - It is not a substitute for `isInteractive()`. This classifies a prompt
29
+ * that has *already failed*, so piped answers are unaffected: an answer
30
+ * that arrives resolves the prompt and never reaches this check. Refusing
31
+ * to prompt up front would break `printf 'y\n' | freespec archive ...`,
32
+ * which works today.
33
+ * - It is not a cancellation check. Ctrl-C raises the same error class, and
34
+ * it reaches a process whose stdin is a pipe just as easily as one at a
35
+ * terminal, so the SIGINT signal - not the terminal - is what proves
36
+ * somebody was there and chose to quit.
37
+ *
38
+ * Beyond that it defers to `isInteractive()`, so `CI`, `OPEN_SPEC_INTERACTIVE=0`
39
+ * and `--no-interactive` count even when a runner allocated a pty. It also
40
+ * counts a redirected stdout: `confirmPrompt` drops to the plain reader whenever
41
+ * *either* stream is not a TTY, so a stdin-TTY-but-stdout-redirected run
42
+ * (`freespec archive x > log.txt` from a terminal) that hits EOF must classify
43
+ * the same way the prompt was selected — otherwise it would leak the raw
44
+ * `ExitPromptError` instead of the `--yes` guidance.
45
+ */
46
+ export declare function isNonInteractivePromptError(error: unknown, value?: boolean | InteractiveOptions): boolean;
47
+ export type ConfirmPrompt = {
48
+ message: string;
49
+ default: boolean;
50
+ };
51
+ /**
52
+ * Ask a yes/no question. A real terminal gets @inquirer's rich prompt;
53
+ * everything else — a pipe, a file redirect, an agent that captures stdout —
54
+ * reads one plain line instead.
55
+ *
56
+ * @inquirer renders `confirm` by writing ANSI cursor-movement escape sequences,
57
+ * and it emits them even when stdout is not a TTY. Redirected to a file those
58
+ * sequences are noise, and in some non-TTY hosts the render loop never settles
59
+ * and repeats `ESC[NNG` cursor moves until the disk fills (#1526). Reading
60
+ * the answer ourselves keeps the single piped answer @inquirer ever supported
61
+ * working (`printf 'y\n' | freespec archive ...`) without emitting any escapes.
62
+ *
63
+ * `io` overrides the streams; it exists for tests and mirrors @inquirer's own
64
+ * `{ input, output }` context. Production callers pass only the prompt.
65
+ */
66
+ export declare function confirmPrompt(prompt: ConfirmPrompt, io?: {
67
+ input?: Readable;
68
+ output?: Writable;
69
+ }): Promise<boolean>;
70
+ /**
71
+ * Whether a spinner can safely render to `stream`.
72
+ *
73
+ * ora decides how many lines to erase with `ceil(width / stream.columns)`, so a
74
+ * TTY that reports zero columns makes that count `Infinity` and `clear()` never
75
+ * ends: the spinner emits cursor-up/clear-line escapes as fast as it can and the
76
+ * command never finishes (#2106). Zero-width TTYs are real — a pty inherited
77
+ * from a pre-commit runner such as lefthook, or `script -qec` — and nothing can
78
+ * be drawn in zero columns anyway, so refuse the spinner and let the caller fall
79
+ * back to its plain output.
80
+ */
81
+ export declare function canRenderSpinner(stream?: {
82
+ isTTY?: boolean;
83
+ columns?: number;
84
+ }): boolean;
85
+ //# sourceMappingURL=interactive.d.ts.map