mirror of
https://github.com/aljazceru/goose.git
synced 2025-12-17 06:04:23 +01:00
166 lines
4.7 KiB
TypeScript
166 lines
4.7 KiB
TypeScript
import type { ExtensionConfig } from '../../../api';
|
|
import { toastService } from '../../../toasts';
|
|
import { activateExtension } from './extension-manager';
|
|
import { DEFAULT_EXTENSION_TIMEOUT } from './utils';
|
|
|
|
/**
|
|
* Build an extension config for stdio from the deeplink URL
|
|
*/
|
|
function getStdioConfig(
|
|
cmd: string,
|
|
parsedUrl: URL,
|
|
name: string,
|
|
description: string,
|
|
timeout: number
|
|
) {
|
|
// Validate that the command is one of the allowed commands
|
|
const allowedCommands = ['cu', 'docker', 'jbang', 'npx', 'uvx', 'goosed', 'npx.cmd'];
|
|
if (!allowedCommands.includes(cmd)) {
|
|
toastService.handleError(
|
|
'Invalid Command',
|
|
`Failed to install extension: Invalid command: ${cmd}. Only ${allowedCommands.join(', ')} are allowed.`,
|
|
{ shouldThrow: true }
|
|
);
|
|
}
|
|
|
|
// Check for security risk with npx -c command
|
|
const args = parsedUrl.searchParams.getAll('arg');
|
|
if (cmd === 'npx' && args.includes('-c')) {
|
|
toastService.handleError(
|
|
'Security Risk',
|
|
'Failed to install extension: npx with -c argument can lead to code injection',
|
|
{ shouldThrow: true }
|
|
);
|
|
}
|
|
|
|
const envList = parsedUrl.searchParams.getAll('env');
|
|
|
|
// Create the extension config
|
|
const config: ExtensionConfig = {
|
|
name: name,
|
|
type: 'stdio',
|
|
cmd: cmd,
|
|
description,
|
|
args: args,
|
|
envs:
|
|
envList.length > 0
|
|
? Object.fromEntries(
|
|
envList.map((env) => {
|
|
const [key] = env.split('=');
|
|
return [key, '']; // Initialize with empty string as value
|
|
})
|
|
)
|
|
: undefined,
|
|
timeout: timeout,
|
|
};
|
|
|
|
return config;
|
|
}
|
|
|
|
/**
|
|
* Build an extension config for SSE from the deeplink URL
|
|
*/
|
|
function getSseConfig(remoteUrl: string, name: string, description: string, timeout: number) {
|
|
const config: ExtensionConfig = {
|
|
name,
|
|
type: 'sse',
|
|
uri: remoteUrl,
|
|
description,
|
|
timeout: timeout,
|
|
};
|
|
|
|
return config;
|
|
}
|
|
|
|
/**
|
|
* Build an extension config for Streamable HTTP from the deeplink URL
|
|
*/
|
|
function getStreamableHttpConfig(
|
|
remoteUrl: string,
|
|
name: string,
|
|
description: string,
|
|
timeout: number
|
|
) {
|
|
const config: ExtensionConfig = {
|
|
name,
|
|
type: 'streamable_http',
|
|
uri: remoteUrl,
|
|
description,
|
|
timeout: timeout,
|
|
};
|
|
|
|
return config;
|
|
}
|
|
|
|
/**
|
|
* Handles adding an extension from a deeplink URL
|
|
*/
|
|
export async function addExtensionFromDeepLink(
|
|
url: string,
|
|
addExtensionFn: (
|
|
name: string,
|
|
extensionConfig: ExtensionConfig,
|
|
enabled: boolean
|
|
) => Promise<void>,
|
|
setView: (
|
|
view: string,
|
|
options:
|
|
| { extensionId: string; showEnvVars: boolean }
|
|
| { deepLinkConfig: ExtensionConfig; showEnvVars: boolean }
|
|
) => void
|
|
) {
|
|
const parsedUrl = new URL(url);
|
|
|
|
if (parsedUrl.protocol !== 'goose:') {
|
|
toastService.handleError(
|
|
'Invalid Protocol',
|
|
'Failed to install extension: Invalid protocol: URL must use the goose:// scheme',
|
|
{ shouldThrow: true }
|
|
);
|
|
}
|
|
|
|
// Check that all required fields are present and not empty
|
|
const requiredFields = ['name'];
|
|
|
|
for (const field of requiredFields) {
|
|
const value = parsedUrl.searchParams.get(field);
|
|
if (!value || value.trim() === '') {
|
|
toastService.handleError(
|
|
'Missing Field',
|
|
`Failed to install extension: The link is missing required field '${field}'`,
|
|
{ shouldThrow: true }
|
|
);
|
|
}
|
|
}
|
|
|
|
const name = parsedUrl.searchParams.get('name')!;
|
|
const parsedTimeout = parsedUrl.searchParams.get('timeout');
|
|
const timeout = parsedTimeout ? parseInt(parsedTimeout, 10) : DEFAULT_EXTENSION_TIMEOUT;
|
|
const description = parsedUrl.searchParams.get('description');
|
|
|
|
const cmd = parsedUrl.searchParams.get('cmd');
|
|
const remoteUrl = parsedUrl.searchParams.get('url');
|
|
const transportType = parsedUrl.searchParams.get('transport') || 'sse'; // Default to SSE for backward compatibility
|
|
|
|
const config = remoteUrl
|
|
? transportType === 'streamable_http'
|
|
? getStreamableHttpConfig(remoteUrl, name, description || '', timeout)
|
|
: getSseConfig(remoteUrl, name, description || '', timeout)
|
|
: getStdioConfig(cmd!, parsedUrl, name, description || '', timeout);
|
|
|
|
// Check if extension requires env vars and go to settings if so
|
|
if (config.envs && Object.keys(config.envs).length > 0) {
|
|
console.log('Environment variables required, redirecting to settings');
|
|
setView('settings', { deepLinkConfig: config, showEnvVars: true });
|
|
return;
|
|
}
|
|
|
|
// If no env vars are required, proceed with adding the extension
|
|
try {
|
|
await activateExtension({ extensionConfig: config, addToConfig: addExtensionFn });
|
|
} catch (error) {
|
|
console.error('Failed to activate extension from deeplink:', error);
|
|
throw error;
|
|
}
|
|
}
|