@selesai/code 0.9.15 → 0.10.1

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 (269) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +6 -9
  3. package/dist/defaults/models.json +2 -2
  4. package/dist/defaults/settings.json +3 -7
  5. package/dist/extensions/package.json +2 -4
  6. package/dist/extensions/pi-zentui/.editorconfig +12 -0
  7. package/dist/extensions/pi-zentui/.github/CODEOWNERS +1 -0
  8. package/dist/extensions/pi-zentui/.github/ISSUE_TEMPLATE/bug_report.yml +110 -0
  9. package/dist/extensions/pi-zentui/.github/ISSUE_TEMPLATE/config.yml +8 -0
  10. package/dist/extensions/pi-zentui/.github/ISSUE_TEMPLATE/config_question.yml +56 -0
  11. package/dist/extensions/pi-zentui/.github/ISSUE_TEMPLATE/feature_request.yml +63 -0
  12. package/dist/extensions/pi-zentui/.github/ISSUE_TEMPLATE/rendering_issue.yml +106 -0
  13. package/dist/extensions/pi-zentui/.github/ISSUE_TEMPLATE/runtime_detection.yml +103 -0
  14. package/dist/extensions/pi-zentui/.github/dependabot.yml +47 -0
  15. package/dist/extensions/pi-zentui/.github/pull_request_template.md +34 -0
  16. package/dist/extensions/pi-zentui/.github/release.yml +18 -0
  17. package/dist/extensions/pi-zentui/.github/workflows/ci.yml +79 -0
  18. package/dist/extensions/pi-zentui/.github/workflows/publish.yml +63 -0
  19. package/dist/extensions/pi-zentui/AGENTS.md +107 -0
  20. package/dist/extensions/pi-zentui/CONTRIBUTING.md +68 -0
  21. package/dist/extensions/pi-zentui/LICENSE +21 -0
  22. package/dist/extensions/pi-zentui/README.md +188 -0
  23. package/dist/extensions/pi-zentui/SECURITY.md +18 -0
  24. package/dist/extensions/pi-zentui/assets/main-cover.png +0 -0
  25. package/dist/extensions/pi-zentui/assets/screenshots/editor-accent-rail.png +0 -0
  26. package/dist/extensions/pi-zentui/assets/screenshots/editor-minimalist.png +0 -0
  27. package/dist/extensions/pi-zentui/assets/screenshots/editor-opencode-copy-friendly.png +0 -0
  28. package/dist/extensions/pi-zentui/assets/screenshots/editor-opencode.png +0 -0
  29. package/dist/extensions/pi-zentui/assets/screenshots/user-message-compact.png +0 -0
  30. package/dist/extensions/pi-zentui/assets/screenshots/user-message-framed-copy-friendly.png +0 -0
  31. package/dist/extensions/pi-zentui/assets/screenshots/user-message-framed.png +0 -0
  32. package/dist/extensions/pi-zentui/assets/screenshots/user-message-labeled.png +0 -0
  33. package/dist/extensions/pi-zentui/assets/zentui.png +0 -0
  34. package/dist/extensions/pi-zentui/biome.json +31 -0
  35. package/dist/extensions/pi-zentui/docs/configuration.md +509 -0
  36. package/dist/extensions/pi-zentui/docs/footer-format.md +162 -0
  37. package/dist/extensions/pi-zentui/extensions/zentui/accent-rail-editor.ts +99 -0
  38. package/dist/extensions/pi-zentui/extensions/zentui/accent-rail-layout-patch.ts +457 -0
  39. package/dist/extensions/pi-zentui/extensions/zentui/completion-menu.ts +157 -0
  40. package/dist/extensions/pi-zentui/extensions/zentui/config.ts +2100 -0
  41. package/dist/extensions/pi-zentui/extensions/zentui/editor-metadata-format.ts +321 -0
  42. package/dist/extensions/pi-zentui/extensions/zentui/editor-transfer.ts +73 -0
  43. package/dist/extensions/pi-zentui/extensions/zentui/extension-status.ts +89 -0
  44. package/dist/extensions/pi-zentui/extensions/zentui/footer-format.ts +364 -0
  45. package/dist/extensions/pi-zentui/extensions/zentui/footer-layout.ts +122 -0
  46. package/dist/extensions/pi-zentui/extensions/zentui/footer.ts +812 -0
  47. package/dist/extensions/pi-zentui/extensions/zentui/format.ts +568 -0
  48. package/dist/extensions/pi-zentui/extensions/zentui/git.ts +384 -0
  49. package/dist/extensions/pi-zentui/extensions/zentui/icons.ts +252 -0
  50. package/dist/extensions/pi-zentui/extensions/zentui/index.ts +1406 -0
  51. package/dist/extensions/pi-zentui/extensions/zentui/interaction-summary.ts +1049 -0
  52. package/dist/extensions/pi-zentui/extensions/zentui/live-context.ts +75 -0
  53. package/dist/extensions/pi-zentui/extensions/zentui/minimalist-editor.ts +463 -0
  54. package/dist/extensions/pi-zentui/extensions/zentui/package-version.ts +651 -0
  55. package/dist/extensions/pi-zentui/extensions/zentui/project-refresh.ts +120 -0
  56. package/dist/extensions/pi-zentui/extensions/zentui/project-state.ts +59 -0
  57. package/dist/extensions/pi-zentui/extensions/zentui/prototype-patch-registry.ts +166 -0
  58. package/dist/extensions/pi-zentui/extensions/zentui/repository-root.ts +98 -0
  59. package/dist/extensions/pi-zentui/extensions/zentui/runtime.ts +841 -0
  60. package/dist/extensions/pi-zentui/extensions/zentui/selector-border.ts +93 -0
  61. package/dist/extensions/pi-zentui/extensions/zentui/session-lifecycle.ts +60 -0
  62. package/dist/extensions/pi-zentui/extensions/zentui/settings-command.ts +1796 -0
  63. package/dist/extensions/pi-zentui/extensions/zentui/settings-previews.ts +128 -0
  64. package/dist/extensions/pi-zentui/extensions/zentui/state.ts +94 -0
  65. package/dist/extensions/pi-zentui/extensions/zentui/style.ts +424 -0
  66. package/dist/extensions/pi-zentui/extensions/zentui/telemetry.ts +82 -0
  67. package/dist/extensions/pi-zentui/extensions/zentui/ui.ts +1290 -0
  68. package/dist/extensions/pi-zentui/extensions/zentui/user-message-osc.ts +248 -0
  69. package/dist/extensions/pi-zentui/extensions/zentui/user-message-styles.ts +215 -0
  70. package/dist/extensions/pi-zentui/extensions/zentui/user-message.ts +206 -0
  71. package/dist/extensions/pi-zentui/extensions/zentui/working-line-messages.ts +24 -0
  72. package/dist/extensions/pi-zentui/extensions/zentui/working-line-spinners.ts +54 -0
  73. package/dist/extensions/pi-zentui/extensions/zentui/working-line.ts +1433 -0
  74. package/dist/extensions/pi-zentui/package-lock.json +5311 -0
  75. package/dist/extensions/pi-zentui/package.json +60 -0
  76. package/dist/extensions/pi-zentui/test/accent-rail-editor.test.ts +232 -0
  77. package/dist/extensions/pi-zentui/test/accent-rail-layout-patch.test.ts +669 -0
  78. package/dist/extensions/pi-zentui/test/completion-menu.test.ts +265 -0
  79. package/dist/extensions/pi-zentui/test/config.test.ts +3119 -0
  80. package/dist/extensions/pi-zentui/test/editor-metadata-format.test.ts +405 -0
  81. package/dist/extensions/pi-zentui/test/editor-transfer.test.ts +260 -0
  82. package/dist/extensions/pi-zentui/test/editor-viewport-indicators.test.ts +1286 -0
  83. package/dist/extensions/pi-zentui/test/extension-compliance.test.ts +7196 -0
  84. package/dist/extensions/pi-zentui/test/extension-status.test.ts +182 -0
  85. package/dist/extensions/pi-zentui/test/fixtures/starship-nerd-font-symbols.toml +172 -0
  86. package/dist/extensions/pi-zentui/test/footer-format.test.ts +431 -0
  87. package/dist/extensions/pi-zentui/test/footer-layout.test.ts +113 -0
  88. package/dist/extensions/pi-zentui/test/footer-repository-path.test.ts +123 -0
  89. package/dist/extensions/pi-zentui/test/format.test.ts +865 -0
  90. package/dist/extensions/pi-zentui/test/git.test.ts +212 -0
  91. package/dist/extensions/pi-zentui/test/icons.test.ts +69 -0
  92. package/dist/extensions/pi-zentui/test/interaction-summary.test.ts +1402 -0
  93. package/dist/extensions/pi-zentui/test/live-context-integration.test.ts +378 -0
  94. package/dist/extensions/pi-zentui/test/live-context.test.ts +139 -0
  95. package/dist/extensions/pi-zentui/test/minimalist-editor.test.ts +811 -0
  96. package/dist/extensions/pi-zentui/test/package-version.test.ts +321 -0
  97. package/dist/extensions/pi-zentui/test/project-refresh.test.ts +250 -0
  98. package/dist/extensions/pi-zentui/test/project-state.test.ts +159 -0
  99. package/dist/extensions/pi-zentui/test/repository-root.test.ts +108 -0
  100. package/dist/extensions/pi-zentui/test/responsive-dependencies.test.ts +825 -0
  101. package/dist/extensions/pi-zentui/test/runtime.test.ts +183 -0
  102. package/dist/extensions/pi-zentui/test/session-lifecycle.test.ts +40 -0
  103. package/dist/extensions/pi-zentui/test/settings-command.test.ts +1381 -0
  104. package/dist/extensions/pi-zentui/test/settings-previews.test.ts +347 -0
  105. package/dist/extensions/pi-zentui/test/state.test.ts +84 -0
  106. package/dist/extensions/pi-zentui/test/telemetry-lifecycle-integration.test.ts +276 -0
  107. package/dist/extensions/pi-zentui/test/telemetry.test.ts +105 -0
  108. package/dist/extensions/pi-zentui/test/turn-summary-lifecycle.integration.test.ts +327 -0
  109. package/dist/extensions/pi-zentui/test/user-message-osc.test.ts +164 -0
  110. package/dist/extensions/pi-zentui/test/user-message-styles.test.ts +292 -0
  111. package/dist/extensions/pi-zentui/test/working-line-lifecycle.integration.test.ts +730 -0
  112. package/dist/extensions/pi-zentui/test/working-line-messages.test.ts +34 -0
  113. package/dist/extensions/pi-zentui/test/working-line.test.ts +1990 -0
  114. package/dist/extensions/pi-zentui/tsconfig.json +16 -0
  115. package/dist/extensions/tps.test.ts +38 -0
  116. package/dist/extensions/{pi-powerline-footer/tps.ts → tps.ts} +4 -0
  117. package/dist/modes/interactive/components/settings-selector.d.ts +1 -1
  118. package/dist/modes/interactive/interactive-mode.js +1 -1
  119. package/dist/skills/design-references/SKILL.md +31 -0
  120. package/dist/skills/design-references/references/clickhouse/DESIGN.md +544 -0
  121. package/dist/themes/adventure.json +84 -0
  122. package/dist/themes/adwaita-dark.json +84 -0
  123. package/dist/themes/arcoiris.json +84 -0
  124. package/dist/themes/arthur.json +84 -0
  125. package/dist/themes/atom.json +84 -0
  126. package/dist/themes/aura.json +84 -0
  127. package/dist/themes/black-metal-bathory.json +84 -0
  128. package/dist/themes/black-metal-burzum.json +84 -0
  129. package/dist/themes/black-metal-khold.json +84 -0
  130. package/dist/themes/box.json +84 -0
  131. package/dist/themes/brogrammer.json +84 -0
  132. package/dist/themes/carbonfox.json +84 -0
  133. package/dist/themes/catppuccin-mocha.json +84 -0
  134. package/dist/themes/citruszest.json +84 -0
  135. package/dist/themes/cursor-dark.json +84 -0
  136. package/dist/themes/cutie-pro.json +84 -0
  137. package/dist/themes/dark-modern.json +84 -0
  138. package/dist/themes/dark-pastel.json +84 -0
  139. package/dist/themes/dimmed-monokai.json +84 -0
  140. package/dist/themes/doom-peacock.json +84 -0
  141. package/dist/themes/dracula-plus.json +84 -0
  142. package/dist/themes/dracula.json +80 -0
  143. package/dist/themes/earthsong.json +84 -0
  144. package/dist/themes/everforest-dark-hard.json +84 -0
  145. package/dist/themes/everforest-dark.json +80 -0
  146. package/dist/themes/fahrenheit.json +84 -0
  147. package/dist/themes/flatland.json +84 -0
  148. package/dist/themes/flexoki-dark.json +84 -0
  149. package/dist/themes/front-end-delight.json +84 -0
  150. package/dist/themes/fun-forrest.json +84 -0
  151. package/dist/themes/galizur.json +84 -0
  152. package/dist/themes/github-dark-colorblind.json +84 -0
  153. package/dist/themes/github-dark-high-contrast.json +84 -0
  154. package/dist/themes/glacier.json +84 -0
  155. package/dist/themes/gruber-darker.json +84 -0
  156. package/dist/themes/gruvbox-dark-hard.json +84 -0
  157. package/dist/themes/gruvbox-dark.json +84 -0
  158. package/dist/themes/gruvbox-material.json +84 -0
  159. package/dist/themes/guezwhoz.json +84 -0
  160. package/dist/themes/hacktober.json +84 -0
  161. package/dist/themes/hardcore.json +84 -0
  162. package/dist/themes/havn-skumring.json +84 -0
  163. package/dist/themes/ic-orange-ppl.json +84 -0
  164. package/dist/themes/iterm2-smoooooth.json +84 -0
  165. package/dist/themes/iterm2-tango-dark.json +84 -0
  166. package/dist/themes/japanesque.json +84 -0
  167. package/dist/themes/jellybeans.json +84 -0
  168. package/dist/themes/kanagawa-wave.json +84 -0
  169. package/dist/themes/kurokula.json +84 -0
  170. package/dist/themes/later-this-evening.json +84 -0
  171. package/dist/themes/lovelace.json +84 -0
  172. package/dist/themes/material-darker.json +84 -0
  173. package/dist/themes/matte-black.json +84 -0
  174. package/dist/themes/mellow.json +84 -0
  175. package/dist/themes/miasma.json +84 -0
  176. package/dist/themes/nord.json +80 -0
  177. package/dist/themes/nvim-dark.json +84 -0
  178. package/dist/themes/one-dark.json +80 -0
  179. package/dist/themes/popping-and-locking.json +84 -0
  180. package/dist/themes/rose-pine.json +80 -0
  181. package/dist/themes/sea-shells.json +84 -0
  182. package/dist/themes/sleepy-hollow.json +84 -0
  183. package/dist/themes/smyck.json +84 -0
  184. package/dist/themes/solarized-dark.json +80 -0
  185. package/dist/themes/tokyo-night.json +80 -0
  186. package/dist/themes/tomorrow-night-bright.json +84 -0
  187. package/dist/themes/tomorrow-night-burns.json +84 -0
  188. package/dist/themes/tomorrow-night.json +84 -0
  189. package/dist/themes/twilight.json +84 -0
  190. package/dist/themes/vague.json +84 -0
  191. package/dist/themes/vesper.json +84 -0
  192. package/dist/themes/xcode-dark-hc.json +84 -0
  193. package/docs/custom-provider.md +0 -1
  194. package/package.json +3 -3
  195. package/dist/extensions/auto-model/auto-model.test.ts +0 -18
  196. package/dist/extensions/auto-model/classifier.ts +0 -23
  197. package/dist/extensions/auto-model/config.ts +0 -51
  198. package/dist/extensions/auto-model/index.ts +0 -22
  199. package/dist/extensions/auto-model/lifecycle.test.ts +0 -24
  200. package/dist/extensions/pi-powerline-footer/.github/workflows/test.yml +0 -27
  201. package/dist/extensions/pi-powerline-footer/CHANGELOG.md +0 -687
  202. package/dist/extensions/pi-powerline-footer/CONTRIBUTING.md +0 -16
  203. package/dist/extensions/pi-powerline-footer/README.md +0 -513
  204. package/dist/extensions/pi-powerline-footer/banner.png +0 -0
  205. package/dist/extensions/pi-powerline-footer/bash-mode/completion.ts +0 -586
  206. package/dist/extensions/pi-powerline-footer/bash-mode/editor.ts +0 -583
  207. package/dist/extensions/pi-powerline-footer/bash-mode/history.ts +0 -204
  208. package/dist/extensions/pi-powerline-footer/bash-mode/shell-session.ts +0 -286
  209. package/dist/extensions/pi-powerline-footer/bash-mode/transcript.ts +0 -108
  210. package/dist/extensions/pi-powerline-footer/bash-mode/types.ts +0 -60
  211. package/dist/extensions/pi-powerline-footer/cd-command.ts +0 -190
  212. package/dist/extensions/pi-powerline-footer/colors.ts +0 -70
  213. package/dist/extensions/pi-powerline-footer/context-usage.ts +0 -156
  214. package/dist/extensions/pi-powerline-footer/currency-rates.ts +0 -142
  215. package/dist/extensions/pi-powerline-footer/editor-composition.ts +0 -32
  216. package/dist/extensions/pi-powerline-footer/editor-performance.ts +0 -99
  217. package/dist/extensions/pi-powerline-footer/git-status.ts +0 -329
  218. package/dist/extensions/pi-powerline-footer/guide.ts +0 -220
  219. package/dist/extensions/pi-powerline-footer/icons.ts +0 -190
  220. package/dist/extensions/pi-powerline-footer/index.ts +0 -3428
  221. package/dist/extensions/pi-powerline-footer/lifecycle.ts +0 -10
  222. package/dist/extensions/pi-powerline-footer/package.json +0 -49
  223. package/dist/extensions/pi-powerline-footer/paths.ts +0 -22
  224. package/dist/extensions/pi-powerline-footer/powerline-config.ts +0 -446
  225. package/dist/extensions/pi-powerline-footer/presets.ts +0 -102
  226. package/dist/extensions/pi-powerline-footer/queue/store.ts +0 -313
  227. package/dist/extensions/pi-powerline-footer/queue/types.ts +0 -53
  228. package/dist/extensions/pi-powerline-footer/render-scheduler.ts +0 -29
  229. package/dist/extensions/pi-powerline-footer/segments.ts +0 -570
  230. package/dist/extensions/pi-powerline-footer/separators.ts +0 -57
  231. package/dist/extensions/pi-powerline-footer/session-usage.ts +0 -44
  232. package/dist/extensions/pi-powerline-footer/shortcuts.ts +0 -61
  233. package/dist/extensions/pi-powerline-footer/tests/bash-mode.test.ts +0 -1905
  234. package/dist/extensions/pi-powerline-footer/tests/cd-command.test.ts +0 -144
  235. package/dist/extensions/pi-powerline-footer/tests/context-usage.test.ts +0 -156
  236. package/dist/extensions/pi-powerline-footer/tests/custom-items.test.ts +0 -344
  237. package/dist/extensions/pi-powerline-footer/tests/editor-composition.test.ts +0 -48
  238. package/dist/extensions/pi-powerline-footer/tests/editor-performance.test.ts +0 -43
  239. package/dist/extensions/pi-powerline-footer/tests/editor-responsiveness.test.ts +0 -136
  240. package/dist/extensions/pi-powerline-footer/tests/fixed-editor-border.test.ts +0 -39
  241. package/dist/extensions/pi-powerline-footer/tests/git-optional-locks.test.ts +0 -64
  242. package/dist/extensions/pi-powerline-footer/tests/git-status.test.ts +0 -92
  243. package/dist/extensions/pi-powerline-footer/tests/guide.test.ts +0 -129
  244. package/dist/extensions/pi-powerline-footer/tests/jump-shortcuts.test.ts +0 -116
  245. package/dist/extensions/pi-powerline-footer/tests/mode-aware-trigger.test.ts +0 -79
  246. package/dist/extensions/pi-powerline-footer/tests/paths.test.ts +0 -63
  247. package/dist/extensions/pi-powerline-footer/tests/queue-store.test.ts +0 -65
  248. package/dist/extensions/pi-powerline-footer/tests/quit-cursor.test.ts +0 -14
  249. package/dist/extensions/pi-powerline-footer/tests/remaining-regressions.test.ts +0 -173
  250. package/dist/extensions/pi-powerline-footer/tests/session-usage.test.ts +0 -47
  251. package/dist/extensions/pi-powerline-footer/tests/stash-shortcut.test.ts +0 -37
  252. package/dist/extensions/pi-powerline-footer/tests/thinking-segment.test.ts +0 -76
  253. package/dist/extensions/pi-powerline-footer/tests/token-stats.test.ts +0 -249
  254. package/dist/extensions/pi-powerline-footer/tests/tps.test.ts +0 -39
  255. package/dist/extensions/pi-powerline-footer/tests/usage-display.test.ts +0 -237
  256. package/dist/extensions/pi-powerline-footer/tests/welcome.test.ts +0 -199
  257. package/dist/extensions/pi-powerline-footer/tests/working-vibes.test.ts +0 -317
  258. package/dist/extensions/pi-powerline-footer/theme.example.json +0 -24
  259. package/dist/extensions/pi-powerline-footer/theme.json +0 -12
  260. package/dist/extensions/pi-powerline-footer/theme.ts +0 -241
  261. package/dist/extensions/pi-powerline-footer/token-stats.ts +0 -304
  262. package/dist/extensions/pi-powerline-footer/tsconfig.json +0 -13
  263. package/dist/extensions/pi-powerline-footer/types.ts +0 -235
  264. package/dist/extensions/pi-powerline-footer/welcome-dismiss.ts +0 -34
  265. package/dist/extensions/pi-powerline-footer/welcome.ts +0 -709
  266. package/dist/extensions/pi-powerline-footer/working-vibes.ts +0 -721
  267. package/dist/extensions/preview-tools-disabled.test.ts +0 -37
  268. package/dist/extensions/preview-tools-disabled.ts +0 -11
  269. package/dist/themes/powerline-footer/theme.json +0 -33
