@selesai/code 0.9.14 → 0.10.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 (263) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +6 -9
  3. package/dist/defaults/models.json +79 -29
  4. package/dist/defaults/settings.json +3 -7
  5. package/dist/extensions/package.json +3 -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/skills/design-references/SKILL.md +31 -0
  118. package/dist/skills/design-references/references/clickhouse/DESIGN.md +544 -0
  119. package/dist/themes/adventure.json +84 -0
  120. package/dist/themes/adwaita-dark.json +84 -0
  121. package/dist/themes/arcoiris.json +84 -0
  122. package/dist/themes/arthur.json +84 -0
  123. package/dist/themes/atom.json +84 -0
  124. package/dist/themes/aura.json +84 -0
  125. package/dist/themes/black-metal-bathory.json +84 -0
  126. package/dist/themes/black-metal-burzum.json +84 -0
  127. package/dist/themes/black-metal-khold.json +84 -0
  128. package/dist/themes/box.json +84 -0
  129. package/dist/themes/brogrammer.json +84 -0
  130. package/dist/themes/carbonfox.json +84 -0
  131. package/dist/themes/catppuccin-mocha.json +84 -0
  132. package/dist/themes/citruszest.json +84 -0
  133. package/dist/themes/cursor-dark.json +84 -0
  134. package/dist/themes/cutie-pro.json +84 -0
  135. package/dist/themes/dark-modern.json +84 -0
  136. package/dist/themes/dark-pastel.json +84 -0
  137. package/dist/themes/dimmed-monokai.json +84 -0
  138. package/dist/themes/doom-peacock.json +84 -0
  139. package/dist/themes/dracula-plus.json +84 -0
  140. package/dist/themes/dracula.json +80 -0
  141. package/dist/themes/earthsong.json +84 -0
  142. package/dist/themes/everforest-dark-hard.json +84 -0
  143. package/dist/themes/everforest-dark.json +80 -0
  144. package/dist/themes/fahrenheit.json +84 -0
  145. package/dist/themes/flatland.json +84 -0
  146. package/dist/themes/flexoki-dark.json +84 -0
  147. package/dist/themes/front-end-delight.json +84 -0
  148. package/dist/themes/fun-forrest.json +84 -0
  149. package/dist/themes/galizur.json +84 -0
  150. package/dist/themes/github-dark-colorblind.json +84 -0
  151. package/dist/themes/github-dark-high-contrast.json +84 -0
  152. package/dist/themes/glacier.json +84 -0
  153. package/dist/themes/gruber-darker.json +84 -0
  154. package/dist/themes/gruvbox-dark-hard.json +84 -0
  155. package/dist/themes/gruvbox-dark.json +84 -0
  156. package/dist/themes/gruvbox-material.json +84 -0
  157. package/dist/themes/guezwhoz.json +84 -0
  158. package/dist/themes/hacktober.json +84 -0
  159. package/dist/themes/hardcore.json +84 -0
  160. package/dist/themes/havn-skumring.json +84 -0
  161. package/dist/themes/ic-orange-ppl.json +84 -0
  162. package/dist/themes/iterm2-smoooooth.json +84 -0
  163. package/dist/themes/iterm2-tango-dark.json +84 -0
  164. package/dist/themes/japanesque.json +84 -0
  165. package/dist/themes/jellybeans.json +84 -0
  166. package/dist/themes/kanagawa-wave.json +84 -0
  167. package/dist/themes/kurokula.json +84 -0
  168. package/dist/themes/later-this-evening.json +84 -0
  169. package/dist/themes/lovelace.json +84 -0
  170. package/dist/themes/material-darker.json +84 -0
  171. package/dist/themes/matte-black.json +84 -0
  172. package/dist/themes/mellow.json +84 -0
  173. package/dist/themes/miasma.json +84 -0
  174. package/dist/themes/nord.json +80 -0
  175. package/dist/themes/nvim-dark.json +84 -0
  176. package/dist/themes/one-dark.json +80 -0
  177. package/dist/themes/popping-and-locking.json +84 -0
  178. package/dist/themes/rose-pine.json +80 -0
  179. package/dist/themes/sea-shells.json +84 -0
  180. package/dist/themes/sleepy-hollow.json +84 -0
  181. package/dist/themes/smyck.json +84 -0
  182. package/dist/themes/solarized-dark.json +80 -0
  183. package/dist/themes/tokyo-night.json +80 -0
  184. package/dist/themes/tomorrow-night-bright.json +84 -0
  185. package/dist/themes/tomorrow-night-burns.json +84 -0
  186. package/dist/themes/tomorrow-night.json +84 -0
  187. package/dist/themes/twilight.json +84 -0
  188. package/dist/themes/vague.json +84 -0
  189. package/dist/themes/vesper.json +84 -0
  190. package/dist/themes/xcode-dark-hc.json +84 -0
  191. package/docs/custom-provider.md +0 -1
  192. package/docs/plans/auto-model-routing-extension.md +490 -0
  193. package/package.json +3 -3
  194. package/dist/extensions/pi-powerline-footer/.github/workflows/test.yml +0 -27
  195. package/dist/extensions/pi-powerline-footer/CHANGELOG.md +0 -687
  196. package/dist/extensions/pi-powerline-footer/CONTRIBUTING.md +0 -16
  197. package/dist/extensions/pi-powerline-footer/README.md +0 -513
  198. package/dist/extensions/pi-powerline-footer/banner.png +0 -0
  199. package/dist/extensions/pi-powerline-footer/bash-mode/completion.ts +0 -586
  200. package/dist/extensions/pi-powerline-footer/bash-mode/editor.ts +0 -583
  201. package/dist/extensions/pi-powerline-footer/bash-mode/history.ts +0 -204
  202. package/dist/extensions/pi-powerline-footer/bash-mode/shell-session.ts +0 -286
  203. package/dist/extensions/pi-powerline-footer/bash-mode/transcript.ts +0 -108
  204. package/dist/extensions/pi-powerline-footer/bash-mode/types.ts +0 -60
  205. package/dist/extensions/pi-powerline-footer/cd-command.ts +0 -190
  206. package/dist/extensions/pi-powerline-footer/colors.ts +0 -70
  207. package/dist/extensions/pi-powerline-footer/context-usage.ts +0 -156
  208. package/dist/extensions/pi-powerline-footer/currency-rates.ts +0 -142
  209. package/dist/extensions/pi-powerline-footer/editor-composition.ts +0 -32
  210. package/dist/extensions/pi-powerline-footer/editor-performance.ts +0 -99
  211. package/dist/extensions/pi-powerline-footer/git-status.ts +0 -329
  212. package/dist/extensions/pi-powerline-footer/guide.ts +0 -220
  213. package/dist/extensions/pi-powerline-footer/icons.ts +0 -190
  214. package/dist/extensions/pi-powerline-footer/index.ts +0 -3428
  215. package/dist/extensions/pi-powerline-footer/lifecycle.ts +0 -10
  216. package/dist/extensions/pi-powerline-footer/package.json +0 -49
  217. package/dist/extensions/pi-powerline-footer/paths.ts +0 -22
  218. package/dist/extensions/pi-powerline-footer/powerline-config.ts +0 -446
  219. package/dist/extensions/pi-powerline-footer/presets.ts +0 -102
  220. package/dist/extensions/pi-powerline-footer/queue/store.ts +0 -313
  221. package/dist/extensions/pi-powerline-footer/queue/types.ts +0 -53
  222. package/dist/extensions/pi-powerline-footer/render-scheduler.ts +0 -29
  223. package/dist/extensions/pi-powerline-footer/segments.ts +0 -570
  224. package/dist/extensions/pi-powerline-footer/separators.ts +0 -57
  225. package/dist/extensions/pi-powerline-footer/session-usage.ts +0 -44
  226. package/dist/extensions/pi-powerline-footer/shortcuts.ts +0 -61
  227. package/dist/extensions/pi-powerline-footer/tests/bash-mode.test.ts +0 -1905
  228. package/dist/extensions/pi-powerline-footer/tests/cd-command.test.ts +0 -144
  229. package/dist/extensions/pi-powerline-footer/tests/context-usage.test.ts +0 -156
  230. package/dist/extensions/pi-powerline-footer/tests/custom-items.test.ts +0 -344
  231. package/dist/extensions/pi-powerline-footer/tests/editor-composition.test.ts +0 -48
  232. package/dist/extensions/pi-powerline-footer/tests/editor-performance.test.ts +0 -43
  233. package/dist/extensions/pi-powerline-footer/tests/editor-responsiveness.test.ts +0 -136
  234. package/dist/extensions/pi-powerline-footer/tests/fixed-editor-border.test.ts +0 -39
  235. package/dist/extensions/pi-powerline-footer/tests/git-optional-locks.test.ts +0 -64
  236. package/dist/extensions/pi-powerline-footer/tests/git-status.test.ts +0 -92
  237. package/dist/extensions/pi-powerline-footer/tests/guide.test.ts +0 -129
  238. package/dist/extensions/pi-powerline-footer/tests/jump-shortcuts.test.ts +0 -116
  239. package/dist/extensions/pi-powerline-footer/tests/mode-aware-trigger.test.ts +0 -79
  240. package/dist/extensions/pi-powerline-footer/tests/paths.test.ts +0 -63
  241. package/dist/extensions/pi-powerline-footer/tests/queue-store.test.ts +0 -65
  242. package/dist/extensions/pi-powerline-footer/tests/quit-cursor.test.ts +0 -14
  243. package/dist/extensions/pi-powerline-footer/tests/remaining-regressions.test.ts +0 -173
  244. package/dist/extensions/pi-powerline-footer/tests/session-usage.test.ts +0 -47
  245. package/dist/extensions/pi-powerline-footer/tests/stash-shortcut.test.ts +0 -37
  246. package/dist/extensions/pi-powerline-footer/tests/thinking-segment.test.ts +0 -76
  247. package/dist/extensions/pi-powerline-footer/tests/token-stats.test.ts +0 -249
  248. package/dist/extensions/pi-powerline-footer/tests/tps.test.ts +0 -39
  249. package/dist/extensions/pi-powerline-footer/tests/usage-display.test.ts +0 -237
  250. package/dist/extensions/pi-powerline-footer/tests/welcome.test.ts +0 -199
  251. package/dist/extensions/pi-powerline-footer/tests/working-vibes.test.ts +0 -317
  252. package/dist/extensions/pi-powerline-footer/theme.example.json +0 -24
  253. package/dist/extensions/pi-powerline-footer/theme.json +0 -12
  254. package/dist/extensions/pi-powerline-footer/theme.ts +0 -241
  255. package/dist/extensions/pi-powerline-footer/token-stats.ts +0 -304
  256. package/dist/extensions/pi-powerline-footer/tsconfig.json +0 -13
  257. package/dist/extensions/pi-powerline-footer/types.ts +0 -235
  258. package/dist/extensions/pi-powerline-footer/welcome-dismiss.ts +0 -34
  259. package/dist/extensions/pi-powerline-footer/welcome.ts +0 -709
  260. package/dist/extensions/pi-powerline-footer/working-vibes.ts +0 -721
  261. package/dist/extensions/preview-tools-disabled.test.ts +0 -37
  262. package/dist/extensions/preview-tools-disabled.ts +0 -11
  263. package/dist/themes/powerline-footer/theme.json +0 -33
