Claude-skill-registry electron
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/electron-nathanielcrowell12-s-photo-vault" ~/.claude/skills/majiayu000-claude-skill-registry-electron && rm -rf "$T"
manifest:
skills/data/electron-nathanielcrowell12-s-photo-vault/SKILL.mdsource content
⚠️ MANDATORY WORKFLOW - DO NOT SKIP
When this skill activates, you MUST follow the expert workflow before writing any code:
-
Spawn Domain Expert using the Task tool with this prompt:
Read the expert prompt at: C:\Users\natha\Stone-Fence-Brain\VENTURES\PhotoVault\claude\experts\electron-expert.md Then research the codebase and write an implementation plan to: docs/claude/plans/electron-[task-name]-plan.md Task: [describe the user's request] -
Spawn QA Critic after expert returns, using Task tool:
Read the QA critic prompt at: C:\Users\natha\Stone-Fence-Brain\VENTURES\PhotoVault\claude\experts\qa-critic-expert.md Review the plan at: docs/claude/plans/electron-[task-name]-plan.md Write critique to: docs/claude/plans/electron-[task-name]-critique.md -
Present BOTH plan and critique to user - wait for approval before implementing
DO NOT read files and start coding. DO NOT rationalize that "this is simple." Follow the workflow.
Electron Desktop App Integration
Core Principles
Main Process vs Renderer Process
The main process runs Node.js. The renderer process runs web content. Never give the renderer direct Node access.
// main.ts - Main process ipcMain.handle('read-file', async (_, path) => { return fs.readFile(path) }) // preload.ts - Bridge contextBridge.exposeInMainWorld('api', { readFile: (path) => ipcRenderer.invoke('read-file', path) }) // renderer.js - Web content const content = await window.api.readFile('/path/to/file')
Never Enable Node Integration
It's a massive security hole. Use contextBridge instead.
// ✅ CORRECT new BrowserWindow({ webPreferences: { nodeIntegration: false, contextIsolation: true, preload: path.join(__dirname, 'preload.js'), }, })
Stream Large Files
Never load entire files into memory. Stream them chunk by chunk.
// ❌ BAD: 4GB file = 4GB RAM usage const buffer = await fs.readFile(largePath) // ✅ GOOD: Stream in chunks const stream = fs.createReadStream(largePath, { highWaterMark: 6 * 1024 * 1024 }) for await (const chunk of stream) { await uploadChunk(chunk) }
Anti-Patterns
Enabling nodeIntegration
// WRONG: Any website loaded can access filesystem new BrowserWindow({ webPreferences: { nodeIntegration: true, // NEVER DO THIS }, })
Exposing sensitive functions to renderer
// WRONG: Renderer can delete anything contextBridge.exposeInMainWorld('api', { deleteFile: (path) => fs.unlink(path), }) // RIGHT: Validate and restrict contextBridge.exposeInMainWorld('api', { deleteUploadedFile: (fileId) => { const safePath = validateAndResolvePath(fileId) return fs.unlink(safePath) }, })
Sending large data through IPC
// WRONG: IPC has limits, blocks main process mainWindow.webContents.send('file-data', fileBuffer) // 500MB buffer! // RIGHT: Send reference, stream separately mainWindow.webContents.send('file-ready', { path: largeFile, size })
Not cleaning up resources
// WRONG: File handles leak const stream = fs.createReadStream(path) stream.pipe(uploadStream) // RIGHT: Proper cleanup const stream = fs.createReadStream(path) try { await pipeline(stream, uploadStream) } finally { stream.destroy() }
Preload Script (Secure Bridge)
// src/preload.ts import { contextBridge, ipcRenderer } from 'electron' contextBridge.exposeInMainWorld('photovault', { selectFiles: () => ipcRenderer.invoke('select-files'), selectFolder: () => ipcRenderer.invoke('select-folder'), startUpload: (galleryId: string, files: string[]) => ipcRenderer.invoke('start-upload', galleryId, files), cancelUpload: () => ipcRenderer.invoke('cancel-upload'), authenticate: () => ipcRenderer.invoke('authenticate'), getAuthState: () => ipcRenderer.invoke('get-auth-state'), logout: () => ipcRenderer.invoke('logout'), onUploadProgress: (callback: (progress: UploadProgress) => void) => { const listener = (_: any, progress: UploadProgress) => callback(progress) ipcRenderer.on('upload-progress', listener) return () => ipcRenderer.removeListener('upload-progress', listener) }, })
Chunked Upload Manager
// src/upload-manager.ts import { createReadStream, statSync } from 'fs' import { EventEmitter } from 'events' const CHUNK_SIZE = 6 * 1024 * 1024 // 6MB chunks const MAX_RETRIES = 3 export class UploadManager extends EventEmitter { private cancelled = false async uploadFile(filePath: string, galleryId: string, hubUrl: string, token: string) { const stats = statSync(filePath) const totalChunks = Math.ceil(stats.size / CHUNK_SIZE) const stream = createReadStream(filePath, { highWaterMark: CHUNK_SIZE }) let chunkIndex = 0 for await (const chunk of stream) { if (this.cancelled) { stream.destroy() throw new Error('Upload cancelled') } await this.uploadChunkWithRetry(chunk, chunkIndex, totalChunks, galleryId, hubUrl, token) chunkIndex++ this.emit('progress', { file: filePath, progress: Math.round((chunkIndex / totalChunks) * 100), }) } } cancel() { this.cancelled = true } }
Protocol Handler for Deep Links
// In main.ts function handleDeepLink(url: URL) { // photovault://auth?token=X&userId=Y if (url.pathname === '//auth' || url.pathname === '/auth') { const token = url.searchParams.get('token') const userId = url.searchParams.get('userId') if (token && userId) { authState = { token, userId, authenticated: true } mainWindow?.webContents.send('auth-complete', authState) mainWindow?.show() } } } // Register protocol app.setAsDefaultProtocolClient('photovault')
PhotoVault Desktop Context
Hub API Endpoints
| Endpoint | Purpose |
|---|---|
| Create gallery, get upload path |
| Upload individual chunk |
| Trigger processing after upload |
| OAuth callback |
Auth Flow
- User clicks "Login" in desktop app
- App opens browser to
{hubUrl}/auth/desktop-callback?desktop=true - User authenticates in browser
- Hub redirects to
photovault://auth?token=X&userId=Y - Protocol handler captures, stores auth, notifies renderer
Build Commands
npm run dev # Development npm run build # Build for current platform npm run build:all # Build for all platforms
Debugging Checklist
- Is nodeIntegration disabled?
- Is contextIsolation enabled?
- Are large files being streamed, not loaded into memory?
- Are IPC messages small (references, not data)?
- Is cleanup happening in finally blocks?
- Is the protocol handler registered?