@selesai/code 0.9.15 → 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 (267) hide show
  1. package/CHANGELOG.md +22 -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/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/package.json +3 -3
  193. package/dist/extensions/auto-model/auto-model.test.ts +0 -18
  194. package/dist/extensions/auto-model/classifier.ts +0 -23
  195. package/dist/extensions/auto-model/config.ts +0 -51
  196. package/dist/extensions/auto-model/index.ts +0 -22
  197. package/dist/extensions/auto-model/lifecycle.test.ts +0 -24
  198. package/dist/extensions/pi-powerline-footer/.github/workflows/test.yml +0 -27
  199. package/dist/extensions/pi-powerline-footer/CHANGELOG.md +0 -687
  200. package/dist/extensions/pi-powerline-footer/CONTRIBUTING.md +0 -16
  201. package/dist/extensions/pi-powerline-footer/README.md +0 -513
  202. package/dist/extensions/pi-powerline-footer/banner.png +0 -0
  203. package/dist/extensions/pi-powerline-footer/bash-mode/completion.ts +0 -586
  204. package/dist/extensions/pi-powerline-footer/bash-mode/editor.ts +0 -583
  205. package/dist/extensions/pi-powerline-footer/bash-mode/history.ts +0 -204
  206. package/dist/extensions/pi-powerline-footer/bash-mode/shell-session.ts +0 -286
  207. package/dist/extensions/pi-powerline-footer/bash-mode/transcript.ts +0 -108
  208. package/dist/extensions/pi-powerline-footer/bash-mode/types.ts +0 -60
  209. package/dist/extensions/pi-powerline-footer/cd-command.ts +0 -190
  210. package/dist/extensions/pi-powerline-footer/colors.ts +0 -70
  211. package/dist/extensions/pi-powerline-footer/context-usage.ts +0 -156
  212. package/dist/extensions/pi-powerline-footer/currency-rates.ts +0 -142
  213. package/dist/extensions/pi-powerline-footer/editor-composition.ts +0 -32
  214. package/dist/extensions/pi-powerline-footer/editor-performance.ts +0 -99
  215. package/dist/extensions/pi-powerline-footer/git-status.ts +0 -329
  216. package/dist/extensions/pi-powerline-footer/guide.ts +0 -220
  217. package/dist/extensions/pi-powerline-footer/icons.ts +0 -190
  218. package/dist/extensions/pi-powerline-footer/index.ts +0 -3428
  219. package/dist/extensions/pi-powerline-footer/lifecycle.ts +0 -10
  220. package/dist/extensions/pi-powerline-footer/package.json +0 -49
  221. package/dist/extensions/pi-powerline-footer/paths.ts +0 -22
  222. package/dist/extensions/pi-powerline-footer/powerline-config.ts +0 -446
  223. package/dist/extensions/pi-powerline-footer/presets.ts +0 -102
  224. package/dist/extensions/pi-powerline-footer/queue/store.ts +0 -313
  225. package/dist/extensions/pi-powerline-footer/queue/types.ts +0 -53
  226. package/dist/extensions/pi-powerline-footer/render-scheduler.ts +0 -29
  227. package/dist/extensions/pi-powerline-footer/segments.ts +0 -570
  228. package/dist/extensions/pi-powerline-footer/separators.ts +0 -57
  229. package/dist/extensions/pi-powerline-footer/session-usage.ts +0 -44
  230. package/dist/extensions/pi-powerline-footer/shortcuts.ts +0 -61
  231. package/dist/extensions/pi-powerline-footer/tests/bash-mode.test.ts +0 -1905
  232. package/dist/extensions/pi-powerline-footer/tests/cd-command.test.ts +0 -144
  233. package/dist/extensions/pi-powerline-footer/tests/context-usage.test.ts +0 -156
  234. package/dist/extensions/pi-powerline-footer/tests/custom-items.test.ts +0 -344
  235. package/dist/extensions/pi-powerline-footer/tests/editor-composition.test.ts +0 -48
  236. package/dist/extensions/pi-powerline-footer/tests/editor-performance.test.ts +0 -43
  237. package/dist/extensions/pi-powerline-footer/tests/editor-responsiveness.test.ts +0 -136
  238. package/dist/extensions/pi-powerline-footer/tests/fixed-editor-border.test.ts +0 -39
  239. package/dist/extensions/pi-powerline-footer/tests/git-optional-locks.test.ts +0 -64
  240. package/dist/extensions/pi-powerline-footer/tests/git-status.test.ts +0 -92
  241. package/dist/extensions/pi-powerline-footer/tests/guide.test.ts +0 -129
  242. package/dist/extensions/pi-powerline-footer/tests/jump-shortcuts.test.ts +0 -116
  243. package/dist/extensions/pi-powerline-footer/tests/mode-aware-trigger.test.ts +0 -79
  244. package/dist/extensions/pi-powerline-footer/tests/paths.test.ts +0 -63
  245. package/dist/extensions/pi-powerline-footer/tests/queue-store.test.ts +0 -65
  246. package/dist/extensions/pi-powerline-footer/tests/quit-cursor.test.ts +0 -14
  247. package/dist/extensions/pi-powerline-footer/tests/remaining-regressions.test.ts +0 -173
  248. package/dist/extensions/pi-powerline-footer/tests/session-usage.test.ts +0 -47
  249. package/dist/extensions/pi-powerline-footer/tests/stash-shortcut.test.ts +0 -37
  250. package/dist/extensions/pi-powerline-footer/tests/thinking-segment.test.ts +0 -76
  251. package/dist/extensions/pi-powerline-footer/tests/token-stats.test.ts +0 -249
  252. package/dist/extensions/pi-powerline-footer/tests/tps.test.ts +0 -39
  253. package/dist/extensions/pi-powerline-footer/tests/usage-display.test.ts +0 -237
  254. package/dist/extensions/pi-powerline-footer/tests/welcome.test.ts +0 -199
  255. package/dist/extensions/pi-powerline-footer/tests/working-vibes.test.ts +0 -317
  256. package/dist/extensions/pi-powerline-footer/theme.example.json +0 -24
  257. package/dist/extensions/pi-powerline-footer/theme.json +0 -12
  258. package/dist/extensions/pi-powerline-footer/theme.ts +0 -241
  259. package/dist/extensions/pi-powerline-footer/token-stats.ts +0 -304
  260. package/dist/extensions/pi-powerline-footer/tsconfig.json +0 -13
  261. package/dist/extensions/pi-powerline-footer/types.ts +0 -235
  262. package/dist/extensions/pi-powerline-footer/welcome-dismiss.ts +0 -34
  263. package/dist/extensions/pi-powerline-footer/welcome.ts +0 -709
  264. package/dist/extensions/pi-powerline-footer/working-vibes.ts +0 -721
  265. package/dist/extensions/preview-tools-disabled.test.ts +0 -37
  266. package/dist/extensions/preview-tools-disabled.ts +0 -11
  267. package/dist/themes/powerline-footer/theme.json +0 -33