@@ -0,0 +1,841 @@
1
+ import { execFile } from "node:child_process";
2
+ import { existsSync, readdirSync, statSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { promisify } from "node:util";
5
+
6
+ const execFileAsync = promisify(execFile);
7
+ const VERSION_TIMEOUT_MS = 2500;
8
+
9
+ export type RuntimeMetadata = {
10
+ name: string;
11
+ symbol: string;
12
+ style: string;
13
+ };
14
+
15
+ export type RuntimeInfo = Pick<RuntimeMetadata, "name" | "symbol" | "style"> & {
16
+ version?: string;
17
+ };
18
+
19
+ export type RuntimeReadResult = { kind: "ok"; runtime?: RuntimeInfo } | { kind: "error" };
20
+
21
+ type RuntimeCacheEntry = {
22
+ fingerprint: string;
23
+ runtime: RuntimeInfo | undefined;
24
+ };
25
+
26
+ const RUNTIME_CACHE_MAX = 32;
27
+ const runtimeInfoCache = new Map<string, RuntimeCacheEntry>();
28
+
29
+ export function clearRuntimeInfoCache(): void {
30
+ runtimeInfoCache.clear();
31
+ }
32
+
33
+ function topLevelFingerprint(entries: readonly string[]): string {
34
+ return entries.slice().sort().join("\0");
35
+ }
36
+
37
+ function cacheRuntimeInfo(
38
+ cwd: string,
39
+ fingerprint: string,
40
+ runtime: RuntimeInfo | undefined,
41
+ ): void {
42
+ // Drop prior fingerprints for the same cwd so stale marker sets do not linger.
43
+ for (const key of runtimeInfoCache.keys()) {
44
+ if (key === cwd || key.startsWith(`${cwd}\0`)) runtimeInfoCache.delete(key);
45
+ }
46
+ const key = `${cwd}\0${fingerprint}`;
47
+ runtimeInfoCache.set(key, { fingerprint, runtime });
48
+ while (runtimeInfoCache.size > RUNTIME_CACHE_MAX) {
49
+ const oldest = runtimeInfoCache.keys().next().value;
50
+ if (oldest === undefined) break;
51
+ runtimeInfoCache.delete(oldest);
52
+ }
53
+ }
54
+
55
+ type RuntimeEnvironment = Record<string, string | undefined>;
56
+
57
+ type DetectionSpec = {
58
+ extensions?: readonly string[];
59
+ files?: readonly string[];
60
+ folders?: readonly string[];
61
+ env?: (env: RuntimeEnvironment) => boolean;
62
+ excludedFiles?: readonly string[];
63
+ };
64
+
65
+ type VersionCommand = {
66
+ command: string;
67
+ args?: readonly string[];
68
+ pattern?: RegExp;
69
+ };
70
+
71
+ type RuntimeDef = RuntimeMetadata & {
72
+ priority: number;
73
+ detect: DetectionSpec;
74
+ version: (cwd: string) => Promise<string | undefined>;
75
+ };
76
+
77
+ // --- Detection utilities ---
78
+
79
+ function hasAnyFile(cwd: string, names: readonly string[]): boolean {
80
+ return names.some((name) => existsSync(join(cwd, name)));
81
+ }
82
+
83
+ function hasAnyFolder(cwd: string, names: readonly string[]): boolean {
84
+ return names.some((name) => {
85
+ try {
86
+ return statSync(join(cwd, name)).isDirectory();
87
+ } catch {
88
+ return false;
89
+ }
90
+ });
91
+ }
92
+
93
+ function entryExtensions(entry: string): string[] {
94
+ const baseName = entry.split(/[\\/]/).pop() ?? entry;
95
+ if (!baseName || baseName.startsWith(".")) return [];
96
+
97
+ const firstDot = baseName.indexOf(".");
98
+ if (firstDot === -1) return [];
99
+
100
+ const extensions = [baseName.slice(firstDot + 1)];
101
+ const lastDot = baseName.lastIndexOf(".");
102
+ if (lastDot !== firstDot) extensions.push(baseName.slice(lastDot + 1));
103
+ return extensions;
104
+ }
105
+
106
+ function hasAnyExtension(entries: readonly string[], extensions: readonly string[]): boolean {
107
+ const extensionSet = new Set(extensions);
108
+ return entries.some((entry) =>
109
+ entryExtensions(entry).some((extension) => extensionSet.has(extension)),
110
+ );
111
+ }
112
+
113
+ function matchesDetection(
114
+ cwd: string,
115
+ entries: string[],
116
+ spec: DetectionSpec,
117
+ env: RuntimeEnvironment,
118
+ ): boolean {
119
+ if (spec.excludedFiles && hasAnyFile(cwd, spec.excludedFiles)) return false;
120
+ return Boolean(
121
+ (spec.files && hasAnyFile(cwd, spec.files)) ||
122
+ (spec.folders && hasAnyFolder(cwd, spec.folders)) ||
123
+ (spec.extensions && hasAnyExtension(entries, spec.extensions)) ||
124
+ spec.env?.(env),
125
+ );
126
+ }
127
+
128
+ // --- Version utilities ---
129
+
130
+ async function runVersion(
131
+ command: string,
132
+ args: readonly string[] = [],
133
+ cwd?: string,
134
+ ): Promise<string | undefined> {
135
+ try {
136
+ const { stdout, stderr } = await execFileAsync(command, [...args], {
137
+ cwd,
138
+ timeout: VERSION_TIMEOUT_MS,
139
+ });
140
+ const text =
141
+ `${typeof stdout === "string" ? stdout : String(stdout)}\n${typeof stderr === "string" ? stderr : String(stderr)}`.trim();
142
+ return text || undefined;
143
+ } catch {
144
+ return undefined;
145
+ }
146
+ }
147
+
148
+ function prefixVersion(version: string | undefined): string | undefined {
149
+ if (!version) return undefined;
150
+ return version.startsWith("v") ? version : `v${version}`;
151
+ }
152
+
153
+ function extractVersion(output: string | undefined, pattern?: RegExp): string | undefined {
154
+ if (!output) return undefined;
155
+ const match = output.match(
156
+ pattern ?? /(?:version\s*)?v?([0-9]+(?:\.[0-9A-Za-z][0-9A-Za-z.+_-]*)*)/i,
157
+ );
158
+ return prefixVersion(match?.[1]);
159
+ }
160
+
161
+ function versionFromCommands(
162
+ commands: readonly VersionCommand[],
163
+ ): () => Promise<string | undefined> {
164
+ return async () => {
165
+ for (const { command, args = [], pattern } of commands) {
166
+ const version = extractVersion(await runVersion(command, args), pattern);
167
+ if (version) return version;
168
+ }
169
+ return undefined;
170
+ };
171
+ }
172
+
173
+ function noVersion(): Promise<undefined> {
174
+ return Promise.resolve(undefined);
175
+ }
176
+
177
+ // --- Priority levels ---
178
+ // 10 = build-system runtimes (checked first to avoid language false-positives)
179
+ // 50 = common runtimes (bun, deno, node, python, etc.)
180
+ // 100 = default (everything else, checked in definition order)
181
+
182
+ const PRIORITY_BUILD_SYSTEM = 10;
183
+ const PRIORITY_COMMON = 50;
184
+ const PRIORITY_DEFAULT = 100;
185
+
186
+ // --- Runtime definitions ---
187
+
188
+ const runtimes: RuntimeDef[] = [
189
+ {
190
+ name: "xmake",
191
+ symbol: "",
192
+ style: "bold green",
193
+ priority: PRIORITY_BUILD_SYSTEM,
194
+ detect: { files: ["xmake.lua"] },
195
+ version: versionFromCommands([{ command: "xmake", args: ["--version"] }]),
196
+ },
197
+ {
198
+ name: "maven",
199
+ symbol: "",
200
+ style: "bold bright-cyan",
201
+ priority: PRIORITY_BUILD_SYSTEM,
202
+ detect: { files: ["pom.xml"] },
203
+ version: versionFromCommands([
204
+ { command: "mvn", args: ["--version"], pattern: /Apache Maven\s+([0-9][^\s]*)/i },
205
+ ]),
206
+ },
207
+ {
208
+ name: "gradle",
209
+ symbol: "",
210
+ style: "bold bright-cyan",
211
+ priority: PRIORITY_BUILD_SYSTEM,
212
+ detect: { files: ["build.gradle", "build.gradle.kts"], folders: ["gradle"] },
213
+ version: versionFromCommands([
214
+ { command: "gradle", args: ["--version"], pattern: /Gradle\s+([0-9][^\s]*)/i },
215
+ ]),
216
+ },
217
+ {
218
+ name: "bun",
219
+ symbol: "",
220
+ style: "bold red",
221
+ priority: PRIORITY_COMMON,
222
+ detect: { files: ["bun.lock", "bun.lockb"] },
223
+ version: async () => prefixVersion(await runVersion("bun", ["--version"])),
224
+ },
225
+ {
226
+ name: "deno",
227
+ symbol: "",
228
+ style: "green bold",
229
+ priority: PRIORITY_COMMON,
230
+ detect: { files: ["deno.json", "deno.jsonc", "deno.lock"] },
231
+ version: async () =>
232
+ extractVersion(await runVersion("deno", ["--version"]), /deno\s+([0-9][^\s]*)/i),
233
+ },
234
+ {
235
+ name: "lua",
236
+ symbol: "",
237
+ style: "bold blue",
238
+ priority: PRIORITY_COMMON,
239
+ detect: {
240
+ extensions: ["lua"],
241
+ files: [
242
+ "stylua.toml",
243
+ ".stylua.toml",
244
+ ".luarc.json",
245
+ ".luarc.jsonc",
246
+ "init.lua",
247
+ ".lua-version",
248
+ ],
249
+ folders: ["lua"],
250
+ excludedFiles: ["xmake.lua"],
251
+ },
252
+ version: async () => {
253
+ const lua = await runVersion("lua", ["-v"]);
254
+ const luaMatch = lua?.match(/Lua\s+([0-9][^\s]*)/i);
255
+ if (luaMatch?.[1]) return prefixVersion(luaMatch[1]);
256
+ const luajit = await runVersion("luajit", ["-v"]);
257
+ const luajitMatch = luajit?.match(/LuaJIT\s+([0-9][^\s]*)/i);
258
+ return prefixVersion(luajitMatch?.[1]);
259
+ },
260
+ },
261
+ {
262
+ name: "nodejs",
263
+ symbol: "",
264
+ style: "bold green",
265
+ priority: PRIORITY_COMMON,
266
+ detect: {
267
+ files: ["package.json", ".node-version", ".nvmrc"],
268
+ excludedFiles: ["bunfig.toml", "bun.lock", "bun.lockb"],
269
+ },
270
+ version: async () => prefixVersion(await runVersion("node", ["--version"])),
271
+ },
272
+ {
273
+ name: "python",
274
+ symbol: "",
275
+ style: "yellow bold",
276
+ priority: PRIORITY_COMMON,
277
+ detect: {
278
+ files: [
279
+ "requirements.txt",
280
+ ".python-version",
281
+ "pyproject.toml",
282
+ "Pipfile",
283
+ "setup.py",
284
+ "setup.cfg",
285
+ ],
286
+ },
287
+ version: async () => {
288
+ const python3 = await runVersion("python3", ["--version"]);
289
+ const python3Match = python3?.match(/Python\s+([0-9][^\s]*)/i);
290
+ if (python3Match?.[1]) return prefixVersion(python3Match[1]);
291
+ const python = await runVersion("python", ["--version"]);
292
+ const pythonMatch = python?.match(/Python\s+([0-9][^\s]*)/i);
293
+ return prefixVersion(pythonMatch?.[1]);
294
+ },
295
+ },
296
+ {
297
+ name: "golang",
298
+ symbol: "",
299
+ style: "bold cyan",
300
+ priority: PRIORITY_COMMON,
301
+ detect: { files: ["go.mod"] },
302
+ version: async () =>
303
+ extractVersion(await runVersion("go", ["version"]), /go version go([0-9][^\s]*)/i),
304
+ },
305
+ {
306
+ name: "rust",
307
+ symbol: "󱘗",
308
+ style: "bold red",
309
+ priority: PRIORITY_COMMON,
310
+ detect: { files: ["Cargo.toml"] },
311
+ version: async () =>
312
+ extractVersion(await runVersion("rustc", ["--version"]), /rustc\s+([0-9][^\s]*)/i),
313
+ },
314
+ {
315
+ name: "java",
316
+ symbol: "",
317
+ style: "red dimmed",
318
+ priority: PRIORITY_COMMON,
319
+ detect: { files: [".java-version"] },
320
+ version: async () => {
321
+ const output = await runVersion("java", ["-version"]);
322
+ const quoted = output?.match(/"([0-9][^"]*)"/);
323
+ if (quoted?.[1]) return prefixVersion(quoted[1]);
324
+ const plain = output?.match(/version\s+([0-9][^\s]*)/i);
325
+ return prefixVersion(plain?.[1]);
326
+ },
327
+ },
328
+ {
329
+ name: "ruby",
330
+ symbol: "",
331
+ style: "bold red",
332
+ priority: PRIORITY_COMMON,
333
+ detect: { files: ["Gemfile", ".ruby-version"] },
334
+ version: async () =>
335
+ extractVersion(await runVersion("ruby", ["--version"]), /ruby\s+([0-9][^\s]*)/i),
336
+ },
337
+ {
338
+ name: "php",
339
+ symbol: "",
340
+ style: "147 bold",
341
+ priority: PRIORITY_COMMON,
342
+ detect: { files: ["composer.json"] },
343
+ version: async () =>
344
+ extractVersion(await runVersion("php", ["--version"]), /PHP\s+([0-9][^\s]*)/i),
345
+ },
346
+ {
347
+ name: "buf",
348
+ symbol: "",
349
+ style: "bold blue",
350
+ priority: PRIORITY_DEFAULT,
351
+ detect: { files: ["buf.yaml", "buf.gen.yaml", "buf.work.yaml"] },
352
+ version: versionFromCommands([{ command: "buf", args: ["--version"] }]),
353
+ },
354
+ {
355
+ name: "cmake",
356
+ symbol: "",
357
+ style: "bold blue",
358
+ priority: PRIORITY_DEFAULT,
359
+ detect: { files: ["CMakeLists.txt", "CMakeCache.txt"] },
360
+ version: versionFromCommands([{ command: "cmake", args: ["--version"] }]),
361
+ },
362
+ {
363
+ name: "cpp",
364
+ symbol: "",
365
+ style: "bold 149",
366
+ priority: PRIORITY_DEFAULT,
367
+ detect: { extensions: ["cpp", "cc", "cxx", "c++", "hpp", "hh", "hxx", "h++", "tcc"] },
368
+ version: versionFromCommands([
369
+ { command: "c++", args: ["--version"] },
370
+ { command: "g++", args: ["--version"] },
371
+ { command: "clang++", args: ["--version"] },
372
+ ]),
373
+ },
374
+ {
375
+ name: "c",
376
+ symbol: "",
377
+ style: "bold 149",
378
+ priority: PRIORITY_DEFAULT,
379
+ detect: { extensions: ["c", "h"] },
380
+ version: versionFromCommands([
381
+ { command: "cc", args: ["--version"] },
382
+ { command: "gcc", args: ["--version"] },
383
+ { command: "clang", args: ["--version"] },
384
+ ]),
385
+ },
386
+ {
387
+ name: "cobol",
388
+ symbol: "",
389
+ style: "bold blue",
390
+ priority: PRIORITY_DEFAULT,
391
+ detect: { extensions: ["cbl", "cob", "CBL", "COB"] },
392
+ version: versionFromCommands([{ command: "cobc", args: ["--version"] }]),
393
+ },
394
+ {
395
+ name: "conda",
396
+ symbol: "",
397
+ style: "bold green",
398
+ priority: PRIORITY_DEFAULT,
399
+ detect: { env: (env) => Boolean(env.CONDA_DEFAULT_ENV?.trim()) && !env.PIXI_ENVIRONMENT_NAME },
400
+ version: noVersion,
401
+ },
402
+ {
403
+ name: "crystal",
404
+ symbol: "",
405
+ style: "bold red",
406
+ priority: PRIORITY_DEFAULT,
407
+ detect: { extensions: ["cr"], files: ["shard.yml"] },
408
+ version: versionFromCommands([{ command: "crystal", args: ["--version"] }]),
409
+ },
410
+ {
411
+ name: "dart",
412
+ symbol: "",
413
+ style: "bold blue",
414
+ priority: PRIORITY_DEFAULT,
415
+ detect: {
416
+ extensions: ["dart"],
417
+ files: ["pubspec.yaml", "pubspec.yml", "pubspec.lock"],
418
+ folders: [".dart_tool"],
419
+ },
420
+ version: versionFromCommands([{ command: "dart", args: ["--version"] }]),
421
+ },
422
+ {
423
+ name: "dotnet",
424
+ symbol: "",
425
+ style: "bold blue",
426
+ priority: PRIORITY_DEFAULT,
427
+ detect: {
428
+ extensions: ["csproj", "fsproj", "xproj"],
429
+ files: [
430
+ "global.json",
431
+ "project.json",
432
+ "Directory.Build.props",
433
+ "Directory.Build.targets",
434
+ "Packages.props",
435
+ ],
436
+ },
437
+ version: versionFromCommands([{ command: "dotnet", args: ["--version"] }]),
438
+ },
439
+ {
440
+ name: "elixir",
441
+ symbol: "",
442
+ style: "bold purple",
443
+ priority: PRIORITY_DEFAULT,
444
+ detect: { files: ["mix.exs"] },
445
+ version: versionFromCommands([
446
+ { command: "elixir", args: ["--version"], pattern: /Elixir\s+([0-9][^\s]*)/i },
447
+ ]),
448
+ },
449
+ {
450
+ name: "elm",
451
+ symbol: "",
452
+ style: "cyan bold",
453
+ priority: PRIORITY_DEFAULT,
454
+ detect: {
455
+ extensions: ["elm"],
456
+ files: ["elm.json", "elm-package.json", ".elm-version"],
457
+ folders: ["elm-stuff"],
458
+ },
459
+ version: versionFromCommands([{ command: "elm", args: ["--version"] }]),
460
+ },
461
+ {
462
+ name: "erlang",
463
+ symbol: "",
464
+ style: "bold red",
465
+ priority: PRIORITY_DEFAULT,
466
+ detect: { files: ["rebar.config", "erlang.mk"] },
467
+ version: versionFromCommands([{ command: "erl", args: ["-version"] }]),
468
+ },
469
+ {
470
+ name: "fennel",
471
+ symbol: "",
472
+ style: "bold green",
473
+ priority: PRIORITY_DEFAULT,
474
+ detect: { extensions: ["fnl"] },
475
+ version: versionFromCommands([{ command: "fennel", args: ["--version"] }]),
476
+ },
477
+ {
478
+ name: "fortran",
479
+ symbol: "",
480
+ style: "bold purple",
481
+ priority: PRIORITY_DEFAULT,
482
+ detect: {
483
+ extensions: [
484
+ "f",
485
+ "F",
486
+ "for",
487
+ "FOR",
488
+ "ftn",
489
+ "FTN",
490
+ "f77",
491
+ "F77",
492
+ "f90",
493
+ "F90",
494
+ "f95",
495
+ "F95",
496
+ "f03",
497
+ "F03",
498
+ "f08",
499
+ "F08",
500
+ "f18",
501
+ "F18",
502
+ ],
503
+ files: ["fpm.toml"],
504
+ },
505
+ version: versionFromCommands([
506
+ { command: "gfortran", args: ["--version"] },
507
+ { command: "flang", args: ["--version"] },
508
+ { command: "flang-new", args: ["--version"] },
509
+ ]),
510
+ },
511
+ {
512
+ name: "gleam",
513
+ symbol: "",
514
+ style: "bold #FFAFF3",
515
+ priority: PRIORITY_DEFAULT,
516
+ detect: { extensions: ["gleam"], files: ["gleam.toml"] },
517
+ version: versionFromCommands([{ command: "gleam", args: ["--version"] }]),
518
+ },
519
+ {
520
+ name: "guix_shell",
521
+ symbol: "",
522
+ style: "yellow bold",
523
+ priority: PRIORITY_DEFAULT,
524
+ detect: { env: (env) => Boolean(env.GUIX_ENVIRONMENT?.trim()) },
525
+ version: noVersion,
526
+ },
527
+ {
528
+ name: "haskell",
529
+ symbol: "",
530
+ style: "bold purple",
531
+ priority: PRIORITY_DEFAULT,
532
+ detect: { extensions: ["hs", "cabal", "hs-boot"], files: ["stack.yaml", "cabal.project"] },
533
+ version: versionFromCommands([{ command: "ghc", args: ["--numeric-version"] }]),
534
+ },
535
+ {
536
+ name: "haxe",
537
+ symbol: "",
538
+ style: "bold fg:202",
539
+ priority: PRIORITY_DEFAULT,
540
+ detect: {
541
+ extensions: ["hx", "hxml"],
542
+ files: ["haxelib.json", "hxformat.json", ".haxerc"],
543
+ folders: [".haxelib", "haxe_libraries"],
544
+ },
545
+ version: versionFromCommands([{ command: "haxe", args: ["--version"] }]),
546
+ },
547
+ {
548
+ name: "helm",
549
+ symbol: "",
550
+ style: "bold white",
551
+ priority: PRIORITY_DEFAULT,
552
+ detect: { files: ["helmfile.yaml", "Chart.yaml"] },
553
+ version: versionFromCommands([{ command: "helm", args: ["version", "--short"] }]),
554
+ },
555
+ {
556
+ name: "julia",
557
+ symbol: "",
558
+ style: "bold purple",
559
+ priority: PRIORITY_DEFAULT,
560
+ detect: { extensions: ["jl"], files: ["Project.toml", "Manifest.toml"] },
561
+ version: versionFromCommands([{ command: "julia", args: ["--version"] }]),
562
+ },
563
+ {
564
+ name: "kotlin",
565
+ symbol: "",
566
+ style: "bold blue",
567
+ priority: PRIORITY_DEFAULT,
568
+ detect: { extensions: ["kt", "kts"] },
569
+ version: versionFromCommands([{ command: "kotlin", args: ["-version"] }]),
570
+ },
571
+ {
572
+ name: "meson",
573
+ symbol: "󰔷",
574
+ style: "blue bold",
575
+ priority: PRIORITY_DEFAULT,
576
+ detect: { env: (env) => env.MESON_DEVENV === "1" && Boolean(env.MESON_PROJECT_NAME?.trim()) },
577
+ version: noVersion,
578
+ },
579
+ {
580
+ name: "mojo",
581
+ symbol: "󰈸",
582
+ style: "bold 208",
583
+ priority: PRIORITY_DEFAULT,
584
+ detect: { extensions: ["mojo", "🔥"] },
585
+ version: versionFromCommands([{ command: "mojo", args: ["--version"] }]),
586
+ },
587
+ {
588
+ name: "nim",
589
+ symbol: "",
590
+ style: "bold yellow",
591
+ priority: PRIORITY_DEFAULT,
592
+ detect: { extensions: ["nim", "nims", "nimble"], files: ["nim.cfg"] },
593
+ version: versionFromCommands([{ command: "nim", args: ["--version"] }]),
594
+ },
595
+ {
596
+ name: "nix_shell",
597
+ symbol: "",
598
+ style: "bold blue",
599
+ priority: PRIORITY_DEFAULT,
600
+ detect: { env: (env) => env.IN_NIX_SHELL === "pure" || env.IN_NIX_SHELL === "impure" },
601
+ version: noVersion,
602
+ },
603
+ {
604
+ name: "ocaml",
605
+ symbol: "",
606
+ style: "bold yellow",
607
+ priority: PRIORITY_DEFAULT,
608
+ detect: {
609
+ extensions: ["opam", "ml", "mli", "re", "rei"],
610
+ files: ["dune", "dune-project", "jbuild", "jbuild-ignore", ".merlin"],
611
+ folders: ["_opam", "esy.lock"],
612
+ },
613
+ version: versionFromCommands([{ command: "ocaml", args: ["-version"] }]),
614
+ },
615
+ {
616
+ name: "odin",
617
+ symbol: "󰟢",
618
+ style: "bold bright-blue",
619
+ priority: PRIORITY_DEFAULT,
620
+ detect: { extensions: ["odin"] },
621
+ version: versionFromCommands([{ command: "odin", args: ["version"] }]),
622
+ },
623
+ {
624
+ name: "opa",
625
+ symbol: "",
626
+ style: "bold blue",
627
+ priority: PRIORITY_DEFAULT,
628
+ detect: { extensions: ["rego"] },
629
+ version: versionFromCommands([{ command: "opa", args: ["version"] }]),
630
+ },
631
+ {
632
+ name: "perl",
633
+ symbol: "",
634
+ style: "bold 149",
635
+ priority: PRIORITY_DEFAULT,
636
+ detect: {
637
+ extensions: ["pl", "pm", "pod"],
638
+ files: [
639
+ "Makefile.PL",
640
+ "Build.PL",
641
+ "cpanfile",
642
+ "cpanfile.snapshot",
643
+ "META.json",
644
+ "META.yml",
645
+ ".perl-version",
646
+ ],
647
+ },
648
+ version: versionFromCommands([{ command: "perl", args: ["--version"] }]),
649
+ },
650
+ {
651
+ name: "pixi",
652
+ symbol: "󰏗",
653
+ style: "yellow bold",
654
+ priority: PRIORITY_DEFAULT,
655
+ detect: {
656
+ files: ["pixi.toml", "pixi.lock"],
657
+ env: (env) => Boolean(env.PIXI_ENVIRONMENT_NAME?.trim()),
658
+ },
659
+ version: versionFromCommands([{ command: "pixi", args: ["--version"] }]),
660
+ },
661
+ {
662
+ name: "pulumi",
663
+ symbol: "",
664
+ style: "bold 5",
665
+ priority: PRIORITY_DEFAULT,
666
+ detect: { files: ["Pulumi.yaml", "Pulumi.yml"] },
667
+ version: versionFromCommands([{ command: "pulumi", args: ["version"] }]),
668
+ },
669
+ {
670
+ name: "purescript",
671
+ symbol: "",
672
+ style: "bold white",
673
+ priority: PRIORITY_DEFAULT,
674
+ detect: { extensions: ["purs"], files: ["spago.dhall", "spago.yaml", "spago.lock"] },
675
+ version: versionFromCommands([{ command: "purs", args: ["--version"] }]),
676
+ },
677
+ {
678
+ name: "raku",
679
+ symbol: "󱖊",
680
+ style: "bold 149",
681
+ priority: PRIORITY_DEFAULT,
682
+ detect: { extensions: ["p6", "pm6", "pod6", "raku", "rakumod"], files: ["META6.json"] },
683
+ version: versionFromCommands([{ command: "raku", args: ["--version"] }]),
684
+ },
685
+ {
686
+ name: "red",
687
+ symbol: "󱍼",
688
+ style: "red bold",
689
+ priority: PRIORITY_DEFAULT,
690
+ detect: { extensions: ["red", "reds"] },
691
+ version: versionFromCommands([{ command: "red", args: ["--version"] }]),
692
+ },
693
+ {
694
+ name: "rlang",
695
+ symbol: "󰟔",
696
+ style: "blue bold",
697
+ priority: PRIORITY_DEFAULT,
698
+ detect: {
699
+ extensions: ["R", "Rd", "Rmd", "Rproj", "Rsx"],
700
+ files: ["DESCRIPTION"],
701
+ folders: [".Rproj.user"],
702
+ },
703
+ version: versionFromCommands([{ command: "R", args: ["--version"] }]),
704
+ },
705
+ {
706
+ name: "scala",
707
+ symbol: "",
708
+ style: "red dimmed",
709
+ priority: PRIORITY_DEFAULT,
710
+ detect: {
711
+ extensions: ["sbt", "scala"],
712
+ files: [".scalaenv", ".sbtenv", "build.sbt"],
713
+ folders: [".metals"],
714
+ },
715
+ version: versionFromCommands([{ command: "scala", args: ["-version"] }]),
716
+ },
717
+ {
718
+ name: "solidity",
719
+ symbol: "",
720
+ style: "bold blue",
721
+ priority: PRIORITY_DEFAULT,
722
+ detect: { extensions: ["sol"] },
723
+ version: versionFromCommands([{ command: "solc", args: ["--version"] }]),
724
+ },
725
+ {
726
+ name: "spack",
727
+ symbol: "",
728
+ style: "bold blue",
729
+ priority: PRIORITY_DEFAULT,
730
+ detect: { env: (env) => Boolean(env.SPACK_ENV?.trim()) },
731
+ version: noVersion,
732
+ },
733
+ {
734
+ name: "swift",
735
+ symbol: "",
736
+ style: "bold 202",
737
+ priority: PRIORITY_DEFAULT,
738
+ detect: { extensions: ["swift"], files: ["Package.swift"] },
739
+ version: versionFromCommands([
740
+ { command: "swift", args: ["--version"], pattern: /Swift version\s+([0-9][^\s]*)/i },
741
+ ]),
742
+ },
743
+ {
744
+ name: "terraform",
745
+ symbol: "",
746
+ style: "bold 105",
747
+ priority: PRIORITY_DEFAULT,
748
+ detect: { extensions: ["tf", "tfplan", "tfstate"], folders: [".terraform"] },
749
+ version: versionFromCommands([
750
+ { command: "terraform", args: ["version"] },
751
+ { command: "tofu", args: ["version"] },
752
+ ]),
753
+ },
754
+ {
755
+ name: "typst",
756
+ symbol: "",
757
+ style: "bold #0093A7",
758
+ priority: PRIORITY_DEFAULT,
759
+ detect: { extensions: ["typ"], files: ["template.typ"] },
760
+ version: versionFromCommands([{ command: "typst", args: ["--version"] }]),
761
+ },
762
+ {
763
+ name: "vagrant",
764
+ symbol: "",
765
+ style: "cyan bold",
766
+ priority: PRIORITY_DEFAULT,
767
+ detect: { files: ["Vagrantfile"] },
768
+ version: versionFromCommands([{ command: "vagrant", args: ["--version"] }]),
769
+ },
770
+ {
771
+ name: "vlang",
772
+ symbol: "",
773
+ style: "blue bold",
774
+ priority: PRIORITY_DEFAULT,
775
+ detect: { extensions: ["v"], files: ["v.mod", "vpkg.json", ".vpkg-lock.json"] },
776
+ version: versionFromCommands([{ command: "v", args: ["version"] }]),
777
+ },
778
+ {
779
+ name: "zig",
780
+ symbol: "",
781
+ style: "bold yellow",
782
+ priority: PRIORITY_DEFAULT,
783
+ detect: { extensions: ["zig"], files: ["build.zig"] },
784
+ version: versionFromCommands([{ command: "zig", args: ["version"] }]),
785
+ },
786
+ ];
787
+
788
+ // Sort once at module load — stable sort preserves definition order within same priority
789
+ const sortedRuntimes = [...runtimes].sort((a, b) => a.priority - b.priority);
790
+
791
+ export const runtimeMetadata: RuntimeMetadata[] = runtimes.map(({ name, symbol, style }) => ({
792
+ name,
793
+ symbol,
794
+ style,
795
+ }));
796
+
797
+ export function detectRuntime(
798
+ cwd: string,
799
+ entries: string[],
800
+ env: RuntimeEnvironment = process.env,
801
+ ): RuntimeDef | undefined {
802
+ for (const runtime of sortedRuntimes) {
803
+ if (matchesDetection(cwd, entries, runtime.detect, env)) return runtime;
804
+ }
805
+ return undefined;
806
+ }
807
+
808
+ export async function readRuntimeInfo(cwd: string): Promise<RuntimeReadResult> {
809
+ let entries: string[];
810
+ try {
811
+ entries = readdirSync(cwd);
812
+ } catch {
813
+ // readdir failure is treated as a transient error so last-good can be kept.
814
+ return { kind: "error" };
815
+ }
816
+
817
+ const fingerprint = topLevelFingerprint(entries);
818
+ const cacheKey = `${cwd}\0${fingerprint}`;
819
+ const cached = runtimeInfoCache.get(cacheKey);
820
+ if (cached && cached.fingerprint === fingerprint) {
821
+ return { kind: "ok", runtime: cached.runtime };
822
+ }
823
+
824
+ try {
825
+ const runtime = detectRuntime(cwd, entries);
826
+ if (!runtime) {
827
+ cacheRuntimeInfo(cwd, fingerprint, undefined);
828
+ return { kind: "ok", runtime: undefined };
829
+ }
830
+ const info: RuntimeInfo = {
831
+ name: runtime.name,
832
+ symbol: runtime.symbol,
833
+ style: runtime.style,
834
+ version: await runtime.version(cwd),
835
+ };
836
+ cacheRuntimeInfo(cwd, fingerprint, info);
837
+ return { kind: "ok", runtime: info };
838
+ } catch {
839
+ return { kind: "error" };
840
+ }
841
+ }