gloomberb 0.2.0 → 0.3.2

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 (311) hide show
  1. package/README.md +20 -19
  2. package/bin/gloomberb +3 -0
  3. package/package.json +5 -4
  4. package/patches/@opentui%2Fcore@0.1.90.patch +356 -0
  5. package/src/app.tsx +1365 -0
  6. package/src/brokers/sync-broker-instance.test.ts +100 -0
  7. package/src/brokers/sync-broker-instance.ts +341 -0
  8. package/src/cli/commands/plugins.ts +173 -0
  9. package/src/cli/commands/portfolio.ts +210 -0
  10. package/src/cli/commands/search.ts +120 -0
  11. package/src/cli/commands/ticker.ts +375 -0
  12. package/src/cli/commands/watchlist.ts +210 -0
  13. package/src/cli/context.ts +62 -0
  14. package/src/cli/errors.ts +13 -0
  15. package/src/cli/helpers.ts +85 -0
  16. package/src/cli/index.ts +143 -0
  17. package/src/cli/ticker-resolution.ts +39 -0
  18. package/src/cli/types.ts +15 -0
  19. package/src/cli.test.ts +361 -0
  20. package/src/components/chart/chart-data.ts +239 -0
  21. package/src/components/chart/chart-renderer.ts +1199 -0
  22. package/src/components/chart/chart-types.ts +82 -0
  23. package/src/components/chart/chart-viewport.ts +21 -0
  24. package/src/components/chart/chart.test.ts +439 -0
  25. package/src/components/chart/comparison-chart-data.test.ts +109 -0
  26. package/src/components/chart/comparison-chart-data.ts +316 -0
  27. package/src/components/chart/comparison-chart-renderer.test.ts +84 -0
  28. package/src/components/chart/comparison-chart-renderer.ts +354 -0
  29. package/src/components/chart/comparison-stock-chart.tsx +1486 -0
  30. package/src/components/chart/cursor-motion.ts +48 -0
  31. package/src/components/chart/native/chart-rasterizer.ts +732 -0
  32. package/src/components/chart/native/kitty-adapter.ts +12 -0
  33. package/src/components/chart/native/kitty-manager.ts +142 -0
  34. package/src/components/chart/native/kitty-protocol.ts +110 -0
  35. package/src/components/chart/native/kitty-support.ts +71 -0
  36. package/src/components/chart/native/kitty.test.ts +620 -0
  37. package/src/components/chart/native/renderer-selection.ts +24 -0
  38. package/src/components/chart/native/surface-manager.ts +225 -0
  39. package/src/components/chart/native/surface-sync.ts +34 -0
  40. package/src/components/chart/stock-chart-renderer-switch.test.tsx +165 -0
  41. package/src/components/chart/stock-chart.test.ts +107 -0
  42. package/src/components/chart/stock-chart.tsx +1556 -0
  43. package/src/components/command-bar/__snapshots__/command-bar.test.tsx.snap +137 -0
  44. package/src/components/command-bar/command-bar.test.tsx +1546 -0
  45. package/src/components/command-bar/command-bar.tsx +4841 -0
  46. package/src/components/command-bar/command-registry.ts +269 -0
  47. package/src/components/command-bar/helpers.test.ts +224 -0
  48. package/src/components/command-bar/helpers.ts +371 -0
  49. package/src/components/command-bar/root-shortcuts.ts +158 -0
  50. package/src/components/command-bar/view-model.test.ts +152 -0
  51. package/src/components/command-bar/view-model.ts +185 -0
  52. package/src/components/command-bar/workflow-ops.test.ts +70 -0
  53. package/src/components/command-bar/workflow-ops.ts +433 -0
  54. package/src/components/command-bar/workflow-types.ts +127 -0
  55. package/src/components/detail-feed-view.test.tsx +105 -0
  56. package/src/components/detail-feed-view.tsx +295 -0
  57. package/src/components/index.ts +32 -0
  58. package/src/components/layout/floating-pane.tsx +49 -0
  59. package/src/components/layout/header.tsx +151 -0
  60. package/src/components/layout/pane-header.tsx +44 -0
  61. package/src/components/layout/pane.tsx +49 -0
  62. package/src/components/layout/pixel-mouse-mode.test.ts +122 -0
  63. package/src/components/layout/pixel-mouse.test.ts +40 -0
  64. package/src/components/layout/shell.test.tsx +651 -0
  65. package/src/components/layout/shell.tsx +1200 -0
  66. package/src/components/layout/status-bar.test.tsx +173 -0
  67. package/src/components/layout/status-bar.tsx +138 -0
  68. package/src/components/markdown-editor.tsx +134 -0
  69. package/src/components/markdown-text.tsx +218 -0
  70. package/src/components/onboarding/onboarding-wizard.test.tsx +184 -0
  71. package/src/components/onboarding/onboarding-wizard.tsx +1365 -0
  72. package/src/components/pane-settings-dialog.tsx +468 -0
  73. package/src/components/pane-template-wizard.test.tsx +52 -0
  74. package/src/components/pane-template-wizard.tsx +199 -0
  75. package/src/components/price-selector-dialog.tsx +195 -0
  76. package/src/components/spinner.tsx +2 -0
  77. package/src/components/tab-bar.tsx +17 -0
  78. package/src/components/ticker-badge-text.test.tsx +160 -0
  79. package/src/components/ticker-badge-text.tsx +58 -0
  80. package/src/components/ticker-badge.tsx +88 -0
  81. package/src/components/ticker-list-table.tsx +188 -0
  82. package/src/components/toggle-list.tsx +74 -0
  83. package/src/components/ui/button.tsx +77 -0
  84. package/src/components/ui/data-table.tsx +210 -0
  85. package/src/components/ui/external-link.tsx +22 -0
  86. package/src/components/ui/fields.tsx +187 -0
  87. package/src/components/ui/frame.tsx +73 -0
  88. package/src/components/ui/index.ts +53 -0
  89. package/src/components/ui/list-view.tsx +166 -0
  90. package/src/components/ui/loading.tsx +70 -0
  91. package/src/components/ui/status.tsx +83 -0
  92. package/src/components/ui/tabs.tsx +47 -0
  93. package/src/components/ui/toggle.tsx +164 -0
  94. package/src/components/ui/ui.test.tsx +134 -0
  95. package/src/data/app-persistence.ts +25 -0
  96. package/src/data/config-store.test.ts +347 -0
  97. package/src/data/config-store.ts +507 -0
  98. package/src/data/plugin-state-store.ts +102 -0
  99. package/src/data/resource-store.ts +323 -0
  100. package/src/data/session-store.ts +52 -0
  101. package/src/data/sqlite-cache.test.ts +103 -0
  102. package/src/data/sqlite-database.ts +62 -0
  103. package/src/data/sqlite-json.ts +19 -0
  104. package/src/data/ticker-repository.ts +45 -0
  105. package/src/data/ticker-store.ts +46 -0
  106. package/src/index.tsx +67 -0
  107. package/src/market-data/coordinator.test.ts +369 -0
  108. package/src/market-data/coordinator.ts +580 -0
  109. package/src/market-data/hooks.tsx +234 -0
  110. package/src/market-data/query-store.ts +28 -0
  111. package/src/market-data/request-types.test.ts +64 -0
  112. package/src/market-data/request-types.ts +70 -0
  113. package/src/market-data/result-types.ts +56 -0
  114. package/src/market-data/selectors.ts +166 -0
  115. package/src/market-data/trace.ts +7 -0
  116. package/src/pane-settings.test.ts +55 -0
  117. package/src/pane-settings.ts +73 -0
  118. package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +350 -0
  119. package/src/plugins/builtin/ai/gloomberb-cli.ts +70 -0
  120. package/src/plugins/builtin/ai/index.test.tsx +139 -0
  121. package/src/plugins/builtin/ai/index.tsx +86 -0
  122. package/src/plugins/builtin/ai/providers.ts +99 -0
  123. package/src/plugins/builtin/ai/runner.ts +82 -0
  124. package/src/plugins/builtin/ai/screener-contract.test.ts +117 -0
  125. package/src/plugins/builtin/ai/screener-contract.ts +174 -0
  126. package/src/plugins/builtin/ai/screener-pane.test.tsx +365 -0
  127. package/src/plugins/builtin/ai/screener-pane.tsx +1161 -0
  128. package/src/plugins/builtin/ai/settings.ts +79 -0
  129. package/src/plugins/builtin/ai/ticker-context.ts +64 -0
  130. package/src/plugins/builtin/ai/utils.ts +71 -0
  131. package/src/plugins/builtin/ask-ai.test.tsx +258 -0
  132. package/src/plugins/builtin/ask-ai.tsx +9 -0
  133. package/src/plugins/builtin/chat-controller.test.ts +217 -0
  134. package/src/plugins/builtin/chat-controller.ts +340 -0
  135. package/src/plugins/builtin/chat.test.tsx +451 -0
  136. package/src/plugins/builtin/chat.tsx +620 -0
  137. package/src/plugins/builtin/comparison-chart.test.tsx +277 -0
  138. package/src/plugins/builtin/comparison-chart.tsx +169 -0
  139. package/src/plugins/builtin/data-providers.ts +16 -0
  140. package/src/plugins/builtin/debug.tsx +351 -0
  141. package/src/plugins/builtin/help.test.tsx +110 -0
  142. package/src/plugins/builtin/help.tsx +281 -0
  143. package/src/plugins/builtin/layout-manager.tsx +264 -0
  144. package/src/plugins/builtin/manual-entry.tsx +25 -0
  145. package/src/plugins/builtin/news.tsx +143 -0
  146. package/src/plugins/builtin/notes-files.ts +58 -0
  147. package/src/plugins/builtin/notes.tsx +396 -0
  148. package/src/plugins/builtin/options-availability.test.tsx +179 -0
  149. package/src/plugins/builtin/options-availability.ts +158 -0
  150. package/src/plugins/builtin/options.tsx +364 -0
  151. package/src/plugins/builtin/portfolio-list/header.tsx +227 -0
  152. package/src/plugins/builtin/portfolio-list/index.tsx +99 -0
  153. package/src/plugins/builtin/portfolio-list/metrics.ts +460 -0
  154. package/src/plugins/builtin/portfolio-list/pane.tsx +533 -0
  155. package/src/plugins/builtin/portfolio-list/settings.ts +270 -0
  156. package/src/plugins/builtin/portfolio-list/summary.tsx +270 -0
  157. package/src/plugins/builtin/portfolio-list/table.tsx +64 -0
  158. package/src/plugins/builtin/portfolio-list.test.ts +74 -0
  159. package/src/plugins/builtin/portfolio-list.test.tsx +1164 -0
  160. package/src/plugins/builtin/portfolio-metrics.test.ts +224 -0
  161. package/src/plugins/builtin/quote-monitor.test.tsx +114 -0
  162. package/src/plugins/builtin/sec.tsx +250 -0
  163. package/src/plugins/builtin/ticker-detail/chart-tab.tsx +43 -0
  164. package/src/plugins/builtin/ticker-detail/financials-tab.tsx +390 -0
  165. package/src/plugins/builtin/ticker-detail/index.tsx +80 -0
  166. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +242 -0
  167. package/src/plugins/builtin/ticker-detail/pane.tsx +183 -0
  168. package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +83 -0
  169. package/src/plugins/builtin/ticker-detail/settings.ts +132 -0
  170. package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +193 -0
  171. package/src/plugins/builtin/ticker-detail.test.tsx +578 -0
  172. package/src/plugins/builtin/yahoo.ts +18 -0
  173. package/src/plugins/event-bus.ts +37 -0
  174. package/src/plugins/ibkr/__snapshots__/trade-tab.test.tsx.snap +35 -0
  175. package/src/plugins/ibkr/account-cache.test.ts +101 -0
  176. package/src/plugins/ibkr/account-cache.ts +137 -0
  177. package/src/plugins/ibkr/broker-adapter.ts +168 -0
  178. package/src/plugins/ibkr/config.test.ts +68 -0
  179. package/src/plugins/ibkr/config.ts +262 -0
  180. package/src/plugins/ibkr/dialogs.tsx +83 -0
  181. package/src/plugins/ibkr/flex.test.ts +107 -0
  182. package/src/plugins/ibkr/flex.ts +267 -0
  183. package/src/plugins/ibkr/fundamental-parser.ts +149 -0
  184. package/src/plugins/ibkr/gateway-helpers.ts +28 -0
  185. package/src/plugins/ibkr/gateway-selection.ts +51 -0
  186. package/src/plugins/ibkr/gateway-service.test.ts +450 -0
  187. package/src/plugins/ibkr/gateway-service.ts +2228 -0
  188. package/src/plugins/ibkr/index.test.tsx +183 -0
  189. package/src/plugins/ibkr/index.tsx +153 -0
  190. package/src/plugins/ibkr/instance-selection.test.ts +121 -0
  191. package/src/plugins/ibkr/instance-selection.ts +36 -0
  192. package/src/plugins/ibkr/price-normalization.test.ts +38 -0
  193. package/src/plugins/ibkr/price-normalization.ts +59 -0
  194. package/src/plugins/ibkr/trade-badge.tsx +27 -0
  195. package/src/plugins/ibkr/trade-tab.test.tsx +225 -0
  196. package/src/plugins/ibkr/trade-tab.tsx +1050 -0
  197. package/src/plugins/ibkr/trade-utils.test.ts +175 -0
  198. package/src/plugins/ibkr/trade-utils.ts +149 -0
  199. package/src/plugins/ibkr/trading-pane.tsx +438 -0
  200. package/src/plugins/ibkr/trading-state.test.ts +109 -0
  201. package/src/plugins/ibkr/trading-state.ts +339 -0
  202. package/src/plugins/loader.ts +71 -0
  203. package/src/plugins/pane-manager.test.ts +103 -0
  204. package/src/plugins/pane-manager.ts +974 -0
  205. package/src/plugins/plugin-persistence.ts +85 -0
  206. package/src/plugins/plugin-runtime.test.tsx +185 -0
  207. package/src/plugins/plugin-runtime.tsx +175 -0
  208. package/src/plugins/prediction-markets/cache.ts +114 -0
  209. package/src/plugins/prediction-markets/categories.ts +223 -0
  210. package/src/plugins/prediction-markets/chart.tsx +512 -0
  211. package/src/plugins/prediction-markets/columns.ts +133 -0
  212. package/src/plugins/prediction-markets/controller-data.ts +614 -0
  213. package/src/plugins/prediction-markets/controller.ts +725 -0
  214. package/src/plugins/prediction-markets/detail/book.tsx +118 -0
  215. package/src/plugins/prediction-markets/detail/outcomes.tsx +102 -0
  216. package/src/plugins/prediction-markets/detail/overview.tsx +73 -0
  217. package/src/plugins/prediction-markets/detail/pane.tsx +308 -0
  218. package/src/plugins/prediction-markets/detail/rules.tsx +16 -0
  219. package/src/plugins/prediction-markets/detail/shared.tsx +29 -0
  220. package/src/plugins/prediction-markets/detail/trades.tsx +30 -0
  221. package/src/plugins/prediction-markets/detail.test.tsx +47 -0
  222. package/src/plugins/prediction-markets/index.tsx +102 -0
  223. package/src/plugins/prediction-markets/keyboard.ts +64 -0
  224. package/src/plugins/prediction-markets/launch.test.ts +126 -0
  225. package/src/plugins/prediction-markets/launch.ts +240 -0
  226. package/src/plugins/prediction-markets/metrics.ts +311 -0
  227. package/src/plugins/prediction-markets/navigation.ts +90 -0
  228. package/src/plugins/prediction-markets/outcome-order.ts +19 -0
  229. package/src/plugins/prediction-markets/pane.test.tsx +425 -0
  230. package/src/plugins/prediction-markets/pane.tsx +294 -0
  231. package/src/plugins/prediction-markets/plugin.test.tsx +570 -0
  232. package/src/plugins/prediction-markets/rows.ts +229 -0
  233. package/src/plugins/prediction-markets/services/fetch.ts +97 -0
  234. package/src/plugins/prediction-markets/services/kalshi-adapter.ts +555 -0
  235. package/src/plugins/prediction-markets/services/polymarket-adapter.ts +660 -0
  236. package/src/plugins/prediction-markets/services/polymarket-ws.ts +108 -0
  237. package/src/plugins/prediction-markets/settings.ts +164 -0
  238. package/src/plugins/prediction-markets/status.ts +24 -0
  239. package/src/plugins/prediction-markets/table.tsx +84 -0
  240. package/src/plugins/prediction-markets/test-helpers.tsx +636 -0
  241. package/src/plugins/prediction-markets/types.ts +186 -0
  242. package/src/plugins/registry.ts +633 -0
  243. package/src/sources/exchange-rates.ts +18 -0
  244. package/src/sources/gloomberb-cloud.test.ts +199 -0
  245. package/src/sources/gloomberb-cloud.ts +326 -0
  246. package/src/sources/provider-errors.ts +19 -0
  247. package/src/sources/provider-router.test.ts +1476 -0
  248. package/src/sources/provider-router.ts +1405 -0
  249. package/src/sources/sec-edgar.test.ts +286 -0
  250. package/src/sources/sec-edgar.ts +526 -0
  251. package/src/sources/yahoo-finance.test.ts +14 -0
  252. package/src/sources/yahoo-finance.ts +1017 -0
  253. package/src/state/app-activity.test.ts +41 -0
  254. package/src/state/app-activity.ts +106 -0
  255. package/src/state/app-bootstrap.test.ts +471 -0
  256. package/src/state/app-bootstrap.ts +308 -0
  257. package/src/state/app-context.test.ts +406 -0
  258. package/src/state/app-context.test.tsx +162 -0
  259. package/src/state/app-context.tsx +981 -0
  260. package/src/state/selectors.ts +67 -0
  261. package/src/state/session-persistence.test.ts +128 -0
  262. package/src/state/session-persistence.ts +161 -0
  263. package/src/state/ticker-refresh-queue.test.ts +46 -0
  264. package/src/state/ticker-refresh-queue.ts +40 -0
  265. package/src/state/use-inline-tickers.ts +174 -0
  266. package/src/state/use-quote-streaming.test.ts +37 -0
  267. package/src/state/use-quote-streaming.test.tsx +70 -0
  268. package/src/state/use-quote-streaming.ts +80 -0
  269. package/src/state/use-session-snapshot.ts +84 -0
  270. package/src/test-support/data-provider.ts +22 -0
  271. package/src/theme/color-utils.ts +67 -0
  272. package/src/theme/colors.ts +128 -0
  273. package/src/theme/themes.test.ts +45 -0
  274. package/src/theme/themes.ts +491 -0
  275. package/src/types/broker.ts +105 -0
  276. package/src/types/config.ts +444 -0
  277. package/src/types/data-provider.ts +81 -0
  278. package/src/types/financials.ts +142 -0
  279. package/src/types/instrument.ts +29 -0
  280. package/src/types/persistence.ts +21 -0
  281. package/src/types/plugin.ts +354 -0
  282. package/src/types/ticker.ts +58 -0
  283. package/src/types/trading.ts +85 -0
  284. package/src/updater.test.ts +282 -0
  285. package/src/updater.ts +303 -0
  286. package/src/utils/api-client.ts +666 -0
  287. package/src/utils/ascii-chart.ts +172 -0
  288. package/src/utils/broker-instances.ts +47 -0
  289. package/src/utils/cli-output.ts +83 -0
  290. package/src/utils/currency-units.test.ts +49 -0
  291. package/src/utils/currency-units.ts +82 -0
  292. package/src/utils/debug-log.ts +133 -0
  293. package/src/utils/exchange-rate-map.ts +8 -0
  294. package/src/utils/format.ts +158 -0
  295. package/src/utils/fuzzy-search.ts +34 -0
  296. package/src/utils/market-status.ts +95 -0
  297. package/src/utils/options.ts +86 -0
  298. package/src/utils/price-history.ts +39 -0
  299. package/src/utils/quote-time.test.ts +22 -0
  300. package/src/utils/quote-time.ts +50 -0
  301. package/src/utils/sec.test.ts +55 -0
  302. package/src/utils/sec.ts +45 -0
  303. package/src/utils/ticker-list.test.ts +22 -0
  304. package/src/utils/ticker-list.ts +35 -0
  305. package/src/utils/ticker-metadata.ts +51 -0
  306. package/src/utils/ticker-search.test.ts +227 -0
  307. package/src/utils/ticker-search.ts +764 -0
  308. package/src/utils/ticker-tokenizer.test.ts +48 -0
  309. package/src/utils/ticker-tokenizer.ts +72 -0
  310. package/src/version.ts +1 -0
  311. package/install.mjs +0 -68
