Overview
SuperCmd’s extension runtime provides full compatibility with Raycast extensions without requiring any modifications to extension code. The runtime achieves this through a sophisticated bundling and shimming system that intercepts extension imports and provides SuperCmd’s implementations of the Raycast API.
Architecture
The extension execution model follows these key principles:
Extension Discovery
Extensions are discovered from configured directories and the Raycast registry
Build-Time Bundling
Extension code is bundled to CommonJS using esbuild at install time
Runtime Shimming
A custom require() function provides React and @raycast/api implementations
Isolated Execution
Extensions run in isolated contexts while sharing React with the host app
Extension Loading
Discovery Process
Extensions are discovered from multiple sources:
function getConfiguredExtensionRoots () : string [] {
const settingsPaths = loadSettings (). customExtensionFolders || [];
const envPaths = process . env . SUPERCMD_EXTENSION_PATHS ?. split ( ':' ) || [];
return [
getManagedExtensionsDir (), // ~/Library/Application Support/SuperCmd/extensions
... settingsPaths ,
... envPaths
];
}
The system scans these directories for valid package.json files that define Raycast extension manifests.
Extension Structure
Each extension must have:
package.json : Manifest defining commands, preferences, and metadata
src/ : Source code directory containing command entry points
assets/ : Optional icon and media files
node_modules/ : Runtime dependencies (installed at build time)
Build System
Bundling with esbuild
SuperCmd bundles extensions at install time, not runtime. This approach provides:
Fast Loading : Pre-built bundles load instantly
Dependency Resolution : All imports are resolved at build time
Code Optimization : Minification and tree-shaking reduce bundle size
Build Configuration
External Modules
Entry Resolution
await esbuild . build ({
entryPoints: [ entryFile ],
absWorkingDir: extPath ,
bundle: true ,
format: 'cjs' ,
platform: 'node' ,
outfile: path . join ( buildDir , ` ${ cmdName } .js` ),
external: [
'react' ,
'react-dom' ,
'@raycast/api' ,
'@raycast/utils' ,
're2' ,
'better-sqlite3' ,
... nodeBuiltins
],
target: 'es2020' ,
jsx: 'automatic' ,
jsxImportSource: 'react' ,
});
The following modules are kept external and provided at runtime:
react , react-dom : Shared with host app
@raycast/api , @raycast/utils : Compatibility shim
Native modules : re2, better-sqlite3, fsevents
Node.js built-ins : All standard modules
function resolveEntryFile ( extPath : string , cmd : any ) : string | null {
const candidates = [
path . join ( srcDir , ` ${ cmdName } .tsx` ),
path . join ( srcDir , ` ${ cmdName } .ts` ),
path . join ( srcDir , cmdName , 'index.tsx' ),
path . join ( srcDir , 'commands' , ` ${ cmdName } .tsx` ),
];
return candidates . find ( p => fs . existsSync ( p )) || null ;
}
TypeScript Configuration
Extensions can define custom TypeScript compiler options:
function getEsbuildTsconfigRaw ( extPath : string ) : string {
const extensionCompilerOptions = getExtensionCompilerOptions ( extPath );
return JSON . stringify ({
compilerOptions: {
target: 'ES2020' ,
jsx: 'react-jsx' ,
jsxImportSource: 'react' ,
strict: false ,
esModuleInterop: true ,
moduleResolution: 'node' ,
... extensionCompilerOptions ,
},
});
}
This allows extensions to use:
Custom baseUrl and paths for import aliases
Alternative jsx configurations
Extension-specific compiler flags
Runtime Execution
Bundle Loading
When a command is executed, SuperCmd loads the pre-built bundle:
export async function getExtensionBundle (
extName : string ,
cmdName : string
) : Promise < ExtensionBundleResult | null > {
const extPath = resolveInstalledExtensionPath ( extName );
let outFile = path . join ( extPath , '.sc-build' , ` ${ cmdName } .js` );
// On-demand build if missing
if ( ! fs . existsSync ( outFile )) {
await buildSingleCommand ( extName , cmdName );
}
const code = fs . readFileSync ( outFile , 'utf-8' );
// ... metadata extraction ...
return { code , title , mode , preferences , ... };
}
Custom Require Shim
The renderer process provides a custom require() function that intercepts module requests:
Extensions share the same React instance as the host app. This is critical for React contexts, hooks, and component lifecycle to work correctly.
// Simplified example of the require shim
function fakeRequire ( moduleName : string ) {
if ( moduleName === 'react' || moduleName === 'react-dom' ) {
return React ; // Shared instance
}
if ( moduleName === '@raycast/api' ) {
return raycastApiShim ; // Compatibility layer
}
if ( moduleName === '@raycast/utils' ) {
return raycastUtilsShim ; // Utility hooks
}
// Node.js built-ins go through Electron IPC
if ( nodeBuiltins . includes ( moduleName )) {
return createNodeShim ( moduleName );
}
throw new Error ( `Module not found: ${ moduleName } ` );
}
Extension Context
Each extension receives a context object with metadata and preferences:
export interface ExtensionContextType {
extensionName : string ;
extensionDisplayName ?: string ;
commandName : string ;
assetsPath : string ; // Path to assets/ directory
supportPath : string ; // Path for extension data storage
owner : string ;
preferences : Record < string , any >;
commandMode : 'view' | 'no-view' | 'menu-bar' ;
}
This context is accessible via the environment object:
import { environment } from '@raycast/api' ;
// Inside extension code
console . log ( environment . extensionName ); // 'my-extension'
console . log ( environment . assetsPath ); // '/path/to/assets'
Preferences System
Preference Definition
Extensions define preferences in their manifest:
{
"preferences" : [
{
"name" : "apiKey" ,
"type" : "password" ,
"required" : true ,
"title" : "API Key" ,
"description" : "Your service API key"
}
],
"commands" : [
{
"name" : "search" ,
"title" : "Search Items" ,
"preferences" : [
{
"name" : "limit" ,
"type" : "textfield" ,
"default" : "10" ,
"title" : "Result Limit"
}
]
}
]
}
Preference Resolution
function parsePreferences ( pkg : any , cmdName : string ) {
const extensionPrefs : Record < string , any > = {};
const commandPrefs : Record < string , any > = {};
// Extension-level preferences
for ( const pref of pkg . preferences || []) {
const resolvedDefault = resolvePlatformDefault ( pref . default );
if ( resolvedDefault !== undefined ) {
extensionPrefs [ pref . name ] = resolvedDefault ;
} else if ( pref . type === 'checkbox' ) {
extensionPrefs [ pref . name ] = false ;
} else if ( pref . type === 'textfield' ) {
extensionPrefs [ pref . name ] = '' ;
}
}
return { extensionPrefs , commandPrefs };
}
Preferences support platform-specific defaults:
function resolvePlatformDefault ( value : any ) : any {
const platformKey = process . platform === 'win32' ? 'Windows' : 'macOS' ;
if ( value && typeof value === 'object' && ! Array . isArray ( value )) {
if ( value . hasOwnProperty ( platformKey )) {
return value [ platformKey ];
}
return value . macOS ?? value . Windows ;
}
return value ;
}
Dependency Management
Runtime Dependencies
Extensions can declare runtime dependencies in package.json:
{
"dependencies" : {
"axios" : "^1.6.0" ,
"date-fns" : "^2.30.0"
}
}
SuperCmd installs these dependencies at extension install time:
function getInstallableRuntimeDeps ( pkg : any ) : string [] {
const deps = {
... ( pkg ?. dependencies || {}),
... ( pkg ?. optionalDependencies || {}),
};
return Object . entries ( deps )
. filter (([ name ]) => ! name . startsWith ( '@raycast/' ))
. map (([ name , version ]) => ` ${ name } @ ${ version } ` );
}
External Packages
Certain packages must remain external due to native bindings or special handling:
Native Modules
HTTP Libraries
const nativeModules = [
're2' , // C++ regex engine
'better-sqlite3' , // SQLite native bindings
'fsevents' , // macOS file system events
];
const httpLibs = [
'axios' ,
'node-fetch' ,
'undici' ,
'tar' ,
'extract-zip' ,
];
These are shimmed in the renderer to route through Electron IPC for proper file system access.
Extensions can specify platform requirements:
{
"platforms" : [ "darwin" ],
"commands" : [
{
"name" : "mac-only" ,
"platforms" : [ "darwin" ]
}
]
}
function isManifestPlatformCompatible ( pkg : any ) : boolean {
if ( ! pkg . platforms ) return true ;
const currentPlatform = process . platform === 'win32' ? 'Windows' : 'macOS' ;
return pkg . platforms . includes ( currentPlatform );
}
Error Handling
Build Failures
When a build fails, SuperCmd provides detailed diagnostics:
if ( ! fs . existsSync ( outFile )) {
let diagnostic = '' ;
try {
const cmd = commands . find (( c : any ) => c ?. name === cmdName );
const entry = resolveEntryFile ( extPath , cmd );
if ( ! cmd ) {
diagnostic = ` Command " ${ cmdName } " not found in package.json.` ;
} else if ( ! entry ) {
diagnostic = ` Entry file not found for " ${ cmdName } ".` ;
} else if ( requiresNodeModules && ! nodeModulesExists ) {
diagnostic = ' node_modules is missing.' ;
}
} catch {}
throw new Error (
`Build failed for ${ extName } / ${ cmdName } . ${ diagnostic } `
);
}
Runtime Errors
Extension errors are captured and reported through the SuperCmd UI:
try {
// Execute extension code
const module = { exports: {} };
const fn = new Function ( 'require' , 'module' , 'exports' , code );
fn ( fakeRequire , module , module . exports );
} catch ( error ) {
console . error ( 'Extension execution error:' , error );
showToast ({
style: Toast . Style . Failure ,
title: 'Extension Error' ,
message: error . message
});
}
Build Caching
Pre-built Bundles All commands are built at install time, eliminating runtime compilation overhead.
Incremental Builds Only changed commands are rebuilt when extensions are updated.
Parallel Bundling Multiple commands can be built in parallel for faster installation.
Shared Dependencies React and common utilities are loaded once and shared across all extensions.
On-Demand Building
If a pre-built bundle is missing, SuperCmd builds it on-demand:
export async function buildSingleCommand (
extName : string ,
cmdName : string
) : Promise < boolean > {
const extPath = resolveInstalledExtensionPath ( extName );
const entryFile = resolveEntryFile ( extPath , cmd );
console . log ( `On-demand building ${ extName } / ${ cmdName } ...` );
await esbuild . build ({ /* ... */ });
return fs . existsSync ( outFile );
}
Best Practices
Keep command entry points small and focused
Use dynamic imports for large dependencies
Minimize the number of external dependencies
Test extensions with NODE_ENV=production
Use console.log() for debugging (appears in main console)
Check .sc-build/ directory for built bundles
Inspect bundled code to verify transformations
Test with real Raycast extensions to ensure compatibility
See Also
Raycast API Learn about the Raycast API compatibility layer
Electron Architecture Understand the Electron process architecture
Native Modules Explore SuperCmd’s native Swift integrations
Extension Registry Install and manage extensions