@involvex/youtube-music-cli 0.0.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 +352 -0
- package/dist/eslint.config.d.ts +2 -0
- package/dist/eslint.config.js +55 -0
- package/dist/source/app.d.ts +4 -0
- package/dist/source/app.js +17 -0
- package/dist/source/cli.d.ts +2 -0
- package/dist/source/cli.js +241 -0
- package/dist/source/components/common/ErrorBoundary.d.ts +15 -0
- package/dist/source/components/common/ErrorBoundary.js +22 -0
- package/dist/source/components/common/Help.d.ts +1 -0
- package/dist/source/components/common/Help.js +10 -0
- package/dist/source/components/common/ShortcutsBar.d.ts +1 -0
- package/dist/source/components/common/ShortcutsBar.js +33 -0
- package/dist/source/components/config/ConfigLayout.d.ts +1 -0
- package/dist/source/components/config/ConfigLayout.js +84 -0
- package/dist/source/components/layouts/MainLayout.d.ts +4 -0
- package/dist/source/components/layouts/MainLayout.js +83 -0
- package/dist/source/components/layouts/PlayerLayout.d.ts +1 -0
- package/dist/source/components/layouts/PlayerLayout.js +10 -0
- package/dist/source/components/layouts/PluginsLayout.d.ts +1 -0
- package/dist/source/components/layouts/PluginsLayout.js +77 -0
- package/dist/source/components/layouts/SearchLayout.d.ts +4 -0
- package/dist/source/components/layouts/SearchLayout.js +81 -0
- package/dist/source/components/player/NowPlaying.d.ts +1 -0
- package/dist/source/components/player/NowPlaying.js +21 -0
- package/dist/source/components/player/PlayerControls.d.ts +1 -0
- package/dist/source/components/player/PlayerControls.js +41 -0
- package/dist/source/components/player/ProgressBar.d.ts +1 -0
- package/dist/source/components/player/ProgressBar.js +18 -0
- package/dist/source/components/player/QueueList.d.ts +4 -0
- package/dist/source/components/player/QueueList.js +30 -0
- package/dist/source/components/player/Suggestions.d.ts +1 -0
- package/dist/source/components/player/Suggestions.js +47 -0
- package/dist/source/components/playlist/PlaylistList.d.ts +1 -0
- package/dist/source/components/playlist/PlaylistList.js +11 -0
- package/dist/source/components/plugins/PluginInstallDialog.d.ts +5 -0
- package/dist/source/components/plugins/PluginInstallDialog.js +41 -0
- package/dist/source/components/plugins/PluginsAvailable.d.ts +5 -0
- package/dist/source/components/plugins/PluginsAvailable.js +55 -0
- package/dist/source/components/plugins/PluginsList.d.ts +8 -0
- package/dist/source/components/plugins/PluginsList.js +18 -0
- package/dist/source/components/search/SearchBar.d.ts +8 -0
- package/dist/source/components/search/SearchBar.js +50 -0
- package/dist/source/components/search/SearchResults.d.ts +10 -0
- package/dist/source/components/search/SearchResults.js +111 -0
- package/dist/source/components/settings/Settings.d.ts +1 -0
- package/dist/source/components/settings/Settings.js +42 -0
- package/dist/source/components/theme/ThemeSwitcher.d.ts +1 -0
- package/dist/source/components/theme/ThemeSwitcher.js +11 -0
- package/dist/source/config/themes.config.d.ts +3 -0
- package/dist/source/config/themes.config.js +63 -0
- package/dist/source/contexts/theme.context.d.ts +13 -0
- package/dist/source/contexts/theme.context.js +29 -0
- package/dist/source/hooks/useKeyboard.d.ts +10 -0
- package/dist/source/hooks/useKeyboard.js +104 -0
- package/dist/source/hooks/useNavigation.d.ts +1 -0
- package/dist/source/hooks/useNavigation.js +5 -0
- package/dist/source/hooks/usePlayer.d.ts +23 -0
- package/dist/source/hooks/usePlayer.js +35 -0
- package/dist/source/hooks/usePlaylist.d.ts +8 -0
- package/dist/source/hooks/usePlaylist.js +50 -0
- package/dist/source/hooks/useSearch.d.ts +8 -0
- package/dist/source/hooks/useSearch.js +76 -0
- package/dist/source/hooks/useTerminalSize.d.ts +4 -0
- package/dist/source/hooks/useTerminalSize.js +24 -0
- package/dist/source/hooks/useTheme.d.ts +6 -0
- package/dist/source/hooks/useTheme.js +5 -0
- package/dist/source/hooks/useYouTubeMusic.d.ts +11 -0
- package/dist/source/hooks/useYouTubeMusic.js +112 -0
- package/dist/source/main.d.ts +4 -0
- package/dist/source/main.js +69 -0
- package/dist/source/services/config/config.service.d.ts +26 -0
- package/dist/source/services/config/config.service.js +125 -0
- package/dist/source/services/logger/logger.service.d.ts +10 -0
- package/dist/source/services/logger/logger.service.js +52 -0
- package/dist/source/services/player/player.service.d.ts +58 -0
- package/dist/source/services/player/player.service.js +349 -0
- package/dist/source/services/player-state/player-state.service.d.ts +24 -0
- package/dist/source/services/player-state/player-state.service.js +122 -0
- package/dist/source/services/plugin/plugin-audio-api.d.ts +17 -0
- package/dist/source/services/plugin/plugin-audio-api.js +36 -0
- package/dist/source/services/plugin/plugin-context.d.ts +5 -0
- package/dist/source/services/plugin/plugin-context.js +256 -0
- package/dist/source/services/plugin/plugin-hooks.service.d.ts +62 -0
- package/dist/source/services/plugin/plugin-hooks.service.js +135 -0
- package/dist/source/services/plugin/plugin-installer.service.d.ts +27 -0
- package/dist/source/services/plugin/plugin-installer.service.js +247 -0
- package/dist/source/services/plugin/plugin-loader.service.d.ts +33 -0
- package/dist/source/services/plugin/plugin-loader.service.js +161 -0
- package/dist/source/services/plugin/plugin-permissions.service.d.ts +72 -0
- package/dist/source/services/plugin/plugin-permissions.service.js +194 -0
- package/dist/source/services/plugin/plugin-registry.service.d.ts +76 -0
- package/dist/source/services/plugin/plugin-registry.service.js +215 -0
- package/dist/source/services/plugin/plugin-ui-api.d.ts +25 -0
- package/dist/source/services/plugin/plugin-ui-api.js +46 -0
- package/dist/source/services/plugin/plugin-updater.service.d.ts +23 -0
- package/dist/source/services/plugin/plugin-updater.service.js +206 -0
- package/dist/source/services/youtube-music/api.d.ts +13 -0
- package/dist/source/services/youtube-music/api.js +371 -0
- package/dist/source/services/youtube-music/search.service.d.ts +11 -0
- package/dist/source/services/youtube-music/search.service.js +38 -0
- package/dist/source/stores/navigation.store.d.ts +10 -0
- package/dist/source/stores/navigation.store.js +67 -0
- package/dist/source/stores/player.store.d.ts +28 -0
- package/dist/source/stores/player.store.js +458 -0
- package/dist/source/stores/plugins.store.d.ts +46 -0
- package/dist/source/stores/plugins.store.js +177 -0
- package/dist/source/types/actions.d.ts +119 -0
- package/dist/source/types/actions.js +1 -0
- package/dist/source/types/cli.types.d.ts +14 -0
- package/dist/source/types/cli.types.js +1 -0
- package/dist/source/types/config.types.d.ts +19 -0
- package/dist/source/types/config.types.js +1 -0
- package/dist/source/types/keyboard.types.d.ts +5 -0
- package/dist/source/types/keyboard.types.js +1 -0
- package/dist/source/types/navigation.types.d.ts +14 -0
- package/dist/source/types/navigation.types.js +1 -0
- package/dist/source/types/player.types.d.ts +16 -0
- package/dist/source/types/player.types.js +1 -0
- package/dist/source/types/playlist.types.d.ts +12 -0
- package/dist/source/types/playlist.types.js +1 -0
- package/dist/source/types/plugin.types.d.ts +239 -0
- package/dist/source/types/plugin.types.js +1 -0
- package/dist/source/types/theme.types.d.ts +18 -0
- package/dist/source/types/theme.types.js +1 -0
- package/dist/source/types/youtube-music.types.d.ts +35 -0
- package/dist/source/types/youtube-music.types.js +1 -0
- package/dist/source/types/youtubei.types.d.ts +60 -0
- package/dist/source/types/youtubei.types.js +3 -0
- package/dist/source/utils/constants.d.ts +65 -0
- package/dist/source/utils/constants.js +82 -0
- package/dist/source/utils/format.d.ts +3 -0
- package/dist/source/utils/format.js +24 -0
- package/dist/test.d.ts +1 -0
- package/dist/test.js +13 -0
- package/package.json +100 -0
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { CONFIG_DIR } from "../../utils/constants.js";
|
|
2
|
+
import { logger } from "../logger/logger.service.js";
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { existsSync, mkdirSync, rmSync, cpSync } from 'node:fs';
|
|
5
|
+
import { execSync } from 'node:child_process';
|
|
6
|
+
const PLUGINS_DIR = join(CONFIG_DIR, 'plugins');
|
|
7
|
+
const DEFAULT_PLUGIN_REPO = 'https://github.com/involvex/youtube-music-cli-plugins';
|
|
8
|
+
/**
|
|
9
|
+
* Plugin installer service
|
|
10
|
+
*/
|
|
11
|
+
class PluginInstallerService {
|
|
12
|
+
/**
|
|
13
|
+
* Install a plugin from GitHub repository
|
|
14
|
+
*/
|
|
15
|
+
async installFromGitHub(repoUrl, pluginName) {
|
|
16
|
+
try {
|
|
17
|
+
logger.info('PluginInstallerService', `Installing from ${repoUrl}`);
|
|
18
|
+
// Ensure plugins directory exists
|
|
19
|
+
if (!existsSync(PLUGINS_DIR)) {
|
|
20
|
+
mkdirSync(PLUGINS_DIR, { recursive: true });
|
|
21
|
+
}
|
|
22
|
+
// Determine plugin name from URL if not provided
|
|
23
|
+
if (!pluginName) {
|
|
24
|
+
const match = repoUrl.match(/\/([^/]+?)(\.git)?$/);
|
|
25
|
+
pluginName = match?.[1] || 'unknown-plugin';
|
|
26
|
+
}
|
|
27
|
+
const targetDir = join(PLUGINS_DIR, pluginName);
|
|
28
|
+
// Check if plugin already exists
|
|
29
|
+
if (existsSync(targetDir)) {
|
|
30
|
+
return {
|
|
31
|
+
success: false,
|
|
32
|
+
error: `Plugin ${pluginName} is already installed`,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
// Clone repository
|
|
36
|
+
try {
|
|
37
|
+
execSync(`git clone "${repoUrl}" "${targetDir}"`, {
|
|
38
|
+
stdio: 'pipe',
|
|
39
|
+
windowsHide: true,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
logger.error('PluginInstallerService', 'Git clone failed:', error);
|
|
44
|
+
return {
|
|
45
|
+
success: false,
|
|
46
|
+
error: `Failed to clone repository: ${error instanceof Error ? error.message : String(error)}`,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
// Validate plugin structure
|
|
50
|
+
const manifestPath = join(targetDir, 'plugin.json');
|
|
51
|
+
if (!existsSync(manifestPath)) {
|
|
52
|
+
// Cleanup
|
|
53
|
+
rmSync(targetDir, { recursive: true, force: true });
|
|
54
|
+
return {
|
|
55
|
+
success: false,
|
|
56
|
+
error: 'Invalid plugin: plugin.json not found',
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
// Check for npm dependencies
|
|
60
|
+
const packageJsonPath = join(targetDir, 'package.json');
|
|
61
|
+
if (existsSync(packageJsonPath)) {
|
|
62
|
+
logger.info('PluginInstallerService', 'Installing plugin dependencies...');
|
|
63
|
+
try {
|
|
64
|
+
execSync('bun install', {
|
|
65
|
+
cwd: targetDir,
|
|
66
|
+
stdio: 'pipe',
|
|
67
|
+
windowsHide: true,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
logger.error('PluginInstallerService', 'Failed to install dependencies:', error);
|
|
72
|
+
// Continue anyway - plugin might still work
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
logger.info('PluginInstallerService', `Successfully installed plugin: ${pluginName}`);
|
|
76
|
+
return {
|
|
77
|
+
success: true,
|
|
78
|
+
pluginId: pluginName,
|
|
79
|
+
message: `Plugin ${pluginName} installed successfully`,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
logger.error('PluginInstallerService', 'Install failed:', error);
|
|
84
|
+
return {
|
|
85
|
+
success: false,
|
|
86
|
+
error: `Installation failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Install a plugin from the default plugin repository
|
|
92
|
+
*/
|
|
93
|
+
async installFromDefaultRepo(pluginName) {
|
|
94
|
+
logger.info('PluginInstallerService', `Installing ${pluginName} from default repo`);
|
|
95
|
+
// For now, use sparse checkout or clone entire repo and copy plugin
|
|
96
|
+
// Simplified: clone entire repo to temp, copy plugin, delete temp
|
|
97
|
+
try {
|
|
98
|
+
const tempDir = join(PLUGINS_DIR, '.temp-install');
|
|
99
|
+
// Clone default repo
|
|
100
|
+
if (!existsSync(tempDir)) {
|
|
101
|
+
execSync(`git clone "${DEFAULT_PLUGIN_REPO}" "${tempDir}"`, {
|
|
102
|
+
stdio: 'pipe',
|
|
103
|
+
windowsHide: true,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
// Pull latest
|
|
108
|
+
execSync('git pull', {
|
|
109
|
+
cwd: tempDir,
|
|
110
|
+
stdio: 'pipe',
|
|
111
|
+
windowsHide: true,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
const pluginSourceDir = join(tempDir, 'plugins', pluginName);
|
|
115
|
+
if (!existsSync(pluginSourceDir)) {
|
|
116
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
117
|
+
return {
|
|
118
|
+
success: false,
|
|
119
|
+
error: `Plugin ${pluginName} not found in default repository`,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
const targetDir = join(PLUGINS_DIR, pluginName);
|
|
123
|
+
if (existsSync(targetDir)) {
|
|
124
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
125
|
+
return {
|
|
126
|
+
success: false,
|
|
127
|
+
error: `Plugin ${pluginName} is already installed`,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
// Copy plugin to plugins directory
|
|
131
|
+
cpSync(pluginSourceDir, targetDir, { recursive: true });
|
|
132
|
+
// Cleanup temp directory
|
|
133
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
134
|
+
// Install dependencies if needed
|
|
135
|
+
const packageJsonPath = join(targetDir, 'package.json');
|
|
136
|
+
if (existsSync(packageJsonPath)) {
|
|
137
|
+
try {
|
|
138
|
+
execSync('bun install', {
|
|
139
|
+
cwd: targetDir,
|
|
140
|
+
stdio: 'pipe',
|
|
141
|
+
windowsHide: true,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
logger.warn('PluginInstallerService', 'Failed to install dependencies:', error);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
success: true,
|
|
150
|
+
pluginId: pluginName,
|
|
151
|
+
message: `Plugin ${pluginName} installed successfully`,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
logger.error('PluginInstallerService', 'Install failed:', error);
|
|
156
|
+
return {
|
|
157
|
+
success: false,
|
|
158
|
+
error: `Installation failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Install a plugin from local directory (for development)
|
|
164
|
+
*/
|
|
165
|
+
async installFromLocal(sourcePath) {
|
|
166
|
+
try {
|
|
167
|
+
if (!existsSync(sourcePath)) {
|
|
168
|
+
return {
|
|
169
|
+
success: false,
|
|
170
|
+
error: `Source path does not exist: ${sourcePath}`,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
const manifestPath = join(sourcePath, 'plugin.json');
|
|
174
|
+
if (!existsSync(manifestPath)) {
|
|
175
|
+
return {
|
|
176
|
+
success: false,
|
|
177
|
+
error: 'Invalid plugin: plugin.json not found',
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
// Read manifest to get plugin ID
|
|
181
|
+
const { readFileSync: fsReadFileSync } = await import('node:fs');
|
|
182
|
+
const manifest = JSON.parse(fsReadFileSync(manifestPath, 'utf-8'));
|
|
183
|
+
const targetDir = join(PLUGINS_DIR, manifest.id);
|
|
184
|
+
if (existsSync(targetDir)) {
|
|
185
|
+
return {
|
|
186
|
+
success: false,
|
|
187
|
+
error: `Plugin ${manifest.id} is already installed`,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
// Copy to plugins directory
|
|
191
|
+
if (!existsSync(PLUGINS_DIR)) {
|
|
192
|
+
mkdirSync(PLUGINS_DIR, { recursive: true });
|
|
193
|
+
}
|
|
194
|
+
cpSync(sourcePath, targetDir, { recursive: true });
|
|
195
|
+
return {
|
|
196
|
+
success: true,
|
|
197
|
+
pluginId: manifest.id,
|
|
198
|
+
message: `Plugin ${manifest.name} installed from local path`,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
catch (error) {
|
|
202
|
+
logger.error('PluginInstallerService', 'Install failed:', error);
|
|
203
|
+
return {
|
|
204
|
+
success: false,
|
|
205
|
+
error: `Installation failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Uninstall a plugin
|
|
211
|
+
*/
|
|
212
|
+
async uninstall(pluginId) {
|
|
213
|
+
const pluginDir = join(PLUGINS_DIR, pluginId);
|
|
214
|
+
if (!existsSync(pluginDir)) {
|
|
215
|
+
return {
|
|
216
|
+
success: false,
|
|
217
|
+
error: `Plugin ${pluginId} is not installed`,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
try {
|
|
221
|
+
rmSync(pluginDir, { recursive: true, force: true });
|
|
222
|
+
return {
|
|
223
|
+
success: true,
|
|
224
|
+
pluginId,
|
|
225
|
+
message: `Plugin ${pluginId} uninstalled successfully`,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
logger.error('PluginInstallerService', 'Uninstall failed:', error);
|
|
230
|
+
return {
|
|
231
|
+
success: false,
|
|
232
|
+
error: `Uninstall failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
// Singleton instance
|
|
238
|
+
let instance = null;
|
|
239
|
+
/**
|
|
240
|
+
* Get the plugin installer service singleton
|
|
241
|
+
*/
|
|
242
|
+
export function getPluginInstallerService() {
|
|
243
|
+
if (!instance) {
|
|
244
|
+
instance = new PluginInstallerService();
|
|
245
|
+
}
|
|
246
|
+
return instance;
|
|
247
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Plugin, PluginInstance } from '../../types/plugin.types.ts';
|
|
2
|
+
/**
|
|
3
|
+
* Plugin loader service - handles dynamic loading of plugins
|
|
4
|
+
*/
|
|
5
|
+
declare class PluginLoaderService {
|
|
6
|
+
private jiti;
|
|
7
|
+
constructor();
|
|
8
|
+
/**
|
|
9
|
+
* Load a plugin from a directory
|
|
10
|
+
*/
|
|
11
|
+
loadPlugin(pluginPath: string): Promise<PluginInstance>;
|
|
12
|
+
/**
|
|
13
|
+
* Validate plugin module structure
|
|
14
|
+
*/
|
|
15
|
+
private isValidPlugin;
|
|
16
|
+
/**
|
|
17
|
+
* Call plugin lifecycle hook safely
|
|
18
|
+
*/
|
|
19
|
+
callHook(plugin: Plugin, hook: 'init' | 'enable' | 'disable' | 'destroy', context: PluginInstance['context']): Promise<void>;
|
|
20
|
+
/**
|
|
21
|
+
* Reload a plugin (useful for development)
|
|
22
|
+
*/
|
|
23
|
+
reloadPlugin(pluginPath: string): Promise<PluginInstance>;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Get the plugin loader service singleton
|
|
27
|
+
*/
|
|
28
|
+
export declare function getPluginLoaderService(): PluginLoaderService;
|
|
29
|
+
/**
|
|
30
|
+
* Reset the singleton (for testing)
|
|
31
|
+
*/
|
|
32
|
+
export declare function resetPluginLoaderService(): void;
|
|
33
|
+
export {};
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { logger } from "../logger/logger.service.js";
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { createJiti } from 'jiti';
|
|
5
|
+
/**
|
|
6
|
+
* Validate plugin manifest
|
|
7
|
+
*/
|
|
8
|
+
function validateManifest(manifest) {
|
|
9
|
+
if (typeof manifest !== 'object' || manifest === null) {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
const m = manifest;
|
|
13
|
+
return (typeof m['id'] === 'string' &&
|
|
14
|
+
typeof m['name'] === 'string' &&
|
|
15
|
+
typeof m['version'] === 'string' &&
|
|
16
|
+
typeof m['description'] === 'string' &&
|
|
17
|
+
typeof m['author'] === 'string' &&
|
|
18
|
+
typeof m['main'] === 'string' &&
|
|
19
|
+
Array.isArray(m['permissions']));
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Plugin loader service - handles dynamic loading of plugins
|
|
23
|
+
*/
|
|
24
|
+
class PluginLoaderService {
|
|
25
|
+
jiti;
|
|
26
|
+
constructor() {
|
|
27
|
+
// Initialize jiti for dynamic TypeScript loading
|
|
28
|
+
this.jiti = createJiti(import.meta.url, {
|
|
29
|
+
interopDefault: true,
|
|
30
|
+
requireCache: false,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Load a plugin from a directory
|
|
35
|
+
*/
|
|
36
|
+
async loadPlugin(pluginPath) {
|
|
37
|
+
logger.info('PluginLoaderService', `Loading plugin from ${pluginPath}`);
|
|
38
|
+
// Load and validate manifest
|
|
39
|
+
const manifestPath = join(pluginPath, 'plugin.json');
|
|
40
|
+
if (!existsSync(manifestPath)) {
|
|
41
|
+
throw new Error(`Plugin manifest not found: ${manifestPath}`);
|
|
42
|
+
}
|
|
43
|
+
const manifestData = readFileSync(manifestPath, 'utf-8');
|
|
44
|
+
const manifest = JSON.parse(manifestData);
|
|
45
|
+
if (!validateManifest(manifest)) {
|
|
46
|
+
throw new Error(`Invalid plugin manifest: ${manifestPath}`);
|
|
47
|
+
}
|
|
48
|
+
logger.debug('PluginLoaderService', `Validated manifest for ${manifest.name} v${manifest.version}`);
|
|
49
|
+
// Load plugin module
|
|
50
|
+
const pluginEntryPath = join(pluginPath, manifest.main);
|
|
51
|
+
if (!existsSync(pluginEntryPath)) {
|
|
52
|
+
throw new Error(`Plugin entry point not found: ${pluginEntryPath}`);
|
|
53
|
+
}
|
|
54
|
+
let pluginModule;
|
|
55
|
+
try {
|
|
56
|
+
// Use jiti to load TypeScript/JavaScript module
|
|
57
|
+
pluginModule = await this.jiti.import(pluginEntryPath);
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
logger.error('PluginLoaderService', `Failed to load plugin module:`, error);
|
|
61
|
+
throw new Error(`Failed to load plugin from ${pluginEntryPath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
62
|
+
}
|
|
63
|
+
// Validate plugin module
|
|
64
|
+
if (!this.isValidPlugin(pluginModule)) {
|
|
65
|
+
throw new Error(`Invalid plugin module: ${pluginEntryPath}`);
|
|
66
|
+
}
|
|
67
|
+
const plugin = pluginModule;
|
|
68
|
+
// Verify manifest consistency
|
|
69
|
+
if (plugin.manifest.id !== manifest.id) {
|
|
70
|
+
logger.warn('PluginLoaderService', `Plugin manifest ID mismatch: ${plugin.manifest.id} vs ${manifest.id}`);
|
|
71
|
+
}
|
|
72
|
+
logger.info('PluginLoaderService', `Successfully loaded plugin: ${manifest.name} v${manifest.version}`);
|
|
73
|
+
// Create plugin instance (context will be injected later)
|
|
74
|
+
const instance = {
|
|
75
|
+
manifest,
|
|
76
|
+
plugin,
|
|
77
|
+
// Note: Context is set by plugin registry
|
|
78
|
+
context: null,
|
|
79
|
+
config: {
|
|
80
|
+
enabled: false,
|
|
81
|
+
config: {},
|
|
82
|
+
permissions: {},
|
|
83
|
+
},
|
|
84
|
+
enabled: false,
|
|
85
|
+
loadedAt: Date.now(),
|
|
86
|
+
};
|
|
87
|
+
return instance;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Validate plugin module structure
|
|
91
|
+
*/
|
|
92
|
+
isValidPlugin(module) {
|
|
93
|
+
if (typeof module !== 'object' || module === null) {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
const m = module;
|
|
97
|
+
// Must have manifest
|
|
98
|
+
if (!m['manifest'] || typeof m['manifest'] !== 'object') {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
// Optional lifecycle hooks must be functions if present
|
|
102
|
+
if (m['init'] && typeof m['init'] !== 'function') {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
if (m['enable'] && typeof m['enable'] !== 'function') {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
if (m['disable'] && typeof m['disable'] !== 'function') {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
if (m['destroy'] && typeof m['destroy'] !== 'function') {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Call plugin lifecycle hook safely
|
|
118
|
+
*/
|
|
119
|
+
async callHook(plugin, hook, context) {
|
|
120
|
+
const hookFn = plugin[hook];
|
|
121
|
+
if (!hookFn) {
|
|
122
|
+
logger.debug('PluginLoaderService', `Plugin ${plugin.manifest.name} has no ${hook} hook`);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
try {
|
|
126
|
+
logger.debug('PluginLoaderService', `Calling ${hook} hook for ${plugin.manifest.name}`);
|
|
127
|
+
await Promise.resolve(hookFn(context));
|
|
128
|
+
logger.debug('PluginLoaderService', `${hook} hook completed for ${plugin.manifest.name}`);
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
logger.error('PluginLoaderService', `Error in ${hook} hook for ${plugin.manifest.name}:`, error);
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Reload a plugin (useful for development)
|
|
137
|
+
*/
|
|
138
|
+
async reloadPlugin(pluginPath) {
|
|
139
|
+
logger.info('PluginLoaderService', `Reloading plugin from ${pluginPath}`);
|
|
140
|
+
// Clear jiti cache for this plugin
|
|
141
|
+
// Note: jiti has requireCache: false, so this should work automatically
|
|
142
|
+
return this.loadPlugin(pluginPath);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// Singleton instance
|
|
146
|
+
let instance = null;
|
|
147
|
+
/**
|
|
148
|
+
* Get the plugin loader service singleton
|
|
149
|
+
*/
|
|
150
|
+
export function getPluginLoaderService() {
|
|
151
|
+
if (!instance) {
|
|
152
|
+
instance = new PluginLoaderService();
|
|
153
|
+
}
|
|
154
|
+
return instance;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Reset the singleton (for testing)
|
|
158
|
+
*/
|
|
159
|
+
export function resetPluginLoaderService() {
|
|
160
|
+
instance = null;
|
|
161
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { PluginPermission, PluginPermissions, PermissionStatus } from '../../types/plugin.types.ts';
|
|
2
|
+
/**
|
|
3
|
+
* Plugin permissions service - manages permission grants and denials
|
|
4
|
+
*/
|
|
5
|
+
declare class PluginPermissionsService {
|
|
6
|
+
private permissions;
|
|
7
|
+
private permissionsPath;
|
|
8
|
+
private configDir;
|
|
9
|
+
onPermissionRequest?: (pluginId: string, permission: PluginPermission) => Promise<boolean>;
|
|
10
|
+
constructor();
|
|
11
|
+
/**
|
|
12
|
+
* Load permissions from disk
|
|
13
|
+
*/
|
|
14
|
+
private load;
|
|
15
|
+
/**
|
|
16
|
+
* Save permissions to disk
|
|
17
|
+
*/
|
|
18
|
+
private save;
|
|
19
|
+
/**
|
|
20
|
+
* Check if a plugin has a specific permission
|
|
21
|
+
*/
|
|
22
|
+
hasPermission(pluginId: string, permission: PluginPermission): boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Get permission status
|
|
25
|
+
*/
|
|
26
|
+
getPermissionStatus(pluginId: string, permission: PluginPermission): PermissionStatus;
|
|
27
|
+
/**
|
|
28
|
+
* Get all permissions for a plugin
|
|
29
|
+
*/
|
|
30
|
+
getPermissions(pluginId: string): PluginPermissions;
|
|
31
|
+
/**
|
|
32
|
+
* Grant a permission to a plugin
|
|
33
|
+
*/
|
|
34
|
+
grantPermission(pluginId: string, permission: PluginPermission): void;
|
|
35
|
+
/**
|
|
36
|
+
* Deny a permission to a plugin
|
|
37
|
+
*/
|
|
38
|
+
denyPermission(pluginId: string, permission: PluginPermission): void;
|
|
39
|
+
/**
|
|
40
|
+
* Request permission from user
|
|
41
|
+
*/
|
|
42
|
+
requestPermission(pluginId: string, permission: PluginPermission): Promise<boolean>;
|
|
43
|
+
/**
|
|
44
|
+
* Grant multiple permissions at once
|
|
45
|
+
*/
|
|
46
|
+
grantPermissions(pluginId: string, permissions: PluginPermission[]): void;
|
|
47
|
+
/**
|
|
48
|
+
* Revoke a permission
|
|
49
|
+
*/
|
|
50
|
+
revokePermission(pluginId: string, permission: PluginPermission): void;
|
|
51
|
+
/**
|
|
52
|
+
* Revoke all permissions for a plugin
|
|
53
|
+
*/
|
|
54
|
+
revokeAllPermissions(pluginId: string): void;
|
|
55
|
+
/**
|
|
56
|
+
* Get all plugin IDs with permissions
|
|
57
|
+
*/
|
|
58
|
+
getAllPluginIds(): string[];
|
|
59
|
+
/**
|
|
60
|
+
* Reset all permissions (for testing or user request)
|
|
61
|
+
*/
|
|
62
|
+
resetAll(): void;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Get the plugin permissions service singleton
|
|
66
|
+
*/
|
|
67
|
+
export declare function getPluginPermissionsService(): PluginPermissionsService;
|
|
68
|
+
/**
|
|
69
|
+
* Reset the singleton (for testing)
|
|
70
|
+
*/
|
|
71
|
+
export declare function resetPluginPermissionsService(): void;
|
|
72
|
+
export {};
|