@@ -0,0 +1,282 @@
1
+ import { afterEach, describe, expect, it, test } from "bun:test";
2
+ import { chmodSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "fs";
3
+ import { tmpdir } from "os";
4
+ import { join } from "path";
5
+ import { gzipSync } from "zlib";
6
+ import {
7
+ canSelfUpdate,
8
+ checkForUpdate,
9
+ checkForUpdateDetailed,
10
+ detectUpdateAction,
11
+ performUpdate,
12
+ resolveSelfUpdateTargetPath,
13
+ type ReleaseInfo,
14
+ type UpdateProgress,
15
+ } from "./updater";
16
+
17
+ const originalFetch = globalThis.fetch;
18
+
19
+ afterEach(() => {
20
+ globalThis.fetch = originalFetch;
21
+ });
22
+
23
+ function expectedAssetName(compressed = false): string {
24
+ const os = process.platform === "darwin" ? "darwin" : "linux";
25
+ const arch = os === "darwin" || process.arch === "arm64" ? "arm64" : "x64";
26
+ return compressed ? `gloomberb-${os}-${arch}.gz` : `gloomberb-${os}-${arch}`;
27
+ }
28
+
29
+ describe("detectUpdateAction", () => {
30
+ test("uses self-update for standalone binaries", () => {
31
+ expect(detectUpdateAction(
32
+ "/Users/vince/.local/bin/gloomberb",
33
+ ["/Users/vince/.local/bin/gloomberb"],
34
+ )).toEqual({ kind: "self" });
35
+ });
36
+
37
+ test("uses manual bun updates for bun-managed installs", () => {
38
+ expect(detectUpdateAction(
39
+ "/opt/homebrew/bin/bun",
40
+ ["/opt/homebrew/bin/bun", "/Users/vince/.bun/install/global/node_modules/gloomberb/bin/gloomberb"],
41
+ )).toEqual({
42
+ kind: "manual",
43
+ command: "bun install -g gloomberb@latest",
44
+ });
45
+ });
46
+
47
+ test("uses manual npm updates for node-managed installs", () => {
48
+ expect(detectUpdateAction(
49
+ "/opt/homebrew/bin/node",
50
+ ["/opt/homebrew/bin/node", "/usr/local/lib/node_modules/gloomberb/bin/gloomberb"],
51
+ )).toEqual({
52
+ kind: "manual",
53
+ command: "npm install -g gloomberb@latest",
54
+ });
55
+ });
56
+
57
+ test("skips updates when launched from source under bun", () => {
58
+ expect(detectUpdateAction(
59
+ "/opt/homebrew/bin/bun",
60
+ ["/opt/homebrew/bin/bun", "src/index.tsx"],
61
+ )).toBeNull();
62
+ });
63
+ });
64
+
65
+ describe("resolveSelfUpdateTargetPath", () => {
66
+ it("rejects Bun runtime paths", () => {
67
+ expect(resolveSelfUpdateTargetPath(
68
+ "/Users/vince/.bun/bin/bun",
69
+ ["/Users/vince/.bun/bin/bun", "src/index.tsx"],
70
+ )).toBeNull();
71
+ });
72
+
73
+ it("rejects Node runtime paths", () => {
74
+ expect(resolveSelfUpdateTargetPath(
75
+ "/usr/local/bin/node",
76
+ ["/usr/local/bin/node", "dist/index.js"],
77
+ )).toBeNull();
78
+ });
79
+
80
+ it("accepts packaged gloomberb binaries", () => {
81
+ expect(resolveSelfUpdateTargetPath(
82
+ "/Applications/gloomberb",
83
+ ["/Applications/gloomberb"],
84
+ )).toBe("/Applications/gloomberb");
85
+ });
86
+ });
87
+
88
+ describe("canSelfUpdate", () => {
89
+ test("returns false for manual update releases", () => {
90
+ expect(canSelfUpdate({
91
+ updateAction: { kind: "manual", command: "bun install -g gloomberb@latest" },
92
+ })).toBe(false);
93
+ });
94
+
95
+ test("returns true for self-update releases", () => {
96
+ expect(canSelfUpdate({
97
+ updateAction: { kind: "self" },
98
+ })).toBe(true);
99
+ });
100
+ });
101
+
102
+ describe("checkForUpdate", () => {
103
+ it("skips update checks when running from source", async () => {
104
+ const originalExecPath = process.execPath;
105
+ const originalArgv = process.argv;
106
+
107
+ try {
108
+ Object.defineProperty(process, "execPath", { value: "/Users/vince/.bun/bin/bun", configurable: true });
109
+ Object.defineProperty(process, "argv", {
110
+ value: ["/Users/vince/.bun/bin/bun", "src/index.tsx"],
111
+ configurable: true,
112
+ });
113
+
114
+ await expect(checkForUpdate("0.2.0")).resolves.toBeNull();
115
+ } finally {
116
+ Object.defineProperty(process, "execPath", { value: originalExecPath, configurable: true });
117
+ Object.defineProperty(process, "argv", { value: originalArgv, configurable: true });
118
+ }
119
+ });
120
+
121
+ it("finds gzipped release assets published on GitHub", async () => {
122
+ const originalExecPath = process.execPath;
123
+ const originalArgv = process.argv;
124
+ globalThis.fetch = (async () => new Response(JSON.stringify({
125
+ tag_name: "v0.3.2",
126
+ published_at: "2026-04-03T00:00:00Z",
127
+ assets: [{
128
+ name: expectedAssetName(true),
129
+ browser_download_url: "https://example.com/gloomberb.gz",
130
+ }],
131
+ }), {
132
+ status: 200,
133
+ headers: { "content-type": "application/json" },
134
+ })) as typeof fetch;
135
+
136
+ try {
137
+ Object.defineProperty(process, "execPath", { value: "/Applications/gloomberb", configurable: true });
138
+ Object.defineProperty(process, "argv", {
139
+ value: ["/Applications/gloomberb"],
140
+ configurable: true,
141
+ });
142
+
143
+ await expect(checkForUpdate("0.3.1")).resolves.toEqual({
144
+ version: "0.3.2",
145
+ tagName: "v0.3.2",
146
+ downloadUrl: "https://example.com/gloomberb.gz",
147
+ publishedAt: "2026-04-03T00:00:00Z",
148
+ updateAction: { kind: "self" },
149
+ compressed: true,
150
+ });
151
+ } finally {
152
+ Object.defineProperty(process, "execPath", { value: originalExecPath, configurable: true });
153
+ Object.defineProperty(process, "argv", { value: originalArgv, configurable: true });
154
+ }
155
+ });
156
+ });
157
+
158
+ describe("checkForUpdateDetailed", () => {
159
+ it("returns a useful error when GitHub rejects the request", async () => {
160
+ const originalExecPath = process.execPath;
161
+ const originalArgv = process.argv;
162
+ globalThis.fetch = (async () => new Response("busy", { status: 503 })) as typeof fetch;
163
+
164
+ try {
165
+ Object.defineProperty(process, "execPath", { value: "/Applications/gloomberb", configurable: true });
166
+ Object.defineProperty(process, "argv", {
167
+ value: ["/Applications/gloomberb"],
168
+ configurable: true,
169
+ });
170
+
171
+ await expect(checkForUpdateDetailed("0.3.1")).resolves.toEqual({
172
+ kind: "error",
173
+ error: "GitHub returned 503",
174
+ });
175
+ } finally {
176
+ Object.defineProperty(process, "execPath", { value: originalExecPath, configurable: true });
177
+ Object.defineProperty(process, "argv", { value: originalArgv, configurable: true });
178
+ }
179
+ });
180
+ });
181
+
182
+ describe("performUpdate", () => {
183
+ it("returns a manual command instead of trying to overwrite Bun-managed installs", async () => {
184
+ const progress: UpdateProgress[] = [];
185
+ const release: ReleaseInfo = {
186
+ version: "9.9.9",
187
+ tagName: "v9.9.9",
188
+ downloadUrl: "https://example.com/gloomberb-darwin-arm64",
189
+ publishedAt: "2026-04-01T00:00:00Z",
190
+ updateAction: { kind: "manual", command: "bun install -g gloomberb@latest" },
191
+ };
192
+
193
+ await performUpdate(release, (entry) => {
194
+ progress.push(entry);
195
+ });
196
+
197
+ expect(progress).toEqual([
198
+ {
199
+ phase: "error",
200
+ error: "Run bun install -g gloomberb@latest",
201
+ },
202
+ ]);
203
+ });
204
+
205
+ it("returns an explicit error instead of overwriting Bun when execution context changes", async () => {
206
+ const originalExecPath = process.execPath;
207
+ const originalArgv = process.argv;
208
+ const progress: UpdateProgress[] = [];
209
+ const release: ReleaseInfo = {
210
+ version: "9.9.9",
211
+ tagName: "v9.9.9",
212
+ downloadUrl: "https://example.com/gloomberb-darwin-arm64",
213
+ publishedAt: "2026-04-01T00:00:00Z",
214
+ updateAction: { kind: "self" },
215
+ };
216
+
217
+ try {
218
+ Object.defineProperty(process, "execPath", { value: "/Users/vince/.bun/bin/bun", configurable: true });
219
+ Object.defineProperty(process, "argv", {
220
+ value: ["/Users/vince/.bun/bin/bun", "src/index.tsx"],
221
+ configurable: true,
222
+ });
223
+
224
+ await performUpdate(release, (entry) => {
225
+ progress.push(entry);
226
+ });
227
+
228
+ expect(progress).toEqual([
229
+ {
230
+ phase: "error",
231
+ error: "Self-update is unavailable when running from source or via Bun/Node. Relaunch the packaged gloomberb binary to update.",
232
+ },
233
+ ]);
234
+ } finally {
235
+ Object.defineProperty(process, "execPath", { value: originalExecPath, configurable: true });
236
+ Object.defineProperty(process, "argv", { value: originalArgv, configurable: true });
237
+ }
238
+ });
239
+
240
+ it("decompresses gzipped release assets before replacing the binary", async () => {
241
+ const originalExecPath = process.execPath;
242
+ const originalArgv = process.argv;
243
+ const tempDir = mkdtempSync(join(tmpdir(), "gloomberb-update-"));
244
+ const execPath = join(tempDir, "gloomberb");
245
+ const nextBinary = Buffer.from("new-binary");
246
+ const payload = gzipSync(nextBinary);
247
+ const progress: UpdateProgress[] = [];
248
+
249
+ writeFileSync(execPath, Buffer.from("old-binary"));
250
+ chmodSync(execPath, 0o755);
251
+ globalThis.fetch = (async () => new Response(payload, {
252
+ status: 200,
253
+ headers: { "content-length": String(payload.length) },
254
+ })) as typeof fetch;
255
+
256
+ try {
257
+ Object.defineProperty(process, "execPath", { value: execPath, configurable: true });
258
+ Object.defineProperty(process, "argv", {
259
+ value: [execPath],
260
+ configurable: true,
261
+ });
262
+
263
+ await performUpdate({
264
+ version: "9.9.9",
265
+ tagName: "v9.9.9",
266
+ downloadUrl: "https://example.com/gloomberb.gz",
267
+ publishedAt: "2026-04-03T00:00:00Z",
268
+ updateAction: { kind: "self" },
269
+ compressed: true,
270
+ }, (entry) => {
271
+ progress.push(entry);
272
+ });
273
+
274
+ expect(readFileSync(execPath)).toEqual(nextBinary);
275
+ expect(progress.at(-1)).toEqual({ phase: "done" });
276
+ } finally {
277
+ Object.defineProperty(process, "execPath", { value: originalExecPath, configurable: true });
278
+ Object.defineProperty(process, "argv", { value: originalArgv, configurable: true });
279
+ rmSync(tempDir, { recursive: true, force: true });
280
+ }
281
+ });
282
+ });
package/src/updater.ts ADDED
@@ -0,0 +1,303 @@
1
+ import { writeFileSync, renameSync, unlinkSync, chmodSync, realpathSync } from "fs";
2
+ import { basename } from "path";
3
+ import { gunzipSync } from "zlib";
4
+
5
+ const REPO = "vincelwt/gloomberb";
6
+ const API_URL = `https://api.github.com/repos/${REPO}/releases/latest`;
7
+
8
+ export interface ReleaseInfo {
9
+ version: string;
10
+ tagName: string;
11
+ downloadUrl: string;
12
+ publishedAt: string;
13
+ updateAction: UpdateAction;
14
+ compressed?: boolean;
15
+ }
16
+
17
+ export interface UpdateProgress {
18
+ phase: "downloading" | "replacing" | "done" | "error";
19
+ percent?: number;
20
+ error?: string;
21
+ }
22
+
23
+ export type UpdateAction =
24
+ | { kind: "self" }
25
+ | { kind: "manual"; command: string };
26
+
27
+ export type UpdateCheckResult =
28
+ | { kind: "available"; release: ReleaseInfo }
29
+ | { kind: "current" }
30
+ | { kind: "disabled" }
31
+ | { kind: "error"; error: string };
32
+
33
+ function compareSemver(a: string, b: string): number {
34
+ const pa = a.split(".").map(Number);
35
+ const pb = b.split(".").map(Number);
36
+ for (let i = 0; i < 3; i++) {
37
+ const diff = (pa[i] ?? 0) - (pb[i] ?? 0);
38
+ if (diff !== 0) return diff;
39
+ }
40
+ return 0;
41
+ }
42
+
43
+ function getAssetBaseName(): string {
44
+ const os = process.platform === "darwin" ? "darwin" : "linux";
45
+ // macOS x64 uses arm64 binary (runs via Rosetta 2)
46
+ const arch = os === "darwin" || process.arch === "arm64" ? "arm64" : "x64";
47
+ return `gloomberb-${os}-${arch}`;
48
+ }
49
+
50
+ function resolveReleaseAsset(
51
+ assets: { name: string; browser_download_url: string }[],
52
+ ): { name: string; browser_download_url: string; compressed: boolean } | null {
53
+ const assetBaseName = getAssetBaseName();
54
+ const gzAsset = assets.find((asset) => asset.name === `${assetBaseName}.gz`);
55
+ if (gzAsset) {
56
+ return { ...gzAsset, compressed: true };
57
+ }
58
+
59
+ const rawAsset = assets.find((asset) => asset.name === assetBaseName);
60
+ if (rawAsset) {
61
+ return { ...rawAsset, compressed: false };
62
+ }
63
+
64
+ return null;
65
+ }
66
+
67
+ function normalizePath(value: string): string {
68
+ return value.replace(/\\/g, "/").toLowerCase();
69
+ }
70
+
71
+ function tryRealpath(value: string): string {
72
+ if (!value) return value;
73
+ try {
74
+ return realpathSync.native(value);
75
+ } catch {
76
+ return value;
77
+ }
78
+ }
79
+
80
+ function resolveEntrypointPath(argv = process.argv): string {
81
+ return tryRealpath(argv[1] ?? "");
82
+ }
83
+
84
+ function isSourceEntrypoint(entrypoint: string): boolean {
85
+ const sourceEntrypointPattern = /\.(c|m)?jsx?$/;
86
+ const tsEntrypointPattern = /\.(c|m)?tsx?$/;
87
+ return sourceEntrypointPattern.test(entrypoint) || tsEntrypointPattern.test(entrypoint);
88
+ }
89
+
90
+ export function resolveSelfUpdateTargetPath(
91
+ execPath = process.execPath,
92
+ argv = process.argv,
93
+ ): string | null {
94
+ const resolvedExecPath = tryRealpath(execPath);
95
+ const normalizedExecPath = normalizePath(resolvedExecPath);
96
+ const execBase = basename(normalizedExecPath);
97
+ const runtimeExecutables = new Set(["bun", "bunx", "node", "nodejs", "npm", "npx", "pnpm", "yarn"]);
98
+ if (runtimeExecutables.has(execBase)) return null;
99
+ if (normalizedExecPath.includes("/.bun/bin/")) return null;
100
+
101
+ const entrypoint = normalizePath(resolveEntrypointPath(argv));
102
+ if (isSourceEntrypoint(entrypoint)) return null;
103
+
104
+ return resolvedExecPath;
105
+ }
106
+
107
+ export function detectUpdateAction(
108
+ execPath = process.execPath,
109
+ argv = process.argv,
110
+ ): UpdateAction | null {
111
+ if (resolveSelfUpdateTargetPath(execPath, argv)) {
112
+ return { kind: "self" };
113
+ }
114
+
115
+ const normalizedExecPath = normalizePath(tryRealpath(execPath));
116
+ const execBase = basename(normalizedExecPath);
117
+ const entrypoint = normalizePath(resolveEntrypointPath(argv));
118
+
119
+ if (!entrypoint || isSourceEntrypoint(entrypoint)) return null;
120
+
121
+ if (
122
+ execBase === "bun"
123
+ || execBase === "bunx"
124
+ || normalizedExecPath.includes("/.bun/bin/")
125
+ || entrypoint.includes("/.bun/install/")
126
+ || entrypoint.includes("/install/global/")
127
+ ) {
128
+ return { kind: "manual", command: "bun install -g gloomberb@latest" };
129
+ }
130
+
131
+ if (
132
+ execBase === "node"
133
+ || execBase === "nodejs"
134
+ || execBase === "npm"
135
+ || execBase === "npx"
136
+ || execBase === "pnpm"
137
+ || execBase === "yarn"
138
+ || entrypoint.includes("/lib/node_modules/")
139
+ || entrypoint.includes("/node_modules/")
140
+ ) {
141
+ return { kind: "manual", command: "npm install -g gloomberb@latest" };
142
+ }
143
+
144
+ return null;
145
+ }
146
+
147
+ export function canSelfUpdate(release: Pick<ReleaseInfo, "updateAction"> | null | undefined): boolean {
148
+ return release?.updateAction.kind === "self";
149
+ }
150
+
151
+ export async function checkForUpdateDetailed(
152
+ currentVersion: string,
153
+ ): Promise<UpdateCheckResult> {
154
+ const updateAction = detectUpdateAction();
155
+ if (!updateAction) {
156
+ return { kind: "disabled" };
157
+ }
158
+
159
+ let timeout: ReturnType<typeof setTimeout> | undefined;
160
+ try {
161
+ const controller = new AbortController();
162
+ timeout = setTimeout(() => controller.abort(), 5000);
163
+
164
+ const res = await fetch(API_URL, {
165
+ signal: controller.signal,
166
+ headers: { Accept: "application/vnd.github+json" },
167
+ });
168
+
169
+ if (!res.ok) {
170
+ return { kind: "error", error: `GitHub returned ${res.status}` };
171
+ }
172
+
173
+ const data = (await res.json()) as {
174
+ tag_name: string;
175
+ published_at: string;
176
+ assets: { name: string; browser_download_url: string }[];
177
+ };
178
+
179
+ const version = data.tag_name.replace(/^v/, "");
180
+ if (compareSemver(version, currentVersion) <= 0) {
181
+ return { kind: "current" };
182
+ }
183
+
184
+ const asset = resolveReleaseAsset(data.assets);
185
+ if (!asset) {
186
+ return {
187
+ kind: "error",
188
+ error: `No compatible release asset found for ${getAssetBaseName()}`,
189
+ };
190
+ }
191
+
192
+ return {
193
+ kind: "available",
194
+ release: {
195
+ version,
196
+ tagName: data.tag_name,
197
+ downloadUrl: asset.browser_download_url,
198
+ publishedAt: data.published_at,
199
+ updateAction,
200
+ compressed: asset.compressed,
201
+ },
202
+ };
203
+ } catch (error: unknown) {
204
+ if (error instanceof Error && error.name === "AbortError") {
205
+ return { kind: "error", error: "Update check timed out" };
206
+ }
207
+ return {
208
+ kind: "error",
209
+ error: error instanceof Error ? error.message : "Update check failed",
210
+ };
211
+ } finally {
212
+ if (timeout) clearTimeout(timeout);
213
+ }
214
+ }
215
+
216
+ export async function checkForUpdate(
217
+ currentVersion: string,
218
+ ): Promise<ReleaseInfo | null> {
219
+ const result = await checkForUpdateDetailed(currentVersion);
220
+ return result.kind === "available" ? result.release : null;
221
+ }
222
+
223
+ export async function performUpdate(
224
+ release: ReleaseInfo,
225
+ onProgress: (p: UpdateProgress) => void,
226
+ ): Promise<void> {
227
+ if (!canSelfUpdate(release)) {
228
+ onProgress({
229
+ phase: "error",
230
+ error: `Run ${release.updateAction.command}`,
231
+ });
232
+ return;
233
+ }
234
+
235
+ const execPath = resolveSelfUpdateTargetPath();
236
+ if (!execPath) {
237
+ onProgress({
238
+ phase: "error",
239
+ error: "Self-update is unavailable when running from source or via Bun/Node. Relaunch the packaged gloomberb binary to update.",
240
+ });
241
+ return;
242
+ }
243
+
244
+ const updatePath = execPath + ".update";
245
+ const oldPath = execPath + ".old";
246
+
247
+ try {
248
+ onProgress({ phase: "downloading", percent: 0 });
249
+
250
+ const res = await fetch(release.downloadUrl);
251
+ if (!res.ok || !res.body) {
252
+ throw new Error(`Download failed: ${res.status}`);
253
+ }
254
+
255
+ const contentLength = Number(res.headers.get("content-length") || 0);
256
+ const reader = res.body.getReader();
257
+ const chunks: Uint8Array[] = [];
258
+ let received = 0;
259
+
260
+ while (true) {
261
+ const { done, value } = await reader.read();
262
+ if (done) break;
263
+ chunks.push(value);
264
+ received += value.length;
265
+ if (contentLength > 0) {
266
+ onProgress({
267
+ phase: "downloading",
268
+ percent: Math.round((received / contentLength) * 100),
269
+ });
270
+ }
271
+ }
272
+
273
+ // Write to temp file
274
+ const blob = new Blob(chunks);
275
+ const buffer = await blob.arrayBuffer();
276
+ const downloaded = Buffer.from(buffer);
277
+ const nextBinary = release.compressed ? gunzipSync(downloaded) : downloaded;
278
+ writeFileSync(updatePath, nextBinary);
279
+ chmodSync(updatePath, 0o755);
280
+
281
+ // Swap binaries
282
+ onProgress({ phase: "replacing" });
283
+ try {
284
+ unlinkSync(oldPath);
285
+ } catch {}
286
+ renameSync(execPath, oldPath);
287
+ renameSync(updatePath, execPath);
288
+ try {
289
+ unlinkSync(oldPath);
290
+ } catch {}
291
+
292
+ onProgress({ phase: "done" });
293
+ } catch (err: unknown) {
294
+ // Clean up temp file on failure
295
+ try {
296
+ unlinkSync(updatePath);
297
+ } catch {}
298
+ onProgress({
299
+ phase: "error",
300
+ error: err instanceof Error ? err.message : "Update failed",
301
+ });
302
+ }
303
+ }