@tokentop/ttop 0.2.2 → 0.3.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 (158) 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 +32 -32
  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 +29 -23
  19. package/src/plugins/notifications/visual-flash.ts +21 -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 +66 -57
  34. package/src/plugins/registry.ts +47 -43
  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/repos/agentSessions.ts +33 -23
  73. package/src/storage/repos/providerSnapshots.ts +5 -5
  74. package/src/storage/repos/usageEvents.ts +40 -28
  75. package/src/storage/types.ts +5 -5
  76. package/src/tui/App.tsx +180 -137
  77. package/src/tui/components/CommandPalette.tsx +46 -34
  78. package/src/tui/components/DebugConsole.tsx +39 -33
  79. package/src/tui/components/DebugPanel.tsx +295 -177
  80. package/src/tui/components/FooterHints.tsx +2 -2
  81. package/src/tui/components/GhostProviderCard.tsx +37 -35
  82. package/src/tui/components/Header.tsx +68 -35
  83. package/src/tui/components/HelpOverlay.tsx +99 -29
  84. package/src/tui/components/InlineGauge.tsx +10 -7
  85. package/src/tui/components/InlineSparkline.tsx +17 -11
  86. package/src/tui/components/KpiStrip.tsx +128 -98
  87. package/src/tui/components/LimitGauge.tsx +96 -59
  88. package/src/tui/components/ModalBackdrop.tsx +4 -4
  89. package/src/tui/components/ProviderAggregateStrip.tsx +69 -38
  90. package/src/tui/components/ProviderCard.tsx +232 -226
  91. package/src/tui/components/ProviderDetailPanel.tsx +133 -58
  92. package/src/tui/components/ProviderLimitsPanel.tsx +126 -89
  93. package/src/tui/components/ProvidersList.tsx +215 -126
  94. package/src/tui/components/SessionDetailsDrawer.tsx +191 -94
  95. package/src/tui/components/SessionsTable.tsx +313 -166
  96. package/src/tui/components/SettingsModal.tsx +231 -214
  97. package/src/tui/components/SidebarBreakdown.tsx +30 -16
  98. package/src/tui/components/Skeleton.tsx +24 -17
  99. package/src/tui/components/SmartSidebar.tsx +360 -258
  100. package/src/tui/components/Sparkline.tsx +64 -52
  101. package/src/tui/components/Spinner.tsx +3 -3
  102. package/src/tui/components/StatusBar.tsx +27 -27
  103. package/src/tui/components/ThemePicker.tsx +67 -66
  104. package/src/tui/components/Toast.tsx +9 -6
  105. package/src/tui/components/UsageGauge.tsx +10 -13
  106. package/src/tui/components/index.ts +5 -5
  107. package/src/tui/contexts/AgentSessionContext.tsx +193 -144
  108. package/src/tui/contexts/ConfigContext.tsx +15 -13
  109. package/src/tui/contexts/DashboardRuntimeContext.tsx +26 -26
  110. package/src/tui/contexts/DemoModeContext.tsx +22 -15
  111. package/src/tui/contexts/DrawerContext.tsx +11 -9
  112. package/src/tui/contexts/InputContext.tsx +2 -2
  113. package/src/tui/contexts/LogContext.tsx +62 -47
  114. package/src/tui/contexts/PluginContext.tsx +232 -183
  115. package/src/tui/contexts/RealTimeActivityContext.tsx +70 -36
  116. package/src/tui/contexts/StorageContext.tsx +151 -117
  117. package/src/tui/contexts/ThemeContext.tsx +21 -16
  118. package/src/tui/contexts/TimeWindowContext.tsx +72 -61
  119. package/src/tui/contexts/ToastContext.tsx +10 -7
  120. package/src/tui/contexts/index.ts +9 -5
  121. package/src/tui/createApp.tsx +8 -12
  122. package/src/tui/debug/captureFrame.ts +25 -28
  123. package/src/tui/debug/snapshot.tsx +179 -162
  124. package/src/tui/driver/assertions.ts +30 -22
  125. package/src/tui/driver/cli.ts +231 -221
  126. package/src/tui/driver/coverage.ts +27 -44
  127. package/src/tui/driver/demo-snapshot.ts +33 -33
  128. package/src/tui/driver/diff.ts +26 -32
  129. package/src/tui/driver/driver.ts +43 -48
  130. package/src/tui/driver/index.ts +36 -40
  131. package/src/tui/driver/recorder.ts +37 -48
  132. package/src/tui/driver/test-workflow.ts +51 -51
  133. package/src/tui/driver/test.ts +25 -25
  134. package/src/tui/hooks/index.ts +24 -20
  135. package/src/tui/hooks/useAnimatedValue.ts +13 -18
  136. package/src/tui/hooks/useDashboardKeyboard.ts +118 -98
  137. package/src/tui/hooks/useDashboardState.ts +26 -24
  138. package/src/tui/hooks/useEmaActivity.ts +82 -74
  139. package/src/tui/hooks/useEntranceAnimation.ts +11 -11
  140. package/src/tui/hooks/useExitAnimation.ts +3 -3
  141. package/src/tui/hooks/usePulse.ts +4 -4
  142. package/src/tui/hooks/useSafeRenderer.ts +5 -5
  143. package/src/tui/hooks/useSessionEnrichedProviders.ts +150 -0
  144. package/src/tui/hooks/useValueFlash.ts +10 -14
  145. package/src/tui/index.tsx +29 -6
  146. package/src/tui/shutdown.ts +19 -0
  147. package/src/tui/utils/providerColor.ts +1 -1
  148. package/src/tui/views/Dashboard.tsx +127 -102
  149. package/src/tui/views/HistoricalTrendsView.tsx +579 -250
  150. package/src/tui/views/ProjectsView.tsx +466 -237
  151. package/src/tui/views/RealTimeDashboard.tsx +150 -111
  152. package/src/tui/views/SettingsView.tsx +222 -191
  153. package/src/tui/views/index.ts +2 -2
  154. package/src/types/opentui-test-utils.d.ts +7 -7
  155. package/src/utils/clipboard.ts +29 -29
  156. package/src/utils/currency.test.ts +53 -53
  157. package/src/utils/currency.ts +35 -32
  158. package/src/version.ts +1 -1
