@tokentop/ttop 0.2.2 → 0.4.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 (160) hide show
  1. package/README.md +2 -5
  2. package/package.json +8 -5
  3. package/src/agents/aggregator.ts +39 -19
  4. package/src/agents/costing.ts +11 -9
  5. package/src/agents/index.ts +7 -9
  6. package/src/agents/types.ts +2 -2
  7. package/src/cli.ts +28 -23
  8. package/src/config/schema.test.ts +42 -40
  9. package/src/config/schema.ts +26 -26
  10. package/src/demo/simulator.ts +161 -122
  11. package/src/plugins/agents/index.ts +2 -2
  12. package/src/plugins/auth-sources.ts +42 -38
  13. package/src/plugins/lifecycle.ts +42 -38
  14. package/src/plugins/loader.test.ts +49 -49
  15. package/src/plugins/loader.ts +67 -63
  16. package/src/plugins/notification-bus.ts +21 -16
  17. package/src/plugins/notifications/index.ts +2 -2
  18. package/src/plugins/notifications/terminal-bell.ts +36 -23
  19. package/src/plugins/notifications/visual-flash.ts +31 -19
  20. package/src/plugins/npm-installer.ts +23 -25
  21. package/src/plugins/plugin-context-factory.ts +10 -10
  22. package/src/plugins/plugin-host.ts +12 -5
  23. package/src/plugins/providers/anthropic.ts +64 -57
  24. package/src/plugins/providers/antigravity.ts +81 -69
  25. package/src/plugins/providers/codex.ts +42 -42
  26. package/src/plugins/providers/gemini.ts +106 -87
  27. package/src/plugins/providers/github-copilot.ts +49 -45
  28. package/src/plugins/providers/index.ts +10 -12
  29. package/src/plugins/providers/minimax.ts +50 -44
  30. package/src/plugins/providers/openai-api.ts +82 -62
  31. package/src/plugins/providers/opencode-zen.ts +46 -39
  32. package/src/plugins/providers/perplexity.ts +55 -46
  33. package/src/plugins/providers/zai.ts +68 -57
  34. package/src/plugins/registry.ts +80 -71
  35. package/src/plugins/sandbox-guard.test.ts +45 -48
  36. package/src/plugins/sandbox-guard.ts +20 -25
  37. package/src/plugins/sandbox.ts +26 -48
  38. package/src/plugins/themes/catppuccin-latte.ts +29 -29
  39. package/src/plugins/themes/catppuccin-mocha.ts +29 -29
  40. package/src/plugins/themes/claude-code.ts +29 -29
  41. package/src/plugins/themes/dracula.ts +29 -29
  42. package/src/plugins/themes/github-light.ts +29 -29
  43. package/src/plugins/themes/gruvbox-dark.ts +29 -29
  44. package/src/plugins/themes/gruvbox-light.ts +29 -29
  45. package/src/plugins/themes/index.ts +15 -15
  46. package/src/plugins/themes/kanagawa.ts +29 -29
  47. package/src/plugins/themes/nord.ts +29 -29
  48. package/src/plugins/themes/one-dark.ts +29 -29
  49. package/src/plugins/themes/opencode.ts +31 -31
  50. package/src/plugins/themes/rose-pine-dawn.ts +29 -29
  51. package/src/plugins/themes/rose-pine.ts +29 -29
  52. package/src/plugins/themes/solarized-light.ts +29 -29
  53. package/src/plugins/themes/tokyo-night.ts +29 -29
  54. package/src/plugins/types/agent.ts +5 -5
  55. package/src/plugins/types/base.ts +35 -24
  56. package/src/plugins/types/index.ts +9 -9
  57. package/src/plugins/types/notification.ts +12 -12
  58. package/src/plugins/types/provider.ts +12 -8
  59. package/src/plugins/types/theme.ts +31 -23
  60. package/src/plugins/update-checker.ts +6 -6
  61. package/src/pricing/estimator.test.ts +39 -44
  62. package/src/pricing/estimator.ts +7 -13
  63. package/src/pricing/fallback.test.ts +30 -34
  64. package/src/pricing/fallback.ts +54 -27
  65. package/src/pricing/index.ts +18 -12
  66. package/src/pricing/models-dev.ts +6 -6
  67. package/src/storage/database.ts +56 -46
  68. package/src/storage/db.ts +12 -12
  69. package/src/storage/index.ts +44 -45
  70. package/src/storage/migrations/index.ts +2 -2
  71. package/src/storage/paths.ts +13 -13
  72. package/src/storage/persistence-service.ts +206 -0
  73. package/src/storage/repos/agentSessions.ts +33 -23
  74. package/src/storage/repos/providerSnapshots.ts +5 -5
  75. package/src/storage/repos/usageEvents.ts +40 -28
  76. package/src/storage/retention.ts +146 -0
  77. package/src/storage/types.ts +5 -5
  78. package/src/tui/App.tsx +180 -137
  79. package/src/tui/components/CommandPalette.tsx +46 -34
  80. package/src/tui/components/DebugConsole.tsx +39 -33
  81. package/src/tui/components/DebugPanel.tsx +314 -183
  82. package/src/tui/components/FooterHints.tsx +2 -2
  83. package/src/tui/components/GhostProviderCard.tsx +37 -35
  84. package/src/tui/components/Header.tsx +68 -35
  85. package/src/tui/components/HelpOverlay.tsx +99 -29
  86. package/src/tui/components/InlineGauge.tsx +10 -7
  87. package/src/tui/components/InlineSparkline.tsx +17 -11
  88. package/src/tui/components/KpiStrip.tsx +128 -98
  89. package/src/tui/components/LimitGauge.tsx +96 -59
  90. package/src/tui/components/ModalBackdrop.tsx +4 -4
  91. package/src/tui/components/ProviderAggregateStrip.tsx +69 -38
  92. package/src/tui/components/ProviderCard.tsx +232 -226
  93. package/src/tui/components/ProviderDetailPanel.tsx +133 -58
  94. package/src/tui/components/ProviderLimitsPanel.tsx +126 -89
  95. package/src/tui/components/ProvidersList.tsx +215 -126
  96. package/src/tui/components/SessionDetailsDrawer.tsx +191 -94
  97. package/src/tui/components/SessionsTable.tsx +320 -169
  98. package/src/tui/components/SettingsModal.tsx +593 -269
  99. package/src/tui/components/SidebarBreakdown.tsx +30 -16
  100. package/src/tui/components/Skeleton.tsx +24 -17
  101. package/src/tui/components/SmartSidebar.tsx +360 -258
  102. package/src/tui/components/Sparkline.tsx +64 -52
  103. package/src/tui/components/Spinner.tsx +3 -3
  104. package/src/tui/components/StatusBar.tsx +27 -27
  105. package/src/tui/components/ThemePicker.tsx +67 -66
  106. package/src/tui/components/Toast.tsx +9 -6
  107. package/src/tui/components/UsageGauge.tsx +10 -13
  108. package/src/tui/components/index.ts +5 -5
  109. package/src/tui/contexts/AgentSessionContext.tsx +164 -151
  110. package/src/tui/contexts/ConfigContext.tsx +15 -13
  111. package/src/tui/contexts/DashboardRuntimeContext.tsx +26 -26
  112. package/src/tui/contexts/DemoModeContext.tsx +22 -15
  113. package/src/tui/contexts/DrawerContext.tsx +11 -9
  114. package/src/tui/contexts/InputContext.tsx +2 -2
  115. package/src/tui/contexts/LogContext.tsx +62 -47
  116. package/src/tui/contexts/PluginContext.tsx +238 -183
  117. package/src/tui/contexts/RealTimeActivityContext.tsx +70 -36
  118. package/src/tui/contexts/StorageContext.tsx +95 -149
  119. package/src/tui/contexts/ThemeContext.tsx +21 -16
  120. package/src/tui/contexts/TimeWindowContext.tsx +72 -61
  121. package/src/tui/contexts/ToastContext.tsx +10 -7
  122. package/src/tui/contexts/index.ts +9 -5
  123. package/src/tui/createApp.tsx +8 -12
  124. package/src/tui/debug/captureFrame.ts +25 -28
  125. package/src/tui/debug/snapshot.tsx +179 -162
  126. package/src/tui/driver/assertions.ts +30 -22
  127. package/src/tui/driver/cli.ts +231 -221
  128. package/src/tui/driver/coverage.ts +27 -44
  129. package/src/tui/driver/demo-snapshot.ts +33 -33
  130. package/src/tui/driver/diff.ts +26 -32
  131. package/src/tui/driver/driver.ts +43 -48
  132. package/src/tui/driver/index.ts +36 -40
  133. package/src/tui/driver/recorder.ts +37 -48
  134. package/src/tui/driver/test-workflow.ts +51 -51
  135. package/src/tui/driver/test.ts +25 -25
  136. package/src/tui/hooks/index.ts +24 -20
  137. package/src/tui/hooks/useAnimatedValue.ts +13 -18
  138. package/src/tui/hooks/useDashboardKeyboard.ts +118 -98
  139. package/src/tui/hooks/useDashboardState.ts +26 -24
  140. package/src/tui/hooks/useEmaActivity.ts +82 -74
  141. package/src/tui/hooks/useEntranceAnimation.ts +11 -11
  142. package/src/tui/hooks/useExitAnimation.ts +7 -3
  143. package/src/tui/hooks/usePulse.ts +4 -4
  144. package/src/tui/hooks/useSafeRenderer.ts +5 -5
  145. package/src/tui/hooks/useSessionEnrichedProviders.ts +150 -0
  146. package/src/tui/hooks/useValueFlash.ts +10 -14
  147. package/src/tui/index.tsx +29 -6
  148. package/src/tui/shutdown.ts +19 -0
  149. package/src/tui/utils/providerColor.ts +1 -1
  150. package/src/tui/views/Dashboard.tsx +164 -102
  151. package/src/tui/views/HistoricalTrendsView.tsx +579 -250
  152. package/src/tui/views/ProjectsView.tsx +495 -243
  153. package/src/tui/views/RealTimeDashboard.tsx +176 -116
  154. package/src/tui/views/SettingsView.tsx +225 -189
  155. package/src/tui/views/index.ts +2 -2
  156. package/src/types/opentui-test-utils.d.ts +7 -7
  157. package/src/utils/clipboard.ts +29 -29
  158. package/src/utils/currency.test.ts +53 -53
  159. package/src/utils/currency.ts +35 -32
  160. package/src/version.ts +1 -1
