@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.
- package/README.md +2 -5
- package/package.json +8 -5
- package/src/agents/aggregator.ts +39 -19
- package/src/agents/costing.ts +11 -9
- package/src/agents/index.ts +7 -9
- package/src/agents/types.ts +2 -2
- package/src/cli.ts +28 -23
- package/src/config/schema.test.ts +42 -40
- package/src/config/schema.ts +26 -26
- package/src/demo/simulator.ts +161 -122
- package/src/plugins/agents/index.ts +2 -2
- package/src/plugins/auth-sources.ts +42 -38
- package/src/plugins/lifecycle.ts +32 -32
- package/src/plugins/loader.test.ts +49 -49
- package/src/plugins/loader.ts +67 -63
- package/src/plugins/notification-bus.ts +21 -16
- package/src/plugins/notifications/index.ts +2 -2
- package/src/plugins/notifications/terminal-bell.ts +29 -23
- package/src/plugins/notifications/visual-flash.ts +21 -19
- package/src/plugins/npm-installer.ts +23 -25
- package/src/plugins/plugin-context-factory.ts +10 -10
- package/src/plugins/plugin-host.ts +12 -5
- package/src/plugins/providers/anthropic.ts +64 -57
- package/src/plugins/providers/antigravity.ts +81 -69
- package/src/plugins/providers/codex.ts +42 -42
- package/src/plugins/providers/gemini.ts +106 -87
- package/src/plugins/providers/github-copilot.ts +49 -45
- package/src/plugins/providers/index.ts +10 -12
- package/src/plugins/providers/minimax.ts +50 -44
- package/src/plugins/providers/openai-api.ts +82 -62
- package/src/plugins/providers/opencode-zen.ts +46 -39
- package/src/plugins/providers/perplexity.ts +55 -46
- package/src/plugins/providers/zai.ts +66 -57
- package/src/plugins/registry.ts +47 -43
- package/src/plugins/sandbox-guard.test.ts +45 -48
- package/src/plugins/sandbox-guard.ts +20 -25
- package/src/plugins/sandbox.ts +26 -48
- package/src/plugins/themes/catppuccin-latte.ts +29 -29
- package/src/plugins/themes/catppuccin-mocha.ts +29 -29
- package/src/plugins/themes/claude-code.ts +29 -29
- package/src/plugins/themes/dracula.ts +29 -29
- package/src/plugins/themes/github-light.ts +29 -29
- package/src/plugins/themes/gruvbox-dark.ts +29 -29
- package/src/plugins/themes/gruvbox-light.ts +29 -29
- package/src/plugins/themes/index.ts +15 -15
- package/src/plugins/themes/kanagawa.ts +29 -29
- package/src/plugins/themes/nord.ts +29 -29
- package/src/plugins/themes/one-dark.ts +29 -29
- package/src/plugins/themes/opencode.ts +31 -31
- package/src/plugins/themes/rose-pine-dawn.ts +29 -29
- package/src/plugins/themes/rose-pine.ts +29 -29
- package/src/plugins/themes/solarized-light.ts +29 -29
- package/src/plugins/themes/tokyo-night.ts +29 -29
- package/src/plugins/types/agent.ts +5 -5
- package/src/plugins/types/base.ts +35 -24
- package/src/plugins/types/index.ts +9 -9
- package/src/plugins/types/notification.ts +12 -12
- package/src/plugins/types/provider.ts +12 -8
- package/src/plugins/types/theme.ts +31 -23
- package/src/plugins/update-checker.ts +6 -6
- package/src/pricing/estimator.test.ts +39 -44
- package/src/pricing/estimator.ts +7 -13
- package/src/pricing/fallback.test.ts +30 -34
- package/src/pricing/fallback.ts +54 -27
- package/src/pricing/index.ts +18 -12
- package/src/pricing/models-dev.ts +6 -6
- package/src/storage/database.ts +56 -46
- package/src/storage/db.ts +12 -12
- package/src/storage/index.ts +44 -45
- package/src/storage/migrations/index.ts +2 -2
- package/src/storage/paths.ts +13 -13
- package/src/storage/repos/agentSessions.ts +33 -23
- package/src/storage/repos/providerSnapshots.ts +5 -5
- package/src/storage/repos/usageEvents.ts +40 -28
- package/src/storage/types.ts +5 -5
- package/src/tui/App.tsx +180 -137
- package/src/tui/components/CommandPalette.tsx +46 -34
- package/src/tui/components/DebugConsole.tsx +39 -33
- package/src/tui/components/DebugPanel.tsx +295 -177
- package/src/tui/components/FooterHints.tsx +2 -2
- package/src/tui/components/GhostProviderCard.tsx +37 -35
- package/src/tui/components/Header.tsx +68 -35
- package/src/tui/components/HelpOverlay.tsx +99 -29
- package/src/tui/components/InlineGauge.tsx +10 -7
- package/src/tui/components/InlineSparkline.tsx +17 -11
- package/src/tui/components/KpiStrip.tsx +128 -98
- package/src/tui/components/LimitGauge.tsx +96 -59
- package/src/tui/components/ModalBackdrop.tsx +4 -4
- package/src/tui/components/ProviderAggregateStrip.tsx +69 -38
- package/src/tui/components/ProviderCard.tsx +232 -226
- package/src/tui/components/ProviderDetailPanel.tsx +133 -58
- package/src/tui/components/ProviderLimitsPanel.tsx +126 -89
- package/src/tui/components/ProvidersList.tsx +215 -126
- package/src/tui/components/SessionDetailsDrawer.tsx +191 -94
- package/src/tui/components/SessionsTable.tsx +313 -166
- package/src/tui/components/SettingsModal.tsx +231 -214
- package/src/tui/components/SidebarBreakdown.tsx +30 -16
- package/src/tui/components/Skeleton.tsx +24 -17
- package/src/tui/components/SmartSidebar.tsx +360 -258
- package/src/tui/components/Sparkline.tsx +64 -52
- package/src/tui/components/Spinner.tsx +3 -3
- package/src/tui/components/StatusBar.tsx +27 -27
- package/src/tui/components/ThemePicker.tsx +67 -66
- package/src/tui/components/Toast.tsx +9 -6
- package/src/tui/components/UsageGauge.tsx +10 -13
- package/src/tui/components/index.ts +5 -5
- package/src/tui/contexts/AgentSessionContext.tsx +193 -144
- package/src/tui/contexts/ConfigContext.tsx +15 -13
- package/src/tui/contexts/DashboardRuntimeContext.tsx +26 -26
- package/src/tui/contexts/DemoModeContext.tsx +22 -15
- package/src/tui/contexts/DrawerContext.tsx +11 -9
- package/src/tui/contexts/InputContext.tsx +2 -2
- package/src/tui/contexts/LogContext.tsx +62 -47
- package/src/tui/contexts/PluginContext.tsx +232 -183
- package/src/tui/contexts/RealTimeActivityContext.tsx +70 -36
- package/src/tui/contexts/StorageContext.tsx +151 -117
- package/src/tui/contexts/ThemeContext.tsx +21 -16
- package/src/tui/contexts/TimeWindowContext.tsx +72 -61
- package/src/tui/contexts/ToastContext.tsx +10 -7
- package/src/tui/contexts/index.ts +9 -5
- package/src/tui/createApp.tsx +8 -12
- package/src/tui/debug/captureFrame.ts +25 -28
- package/src/tui/debug/snapshot.tsx +179 -162
- package/src/tui/driver/assertions.ts +30 -22
- package/src/tui/driver/cli.ts +231 -221
- package/src/tui/driver/coverage.ts +27 -44
- package/src/tui/driver/demo-snapshot.ts +33 -33
- package/src/tui/driver/diff.ts +26 -32
- package/src/tui/driver/driver.ts +43 -48
- package/src/tui/driver/index.ts +36 -40
- package/src/tui/driver/recorder.ts +37 -48
- package/src/tui/driver/test-workflow.ts +51 -51
- package/src/tui/driver/test.ts +25 -25
- package/src/tui/hooks/index.ts +24 -20
- package/src/tui/hooks/useAnimatedValue.ts +13 -18
- package/src/tui/hooks/useDashboardKeyboard.ts +118 -98
- package/src/tui/hooks/useDashboardState.ts +26 -24
- package/src/tui/hooks/useEmaActivity.ts +82 -74
- package/src/tui/hooks/useEntranceAnimation.ts +11 -11
- package/src/tui/hooks/useExitAnimation.ts +3 -3
- package/src/tui/hooks/usePulse.ts +4 -4
- package/src/tui/hooks/useSafeRenderer.ts +5 -5
- package/src/tui/hooks/useSessionEnrichedProviders.ts +150 -0
- package/src/tui/hooks/useValueFlash.ts +10 -14
- package/src/tui/index.tsx +29 -6
- package/src/tui/shutdown.ts +19 -0
- package/src/tui/utils/providerColor.ts +1 -1
- package/src/tui/views/Dashboard.tsx +127 -102
- package/src/tui/views/HistoricalTrendsView.tsx +579 -250
- package/src/tui/views/ProjectsView.tsx +466 -237
- package/src/tui/views/RealTimeDashboard.tsx +150 -111
- package/src/tui/views/SettingsView.tsx +222 -191
- package/src/tui/views/index.ts +2 -2
- package/src/types/opentui-test-utils.d.ts +7 -7
- package/src/utils/clipboard.ts +29 -29
- package/src/utils/currency.test.ts +53 -53
- package/src/utils/currency.ts +35 -32
- package/src/version.ts +1 -1
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { default as
|
|
2
|
-
export { default as
|
|
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
|
|
2
|
-
import * as
|
|
3
|
-
import * as
|
|
4
|
-
import type { PluginPermissions } from
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
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(),
|
|
9
|
-
const OPENCODE_CONFIG_PATH = path.join(os.homedir(),
|
|
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,
|
|
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<
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
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
|
-
|
|
87
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
110
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
134
|
-
|
|
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
|
-
|
|
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 =
|
|
166
|
-
|
|
168
|
+
const apiKey =
|
|
169
|
+
resolveEnvValue(providerConfig.key) ??
|
|
170
|
+
resolveEnvValue(providerConfig.options?.apiKey as string);
|
|
167
171
|
if (apiKey) {
|
|
168
172
|
return {
|
|
169
|
-
type:
|
|
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
|
|
185
|
+
os: process.platform as "darwin" | "linux" | "win32",
|
|
182
186
|
homedir: os.homedir(),
|
|
183
187
|
arch: process.arch,
|
|
184
188
|
},
|
package/src/plugins/lifecycle.ts
CHANGED
|
@@ -5,23 +5,23 @@
|
|
|
5
5
|
* with safeInvoke for error isolation and circuit breaker protection.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import
|
|
9
|
-
import { pluginRegistry } from
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
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
|
-
|
|
|
20
|
-
|
|
|
21
|
-
|
|
|
22
|
-
|
|
|
23
|
-
|
|
|
24
|
-
|
|
|
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:
|
|
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:
|
|
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(
|
|
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 !==
|
|
96
|
+
if (entry.state !== "loaded") continue;
|
|
94
97
|
|
|
95
|
-
const ok = await this.invokeHook(plugin,
|
|
96
|
-
entry.state = ok ?
|
|
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 !==
|
|
108
|
+
if (entry.state !== "initialized") continue;
|
|
106
109
|
|
|
107
|
-
const ok = await this.invokeHook(plugin,
|
|
108
|
-
entry.state = ok ?
|
|
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 !==
|
|
120
|
+
if (!entry || entry.state !== "started") continue;
|
|
118
121
|
|
|
119
|
-
const ok = await this.invokeHook(plugin,
|
|
120
|
-
entry.state = ok ?
|
|
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 ===
|
|
133
|
+
if (entry.state === "destroyed") continue;
|
|
131
134
|
|
|
132
|
-
await this.invokeHook(plugin,
|
|
133
|
-
entry.state =
|
|
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 ===
|
|
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,
|
|
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
|
|
2
|
-
import { validatePlugin } from
|
|
3
|
-
import { CURRENT_API_VERSION } from
|
|
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:
|
|
7
|
-
type:
|
|
8
|
-
name:
|
|
9
|
-
version:
|
|
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(
|
|
21
|
-
test(
|
|
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(
|
|
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(
|
|
39
|
+
test("rejects plugin with invalid id format", async () => {
|
|
40
40
|
const plugin = {
|
|
41
41
|
...validPlugin,
|
|
42
|
-
id:
|
|
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,
|
|
47
|
+
expect(hasError(result.errors, "kebab-case")).toBe(true);
|
|
48
48
|
});
|
|
49
49
|
|
|
50
|
-
test(
|
|
50
|
+
test("rejects plugin missing type", async () => {
|
|
51
51
|
const plugin = {
|
|
52
|
-
id:
|
|
53
|
-
name:
|
|
54
|
-
version:
|
|
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,
|
|
61
|
+
expect(hasError(result.errors, "must be one of")).toBe(true);
|
|
62
62
|
});
|
|
63
63
|
|
|
64
|
-
test(
|
|
64
|
+
test("rejects plugin with invalid type", async () => {
|
|
65
65
|
const plugin = {
|
|
66
66
|
...validPlugin,
|
|
67
|
-
type:
|
|
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,
|
|
72
|
+
expect(hasError(result.errors, "provider, agent, theme, notification")).toBe(true);
|
|
73
73
|
});
|
|
74
74
|
|
|
75
|
-
test(
|
|
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(
|
|
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(
|
|
97
|
+
test("rejects plugin with invalid version format", async () => {
|
|
98
98
|
const plugin = {
|
|
99
99
|
...validPlugin,
|
|
100
|
-
version:
|
|
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(
|
|
108
|
+
test("rejects plugin missing permissions", async () => {
|
|
109
109
|
const plugin = {
|
|
110
|
-
id:
|
|
111
|
-
type:
|
|
112
|
-
name:
|
|
113
|
-
version:
|
|
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(
|
|
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(
|
|
133
|
+
test("rejects provider plugin missing auth", async () => {
|
|
134
134
|
const providerPlugin = {
|
|
135
135
|
...validPlugin,
|
|
136
|
-
type:
|
|
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(
|
|
145
|
+
test("accepts valid provider plugin with auth and fetchUsage", async () => {
|
|
146
146
|
const providerPlugin = {
|
|
147
147
|
...validPlugin,
|
|
148
|
-
type:
|
|
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(
|
|
160
|
+
test("rejects theme plugin missing colors object", async () => {
|
|
161
161
|
const themePlugin = {
|
|
162
162
|
...validPlugin,
|
|
163
|
-
type:
|
|
164
|
-
colorScheme:
|
|
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(
|
|
172
|
+
test("rejects theme plugin missing colorScheme", async () => {
|
|
173
173
|
const themePlugin = {
|
|
174
174
|
...validPlugin,
|
|
175
|
-
type:
|
|
176
|
-
colors: { primary:
|
|
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,
|
|
181
|
+
expect(hasError(result.errors, "colorScheme")).toBe(true);
|
|
182
182
|
});
|
|
183
183
|
|
|
184
|
-
test(
|
|
184
|
+
test("accepts valid theme plugin", async () => {
|
|
185
185
|
const themePlugin = {
|
|
186
186
|
...validPlugin,
|
|
187
|
-
type:
|
|
188
|
-
colorScheme:
|
|
189
|
-
colors: { primary:
|
|
187
|
+
type: "theme",
|
|
188
|
+
colorScheme: "dark",
|
|
189
|
+
colors: { primary: "#fff" },
|
|
190
190
|
};
|
|
191
191
|
|
|
192
192
|
const result = await validatePlugin(themePlugin);
|