@@ -1,2 +1,2 @@
1
- export { default as opencodeAgentPlugin } from '@tokentop/agent-opencode';
2
- export { default as claudeCodeAgentPlugin } from '@tokentop/agent-claude-code';
1
+ export { default as claudeCodeAgentPlugin } from "@tokentop/agent-claude-code";
2
+ export { default as opencodeAgentPlugin } from "@tokentop/agent-opencode";
@@ -1,15 +1,15 @@
1
- import * as fs from 'fs/promises';
2
- import * as path from 'path';
3
- import * as os from 'os';
4
- import type { PluginPermissions } from './types/base.ts';
5
- import type { AuthSources, OpenCodeAuthEntry } from './types/provider.ts';
6
- import { PluginPermissionError } from './types/base.ts';
1
+ import * as fs from "fs/promises";
2
+ import * as os from "os";
3
+ import * as path from "path";
4
+ import type { PluginPermissions } from "./types/base.ts";
5
+ import { PluginPermissionError } from "./types/base.ts";
6
+ import type { AuthSources, OpenCodeAuthEntry } from "./types/provider.ts";
7
7
 
8
- const OPENCODE_AUTH_PATH = path.join(os.homedir(), '.local/share/opencode/auth.json');
9
- const OPENCODE_CONFIG_PATH = path.join(os.homedir(), '.config/opencode/opencode.json');
8
+ const OPENCODE_AUTH_PATH = path.join(os.homedir(), ".local/share/opencode/auth.json");
9
+ const OPENCODE_CONFIG_PATH = path.join(os.homedir(), ".config/opencode/opencode.json");
10
10
 