@@ -0,0 +1,321 @@
1
+ import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { describe, expect, it } from "vitest";
5
+ import {
6
+ __test__,
7
+ PACKAGE_VERSION_ECOSYSTEMS,
8
+ readPackageVersion,
9
+ } from "../extensions/zentui/package-version";
10
+
11
+ function makeProject(files: Record<string, string>): string {
12
+ const cwd = mkdtempSync(join(tmpdir(), "zentui-package-version-"));
13
+ for (const [name, content] of Object.entries(files)) {
14
+ const full = join(cwd, name);
15
+ if (name.includes("/")) {
16
+ mkdirSync(join(cwd, name.slice(0, name.indexOf("/"))), { recursive: true });
17
+ }
18
+ writeFileSync(full, content, "utf8");
19
+ }
20
+ return cwd;
21
+ }
22
+
23
+ function expectEcosystem(cwd: string, ecosystem: string, version: string) {
24
+ const result = readPackageVersion(cwd);
25
+ expect(result).not.toBeNull();
26
+ if (result === null) throw new Error("expected package version result");
27
+ expect(result.ecosystem).toBe(ecosystem);
28
+ expect(result.version).toBe(version);
29
+ }
30
+
31
+ describe("readPackageVersion", () => {
32
+ it("returns null when no manifest is present", () => {
33
+ const cwd = makeProject({});
34
+ expect(readPackageVersion(cwd)).toBeNull();
35
+ });
36
+
37
+ it("parses package.json for npm/bun projects (top-level `version`)", () => {
38
+ const cwd = makeProject({
39
+ "package.json": JSON.stringify({ name: "demo", version: "1.2.3" }),
40
+ });
41
+ expectEcosystem(cwd, "nodejs", "1.2.3");
42
+ });
43
+
44
+ it("strips surrounding quotes from package.json values", () => {
45
+ const cwd = makeProject({
46
+ "package.json": JSON.stringify({ name: "demo", version: '"2.0.0"' }, null, 2),
47
+ });
48
+ expect(readPackageVersion(cwd)?.version).toBe("2.0.0");
49
+ });
50
+
51
+ it("returns null for malformed package.json JSON without throwing", () => {
52
+ const cwd = makeProject({ "package.json": "{ this is not json" });
53
+ expect(readPackageVersion(cwd)).toBeNull();
54
+ });
55
+
56
+ it("rejects terminal controls in a JSON-decoded package version", () => {
57
+ const cwd = makeProject({
58
+ "package.json": JSON.stringify({ version: "1.2.3\x1b]52;c;c2VjcmV0\x07" }),
59
+ });
60
+ expect(readPackageVersion(cwd)).toBeNull();
61
+ });
62
+
63
+ it("parses deno.json and deno.jsonc", () => {
64
+ const denoJson = makeProject({ "deno.json": JSON.stringify({ version: "0.1.0" }) });
65
+ const denoJsonc = makeProject({
66
+ "deno.jsonc": `// leading comment\n${JSON.stringify({ version: "0.2.0" })}`,
67
+ });
68
+ expect(readPackageVersion(denoJson)?.version).toBe("0.1.0");
69
+ expect(readPackageVersion(denoJsonc)?.version).toBe("0.2.0");
70
+ });
71
+
72
+ it("parses Maven pom.xml first child <version>", () => {
73
+ const cwd = makeProject({
74
+ "pom.xml":
75
+ "<project><modelVersion>4.0.0</modelVersion><version>3.1.4</version><artifactId>x</artifactId></project>",
76
+ });
77
+ expectEcosystem(cwd, "maven", "3.1.4");
78
+ });
79
+
80
+ it("parses Gradle gradle.properties", () => {
81
+ const cwd = makeProject({ "gradle.properties": "version=4.5.6\ngroup=com.example\n" });
82
+ expectEcosystem(cwd, "gradle", "4.5.6");
83
+ });
84
+
85
+ it("parses Python pyproject.toml [project].version", () => {
86
+ const cwd = makeProject({
87
+ "pyproject.toml": `[project]\nname = "x"\nversion = "1.0.0"\n`,
88
+ });
89
+ expectEcosystem(cwd, "python", "1.0.0");
90
+ });
91
+
92
+ it("parses Python pyproject.toml poetry fallback", () => {
93
+ const cwd = makeProject({
94
+ "pyproject.toml": `[tool.poetry]\nname = "x"\nversion = "2.0.0"\n`,
95
+ });
96
+ expectEcosystem(cwd, "python", "2.0.0");
97
+ });
98
+
99
+ it("parses Python setup.cfg [metadata].version", () => {
100
+ const cwd = makeProject({
101
+ "setup.cfg": "[metadata]\nname = x\nversion = 3.2.1\n[options]\ninstall_requires = []\n",
102
+ });
103
+ expectEcosystem(cwd, "python", "3.2.1");
104
+ });
105
+
106
+ it("parses Rust Cargo.toml [package].version", () => {
107
+ const cwd = makeProject({
108
+ "Cargo.toml": `[package]\nname = "x"\nversion = "0.4.2"\nedition = "2021"\n`,
109
+ });
110
+ expectEcosystem(cwd, "rust", "0.4.2");
111
+ });
112
+
113
+ it("parses Rust Cargo.toml workspace-inherited version", () => {
114
+ const cwd = makeProject({
115
+ "Cargo.toml": `[package]\nname = "x"\nversion.workspace = true\n\n[workspace]\nversion = "0.9.0"\n`,
116
+ });
117
+ expectEcosystem(cwd, "rust", "0.9.0");
118
+ });
119
+
120
+ it("parses PHP composer.json version", () => {
121
+ const cwd = makeProject({
122
+ "composer.json": JSON.stringify({ name: "demo/demo", version: "1.4.0" }),
123
+ });
124
+ expectEcosystem(cwd, "php", "1.4.0");
125
+ });
126
+
127
+ it("parses Crystal shard.yml top-level version", () => {
128
+ const cwd = makeProject({
129
+ "shard.yml": `name: x\nversion: 0.3.0\n\ndependencies:\n yaml: ~> 1.0\n`,
130
+ });
131
+ expectEcosystem(cwd, "crystal", "0.3.0");
132
+ });
133
+
134
+ it("parses Dart pubspec.yaml version", () => {
135
+ const cwd = makeProject({
136
+ "pubspec.yaml": `name: x\nversion: 2.5.0\nenvironment:\n sdk: ">=3.0.0"\n`,
137
+ });
138
+ expectEcosystem(cwd, "dart", "2.5.0");
139
+ });
140
+
141
+ it("parses Dart pubspec.yml fallback", () => {
142
+ const cwd = makeProject({
143
+ "pubspec.yml": `name: x\nversion: 2.6.0\n`,
144
+ });
145
+ expectEcosystem(cwd, "dart", "2.6.0");
146
+ });
147
+
148
+ it("parses Elixir mix.exs project version literal", () => {
149
+ const cwd = makeProject({
150
+ "mix.exs":
151
+ 'defmodule Demo.MixProject do\n use Mix.Project\n\n def project do\n [\n app: :demo,\n version: "1.7.3",\n elixir: "~> 1.14"\n ]\n end\nend\n',
152
+ });
153
+ expectEcosystem(cwd, "elixir", "1.7.3");
154
+ });
155
+
156
+ it("parses elm.json top-level version", () => {
157
+ const cwd = makeProject({
158
+ "elm.json": JSON.stringify({
159
+ type: "application",
160
+ "source-directories": ["src"],
161
+ "elm-version": "0.19.1",
162
+ version: "1.0.0",
163
+ dependencies: { direct: {}, indirect: {} },
164
+ "test-dependencies": { direct: {}, indirect: {} },
165
+ }),
166
+ });
167
+ expectEcosystem(cwd, "elm", "1.0.0");
168
+ });
169
+
170
+ it("parses Fortran fpm.toml version", () => {
171
+ const cwd = makeProject({
172
+ "fpm.toml": `[package]\nname = "x"\nversion = "0.2.1"\n`,
173
+ });
174
+ expectEcosystem(cwd, "fortran", "0.2.1");
175
+ });
176
+
177
+ it("parses Gleam gleam.toml top-level version", () => {
178
+ const cwd = makeProject({
179
+ "gleam.toml": `name = "x"\nversion = "1.0.0"\ndescription = "demo"\n`,
180
+ });
181
+ expectEcosystem(cwd, "gleam", "1.0.0");
182
+ });
183
+
184
+ it("parses Haskell *.cabal version in package stanza", () => {
185
+ const cwd = makeProject({
186
+ "x.cabal": `cabal-version: 2.4\nname: x\nversion: 0.6.0\n\nlibrary\n exposed-modules: X\n build-depends: base >= 4.14\n`,
187
+ });
188
+ expectEcosystem(cwd, "haskell", "0.6.0");
189
+ });
190
+
191
+ it("parses Helm Chart.yaml chart version", () => {
192
+ const cwd = makeProject({
193
+ "Chart.yaml":
194
+ 'apiVersion: v2\nname: x\ndescription: demo\nversion: 0.1.2\nappVersion: "1.0.0"\n',
195
+ });
196
+ expectEcosystem(cwd, "helm", "0.1.2");
197
+ });
198
+
199
+ it("parses Julia Project.toml top-level version", () => {
200
+ const cwd = makeProject({
201
+ "Project.toml": `name = "X"\nversion = "0.5.2"\n\n[deps]\n\n[compat]\n`,
202
+ });
203
+ expectEcosystem(cwd, "julia", "0.5.2");
204
+ });
205
+
206
+ it("parses Meson meson.build static project() version literal", () => {
207
+ const cwd = makeProject({
208
+ "meson.build":
209
+ "project('x', 'c', version: '0.3.0', license: 'MIT')\n\n executable('x', 'main.c')\n",
210
+ });
211
+ expectEcosystem(cwd, "meson", "0.3.0");
212
+ });
213
+
214
+ it("parses Nim *.nimble top-level version", () => {
215
+ const cwd = makeProject({
216
+ "x.nimble": `# Package\nversion = "0.2.1"\nauthor = "x"\ndescription = "demo"\nlicense = "MIT"\n\nrequires "nim >= 1.6"\n`,
217
+ });
218
+ expectEcosystem(cwd, "nim", "0.2.1");
219
+ });
220
+
221
+ it("parses Ruby *.gemspec spec.version assignment", () => {
222
+ const cwd = makeProject({
223
+ "x.gemspec": `Gem::Specification.new do |s|\n s.name = 'x'\n s.version = '1.2.0'\n s.summary = 'demo'\nend\n`,
224
+ });
225
+ expectEcosystem(cwd, "ruby", "1.2.0");
226
+ });
227
+
228
+ it("parses Vlang v.mod module version", () => {
229
+ const cwd = makeProject({
230
+ "v.mod": `Module {\n\tname: 'x'\n\tversion: '0.4.0'\n\tdescription: 'demo'\n\tauthor: 'x'\n}\n`,
231
+ });
232
+ expectEcosystem(cwd, "vlang", "0.4.0");
233
+ });
234
+
235
+ it("parses Xmake xmake.lua static set_version literal", () => {
236
+ const cwd = makeProject({
237
+ "xmake.lua": `add_rules("mode.debug", "mode.release")\nset_version("1.6.0")\n\ntarget("demo")\n`,
238
+ });
239
+ expectEcosystem(cwd, "xmake", "1.6.0");
240
+ });
241
+
242
+ it("returns null for manifests without a parseable version (silent fallback)", () => {
243
+ const cases: Array<[string, string]> = [
244
+ ["package.json", JSON.stringify({ name: "demo" })],
245
+ ["deno.json", JSON.stringify({ name: "demo" })],
246
+ ["Cargo.toml", `[package]\nname = "x"\nedition = "2021"\n`],
247
+ ["pyproject.toml", `[project]\nname = "x"\n`],
248
+ ["meson.build", "project('x', 'c')\n"],
249
+ ["xmake.lua", "add_rules('mode.debug')\n"],
250
+ ];
251
+ for (const [file, content] of cases) {
252
+ const cwd = makeProject({ [file]: content });
253
+ expect(readPackageVersion(cwd)).toBeNull();
254
+ }
255
+ });
256
+
257
+ it("prefers package.json over Chart.yaml when both exist (deterministic priority)", () => {
258
+ const cwd = makeProject({
259
+ "package.json": JSON.stringify({ version: "1.0.0" }),
260
+ "Chart.yaml": "apiVersion: v2\nversion: 2.0.0\n",
261
+ });
262
+ const result = readPackageVersion(cwd);
263
+ expect(result).not.toBeNull();
264
+ expect(result?.ecosystem).toBe("nodejs");
265
+ expect(result?.version).toBe("1.0.0");
266
+ });
267
+
268
+ it("strips a single leading 'v' from semver versions", () => {
269
+ const cwd = makeProject({
270
+ "package.json": JSON.stringify({ version: "v3.2.1" }),
271
+ });
272
+ expect(readPackageVersion(cwd)?.version).toBe("3.2.1");
273
+ });
274
+
275
+ it("does not shell out and survives missing manifests silently", () => {
276
+ const cwd = mkdtempSync(join(tmpdir(), "zentui-empty-project-"));
277
+ expect(readPackageVersion(cwd)).toBeNull();
278
+ });
279
+
280
+ it("exposes PACKAGE_VERSION_ECOSYSTEMS as a stable, sorted-ish list", () => {
281
+ expect(PACKAGE_VERSION_ECOSYSTEMS).toContain("bun");
282
+ expect(PACKAGE_VERSION_ECOSYSTEMS).toContain("nodejs");
283
+ expect(PACKAGE_VERSION_ECOSYSTEMS).toContain("python");
284
+ expect(PACKAGE_VERSION_ECOSYSTEMS).toContain("rust");
285
+ expect(PACKAGE_VERSION_ECOSYSTEMS).toContain("haskell");
286
+ expect(PACKAGE_VERSION_ECOSYSTEMS).toContain("ruby");
287
+ expect(PACKAGE_VERSION_ECOSYSTEMS).toContain("nim");
288
+ });
289
+ });
290
+
291
+ describe("cleanVersion", () => {
292
+ const { cleanVersion } = __test__;
293
+
294
+ it.each([
295
+ ["1.2.3", "1.2.3"],
296
+ ['"1.2.3"', "1.2.3"],
297
+ ["'1.2.3'", "1.2.3"],
298
+ [" 1.2.3 ", "1.2.3"],
299
+ ["v3.2.1", "3.2.1"],
300
+ ["0.0.1-alpha", "0.0.1-alpha"],
301
+ ])("cleans %j -> %j", (input, expected) => {
302
+ expect(cleanVersion(input)).toBe(expected);
303
+ });
304
+
305
+ it.each(["", " ", "not a version", "1.2.3 with spaces", "{1.2.3}"])("rejects %j", (input) => {
306
+ expect(cleanVersion(input)).toBeUndefined();
307
+ });
308
+
309
+ it.each([
310
+ "1.2.3\x07",
311
+ "1.2.3\x1b",
312
+ "1.2.3\x7f",
313
+ "1.2.3\x9d",
314
+ "1.2.3\x9c",
315
+ "1.2.3\x1b[31m",
316
+ "1.2.3\n",
317
+ "1.2.3\t",
318
+ ])("rejects terminal control input %j", (input) => {
319
+ expect(cleanVersion(input)).toBeUndefined();
320
+ });
321
+ });
@@ -0,0 +1,250 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+ import {
3
+ createProjectRefreshScheduler,
4
+ startProjectRefreshInterval,
5
+ } from "../extensions/zentui/project-refresh";
6
+
7
+ async function flushPromises(): Promise<void> {
8
+ await Promise.resolve();
9
+ await Promise.resolve();
10
+ }
11
+
12
+ describe("startProjectRefreshInterval", () => {
13
+ afterEach(() => {
14
+ vi.useRealTimers();
15
+ });
16
+
17
+ it("runs the refresh callback at the configured interval", () => {
18
+ vi.useFakeTimers();
19
+ const refresh = vi.fn();
20
+
21
+ const stop = startProjectRefreshInterval(30_000, refresh);
22
+
23
+ vi.advanceTimersByTime(29_999);
24
+ expect(refresh).not.toHaveBeenCalled();
25
+
26
+ vi.advanceTimersByTime(1);
27
+ expect(refresh).toHaveBeenCalledTimes(1);
28
+
29
+ vi.advanceTimersByTime(30_000);
30
+ expect(refresh).toHaveBeenCalledTimes(2);
31
+
32
+ stop();
33
+ vi.advanceTimersByTime(30_000);
34
+ expect(refresh).toHaveBeenCalledTimes(2);
35
+ });
36
+
37
+ it("does not start a timer when polling is disabled", () => {
38
+ vi.useFakeTimers();
39
+ const refresh = vi.fn();
40
+
41
+ const stop = startProjectRefreshInterval(0, refresh);
42
+
43
+ vi.advanceTimersByTime(120_000);
44
+ expect(refresh).not.toHaveBeenCalled();
45
+ expect(() => stop()).not.toThrow();
46
+ });
47
+ });
48
+
49
+ describe("createProjectRefreshScheduler", () => {
50
+ afterEach(() => {
51
+ vi.useRealTimers();
52
+ });
53
+
54
+ it("throttles bursty project refresh requests", async () => {
55
+ vi.useFakeTimers();
56
+ const refresh = vi.fn<(...args: [string]) => Promise<void>>(() => Promise.resolve());
57
+ const afterRefresh = vi.fn();
58
+ const scheduler = createProjectRefreshScheduler(refresh, afterRefresh, 5_000);
59
+
60
+ scheduler.schedule("initial");
61
+ await flushPromises();
62
+
63
+ expect(refresh).toHaveBeenCalledTimes(1);
64
+ expect(refresh).toHaveBeenLastCalledWith("initial", expect.anything());
65
+ expect(afterRefresh).toHaveBeenCalledTimes(1);
66
+
67
+ scheduler.schedule("first-pending");
68
+ scheduler.schedule("latest-pending");
69
+ await flushPromises();
70
+
71
+ expect(refresh).toHaveBeenCalledTimes(1);
72
+
73
+ vi.advanceTimersByTime(4_999);
74
+ await flushPromises();
75
+ expect(refresh).toHaveBeenCalledTimes(1);
76
+
77
+ vi.advanceTimersByTime(1);
78
+ await flushPromises();
79
+
80
+ expect(refresh).toHaveBeenCalledTimes(2);
81
+ expect(refresh).toHaveBeenLastCalledWith("latest-pending", expect.anything());
82
+ expect(afterRefresh).toHaveBeenCalledTimes(2);
83
+ });
84
+
85
+ it("coalesces refreshes requested while a refresh is in flight", async () => {
86
+ vi.useFakeTimers();
87
+ let finishRefresh: (() => void) | undefined;
88
+ const refresh = vi.fn(
89
+ () =>
90
+ new Promise<void>((resolve) => {
91
+ finishRefresh = resolve;
92
+ }),
93
+ );
94
+ const afterRefresh = vi.fn();
95
+ const scheduler = createProjectRefreshScheduler(refresh, afterRefresh, 5_000);
96
+
97
+ scheduler.schedule("initial");
98
+ scheduler.schedule("pending");
99
+ await flushPromises();
100
+
101
+ expect(refresh).toHaveBeenCalledTimes(1);
102
+ expect(afterRefresh).not.toHaveBeenCalled();
103
+
104
+ finishRefresh?.();
105
+ await flushPromises();
106
+
107
+ expect(afterRefresh).toHaveBeenCalledTimes(1);
108
+ expect(refresh).toHaveBeenCalledTimes(1);
109
+
110
+ vi.advanceTimersByTime(5_000);
111
+ await flushPromises();
112
+
113
+ expect(refresh).toHaveBeenCalledTimes(2);
114
+ expect(refresh).toHaveBeenLastCalledWith("pending", expect.anything());
115
+ });
116
+
117
+ it("preserves force intent while a refresh is in flight", async () => {
118
+ vi.useFakeTimers();
119
+ let finishInitialRefresh: (() => void) | undefined;
120
+ const refresh = vi.fn((target: string) => {
121
+ if (target !== "initial") return Promise.resolve();
122
+ return new Promise<void>((resolve) => {
123
+ finishInitialRefresh = resolve;
124
+ });
125
+ });
126
+ const afterRefresh = vi.fn();
127
+ const scheduler = createProjectRefreshScheduler(refresh, afterRefresh, 5_000);
128
+
129
+ scheduler.schedule("initial");
130
+ scheduler.schedule("dependency-change", { force: true });
131
+ await flushPromises();
132
+
133
+ expect(refresh).toHaveBeenCalledTimes(1);
134
+
135
+ finishInitialRefresh?.();
136
+ await flushPromises();
137
+ await flushPromises();
138
+
139
+ expect(refresh).toHaveBeenCalledTimes(2);
140
+ expect(refresh).toHaveBeenLastCalledWith("dependency-change", expect.anything());
141
+ expect(afterRefresh).toHaveBeenCalledTimes(2);
142
+
143
+ scheduler.schedule("ordinary-follow-up");
144
+ await flushPromises();
145
+ expect(refresh).toHaveBeenCalledTimes(2);
146
+
147
+ vi.advanceTimersByTime(4_999);
148
+ await flushPromises();
149
+ expect(refresh).toHaveBeenCalledTimes(2);
150
+
151
+ vi.advanceTimersByTime(1);
152
+ await flushPromises();
153
+ expect(refresh).toHaveBeenCalledTimes(3);
154
+ expect(refresh).toHaveBeenLastCalledWith("ordinary-follow-up", expect.anything());
155
+ });
156
+
157
+ it("supports forced refreshes for initial status reads", async () => {
158
+ vi.useFakeTimers();
159
+ const refresh = vi.fn<(...args: [string]) => Promise<void>>(() => Promise.resolve());
160
+ const afterRefresh = vi.fn();
161
+ const scheduler = createProjectRefreshScheduler(refresh, afterRefresh, 5_000);
162
+
163
+ scheduler.schedule("initial");
164
+ await flushPromises();
165
+ scheduler.schedule("forced", { force: true });
166
+ await flushPromises();
167
+
168
+ expect(refresh).toHaveBeenCalledTimes(2);
169
+ expect(refresh).toHaveBeenLastCalledWith("forced", expect.anything());
170
+ });
171
+
172
+ it("recovers from failed refreshes", async () => {
173
+ vi.useFakeTimers();
174
+ const refresh = vi
175
+ .fn<(...args: [string]) => Promise<void>>()
176
+ .mockRejectedValueOnce(new Error("slow git command failed"))
177
+ .mockResolvedValue(undefined);
178
+ const afterRefresh = vi.fn();
179
+ const scheduler = createProjectRefreshScheduler(refresh, afterRefresh, 5_000);
180
+
181
+ scheduler.schedule("initial");
182
+ await flushPromises();
183
+ scheduler.schedule("next", { force: true });
184
+ await flushPromises();
185
+
186
+ expect(refresh).toHaveBeenCalledTimes(2);
187
+ expect(refresh).toHaveBeenLastCalledWith("next", expect.anything());
188
+ expect(afterRefresh).toHaveBeenCalledTimes(2);
189
+ });
190
+
191
+ it("invalidates an active run before starting a dependency replacement", async () => {
192
+ const resolvers = new Map<string, () => void>();
193
+ const applied: string[] = [];
194
+ const refresh = vi.fn(
195
+ (target: string, run: { isCurrent: () => boolean }) =>
196
+ new Promise<void>((resolve) => {
197
+ resolvers.set(target, () => {
198
+ if (run.isCurrent()) applied.push(target);
199
+ resolve();
200
+ });
201
+ }),
202
+ );
203
+ const afterRefresh = vi.fn();
204
+ const scheduler = createProjectRefreshScheduler(refresh, afterRefresh, 0);
205
+
206
+ scheduler.schedule("old");
207
+ scheduler.invalidate();
208
+ scheduler.schedule("replacement", { force: true });
209
+ expect(refresh).toHaveBeenCalledTimes(2);
210
+
211
+ resolvers.get("old")?.();
212
+ await flushPromises();
213
+ expect(applied).toEqual([]);
214
+ expect(afterRefresh).not.toHaveBeenCalled();
215
+
216
+ resolvers.get("replacement")?.();
217
+ await flushPromises();
218
+ expect(applied).toEqual(["replacement"]);
219
+ expect(afterRefresh).toHaveBeenCalledTimes(1);
220
+ });
221
+
222
+ it("makes stopped runs stale when a restarted run finishes first", async () => {
223
+ const resolvers = new Map<string, () => void>();
224
+ const applied: string[] = [];
225
+ const refresh = vi.fn(
226
+ (target: string, run: { isCurrent: () => boolean }) =>
227
+ new Promise<void>((resolve) => {
228
+ resolvers.set(target, () => {
229
+ if (run.isCurrent()) applied.push(target);
230
+ resolve();
231
+ });
232
+ }),
233
+ );
234
+ const afterRefresh = vi.fn();
235
+ const scheduler = createProjectRefreshScheduler(refresh, afterRefresh, 0);
236
+
237
+ scheduler.schedule("old");
238
+ scheduler.stop();
239
+ scheduler.schedule("new");
240
+ expect(refresh).toHaveBeenCalledTimes(2);
241
+
242
+ resolvers.get("new")?.();
243
+ await flushPromises();
244
+ resolvers.get("old")?.();
245
+ await flushPromises();
246
+
247
+ expect(applied).toEqual(["new"]);
248
+ expect(afterRefresh).toHaveBeenCalledTimes(1);
249
+ });
250
+ });