@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,174 @@
1
+ import { createInterface } from 'node:readline';
2
+ /**
3
+ * Resolves whether non-interactive mode is requested.
4
+ * Handles both explicit `noInteractive: true` and Commander.js style `interactive: false`.
5
+ * Use this helper instead of manually checking options.noInteractive to avoid bugs.
6
+ */
7
+ export function resolveNoInteractive(value) {
8
+ if (typeof value === 'boolean')
9
+ return value;
10
+ return value?.noInteractive === true || value?.interactive === false;
11
+ }
12
+ export function isInteractive(value) {
13
+ if (resolveNoInteractive(value))
14
+ return false;
15
+ if (process.env.OPEN_SPEC_INTERACTIVE === '0')
16
+ return false;
17
+ // Respect the standard CI environment variable (set by GitHub Actions, GitLab CI, Travis, etc.)
18
+ if ('CI' in process.env)
19
+ return false;
20
+ return !!process.stdin.isTTY;
21
+ }
22
+ /**
23
+ * True when a prompt failed because no answer could be read — an agent or a
24
+ * script that ran the command with stdin closed, a CI job, or a shell whose
25
+ * stdin is not a terminal. @inquirer rejects those with `User force closed
26
+ * the prompt with 0 null`, which is accurate and useless: it names no flag
27
+ * and no next step (#1479).
28
+ *
29
+ * Two things it deliberately is not:
30
+ *
31
+ * - It is not a substitute for `isInteractive()`. This classifies a prompt
32
+ * that has *already failed*, so piped answers are unaffected: an answer
33
+ * that arrives resolves the prompt and never reaches this check. Refusing
34
+ * to prompt up front would break `printf 'y\n' | freespec archive ...`,
35
+ * which works today.
36
+ * - It is not a cancellation check. Ctrl-C raises the same error class, and
37
+ * it reaches a process whose stdin is a pipe just as easily as one at a
38
+ * terminal, so the SIGINT signal - not the terminal - is what proves
39
+ * somebody was there and chose to quit.
40
+ *
41
+ * Beyond that it defers to `isInteractive()`, so `CI`, `OPEN_SPEC_INTERACTIVE=0`
42
+ * and `--no-interactive` count even when a runner allocated a pty. It also
43
+ * counts a redirected stdout: `confirmPrompt` drops to the plain reader whenever
44
+ * *either* stream is not a TTY, so a stdin-TTY-but-stdout-redirected run
45
+ * (`freespec archive x > log.txt` from a terminal) that hits EOF must classify
46
+ * the same way the prompt was selected — otherwise it would leak the raw
47
+ * `ExitPromptError` instead of the `--yes` guidance.
48
+ */
49
+ export function isNonInteractivePromptError(error, value) {
50
+ if (!(error instanceof Error))
51
+ return false;
52
+ const failedPrompt = error.name === 'ExitPromptError' || error.message.includes('force closed the prompt');
53
+ if (!failedPrompt)
54
+ return false;
55
+ if (error.message.includes('SIGINT'))
56
+ return false;
57
+ return !isInteractive(value) || !process.stdout.isTTY;
58
+ }
59
+ /**
60
+ * Ask a yes/no question. A real terminal gets @inquirer's rich prompt;
61
+ * everything else — a pipe, a file redirect, an agent that captures stdout —
62
+ * reads one plain line instead.
63
+ *
64
+ * @inquirer renders `confirm` by writing ANSI cursor-movement escape sequences,
65
+ * and it emits them even when stdout is not a TTY. Redirected to a file those
66
+ * sequences are noise, and in some non-TTY hosts the render loop never settles
67
+ * and repeats `ESC[NNG` cursor moves until the disk fills (#1526). Reading
68
+ * the answer ourselves keeps the single piped answer @inquirer ever supported
69
+ * working (`printf 'y\n' | freespec archive ...`) without emitting any escapes.
70
+ *
71
+ * `io` overrides the streams; it exists for tests and mirrors @inquirer's own
72
+ * `{ input, output }` context. Production callers pass only the prompt.
73
+ */
74
+ export async function confirmPrompt(prompt, io = {}) {
75
+ const input = io.input ?? process.stdin;
76
+ const output = io.output ?? process.stdout;
77
+ const isTerminal = Boolean(input.isTTY) &&
78
+ Boolean(output.isTTY);
79
+ if (isTerminal) {
80
+ const { confirm } = await import('@inquirer/prompts');
81
+ return confirm(prompt);
82
+ }
83
+ return readYesNo(prompt, input, output);
84
+ }
85
+ function readYesNo(prompt, input, output) {
86
+ return new Promise((resolve, reject) => {
87
+ let settled = false;
88
+ // Detach the input-stream error listener on settle: `input` is the
89
+ // long-lived process.stdin, so a leftover listener would accumulate across
90
+ // archive's successive prompts and swallow a later, unrelated stdin error.
91
+ const cleanup = () => {
92
+ input.removeListener('error', onError);
93
+ };
94
+ const blockOnNoAnswer = () => {
95
+ if (settled)
96
+ return;
97
+ settled = true;
98
+ cleanup();
99
+ // No line could be read (stdin closed / EOF). Mirror @inquirer's failure
100
+ // so callers that classify it — isNonInteractivePromptError, the #1479
101
+ // "rerun with --yes" guidance — keep working unchanged.
102
+ const error = new Error('User force closed the prompt');
103
+ error.name = 'ExitPromptError';
104
+ reject(error);
105
+ };
106
+ // A stdin error surfaces on the interface (readline forwards input-stream
107
+ // errors since Node 16). Without a handler the promise would hang and the
108
+ // 'error' would go unhandled; settle it with the real fault instead.
109
+ const onError = (err) => {
110
+ if (settled)
111
+ return;
112
+ settled = true;
113
+ cleanup();
114
+ rl.close();
115
+ reject(err instanceof Error ? err : new Error(String(err)));
116
+ };
117
+ // An earlier prompt may have already drained stdin (only one piped answer
118
+ // was ever supported). A fresh readline over an ended stream never emits
119
+ // 'close', so guard here rather than hang and exit as a no-op.
120
+ if (input.readableEnded) {
121
+ blockOnNoAnswer();
122
+ return;
123
+ }
124
+ output.write(`${prompt.message} ${prompt.default ? '(Y/n)' : '(y/N)'} `);
125
+ // terminal:false guarantees readline never emits its own line-editing
126
+ // escapes — an escape-free read is the whole point here.
127
+ const rl = createInterface({ input, terminal: false });
128
+ input.once('error', onError);
129
+ rl.once('error', onError);
130
+ rl.once('line', (line) => {
131
+ if (settled)
132
+ return;
133
+ settled = true;
134
+ cleanup();
135
+ rl.close();
136
+ output.write('\n');
137
+ // Mirror @inquirer/confirm's parser (prefix match on y/yes and n/no,
138
+ // otherwise the default) so a piped answer resolves identically to the
139
+ // interactive prompt it replaces.
140
+ const answer = line.trim();
141
+ if (/^(y|yes)/i.test(answer)) {
142
+ resolve(true);
143
+ }
144
+ else if (/^(n|no)/i.test(answer)) {
145
+ resolve(false);
146
+ }
147
+ else {
148
+ resolve(prompt.default);
149
+ }
150
+ });
151
+ rl.once('close', () => {
152
+ blockOnNoAnswer();
153
+ });
154
+ });
155
+ }
156
+ /**
157
+ * Whether a spinner can safely render to `stream`.
158
+ *
159
+ * ora decides how many lines to erase with `ceil(width / stream.columns)`, so a
160
+ * TTY that reports zero columns makes that count `Infinity` and `clear()` never
161
+ * ends: the spinner emits cursor-up/clear-line escapes as fast as it can and the
162
+ * command never finishes (#2106). Zero-width TTYs are real — a pty inherited
163
+ * from a pre-commit runner such as lefthook, or `script -qec` — and nothing can
164
+ * be drawn in zero columns anyway, so refuse the spinner and let the caller fall
165
+ * back to its plain output.
166
+ */
167
+ export function canRenderSpinner(stream = process.stderr) {
168
+ // A non-TTY stream never enters the clear loop: ora disables itself and
169
+ // prints one plain line instead.
170
+ if (!stream.isTTY)
171
+ return true;
172
+ return (stream.columns ?? 0) > 0;
173
+ }
174
+ //# sourceMappingURL=interactive.js.map
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Returns the ids of active changes: every `<名称>变更` directory directly under freespec/.
3
+ *
4
+ * A change is resolved by its directory alone - the same rule `list`,
5
+ * `status`, `instructions` and `validate` use (`getAvailableChanges`).
6
+ * Requiring proposal.md here made `freespec show` and shell completion miss
7
+ * changes those commands resolve: `freespec new change <name>` scaffolds only
8
+ * `.freespec.yaml`, and a custom schema need not define a proposal artifact at
9
+ * all (#1161).
10
+ */
11
+ export declare function getActiveChangeIds(root?: string): Promise<string[]>;
12
+ /** Main specs and archived changes are not part of this workflow. */
13
+ export declare function getSpecIds(_root?: string): Promise<string[]>;
14
+ export declare function getArchivedChangeIds(_root?: string): Promise<string[]>;
15
+ //# sourceMappingURL=item-discovery.d.ts.map
@@ -0,0 +1,33 @@
1
+ import { promises as fs } from 'fs';
2
+ import { changeParentDir, isChangeDirectoryName } from '../core/change-layout.js';
3
+ /**
4
+ * Returns the ids of active changes: every `<名称>变更` directory directly under freespec/.
5
+ *
6
+ * A change is resolved by its directory alone - the same rule `list`,
7
+ * `status`, `instructions` and `validate` use (`getAvailableChanges`).
8
+ * Requiring proposal.md here made `freespec show` and shell completion miss
9
+ * changes those commands resolve: `freespec new change <name>` scaffolds only
10
+ * `.freespec.yaml`, and a custom schema need not define a proposal artifact at
11
+ * all (#1161).
12
+ */
13
+ export async function getActiveChangeIds(root = process.cwd()) {
14
+ const changesPath = changeParentDir(root);
15
+ try {
16
+ const entries = await fs.readdir(changesPath, { withFileTypes: true });
17
+ return entries
18
+ .filter((entry) => entry.isDirectory() && isChangeDirectoryName(entry.name))
19
+ .map((entry) => entry.name)
20
+ .sort();
21
+ }
22
+ catch {
23
+ return [];
24
+ }
25
+ }
26
+ /** Main specs and archived changes are not part of this workflow. */
27
+ export async function getSpecIds(_root = process.cwd()) {
28
+ return [];
29
+ }
30
+ export async function getArchivedChangeIds(_root = process.cwd()) {
31
+ return [];
32
+ }
33
+ //# sourceMappingURL=item-discovery.js.map
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Line-ending helpers.
3
+ *
4
+ * Every parser in this codebase normalizes CRLF to LF on the way in, so all
5
+ * serialization logic can assume '\n'. That leaves the write side responsible
6
+ * for restoring whatever convention the file already used: without it, editing
7
+ * one requirement in a CRLF spec rewrites every line of the file and buries the
8
+ * real change in the diff.
9
+ */
10
+ export type LineEnding = '\n' | '\r\n';
11
+ /**
12
+ * The dominant line ending in `content`, or undefined when it holds no line
13
+ * break to judge from.
14
+ *
15
+ * Mixed files resolve to whichever ending is more common, with CRLF winning a
16
+ * tie: a file that is mostly CRLF is a CRLF file that picked up a stray LF, and
17
+ * settling the whole file on one ending is what keeps later diffs small.
18
+ */
19
+ export declare function detectLineEnding(content: string): LineEnding | undefined;
20
+ /**
21
+ * Re-apply `ending` to LF-normalized `content`.
22
+ *
23
+ * Normalizes to LF first, so the result is uniform even if the caller passed
24
+ * content that already contained CRLF.
25
+ */
26
+ export declare function applyLineEnding(content: string, ending: LineEnding): string;
27
+ /**
28
+ * Rewrite `content` to match the convention of `original`.
29
+ *
30
+ * When `original` has no line break to learn from, `content` is left as LF —
31
+ * the portable default this project writes new files with.
32
+ */
33
+ export declare function matchLineEnding(content: string, original: string): string;
34
+ //# sourceMappingURL=line-endings.d.ts.map
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Line-ending helpers.
3
+ *
4
+ * Every parser in this codebase normalizes CRLF to LF on the way in, so all
5
+ * serialization logic can assume '\n'. That leaves the write side responsible
6
+ * for restoring whatever convention the file already used: without it, editing
7
+ * one requirement in a CRLF spec rewrites every line of the file and buries the
8
+ * real change in the diff.
9
+ */
10
+ /**
11
+ * The dominant line ending in `content`, or undefined when it holds no line
12
+ * break to judge from.
13
+ *
14
+ * Mixed files resolve to whichever ending is more common, with CRLF winning a
15
+ * tie: a file that is mostly CRLF is a CRLF file that picked up a stray LF, and
16
+ * settling the whole file on one ending is what keeps later diffs small.
17
+ */
18
+ export function detectLineEnding(content) {
19
+ const crlf = content.match(/\r\n/g)?.length ?? 0;
20
+ // Count LFs not preceded by CR, so CRLF is never also counted as LF.
21
+ const lf = content.match(/(?<!\r)\n/g)?.length ?? 0;
22
+ if (crlf === 0 && lf === 0)
23
+ return undefined;
24
+ return crlf >= lf ? '\r\n' : '\n';
25
+ }
26
+ /**
27
+ * Re-apply `ending` to LF-normalized `content`.
28
+ *
29
+ * Normalizes to LF first, so the result is uniform even if the caller passed
30
+ * content that already contained CRLF.
31
+ */
32
+ export function applyLineEnding(content, ending) {
33
+ const normalized = content.replace(/\r\n/g, '\n');
34
+ return ending === '\n' ? normalized : normalized.replace(/\n/g, '\r\n');
35
+ }
36
+ /**
37
+ * Rewrite `content` to match the convention of `original`.
38
+ *
39
+ * When `original` has no line break to learn from, `content` is left as LF —
40
+ * the portable default this project writes new files with.
41
+ */
42
+ export function matchLineEnding(content, original) {
43
+ const ending = detectLineEnding(original);
44
+ return ending === undefined ? content : applyLineEnding(content, ending);
45
+ }
46
+ //# sourceMappingURL=line-endings.js.map
@@ -0,0 +1,3 @@
1
+ export declare function nearestMatches(input: string, candidates: string[], max?: number): string[];
2
+ export declare function levenshtein(a: string, b: string): number;
3
+ //# sourceMappingURL=match.d.ts.map
@@ -0,0 +1,22 @@
1
+ export function nearestMatches(input, candidates, max = 5) {
2
+ const scored = candidates.map(candidate => ({ candidate, distance: levenshtein(input, candidate) }));
3
+ scored.sort((a, b) => a.distance - b.distance);
4
+ return scored.slice(0, max).map(s => s.candidate);
5
+ }
6
+ export function levenshtein(a, b) {
7
+ const m = a.length;
8
+ const n = b.length;
9
+ const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
10
+ for (let i = 0; i <= m; i++)
11
+ dp[i][0] = i;
12
+ for (let j = 0; j <= n; j++)
13
+ dp[0][j] = j;
14
+ for (let i = 1; i <= m; i++) {
15
+ for (let j = 1; j <= n; j++) {
16
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
17
+ dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost);
18
+ }
19
+ }
20
+ return dp[m][n];
21
+ }
22
+ //# sourceMappingURL=match.js.map
@@ -0,0 +1,35 @@
1
+ /** A namespace folder under `freespec/` and the change directories buried in it. */
2
+ export interface NestedChangeFinding {
3
+ /** The directory name directly under `freespec/` that is not a change. */
4
+ name: string;
5
+ /**
6
+ * Slash-separated paths of the nested change directories, relative to
7
+ * `freespec/`, sorted. Always at least one entry.
8
+ */
9
+ nested: string[];
10
+ }
11
+ /**
12
+ * Reports whether the directory `freespec/<name>/` is a namespace folder holding
13
+ * one or more nested change directories rather than a change of its own.
14
+ *
15
+ * Returns undefined for every ordinary change, including a scaffolded one with
16
+ * no artifacts yet: only a *nested* change directory distinguishes the two.
17
+ */
18
+ export declare function findNestedChangesIn(changesDir: string, name: string): Promise<NestedChangeFinding | undefined>;
19
+ /**
20
+ * The same check across every candidate directory under `freespec/`, for the
21
+ * commands that enumerate rather than resolve a single name.
22
+ */
23
+ export declare function findNestedChanges(changesDir: string, names: string[]): Promise<NestedChangeFinding[]>;
24
+ /**
25
+ * The stable phrase every nested-change diagnostic contains. Callers that have
26
+ * only the rendered message - `validate`'s next-steps, which branches on the
27
+ * exact issue it is advising about - recognise the case by this.
28
+ */
29
+ export declare const NESTED_CHANGE_ISSUE_MARKER = "is not a change: it is a folder wrapping";
30
+ /**
31
+ * The one explanation every surface prints, so the constraint and the way out
32
+ * read identically wherever a user meets them.
33
+ */
34
+ export declare function describeNestedChange(finding: NestedChangeFinding): string;
35
+ //# sourceMappingURL=nested-change.d.ts.map
@@ -0,0 +1,185 @@
1
+ import { promises as fs } from 'fs';
2
+ import path from 'path';
3
+ import { artifactOutputExists } from '../core/artifact-graph/outputs.js';
4
+ import { resolveSchema } from '../core/artifact-graph/resolver.js';
5
+ import { METADATA_FILENAME, resolveSchemaForChange } from './change-metadata.js';
6
+ /**
7
+ * Files that only ever sit at the root of a change directory. Their presence
8
+ * inside a directory that is *not* directly under `freespec/` is one of the two
9
+ * signals that someone laid a change out in a namespace folder
10
+ * (`freespec/<area>/<名称>变更/`), which this workflow does not support: a change
11
+ * is a directory directly under `freespec/`, named `<名称>变更`.
12
+ *
13
+ * The list is deliberately short. Every entry is written by FreeSpec itself for
14
+ * the default schema - `.freespec.yaml` by `freespec new change` for *every*
15
+ * schema. A custom schema whose root artifacts are named differently can still
16
+ * be missed when the directory was created by hand rather than by the CLI; that
17
+ * misses the diagnostic and behaves exactly as it did before, which is the safe
18
+ * way round.
19
+ */
20
+ const CHANGE_ROOT_MARKERS = [
21
+ METADATA_FILENAME,
22
+ '需求文档.md',
23
+ '设计文档.md',
24
+ '任务清单.md',
25
+ '测试用例.md',
26
+ ];
27
+ /**
28
+ * How far below a candidate directory to look for a nested change. One level
29
+ * covers a nested layout (`freespec/移动端/刷新变更/`); the
30
+ * extra levels cover a store namespaced by more than one axis
31
+ * (`freespec/移动端/苹果/刷新变更/`) without turning this into an unbounded
32
+ * walk of every change's spec tree.
33
+ */
34
+ const MAX_NESTING_DEPTH = 3;
35
+ async function hasChangeRootMarker(dir) {
36
+ for (const marker of CHANGE_ROOT_MARKERS) {
37
+ const stats = await fs.stat(path.join(dir, marker)).catch(() => undefined);
38
+ if (stats?.isFile())
39
+ return true;
40
+ }
41
+ return false;
42
+ }
43
+ /**
44
+ * Whether `dir` is a change rather than a folder wrapping one.
45
+ *
46
+ * Two independent signals, because either alone leaves a hole. A root artifact
47
+ * covers the CLI-created change and the hand-made one that starts from a
48
+ * proposal. A populated `specs/` covers the change that starts from its delta
49
+ * specs - the case that arrives here most often, since a nested change is
50
+ * always hand-made (`freespec new change` rejects a name with a separator) and
51
+ * "mkdir the tree, write the deltas" is how someone gets there.
52
+ *
53
+ * The `specs/` signal is what keeps a custom schema safe. A schema may generate
54
+ * every root artifact into a subdirectory (`generates: rfc/proposal.md`), and
55
+ * such a change, created by hand so it has no `.freespec.yaml`, would otherwise
56
+ * look exactly like a namespace folder wrapping a change called `rfc`. Its
57
+ * delta specs still live under `specs/`, so it is recognised as the change it
58
+ * is. `specs/**\/*.md` is fixed by the delta format itself, not by the schema.
59
+ */
60
+ async function looksLikeChange(dir, projectRoot) {
61
+ if (await hasChangeRootMarker(dir))
62
+ return true;
63
+ const thinking = await fs.stat(path.join(dir, '深思')).catch(() => undefined);
64
+ if (thinking?.isDirectory())
65
+ return true;
66
+ return hasSchemaOutput(dir, projectRoot);
67
+ }
68
+ /**
69
+ * Whether `dir` already holds a file exactly where the schema it resolves to
70
+ * would generate one - the signal `freespec status` itself uses to call an
71
+ * artifact done. It covers the subdirectory-rooted custom schema before any
72
+ * delta spec exists, when neither other signal can. A schema that cannot be
73
+ * resolved gives no signal, which leaves the other two in charge.
74
+ */
75
+ function hasSchemaOutput(dir, projectRoot) {
76
+ try {
77
+ const schema = resolveSchema(resolveSchemaForChange(dir, undefined, projectRoot), projectRoot);
78
+ return schema.artifacts.some((artifact) => artifactOutputExists(dir, artifact.generates));
79
+ }
80
+ catch {
81
+ return false;
82
+ }
83
+ }
84
+ /**
85
+ * Whether `dir` holds any file of its own. A change directory with content at
86
+ * its root - a custom schema's artifact under a name this module does not know,
87
+ * a README, a note - is never reported as a namespace folder. A namespace
88
+ * folder someone created with `mkdir` holds nothing but directories.
89
+ */
90
+ async function hasOwnFile(dir) {
91
+ const entries = await fs
92
+ .readdir(dir, { withFileTypes: true })
93
+ .catch(() => []);
94
+ return entries.some((entry) => !entry.name.startsWith('.') && !entry.isDirectory());
95
+ }
96
+ /**
97
+ * Subdirectories of `dir`, or none when it cannot be read. This probe only ever
98
+ * adds a diagnostic, so an unreadable directory falls back to the behaviour
99
+ * that existed before it rather than failing the command around it.
100
+ */
101
+ async function readSubdirectories(dir) {
102
+ const entries = await fs
103
+ .readdir(dir, { withFileTypes: true })
104
+ .catch(() => []);
105
+ return entries
106
+ .filter((entry) => entry.isDirectory() && !entry.name.startsWith('.'))
107
+ .map((entry) => entry.name);
108
+ }
109
+ /**
110
+ * Collects the nested change directories under `dir`, deepest search bounded by
111
+ * `MAX_NESTING_DEPTH`. A directory that carries a marker is reported and not
112
+ * descended into: its own `specs/` tree is part of that change, not a further
113
+ * nesting level.
114
+ */
115
+ async function collectNested(dir, prefix, depth, found, projectRoot) {
116
+ if (depth > MAX_NESTING_DEPTH)
117
+ return;
118
+ for (const child of await readSubdirectories(dir)) {
119
+ const childPath = path.join(dir, child);
120
+ const id = `${prefix}/${child}`;
121
+ if (await looksLikeChange(childPath, projectRoot)) {
122
+ found.push(id);
123
+ continue;
124
+ }
125
+ await collectNested(childPath, id, depth + 1, found, projectRoot);
126
+ }
127
+ }
128
+ /**
129
+ * Reports whether the directory `freespec/<name>/` is a namespace folder holding
130
+ * one or more nested change directories rather than a change of its own.
131
+ *
132
+ * Returns undefined for every ordinary change, including a scaffolded one with
133
+ * no artifacts yet: only a *nested* change directory distinguishes the two.
134
+ */
135
+ export async function findNestedChangesIn(changesDir, name) {
136
+ // `archive` is not a change and never was, so its dated entries must not be
137
+ // read as nested changes and offered up for renaming. `list`, `status` and
138
+ // `validate` already exclude it; `change show` does not.
139
+ if (name === 'archive' || name.startsWith('.'))
140
+ return undefined;
141
+ const dir = path.join(changesDir, name);
142
+ // Change folders sit directly under <root>/freespec.
143
+ const projectRoot = path.resolve(changesDir, '..');
144
+ if (await looksLikeChange(dir, projectRoot))
145
+ return undefined;
146
+ if (await hasOwnFile(dir))
147
+ return undefined;
148
+ const nested = [];
149
+ await collectNested(dir, name, 1, nested, projectRoot);
150
+ if (nested.length === 0)
151
+ return undefined;
152
+ return { name, nested: nested.sort() };
153
+ }
154
+ /**
155
+ * The same check across every candidate directory under `freespec/`, for the
156
+ * commands that enumerate rather than resolve a single name.
157
+ */
158
+ export async function findNestedChanges(changesDir, names) {
159
+ const findings = await Promise.all(names.map((name) => findNestedChangesIn(changesDir, name)));
160
+ return findings.filter((finding) => finding !== undefined);
161
+ }
162
+ /**
163
+ * The stable phrase every nested-change diagnostic contains. Callers that have
164
+ * only the rendered message - `validate`'s next-steps, which branches on the
165
+ * exact issue it is advising about - recognise the case by this.
166
+ */
167
+ export const NESTED_CHANGE_ISSUE_MARKER = 'is not a change: it is a folder wrapping';
168
+ /** Flattens a namespace path into the change name FreeSpec would accept. */
169
+ function flatten(nestedId) {
170
+ return nestedId.split('/').join('-');
171
+ }
172
+ /**
173
+ * The one explanation every surface prints, so the constraint and the way out
174
+ * read identically wherever a user meets them.
175
+ */
176
+ export function describeNestedChange(finding) {
177
+ const list = finding.nested.map((id) => `freespec/${id}/`).join(', ');
178
+ const example = flatten(finding.nested[0]);
179
+ return (`"${finding.name}" ${NESTED_CHANGE_ISSUE_MARKER} ${list}. ` +
180
+ 'A change must be a directory directly under freespec/, named with a 变更 suffix, so those ' +
181
+ 'nested directories are invisible while the folder around them ' +
182
+ 'is reported as a change. ' +
183
+ `Rename each nested change to a flat name (for example "${example}").`);
184
+ }
185
+ //# sourceMappingURL=nested-change.js.map
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Supported shell types for completion generation
3
+ */
4
+ export type SupportedShell = 'zsh' | 'bash' | 'fish' | 'powershell';
5
+ /**
6
+ * Result of shell detection
7
+ */
8
+ export interface ShellDetectionResult {
9
+ /** The detected shell if supported, otherwise undefined */
10
+ shell: SupportedShell | undefined;
11
+ /** The raw shell name detected (even if unsupported), or undefined if nothing detected */
12
+ detected: string | undefined;
13
+ }
14
+ /**
15
+ * Detects the current user's shell based on the parent process and environment
16
+ *
17
+ * @returns Detection result with supported shell and raw detected name
18
+ */
19
+ export declare function detectShell(): ShellDetectionResult;
20
+ //# sourceMappingURL=shell-detection.d.ts.map
@@ -0,0 +1,94 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ /**
3
+ * Map a raw shell name/path to a supported shell, if any.
4
+ */
5
+ function matchSupportedShell(name) {
6
+ // Match the executable basename exactly so lookalikes such as `fish-lsp` or
7
+ // `bash-language-server` don't get mistaken for the shell itself. Login
8
+ // shells report a leading dash (e.g. `-zsh`), so strip it first.
9
+ const executable = name.trim().toLowerCase().split('/').pop()?.replace(/^-/, '');
10
+ if (executable === 'zsh')
11
+ return 'zsh';
12
+ if (executable === 'bash')
13
+ return 'bash';
14
+ if (executable === 'fish')
15
+ return 'fish';
16
+ return undefined;
17
+ }
18
+ /**
19
+ * Detect the interactive shell from the parent process.
20
+ *
21
+ * `process.env.SHELL` is only the login shell, so users whose interactive shell
22
+ * differs from it (e.g. running fish while their login shell is bash) are
23
+ * misdetected. Inspecting the parent process reflects the shell that actually
24
+ * launched freespec. POSIX-only and best-effort — any failure returns undefined
25
+ * so the caller falls back to `$SHELL`.
26
+ *
27
+ * @returns The supported shell running as the parent process, or undefined
28
+ */
29
+ function detectShellFromParentProcess() {
30
+ // `ps` is POSIX-only; Windows shells are handled via PSModulePath/COMSPEC.
31
+ if (process.platform === 'win32') {
32
+ return undefined;
33
+ }
34
+ const ppid = process.ppid;
35
+ if (!ppid || ppid <= 1) {
36
+ return undefined;
37
+ }
38
+ try {
39
+ const comm = execFileSync('ps', ['-p', String(ppid), '-o', 'comm='], {
40
+ encoding: 'utf8',
41
+ timeout: 1000,
42
+ }).trim();
43
+ if (!comm) {
44
+ return undefined;
45
+ }
46
+ // Only trust the parent process when it maps to a supported shell; an
47
+ // unrelated parent (node, npm, sudo, a pager) falls through to `$SHELL`.
48
+ return matchSupportedShell(comm);
49
+ }
50
+ catch {
51
+ return undefined;
52
+ }
53
+ }
54
+ /**
55
+ * Detects the current user's shell based on the parent process and environment
56
+ *
57
+ * @returns Detection result with supported shell and raw detected name
58
+ */
59
+ export function detectShell() {
60
+ // Prefer the actual running shell (parent process) over `$SHELL`, which only
61
+ // reflects the login shell and misses users whose interactive shell differs.
62
+ const parentShell = detectShellFromParentProcess();
63
+ if (parentShell) {
64
+ return { shell: parentShell, detected: parentShell };
65
+ }
66
+ // Try SHELL environment variable next (Unix-like systems)
67
+ const shellPath = process.env.SHELL;
68
+ if (shellPath) {
69
+ const supported = matchSupportedShell(shellPath);
70
+ if (supported) {
71
+ return { shell: supported, detected: supported };
72
+ }
73
+ // Shell detected but not supported
74
+ // Extract shell name from path (e.g., /bin/tcsh -> tcsh)
75
+ const match = shellPath.match(/\/([^/]+)$/);
76
+ const detectedName = match ? match[1] : shellPath;
77
+ return { shell: undefined, detected: detectedName };
78
+ }
79
+ // Check for PowerShell on Windows
80
+ // PSModulePath is a reliable PowerShell-specific environment variable
81
+ if (process.env.PSModulePath || process.platform === 'win32') {
82
+ const comspec = process.env.COMSPEC?.toLowerCase();
83
+ // If PSModulePath exists, we're definitely in PowerShell
84
+ if (process.env.PSModulePath) {
85
+ return { shell: 'powershell', detected: 'powershell' };
86
+ }
87
+ // On Windows without PSModulePath, we might be in cmd.exe
88
+ if (comspec?.includes('cmd.exe')) {
89
+ return { shell: undefined, detected: 'cmd.exe' };
90
+ }
91
+ }
92
+ return { shell: undefined, detected: undefined };
93
+ }
94
+ //# sourceMappingURL=shell-detection.js.map