11
11
  function expandHome(filePath: string): string {
12
- if (filePath.startsWith('~')) {
12
+ if (filePath.startsWith("~")) {
13
13
  return path.join(os.homedir(), filePath.slice(1));
14
14
  }
15
15
  return filePath;
@@ -35,7 +35,7 @@ function isEnvVarAllowed(name: string, permissions: PluginPermissions): boolean
35
35
 
36
36
  async function readJsonFile<T>(filePath: string): Promise<T | null> {
37
37
  try {
38
- const content = await fs.readFile(filePath, 'utf-8');
38
+ const content = await fs.readFile(filePath, "utf-8");
39
39
  return JSON.parse(content) as T;
40
40
  } catch {
41
41
  return null;
@@ -43,14 +43,17 @@ async function readJsonFile<T>(filePath: string): Promise<T | null> {
43
43
  }
44
44
 
45
45
  interface OpenCodeConfig {
46
- provider?: Record<string, {
47
- name?: string;
48
- key?: string;
49
- options?: {
50
- apiKey?: string;
51
- [key: string]: unknown;
52
- };
53
- }>;
46
+ provider?: Record<
47
+ string,
48
+ {
49
+ name?: string;
50
+ key?: string;
51
+ options?: {
52
+ apiKey?: string;
53
+ [key: string]: unknown;
54
+ };
55
+ }
56
+ >;
54
57
  }
55
58
 
56
59
  function resolveEnvValue(value: string | undefined): string | undefined {
@@ -69,8 +72,8 @@ export function createAuthSources(pluginId: string, permissions: PluginPermissio
69
72
  if (!isEnvVarAllowed(name, permissions)) {
70
73
  throw new PluginPermissionError(
71
74
  pluginId,
72
- 'env',
73
- `Environment variable "${name}" not in allowlist`
75
+ "env",
76
+ `Environment variable "${name}" not in allowlist`,
74
77
  );
75
78
  }
76
79
  return process.env[name];
@@ -83,19 +86,19 @@ export function createAuthSources(pluginId: string, permissions: PluginPermissio
83
86
  if (!permissions.filesystem?.read) {
84
87
  throw new PluginPermissionError(
85
88
  pluginId,
86
- 'filesystem',
87
- 'Filesystem read access not permitted'
89
+ "filesystem",
90
+ "Filesystem read access not permitted",
88
91
  );
89
92
  }
90
93
  if (!isPathAllowed(filePath, permissions)) {
91
94
  throw new PluginPermissionError(
92
95
  pluginId,
93
- 'filesystem',
94
- `Path "${filePath}" not in allowlist`
96
+ "filesystem",
97
+ `Path "${filePath}" not in allowlist`,
95
98
  );
96
99
  }
97
100
  try {
98
- return await fs.readFile(resolved, 'utf-8');
101
+ return await fs.readFile(resolved, "utf-8");
99
102
  } catch {
100
103
  return null;
101
104
  }
@@ -106,19 +109,19 @@ export function createAuthSources(pluginId: string, permissions: PluginPermissio
106
109
  if (!permissions.filesystem?.read) {
107
110
  throw new PluginPermissionError(
108
111
  pluginId,
109
- 'filesystem',
110
- 'Filesystem read access not permitted'
112
+ "filesystem",
113
+ "Filesystem read access not permitted",
111
114
  );
112
115
  }
113
116
  if (!isPathAllowed(filePath, permissions)) {
114
117
  throw new PluginPermissionError(
115
118
  pluginId,
116
- 'filesystem',
117
- `Path "${filePath}" not in allowlist`
119
+ "filesystem",
120
+ `Path "${filePath}" not in allowlist`,
118
121
  );
119
122
  }
120
123
  try {
121
- const content = await fs.readFile(resolved, 'utf-8');
124
+ const content = await fs.readFile(resolved, "utf-8");
122
125
  return JSON.parse(content) as T;
123
126
  } catch {
124
127
  return null;
@@ -130,15 +133,15 @@ export function createAuthSources(pluginId: string, permissions: PluginPermissio
130
133
  if (!permissions.filesystem?.read) {
131
134
  throw new PluginPermissionError(
132
135
  pluginId,
133
- 'filesystem',
134
- 'Filesystem read access not permitted'
136
+ "filesystem",
137
+ "Filesystem read access not permitted",
135
138
  );
136
139
  }
137
140
  if (!isPathAllowed(filePath, permissions)) {
138
141
  throw new PluginPermissionError(
139
142
  pluginId,
140
- 'filesystem',
141
- `Path "${filePath}" not in allowlist`
143
+ "filesystem",
144
+ `Path "${filePath}" not in allowlist`,
142
145
  );
143
146
  }
144
147
  try {
@@ -162,11 +165,12 @@ export function createAuthSources(pluginId: string, permissions: PluginPermissio
162
165
  if (config?.provider) {
163
166
  const providerConfig = config.provider[key];
164
167
  if (providerConfig) {
165
- const apiKey = resolveEnvValue(providerConfig.key) ??
166
- resolveEnvValue(providerConfig.options?.apiKey as string);
168
+ const apiKey =
169
+ resolveEnvValue(providerConfig.key) ??
170
+ resolveEnvValue(providerConfig.options?.apiKey as string);
167
171
  if (apiKey) {
168
172
  return {
169
- type: 'api',
173
+ type: "api",
170
174
  key: apiKey,
171
175
  };
172
176
  }
@@ -178,7 +182,7 @@ export function createAuthSources(pluginId: string, permissions: PluginPermissio
178
182
  },
179
183
 
180
184
  platform: {
181
- os: process.platform as 'darwin' | 'linux' | 'win32',
185
+ os: process.platform as "darwin" | "linux" | "win32",
182
186
  homedir: os.homedir(),
183
187
  arch: process.arch,
184
188
  },
@@ -5,23 +5,23 @@
5
5
  * with safeInvoke for error isolation and circuit breaker protection.
6
6
  */
7
7
 
8
- import type { BasePlugin, PluginLifecycleContext, PluginLogger } from './types/base.ts';
9
- import { pluginRegistry } from './registry.ts';
10
- import { safeInvoke } from './plugin-host.ts';
11
- import { createPluginLogger } from './sandbox.ts';
12
- import { runInPluginGuard } from './sandbox-guard.ts';
8
+ import { safeInvoke } from "./plugin-host.ts";
9
+ import { pluginRegistry } from "./registry.ts";
10
+ import { createPluginLogger } from "./sandbox.ts";
11
+ import { runInPluginGuard } from "./sandbox-guard.ts";
12
+ import type { BasePlugin, PluginLifecycleContext, PluginLogger } from "./types/base.ts";
13
13
 
14
14
  // ---------------------------------------------------------------------------
15
15
  // State machine
16
16
  // ---------------------------------------------------------------------------
17
17
 
18
18
  export type PluginLifecycleState =
19
- | 'loaded'
20
- | 'initialized'
21
- | 'started'
22
- | 'stopped'
23
- | 'destroyed'
24
- | 'failed';
19
+ | "loaded"
20
+ | "initialized"
21
+ | "started"
22
+ | "stopped"
23
+ | "destroyed"
24
+ | "failed";
25
25
 
26
26
  interface PluginLifecycleEntry {
27
27
  pluginId: string;
@@ -44,7 +44,7 @@ class PluginLifecycleManager {
44
44
  if (!entry) {
45
45
  entry = {
46
46
  pluginId: plugin.id,
47
- state: 'loaded',
47
+ state: "loaded",
48
48
  config: { ...(plugin.defaultConfig ?? {}) },
49
49
  logger: createPluginLogger(plugin.id),
50
50
  };
@@ -59,7 +59,7 @@ class PluginLifecycleManager {
59
59
 
60
60
  private async invokeHook(
61
61
  plugin: BasePlugin,
62
- hook: 'initialize' | 'start' | 'stop' | 'destroy',
62
+ hook: "initialize" | "start" | "stop" | "destroy",
63
63
  ): Promise<boolean> {
64
64
  const fn = plugin[hook];
65
65
  if (!fn) return true;
@@ -72,7 +72,10 @@ class PluginLifecycleManager {
72
72
  Promise.race([
73
73
  fn.call(plugin, ctx),
74
74
  new Promise<never>((_, reject) =>
75
- setTimeout(() => reject(new Error(`${hook} timed out after ${HOOK_TIMEOUT_MS}ms`)), HOOK_TIMEOUT_MS),
75
+ setTimeout(
76
+ () => reject(new Error(`${hook} timed out after ${HOOK_TIMEOUT_MS}ms`)),
77
+ HOOK_TIMEOUT_MS,
78
+ ),
76
79
  ),
77
80
  ]),
78
81
  ),
@@ -90,10 +93,10 @@ class PluginLifecycleManager {
90
93
 
91
94
  for (const plugin of plugins) {
92
95
  const entry = this.getOrCreate(plugin);
93
- if (entry.state !== 'loaded') continue;
96
+ if (entry.state !== "loaded") continue;
94
97
 
95
- const ok = await this.invokeHook(plugin, 'initialize');
96
- entry.state = ok ? 'initialized' : 'failed';
98
+ const ok = await this.invokeHook(plugin, "initialize");
99
+ entry.state = ok ? "initialized" : "failed";
97
100
  }
98
101
  }
99
102
 
@@ -102,10 +105,10 @@ class PluginLifecycleManager {
102
105
 
103
106
  for (const plugin of plugins) {
104
107
  const entry = this.getOrCreate(plugin);
105
- if (entry.state !== 'initialized') continue;
108
+ if (entry.state !== "initialized") continue;
106
109
 
107
- const ok = await this.invokeHook(plugin, 'start');
108
- entry.state = ok ? 'started' : 'failed';
110
+ const ok = await this.invokeHook(plugin, "start");
111
+ entry.state = ok ? "started" : "failed";
109
112
  }
110
113
  }
111
114
 
@@ -114,10 +117,10 @@ class PluginLifecycleManager {
114
117
 
115
118
  for (const plugin of plugins) {
116
119
  const entry = this.entries.get(plugin.id);
117
- if (!entry || entry.state !== 'started') continue;
120
+ if (!entry || entry.state !== "started") continue;
118
121
 
119
- const ok = await this.invokeHook(plugin, 'stop');
120
- entry.state = ok ? 'stopped' : 'failed';
122
+ const ok = await this.invokeHook(plugin, "stop");
123
+ entry.state = ok ? "stopped" : "failed";
121
124
  }
122
125
  }
123
126
 
@@ -127,27 +130,24 @@ class PluginLifecycleManager {
127
130
  for (const plugin of plugins) {
128
131
  const entry = this.entries.get(plugin.id);
129
132
  if (!entry) continue;
130
- if (entry.state === 'destroyed') continue;
133
+ if (entry.state === "destroyed") continue;
131
134
 
132
- await this.invokeHook(plugin, 'destroy');
133
- entry.state = 'destroyed';
135
+ await this.invokeHook(plugin, "destroy");
136
+ entry.state = "destroyed";
134
137
  }
135
138
  }
136
139
 
137
- async notifyConfigChange(
138
- pluginId: string,
139
- newConfig: Record<string, unknown>,
140
- ): Promise<void> {
140
+ async notifyConfigChange(pluginId: string, newConfig: Record<string, unknown>): Promise<void> {
141
141
  const plugin = pluginRegistry.getAllPlugins().find((p) => p.id === pluginId);
142
142
  if (!plugin?.onConfigChange) return;
143
143
 
144
144
  const entry = this.entries.get(pluginId);
145
- if (!entry || entry.state === 'destroyed' || entry.state === 'failed') return;
145
+ if (!entry || entry.state === "destroyed" || entry.state === "failed") return;
146
146
 
147
147
  entry.config = { ...newConfig };
148
148
 
149
149
  const ctx = this.makeCtx(entry);
150
- await safeInvoke(pluginId, 'onConfigChange', () =>
150
+ await safeInvoke(pluginId, "onConfigChange", () =>
151
151
  runInPluginGuard(pluginId, plugin.permissions, () =>
152
152
  Promise.resolve(plugin.onConfigChange!(newConfig, ctx)),
153
153
  ),
@@ -1,12 +1,12 @@
1
- import { describe, expect, test } from 'bun:test';
2
- import { validatePlugin } from './loader.ts';
3
- import { CURRENT_API_VERSION } from './types/base.ts';
1
+ import { describe, expect, test } from "bun:test";
2
+ import { validatePlugin } from "./loader.ts";
3
+ import { CURRENT_API_VERSION } from "./types/base.ts";
4
4
 
5
5
  const validPlugin = {
6
- id: 'test-plugin',
7
- type: 'notification',
8
- name: 'Test Plugin',
9
- version: '1.0.0',
6
+ id: "test-plugin",
7
+ type: "notification",
8
+ name: "Test Plugin",
9
+ version: "1.0.0",
10
10
  apiVersion: CURRENT_API_VERSION,
11
11
  permissions: {},
12
12
  initialize: async () => {},
@@ -17,18 +17,18 @@ function hasError(errors: string[], fragment: string): boolean {
17
17
  return errors.some((error) => error.includes(fragment));
18
18
  }
19
19
 
20
- describe('validatePlugin', () => {
21
- test('accepts a valid minimal plugin object', async () => {
20
+ describe("validatePlugin", () => {
21
+ test("accepts a valid minimal plugin object", async () => {
22
22
  const result = await validatePlugin(validPlugin);
23
23
 
24
24
  expect(result.valid).toBe(true);
25
25
  expect(result.errors).toHaveLength(0);
26
26
  });
27
27
 
28
- test('rejects plugin missing id', async () => {
28
+ test("rejects plugin missing id", async () => {
29
29
  const plugin = {
30
30
  ...validPlugin,
31
- id: '',
31
+ id: "",
32
32
  };
33
33
 
34
34
  const result = await validatePlugin(plugin);
@@ -36,46 +36,46 @@ describe('validatePlugin', () => {
36
36
  expect(hasError(result.errors, 'non-empty string "id"')).toBe(true);
37
37
  });
38
38
 
39
- test('rejects plugin with invalid id format', async () => {
39
+ test("rejects plugin with invalid id format", async () => {
40
40
  const plugin = {
41
41
  ...validPlugin,
42
- id: 'Bad Plugin',
42
+ id: "Bad Plugin",
43
43
  };
44
44
 
45
45
  const result = await validatePlugin(plugin);
46
46
  expect(result.valid).toBe(false);
47
- expect(hasError(result.errors, 'kebab-case')).toBe(true);
47
+ expect(hasError(result.errors, "kebab-case")).toBe(true);
48
48
  });
49
49
 
50
- test('rejects plugin missing type', async () => {
50
+ test("rejects plugin missing type", async () => {
51
51
  const plugin = {
52
- id: 'test-plugin',
53
- name: 'Test Plugin',
54
- version: '1.0.0',
52
+ id: "test-plugin",
53
+ name: "Test Plugin",
54
+ version: "1.0.0",
55
55
  apiVersion: CURRENT_API_VERSION,
56
56
  permissions: {},
57
57
  };
58
58
 
59
59
  const result = await validatePlugin(plugin);
60
60
  expect(result.valid).toBe(false);
61
- expect(hasError(result.errors, 'must be one of')).toBe(true);
61
+ expect(hasError(result.errors, "must be one of")).toBe(true);
62
62
  });
63
63
 
64
- test('rejects plugin with invalid type', async () => {
64
+ test("rejects plugin with invalid type", async () => {
65
65
  const plugin = {
66
66
  ...validPlugin,
67
- type: 'invalid',
67
+ type: "invalid",
68
68
  };
69
69
 
70
70
  const result = await validatePlugin(plugin);
71
71
  expect(result.valid).toBe(false);
72
- expect(hasError(result.errors, 'provider, agent, theme, notification')).toBe(true);
72
+ expect(hasError(result.errors, "provider, agent, theme, notification")).toBe(true);
73
73
  });
74
74
 
75
- test('rejects plugin missing name', async () => {
75
+ test("rejects plugin missing name", async () => {
76
76
  const plugin = {
77
77
  ...validPlugin,
78
- name: '',
78
+ name: "",
79
79
  };
80
80
 
81
81
  const result = await validatePlugin(plugin);
@@ -83,10 +83,10 @@ describe('validatePlugin', () => {
83
83
  expect(hasError(result.errors, 'non-empty string "name"')).toBe(true);
84
84
  });
85
85
 
86
- test('rejects plugin missing version', async () => {
86
+ test("rejects plugin missing version", async () => {
87
87
  const plugin = {
88
88
  ...validPlugin,
89
- version: '',
89
+ version: "",
90
90
  };
91
91
 
92
92
  const result = await validatePlugin(plugin);
@@ -94,10 +94,10 @@ describe('validatePlugin', () => {
94
94
  expect(hasError(result.errors, 'valid semver "version"')).toBe(true);
95
95
  });
96
96
 
97
- test('rejects plugin with invalid version format', async () => {
97
+ test("rejects plugin with invalid version format", async () => {
98
98
  const plugin = {
99
99
  ...validPlugin,
100
- version: 'v1',
100
+ version: "v1",
101
101
  };
102
102
 
103
103
  const result = await validatePlugin(plugin);
@@ -105,12 +105,12 @@ describe('validatePlugin', () => {
105
105
  expect(hasError(result.errors, 'valid semver "version"')).toBe(true);
106
106
  });
107
107
 
108
- test('rejects plugin missing permissions', async () => {
108
+ test("rejects plugin missing permissions", async () => {
109
109
  const plugin = {
110
- id: 'test-plugin',
111
- type: 'notification',
112
- name: 'Test Plugin',
113
- version: '1.0.0',
110
+ id: "test-plugin",
111
+ type: "notification",
112
+ name: "Test Plugin",
113
+ version: "1.0.0",
114
114
  apiVersion: CURRENT_API_VERSION,
115
115
  };
116
116
 
@@ -119,7 +119,7 @@ describe('validatePlugin', () => {
119
119
  expect(hasError(result.errors, 'declare "permissions" object')).toBe(true);
120
120
  });
121
121
 
122
- test('rejects plugin with wrong apiVersion', async () => {
122
+ test("rejects plugin with wrong apiVersion", async () => {
123
123
  const plugin = {
124
124
  ...validPlugin,
125
125
  apiVersion: CURRENT_API_VERSION + 1,
@@ -130,10 +130,10 @@ describe('validatePlugin', () => {
130
130
  expect(hasError(result.errors, `expected ${CURRENT_API_VERSION}`)).toBe(true);
131
131
  });
132
132
 
133
- test('rejects provider plugin missing auth', async () => {
133
+ test("rejects provider plugin missing auth", async () => {
134
134
  const providerPlugin = {
135
135
  ...validPlugin,
136
- type: 'provider',
136
+ type: "provider",
137
137
  fetchUsage: async () => [],
138
138
  };
139
139
 
@@ -142,10 +142,10 @@ describe('validatePlugin', () => {
142
142
  expect(hasError(result.errors, 'declare "auth" object')).toBe(true);
143
143
  });
144
144
 
145
- test('accepts valid provider plugin with auth and fetchUsage', async () => {
145
+ test("accepts valid provider plugin with auth and fetchUsage", async () => {
146
146
  const providerPlugin = {
147
147
  ...validPlugin,
148
- type: 'provider',
148
+ type: "provider",
149
149
  auth: {
150
150
  discover: async () => undefined,
151
151
  isConfigured: () => true,
@@ -157,11 +157,11 @@ describe('validatePlugin', () => {
157
157
  expect(result.valid).toBe(true);
158
158
  });
159
159
 
160
- test('rejects theme plugin missing colors object', async () => {
160
+ test("rejects theme plugin missing colors object", async () => {
161
161
  const themePlugin = {
162
162
  ...validPlugin,
163
- type: 'theme',
164
- colorScheme: 'dark',
163
+ type: "theme",
164
+ colorScheme: "dark",
165
165
  };
166
166
 
167
167
  const result = await validatePlugin(themePlugin);
@@ -169,24 +169,24 @@ describe('validatePlugin', () => {
169
169
  expect(hasError(result.errors, '"colors" object')).toBe(true);
170
170
  });
171
171
 
172
- test('rejects theme plugin missing colorScheme', async () => {
172
+ test("rejects theme plugin missing colorScheme", async () => {
173
173
  const themePlugin = {
174
174
  ...validPlugin,
175
- type: 'theme',
176
- colors: { primary: '#fff' },
175
+ type: "theme",
176
+ colors: { primary: "#fff" },
177
177
  };
178
178
 
179
179
  const result = await validatePlugin(themePlugin);
180
180
  expect(result.valid).toBe(false);
181
- expect(hasError(result.errors, 'colorScheme')).toBe(true);
181
+ expect(hasError(result.errors, "colorScheme")).toBe(true);
182
182
  });
183
183
 
184
- test('accepts valid theme plugin', async () => {
184
+ test("accepts valid theme plugin", async () => {
185
185
  const themePlugin = {
186
186
  ...validPlugin,
187
- type: 'theme',
188
- colorScheme: 'dark',
189
- colors: { primary: '#fff' },
187
+ type: "theme",
188
+ colorScheme: "dark",
189
+ colors: { primary: "#fff" },
190
190
  };
191
191
 
192
192
  const result = await validatePlugin(themePlugin);