@@ -0,0 +1,651 @@
1
+ /**
2
+ * Project package version detection (Starship `package` module parity).
3
+ *
4
+ * Pure file parsing only — no shell-outs, no parent-directory traversal,
5
+ * no render-time reads. The reader operates on the current cwd and the
6
+ * same top-level entries the runtime detector sees, then hands back a
7
+ * silent null when no manifest is present or any parser fails.
8
+ *
9
+ * Supported manifest sources (intersected with runtimes Zentui detects):
10
+ * bun / nodejs → package.json
11
+ * deno → deno.json / deno.jsonc
12
+ * maven → pom.xml
13
+ * gradle → gradle.properties
14
+ * python → pyproject.toml, setup.cfg
15
+ * rust → Cargo.toml (package.version or workspace-inherited)
16
+ * php → composer.json
17
+ * crystal → shard.yml
18
+ * dart → pubspec.yaml, pubspec.yml
19
+ * elixir → mix.exs
20
+ * elm → elm.json
21
+ * fortran → fpm.toml
22
+ * gleam → gleam.toml
23
+ * haskell → *.cabal
24
+ * helm → Chart.yaml
25
+ * julia → Project.toml
26
+ * meson → meson.build
27
+ * nim → *.nimble
28
+ * ruby → *.gemspec
29
+ * vlang → v.mod
30
+ * xmake → xmake.lua
31
+ */
32
+
33
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
34
+ import { join } from "node:path";
35
+
36
+ export type PackageVersionResult = {
37
+ /** Starship ecosystem key — matches `runtimeMetadata[].name` where it exists. */
38
+ ecosystem: string;
39
+ /** Raw version string as authored in the manifest. */
40
+ version: string;
41
+ };
42
+
43
+ export type PackageVersionReadResult =
44
+ | { kind: "ok"; result: PackageVersionResult | null }
45
+ | { kind: "error" };
46
+
47
+ /**
48
+ * A single manifest source. `kind` selects the lookup mode:
49
+ * "file" — try each of `files` exactly (cheap, no scan).
50
+ * "extensions" — pick the first entry in `cwd` whose extension matches one in `files`.
51
+ * `parse` receives the raw file text and either returns a cleaned version
52
+ * string or `undefined`. Parsers must be total (never throw); any
53
+ * failure means `undefined`.
54
+ */
55
+ type ManifestSource = {
56
+ files: readonly string[];
57
+ parse: (raw: string) => string | undefined;
58
+ kind?: "file" | "extensions";
59
+ };
60
+
61
+ export const PACKAGE_VERSION_ECOSYSTEMS = [
62
+ "bun",
63
+ "nodejs",
64
+ "deno",
65
+ "maven",
66
+ "gradle",
67
+ "python",
68
+ "rust",
69
+ "php",
70
+ "crystal",
71
+ "dart",
72
+ "elixir",
73
+ "elm",
74
+ "fortran",
75
+ "gleam",
76
+ "haskell",
77
+ "helm",
78
+ "julia",
79
+ "meson",
80
+ "nim",
81
+ "ruby",
82
+ "vlang",
83
+ "xmake",
84
+ ] as const;
85
+
86
+ // --- String cleaning -----------------------------------------------------
87
+
88
+ /**
89
+ * Strip surrounding quotes / leading `v` and trim trailing metadata.
90
+ * Empty / whitespace-only values return `undefined`.
91
+ */
92
+ function cleanVersion(value: string | undefined): string | undefined {
93
+ if (!value || /[\u0000-\u001f\u007f-\u009f]/.test(value)) return undefined;
94
+ let text = value.trim();
95
+ if (!text) return undefined;
96
+
97
+ if (text.length >= 2) {
98
+ const first = text[0];
99
+ const last = text[text.length - 1];
100
+ if ((first === '"' && last === '"') || (first === "'" && last === "'")) {
101
+ text = text.slice(1, -1);
102
+ }
103
+ }
104
+
105
+ while (text.startsWith("v") || text.startsWith("V")) {
106
+ // Only strip `v` when the next char is a digit — `v3.2.1` → `3.2.1`,
107
+ // but a bare word like `via` is left alone.
108
+ const next = text[1];
109
+ if (!next || next < "0" || next > "9") break;
110
+ text = text.slice(1);
111
+ }
112
+
113
+ text = text.trim();
114
+ if (!text) return undefined;
115
+
116
+ // Reject embedded whitespace and manifest syntax junk. Terminal controls
117
+ // were rejected before normalization so trimming cannot hide them.
118
+ if (/\s/.test(text)) return undefined;
119
+ if (/[{}\\<>]/.test(text)) return undefined;
120
+
121
+ return text;
122
+ }
123
+
124
+ // --- JSON helpers --------------------------------------------------------
125
+
126
+ function safeJsonParse(raw: string): unknown {
127
+ try {
128
+ return JSON.parse(raw);
129
+ } catch {
130
+ return undefined;
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Strip `//` and `/* * /` comments so JSON-with-comments can be parsed by
136
+ * `JSON.parse`. Quoted strings are preserved. Only used for `.jsonc`.
137
+ */
138
+ function stripJsonComments(raw: string): string {
139
+ let result = "";
140
+ let inString = false;
141
+ let stringQuote = "";
142
+ let inBlockComment = false;
143
+ let inLineComment = false;
144
+ for (let i = 0; i < raw.length; i++) {
145
+ const ch = raw[i];
146
+ const next = raw[i + 1];
147
+ if (inLineComment) {
148
+ if (ch === "\n") {
149
+ inLineComment = false;
150
+ result += "\n";
151
+ }
152
+ continue;
153
+ }
154
+ if (inBlockComment) {
155
+ if (ch === "*" && next === "/") {
156
+ inBlockComment = false;
157
+ i += 1;
158
+ }
159
+ continue;
160
+ }
161
+ if (inString) {
162
+ result += ch;
163
+ if (ch === "\\" && next !== undefined) {
164
+ result += next;
165
+ i += 1;
166
+ } else if (ch === stringQuote) {
167
+ inString = false;
168
+ }
169
+ continue;
170
+ }
171
+ if (ch === "/" && next === "/") {
172
+ inLineComment = true;
173
+ i += 1;
174
+ continue;
175
+ }
176
+ if (ch === "/" && next === "*") {
177
+ inBlockComment = true;
178
+ i += 1;
179
+ continue;
180
+ }
181
+ if (ch === '"' || ch === "'") {
182
+ inString = true;
183
+ stringQuote = ch;
184
+ result += ch;
185
+ continue;
186
+ }
187
+ result += ch;
188
+ }
189
+ return result;
190
+ }
191
+
192
+ function safeJsoncParse(raw: string): unknown {
193
+ try {
194
+ return JSON.parse(stripJsonComments(raw));
195
+ } catch {
196
+ return undefined;
197
+ }
198
+ }
199
+
200
+ function readJsonVersionField(raw: string, key: string): string | undefined {
201
+ const parsed = safeJsonParse(raw);
202
+ if (!parsed || typeof parsed !== "object") return undefined;
203
+ const value = (parsed as Record<string, unknown>)[key];
204
+ return typeof value === "string" ? value : undefined;
205
+ }
206
+
207
+ function readJsoncVersionField(raw: string, key: string): string | undefined {
208
+ const parsed = safeJsoncParse(raw);
209
+ if (!parsed || typeof parsed !== "object") return undefined;
210
+ const value = (parsed as Record<string, unknown>)[key];
211
+ return typeof value === "string" ? value : undefined;
212
+ }
213
+
214
+ // --- TOML helpers --------------------------------------------------------
215
+
216
+ /**
217
+ * Parse only the static `key = "value"` (or `key = 'value'`) assignment of a
218
+ * simple TOML section header (e.g. `[project]`, `[tool.poetry]`, `[workspace]`).
219
+ * Sufficient for the manifest keys we care about; inline tables and
220
+ * multi-line arrays fall back to `undefined`.
221
+ *
222
+ * Sections are matched as a leading `[name]` (or `[[name]]`) header; key
223
+ * matches continue until the next section header or end of file.
224
+ */
225
+ function readTomlSectionKeyValue(raw: string, section: string, key: string): string | undefined {
226
+ const lines = raw.split(/\r?\n/);
227
+ let inTargetSection = false;
228
+
229
+ for (const line of lines) {
230
+ const trimmed = line.trim();
231
+ if (!trimmed || trimmed.startsWith("#")) continue;
232
+
233
+ const headerMatch = trimmed.match(/^\[\[?([^\]]+)\]?\]\s*$/);
234
+ if (headerMatch) {
235
+ const name = (headerMatch[1] ?? "").trim();
236
+ inTargetSection = name === section;
237
+ continue;
238
+ }
239
+
240
+ if (!inTargetSection) continue;
241
+
242
+ const match = trimmed.match(/^([A-Za-z0-9_-]+)\s*=\s*(.*)$/);
243
+ if (!match) continue;
244
+ const candidateKey = match[1] ?? "";
245
+ if (candidateKey !== key) continue;
246
+
247
+ let value = (match[2] ?? "").trim();
248
+ const trailingComment = value.indexOf("#");
249
+ if (trailingComment >= 0 && !isInsideString(value, trailingComment)) {
250
+ value = value.slice(0, trailingComment).trim();
251
+ }
252
+ return value || undefined;
253
+ }
254
+
255
+ return undefined;
256
+ }
257
+
258
+ /** A key-value pair at the top of a TOML file, before any section header. */
259
+ function readTopLevelTomlValue(raw: string, key: string): string | undefined {
260
+ const lines = raw.split(/\r?\n/);
261
+ for (const line of lines) {
262
+ const trimmed = line.trim();
263
+ if (!trimmed || trimmed.startsWith("#")) continue;
264
+ if (trimmed.startsWith("[")) return undefined; // entered a section
265
+ const match = trimmed.match(/^([A-Za-z0-9_-]+)\s*=\s*(.*?)\s*$/);
266
+ if (!match) continue;
267
+ const candidateKey = match[1] ?? "";
268
+ if (candidateKey !== key) continue;
269
+ return trimQuotes((match[2] ?? "").trim());
270
+ }
271
+ return undefined;
272
+ }
273
+
274
+ function trimQuotes(value: string): string {
275
+ if (value.length < 2) return value;
276
+ const first = value[0];
277
+ const last = value[value.length - 1];
278
+ if ((first === '"' && last === '"') || (first === "'" && last === "'")) {
279
+ return value.slice(1, -1);
280
+ }
281
+ return value;
282
+ }
283
+
284
+ function isInsideString(value: string, index: number): boolean {
285
+ let inSingle = false;
286
+ let inDouble = false;
287
+ for (let i = 0; i < index; i++) {
288
+ const ch = value[i];
289
+ if (ch === "\\") {
290
+ i += 1;
291
+ continue;
292
+ }
293
+ if (!inDouble && ch === "'") inSingle = !inSingle;
294
+ else if (!inSingle && ch === '"') inDouble = !inDouble;
295
+ }
296
+ return inSingle || inDouble;
297
+ }
298
+
299
+ // --- YAML helper (line-oriented; supports only top-level `key:`) ---------
300
+
301
+ function readYamlTopLevelValue(raw: string, key: string): string | undefined {
302
+ const lines = raw.split(/\r?\n/);
303
+ const target = `${key}:`;
304
+ for (const line of lines) {
305
+ if (line.startsWith(" ") || line.startsWith("\t")) continue;
306
+ if (!line.startsWith(target)) continue;
307
+ const value = line.slice(target.length).trim();
308
+ return value || undefined;
309
+ }
310
+ return undefined;
311
+ }
312
+
313
+ // --- Per-ecosystem parsers -----------------------------------------------
314
+
315
+ /**
316
+ * Locate a `package` stanza in a `.cabal` file and extract its `version`
317
+ * field. Modern cabal files (>= 1.12) drop the explicit `package`
318
+ * keyword; the file itself IS the package stanza, with `library`,
319
+ * `executable`, `test-suite`, `common`, etc. stanzas delimited by
320
+ * column-0 openers. We accept both indented (` version: …`) and
321
+ * top-level (`version: …`) `version:` lines up to the first stanza opener.
322
+ */
323
+ function extractCabalPackageVersion(raw: string): string | undefined {
324
+ const lines = raw.split(/\r?\n/);
325
+ let packageStart = 0;
326
+ for (let i = 0; i < lines.length; i++) {
327
+ const line = lines[i] ?? "";
328
+ if (/^package\s*$/i.test(line.trim())) {
329
+ packageStart = i + 1;
330
+ break;
331
+ }
332
+ }
333
+
334
+ for (let i = packageStart; i < lines.length; i++) {
335
+ const line = lines[i] ?? "";
336
+ if (line.trim() === "") continue;
337
+ if (line.trim().startsWith("--")) continue;
338
+ // A non-indented, non-empty line closes the package stanza.
339
+ if (!/^\s/.test(line)) break;
340
+ const match = line.match(/^\s*version\s*:\s*(.+?)\s*$/i);
341
+ if (match) return cleanVersion(match[1]);
342
+ }
343
+
344
+ // Legacy form: `version:` lives at column 0 alongside `name:`, before
345
+ // any stanza opener (e.g. `library`).
346
+ for (let i = packageStart; i < lines.length; i++) {
347
+ const line = lines[i] ?? "";
348
+ const trimmed = line.trim();
349
+ if (trimmed === "" || trimmed.startsWith("--")) continue;
350
+ // First non-keyword, non-empty line ends the package section.
351
+ if (!/^[a-zA-Z][\w-]*\s*:/.test(trimmed)) break;
352
+ const match = trimmed.match(/^version\s*:\s*(.+?)\s*$/i);
353
+ if (match) return cleanVersion(match[1]);
354
+ }
355
+
356
+ return undefined;
357
+ }
358
+
359
+ const parsers: readonly ManifestSource[] = [
360
+ // npm / bun — package.json
361
+ {
362
+ files: ["package.json"],
363
+ parse: (raw) => cleanVersion(readJsonVersionField(raw, "version")),
364
+ },
365
+ // deno — deno.json / deno.jsonc
366
+ {
367
+ files: ["deno.json", "deno.jsonc"],
368
+ parse: (raw) => cleanVersion(readJsoncVersionField(raw, "version")),
369
+ },
370
+ // maven — pom.xml: project/version (direct child, simple case).
371
+ {
372
+ files: ["pom.xml"],
373
+ parse: (raw) => {
374
+ const match = raw.match(/<project\b[^>]*>[\s\S]*?<version>\s*([^<]+?)\s*<\/version>/i);
375
+ return cleanVersion(match?.[1]);
376
+ },
377
+ },
378
+ // gradle — gradle.properties key=value
379
+ {
380
+ files: ["gradle.properties"],
381
+ parse: (raw) => {
382
+ for (const line of raw.split(/\r?\n/)) {
383
+ const match = line.match(/^\s*version\s*[:=]\s*(.+?)\s*$/i);
384
+ if (match) return cleanVersion(match[1]);
385
+ }
386
+ return undefined;
387
+ },
388
+ },
389
+ // python — pyproject.toml ([project].version or [tool.poetry].version)
390
+ {
391
+ files: ["pyproject.toml"],
392
+ parse: (raw) => {
393
+ const project = cleanVersion(readTomlSectionKeyValue(raw, "project", "version"));
394
+ if (project) return project;
395
+ return cleanVersion(readTomlSectionKeyValue(raw, "tool.poetry", "version"));
396
+ },
397
+ },
398
+ // python — setup.cfg ([metadata].version)
399
+ {
400
+ files: ["setup.cfg"],
401
+ parse: (raw) => cleanVersion(readTomlSectionKeyValue(raw, "metadata", "version")),
402
+ },
403
+ // rust — Cargo.toml ([package].version, with workspace inheritance)
404
+ {
405
+ files: ["Cargo.toml"],
406
+ parse: (raw) => {
407
+ // Direct assignment wins (string literal version).
408
+ const direct = readTomlSectionKeyValue(raw, "package", "version");
409
+ const directClean = cleanVersion(direct);
410
+ if (directClean && !/^\s*version\s*\.\s*workspace\s*=\s*true/i.test(direct ?? "")) {
411
+ return directClean;
412
+ }
413
+
414
+ // Workspace inheritance: package declares `version.workspace = true`
415
+ // and the [workspace] block carries the actual version.
416
+ if (!/\bversion\s*\.\s*workspace\s*=\s*true/i.test(raw)) return undefined;
417
+ const workspaceMatch = raw.match(/\[workspace\][\s\S]*?version\s*=\s*["']([^"']+)["']/i);
418
+ return cleanVersion(workspaceMatch?.[1]);
419
+ },
420
+ },
421
+ // php — composer.json
422
+ {
423
+ files: ["composer.json"],
424
+ parse: (raw) => cleanVersion(readJsonVersionField(raw, "version")),
425
+ },
426
+ // crystal — shard.yml (top-level `version: …`)
427
+ {
428
+ files: ["shard.yml"],
429
+ parse: (raw) => cleanVersion(readYamlTopLevelValue(raw, "version")),
430
+ },
431
+ // dart — pubspec.yaml / pubspec.yml (top-level `version: …`)
432
+ {
433
+ files: ["pubspec.yaml", "pubspec.yml"],
434
+ parse: (raw) => cleanVersion(readYamlTopLevelValue(raw, "version")),
435
+ },
436
+ // elixir — mix.exs: `version: "…"` as a Mix.Project option (string literal form).
437
+ {
438
+ files: ["mix.exs"],
439
+ parse: (raw) => {
440
+ const match = raw.match(/(?:^|\n)\s*version\s*:\s*["']([^"']+)["']/);
441
+ return cleanVersion(match?.[1]);
442
+ },
443
+ },
444
+ // elm — elm.json: top-level `version` (only present in 0.19.0 application projects).
445
+ {
446
+ files: ["elm.json"],
447
+ parse: (raw) => cleanVersion(readJsonVersionField(raw, "version")),
448
+ },
449
+ // fortran — fpm.toml: [package] or [project] version.
450
+ {
451
+ files: ["fpm.toml"],
452
+ parse: (raw) => {
453
+ const fromPackage = cleanVersion(readTomlSectionKeyValue(raw, "package", "version"));
454
+ if (fromPackage) return fromPackage;
455
+ return cleanVersion(readTomlSectionKeyValue(raw, "project", "version"));
456
+ },
457
+ },
458
+ // gleam — gleam.toml top-level version
459
+ {
460
+ files: ["gleam.toml"],
461
+ parse: (raw) => cleanVersion(readTopLevelTomlValue(raw, "version")),
462
+ },
463
+ // haskell — *.cabal: `version:` inside a `package` stanza.
464
+ {
465
+ files: [".cabal"],
466
+ kind: "extensions",
467
+ parse: (raw) => extractCabalPackageVersion(raw),
468
+ },
469
+ // helm — Chart.yaml: top-level `version:` (chart version, distinct from appVersion)
470
+ {
471
+ files: ["Chart.yaml"],
472
+ parse: (raw) => cleanVersion(readYamlTopLevelValue(raw, "version")),
473
+ },
474
+ // julia — Project.toml: top-level `version = "…"`
475
+ {
476
+ files: ["Project.toml"],
477
+ parse: (raw) => cleanVersion(readTopLevelTomlValue(raw, "version")),
478
+ },
479
+ // meson — meson.build: static `project(..., version: '…')` form only.
480
+ {
481
+ files: ["meson.build"],
482
+ parse: (raw) => {
483
+ const match = raw.match(/\bproject\s*\([^)]*?\bversion\s*:\s*['"]([^'"]+)['"]/);
484
+ return cleanVersion(match?.[1]);
485
+ },
486
+ },
487
+ // nim — *.nimble: top-level `version = "…"` (ignore indented blocks).
488
+ {
489
+ files: [".nimble"],
490
+ kind: "extensions",
491
+ parse: (raw) => cleanVersion(readTopLevelTomlValue(raw, "version")),
492
+ },
493
+ // ruby — *.gemspec: `spec.version = "…"` static literal.
494
+ {
495
+ files: [".gemspec"],
496
+ kind: "extensions",
497
+ parse: (raw) => {
498
+ const quoted = raw.match(/\b(?:spec\.|\.)?version\s*=\s*["']([^"']+)["']/);
499
+ if (quoted) return cleanVersion(quoted[1]);
500
+ return undefined;
501
+ },
502
+ },
503
+ // vlang — v.mod: `version: '…'` after the module block.
504
+ {
505
+ files: ["v.mod"],
506
+ parse: (raw) => {
507
+ const match = raw.match(/\bversion\s*:\s*['"]([^'']+)['"]/);
508
+ if (match) return cleanVersion(match[1]);
509
+ const numeric = raw.match(/\bversion\s*:\s*([0-9][^\s,]+)/);
510
+ return cleanVersion(numeric?.[1]);
511
+ },
512
+ },
513
+ // xmake — xmake.lua: static `set_version("…")` form only.
514
+ {
515
+ files: ["xmake.lua"],
516
+ parse: (raw) => {
517
+ const match = raw.match(/^\s*set_version\s*\(\s*["']([^"']+)["']\s*\)/m);
518
+ return cleanVersion(match?.[1]);
519
+ },
520
+ },
521
+ ];
522
+
523
+ // --- File lookup ---------------------------------------------------------
524
+
525
+ function ecosystemFor(file: string): string {
526
+ const lower = file.toLowerCase();
527
+ if (lower === "package.json") return "nodejs";
528
+ if (lower === "pom.xml") return "maven";
529
+ if (lower === "gradle.properties") return "gradle";
530
+ if (lower === "deno.json" || lower === "deno.jsonc") return "deno";
531
+ if (lower === "pyproject.toml") return "python";
532
+ if (lower === "setup.cfg") return "python";
533
+ if (lower === "cargo.toml") return "rust";
534
+ if (lower === "composer.json") return "php";
535
+ if (lower === "shard.yml") return "crystal";
536
+ if (lower === "pubspec.yaml" || lower === "pubspec.yml") return "dart";
537
+ if (lower === "mix.exs") return "elixir";
538
+ if (lower === "elm.json") return "elm";
539
+ if (lower === "fpm.toml") return "fortran";
540
+ if (lower === "gleam.toml") return "gleam";
541
+ if (lower.endsWith(".cabal")) return "haskell";
542
+ if (lower === "chart.yaml") return "helm";
543
+ if (lower === "project.toml") return "julia";
544
+ if (lower === "meson.build") return "meson";
545
+ if (lower.endsWith(".nimble")) return "nim";
546
+ if (lower.endsWith(".gemspec")) return "ruby";
547
+ if (lower === "v.mod") return "vlang";
548
+ if (lower === "xmake.lua") return "xmake";
549
+ return "unknown";
550
+ }
551
+
552
+ function safeReadFile(path: string): string | undefined {
553
+ try {
554
+ return readFileSync(path, "utf8");
555
+ } catch {
556
+ return undefined;
557
+ }
558
+ }
559
+
560
+ /**
561
+ * Test if a `cwd` entry basename ends with one of the given extensions.
562
+ * `extensions` should include the leading dot.
563
+ */
564
+ function hasAnyExtension(name: string, extensions: readonly string[]): boolean {
565
+ const lower = name.toLowerCase();
566
+ for (const ext of extensions) {
567
+ if (lower.endsWith(ext)) return true;
568
+ }
569
+ return false;
570
+ }
571
+
572
+ export function readPackageVersion(cwd: string): PackageVersionResult | null {
573
+ // File-mode readers: cheap `existsSync` checks in deterministic order.
574
+ for (let i = 0; i < parsers.length; i++) {
575
+ const source = parsers[i];
576
+ if (!source || source.kind === "extensions") continue;
577
+ for (const file of source.files) {
578
+ const full = join(cwd, file);
579
+ if (!existsSync(full)) continue;
580
+ const raw = safeReadFile(full);
581
+ if (raw === undefined) continue;
582
+ let version: string | undefined;
583
+ try {
584
+ version = source.parse(raw);
585
+ } catch {
586
+ continue;
587
+ }
588
+ if (version) return { ecosystem: ecosystemFor(file), version };
589
+ }
590
+ }
591
+
592
+ // Extension-mode readers: one readdir, then try each candidate against
593
+ // its parser. The reading order matches `parsers` declaration order so
594
+ // Haskell (`*.cabal`) is preferred over Ruby (`*.gemspec`) when both
595
+ // exist (very rare; deterministic and documented).
596
+ let entries: readonly string[];
597
+ try {
598
+ entries = readdirSync(cwd);
599
+ } catch {
600
+ return null;
601
+ }
602
+
603
+ for (let i = 0; i < parsers.length; i++) {
604
+ const source = parsers[i];
605
+ if (source?.kind !== "extensions") continue;
606
+ for (const entry of entries) {
607
+ if (!hasAnyExtension(entry, source.files)) continue;
608
+ const full = join(cwd, entry);
609
+ const raw = safeReadFile(full);
610
+ if (raw === undefined) continue;
611
+ let version: string | undefined;
612
+ try {
613
+ version = source.parse(raw);
614
+ } catch {
615
+ continue;
616
+ }
617
+ if (version) return { ecosystem: ecosystemFor(entry), version };
618
+ }
619
+ }
620
+
621
+ return null;
622
+ }
623
+
624
+ /**
625
+ * Async wrapper for use from the project-refresh path. `null` results from
626
+ * `readPackageVersion` (no manifest present) are surfaced as `ok` with a
627
+ * null result so the caller can distinguish "no manifest in this cwd"
628
+ * (clear state) from "could not read cwd" / parse failure (error, keep
629
+ * last-good). The synchronous reader never throws on its own; we still
630
+ * wrap defensively so callers using this through a scheduler cannot be
631
+ * broken by future filesystem exceptions.
632
+ */
633
+ export async function readPackageVersionResult(cwd: string): Promise<PackageVersionReadResult> {
634
+ try {
635
+ return { kind: "ok", result: readPackageVersion(cwd) };
636
+ } catch {
637
+ return { kind: "error" };
638
+ }
639
+ }
640
+
641
+ /**
642
+ * Exposed for testing — the parser table and the `cleanVersion` helper.
643
+ * Lets unit tests cover every ecosystem and edge case without writing
644
+ * temporary filesystem fixtures.
645
+ */
646
+ export const __test__ = {
647
+ parsers,
648
+ cleanVersion,
649
+ ecosystemFor,
650
+ hasAnyExtension,
651
+ };