Viberunner is a powerful runtime container for your vibe-coded apps. It allows you to manage and run several React-based utilities and apps on your desktop, with powerful matchers to select the right app based on the context.
Viberunner is a single-player utility meant to be used by one person on their own computer. It's not a way to ship your React product to your users. Rather, it allows you to easily manage, organize, and create vibe-coded system utilities that can do just about anything, including:
Standalone utilities:
- Realtime clipboard watcher and history
- Dotfile editor to edit your most common global dotfiles (.bashrc, .vimprofile, .zshrc, etc)
- Mouse jiggler that moves your mouse every X seconds
- Port sniffer that shows open ports and associated processes
Contextual apps:
- Image redactor: drag and drop an image and draw to redact certain areas
- YouTube Thumbnail Preview: drag a thumbnail and preview how it would look on YouTube with ability to set title and channel name.
- Package upgrader: drag and drop a package.json file and check which packages need upgrading, and upgrade them with one click.
- When you launch Viberunner, you'll be asked to choose a directory of where your apps are located.
- Viberunner apps are just regular React apps which have deep system access (Node, Python; anything a terminal can do, your Viberunner apps can do).
- You build a new Viberunner app using your LLM agent of choice (Cursor, for example), by feeding it this README file and instructing it to create a new "app" based on your requirements.
- Save your app in the apps directory you set in step 1, and launch Viberunner.
- You can now run your new app with one click!
Important: Because Viberunner gives unrestricted system access to your apps (it's the only way to build a powerful utility), you should never run untrusted code. See the Security section at the end of this readme.
Viberunner is free during public alpha, but may be monetized in the future to support its development.
- π± Chrome-style Tabbed Interface: Open multiple files and apps simultaneously with smooth tab switching
- π¨ Enhanced File Matching: Go beyond MIME types with filename patterns, content analysis, and priority-based selection
- βοΈ User Preferences System: Runners can store and retrieve user preferences with a powerful API
- π§ Direct Node.js Access: Runners have full access to Node.js APIs for maximum flexibility and performance
- π Modern Dark UI: Beautiful glassmorphism interface with smooth animations and native tab styling
- βοΈ React-Based Runners: Build components using React 18+ with TypeScript support
- π Hot Reloading: Instant runner updates during development
- π File Interaction: Read, analyze, and even modify files with user permission
- π― Priority System: Ensure the most specific runner wins for each file
- π Standalone Runners: Create utility apps that don't require file input
- π Runner Isolation: Perfect CSS and JavaScript isolation between tabs
- π Custom Runner Icons: Personalize your apps with custom icons
- π Flexible Launch Modes: Run apps as tabs, dock windows, or menu bar items
- Quick Start
- Runner Architecture
- Tabbed Interface
- User Preferences System
- [Runner API - Direct Node.js Access](#-app -api---direct-nodejs-access)
- Enhanced Matching System
- Creating Your First Runner
- Configuration Reference
- Custom Runner Icons
- Launch Modes
- Component Development
- File Analysis & APIs
- Advanced Examples
- Build & Distribution
- Best Practices
- Troubleshooting
- Runner Cleanup API
Viberunner supports custom icons for your runners, making them easily recognizable in the launcher and tabs.
- Add an icon file to your runner directory (PNG, SVG, JPG, etc.)
- Reference it in package.json using the
iconfield with a relative path
{
"name": "JSON Formatter",
"description": "Pretty print and validate JSON files",
"icon": "icon.png",
"matchers": [
{
"type": "mimetype",
"mimetype": "application/json",
"priority": 60
}
]
}- Recommended size: 32x32 pixels (will be scaled automatically)
- Supported formats: PNG, SVG, JPG, GIF, WebP
- File location: Must be within your runner directory
- Path: Relative to your runner root (e.g.,
"icon.png","assets/icon.svg")
Custom icons are displayed in:
- Launcher: Standalone app cards and file app listings
- Tabs: Tab icons for opened runners
- Runner Selection: When multiple apps match a file
If no custom icon is provided, Viberunner uses the Viberunner logo as the default icon for all apps. This provides a consistent and professional appearance while maintaining visual distinction through custom icons when available.
Viberunner supports three different launch modes that control how and where your runners appear when launched. This gives you flexibility in creating different types of applications that integrate with macOS in various ways.
- Behavior: Runner launches as a new tab in the main Viberunner window
- Use Case: Most common mode for file processors, viewers, and general utilities
- Integration: Fully integrated with Viberunner's tabbed interface
- Examples: JSON formatter, image viewer, text editor
- Behavior: Launches as a separate window with its own dock icon
- Use Case: Independent applications that need to run alongside other apps
- Integration: Separate from main Viberunner window, appears in dock
- Examples: System monitor, standalone utilities, productivity apps
- Behavior: Launches as an icon in the macOS system menu bar
- Use Case: Background utilities, system monitors, quick-access tools
- Integration: Minimal footprint, always accessible from menu bar
- Examples: Clipboard manager, system stats, quick notes
Set the launch mode in your runner's package.json:
{
"name": "My Runner",
"description": "Example runner",
"viberunner": {
"launchMode": "newTab",
"matchers": [
{
"type": "mimetype",
"mimetype": "application/json",
"priority": 60
}
]
}
}{
"name": "JSON Formatter",
"description": "Format and validate JSON files",
"viberunner": {
"launchMode": "newTab",
"matchers": [
{
"type": "mimetype",
"mimetype": "application/json",
"priority": 60
}
]
}
}{
"name": "System Monitor",
"description": "Real-time system performance monitoring",
"viberunner": {
"launchMode": "macDock",
"standalone": true
}
}{
"name": "Clipboard Manager",
"description": "Track and manage clipboard history",
"viberunner": {
"launchMode": "macMenuBar",
"standalone": true
}
}- New Tab: Use for file-based runners and utilities that benefit from tabbed organization
- Mac Dock: Use for standalone applications that need persistent access and window management
- Mac Menu Bar: Use for lightweight utilities that run in the background and need quick access
- Node.js 18+
- npm or yarn
- Viberunner app installed
# 1. Create runner directory
mkdir my-awesome-runner
cd my-awesome-runner
# 2. Initialize npm project
npm init -y
# 3. Install dependencies
npm install react react-dom
npm install -D @types/react @types/react-dom @vitejs/plugin-react typescript vite
# 4. Create required files
touch package.json tsconfig.json vite.config.ts src/App.tsxmy-awesome-runner/
βββ package.json # NPM dependencies
βββ tsconfig.json # TypeScript config
βββ icon.png # Custom icon (32x32 recommended)
βββ vite.config.ts # Build configuration
βββ src/
β βββ App.tsx # Main React component
βββ dist/
βββ bundle.iife.js # Built output (generated)
- File Drop: User drops a file into Viberunner
- Analysis: Viberunner analyzes the file (path, content, metadata)
- Matching: Enhanced matcher system finds compatible runners
- Selection: Best match or user selection if multiple options
- Loading: Runner component is dynamically loaded
- Rendering: React component renders with file data
File Drop β File Analysis β Matcher Evaluation β Runner Selection β Component Loading β Rendering
Viberunner features a Chrome-style tabbed interface that allows users to work with multiple files and applications simultaneously. Each tab maintains its own state and can switch between different apps seamlessly.
- File Drop: Dropping a file opens it in a new tab (or transforms the current "New Tab")
- Standalone Runners: Launching standalone apps creates dedicated tabs
- New Tab Button: Click the "+" button to create a new empty tab
- Click to Switch: Click any tab to switch to it instantly
- Visual Feedback: Active tab is highlighted with proper visual hierarchy
- Content Isolation: Each tab maintains completely isolated content and state
- Close Button: Hover over tabs to reveal the close button (β)
- Auto-cleanup: Closing tabs automatically cleans up resources and React components
- Last Tab Protection: New tab is automatically created if you close the last tab
-
New Tab π
- Clean interface for launching apps or dropping files
- Shows directory controls and available apps
- Transforms into a file/app tab when content is loaded
-
File Tabs π
- Display file-based runners
- Show filename and app name in tab title
- Include close button for easy management
-
Standalone Runner Tabs β‘
- Run utility applications that don't require file input
- Show app name and icon in tab title
- Independent of file operations
- Chrome-style Design: Native-looking tabs with proper shadows and gradients
- Custom Icons: Each tab displays the app's custom icon or Viberunner logo
- Smooth Animations: Fluid transitions when switching between tabs
- Hover Effects: Interactive feedback for better user experience
- Active State: Clear visual distinction for the currently active tab
Cleanup:
function MyRunner({ tabId, runnerId, fileInput }) {
// Each tab has a unique tabId for cleanup registration
React.useEffect(() => {
const cleanup = () => {
// Cleanup timers, listeners, etc.
}
// Register cleanup function for this tab
window.registerCleanup(tabId, cleanup)
return cleanup
}, [tabId])
return <div>Your app content</div>
}Viberunner provides a comprehensive user preferences system that allows apps to store and retrieve user settings persistently. Preferences are stored in each app's package.json file and survive between sessions.
Preferences are automatically stored in your runner's configuration entry in package.json:
{
"viberunner": {
"name": "My Awesome Runner",
"description": "A great runner",
"mimetypes": ["application/json"],
"userPreferences": {
"theme": "dark",
"fontSize": 14,
"autoSave": true,
"recentFiles": ["file1.json", "file2.json"],
"customSettings": {
"nested": "values"
}
}
}
}Access preferences through the global api object:
// Get all preferences for your app
const prefs = api.getRunnerPreferences(runnerId)
// Get a specific preference with default fallback
const theme = api.getRunnerPreference(runnerId, "theme", "light")
// Set a single preference
api.updateRunnerPreference(runnerId, "theme", "dark")
// Replace all preferences
api.setRunnerPreferences(runnerId, { theme: "dark", fontSize: 16 })
// Remove a preference
api.removeRunnerPreference(runnerId, "oldSetting")For easier usage, create a preferences helper with your app ID:
function MyRunner({ runnerId }) {
// Create preferences helper
const prefs = window.createPreferencesHelper(runnerId)
// Type-safe getters with defaults
const theme = prefs.getString("theme", "light")
const fontSize = prefs.getNumber("fontSize", 12)
const autoSave = prefs.getBoolean("autoSave", false)
const settings = prefs.getObject("settings", {})
// Simple setters
const handleThemeChange = (newTheme) => {
prefs.set("theme", newTheme)
}
return (
<div className={`app-${theme}`}>
<button onClick={() => handleThemeChange("dark")}>Dark Theme</button>
</div>
)
}const prefs = window.createPreferencesHelper(runnerId)
// String with default
const theme = prefs.getString("theme", "light")
// Number with default
const fontSize = prefs.getNumber("fontSize", 12)
// Boolean with default
const autoSave = prefs.getBoolean("autoSave", false)
// Object with default
const config = prefs.getObject("config", {})
// Array with default
const recentFiles = prefs.getArray("recentFiles", [])// Add item to array
prefs.pushToArray("recentFiles", "/path/to/new/file.json")
// Remove item from array
prefs.removeFromArray("recentFiles", "/path/to/old/file.json")
// Get array safely
const files = prefs.getArray("recentFiles", [])// Get all preferences
const allPrefs = prefs.getAll()
// Replace all preferences
prefs.setAll({
theme: "dark",
fontSize: 14,
autoSave: true,
})
// Clear all preferences
prefs.clear()function ThemedRunner({ runnerId }) {
const prefs = window.createPreferencesHelper(runnerId)
const [theme, setTheme] = React.useState(prefs.getString("theme", "light"))
const toggleTheme = () => {
const newTheme = theme === "light" ? "dark" : "light"
setTheme(newTheme)
prefs.set("theme", newTheme)
}
return (
<div className={`app-${theme}`}>
<button onClick={toggleTheme}>
Switch to {theme === "light" ? "dark" : "light"} theme
</button>
</div>
)
}- Use Type-Safe Getters: Always use the appropriate getter method (
getString,getNumber, etc.) - Provide Defaults: Always specify sensible default values
- Group Related Settings: Use objects for complex configuration
- Validate Input: Ensure preference values are valid before storing
- Performance: Cache frequently accessed preferences in component state
- Error Handling: The API includes built-in error handling and graceful fallbacks
The preferences system includes robust error handling:
- File Access Errors: Returns empty object/default values if file can't be read
- JSON Parse Errors: Gracefully handles corrupted preference data
- Write Errors: Returns
falsefor failed operations,truefor success - Type Safety: Helper methods ensure correct data types are returned
Runners have direct access to Node.js APIs instead of receiving pre-processed file content. This provides better performance, flexibility, and avoids data corruption issues.
Your React component receives RunnerProps for component props:
export interface FileInput {
path: string
mimetype: string
}
export interface RunnerProps {
dataDirectory: string
fileInput?: FileInput
}
### Available APIs
Runners have access to direct Node.js modules:
```javascript
// Path utilities
const dirname = path.dirname(filePath)
const basename = path.basename(filePath)
const extname = path.extname(filePath)
// MIME type detection
const mimeType = mime.lookup(filePath)
// Direct Node.js access
const fs = fs
const path = path
const customModule = require("some-module")- No data corruption - Direct file access eliminates base64 encoding issues
- Better performance - Only read what you need, when you need it
- More flexibility - Access entire filesystem, not just the dropped file
- Simpler code - No need to handle multiple encoding formats
- Enhanced capabilities - Can read config files, create temp files, etc.
// Read image directly when needed, no corruption risk
const imageBuffer = fs.readFileSync(fileInput.path)
const imageData = `data:${fileInput.mimetype};base64,${imageBuffer.toString(
"base64"
)}`The new matching system supports multiple criteria types with priority-based selection:
{
"type": "filename",
"pattern": "package.json",
"priority": 100
}- Exact match:
"package.json" - Glob patterns:
"*.config.js","test-*.json" - Wildcards:
*(any characters),?(single character)
{
"type": "filename-contains",
"substring": "kanban",
"priority": 80
}- Substring matching: Matches if filename contains the specified text
- Case insensitive:
"kanban"matches"Kanban","KANBAN", etc. - Optional extension filter: Add
"extension"to also check file extension - Examples:
"kanban"matches:foo-kanban.txt,my-kanban-board.json,kanban-data.csv"config"matches:app.config.js,webpack-config.dev.js,my-config.yaml
{
"type": "filename-contains",
"substring": "kanban",
"extension": "txt",
"priority": 85
}- With extension: Only matches files containing "kanban" AND having
.txtextension - Extension formats: Use
"txt"or".txt"(both work the same) - Examples:
- β
Matches:
team-kanban.txt,my-kanban-board.txt - β Doesn't match:
team-kanban.json,kanban-data.csv
- β
Matches:
{
"type": "path-pattern",
"pattern": "**/src/**/*.tsx",
"priority": 80
}- Deep matching:
**/package.json(any depth) - Directory patterns:
src/**/*.js - Absolute paths:
/Users/*/Desktop/*.md
{
"type": "content-json",
"requiredProperties": ["dependencies", "scripts"],
"priority": 90
}- Property detection: Must contain specific JSON properties
- Nested properties:
"dependencies.react" - Array elements:
"scripts[0]"
{
"type": "content-regex",
"regex": "^#!/usr/bin/env node",
"priority": 70
}- Shebang detection: Find Node.js scripts
- Content patterns: API keys, specific formats
- Multi-line matching: Use appropriate regex flags
{
"type": "file-size",
"minSize": 1048576,
"maxSize": 104857600,
"priority": 60
}- Size in bytes:
minSize,maxSize - Large file handling: Different runners for different sizes
- Memory optimization: Skip content reading for huge files
{
"type": "combined",
"operator": "AND",
"priority": 95,
"conditions": [
{
"type": "mimetype",
"mimetype": "application/json",
"priority": 50
},
{
"type": "filename",
"pattern": "*.config.*",
"priority": 50
}
]
}- Logical operators:
"AND","OR" - Complex conditions: Combine any matcher types
- Nested logic: Conditions can contain other combined matchers
{
"type": "mimetype",
"mimetype": "image/png",
"priority": 50
}- Backward compatibility: Still works with existing runners
- Standard MIME types:
image/*,text/*,application/* - Lower priority: Enhanced matchers take precedence
- Higher numbers = higher priority
- Most specific match wins
- Suggested ranges:
- 90-100: Exact filename matches (
package.json) - 70-89: Content-based matching with filename patterns
- 50-69: MIME type or general content patterns
- 30-49: Broad file size or extension patterns
- 10-29: Fallback or experimental matchers
- 90-100: Exact filename matches (
Let's create a JSON Formatter runner step by step:
{
"name": "JSON Formatter",
"description": "Pretty print and validate JSON files with syntax highlighting",
"version": "1.0.0",
"author": "Your Name",
"viberunner": {
"launchMode": "newTab",
"icon": "icon.png",
"matchers": [
{
"type": "mimetype",
"mimetype": "application/json",
"priority": 60
},
{
"type": "filename",
"pattern": "*.json",
"priority": 70
},
{
"type": "content-json",
"requiredProperties": [],
"priority": 50
}
]
}
}import { defineConfig } from "vite"
import react from "@vitejs/plugin-react"
export default defineConfig({
plugins: [
react({
jsxRuntime: "classic", // Important: Use classic JSX runtime
}),
],
build: {
lib: {
entry: "src/App.tsx",
name: "JsonFormatter",
fileName: "bundle",
formats: ["iife"],
},
rollupOptions: {
external: ["react", "react-dom"],
output: {
globals: {
react: "React",
"react-dom": "ReactDOM",
},
},
},
},
}){
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react", // Important: Use classic JSX
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}import React from "react"
const fs = require("fs")
interface FileData {
path: string
mimetype: string
}
interface JsonFormatterProps {
fileData: FileData
}
const JsonFormatter: React.FC<JsonFormatterProps> = ({ fileData }) => {
const [jsonData, setJsonData] = React.useState<any>(null)
const [error, setError] = React.useState<string | null>(null)
const [formatted, setFormatted] = React.useState<string>("")
React.useEffect(() => {
try {
// Try to parse the JSON content
let content = fs.readFileSync(filePath)
// Handle base64 encoded content
if (
fileData.mimetype === "application/json" &&
!content.startsWith("{")
) {
content = atob(content)
}
const parsed = JSON.parse(content)
setJsonData(parsed)
setFormatted(JSON.stringify(parsed, null, 2))
setError(null)
} catch (err) {
setError(
`Invalid JSON: ${err instanceof Error ? err.message : "Unknown error"}`
)
}
}, [fileData])
const handleSave = async () => {
if (!formatted || error) return
try {
// Save the formatted JSON back to the original file
fs.writeFileSync(fileData.path, formatted, "utf8")
alert(`JSON saved successfully!`)
} catch (err) {
alert(`Error saving file: ${err}`)
}
}
if (error) {
return (
<div style={{ padding: "20px", color: "#ef4444" }}>
<h3>β JSON Parse Error</h3>
<p>{error}</p>
<pre
style={{
background: "#1e1e1e",
padding: "10px",
borderRadius: "4px",
fontSize: "12px",
overflow: "auto",
maxHeight: "200px",
}}
>
{fileData.content.slice(0, 1000)}...
</pre>
</div>
)
}
return (
<div style={{ padding: "20px", background: "#0a0a0a", color: "#fff" }}>
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginBottom: "20px",
}}
>
<h3>π JSON Formatter</h3>
<button
onClick={handleSave}
style={{
background: "#10b981",
color: "white",
border: "none",
borderRadius: "8px",
padding: "8px 16px",
cursor: "pointer",
}}
>
πΎ Save Formatted
</button>
</div>
<div
style={{
background: "#1e1e1e",
borderRadius: "8px",
padding: "15px",
border: "1px solid #333",
}}
>
<pre
style={{
margin: 0,
fontSize: "14px",
lineHeight: "1.5",
overflow: "auto",
maxHeight: "600px",
}}
>
{formatted}
</pre>
</div>
<div
style={{
marginTop: "15px",
fontSize: "12px",
color: "#888",
display: "flex",
gap: "20px",
}}
>
<span>π {fileData.analysis?.filename || "Unknown"}</span>
<span>
π{" "}
{fileData.analysis?.size
? `${(fileData.analysis.size / 1024).toFixed(1)} KB`
: "Unknown size"}
</span>
<span>π {Object.keys(jsonData || {}).length} properties</span>
</div>
</div>
)
}
// Export the component for Viberunner to load
export default JsonFormatter
// Global registration for IIFE bundle
if (typeof window !== "undefined" && (window as any).__RENDER_RUNNER__) {
;(window as any).__RENDER_RUNNER__(JsonFormatter)
}npm run buildThis creates dist/bundle.iife.js that Viberunner can load.
{
// Launch mode - controls how the runner appears (required)
"launchMode": "newTab | macDock | macMenuBar",
"matchers": [
{
"type": "mimetype | filename | filename-contains | path-pattern | content-json | content-regex | file-size | combined",
"priority": "number (required, 1-100)",
// Type-specific properties
"mimetype": "string (for mimetype)",
"pattern": "string (for filename/path-pattern)",
"substring": "string (for filename-contains)",
"extension": "string (optional, for filename-contains)",
"requiredProperties": ["string"] // (for content-json)
"regex": "string (for content-regex)",
"minSize": "number (for file-size)",
"maxSize": "number (for file-size)",
// Combined matcher properties
"operator": "AND | OR (for combined)",
"conditions": [
// Array of other matchers
]
}
],
// Legacy support (optional)
"mimetypes": ["string"],
// Standalone runners (no file input required)
"standalone": "boolean (optional)",
// Custom icon (relative path to icon file in app directory)
"icon": "string (optional)",
}Viberunner supports standalone runners that don't require file input. These can be utilities, dashboards, or any application that operates independently.
To create a standalone runner, set "standalone": true in your package.json viberunner entry:
{
"standalone": true
}const WeatherDashboard: React.FC<RunnerProps> = ({ fileData }) => {
// fileData will be null - this is a standalone utility
const [weather, setWeather] = useState(null)
useEffect(() => {
// Fetch weather data using Node.js APIs
const https = require("https")
// ... make API calls, read config files, etc.
}, [])
return (
<div style={{ padding: "20px" }}>
<h2>π€οΈ Weather Dashboard</h2>
{/* Your utility UI here */}
</div>
)
}
export default WeatherDashboardStandalone runners appear in the "Utilities" section of the sidebar with Launch buttons. They can:
- Access full Node.js APIs via
require() - Make HTTP requests to external services
- Read/write files and configurations
- Execute shell commands
- Create persistent data stores
- Build complex UI applications
This enables Viberunner to serve as a platform for any kind of utility or application, not just file runners!
Runners have complete access to Node.js modules via require():
// File system operations
const fs = require("fs")
const content = fs.readFileSync("/path/to/file", "utf8")
fs.writeFileSync("/path/to/output.txt", "Hello World")
// Path utilities
const path = require("path")
const filename = path.basename("/foo/bar/baz.txt") // 'baz.txt'
const dir = path.dirname("/foo/bar/baz.txt") // '/foo/bar'
// Operating system info
const os = require("os")
const homeDir = os.homedir()
const platform = os.platform()
// Child processes
const { spawn, exec } = require("child_process")
const result = exec("ls -la", (error, stdout, stderr) => {
console.log(stdout)
})
// Crypto operations
const crypto = require("crypto")
const hash = crypto.createHash("sha256").update("data").digest("hex")
// HTTP requests
const https = require("https")
// ... make API calls
// Any other Node.js module
const util = require("util")
const events = require("events")
// ... etcUse these CSS variables for consistent theming:
:root {
--bg-primary: #0a0a0a;
--bg-secondary: #1a1a1a;
--bg-card: #1e1e1e;
--text-primary: #ffffff;
--text-secondary: #b3b3b3;
--text-muted: #808080;
--accent-primary: #3b82f6;
--border-color: #333333;
}const MyRunner: React.FC<RunnerProps> = ({ fileData }) => {
// 1. Use React hooks for state management
const [data, setData] = useState(null)
const [loading, setLoading] = useState(true)
// 2. Handle file analysis in useEffect
useEffect(() => {
const analyzeFile = async () => {
try {
setLoading(true)
// Process fileData
const processed = await processFile(fileData)
setData(processed)
} catch (error) {
console.error("Error processing file:", error)
} finally {
setLoading(false)
}
}
analyzeFile()
}, [fileData])
// 3. Handle loading states
if (loading) {
return <div className="loading">Processing file...</div>
}
// 4. Handle error states
if (!data) {
return <div className="error">Failed to process file</div>
}
// 5. Return your visualization
return <div className="runner-container">{/* Your visualization here */}</div>
}{
"name": "Package Upgrader",
"description": "Check and upgrade npm dependencies",
"viberunner": {
"matchers": [
{
"type": "filename",
"pattern": "package.json",
"priority": 100
},
{
"type": "content-json",
"requiredProperties": ["dependencies"],
"priority": 80
}
]
}
}{
"name": "Config Analyzer",
"description": "Analyze configuration files across different formats",
"viberunner": {
"matchers": [
{
"type": "combined",
"operator": "OR",
"priority": 85,
"conditions": [
{
"type": "filename",
"pattern": "*.config.js",
"priority": 90
},
{
"type": "filename",
"pattern": "*.config.json",
"priority": 90
},
{
"type": "filename",
"pattern": ".env*",
"priority": 85
}
]
}
]
}
}{
"name": "Large File Analyzer",
"description": "Special handling for files larger than 100MB",
"viberunner": {
"matchers": [
{
"type": "file-size",
"minSize": 104857600,
"priority": 70
}
]
}
}{
"name": "Node.js Script Runner",
"description": "Detect and analyze Node.js scripts",
"viberunner": {
"matchers": [
{
"type": "content-regex",
"regex": "^#!/usr/bin/env node",
"priority": 95
},
{
"type": "combined",
"operator": "AND",
"priority": 80,
"conditions": [
{
"type": "filename",
"pattern": "*.js",
"priority": 50
},
{
"type": "content-regex",
"regex": "require\\(|import .* from",
"priority": 50
}
]
}
]
}
}{
"name": "Kanban Board Runner",
"description": "Visualize kanban boards from various file formats",
"viberunner": {
"matchers": [
{
"type": "filename-contains",
"substring": "kanban",
"extension": "txt",
"priority": 90
},
{
"type": "filename-contains",
"substring": "kanban",
"extension": "json",
"priority": 85
},
{
"type": "filename-contains",
"substring": "board",
"priority": 75
},
{
"type": "combined",
"operator": "AND",
"priority": 80,
"conditions": [
{
"type": "filename-contains",
"substring": "project",
"priority": 50
},
{
"type": "mimetype",
"mimetype": "application/json",
"priority": 50
}
]
}
]
}
}This will match files like:
team-kanban.txtβ (substring: "kanban", extension: ".txt", priority: 90)my-kanban-data.jsonβ (substring: "kanban", extension: ".json", priority: 85)project-board.csvβ (substring: "board", priority: 75)team-kanban.csvβ (contains "kanban" but wrong extension, doesn't contain "board")my-project-data.jsonβ (combined: contains "project" AND is JSON, priority: 80)random-file.txtβ (no matching substring)
# 1. Install dependencies
npm install
# 2. Development with hot reload
npm run dev
# 3. Build for production
npm run build
# 4. Test the built runner
# Copy to your runners directory and reload in ViberunnerYour build process must generate:
dist/bundle.iife.js- IIFE format bundle- The bundle must be self-contained
- External deps:
reactandreact-dom(provided by Viberunner)
- Be specific: Higher priority for more specific matches
- Use combinations: Combine filename + content for precision
- Fallback gracefully: Include broader matchers with lower priority
- Test edge cases: Empty files, binary files, huge files
- Lazy loading: Don't process everything upfront
- Memory conscious: Handle large files appropriately
- Async operations: Use async/await for file operations
- Error boundaries: Wrap components in error handling
- Loading states: Show progress for slow operations
- Error handling: Graceful degradation for corrupted files
- Consistent styling: Use the dark theme variables
- Responsive design: Work on different screen sizes
- Read-only by default: Don't modify files without permission
- Backup before save: Consider file backup strategies
- Validate input: Check file integrity before processing
- Limit scope: Only access files explicitly dropped
- TypeScript: Use strong typing for better development
- Modular code: Break complex runners into components
- Testing: Test with various file types and sizes
- Documentation: Comment complex logic
# Ensure React is available globally
npm install react react-dom
# Check vite.config.ts external configuration
external: ['react', 'react-dom']- Verify
dist/bundle.iife.jsexists - Check console for JavaScript errors
- Ensure JSX runtime is set to 'classic'
- Verify global export is working
- Check priority values (higher = more specific)
- Verify JSON syntax in package.json
- Test patterns with simple cases first
- Use console.log to debug file analysis
- File might be binary (check mimetype)
- File might be too large (>10MB limit)
- Check file permissions
- Verify encoding (UTF-8 expected)
// Add this to your component for debugging
const DebugInfo: React.FC<{ fileData: FileData }> = ({ fileData }) => (
<details style={{ marginTop: "20px", fontSize: "12px" }}>
<summary>π Debug Info</summary>
<pre style={{ background: "#1e1e1e", padding: "10px" }}>
{JSON.stringify(fileData, null, 2)}
</pre>
</details>
)- Check console logs: Both main process and renderer
- Verify file structure: Ensure all required files exist
- Test incrementally: Start simple, add complexity gradually
- Use debug runner: Create a simple debug runner first
// Register a cleanup callback for the current tab
registerCleanup(tabId, cleanupFunction)function MyRunner({ tabId }) {
const [interval, setInterval] = useState(null)
useEffect(() => {
// Start some process
const intervalId = setInterval(() => {
console.log("Processing...")
}, 1000)
setInterval(intervalId)
// Register cleanup callback
registerCleanup(tabId, () => {
console.log("Cleaning up interval")
clearInterval(intervalId)
})
// Component cleanup (React unmount)
return () => {
clearInterval(intervalId)
}
}, [tabId])
return <div>My Runner Content</div>
}Cleanup callbacks are automatically executed when:
- A tab is closed by the user
- The application is shutting down
- A tab is being replaced (rare edge cases)
- Always register cleanup: Even if you think your app doesn't need it
- Multiple callbacks: Register separate callbacks for different types of cleanup
- Error handling: Cleanup callbacks are wrapped in try-catch, but handle your own errors when possible
- Immediate cleanup: Also implement React's
useEffectcleanup for immediate component unmounting
Viberunner apps have full access to Node.js APIs, including the ability to execute system commands and detect external tools. However, robust execution requires careful error handling and proper command structure.
When checking for external tools or executing commands, prefer spawn over exec for better error handling and output control:
// β Less reliable approach
const { exec } = require("child_process")
exec("python3 --version", (error, stdout, stderr) => {
// Can fail due to shell quirks, output buffering, etc.
})
// β
More reliable approach
const { spawn } = require("child_process")
const pythonCheck = spawn("python3", ["--version"], { shell: true })
let output = ""
let error = ""
pythonCheck.stdout?.on("data", (data) => {
output += data.toString()
})
pythonCheck.stderr?.on("data", (data) => {
error += data.toString()
})
pythonCheck.on("close", (code) => {
const isAvailable =
code === 0 && (output.includes("Python 3") || error.includes("Python 3"))
console.log("Python3 available:", isAvailable)
})
pythonCheck.on("error", (err) => {
console.log("Python3 check failed:", err)
// Handle gracefully
})For complex tool detection (e.g., checking both the tool and its dependencies), use a multi-stage approach:
const checkToolAvailability = (childProcess) => {
const { spawn } = childProcess
// Stage 1: Check if base tool exists
const toolCheck = spawn("python3", ["--version"], { shell: true })
toolCheck.on("close", (code) => {
if (code === 0) {
console.log("Python3 is available, checking dependencies...")
checkToolDependencies(childProcess)
} else {
console.log("Python3 not available, using fallback approach")
useFallbackApproach()
}
})
toolCheck.on("error", (err) => {
console.log("Tool check failed:", err)
useFallbackApproach()
})
}
const checkToolDependencies = (childProcess) => {
const { spawn } = childProcess
// Stage 2: Check if required modules/dependencies exist
const depCheck = spawn(
"python3",
["-c", 'import some_required_module; print("available")'],
{ shell: true }
)
let output = ""
depCheck.stdout?.on("data", (data) => {
output += data.toString()
})
depCheck.on("close", (code) => {
if (code === 0 && output.includes("available")) {
console.log("All dependencies available")
enableFullFunctionality()
} else {
console.log("Dependencies missing, using limited functionality")
useLimitedFunctionality()
}
})
}Handle platform differences gracefully:
const getPlatformSpecificCommand = () => {
const os = require("os")
const platform = os.platform()
switch (platform) {
case "darwin": // macOS
return ["python3", "--version"]
case "win32": // Windows
return ["python", "--version"] // Often just 'python' on Windows
case "linux": // Linux
return ["python3", "--version"]
default:
throw new Error(`Unsupported platform: ${platform}`)
}
}
const checkCrossPlatform = () => {
try {
const [command, ...args] = getPlatformSpecificCommand()
const check = spawn(command, args, { shell: true })
// ... handle as above
} catch (error) {
console.error("Platform not supported:", error)
// Use most basic fallback
}
}- Always handle both
errorandcloseevents for spawned processes - Provide clear console logging for debugging
- Implement progressive fallbacks rather than hard failures
- Cache detection results to avoid repeated expensive checks
- Handle edge cases like command not found, permission denied, etc.
const robustToolCheck = (toolName, args = ["--version"]) => {
return new Promise((resolve) => {
const { spawn } = require("child_process")
const process = spawn(toolName, args, { shell: true })
let output = ""
let errorOutput = ""
process.stdout?.on("data", (data) => {
output += data.toString()
})
process.stderr?.on("data", (data) => {
errorOutput += data.toString()
})
process.on("close", (code) => {
resolve({
available: code === 0,
output,
error: errorOutput,
exitCode: code,
})
})
process.on("error", (err) => {
resolve({
available: false,
output: "",
error: err.message,
exitCode: -1,
})
})
// Timeout protection
setTimeout(() => {
process.kill()
resolve({
available: false,
output: "",
error: "Command timeout",
exitCode: -1,
})
}, 5000) // 5 second timeout
})
}
// Usage
const checkTool = async () => {
const result = await robustToolCheck("python3")
if (result.available) {
console.log("Tool available:", result.output)
} else {
console.log("Tool not available:", result.error)
}
}This approach ensures your Viberunner apps can reliably detect and work with external tools while providing graceful fallbacks when tools aren't available.
You now have everything needed to create powerful, sophisticated runners for Viberunner! The enhanced matching system allows for precise file targeting, while the React-based architecture provides a familiar development experience.
Start with simple runners and gradually add complexity. The priority-based matching ensures your runners activate exactly when they should, creating a seamless user experience.
Happy visualizing! π