@@ -1,2 +1,2 @@
1
- export { Dashboard } from './Dashboard.tsx';
2
- export { ProjectsView } from './ProjectsView.tsx';
1
+ export { Dashboard } from "./Dashboard.tsx";
2
+ export { ProjectsView } from "./ProjectsView.tsx";
@@ -1,14 +1,14 @@
1
- declare module '@opentui/react/test-utils' {
2
- import type { TestRendererOptions } from '@opentui/core/testing';
3
- import type { ReactNode } from 'react';
1
+ declare module "@opentui/react/test-utils" {
2
+ import type { TestRendererOptions } from "@opentui/core/testing";
3
+ import type { ReactNode } from "react";
4
4
 
5
5
  export function testRender(
6
6
  node: ReactNode,
7
- testRendererOptions: TestRendererOptions
7
+ testRendererOptions: TestRendererOptions,
8
8
  ): Promise<{
9
- renderer: import('@opentui/core/testing').TestRenderer;
10
- mockInput: import('@opentui/core/testing').MockInput;
11
- mockMouse: import('@opentui/core/testing').MockMouse;
9
+ renderer: import("@opentui/core/testing").TestRenderer;
10
+ mockInput: import("@opentui/core/testing").MockInput;
11
+ mockMouse: import("@opentui/core/testing").MockMouse;
12
12
  renderOnce: () => Promise<void>;
13
13
  captureCharFrame: () => string;
14
14
  resize: (width: number, height: number) => void;
@@ -1,5 +1,5 @@
1
- import { $ } from 'bun';
2
- import { platform } from 'os';
1
+ import { $ } from "bun";
2
+ import { platform } from "os";
3
3
 
4
4
  /**
5
5
  * Custom clipboard implementation with platform-specific fallbacks.
@@ -23,10 +23,10 @@ import { platform } from 'os';
23
23
  */
24
24
  function writeOsc52(text: string): void {
25
25
  if (!process.stdout.isTTY) return;
26
- const base64 = Buffer.from(text).toString('base64');
26
+ const base64 = Buffer.from(text).toString("base64");
27
27
  const osc52 = `\x1b]52;c;${base64}\x07`;
28
28
  // tmux and screen require DCS passthrough wrapping
29
- const passthrough = process.env['TMUX'] || process.env['STY'];
29
+ const passthrough = process.env["TMUX"] || process.env["STY"];
30
30
  const sequence = passthrough ? `\x1bPtmux;\x1b${osc52}\x1b\\` : osc52;
31
31
  process.stdout.write(sequence);
32
32
  }
@@ -38,30 +38,30 @@ function getCopyMethod(): (text: string) => Promise<void> {
38
38
 
39
39
  const os = platform();
40
40
 
41
- if (os === 'darwin' && Bun.which('osascript')) {
41
+ if (os === "darwin" && Bun.which("osascript")) {
42
42
  copyMethod = async (text: string) => {
43
- const escaped = text.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
43
+ const escaped = text.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
44
44
  await $`osascript -e 'set the clipboard to "${escaped}"'`.nothrow().quiet();
45
45
  };
46
46
  return copyMethod;
47
47
  }
48
48
 
49
- if (os === 'linux') {
50
- if (process.env['WAYLAND_DISPLAY'] && Bun.which('wl-copy')) {
49
+ if (os === "linux") {
50
+ if (process.env["WAYLAND_DISPLAY"] && Bun.which("wl-copy")) {
51
51
  copyMethod = async (text: string) => {
52
- const proc = Bun.spawn(['wl-copy'], { stdin: 'pipe', stdout: 'ignore', stderr: 'ignore' });
52
+ const proc = Bun.spawn(["wl-copy"], { stdin: "pipe", stdout: "ignore", stderr: "ignore" });
53
53
  proc.stdin.write(text);
54
54
  proc.stdin.end();
55
55
  await proc.exited.catch(() => {});
56
56
  };
57
57
  return copyMethod;
58
58
  }
59
- if (Bun.which('xclip')) {
59
+ if (Bun.which("xclip")) {
60
60
  copyMethod = async (text: string) => {
61
- const proc = Bun.spawn(['xclip', '-selection', 'clipboard'], {
62
- stdin: 'pipe',
63
- stdout: 'ignore',
64
- stderr: 'ignore',
61
+ const proc = Bun.spawn(["xclip", "-selection", "clipboard"], {
62
+ stdin: "pipe",
63
+ stdout: "ignore",
64
+ stderr: "ignore",
65
65
  });
66
66
  proc.stdin.write(text);
67
67
  proc.stdin.end();
@@ -69,12 +69,12 @@ function getCopyMethod(): (text: string) => Promise<void> {
69
69
  };
70
70
  return copyMethod;
71
71
  }
72
- if (Bun.which('xsel')) {
72
+ if (Bun.which("xsel")) {
73
73
  copyMethod = async (text: string) => {
74
- const proc = Bun.spawn(['xsel', '--clipboard', '--input'], {
75
- stdin: 'pipe',
76
- stdout: 'ignore',
77
- stderr: 'ignore',
74
+ const proc = Bun.spawn(["xsel", "--clipboard", "--input"], {
75
+ stdin: "pipe",
76
+ stdout: "ignore",
77
+ stderr: "ignore",
78
78
  });
79
79
  proc.stdin.write(text);
80
80
  proc.stdin.end();
@@ -84,21 +84,21 @@ function getCopyMethod(): (text: string) => Promise<void> {
84
84
  }
85
85
  }
86
86
 
87
- if (os === 'win32') {
87
+ if (os === "win32") {
88
88
  copyMethod = async (text: string) => {
89
89
  const proc = Bun.spawn(
90
90
  [
91
- 'powershell.exe',
92
- '-NonInteractive',
93
- '-NoProfile',
94
- '-Command',
95
- '[Console]::InputEncoding = [System.Text.Encoding]::UTF8; Set-Clipboard -Value ([Console]::In.ReadToEnd())',
91
+ "powershell.exe",
92
+ "-NonInteractive",
93
+ "-NoProfile",
94
+ "-Command",
95
+ "[Console]::InputEncoding = [System.Text.Encoding]::UTF8; Set-Clipboard -Value ([Console]::In.ReadToEnd())",
96
96
  ],
97
97
  {
98
- stdin: 'pipe',
99
- stdout: 'ignore',
100
- stderr: 'ignore',
101
- }
98
+ stdin: "pipe",
99
+ stdout: "ignore",
100
+ stderr: "ignore",
101
+ },
102
102
  );
103
103
  proc.stdin.write(text);
104
104
  proc.stdin.end();
@@ -1,106 +1,106 @@
1
- import { describe, expect, test } from 'bun:test';
1
+ import { describe, expect, test } from "bun:test";
2
2
  import {
3
3
  formatBudgetDisplay,
4
4
  formatCurrency,
5
5
  isValidCurrencyInput,
6
6
  parseCurrencyInput,
7
- } from './currency.ts';
7
+ } from "./currency.ts";
8
8
 
9
- describe('parseCurrencyInput', () => {
10
- test('returns null for empty or whitespace input', () => {
11
- expect(parseCurrencyInput('')).toBeNull();
12
- expect(parseCurrencyInput(' ')).toBeNull();
9
+ describe("parseCurrencyInput", () => {
10
+ test("returns null for empty or whitespace input", () => {
11
+ expect(parseCurrencyInput("")).toBeNull();
12
+ expect(parseCurrencyInput(" ")).toBeNull();
13
13
  });
14
14
 
15
- test('parses US format with thousands and decimal separators', () => {
16
- expect(parseCurrencyInput('1,234.56')).toBe(1234.56);
15
+ test("parses US format with thousands and decimal separators", () => {
16
+ expect(parseCurrencyInput("1,234.56")).toBe(1234.56);
17
17
  });
18
18
 
19
- test('parses European format with period thousands and comma decimal', () => {
20
- expect(parseCurrencyInput('1.234,56')).toBe(1234.56);
19
+ test("parses European format with period thousands and comma decimal", () => {
20
+ expect(parseCurrencyInput("1.234,56")).toBe(1234.56);
21
21
  });
22
22
 
23
- test('parses plain numbers', () => {
24
- expect(parseCurrencyInput('42.50')).toBe(42.5);
23
+ test("parses plain numbers", () => {
24
+ expect(parseCurrencyInput("42.50")).toBe(42.5);
25
25
  });
26
26
 
27
- test('strips supported currency symbols before parsing', () => {
28
- expect(parseCurrencyInput('$100')).toBe(100);
29
- expect(parseCurrencyInput('€100')).toBe(100);
30
- expect(parseCurrencyInput('£100')).toBe(100);
27
+ test("strips supported currency symbols before parsing", () => {
28
+ expect(parseCurrencyInput("$100")).toBe(100);
29
+ expect(parseCurrencyInput("€100")).toBe(100);
30
+ expect(parseCurrencyInput("£100")).toBe(100);
31
31
  });
32
32
 
33
- test('treats comma as decimal separator when not thousands grouping', () => {
34
- expect(parseCurrencyInput('10,50')).toBe(10.5);
33
+ test("treats comma as decimal separator when not thousands grouping", () => {
34
+ expect(parseCurrencyInput("10,50")).toBe(10.5);
35
35
  });
36
36
 
37
- test('treats comma as thousands separator with 3 digits after comma', () => {
38
- expect(parseCurrencyInput('1,234')).toBe(1234);
37
+ test("treats comma as thousands separator with 3 digits after comma", () => {
38
+ expect(parseCurrencyInput("1,234")).toBe(1234);
39
39
  });
40
40
 
41
- test('returns null for invalid input', () => {
42
- expect(parseCurrencyInput('abc')).toBeNull();
41
+ test("returns null for invalid input", () => {
42
+ expect(parseCurrencyInput("abc")).toBeNull();
43
43
  });
44
44
 
45
- test('rounds parsed values to 2 decimal places', () => {
46
- const parsed = parseCurrencyInput('10.129');
45
+ test("rounds parsed values to 2 decimal places", () => {
46
+ const parsed = parseCurrencyInput("10.129");
47
47
  expect(parsed).toBe(Math.round(10.129 * 100) / 100);
48
48
  });
49
49
  });
50
50
 
51
- describe('formatCurrency', () => {
52
- test('returns None for null input', () => {
53
- expect(formatCurrency(null)).toBe('None');
51
+ describe("formatCurrency", () => {
52
+ test("returns None for null input", () => {
53
+ expect(formatCurrency(null)).toBe("None");
54
54
  });
55
55
 
56
- test('formats compact values >= 1000 as k notation', () => {
57
- expect(formatCurrency(1234, { compact: true })).toBe('$1.2k');
56
+ test("formats compact values >= 1000 as k notation", () => {
57
+ expect(formatCurrency(1234, { compact: true })).toBe("$1.2k");
58
58
  });
59
59
 
60
- test('formats compact values >= 100 as rounded integer', () => {
61
- expect(formatCurrency(234.4, { compact: true })).toBe('$234');
60
+ test("formats compact values >= 100 as rounded integer", () => {
61
+ expect(formatCurrency(234.4, { compact: true })).toBe("$234");
62
62
  });
63
63
 
64
- test('formats compact values >= 10 with one decimal place', () => {
65
- expect(formatCurrency(42.34, { compact: true })).toBe('$42.3');
64
+ test("formats compact values >= 10 with one decimal place", () => {
65
+ expect(formatCurrency(42.34, { compact: true })).toBe("$42.3");
66
66
  });
67
67
 
68
- test('formats compact values < 10 with two decimal places', () => {
69
- expect(formatCurrency(9.5, { compact: true })).toBe('$9.50');
68
+ test("formats compact values < 10 with two decimal places", () => {
69
+ expect(formatCurrency(9.5, { compact: true })).toBe("$9.50");
70
70
  });
71
71
 
72
- test('formats non-compact values with locale separators and two decimals', () => {
73
- expect(formatCurrency(1234.5)).toBe('$1,234.50');
72
+ test("formats non-compact values with locale separators and two decimals", () => {
73
+ expect(formatCurrency(1234.5)).toBe("$1,234.50");
74
74
  });
75
75
 
76
- test('omits currency symbol when showSymbol is false', () => {
77
- expect(formatCurrency(1234.5, { showSymbol: false })).toBe('1,234.50');
78
- expect(formatCurrency(1234, { compact: true, showSymbol: false })).toBe('1.2k');
76
+ test("omits currency symbol when showSymbol is false", () => {
77
+ expect(formatCurrency(1234.5, { showSymbol: false })).toBe("1,234.50");
78
+ expect(formatCurrency(1234, { compact: true, showSymbol: false })).toBe("1.2k");
79
79
  });
80
80
  });
81
81
 
82
- describe('formatBudgetDisplay', () => {
83
- test('returns None for null input', () => {
84
- expect(formatBudgetDisplay(null)).toBe('None');
82
+ describe("formatBudgetDisplay", () => {
83
+ test("returns None for null input", () => {
84
+ expect(formatBudgetDisplay(null)).toBe("None");
85
85
  });
86
86
 
87
- test('delegates formatting to formatCurrency for numeric values', () => {
87
+ test("delegates formatting to formatCurrency for numeric values", () => {
88
88
  expect(formatBudgetDisplay(1234.5)).toBe(formatCurrency(1234.5));
89
89
  });
90
90
  });
91
91
 
92
- describe('isValidCurrencyInput', () => {
93
- test('returns true for empty input (represents no value)', () => {
94
- expect(isValidCurrencyInput('')).toBe(true);
95
- expect(isValidCurrencyInput(' ')).toBe(true);
92
+ describe("isValidCurrencyInput", () => {
93
+ test("returns true for empty input (represents no value)", () => {
94
+ expect(isValidCurrencyInput("")).toBe(true);
95
+ expect(isValidCurrencyInput(" ")).toBe(true);
96
96
  });
97
97
 
98
- test('returns true for valid numeric input', () => {
99
- expect(isValidCurrencyInput('42.50')).toBe(true);
100
- expect(isValidCurrencyInput('1.234,56')).toBe(true);
98
+ test("returns true for valid numeric input", () => {
99
+ expect(isValidCurrencyInput("42.50")).toBe(true);
100
+ expect(isValidCurrencyInput("1.234,56")).toBe(true);
101
101
  });
102
102
 
103
- test('returns false for invalid input', () => {
104
- expect(isValidCurrencyInput('abc')).toBe(false);
103
+ test("returns false for invalid input", () => {
104
+ expect(isValidCurrencyInput("abc")).toBe(false);
105
105
  });
106
106
  });
@@ -3,77 +3,80 @@
3
3
  * Handles US format (1,234.56) and European format (1.234,56).
4
4
  */
5
5
  export function parseCurrencyInput(input: string): number | null {
6
- if (!input || input.trim() === '') {
6
+ if (!input || input.trim() === "") {
7
7
  return null;
8
8
  }
9
-
10
- let normalized = input.trim().replace(/[$€£]/g, '').replace(/\s/g, '');
11
-
12
- const hasComma = normalized.includes(',');
13
- const hasPeriod = normalized.includes('.');
14
-
9
+
10
+ let normalized = input.trim().replace(/[$€£]/g, "").replace(/\s/g, "");
11
+
12
+ const hasComma = normalized.includes(",");
13
+ const hasPeriod = normalized.includes(".");
14
+
15
15
  if (hasComma && hasPeriod) {
16
- const lastComma = normalized.lastIndexOf(',');
17
- const lastPeriod = normalized.lastIndexOf('.');
18
-
16
+ const lastComma = normalized.lastIndexOf(",");
17
+ const lastPeriod = normalized.lastIndexOf(".");
18
+
19
19
  if (lastComma > lastPeriod) {
20
20
  // European: 1.234,56 → remove periods, comma becomes decimal
21
- normalized = normalized.replace(/\./g, '').replace(',', '.');
21
+ normalized = normalized.replace(/\./g, "").replace(",", ".");
22
22
  } else {
23
23
  // US: 1,234.56 → remove commas
24
- normalized = normalized.replace(/,/g, '');
24
+ normalized = normalized.replace(/,/g, "");
25
25
  }
26
26
  } else if (hasComma) {
27
- const parts = normalized.split(',');
27
+ const parts = normalized.split(",");
28
28
  // 3 digits after comma = thousands separator (1,234), otherwise decimal (10,50)
29
29
  if (parts.length === 2 && parts[1]!.length === 3 && parts[0]!.length > 0) {
30
- normalized = normalized.replace(',', '');
30
+ normalized = normalized.replace(",", "");
31
31
  } else {
32
- normalized = normalized.replace(',', '.');
32
+ normalized = normalized.replace(",", ".");
33
33
  }
34
34
  }
35
-
35
+
36
36
  const parsed = parseFloat(normalized);
37
-
37
+
38
38
  if (isNaN(parsed) || !isFinite(parsed)) {
39
39
  return null;
40
40
  }
41
-
41
+
42
42
  return Math.round(parsed * 100) / 100;
43
43
  }
44
44
 
45
- export function formatCurrency(value: number | null, options?: {
46
- compact?: boolean;
47
- showSymbol?: boolean;
48
- }): string {
45
+ export function formatCurrency(
46
+ value: number | null,
47
+ options?: {
48
+ compact?: boolean;
49
+ showSymbol?: boolean;
50
+ },
51
+ ): string {
49
52
  if (value === null) {
50
- return 'None';
53
+ return "None";
51
54
  }
52
-
55
+
53
56
  const { compact = false, showSymbol = true } = options ?? {};
54
- const symbol = showSymbol ? '$' : '';
55
-
57
+ const symbol = showSymbol ? "$" : "";
58
+
56
59
  if (compact) {
57
60
  if (value >= 1000) return `${symbol}${(value / 1000).toFixed(1)}k`;
58
61
  if (value >= 100) return `${symbol}${Math.round(value)}`;
59
62
  if (value >= 10) return `${symbol}${value.toFixed(1)}`;
60
63
  return `${symbol}${value.toFixed(2)}`;
61
64
  }
62
-
63
- return `${symbol}${value.toLocaleString('en-US', {
64
- minimumFractionDigits: 2,
65
- maximumFractionDigits: 2
65
+
66
+ return `${symbol}${value.toLocaleString("en-US", {
67
+ minimumFractionDigits: 2,
68
+ maximumFractionDigits: 2,
66
69
  })}`;
67
70
  }
68
71
 
69
72
  export function formatBudgetDisplay(value: number | null): string {
70
73
  if (value === null) {
71
- return 'None';
74
+ return "None";
72
75
  }
73
76
  return formatCurrency(value);
74
77
  }
75
78
 
76
79
  export function isValidCurrencyInput(input: string): boolean {
77
- if (input.trim() === '') return true;
80
+ if (input.trim() === "") return true;
78
81
  return parseCurrencyInput(input) !== null;
79
82
  }
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const VERSION = '0.2.2'; // x-release-please-version
1
+ export const VERSION = "0.4.0"; // x-release-please-version